7강 실습 중심 ⏱ 약 30분

 

0. 학습 목표

→ WHERE·LIKE로 이름의 일부만으로 사용자를 찾는 검색 기능을 만듭니다.

더보기

0.1 이번 글에서 다룰 내용

 

이번 글은 실습 중심 강의입니다.

 

6강의 전체 조회는 모든 사용자를 보여 줬습니다.

이번에는 WHERE 조건과 LIKE 연산자로 이름의 일부만으로 원하는 사용자를 찾습니다.

 

6강의 표(QTableWidget)와 결과 채우기 방식을 그대로 재사용하므로,

새로 배우는 것은 검색 조건을 만드는 부분뿐입니다. 검색어 역시 5강처럼 %s로 바인딩합니다.

 

구분 내용
이해할 것 WHERE 조건과 LIKE 부분 검색, 검색어를 %s로 바인딩하는 이유
만들 것 검색어 입력칸·검색 버튼과 search_users 메서드, 빈 검색어 시 전체 조회 폴백

 

 

 

 

0.2 이번 강의 실습 내용

 

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

  1. 검색어 입력칸과 검색 버튼을 표 위에 추가합니다.
  2. search_users 메서드에서 WHERE ... LIKE %s로 검색합니다.
  3. 검색어가 비어 있으면 전체 조회로 넘어가게 처리합니다.
  4. 부분 검색·결과 없음·빈 검색어를 각각 확인합니다.

 

1. 무엇을 추가하는가

→ 표 위에 검색어 입력칸과 검색 버튼을 가로로 배치합니다.

더보기

검색어 입력칸과 검색 버튼을 한 줄에 나란히 놓기 위해 가로 배치용 QHBoxLayout을 새로 사용합니다.

[화면 캡처: 표 위에 '검색어' 라벨·입력칸·검색 버튼이 한 줄로 놓인 창]

 

 

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

from PySide6.QtWidgets import ...   ← ① QHBoxLayout import 추가

class MySqlDemoWindow(QWidget):
    def __init__(self):
        ... 접속 정보 입력 필드 (2강) ...
        ... 접속 버튼 3개 (2·3·4강) ...
        ... 사용자 추가 입력칸·버튼 (5강) ...
        ... 검색어 입력칸·검색 버튼 생성        ← ② search_edit·btn_search 만들기
        ... search_layout 가로 배치            ← ③ QHBoxLayout으로 한 줄 구성
        ... 표(QTableWidget)·조회 버튼 (6강) ...
        ... main_layout 배치                   ← ④ search_layout을 표보다 위에 넣기
        ... 시그널 연결                        ← ⑤ 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강 메서드 (그대로)
        ...
    # ⑥ search_users 메서드를 여기(클래스 안)에 새로 추가

 

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

⑥은 클래스 안에 새로 추가되는 메서드입니다. 아래는 ①~⑤의 실제 코드입니다.

from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QLineEdit,             # (2강)
    QPushButton, QVBoxLayout, QFormLayout, QMessageBox,    # (2강)
    QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,   # (6강)
    QHBoxLayout,   # 추가 (①, 검색줄 가로 배치용)
)

        # ── ② 검색어 입력칸·검색 버튼 생성 부분 ──
        self.search_edit = QLineEdit()                              # 추가
        self.search_edit.setPlaceholderText("사용자 이름 검색어 입력")   # 추가
        self.btn_search = QPushButton("검색")                       # 추가

        # ── ③ search_layout 가로 배치 부분 ──
        search_layout = QHBoxLayout()                  # 추가
        search_layout.addWidget(QLabel("검색어"))       # 추가
        search_layout.addWidget(self.search_edit)      # 추가
        search_layout.addWidget(self.btn_search)       # 추가

        # ── (6강) 표·조회 버튼 생성은 그대로 ──
        self.btn_load_users = QPushButton("users 테이블 전체 조회")   # (6강)
        self.table_users = QTableWidget()                            # (6강)
        # ... 표 설정 코드 (6강, 그대로) ...

        # ── ④ main_layout 배치 부분 ──
        main_layout.addWidget(self.btn_insert_user)     # (5강)
        main_layout.addLayout(search_layout)            # 추가 (사용자 추가 버튼 바로 아래, 표보다 위)
        main_layout.addWidget(self.btn_load_users)      # (6강)
        main_layout.addWidget(self.table_users)         # (6강)

        # ── ⑤ 시그널 연결 부분 ──
        self.btn_load_users.clicked.connect(self.load_users)   # (6강)
        self.btn_search.clicked.connect(self.search_users)     # 추가

 

setPlaceholderText는 입력칸이 비어 있을 때 흐릿한 안내 문구를 보여 줍니다.

검색줄을 조회 버튼과 표보다 위에 배치해, 검색해서 표를 채우는 흐름이 위에서 아래로 자연스럽게 이어지게 했습니다.

 

2. 검색 조건 이해하기

→ WHERE·LIKE의 의미와, 검색어를 바인딩하는 방식을 정리합니다.

더보기

2.1 WHERE와 LIKE

 

WHERE는 조건에 맞는 행만 골라 오는 SQL 구문입니다.

여기에 LIKE를 더하면 정확히 일치하지 않아도 일부만 포함하는 행을 찾을 수 있습니다.

검색어 앞뒤에 %를 붙이는 것이 핵심입니다.

SELECT id, username, password
FROM users
WHERE username LIKE %s      -- 값 자리는 바인딩으로 채운다
ORDER BY id ASC

 

%는 '아무 글자나 와도 된다'는 뜻입니다.

그래서 %jo%로 검색하면 이름 어딘가에 jo가 들어간 john, jordan이 모두 걸립니다.

 

 

 

 

2.2 %는 SQL에, 검색어는 바인딩으로

 

여기서 헷갈리기 쉬운 점이 있습니다.

LIKE%는 SQL 문법의 일부이고, 사용자가 입력한 검색어는 값입니다.

그래서 %는 SQL에 두지 않고, 파이썬에서 검색어 양쪽에 %를 붙인 문자열을 만들어 그것을 %s 자리에 바인딩합니다.

like_value = f"%{keyword}%"          # 검색어 양쪽에 % 를 붙인 '값'
cursor.execute(query, (like_value,)) # 그 값을 %s 자리에 바인딩

 

5강의 규칙이 그대로 이어집니다.

검색어도 사용자 입력이므로 문자열로 직접 이어 붙이지 않고 %s로 넘겨, SQL Injection을 막습니다.

값이 하나뿐이어도 튜플 형태 (like_value,)로 넘기는 점에 주의하세요.

 

3. 검색 기능 구현하기

→ search_users 메서드를 작성하고, 빈 검색어 처리를 함께 넣습니다.

더보기

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

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

표를 채우는 반복문은 6강 load_users와 같고, 달라지는 부분은 검색 조건 SQL과 빈 검색어 처리뿐입니다.

[화면 캡처: 검색어 '02' 입력 후 검색해 일부 사용자만 표에 남은 화면]

 

    # ⑥ load_users 메서드 아래에 새로 추가
    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()
            # 표 채우기 — 6강과 동일한 패턴
            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()

 

표를 채우는 부분이 6강과 똑같다는 점이 중요합니다.

조회 결과가 전체든 일부든, setRowCountsetItem 패턴은 그대로입니다.

결과가 0건일 때도 오류가 아니라 '결과 없음' 안내로 처리해, 사용자가 상황을 명확히 알 수 있게 했습니다.

 

▶ 지금 해보세요

  1. 1에서 본 ①~⑤ 위치에 검색줄 구성과 시그널 연결을 __init__에 반영합니다.
  2. ⑥ 위치에 search_users 메서드를 작성합니다.
  3. 표를 채우는 반복문이 6강 load_users와 같은지 비교해 봅니다.

 

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

→ 부분 검색·결과 없음·빈 검색어를 각각 확인합니다.

더보기

main.py를 실행하고, 5강에서 여러 사용자를 넣어 둔 pyqtdb에 접속합니다.

검색어를 바꿔 가며 LIKE가 어떻게 동작하는지 확인합니다.

 

[화면 캡처: 결과가 없을 때 하단 라벨에 '결과가 없습니다' 메시지가 표시된 화면]

 

▶ 지금 해보세요

  1. 이름의 일부(예: jo)를 입력하고 검색해, 포함하는 사용자만 남는지 확인합니다.
  2. 존재하지 않는 값(예: zzz)을 검색해, '결과가 없습니다' 안내가 뜨는지 확인합니다.
  3. 검색어를 비운 채 검색 버튼을 눌러, 전체 조회로 넘어가는지 확인합니다.
  4. db_log_all_steps.txt에 검색 로그가 남았는지 확인합니다.

✔ 정리: 전체 조회(6강)를 그대로 두고 WHERE ... LIKE만 더해 검색을 완성했습니다. 검색어는 %s로 바인딩하고, 결과를 채우는 표 패턴은 조회와 공유합니다. 빈 검색어는 전체 조회로 자연스럽게 이어집니다.

 

5. 이번 강의 완성본

→ 2~7강까지 이어 붙인 전체 코드를 한 번에 확인합니다.

더보기

본문에서는 변경분만 다뤘습니다.

아래는 위치를 대조하거나 코드가 어긋났을 때 기준으로 삼을 mainwindow.py 전체입니다.

main.py는 2강에서 작성한 것과 동일하므로 여기서는 mainwindow.py만 싣습니다.

7강.zip
0.00MB
import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QLineEdit,
    QPushButton, QVBoxLayout, QFormLayout, QMessageBox,
    QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
    QHBoxLayout,
)
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(480, 600)

        # 접속 정보 입력 필드
        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")

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

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

        # 저장할 사용자 정보 입력칸 (접속 정보와 별개)
        self.username_edit = QLineEdit()
        self.password_user_edit = QLineEdit()
        self.password_user_edit.setEchoMode(QLineEdit.Password)

        self.btn_insert_user = 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)

        # 조회 결과를 담을 표
        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 테이블 전체 조회")

        main_layout = QVBoxLayout(self)
        main_layout.addLayout(form_layout)
        main_layout.addWidget(self.btn_connect)
        main_layout.addWidget(self.btn_create_db)
        main_layout.addWidget(self.btn_create_table)
        main_layout.addWidget(self.result_label)
        main_layout.addWidget(self.username_edit)
        main_layout.addWidget(self.password_user_edit)
        main_layout.addWidget(self.btn_insert_user)
        main_layout.addLayout(search_layout)
        main_layout.addWidget(self.btn_load_users)
        main_layout.addWidget(self.table_users)

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

 

→ 다음 강의 (8강): 마지막으로 아이디·비밀번호가 일치하는지 확인하는 로그인 기능을 만듭니다. 로그인도 결국 WHERE 조건 조회라는 점과 함께, 지금까지 평문으로 다뤄 온 비밀번호를 실무에서는 왜, 어떻게 안전하게 저장하는지 정리하며 시리즈를 마칩니다.