1. 목표

더보기

이 단계의 학습 목표

  • QLineEdit, QPushButton을 사용해 간단한 입력 폼을 구성할 수 있다.
  • MySQL INSERT SQL 문을 이해하고 직접 작성할 수 있다.
  • 파라미터 바인딩을 사용해 SQL Injection을 방지할 수 있다.
  • PySide6의 QMessageBox로 성공, 실패 메시지를 표시할 수 있다.
  • QFile, QTextStream을 사용해 삽입 로그를 파일로 남길 수 있다.

전제 조건

  • 데이터베이스 pyqtdb가 이미 생성되어 있다.
  • pyqtdb 안에 users 테이블이 이미 생성되어 있다.
    (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(150), password VARCHAR(150))

 

2. 전체 로직

더보기
import sys

from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QFormLayout,
    QMessageBox,
    QGroupBox,
)
from PySide6.QtCore import QFile, QIODevice, QTextStream

import mysql.connector as mc


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

        self.setWindowTitle("PySide6 MySQL 연동 통합 데모")
        self.resize(540, 460)

        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("데이터베이스 생성 (TestDB)")
        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.result_label = QLabel("결과 메시지가 여기에 표시됩니다.")

        main_layout = QVBoxLayout(self)
        main_layout.addWidget(conn_group)
        main_layout.addWidget(insert_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)

    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

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

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

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

        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.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 + 주석 설명

더보기

동일


 

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

더보기

 

  • 위쪽 그룹: 1~3단계 (연결, DB 생성, 테이블 생성)
  • 아래쪽 그룹: 4단계 (INSERT)

 

class MySqlInsertDemoWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle("PySide6 MySQL 연동 통합 데모")
        self.resize(540, 460)

        # 이전 단계와 동일한 형태의 접속 정보 입력란이다.
        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")

        # 연결, DB, 테이블 생성에 공통으로 사용할 입력 UI를 폼 레이아웃에 배치한다.
        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)

        # 1단계: 연결 테스트용 버튼
        self.btn_connect = QPushButton("연결 테스트")
        # 2단계: 데이터베이스 생성용 버튼
        self.btn_create_db = QPushButton("데이터베이스 생성 (TestDB)")
        # 3단계: users 테이블 생성용 버튼
        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)
        # 4단계에서 사용할 사용자 이름과 비밀번호 입력란이다.
        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)

        # users 테이블에 데이터를 추가하는 버튼이다.
        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.result_label = QLabel("결과 메시지가 여기에 표시됩니다.")

        # 메인 레이아웃에 위쪽 그룹과 아래쪽 그룹을 차례로 배치한다.
        main_layout = QVBoxLayout(self)
        main_layout.addWidget(conn_group)
        main_layout.addWidget(insert_group)
        main_layout.addWidget(self.result_label)

 

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

더보기
        # 4단계: users 테이블에 데이터 INSERT
        self.btn_insert_user.clicked.connect(self.insert_user)

 

6. <Data 삽입> 주요 구조

더보기

6.1 <Data 삽입> 핵심 구조 요약

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

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

# 3. SQL 실행
cursor = conn.cursor()
cursor.execute(INSERT_SQL, VALUES)
conn.commit()

# 4. 종료
conn.close()

 

 

6.2 <Data 삽입> 흐름

import mysql.connector as mc


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

    # 2. INSERT에 사용할 데이터 준비 (GUI 입력값에 해당)
    username = "sample_user"
    user_password = "1234"

    # 3. INSERT SQL 준비 (값은 %s 플레이스홀더 사용)
    insert_sql = "INSERT INTO users (username, password) VALUES (%s, %s)"
    values = (username, user_password)

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

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

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

        # 7. INSERT SQL 실행
        cursor.execute(insert_sql, values)

        # 8. 데이터 변경이므로 반드시 커밋 수행
        conn.commit()

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


# users 테이블 데이터 삽입 흐름 실행
users_insert_flow()

 

  • DB 접속 정보 준비
  • 삽입할 데이터 준비
  • INSERT SQL 작성
  • 데이터베이스까지 포함하여 연결
  • 연결 상태 확인
  • 커서 생성
  • INSERT SQL 실행
  • commit으로 데이터 저장 확정
  • 연결 종료

 

 

7. 기능 구현 소스코드

더보기

7.1 연결 테스트 함수

    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

        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.is_connected():
                    conn.close()
            except Exception:
                pass

이 함수에서 비밀번호를 GUI에서 받아오기 때문에
root에 비밀번호가 있으면 반드시 여기 입력해야 한다.

 

 

 

 

7.2 INSERT 함수에서의 개선

    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

        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.is_connected():
                    conn.close()
            except Exception:
                pass

 

 

 

 

7.3 1045 오류 처리

    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}")

 

8. 실행 테스트

더보기

단계별 버튼 실행 순서

  • 호스트, 사용자, 비밀번호, 데이터베이스 이름(pyqtdb 등)을 입력한다.
  • 연결 테스트 버튼을 눌러 1045 오류가 없는지 먼저 확인한다.
  • 데이터베이스 생성 버튼으로 DB를 만든다.
    이미 있으면 오류가 날 수 있으나 로그로 남겨도 좋다.
  • users 테이블 생성 버튼으로 테이블을 만든다.
  • 아래쪽 사용자인풋 영역에서 username, password를 입력한 뒤
    users 테이블에 사용자 추가 버튼을 누른다.


9.학습 주요 포인트

더보기
  • 1045 Access denied 오류는
    거의 항상 계정이나 비밀번호, 권한 문제다.

  • 접속 정보(host, user, password, db_name)를
    코드에 하드코딩하지 않고,
    하나의 UI에서 공통으로 입력받아
    연결, DB 생성, 테이블 생성, INSERT에서 모두 재사용하는 구조

  • 이전 단계에서 만든 기능을
    새로운 단계의 코드에 흡수·통합하여 프로젝트가 확장

  • 파라미터 쿼리 사용, QTextStream 기반 로그 기록, QMessageBox 피드백은 그대로 유지하면서
    접속 정보 부분만 개선해 현재 환경에 맞게 동작하도록 만들었다.

  • 지금 단계까지 완료되면,
    이후 QTableWidget을 사용한 조회, WHERE 검색, 로그인 기능 구현도
    같은 UI 구조를 그대로 재사용하면서 확장할 수 있다.

 

단계별 완성 파일