학습 로그/c# c++

D+66[C+MySQl] 회원가입/로그인을 MySQL서버와 연동하기 과제

goodjob^^ 2026. 7. 12. 23:36

ㅇ 일 자 : 2026년 7월 12일(일) 
ㅇ 주 제 :  회원가입 / 로그인을 MySQL서버와 연동하기 학습

 

1. 구현할 내용

PySide6 GUI
QTabWidget(회원가입 / 로그인)
MySQL Server 연동(mysql-connector-python)
회원가입
로그인
아이디 중복 확인
이메일 @ 검사
Parameter Binding(%s)
try / except / finally
conn=None 사용
문자열 직접 SQL 조합 금지
 
1.1. 구현 조건
회원가입과 로그인으로 구성된 PySide6 프로그램 작성
DB 연동해서 회원가입, 로그인 구현하기
로그인 탭은 아이디, 비밀번호 일치 여부에 따라 성공, 실패를 표시하기
server는 MySQL 서버 사용

1.1.1.필수 구현 조건
QTabWidget으로 회원가입 탭과 로그인 탭을 분리 구현
회원가입시 아이디, 이메일, 비밀번호 모두 입력받기
SQL 값은 %s 파리미터 바인딩으로 전달
문자열 직접 조합은 금지
DB연결은 CONN=None + try/except/finally 구조 사용
아이디, 이메일, 비밀번호 중 하나라도 비어 있으면 처리후 저장 차단
이메일 형식은 최소한 "@" 포함 여부만 확인 (정구식 검증은 선택)
 

2. 설치 라이브러리

pip install PySide6
pip install mysql-connector-python

 

2.1.  2방안 구현 내용 (화면, 사진)  

로그인 화면

 

서버에서 사용가능한 아이디 확인 알림

 

회원가입이 완료되었음을 알림

 

비밀번호 찾기를 위한 보인 확인 화면

 

비밀번호 변경후 알림 화면

 

회원정보 수정을 위한 인증 화면

 

회원정보 수정후 완료 알림 화면

 

3. Ⅰ방안

3.1. MySQL DB 생성

//PyqtDB
// pyqtdb라는 이름의 데이터베이스를 생성하며, 이모지 저장 등이 가능한 utf8mb4 문자셋을 설정
CREATE DATABASE pyqtdb CHARACTER SET utf8mb4; 

// 이후 진행할 모든 작업(테이블 생성 등)을 pyqtdb 데이터베이스 안에서 수행하도록 지정
USE pyqtdb; 

// 사용자 정보를 저장할 users 테이블을 생성
CREATE TABLE users( 
   // 각 행을 식별하는 고유 ID. 숫자가 자동으로 1씩 증가하며 기본키(PRIMARY KEY)로 설정
    id INT AUTO_INCREMENT PRIMARY KEY, 
    
   // 사용자 아이디를 저장, 최대 30자까지 입력 가능하며 중복될 수 없고(UNIQUE) 빈 값도 허용하지 않음(NOT NULL).
    username VARCHAR(30) UNIQUE NOT NULL, 
    
  // 이메일 주소를 저장. 최대 100자까지 입력 가능하며 빈 값을 허용하지 않음
    email VARCHAR(100) NOT NULL, 
    
  // 암호화된 비밀번호를 저장. 최대 100자까지 입력 가능하며 빈 값을 허용하지 않음
    password VARCHAR(100) NOT NULL 
);

 
3.2. mysql_membership.py

import sys  # 시스템 관련 파라미터와 함수를 사용하기 위해 임포트
import mysql.connector as mc  # MySQL 데이터베이스 연결을 위한 라이브러리 임포트
# PySide6 GUI 위젯 및 레이아웃 클래스들을 임포트
from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QFormLayout,
    QMessageBox,
    QTabWidget
)


class MainWindow(QWidget):  # QWidget을 상속받는 메인 윈도우 클래스 정의

    def __init__(self):  # 클래스 생성자 정의
        super().__init__()  # 부모 클래스(QWidget)의 생성자 호출

        self.setWindowTitle("회원가입 / 로그인")  # 윈도우 창 제목 설정

        tabs = QTabWidget()  # 탭 전환을 위한 QTabWidget 객체 생성

        tabs.addTab(self.build_signup_tab(), "회원가입")  # 회원가입 탭 추가
        tabs.addTab(self.build_login_tab(), "로그인")  # 로그인 탭 추가

        layout = QVBoxLayout(self)  # 메인 윈도우용 수직 레이아웃 생성
        layout.addWidget(tabs)  # 레이아웃에 탭 위젯 추가


    # 회원가입 화면
    # ===============

    def build_signup_tab(self):  # 회원가입 탭 화면을 구성하는 메서드

        tab = QWidget()  # 탭으로 사용할 위젯 객체 생성

        # 입력 필드(텍스트 박스) 생성
        self.signup_username_edit = QLineEdit()
        self.signup_email_edit = QLineEdit()
        self.signup_password_edit = QLineEdit()

        self.signup_password_edit.setEchoMode(QLineEdit.Password)  # 비밀번호 입력 시 문자 숨김 처리

        form = QFormLayout()  # 라벨과 입력 필드를 나란히 배치하는 폼 레이아웃 생성

        # 폼 레이아웃에 행 추가
        form.addRow("아이디", self.signup_username_edit)
        form.addRow("이메일", self.signup_email_edit)
        form.addRow("비밀번호", self.signup_password_edit)

        btn_signup = QPushButton("가입하기")  # 회원가입 버튼 생성

        self.signup_result_label = QLabel("")  # 결과 메시지를 표시할 라벨 생성

        layout = QVBoxLayout(tab)  # 탭 위젯용 수직 레이아웃 생성

        # 레이아웃에 위젯들 배치
        layout.addLayout(form)
        layout.addWidget(btn_signup)
        layout.addWidget(self.signup_result_label)

        btn_signup.clicked.connect(self.signup)  # 버튼 클릭 시 signup 메서드 실행되도록 연결

        return tab  # 구성된 탭 위젯 반환


    # 로그인 화면
    # ===============

    def build_login_tab(self):  # 로그인 탭 화면을 구성하는 메서드

        tab = QWidget()  # 탭으로 사용할 위젯 객체 생성

        # 입력 필드 생성
        self.login_username_edit = QLineEdit()
        self.login_password_edit = QLineEdit()

        self.login_password_edit.setEchoMode(QLineEdit.Password)  # 비밀번호 입력 시 문자 숨김 처리

        form = QFormLayout()  # 폼 레이아웃 생성

        # 폼 레이아웃에 행 추가
        form.addRow("아이디", self.login_username_edit)
        form.addRow("비밀번호", self.login_password_edit)

        btn_login = QPushButton("로그인")  # 로그인 버튼 생성

        self.login_result_label = QLabel("")  # 결과 메시지를 표시할 라벨 생성

        layout = QVBoxLayout(tab)  # 탭 위젯용 수직 레이아웃 생성

        # 레이아웃에 위젯들 배치
        layout.addLayout(form)
        layout.addWidget(btn_login)
        layout.addWidget(self.login_result_label)

        btn_login.clicked.connect(self.login)  # 버튼 클릭 시 login 메서드 실행되도록 연결

        return tab  # 구성된 탭 위젯 반환


    # 회원가입 로직
    # ===========

    def signup(self):  # 회원가입 로직 처리 메서드

        # 입력 필드에서 텍스트 추출 및 앞뒤 공백 제거
        username = self.signup_username_edit.text().strip()
        email = self.signup_email_edit.text().strip()
        password = self.signup_password_edit.text()

        # 빈칸 검사
        if not username or not email or not password:
            QMessageBox.warning(
                self,
                "입력 오류",
                "아이디, 이메일, 비밀번호를 모두 입력하세요."
            )
            return

        # 이메일 형식 검사
        if "@" not in email:
            QMessageBox.warning(
                self,
                "이메일 오류",
                "올바른 이메일 형식이 아닙니다."
            )
            return

        conn = None

        try:
            # MySQL 데이터베이스 연결 시도
            conn = mc.connect(
                host="localhost",
                user="root",
                password="비밀번호",  # 실제 DB 비밀번호로 수정 필요
                database="pyqtdb"
            )

            cursor = conn.cursor()

            # 1. 아이디 중복 확인
            sql_check = """
            SELECT id FROM users WHERE username=%s
            """
            cursor.execute(sql_check, (username,))

            if cursor.fetchone():
                self.signup_result_label.setText("이미 존재하는 아이디입니다.")
                return

            # 2. 회원 저장 (에러 발생 구문 수정 및 추가)
            sql_insert = """
            INSERT INTO users (username, email, password) 
            VALUES (%s, %s, %s)
            """
            cursor.execute(sql_insert, (username, email, password))
            conn.commit()  # 데이터베이스에 최종 반영

            self.signup_result_label.setText("회원가입이 완료되었습니다!")
            
            # 가입 성공 후 입력 필드 초기화
            self.signup_username_edit.clear()
            self.signup_email_edit.clear()
            self.signup_password_edit.clear()

        except mc.Error as e:
            # 데이터베이스 관련 오류 예외 처리
            QMessageBox.critical(self, "DB 오류", f"데이터베이스 에러 발생: {e}")
            self.signup_result_label.setText("회원가입 실패")
            
        finally:
            # 에러 발생 여부와 상관없이 연결 닫기
            if conn and conn.is_connected():
                cursor.close()
                conn.close()


    # 로그인 로직
    # ===========

    def login(self):  # 로그인 로직 처리 메서드
        username = self.login_username_edit.text().strip()
        password = self.login_password_edit.text()

        if not username or not password:
            QMessageBox.warning(self, "입력 오류", "아이디와 비밀번호를 모두 입력하세요.")
            return

        conn = None

        try:
            conn = mc.connect(
                host="localhost",
                user="root",
                password="비밀번호",  # 실제 DB 비밀번호로 수정 필요
                database="pyqtdb"
            )
            cursor = conn.cursor()

            # 아이디와 비밀번호가 일치하는지 확인
            sql = """
            SELECT id FROM users WHERE username=%s AND password=%s
            """
            cursor.execute(sql, (username, password))
            user = cursor.fetchone()

            if user:
                self.login_result_label.setText("로그인 성공!")
                QMessageBox.information(self, "성공", f"{username}님, 환영합니다!")
            else:
                self.login_result_label.setText("아이디 또는 비밀번호가 틀렸습니다.")

        except mc.Error as e:
            QMessageBox.critical(self, "DB 오류", f"데이터베이스 에러 발생: {e}")
            self.login_result_label.setText("로그인 실패")
            
        finally:
            if conn and conn.is_connected():
                cursor.close()
                conn.close()


# 프로그램 실행부
# =============
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

  
 

4. Ⅱ 방안       

4.1. SQLProject / requirements.txt     설명서(실행 메뉴얼)        * LLM응용

# SQLProject — PySide6 + MySQL 회원관리 프로그램

# 회원가입/로그인을 MySQL 서버와 연동하는 기본 GUI 프로그램

### 프로젝트 구조
```
SQLProject/
├── main.py              # 실행 진입점 (PySide6 GUI)
├── db.py                 # MySQL 연동 함수 (회원가입/로그인/중복확인)
├── config.py              # DB 접속 정보
├── requirements.txt
├── sql/
│   └── create_table.sql   # DB / 테이블 생성 스크립트
└── README.md
```
### 1. 준비 (MySQL)
1. MySQL 서버가 로컬(또는 원격)에 설치되어 실행 중
2. MySQL Workbench 또는 CLI에서 `sql/create_table.sql`을 실행하여
   데이터베이스(`sql_project_db`)와 `users` 테이블을 생성

터미널창:$  mysql -u root -p < sql/create_table.sql

## 2. config.py 설정
`config.py`를 열어 본인 MySQL 환경에 맞게 수정

```python
DB_CONFIG = {
    "host": "localhost",
    "port": 3306,
    "user": "wolf",
    "password": "본인비번_1234",
    "database": "sql_project_db"
}
```

## 3. PyCharm에서 실행하는 방법
1. PyCharm에서 `SQLProject` 폴더를 **Open** 하여 프로젝트로 열기
2. `File > Settings > Project: SQLProject > Python Interpreter`에서
   가상환경(venv)을 새로 만들거나 기존 인터프리터를 선택
3. 터미널(PyCharm 하단 Terminal 탭)에서 아래 명령으로 패키지를 설치
>>>  pip install -r requirements.txt

4. `main.py`를 우클릭 → Run 'main' 실행
   (또는 main.py를 연 상태에서 Shift+F10 / 실행 버튼 클릭)

## 4. 기능 설명
회원가입: 아이디/비밀번호/비밀번호 확인/이메일/이름 입력 후 가입 
아이디 중복 확인 : 가입 전 `SELECT COUNT(*) ... WHERE user_id = %s` 로 확인, 통과해야 가입 버튼 동작 
이메일 형식 검사 : `@`가 정확히 1개 포함되고 도메인에 `.`이 있는지 검사 
로그인 : 아이디로 조회 후 SHA-256 해시된 비밀번호 비교 
비밀번호 저장 : 평문 저장 대신 SHA-256 해시로 저장 (`hashlib`) 
비밀번호 찾기 : 아이디+이메일 일치 여부로 본인 확인 후, 통과 시에만 새 비밀번호 입력란 활성화 
회원정보 수정 : 아이디+현재 비밀번호로 로그인 인증 후, 기존 이메일/이름을 불러와 수정 (비밀번호는 입력 시에만 변경) 

### 탭 구성 (총 4개)
1. 회원가입
2. 로그인
3. 비밀번호 찾기 — 2단계(본인 확인 → 재설정) 구조, 확인 전에는 새 비밀번호 입력란이 비활성화됨
4. 회원정보 수정 — 2단계(로그인 인증 → 수정) 구조, 인증 전에는 수정 입력란이 비활성화됨

## 5. 구현 규칙 (요구사항 반영)
- 모든 DB 함수는 `conn = None` 으로 시작한 뒤 `try / except / finally` 구조 사용
- `finally` 블록에서 `cursor.close()`, `conn.close()`를 반드시 수행하여 커넥션 누수를 방지
- 모든 SQL은 **Parameter Binding(`%s`)** 사용, f-string이나 `+` 연산으로 SQL을 직접 조합하지 않음
  (SQL Injection 방지)
- GUI(`main.py`)와 DB 로직(`db.py`)을 분리하여 유지보수성 확보

## 6. 문제 해결 (Troubleshooting)
- `Access denied for user` 오류: `config.py`의 `user`/`password` 값 확인하기
- `Unknown database 'sql_project_db'` 오류: `sql/create_table.sql`을 먼저 실행했는지 확인하기
- `ModuleNotFoundError: No module named 'PySide6'`: `pip install -r requirements.txt`를 실행했는지 확인하기

 
4.2. SQLProject / main.py   // 실행 진입점 (PySide6 GUI)

# main.py
# PySide6 기반 회원관리 프로그램 (회원가입 / 로그인)
# PyCharm에서 바로 실행 가능하도록 작성됨 (진입점: main.py)

import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QTabWidget, QMainWindow,
    QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,
    QFormLayout, QMessageBox
)
from PySide6.QtCore import Qt

import db


class SignupTab(QWidget):
    """회원가입 탭"""

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_pw = QLineEdit()
        self.input_pw.setEchoMode(QLineEdit.Password)
        self.input_pw_confirm = QLineEdit()
        self.input_pw_confirm.setEchoMode(QLineEdit.Password)
        self.input_email = QLineEdit()
        self.input_name = QLineEdit()

        self.btn_check_id = QPushButton("아이디 중복 확인")
        self.btn_check_id.clicked.connect(self.check_duplicate_id)

        self.btn_signup = QPushButton("회원가입")
        self.btn_signup.clicked.connect(self.handle_signup)

        # 중복 확인을 통과했는지 여부를 추적하는 플래그
        self.id_checked = False

        id_row = QHBoxLayout()
        id_row.addWidget(self.input_id)
        id_row.addWidget(self.btn_check_id)

        form = QFormLayout()
        form.addRow("아이디", id_row)
        form.addRow("비밀번호", self.input_pw)
        form.addRow("비밀번호 확인", self.input_pw_confirm)
        form.addRow("이메일", self.input_email)
        form.addRow("이름", self.input_name)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_signup)
        layout.addStretch()
        self.setLayout(layout)

        # 아이디를 수정하면 다시 중복 확인을 받도록 초기화
        self.input_id.textChanged.connect(self.reset_id_check)

    def reset_id_check(self):
        self.id_checked = False

    def check_duplicate_id(self):
        user_id = self.input_id.text().strip()
        if not user_id:
            QMessageBox.warning(self, "확인", "아이디를 입력해 주세요.")
            return

        is_dup = db.is_id_duplicate(user_id)
        if is_dup:
            QMessageBox.warning(self, "중복 확인", "이미 사용 중인 아이디입니다.")
            self.id_checked = False
        else:
            QMessageBox.information(self, "중복 확인", "사용 가능한 아이디입니다.")
            self.id_checked = True

    def is_valid_email(self, email: str) -> bool:
        """
        이메일 형식 검사 (요구사항: @ 포함 여부 검사)
        - '@'가 정확히 1개 포함되어 있어야 함
        - '@' 앞뒤로 문자열이 존재해야 함
        """
        if email.count("@") != 1:
            return False
        local_part, domain_part = email.split("@")
        if not local_part or not domain_part:
            return False
        if "." not in domain_part:
            return False
        return True

    def handle_signup(self):
        user_id = self.input_id.text().strip()
        password = self.input_pw.text()
        password_confirm = self.input_pw_confirm.text()
        email = self.input_email.text().strip()
        name = self.input_name.text().strip()

        # 1) 빈 값 검사
        if not (user_id and password and password_confirm and email and name):
            QMessageBox.warning(self, "입력 오류", "모든 항목을 입력해 주세요.")
            return

        # 2) 아이디 중복 확인 여부
        if not self.id_checked:
            QMessageBox.warning(self, "확인 필요", "아이디 중복 확인을 먼저 진행해 주세요.")
            return

        # 3) 비밀번호 일치 확인
        if password != password_confirm:
            QMessageBox.warning(self, "입력 오류", "비밀번호가 일치하지 않습니다.")
            return

        # 4) 이메일 @ 형식 검사
        if not self.is_valid_email(email):
            QMessageBox.warning(self, "입력 오류", "올바른 이메일 형식이 아닙니다. (예: user@example.com)")
            return

        # 5) DB에 회원가입 요청
        success, message = db.register_user(user_id, password, email, name)
        if success:
            QMessageBox.information(self, "회원가입", message)
            self.clear_form()
        else:
            QMessageBox.critical(self, "회원가입 실패", message)

    def clear_form(self):
        self.input_id.clear()
        self.input_pw.clear()
        self.input_pw_confirm.clear()
        self.input_email.clear()
        self.input_name.clear()
        self.id_checked = False


class LoginTab(QWidget):
    """로그인 탭"""

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_pw = QLineEdit()
        self.input_pw.setEchoMode(QLineEdit.Password)

        self.btn_login = QPushButton("로그인")
        self.btn_login.clicked.connect(self.handle_login)

        # 엔터키로도 로그인되도록 설정
        self.input_pw.returnPressed.connect(self.handle_login)

        form = QFormLayout()
        form.addRow("아이디", self.input_id)
        form.addRow("비밀번호", self.input_pw)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_login)
        layout.addStretch()
        self.setLayout(layout)

    def handle_login(self):
        user_id = self.input_id.text().strip()
        password = self.input_pw.text()

        if not user_id or not password:
            QMessageBox.warning(self, "입력 오류", "아이디와 비밀번호를 입력해 주세요.")
            return

        success, message = db.login_user(user_id, password)
        if success:
            QMessageBox.information(self, "로그인 성공", message)
        else:
            QMessageBox.critical(self, "로그인 실패", message)


class ResetPasswordTab(QWidget):
    """비밀번호 찾기(재설정) 탭
    1단계: 아이디 + 이메일로 본인 확인
    2단계: 확인 통과 시 새 비밀번호 입력란이 활성화되어 재설정 가능
    """

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_email = QLineEdit()

        self.btn_verify = QPushButton("본인 확인")
        self.btn_verify.clicked.connect(self.handle_verify)

        self.input_new_pw = QLineEdit()
        self.input_new_pw.setEchoMode(QLineEdit.Password)
        self.input_new_pw_confirm = QLineEdit()
        self.input_new_pw_confirm.setEchoMode(QLineEdit.Password)

        self.btn_reset = QPushButton("비밀번호 재설정")
        self.btn_reset.clicked.connect(self.handle_reset)

        self.verified = False
        self.set_reset_enabled(False)

        form = QFormLayout()
        form.addRow("아이디", self.input_id)
        form.addRow("이메일", self.input_email)

        form2 = QFormLayout()
        form2.addRow("새 비밀번호", self.input_new_pw)
        form2.addRow("새 비밀번호 확인", self.input_new_pw_confirm)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_verify)
        layout.addWidget(QLabel("― 본인 확인 후 아래에서 새 비밀번호를 설정하세요 ―"))
        layout.addLayout(form2)
        layout.addWidget(self.btn_reset)
        layout.addStretch()
        self.setLayout(layout)

        self.input_id.textChanged.connect(self.reset_verification)
        self.input_email.textChanged.connect(self.reset_verification)

    def set_reset_enabled(self, enabled: bool):
        self.input_new_pw.setEnabled(enabled)
        self.input_new_pw_confirm.setEnabled(enabled)
        self.btn_reset.setEnabled(enabled)

    def reset_verification(self):
        self.verified = False
        self.set_reset_enabled(False)

    def handle_verify(self):
        user_id = self.input_id.text().strip()
        email = self.input_email.text().strip()

        if not user_id or not email:
            QMessageBox.warning(self, "입력 오류", "아이디와 이메일을 모두 입력해 주세요.")
            return

        ok = db.verify_id_email(user_id, email)
        if ok:
            QMessageBox.information(self, "본인 확인", "본인 확인이 완료되었습니다. 새 비밀번호를 입력해 주세요.")
            self.verified = True
            self.set_reset_enabled(True)
        else:
            QMessageBox.warning(self, "본인 확인 실패", "아이디와 이메일이 일치하지 않습니다.")
            self.verified = False
            self.set_reset_enabled(False)

    def handle_reset(self):
        if not self.verified:
            QMessageBox.warning(self, "확인 필요", "먼저 본인 확인을 진행해 주세요.")
            return

        user_id = self.input_id.text().strip()
        new_pw = self.input_new_pw.text()
        new_pw_confirm = self.input_new_pw_confirm.text()

        if not new_pw or not new_pw_confirm:
            QMessageBox.warning(self, "입력 오류", "새 비밀번호를 입력해 주세요.")
            return

        if new_pw != new_pw_confirm:
            QMessageBox.warning(self, "입력 오류", "새 비밀번호가 일치하지 않습니다.")
            return

        success, message = db.reset_password(user_id, new_pw)
        if success:
            QMessageBox.information(self, "비밀번호 재설정", message)
            self.clear_form()
        else:
            QMessageBox.critical(self, "재설정 실패", message)

    def clear_form(self):
        self.input_id.clear()
        self.input_email.clear()
        self.input_new_pw.clear()
        self.input_new_pw_confirm.clear()
        self.verified = False
        self.set_reset_enabled(False)


class EditProfileTab(QWidget):
    """회원정보 수정 탭
    1단계: 아이디 + 현재 비밀번호로 로그인 인증
    2단계: 인증 통과 시 이메일/이름/비밀번호(선택) 수정 가능
    """

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_current_pw = QLineEdit()
        self.input_current_pw.setEchoMode(QLineEdit.Password)

        self.btn_auth = QPushButton("본인 인증")
        self.btn_auth.clicked.connect(self.handle_auth)

        self.input_email = QLineEdit()
        self.input_name = QLineEdit()
        self.input_new_pw = QLineEdit()
        self.input_new_pw.setEchoMode(QLineEdit.Password)
        self.input_new_pw.setPlaceholderText("변경하지 않으려면 비워두세요")

        self.btn_update = QPushButton("정보 수정")
        self.btn_update.clicked.connect(self.handle_update)

        self.authenticated = False
        self.current_user_id = None
        self.set_edit_enabled(False)

        auth_form = QFormLayout()
        auth_form.addRow("아이디", self.input_id)
        auth_form.addRow("현재 비밀번호", self.input_current_pw)

        edit_form = QFormLayout()
        edit_form.addRow("이메일", self.input_email)
        edit_form.addRow("이름", self.input_name)
        edit_form.addRow("새 비밀번호", self.input_new_pw)

        layout = QVBoxLayout()
        layout.addLayout(auth_form)
        layout.addWidget(self.btn_auth)
        layout.addWidget(QLabel("― 본인 인증 후 아래 정보를 수정하세요 ―"))
        layout.addLayout(edit_form)
        layout.addWidget(self.btn_update)
        layout.addStretch()
        self.setLayout(layout)

        self.input_id.textChanged.connect(self.reset_auth)
        self.input_current_pw.textChanged.connect(self.reset_auth)

    def set_edit_enabled(self, enabled: bool):
        self.input_email.setEnabled(enabled)
        self.input_name.setEnabled(enabled)
        self.input_new_pw.setEnabled(enabled)
        self.btn_update.setEnabled(enabled)

    def reset_auth(self):
        self.authenticated = False
        self.current_user_id = None
        self.set_edit_enabled(False)
        self.input_email.clear()
        self.input_name.clear()

    def handle_auth(self):
        user_id = self.input_id.text().strip()
        password = self.input_current_pw.text()

        if not user_id or not password:
            QMessageBox.warning(self, "입력 오류", "아이디와 현재 비밀번호를 입력해 주세요.")
            return

        success, message = db.login_user(user_id, password)
        if not success:
            QMessageBox.critical(self, "인증 실패", message)
            self.reset_auth()
            return

        ok, result = db.get_user_info(user_id)
        if not ok:
            QMessageBox.critical(self, "조회 실패", result)
            return

        self.authenticated = True
        self.current_user_id = user_id
        self.input_email.setText(result["email"])
        self.input_name.setText(result["name"])
        self.set_edit_enabled(True)
        QMessageBox.information(self, "본인 인증", "인증되었습니다. 정보를 수정할 수 있습니다.")

    def is_valid_email(self, email: str) -> bool:
        if email.count("@") != 1:
            return False
        local_part, domain_part = email.split("@")
        if not local_part or not domain_part:
            return False
        if "." not in domain_part:
            return False
        return True

    def handle_update(self):
        if not self.authenticated or not self.current_user_id:
            QMessageBox.warning(self, "인증 필요", "먼저 본인 인증을 진행해 주세요.")
            return

        email = self.input_email.text().strip()
        name = self.input_name.text().strip()
        new_pw = self.input_new_pw.text()  # 비워두면 비밀번호는 변경하지 않음

        if not email or not name:
            QMessageBox.warning(self, "입력 오류", "이메일과 이름을 입력해 주세요.")
            return

        if not self.is_valid_email(email):
            QMessageBox.warning(self, "입력 오류", "올바른 이메일 형식이 아닙니다. (예: user@example.com)")
            return

        success, message = db.update_user_info(self.current_user_id, email, name, new_pw or None)
        if success:
            QMessageBox.information(self, "회원정보 수정", message)
            self.input_new_pw.clear()
        else:
            QMessageBox.critical(self, "수정 실패", message)


class MainWindow(QMainWindow):
    """메인 윈도우: QTabWidget으로 회원가입/로그인/비밀번호 찾기/회원정보 수정 탭 구성"""

    def __init__(self):
        super().__init__()
        self.setWindowTitle("SQLProject - 회원관리 시스템")
        self.setFixedSize(440, 420)

        tabs = QTabWidget()
        tabs.addTab(SignupTab(), "회원가입")
        tabs.addTab(LoginTab(), "로그인")
        tabs.addTab(ResetPasswordTab(), "비밀번호 찾기")
        tabs.addTab(EditProfileTab(), "회원정보 수정")

        self.setCentralWidget(tabs)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

 
4.3. SQLProject / db.py     // MySQL 연동 함수 (회원가입/로그인/중복확인)

# db.py
# MySQL 연동을 담당하는 모듈
# - mysql-connector-python 사용
# - 모든 SQL 실행 함수는 conn = None 으로 시작
# - try / except / finally 구조로 연결 예외 처리 및 반드시 close() 수행
# - SQL 인젝션 방지를 위해 문자열 직접 조합(f-string, + 연산 등) 금지
#   -> 반드시 %s Parameter Binding 사용

import hashlib
import mysql.connector
from mysql.connector import Error
from config import DB_CONFIG


def get_connection():
    """
    MySQL 서버에 연결하고 connection 객체를 반환한다.
    실패 시 None을 반환한다.
    """
    conn = None
    try:
        conn = mysql.connector.connect(**DB_CONFIG)
        return conn
    except Error as e:
        print(f"[DB 연결 오류] {e}")
        return None


def hash_password(password: str) -> str:
    """
    비밀번호를 SHA-256으로 해시하여 저장한다.
    평문 비밀번호를 DB에 그대로 저장하지 않기 위함이다.
    """
    return hashlib.sha256(password.encode("utf-8")).hexdigest()


def is_id_duplicate(user_id: str) -> bool:
    """
    아이디 중복 여부를 확인한다.
    이미 존재하면 True, 존재하지 않으면 False를 반환한다.
    오류 발생 시 안전하게 True를 반환하여 가입을 막는다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return True  # 연결 실패 시 안전하게 중복으로 처리

        cursor = conn.cursor()
        sql = "SELECT COUNT(*) FROM users WHERE user_id = %s"
        cursor.execute(sql, (user_id,))  # Parameter Binding
        (count,) = cursor.fetchone()
        return count > 0

    except Error as e:
        print(f"[중복 확인 오류] {e}")
        return True

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def register_user(user_id: str, password: str, email: str, name: str) -> tuple[bool, str]:
    """
    회원가입을 수행한다.
    성공 시 (True, "성공 메시지"), 실패 시 (False, "실패 사유")를 반환한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()
        sql = """
            INSERT INTO users (user_id, password, email, name)
            VALUES (%s, %s, %s, %s)
        """
        hashed_pw = hash_password(password)
        cursor.execute(sql, (user_id, hashed_pw, email, name))  # Parameter Binding
        conn.commit()
        return True, "회원가입이 완료되었습니다."

    except Error as e:
        # UNIQUE 제약 위반 등 DB 오류 처리
        if conn is not None:
            conn.rollback()
        print(f"[회원가입 오류] {e}")
        return False, f"회원가입 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def find_id_by_email(email: str) -> tuple[bool, str]:
    """
    이메일로 아이디를 찾는다. (아이디 찾기용 - 필요 시 사용)
    성공 시 (True, "user_id"), 실패 시 (False, "실패 사유")를 반환한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()
        sql = "SELECT user_id FROM users WHERE email = %s"
        cursor.execute(sql, (email,))  # Parameter Binding
        row = cursor.fetchone()

        if row is None:
            return False, "해당 이메일로 가입된 아이디가 없습니다."
        return True, row[0]

    except Error as e:
        print(f"[아이디 찾기 오류] {e}")
        return False, f"조회 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def verify_id_email(user_id: str, email: str) -> bool:
    """
    비밀번호 찾기(재설정) 전 본인 확인 단계.
    아이디와 이메일이 DB에 등록된 정보와 일치하면 True를 반환한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False

        cursor = conn.cursor()
        sql = "SELECT COUNT(*) FROM users WHERE user_id = %s AND email = %s"
        cursor.execute(sql, (user_id, email))  # Parameter Binding
        (count,) = cursor.fetchone()
        return count > 0

    except Error as e:
        print(f"[본인 확인 오류] {e}")
        return False

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def reset_password(user_id: str, new_password: str) -> tuple[bool, str]:
    """
    본인 확인(verify_id_email)을 통과한 사용자의 비밀번호를 재설정한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()
        sql = "UPDATE users SET password = %s WHERE user_id = %s"
        hashed_pw = hash_password(new_password)
        cursor.execute(sql, (hashed_pw, user_id))  # Parameter Binding
        conn.commit()

        if cursor.rowcount == 0:
            return False, "비밀번호 변경에 실패했습니다. 아이디를 확인해 주세요."
        return True, "비밀번호가 성공적으로 변경되었습니다."

    except Error as e:
        if conn is not None:
            conn.rollback()
        print(f"[비밀번호 재설정 오류] {e}")
        return False, f"비밀번호 재설정 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def get_user_info(user_id: str) -> tuple[bool, dict | str]:
    """
    회원정보 수정 화면에 기존 정보를 채워주기 위해 사용자 정보를 조회한다.
    성공 시 (True, {"email":..., "name":...}), 실패 시 (False, "실패 사유")를 반환한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()
        sql = "SELECT email, name FROM users WHERE user_id = %s"
        cursor.execute(sql, (user_id,))  # Parameter Binding
        row = cursor.fetchone()

        if row is None:
            return False, "존재하지 않는 아이디입니다."

        email, name = row
        return True, {"email": email, "name": name}

    except Error as e:
        print(f"[회원정보 조회 오류] {e}")
        return False, f"조회 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def update_user_info(user_id: str, email: str, name: str, new_password: str | None = None) -> tuple[bool, str]:
    """
    회원정보(이메일, 이름, [선택]비밀번호)를 수정한다.
    new_password가 None이거나 빈 문자열이면 비밀번호는 변경하지 않는다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()

        if new_password:
            sql = "UPDATE users SET email = %s, name = %s, password = %s WHERE user_id = %s"
            hashed_pw = hash_password(new_password)
            params = (email, name, hashed_pw, user_id)
        else:
            sql = "UPDATE users SET email = %s, name = %s WHERE user_id = %s"
            params = (email, name, user_id)

        cursor.execute(sql, params)  # Parameter Binding
        conn.commit()
        return True, "회원정보가 수정되었습니다."

    except Error as e:
        if conn is not None:
            conn.rollback()
        print(f"[회원정보 수정 오류] {e}")
        return False, f"회원정보 수정 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()


def login_user(user_id: str, password: str) -> tuple[bool, str]:
    """
    로그인을 수행한다.
    아이디/비밀번호가 일치하면 (True, "환영 메시지"),
    일치하지 않으면 (False, "실패 사유")를 반환한다.
    """
    conn = None
    cursor = None
    try:
        conn = get_connection()
        if conn is None:
            return False, "데이터베이스 서버에 연결할 수 없습니다."

        cursor = conn.cursor()
        sql = "SELECT password, name FROM users WHERE user_id = %s"
        cursor.execute(sql, (user_id,))  # Parameter Binding
        row = cursor.fetchone()

        if row is None:
            return False, "존재하지 않는 아이디입니다."

        stored_password, name = row
        hashed_input_pw = hash_password(password)

        if stored_password == hashed_input_pw:
            return True, f"{name}님, 환영합니다!"
        else:
            return False, "비밀번호가 일치하지 않습니다."

    except Error as e:
        print(f"[로그인 오류] {e}")
        return False, f"로그인 중 오류가 발생했습니다: {e}"

    finally:
        if cursor is not None:
            cursor.close()
        if conn is not None and conn.is_connected():
            conn.close()

 
4.4. SQLProject / config.py  //  # DB 접속 정보

# config.py
# MySQL 서버 접속 정보
# 본인 환경에 맞게 값을 수정해서 사용
"""
DB_CONFIG = {
    "host": "localhost",       # MySQL 서버 주소
    "port": 3306,               # MySQL 포트 (기본 3306)
    "user": "root",             # MySQL 사용자명
    "password": "1234",         # MySQL 비밀번호 (본인 환경에 맞게 수정)
    "database": "sql_project_db"  # 사용할 데이터베이스명
}
"""

DB_CONFIG = {
    "host": "127.0.0.1",       # MySQL 서버 주소
    "port": 3306,               # MySQL 포트 (기본 3306)
    "user": "root",             # MySQL 사용자명
    "password": "1234",         # MySQL 비밀번호 (본인 환경에 맞게 수정)
    "database": "sql_project_db"  # 사용할 데이터베이스명
}

 
4.5. SQLProject / sql / create_table.sql     // DB / 테이블 생성 스크립트

// create_table.sql
// SQLProject 회원관리용 데이터베이스 및 테이블 생성 스크립트

// sql_project_db 데이터베이스가 존재하지 않을 때만 새로 생성
// 데이터베이스 기본 문자셋은 이모지 저장이 가능한 utf8mb4로, 정렬 규칙은 utf8mb4_general_ci로 설정
CREATE DATABASE IF NOT EXISTS sql_project_db
    DEFAULT CHARACTER SET utf8mb4
    COLLATE utf8mb4_general_ci;

// 이후 진행할 작업(테이블 생성 등)을 위해 sql_project_db 데이터베이스를 선택
USE sql_project_db;

// users 테이블이 존재하지 않을 때만 회원 정보를 저장할 테이블을 생성
CREATE TABLE IF NOT EXISTS users (
    id          INT AUTO_INCREMENT PRIMARY KEY, // 고유 식별자 복수 일련번호 (자동 증가, 기본키)
    user_id     VARCHAR(50)  NOT NULL UNIQUE,  // 로그인 아이디 (중복 불가)
    password    VARCHAR(255) NOT NULL,         // 비밀번호 (해시 저장)
    email       VARCHAR(100) NOT NULL,         // 이메일
    name        VARCHAR(50)  NOT NULL,         // 이름
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP // 계정 생성 일시 (데이터 삽입 시 현재 시간 자동 입력)
);

MySQL creat 테이블 생성 화면
SQLProject.zip
0.01MB