1. 목표

더보기
  • WHERE 조건문을 사용해 특정 사용자만 골라서 조회하는 방법을 익힌다.
  • LIKE 연산자를 사용해 부분 검색 기능을 구현한다.
  • 검색어를 입력하는 QLineEdit와 검색 버튼(QPushButton)으로 검색 UI를 구성한다.
  • 검색 결과를 기존 QTableWidget에 다시 채워 넣어, 전체 조회와 검색 조회를 모두 지원한다.
  • 검색 실패, 접속 실패 등 예외 상황에서 적절한 메시지를 띄우고 로그를 남기는 흐름을 이해한다.

 

2. 전체 로직

더보기
import sys

from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QHBoxLayout,
    QFormLayout,
    QMessageBox,
    QGroupBox,
    QTableWidget,
    QTableWidgetItem,
    QHeaderView,
    QAbstractItemView,
)
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("PySide6 MySQL 사용자 관리 데모")
        self.resize(820, 680)

        self.host_edit = QLineEdit()
        self.user_edit = QLineEdit()
        self.password_edit = QLineEdit()
        self.password_edit.setEchoMode(QLineEdit.Password)
        self.db_edit = QLineEdit()

        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 테이블 생성")

        conn_button_layout = QVBoxLayout()
        conn_button_layout.addWidget(self.btn_connect)
        conn_button_layout.addWidget(self.btn_create_db)
        conn_button_layout.addWidget(self.btn_create_table)

        conn_group_layout = QVBoxLayout()
        conn_group_layout.addLayout(conn_form)
        conn_group_layout.addLayout(conn_button_layout)

        conn_group = QGroupBox("1~3단계: 연결·DB·테이블 생성")
        conn_group.setLayout(conn_group_layout)

        self.username_edit = QLineEdit()
        self.password_user_edit = QLineEdit()
        self.password_user_edit.setEchoMode(QLineEdit.Password)

        insert_form = QFormLayout()
        insert_form.addRow("사용자 이름", self.username_edit)
        insert_form.addRow("사용자 비밀번호", self.password_user_edit)

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

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

        insert_group = QGroupBox("4단계: PySide6 폼에서 데이터 입력 후 MySQL 저장")
        insert_group.setLayout(insert_group_layout)

        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 테이블 전체 조회")

        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)

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

        table_group = QGroupBox("5~6단계: 데이터 조회 및 검색 결과 표시")
        table_group.setLayout(table_group_layout)

        self.result_label = QLabel("결과 메시지가 여기에 표시됩니다.")
        self.result_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        main_layout = QVBoxLayout(self)
        main_layout.addWidget(conn_group)
        main_layout.addWidget(insert_group)
        main_layout.addWidget(table_group)
        main_layout.addWidget(self.result_label)

        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)

    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.handle_mysql_error(e, "연결 중 오류가 발생했습니다.", "연결 오류")
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    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

        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)
            self.write_log_with_qt(f"데이터베이스 생성: {db_name}")

        except mc.Error as e:
            self.handle_mysql_error(e, "데이터베이스 생성에 실패했습니다.", "데이터베이스 생성 오류")
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    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.handle_mysql_error(e, "테이블 생성 중 오류가 발생했습니다.", "테이블 생성 오류")
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    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().strip()

        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.handle_mysql_error(e, "데이터 저장 중 오류가 발생했습니다.", "저장 오류", username=username)
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    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, row_data in enumerate(rows):
                id_value, username_value, password_value = row_data

                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.handle_mysql_error(e, "데이터 조회 중 오류가 발생했습니다.", "조회 오류")
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    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, "검색어 없음", "검색어가 비어 있습니다.\n전체 목록을 조회합니다.")
            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, row_data in enumerate(rows):
                id_value, username_value, password_value = row_data

                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.handle_mysql_error(e, "데이터 검색 중 오류가 발생했습니다.", "검색 오류")
        finally:
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

    def handle_mysql_error(self, error, ui_message, title, username=None):
        self.result_label.setText(ui_message)

        if hasattr(error, "errno") and error.errno == 1045:
            detail = (
                "접속이 거부되었습니다.\n\n"
                "가능한 원인\n"
                "1. 사용자 이름이 잘못되었습니다.\n"
                "2. 비밀번호가 잘못되었거나 비밀번호가 필요한데 비워 둔 상태입니다.\n"
                "3. 현재 MySQL 서버 설정에서 root 계정 대신 별도 계정 사용이 요구될 수 있습니다.\n\n"
                "해결 방법\n"
                "1. MySQL 클라이언트에서 직접 로그인해 사용자와 비밀번호를 확인합니다.\n"
                "2. 이 창의 사용자와 비밀번호 입력란에 동일한 정보를 입력합니다.\n"
                "3. 필요하다면 애플리케이션 전용 계정을 새로 생성하고 권한을 부여합니다."
            )
            QMessageBox.critical(
                self,
                title,
                f"{ui_message}\n\n오류 코드: 1045\n오류 내용: {error}\n\n{detail}",
            )
        else:
            QMessageBox.critical(
                self,
                title,
                f"{ui_message}\n\n오류 내용: {error}",
            )

        if username is not None:
            self.write_log_with_qt(f"사용자 추가 실패: {username}, 오류: {error}")
        else:
            self.write_log_with_qt(f"MySQL 작업 실패, 오류: {error}")

    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()
# main.py

import sys
from PySide6.QtWidgets import QApplication

from mainwindow import MySqlDemoWindow

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MySqlDemoWindow()
    w.show()
    sys.exit(app.exec())


3. 필요한 import + 주석 설명

더보기
from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QHBoxLayout,  # 검색어 입력란과 검색 버튼을 한 줄에 배치하기 위한 가로 레이아웃
    QFormLayout,
    QMessageBox,
    QGroupBox,
    QTableWidget,
    QTableWidgetItem,
    QHeaderView,
    QAbstractItemView,
)

이번 단계에서 이전 단계와 비교해 새롭게 추가된 항목은 QHBoxLayout 하나이다.

QHBoxLayout
검색어 입력란과 검색 버튼을 좌우로 나란히 배치하기 위해 사용하는 가로 레이아웃이다.
이전까지는 주로 QVBoxLayout을 사용해 위에서 아래로 쌓았다.
검색 UI는 한 줄에 정리하는 것이 보기 좋기 때문에 QHBoxLayout을 추가했다.


 

4. GUI 구현부 소스코드와 주석

더보기
class MySqlDemoWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

		# . . . 
        
        # 4) 사용자 목록을 보여 줄 QTableWidget과 조회 버튼
        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 테이블 전체 조회")

        # 6단계: 검색어 입력란과 검색 버튼 추가
        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)

        # 검색 UI + 전체 조회 버튼 + 테이블을 하나의 그룹박스로 묶기
        table_group_layout = QVBoxLayout()
        table_group_layout.addLayout(search_layout)
        table_group_layout.addWidget(self.btn_load_users)
        table_group_layout.addWidget(self.table_users)

        table_group = QGroupBox("5~6단계: 데이터 조회 및 검색 결과 표시")
        table_group.setLayout(table_group_layout)

 

  • 검색어 입력란 search_edit에는 자리 표시 텍스트를 넣어, 어떤 용도인지 직관적으로 보여 준다.
  • 검색어 레이블, search_edit, btn_search를 QHBoxLayout으로 한 줄에 배치해 검색 영역을 한 눈에 보이도록 했다.
  • 기존 5단계에서 사용하던 btn_load_users와 table_users는 그대로 유지하고,
    그 위에 검색 UI를 추가한 형태라 학습 흐름이 자연스럽게 이어진다.

 


 

5. 시그널과 슬롯 연결 부분

더보기
        # 기존 연결
        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)
  • clicked 시그널을 search_users 슬롯 메서드에 연결했다.
  • 이제 검색 버튼이 눌리면
    1. 검색어를 읽고
    2. WHERE + LIKE 조건으로 SELECT를 실행하고
    3. 결과를 QTableWidget에 채워 넣는 흐름이 실행된다.
  • 기존 전체 조회 버튼 btn_load_users는 그대로 유지

 

6. < DATA 조건 검색> 주요 구조

더보기

6.1 <DB 조건 검색> 핵심 요약

# 1. 연결
conn = mc.connect(서버_및_DB_접속_정보)

# 2. 확인
if conn.is_connected():
    성공_처리()

# 3. SQL 실행 + 조건 값 바인딩
cursor = conn.cursor()
cursor.execute(SELECT_WHERE_SQL, (PARAMS,))
rows = cursor.fetchall()

# 4. 결과 사용 (UI/테이블에 표시 등)
결과_표시(rows)

# 5. 종료
conn.close()

조건 검색에서 핵심은 문자열을 직접 붙이지 않고, execute(SQL, 파라미터)로 바인딩

 

 

6.2 <DB 조건 검색> 흐름

import mysql.connector as mc


def users_select_search_flow():
    # 1. DB 서버 및 데이터베이스 접속 정보 준비
    host = "localhost"
    user = "root"
    password = "password"
    database = "test_db"

    # 2. 검색어 준비 (GUI 입력값에 해당)
    keyword = "kim"

    # 3. SELECT 조건 검색 SQL 준비 (WHERE + LIKE)
    select_where_sql = """
    SELECT id, username, password
    FROM users
    WHERE username LIKE %s
    ORDER BY id ASC
    """

    # 4. LIKE 패턴 값 구성 (검색어를 포함하는 데이터)
    like_value = f"%{keyword}%"

    # 5. 지정된 데이터베이스에 연결
    conn = mc.connect(
        host=host,
        user=user,
        password=password,
        database=database
    )

    # 6. 서버와의 연결 상태 확인
    if conn.is_connected():
        print("DB 연결 성공")

        # 7. SQL 실행을 위한 커서 생성
        cursor = conn.cursor()

        # 8. 조건 검색 SQL 실행 (파라미터 바인딩)
        cursor.execute(select_where_sql, (like_value,))

        # 9. 조회 결과 가져오기
        rows = cursor.fetchall()

        # 10. 조회 결과 사용 (여기서는 출력, 실제 GUI에서는 테이블에 표시)
        for row in rows:
            print(row)

    # 11. 모든 DB 작업이 끝난 후 연결 종료
    conn.close()


# 조건 검색 흐름 실행
users_select_search_flow()

 

  1. DB 접속 정보 준비
  2. 검색어 준비
  3. WHERE + LIKE SELECT SQL 작성
  4. LIKE 패턴 값 구성("%keyword%")
  5. DB 연결
  6. 연결 상태 확인
  7. 커서 생성
  8. SQL 실행(파라미터 바인딩)
  9. fetchall()로 결과 수집
  10. 결과 표시(출력/테이블 반영)
  11. 연결 종료

 

7. 기능 구현 소스코드

더보기
    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, "검색어 없음", "검색어가 비어 있습니다.\n전체 목록을 조회합니다.")
            self.load_users()
            return

        conn = None

        try:
            # MySQL 서버와 특정 데이터베이스에 연결
            conn = mc.connect(
                host=host,
                user=user,
                password=password,
                database=db_name,
            )

            cursor = conn.cursor()

            # WHERE + LIKE 를 사용한 부분 검색
            # username 컬럼에서 keyword가 부분 일치하는 행만 선택
            query = """
            SELECT id, username, password
            FROM users
            WHERE username LIKE %s
            ORDER BY id ASC
            """
            like_value = f"%{keyword}%"

            # 파라미터 바인딩 방식으로 LIKE 값 전달
            cursor.execute(query, (like_value,))
            rows = cursor.fetchall()

            # 결과 행 수만큼 QTableWidget의 행 수 설정
            self.table_users.setRowCount(len(rows))

            for row_index, row_data in enumerate(rows):
                id_value, username_value, password_value = row_data

                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:
            # 공통 MySQL 오류 처리
            self.handle_mysql_error(e, "데이터 검색 중 오류가 발생했습니다.", "검색 오류")
        finally:
            # 연결 자원 정리
            try:
                if conn and conn.is_connected():
                    conn.close()
            except Exception:
                pass

 

  • WHERE 조건문 기초
    WHERE username LIKE %s 구문을 통해 조건에 맞는 행만 필터링한다.

  • 부분 검색 구현
    like_value = f"%{keyword}%" 로 앞뒤에 퍼센트 기호를 붙여 부분 일치를 구현했다.
    예를 들어 keyword 값이 jo이면 john, jordan 등이 모두 검색된다.

  • 파라미터 쿼리 사용
    문자열을 직접 이어 붙이지 않고 cursor.execute(query, (like_value,)) 방식으로 전달해
    SQL Injection 위험을 줄인다.

  • QTableWidget 갱신
    전체 조회와 거의 같은 방식으로 setRowCount, setItem을 호출해 결과를 다시 채운다.

 

7. 실행 테스트

더보기

전체 조회 기능 확인

  • 접속 정보와 DB 이름을 올바르게 입력한다.
  • users 테이블 전체 조회 버튼을 클릭한다.
  • QTableWidget에 모든 사용자 데이터가 id 순서대로 표시되는지 확인한다.

 

검색 기능 테스트 1: 완전 일치에 가까운 검색

  • 검색어 입력란에 정확한 사용자 이름을 입력한다.
    예: john
  • 검색 버튼을 클릭한다.
  • QTableWidget에 john인 사용자만 남는지 확인한다.
  • 하단 라벨에 검색 결과 행 수가 표시되는지 확인한다.

 

검색 기능 테스트 2: 부분 검색

  • 검색어 입력란에 이름의 일부만 입력한다.
    예: o
  • 검색 버튼을 클릭한다.
  • 이름에 o가 포함된 사용자만 표시되는지 확인한다.

 

검색 기능 테스트 3: 결과 없음

  • 검색어 입력란에 존재하지 않는 이름을 입력한다.
    예: xyz_not_found
  • 검색 버튼을 클릭한다.
  • 테이블 행 수가 0이 되고
    결과 라벨에 결과가 없다는 메시지가 표시되는지 확인한다.

 

검색어 공백 처리 테스트

  • 검색어를 비워 둔 상태에서 검색 버튼을 클릭한다.
  • 검색어 없음 안내 메시지가 뜨고, 전체 조회가 자동 실행되는지 확인한다.

 

오류 처리 테스트

  • 의도적으로 잘못된 DB 이름이나 잘못된 비밀번호를 입력한 뒤
    검색 버튼을 눌러 MySQL 오류가 나는 상황을 만들어 본다.
  • 공통 오류 처리 함수가 실행되어 의미 있는 안내 메시지와 로그가 남는지 확인한다.


8.학습 주요 포인트

더보기

 

WHERE, LIKE를 이용한 조건 조회

  • WHERE username LIKE %s 구문으로 특정 조건을 만족하는 행만 선택할 수 있다.
  • f"%{keyword}%" 형식으로 부분 검색을 구현해 사용자 경험을 개선할 수 있다.

 

QTableWidget을 활용한 결과 갱신

  • 전체 조회와 검색 모두 같은 테이블과 같은 컬럼 구조를 사용한다.
  • SELECT 결과가 바뀌어도 setRowCount와 setItem 패턴만 알면 쉽게 갱신할 수 있다.

 

파라미터 쿼리와 보안

  • cursor.execute(query, (like_value,)) 형식의 파라미터 바인딩을 사용하면
    문자열 결합 방식보다 안전하다.
  • INSERT, SELECT, SEARCH 모든 곳에서 동일 패턴을 사용해 SQL Injection 가능성을 줄인다.

 

예외 처리와 사용자 피드백

  • 검색 중 발생하는 MySQL 오류도 기존 handle_mysql_error로 통합 처리한다.
  • UI 라벨과 메시지 박스를 함께 사용해, 학생과 최종 사용자 모두에게 이해하기 쉬운 피드백을 제공한다.
  • QFile, QTextStream을 사용해 모든 단계를 로그로 남기는 패턴은
    이후 학생 관리 프로젝트에서 디버깅과 이력 관리에 활용할 수 있다.

 

단계 간 확장성

  • 5단계에서 구현한 전체 조회(load_users)를 그대로 두고
    6단계에서 WHERE + LIKE만 더해 search_users를 추가함으로써
    앞에서 배운 개념을 재사용하면서 자연스럽게 난이도를 높였다.
  • 이후 로그인 기능, 복합 조건 검색, 페이징 등도 같은 패턴 위에 추가할 수 있다.

 


 

단계별 완성 파일