8강 실습 중심 ⏱ 약 40분

 

0. 학습 목표

→ 아이디·비밀번호로 로그인을 확인하고, 비밀번호를 안전하게 다루는 방법을 정리합니다.

더보기

0.1 이번 글에서 다룰 내용

 

이번 글은 실습 중심 강의이자 시리즈의 마지막입니다.

 

아이디와 비밀번호가 일치하는 사용자가 있는지 확인하는 로그인 기능을 만듭니다.

\새로 보이지만, 로그인도 결국 WHERE 조건으로 한 건을 찾는 조회입니다.

 

그리고 지금까지 학습 편의를 위해 평문으로 다뤄 온 비밀번호를,

실무에서는 왜 그렇게 저장하면 안 되는지와 어떻게 바꾸는지를 정리하며 시리즈를 마칩니다.

 

구분 내용
이해할 것 로그인이 WHERE 조회라는 점, fetchone의 역할, 평문 저장의 위험
만들 것 로그인 입력칸·버튼과 check_login 메서드, 성공·실패 안내

 

 

 

 

0.2 이번 강의 실습 내용

 

이번 강의에서 직접 해보는 것은 다음과 같습니다.

  1. 로그인 아이디·비밀번호 입력칸과 버튼을 추가합니다.
  2. check_login 메서드에서 WHERE ... AND ...로 일치 여부를 확인합니다.
  3. 로그인 성공과 실패를 각각 확인합니다.
  4. 비밀번호를 안전하게 저장하는 방식을 개념으로 정리합니다.

 

1. 무엇을 추가하는가

→ 로그인 입력칸·버튼을 추가하고, 흩어져 있던 입력칸을 기능별 그룹으로 묶어 정리합니다.

더보기

로그인에 쓸 아이디·비밀번호 입력칸을 새로 만듭니다.

 

5강의 사용자 추가용 입력칸과 헷갈리지 않도록

login_username_edit·login_password_edit라는 별도 이름을 씁니다.

 

그동안 입력칸들이 라벨 없이 세로로만 쌓여 어떤 칸이 무엇인지 알아보기 어려웠으므로,

이번 마지막 강의에서는 각 입력칸에 setPlaceholderText로 안내문을 붙이고,

기능별로 QGroupBox(제목 있는 사각 박스)로 묶어 화면을 정리합니다.

[화면 캡처: 기능별 그룹 박스와 라벨·안내문이 적용되어 각 입력칸이 구분되는 창]

 

메서드 로직은 그대로이고 __init__의 배치 부분만 바뀝니다.

이를 위해 QGroupBox를 새로 import 합니다. 실제 diff 코드를 보기 전에, 이번에 손대는 자리를 먼저 지도로 확인합니다.

class MySqlDemoWindow(QWidget):
    def __init__(self):
        ... 접속 정보 입력 필드 (2강) ...
        ... 접속 버튼 3개 (2·3·4강) ...
        ... 사용자 추가 입력칸·버튼 (5강) ...
        ... 검색줄·표·조회 버튼 (6·7강) ...
        ... 로그인 입력칸·버튼·라벨 + 그룹 구성   ← ① login_ 위젯 만들고 QGroupBox로 묶기
        ... main_layout 배치                    ← ② 기능별 그룹 4개를 순서대로 넣기
        ... 시그널 연결                          ← ③ clicked.connect 연결

    def connect_to_server_or_db(self):   # 2강 메서드 (그대로)
        ...
    def create_database(self):           # 3강 메서드 (그대로)
        ...
    def write_log_with_qt(self, message):   # 4강 메서드 (그대로)
        ...
    def create_users_table(self):        # 4강 메서드 (그대로)
        ...
    def insert_user(self):               # 5강 메서드 (그대로)
        ...
    def load_users(self):                # 6강 메서드 (그대로)
        ...
    def search_users(self):              # 7강 메서드 (그대로)
        ...
    # ④ check_login 메서드를 여기(클래스 안)에 새로 추가

 

①~③은 __init__ 안에서 동시에 바뀌는 자리이고,

④는 클래스 안에 새로 추가되는 메서드입니다.

 

아래는 ①~③의 실제 코드입니다.

②에서는 기존에 위젯을 하나씩 addWidget하던 방식 대신,

완성본에서 기능별로 묶은 그룹 박스 4개를 순서대로 넣습니다.

 

[화면 캡처: 로그인 그룹이 표 아래에 추가되어 아이디·비밀번호 칸이 라벨과 함께 보이는 창]

 

        # ── ① 로그인 입력칸·버튼·라벨 생성 + 그룹 구성 (사용자 추가용 입력칸과 별개) ──
        self.login_username_edit = QLineEdit()                            # 추가
        self.login_username_edit.setPlaceholderText("로그인 아이디")        # 추가 (빈 칸 구분용 안내문)
        self.login_password_edit = QLineEdit()                            # 추가
        self.login_password_edit.setEchoMode(QLineEdit.Password)          # 추가
        self.login_password_edit.setPlaceholderText("로그인 비밀번호")      # 추가

        self.btn_login = QPushButton("로그인 확인")                         # 추가
        self.login_result_label = QLabel("로그인 결과가 여기에 표시됩니다.")   # 추가
        self.login_result_label.setWordWrap(True)                         # 추가 (긴 메시지 줄바꿈)

        login_form = QFormLayout()                                        # 추가 (라벨 : 입력칸 좌우 배치)
        login_form.addRow("아이디", self.login_username_edit)              # 추가
        login_form.addRow("비밀번호", self.login_password_edit)            # 추가

        login_box = QVBoxLayout()                                         # 추가
        login_box.addLayout(login_form)                                   # 추가
        login_box.addWidget(self.btn_login)                               # 추가
        login_box.addWidget(self.login_result_label)                      # 추가

        login_group = QGroupBox("4. 로그인 확인")                           # 추가 (제목 있는 박스로 구획 분리)
        login_group.setLayout(login_box)                                  # 추가

        # ── ② main_layout 배치 부분 (완성본은 기능별 그룹 4개로 정리) ──
        main_layout.addWidget(conn_group)       # 접속 정보 그룹 (2·3·4강)
        main_layout.addWidget(insert_group)     # 사용자 추가 그룹 (5강)
        main_layout.addWidget(query_group)      # 조회·검색 그룹 (6·7강)
        main_layout.addWidget(login_group)      # 추가 (조회·검색 그룹 바로 아래)

        # ── ③ 시그널 연결 부분 ──
        self.btn_search.clicked.connect(self.search_users)   # (7강)
        self.btn_login.clicked.connect(self.check_login)     # 추가

 

로그인 결과를 공용 상태 라벨이 아니라 전용 라벨(login_result_label)에 표시해,

다른 작업 메시지와 섞이지 않게 했습니다.

 

각 그룹 박스의 제목과 QFormLayout의 라벨 덕분에, 이제 어떤 칸이 어떤 기능의 입력인지 화면에서 바로 구분됩니다.

 

💡 팁: 이번 UI 정리는 화면 배치만 바꿀 뿐 메서드 로직은 전혀 건드리지 않습니다. 위젯 이름(host_edit, username_edit, login_username_edit 등)이 그대로라, 5~7강에서 작성한 메서드는 수정 없이 그대로 동작합니다.

 

2. 로그인 판정 이해하기

→ 로그인이 WHERE 조회라는 점과, fetchone으로 판정하는 방식을 정리합니다.

더보기

2.1 로그인도 결국 조회다

 

로그인은 특별한 기능처럼 보이지만, 본질은 "입력한 아이디·비밀번호와 모두 일치하는 사용자가 있는가"를 묻는 조회입니다. 7강의 WHERE에 조건을 하나 더해 두 값이 모두 맞는 행을 찾고, 그런 행이 있으면 로그인 성공으로 판정합니다.

SELECT id, username
FROM users
WHERE username = %s AND password = %s   -- 두 조건이 모두 맞아야
LIMIT 1                                 -- 한 건만 확인하면 충분

 

AND로 두 조건을 묶어 아이디와 비밀번호가 모두 일치해야 결과가 나오게 합니다.

LIMIT 1은 일치하는 행이 있는지만 확인하면 되므로 한 건만 가져오라는 뜻입니다.

 

 

 

 

2.2 fetchall이 아니라 fetchone

 

6~7강의 조회·검색은 여러 행이 필요해 fetchall()로 전부 가져왔습니다.

로그인은 일치하는 행이 있는지 없는지만 알면 되므로, 한 건만 가져오는 fetchone()을 씁니다.

 

메서드 가져오는 것 쓰는 곳
fetchall() 조건에 맞는 모든 행 전체 조회, 검색
fetchone() 첫 한 행 (없으면 None) 로그인 판정

fetchone()의 결과가 있으면(행이 존재하면) 로그인 성공, None이면 실패입니다.

조회 계열이므로 이번에도 commit()은 필요 없습니다.

 

3. 로그인 기능 구현하기

→ check_login 메서드를 작성해, 일치 여부로 성공·실패를 판정합니다.

더보기

클래스에 check_login 메서드를 추가합니다.

위치 지도의 ④번 자리, 즉 search_users 메서드 아래입니다.

 

접속 정보 검증, 파라미터 바인딩, 예외 처리는 지금까지와 같고,

새로운 부분은 fetchone() 결과로 성공·실패를 나누는 판정입니다.

 

 

    # ④ search_users 메서드 아래에 새로 추가
    def check_login(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        login_username = self.login_username_edit.text().strip()
        login_password = self.login_password_edit.text()   # 비밀번호는 strip하지 않음

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
            return
        if not login_username or not login_password:
            QMessageBox.warning(self, "입력 오류", "로그인 아이디와 비밀번호를 모두 입력해야 합니다.")
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            query = """
            SELECT id, username
            FROM users
            WHERE username = %s AND password = %s
            LIMIT 1
            """
            # 두 값 모두 %s 로 바인딩한다.
            cursor.execute(query, (login_username, login_password))
            row = cursor.fetchone()   # 일치하는 한 건 (없으면 None)

            if row:
                msg = f"{login_username} 사용자가 정상적으로 인증되었습니다."
                self.login_result_label.setText(msg)
                QMessageBox.information(self, "로그인 성공", msg)
                self.write_log_with_qt(f"로그인 성공: {db_name}.users, username={login_username}")
            else:
                msg = "아이디 또는 비밀번호가 올바르지 않습니다."
                self.login_result_label.setText(msg)
                QMessageBox.warning(self, "로그인 실패", msg)
                self.write_log_with_qt(f"로그인 실패: {db_name}.users, username={login_username}")
        except mc.Error as e:
            self.login_result_label.setText("로그인 처리 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "로그인 오류", f"로그인 처리에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

 

실패 메시지를 "아이디 또는 비밀번호가 올바르지 않습니다"로 합쳐서 보여 주는 점에 주목하세요.

어느 쪽이 틀렸는지 알려 주면 공격자가 존재하는 아이디를 추측하는 데 도움이 되므로,

실무에서도 이렇게 뭉뚱그려 안내합니다.

▶ 지금 해보세요

  1. 1에서 본 ①~③ 위치에 로그인 입력칸·버튼·시그널을 __init__에 반영합니다.
  2. ④ 위치에 check_login 메서드를 작성합니다.
  3. fetchone() 결과의 유무로 성공·실패를 나누는 부분을 확인합니다.

 

4. 실행하고 결과 확인하기

→ 로그인 성공과 실패를 각각 확인합니다.

더보기

main.py를 실행하고 pyqtdb에 접속합니다.

5강에서 저장해 둔 사용자(예: testuser / 1234)로 로그인해 성공을, 틀린 비밀번호로 실패를 확인합니다.

 

[화면 캡처: 로그인 실패 시 나타나는 경고 팝업]

 

▶ 지금 해보세요

  1. 저장된 아이디·비밀번호를 정확히 입력하고 로그인해 성공 팝업을 확인합니다.
  2. 비밀번호를 일부러 틀리게 입력해 실패 안내가 뜨는지 확인합니다.
  3. 존재하지 않는 아이디로도 시도해, 동일한 실패 메시지가 나오는지 확인합니다.
  4. db_log_all_steps.txt에 성공·실패 로그가 남았는지 확인합니다.

 

5. 비밀번호는 왜 평문이면 안 될까

→ 지금까지 평문으로 다룬 비밀번호를, 실무에서 안전하게 저장하는 방법을 정리합니다.

더보기

5.1 평문 저장의 위험

 

이 시리즈는 학습을 단순하게 하려고 비밀번호를 입력한 그대로(평문) 저장하고,

로그인 때도 password = %s로 그대로 비교했습니다.

 

하지만 이렇게 하면 DB가 유출되는 순간 모든 사용자의 비밀번호가 그대로 노출됩니다.

많은 사람이 여러 사이트에서 같은 비밀번호를 쓰기 때문에, 피해는 이 앱 하나에 그치지 않습니다.

 

그래서 실무에서는 비밀번호를 절대 평문으로 저장하지 않습니다.

6강에서 표에 password를 그대로 띄운 것도 학습 확인용일 뿐, 실제 앱에서는 하지 않는 일입니다.

 

 

 

 

5.2 해시로 저장하고 해시로 비교한다

 

대신 비밀번호를 해시(hash)로 바꾸어 저장합니다.

해시는 원래 값을 되돌릴 수 없는 형태로 변환하는 것으로, 저장된 해시만 봐서는 원래 비밀번호를 알 수 없습니다.

로그인 때는 입력한 비밀번호를 같은 방식으로 해시한 뒤, 저장된 해시와 일치하는지를 확인합니다.

 

단계 평문 대신 하는 일
저장할 때 입력한 비밀번호를 해시로 변환해, 그 해시 값을 DB에 저장
로그인할 때 입력한 비밀번호를 같은 방식으로 해시해, 저장된 해시와 일치하는지 확인

파이썬에서는 bcrypt 같은 라이브러리가 이 과정을 안전하게 처리해 줍니다.

 

저장할 때 bcrypt.hashpw로 해시를 만들어 넣고,

로그인할 때 bcrypt.checkpw로 입력값과 저장된 해시를 비교하는 방식입니다.

 

✔ 참고: 이 시리즈에서 다룬 INSERT·SELECT WHERE 구조는 그대로 두고, 저장·비교 대상만 평문에서 해시로 바꾸면 됩니다. SQL 흐름은 동일하고, 비밀번호를 다루는 지점만 안전하게 교체하는 것이 핵심입니다.

 

6. 이번 강의 완성본

→ 2강부터 8강까지 이어 붙이고, UI를 기능별 그룹으로 정리한 시리즈 전체 최종 통합본입니다.

더보기

이 시리즈는 마지막 강의이므로, 아래 mainwindow.py가 곧 전체 통합본입니다.

별도의 통합본 요청 없이 이 섹션으로 갈음합니다.

 

앞 강의 대비 달라진 점은 QGroupBox import 추가와, __init__에서 입력칸을 기능별 그룹 4개로 묶고 placeholder·라벨을 붙인 배치뿐이며, 메서드는 모두 동일합니다. main.py는 2강에서 작성한 것과 동일합니다.

8강.zip
0.00MB
import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QLineEdit,
    QPushButton, QVBoxLayout, QFormLayout, QMessageBox,
    QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
    QHBoxLayout, QGroupBox,
)
from PySide6.QtCore import QFile, QIODevice, QTextStream, Qt
import mysql.connector as mc


class MySqlDemoWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("MySQL 연결 설정")
        self.resize(520, 820)

        # ── 1) 서버 접속 정보 그룹 ──
        self.host_edit = QLineEdit()
        self.host_edit.setPlaceholderText("예: localhost")
        self.user_edit = QLineEdit()
        self.user_edit.setPlaceholderText("예: root")
        self.password_edit = QLineEdit()
        self.password_edit.setEchoMode(QLineEdit.Password)
        self.password_edit.setPlaceholderText("MySQL 접속 비밀번호")
        self.db_edit = QLineEdit()
        self.db_edit.setPlaceholderText("예: pyqtdb")

        self.host_edit.setText("localhost")
        self.user_edit.setText("root")

        conn_form = QFormLayout()
        conn_form.addRow("호스트", self.host_edit)
        conn_form.addRow("사용자", self.user_edit)
        conn_form.addRow("비밀번호", self.password_edit)
        conn_form.addRow("데이터베이스 이름", self.db_edit)

        self.btn_connect = QPushButton("연결 테스트")
        self.btn_create_db = QPushButton("데이터베이스 생성")
        self.btn_create_table = QPushButton("users 테이블 생성")
        self.result_label = QLabel("연결 결과가 여기에 표시됩니다.")
        self.result_label.setWordWrap(True)

        conn_box = QVBoxLayout()
        conn_box.addLayout(conn_form)
        conn_box.addWidget(self.btn_connect)
        conn_box.addWidget(self.btn_create_db)
        conn_box.addWidget(self.btn_create_table)
        conn_box.addWidget(self.result_label)

        conn_group = QGroupBox("1. 서버 접속 정보")
        conn_group.setLayout(conn_box)

        # ── 2) 사용자 추가 그룹 (접속 정보와 별개) ──
        self.username_edit = QLineEdit()
        self.username_edit.setPlaceholderText("저장할 사용자 아이디")
        self.password_user_edit = QLineEdit()
        self.password_user_edit.setEchoMode(QLineEdit.Password)
        self.password_user_edit.setPlaceholderText("저장할 비밀번호")

        self.btn_insert_user = QPushButton("users 테이블에 사용자 추가")

        insert_form = QFormLayout()
        insert_form.addRow("아이디", self.username_edit)
        insert_form.addRow("비밀번호", self.password_user_edit)

        insert_box = QVBoxLayout()
        insert_box.addLayout(insert_form)
        insert_box.addWidget(self.btn_insert_user)

        insert_group = QGroupBox("2. 사용자 추가")
        insert_group.setLayout(insert_box)

        # ── 3) 조회 · 검색 그룹 ──
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("사용자 이름 검색어 입력")
        self.btn_search = QPushButton("검색")

        search_layout = QHBoxLayout()
        search_layout.addWidget(QLabel("검색어"))
        search_layout.addWidget(self.search_edit)
        search_layout.addWidget(self.btn_search)

        self.table_users = QTableWidget()
        self.table_users.setColumnCount(3)
        self.table_users.setHorizontalHeaderLabels(["id", "username", "password"])
        self.table_users.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.table_users.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.table_users.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)

        self.btn_load_users = QPushButton("users 테이블 전체 조회")

        query_box = QVBoxLayout()
        query_box.addLayout(search_layout)
        query_box.addWidget(self.btn_load_users)
        query_box.addWidget(self.table_users)

        query_group = QGroupBox("3. 조회 · 검색")
        query_group.setLayout(query_box)

        # ── 4) 로그인 확인 그룹 (사용자 추가용 입력칸과 별개) ──
        self.login_username_edit = QLineEdit()
        self.login_username_edit.setPlaceholderText("로그인 아이디")
        self.login_password_edit = QLineEdit()
        self.login_password_edit.setEchoMode(QLineEdit.Password)
        self.login_password_edit.setPlaceholderText("로그인 비밀번호")

        self.btn_login = QPushButton("로그인 확인")
        self.login_result_label = QLabel("로그인 결과가 여기에 표시됩니다.")
        self.login_result_label.setWordWrap(True)

        login_form = QFormLayout()
        login_form.addRow("아이디", self.login_username_edit)
        login_form.addRow("비밀번호", self.login_password_edit)

        login_box = QVBoxLayout()
        login_box.addLayout(login_form)
        login_box.addWidget(self.btn_login)
        login_box.addWidget(self.login_result_label)

        login_group = QGroupBox("4. 로그인 확인")
        login_group.setLayout(login_box)

        # ── 전체 배치: 기능별 그룹 4개 ──
        main_layout = QVBoxLayout(self)
        main_layout.addWidget(conn_group)
        main_layout.addWidget(insert_group)
        main_layout.addWidget(query_group)
        main_layout.addWidget(login_group)

        # ── 시그널 연결 ──
        self.btn_connect.clicked.connect(self.connect_to_server_or_db)
        self.btn_create_db.clicked.connect(self.create_database)
        self.btn_create_table.clicked.connect(self.create_users_table)
        self.btn_insert_user.clicked.connect(self.insert_user)
        self.btn_load_users.clicked.connect(self.load_users)
        self.btn_search.clicked.connect(self.search_users)
        self.btn_login.clicked.connect(self.check_login)

    def connect_to_server_or_db(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        if not host or not user:
            QMessageBox.warning(self, "입력 오류", "호스트와 사용자 이름은 반드시 입력해야 합니다.")
            return

        conn = None
        try:
            if db_name:
                conn = mc.connect(host=host, user=user, password=password, database=db_name)
            else:
                conn = mc.connect(host=host, user=user, password=password)

            if conn.is_connected():
                if db_name:
                    msg = f"MySQL 서버에 연결되었습니다.\n사용 중인 데이터베이스: {db_name}"
                else:
                    msg = "MySQL 서버에 연결되었습니다.\n데이터베이스 이름은 지정되지 않았습니다."
                self.result_label.setText(msg)
                QMessageBox.information(self, "연결 성공", msg)
        except mc.Error as e:
            self.result_label.setText("연결 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "연결 오류", f"MySQL 연결에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def create_database(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "호스트, 사용자, 데이터베이스 이름을 모두 입력해야 합니다.")
            return

        if not db_name.isidentifier():
            QMessageBox.warning(self, "입력 오류", "데이터베이스 이름은 영문·숫자·밑줄만 사용할 수 있습니다.")
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password)
            cursor = conn.cursor()
            cursor.execute(f"CREATE DATABASE {db_name}")
            conn.commit()

            msg = f"{db_name} 데이터베이스가 생성되었습니다."
            self.result_label.setText(msg)
            QMessageBox.information(self, "생성 완료", msg)
        except mc.Error as e:
            self.result_label.setText("데이터베이스 생성에 실패했습니다.")
            QMessageBox.critical(self, "생성 오류", f"데이터베이스 생성에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def write_log_with_qt(self, message):
        file = QFile("db_log_all_steps.txt")
        if not file.open(QIODevice.Append | QIODevice.Text):
            return
        stream = QTextStream(file)
        stream << message << "\n"
        file.close()

    def create_users_table(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "테이블을 만들 데이터베이스 이름까지 모두 입력해야 합니다.")
            return

        create_table_sql = """
        CREATE TABLE users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            username VARCHAR(150) NOT NULL,
            password VARCHAR(150) NOT NULL
        )
        """

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            cursor.execute(create_table_sql)
            conn.commit()

            msg = f"{db_name} 데이터베이스에 users 테이블이 생성되었습니다."
            self.result_label.setText(msg)
            QMessageBox.information(self, "테이블 생성 완료", msg)
            self.write_log_with_qt(f"테이블 생성: {db_name}.users")
        except mc.Error as e:
            self.result_label.setText("테이블 생성 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "테이블 생성 오류", f"users 테이블 생성에 실패했습니다.\n오류 내용: {e}")
            self.write_log_with_qt(f"테이블 생성 실패: {db_name}.users, 오류: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def insert_user(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        username = self.username_edit.text().strip()
        user_password = self.password_user_edit.text()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
            return
        if not username or not user_password:
            QMessageBox.warning(self, "입력 오류", "사용자 이름과 비밀번호를 모두 입력해야 합니다.")
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            query = "INSERT INTO users (username, password) VALUES (%s, %s)"
            values = (username, user_password)
            cursor.execute(query, values)
            conn.commit()

            msg = f"사용자 {username} 정보가 저장되었습니다."
            self.result_label.setText(msg)
            QMessageBox.information(self, "저장 완료", msg)
            self.write_log_with_qt(f"사용자 추가 성공: {db_name}.users, username={username}")

            self.username_edit.clear()
            self.password_user_edit.clear()
            self.username_edit.setFocus()
        except mc.Error as e:
            self.result_label.setText("데이터 저장 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "저장 오류", f"데이터 저장에 실패했습니다.\n오류 내용: {e}")
            self.write_log_with_qt(f"사용자 추가 실패: username={username}, 오류: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def load_users(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            query = "SELECT id, username, password FROM users ORDER BY id ASC"
            cursor.execute(query)
            rows = cursor.fetchall()

            self.table_users.setRowCount(len(rows))
            for row_index, (id_value, username_value, password_value) in enumerate(rows):
                item_id = QTableWidgetItem(str(id_value))
                item_username = QTableWidgetItem(username_value)
                item_password = QTableWidgetItem(password_value)
                item_id.setTextAlignment(Qt.AlignCenter)
                item_username.setTextAlignment(Qt.AlignCenter)
                item_password.setTextAlignment(Qt.AlignCenter)
                self.table_users.setItem(row_index, 0, item_id)
                self.table_users.setItem(row_index, 1, item_username)
                self.table_users.setItem(row_index, 2, item_password)

            msg = f"총 {len(rows)}개의 사용자 데이터를 조회했습니다."
            self.result_label.setText(msg)
            self.write_log_with_qt(f"사용자 조회: {db_name}.users, 행 수={len(rows)}")
        except mc.Error as e:
            self.result_label.setText("데이터 조회 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "조회 오류", f"데이터 조회에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def search_users(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()
        keyword = self.search_edit.text().strip()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
            return

        if not keyword:
            QMessageBox.information(self, "검색어 없음", "검색어가 비어 있어 전체 목록을 조회합니다.")
            self.load_users()
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            query = """
            SELECT id, username, password
            FROM users
            WHERE username LIKE %s
            ORDER BY id ASC
            """
            like_value = f"%{keyword}%"
            cursor.execute(query, (like_value,))

            rows = cursor.fetchall()
            self.table_users.setRowCount(len(rows))
            for row_index, (id_value, username_value, password_value) in enumerate(rows):
                item_id = QTableWidgetItem(str(id_value))
                item_username = QTableWidgetItem(username_value)
                item_password = QTableWidgetItem(password_value)
                item_id.setTextAlignment(Qt.AlignCenter)
                item_username.setTextAlignment(Qt.AlignCenter)
                item_password.setTextAlignment(Qt.AlignCenter)
                self.table_users.setItem(row_index, 0, item_id)
                self.table_users.setItem(row_index, 1, item_username)
                self.table_users.setItem(row_index, 2, item_password)

            if rows:
                msg = f"검색어 '{keyword}' 결과 {len(rows)}건을 찾았습니다."
            else:
                msg = f"검색어 '{keyword}' 에 해당하는 결과가 없습니다."
            self.result_label.setText(msg)
            self.write_log_with_qt(f"사용자 검색: {db_name}.users, keyword={keyword}, 행 수={len(rows)}")
        except mc.Error as e:
            self.result_label.setText("데이터 검색 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "검색 오류", f"데이터 검색에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

    def check_login(self):
        host = self.host_edit.text().strip()
        user = self.user_edit.text().strip()
        password = self.password_edit.text()
        db_name = self.db_edit.text().strip()

        login_username = self.login_username_edit.text().strip()
        login_password = self.login_password_edit.text()

        if not host or not user or not db_name:
            QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
            return
        if not login_username or not login_password:
            QMessageBox.warning(self, "입력 오류", "로그인 아이디와 비밀번호를 모두 입력해야 합니다.")
            return

        conn = None
        try:
            conn = mc.connect(host=host, user=user, password=password, database=db_name)
            cursor = conn.cursor()
            query = """
            SELECT id, username
            FROM users
            WHERE username = %s AND password = %s
            LIMIT 1
            """
            cursor.execute(query, (login_username, login_password))
            row = cursor.fetchone()

            if row:
                msg = f"{login_username} 사용자가 정상적으로 인증되었습니다."
                self.login_result_label.setText(msg)
                QMessageBox.information(self, "로그인 성공", msg)
                self.write_log_with_qt(f"로그인 성공: {db_name}.users, username={login_username}")
            else:
                msg = "아이디 또는 비밀번호가 올바르지 않습니다."
                self.login_result_label.setText(msg)
                QMessageBox.warning(self, "로그인 실패", msg)
                self.write_log_with_qt(f"로그인 실패: {db_name}.users, username={login_username}")
        except mc.Error as e:
            self.login_result_label.setText("로그인 처리 중 오류가 발생했습니다.")
            QMessageBox.critical(self, "로그인 오류", f"로그인 처리에 실패했습니다.\n오류 내용: {e}")
        finally:
            if conn and conn.is_connected():
                conn.close()

 

 

7. 시리즈 마무리

→ 1강부터 8강까지 쌓아 온 흐름을 한눈에 정리합니다.

더보기

이 시리즈는 하나의 창에 기능을 한 단계씩 얹어, 데이터베이스 연동의 전 과정을 이어서 경험하도록 구성했습니다.

각 단계는 앞 단계의 코드를 그대로 두고 버튼·메서드만 더하는 방식으로 확장됐습니다.

1강  도구 선택      →  Connector/Python을 쓰는 이유
2강  연결           →  conn = None + try·except·finally 패턴
3강  DB 생성        →  식별자는 검증 (isidentifier)
4강  테이블 생성     →  database 지정 연결 + 로그 기록
5강  데이터 삽입     →  값은 %s 로 바인딩 (SQL Injection 방지)
6강  조회           →  SELECT + fetchall + QTableWidget
7강  검색           →  WHERE·LIKE 부분 검색
8강  로그인         →  WHERE ... AND ... + fetchone, 그리고 비밀번호 보안

 

특히 연결·확인·SQL 실행·종료라는 뼈대와

'식별자는 검증, 값은 바인딩'이라는 규칙은 모든 단계를 진행했습니다.

 

이 두 가지를 손에 익혔다면,

앞으로 다른 테이블이나 다른 기능(수정·삭제, 복합 조건 검색 등)도 같은 패턴 위에 얹어 확장할 수 있습니다.

 

✔ 정리: 연결부터 로그인까지 하나의 프로젝트로 완성했습니다. 다음 단계로 나아가려면, 이번에 정리한 비밀번호 해싱을 실제로 적용해 보고, 반복되는 연결 코드를 함수로 묶어 정리해 보는 것을 권합니다.