로보테크AI

융합_로보테크 AI 자율주행 로봇 개발자 과정-26/02/23[PyQt]

steezer 2026. 2. 23. 17:08

PyQt
GUI 애플리케이션을 만들 때 사용하는 프레임워크
Qt 기반:  C++ GUI 프레임워크인 Qt를 파이썬에서 사용할 수 있도록 만든 것이 PyQt
크로스 플랫폼 지원: Windows, macOS, Linux 등 다양한 운영체제에서 같은 코드로 실행할 수 있음
풍부한 위젯 제공: 버튼, 텍스트 상자, 표, 그래프 등 수많은 기본 위젯을 제공하며, 이를 조합해 다양한 형태의 프로그램을 만들 수 있음

 

VSC 사용

 

PyQt5 설치

pip install PyQt5

 

Qt Designer

PyQt를 이용하여 GUI프로그래밍을 할 때 손쉽게 프로그램의 레이아웃을 편집할 수 있도록 해주는 편집기

pip install PyQt5-tools

 

가상환경 생성

 

파이썬 3.9 설치

 

 

import sys
from PyQt5.QtWidgets import *
from PyQt5 import uic

#UI파일 연결
form_class = uic.loadUiType("sample.ui")[0]

#화면을 띄우는데 사용되는 클래스 선언
class Window(QMainWindow, form_class) :
    def __init__(self) :
        super().__init__()
        self.setupUi(self)

if __name__ == "__main__" :
    #QApplication : 프로그램을 실행시켜주는 클래스
    app = QApplication(sys.argv) 

    #WindowClass의 인스턴스 생성
    myWindow = Window() 

    #프로그램 화면을 보여주는 코드
    myWindow.show()

    #프로그램을 이벤트루프로 진입시키는(프로그램을 작동시키는) 코드
    app.exec_()

 

기본 위젯과 레이아웃

위젯: 버튼, 텍스트 입력창, 라벨 등

레이아웃: 위젯들을 화면에 배치하는 방법을 정의하는 도구

 

QLabel: 텍스트나 이미지를 화면에 표시할 때 사용하는 가장 기본적인 위젯, 단순 정보 보여주기 용

setText("텍스트") 라벨에 표시할 텍스트 변경
setPixmap(QPixmap("이미지.png")) 이미지를 표시
setAlignment(Qt.AlignCenter) 텍스트/이미지 정렬 설정

 

 

QPushButton: 사용자가 클릭했을 때 특정 동작을 수행하도록 연결할 수 있음, 메뉴 실행, 저장, 닫기 등의 액션에 활용

setText("버튼명") 버튼의 텍스트 변경
setEnabled(False) 버튼 비활성화
clicked.connect(func) 버튼 클릭 시 실행될 함수 연결

 

QLineEdit: 한 줄짜리 텍스트 입력 필드(로그인 아이디, 검색창, 숫자 입력 등에 활용)

setText("텍스트") 초기 텍스트 설정
text() 입력된 문자열 반환
setPlaceholderText("힌트") 입력 전 표시되는 안내 문구
clear() 입력된 내용 삭제

 

QTextEdit: 여러 줄 입력이 가능한 텍스트 영역( 메모장, 채팅창, 로그 기록 출력 등에서 자주 사용)

setPlainText("텍스트") 기본 문자열 입력
setHtml("<h1>제목</h1>") HTML 형식 문자열 입력
toPlainText() 입력된 텍스트 가져오기
clear() 내용 삭제

 

import sys
from PyQt5.QtWidgets import *
from PyQt5 import uic

form_class = uic.loadUiType("sample.ui")[0]

class Window(QMainWindow, form_class):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.plainTextEdit.setPlainText("여기에 글을 입력해 보세요!")
        self.btnClear.clicked.connect(self.clearText)

    def clearText(self):
        self.plainTextEdit.clear()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

레이아웃 구성 방법

레이아웃 클래스 사용

 

QVBoxLayout: 수직 배치 방식을 제공하는 레이아웃 클래스로, 위젯을 위에서 아래로 차례대로 배치

import sys
from PyQt5.QtWidgets import *

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        window = QWidget() # 화면 위젯
        layout = QVBoxLayout()
        layout.addWidget(QPushButton("버튼 1"))
        layout.addWidget(QPushButton("버튼 2"))
        layout.addWidget(QPushButton("버튼 3"))

        window.setLayout(layout) # 정의한 레이아웃을 화면 위젯에 적용하기
        self.setCentralWidget(window) # 정의한 화면을 창에다가 적용하기 

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

QHBoxLayout: 수평 배치 방식을 제공하는 레이아웃 클래스로, 위젯을 왼쪽에서 오른쪽으로 차례대로 배치

import sys
from PyQt5.QtWidgets import *

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        window = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(QPushButton("버튼 1"))
        layout.addWidget(QPushButton("버튼 2"))
        layout.addWidget(QPushButton("버튼 3"))

        window.setLayout(layout)
        self.setCentralWidget(window)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

QGridLayout: 격자 배치 방식을 제공, 위젯을 행(row), 열(column) 단위로 배치할 수 있어 계산기 같은 UI를 만들 때 유용

import sys
from PyQt5.QtWidgets import *

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        window = QWidget()
        
        layout = QGridLayout()
        layout.addWidget(QPushButton("(0,0)"), 0, 0)
        layout.addWidget(QPushButton("(0,1)"), 0, 1)
        layout.addWidget(QPushButton("(1,0)"), 1, 0)
        layout.addWidget(QPushButton("(1,1)"), 1, 1)

        window.setLayout(layout)
        self.setCentralWidget(window)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

시그널과 슬롯

시그널(Signal): 특정 이벤트 발생을 알림
슬롯(Slot): 시그널 발생 시 실행되는 함수

 

위젯 시그널 설명
QPushButton clicked 버튼이 클릭될 때 발생
pressed 버튼이 눌러지는 순간 발생
released 버튼이 눌렸다가 떼어질 때 발생
QLineEdit textChanged(str) 입력 내용이 바뀔 때마다 발생
returnPressed Enter(리턴) 키 입력 시 발생
editingFinished 입력 후 focus가 다른 곳으로 이동할 때 발생
QTextEdit textChanged 텍스트 내용이 바뀔 때 발생
cursorPositionChanged 커서 위치가 바뀔 때 발생
QCheckBox stateChanged(int) 체크박스 상태(체크/해제)가 변경될 때 발생
toggled(bool) 토글 상태가 바뀔 때 발생
QComboBox currentIndexChanged(int) 선택된 항목의 인덱스가 바뀔 때 발생
currentTextChanged(str) 선택된 항목의 텍스트가 바뀔 때 발생
QSlider valueChanged(int) 슬라이더 값이 변경될 때 발생
sliderPressed 슬라이더가 잡히는 순간 발생
sliderReleased 슬라이더가 놓일 때 발생

 

import sys
import random
from PyQt5.QtWidgets import *
from PyQt5 import uic

form_class = uic.loadUiType("sample.ui")[0]

class Window(QMainWindow, form_class):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        self.lineEdit.setPlaceholderText("여기에 입력하세요")
        self.lineEdit.returnPressed.connect(self.changeBackgroundColor)

    def changeBackgroundColor(self):
        r = random.randint(0, 255)
        g = random.randint(0, 255)
        b = random.randint(0, 255)
        self.lineEdit.setStyleSheet(f"background-color: rgb({r}, {g}, {b});")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QPushButton,
    QLineEdit, QTextEdit, QCheckBox, QComboBox,
    QSlider, QLabel
)
from PyQt5.QtCore import Qt


class SignalTestWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt5 Signal Test")
        self.resize(500, 600)

        layout = QVBoxLayout()

        # ===== QPushButton =====
        layout.addWidget(QLabel("QPushButton"))
        self.btn = QPushButton("버튼 테스트")
        layout.addWidget(self.btn)

        self.btn.clicked.connect(lambda: self.log("QPushButton → clicked"))
        self.btn.pressed.connect(lambda: self.log("QPushButton → pressed"))
        self.btn.released.connect(lambda: self.log("QPushButton → released"))

        # ===== QLineEdit =====
        layout.addWidget(QLabel("QLineEdit"))
        self.lineEdit = QLineEdit()
        layout.addWidget(self.lineEdit)

        self.lineEdit.textChanged.connect(
            lambda text: self.log(f"QLineEdit → textChanged: {text}")
        )
        self.lineEdit.returnPressed.connect(
            lambda: self.log("QLineEdit → returnPressed")
        )
        self.lineEdit.editingFinished.connect(
            lambda: self.log("QLineEdit → editingFinished")
        )

        # ===== QTextEdit =====
        layout.addWidget(QLabel("QTextEdit"))
        self.textEdit = QTextEdit()
        layout.addWidget(self.textEdit)

        self.textEdit.textChanged.connect(
            lambda: self.log("QTextEdit → textChanged")
        )
        self.textEdit.cursorPositionChanged.connect(
            lambda: self.log("QTextEdit → cursorPositionChanged")
        )

        # ===== QCheckBox =====
        layout.addWidget(QLabel("QCheckBox"))
        self.checkBox = QCheckBox("체크박스 테스트")
        layout.addWidget(self.checkBox)

        self.checkBox.stateChanged.connect(
            lambda state: self.log(f"QCheckBox → stateChanged: {state}")
        )
        self.checkBox.toggled.connect(
            lambda checked: self.log(f"QCheckBox → toggled: {checked}")
        )

        # ===== QComboBox =====
        layout.addWidget(QLabel("QComboBox"))
        self.comboBox = QComboBox()
        self.comboBox.addItems(["항목 1", "항목 2", "항목 3"])
        layout.addWidget(self.comboBox)

        self.comboBox.currentIndexChanged.connect(
            lambda index: self.log(f"QComboBox → currentIndexChanged: {index}")
        )
        self.comboBox.currentTextChanged.connect(
            lambda text: self.log(f"QComboBox → currentTextChanged: {text}")
        )

        # ===== QSlider =====
        layout.addWidget(QLabel("QSlider"))
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, 100)
        layout.addWidget(self.slider)

        self.slider.valueChanged.connect(
            lambda value: self.log(f"QSlider → valueChanged: {value}")
        )
        self.slider.sliderPressed.connect(
            lambda: self.log("QSlider → sliderPressed")
        )
        self.slider.sliderReleased.connect(
            lambda: self.log("QSlider → sliderReleased")
        )

        # ===== 로그창 =====
        layout.addWidget(QLabel("Event Log"))
        self.logBox = QTextEdit()
        self.logBox.setReadOnly(True)
        layout.addWidget(self.logBox)

        self.setLayout(layout)

    def log(self, message):
        self.logBox.append(message)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SignalTestWindow()
    window.show()
    sys.exit(app.exec_())

 

데이터 표시하기

표를 만드는 클래스와 모델-뷰 구조

PyQt에서 데이터를 표 형식으로 표시할 때는 QTableWidget과 QTableView 두 가지 위젯을 사용

소규모 데이터를 셀에 직접 넣어가며 작업할 때는 QTableWidget

데이터를 외부 모델과 연결해 표시하고자 할 때는 QTableView

구분  QTableWidget  QTableView
특징 간단한 테이블 UI 제작에 사용 모델-뷰(Model-View) 구조 기반으로 동작
데이터 입력 셀에 직접 텍스트를 넣거나 수정 가능 외부 모델(리스트, DB 등)과 연결해 표시
활용도 소규모 데이터나 빠른 UI 테스트에 적합 대규모 데이터 처리 및 확장성 높은 애플리케이션에 적합

 

PyQt는 데이터를 표시할 때 Model-View 구조를 사용

Model (모델): 실제 데이터가 저장되는 곳 (리스트, DB, 파일 등)

View (뷰): 데이터를 화면에 보여주는 UI (QTableView, QListView 등)

Delegate (델리게이트, 선택사항): 데이터가 뷰에 표시되는 방식을 정의 (예: 셀 안에 체크박스 넣기)

 

QTableWidget은 내부적으로 간단한 모델을 내장하고 있어 별도의 모델 작성 없이 바로 데이터 입력이 가능

QTableView는 외부 모델을 연결해야 하며, 대표적으로 QStandardItemModel, QSqlTableModel 등을 사용

 

델리게이트

모델에 저장된 데이터를 어떻게 화면에 표시할지, 그리고 어떻게 편집할지를 담당

 

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QStandardItemModel, QStandardItem

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        window = QWidget()
        layout = QVBoxLayout()
        
        # 테이블 생성
        table_view = QTableView()

        # 모델 생성 (행, 열)
        model = QStandardItemModel()
        model.setHorizontalHeaderLabels(["이름", "가입 여부"])
        
        # 행, 열 너비를 창 너비에 맞게 늘리기
        table_view.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
        table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
                
        # 리스트 데이터 출력
        data_list = [
            ["홍길동", True],
            ["김철수", False],
            ["이영희", True]
        ]

        for name, joined in data_list:
            name_item = QStandardItem(name)
            check_item = QStandardItem()
            check_item.setCheckable(True)  # 체크박스로 표시
            check_item.setCheckState(2 if joined else 0)  # 2=Checked, 0=Unchecked
            model.appendRow([name_item, check_item])

        table_view.setModel(model)

        layout.addWidget(table_view)
        window.setLayout(layout)
        self.setCentralWidget(window)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

bool 형식으로 표현된 ‘가입 여부’라는 필드를 체크박스 형태로 화면에 표시하는데, 이를 가리켜 ‘체크박스 형태의 델리게이트’라고 함

 

PyQt에서 기본적으로 제공되는 델리게이트(delegate)는 QStyledItemDelegate

이 클래스는 아이템 데이터의 Qt.ItemDataRole과 QVariant 타입을 해석해서 자동으로 적절한 에디터(위젯)를 제공

별도의 델리게이트 클래스를 쓰지 않아도 데이터 타입이나 속성값에 따라 다른 편집기가 기본 제공

데이터 유형 / 속성 QStyledItemDelegate에서 제공되는 에디터 비고
일반 텍스트 (QString) QLineEdit 문자열 입력용
정수/실수 (int, float, double) QSpinBox / QDoubleSpinBox 숫자 범위 자동 적용 가능
불리언 (bool) QCheckBox 체크 상태 On/Off
열거형 (enum) QComboBox 선택형 데이터 표현
날짜/시간 (QDate, QTime, QDateTime) QDateEdit / QTimeEdit / QDateTimeEdit 달력 위젯 포함
아이콘/이미지 (QIcon, QPixmap) 기본적으로 표시만 지원, 직접 편집 불가 편집 원하면 커스텀 delegate 필요

 

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt


class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        window = QWidget()
        layout = QVBoxLayout()

        # 테이블 생성
        table_view = QTableView()

        # 모델 생성 (행, 열)
        model = QStandardItemModel()
        model.setHorizontalHeaderLabels(["이름", "가입 여부", "나이"])

        # 행, 열 너비를 창 너비에 맞게 늘리기
        table_view.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
        table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        # 리스트 데이터 출력
        data_list = [
            ["홍길동", True, "24"],
            ["김철수", False, "23"],
            ["이영희", True, "26"],
            ["홍철희", False, "25"]
        ]

        for name, joined, age in data_list:
            name_item = QStandardItem(name)
            check_item = QStandardItem()
            check_item.setCheckable(True)  # 체크박스로 표시
            check_item.setCheckState(2 if joined else 0)  # 2=Checked, 0=Unchecked
            age_item = QStandardItem()
            age_item.setData(age, Qt.DisplayRole)
            model.appendRow([name_item, check_item, age_item])

        table_view.setModel(model)

        layout.addWidget(table_view)
        window.setLayout(layout)
        self.setCentralWidget(window)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = Window()
    myWindow.show()
    app.exec_()

 

파이썬과 MySQL 함께 사용

CREATE TABLE users (
id int PRIMARY KEY AUTO_INCREMENT,
name varchar(255),
email varchar(255)
);

import pymysql

# 데이터베이스 연결
conn = pymysql.connect(
    host="localhost",
    user="root",
    password="0000",
    database="test_db",
    charset="utf8"
)

# 커서 생성
cur = conn.cursor()

# SQL 실행
cur.execute("SELECT VERSION()")

# 결과 가져오기
result = cur.fetchone()
print("Database version:", result)

# 연결 종료
cur.close()
conn.close()

C:\guiprj\.venv\Scripts\python.exe C:\guiprj\main.py 
Database version: ('8.0.44',)

 

INSERT: 데이터 추가

# %s : 문자열이 들어간다는 의미의 빈칸 
sql = "INSERT INTO users (name, email) VALUES (%s, %s)"
cur.execute(sql, ("홍길동", "hong@test.com"))
conn.commit()

SELECT: 데이터 조회

cur.execute("SELECT id, name FROM users")
rows = cur.fetchall()
for row in rows:
    print(row)

UPDATE: 데이터 수정

sql = "UPDATE users SET email=%s WHERE name=%s"
cur.execute(sql, ("newhong@test.com", "홍길동"))
conn.commit()

DELETE: 데이터 삭제

sql = "DELETE FROM users WHERE name=%s"
cur.execute(sql, ("홍길동",))
conn.commit()

 

import pymysql

try:
    conn = pymysql.connect(
        host="localhost",
        user="root",
        password="0000",
        database="test_db",
        charset="utf8"
    )
    cur = conn.cursor()

    sql = "INSERT INTO users (name, email) VALUES (%s, %s)"
    cur.execute(sql, ("김철수", "kim@test.com"))
    conn.commit()

except Exception as e:
    print("에러 발생:", e)
    conn.rollback()  # 문제가 생기면 변경 내용을 취소

finally:
    # 자원 정리
    cur.close()
    conn.close()

 

PyQt와 MySQL 통합하기

데이터베이스 생성

CREATE DATABASE IF NOT EXISTS sampledb DEFAULT CHARACTER SET utf8mb4;

USE sampledb;

CREATE TABLE IF NOT EXISTS users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) UNIQUE NOT NULL,
  password VARCHAR(100) NOT NULL
);

-- username key가 중복일 경우 에러 대신 업데이트를 실행합니다.
INSERT INTO users (username, password) VALUES ('admin', 'admin123')
ON DUPLICATE KEY UPDATE password=VALUES(password);

-- 화면 표시/추가용 데이터 테이블(회원 목록 예시)
CREATE TABLE IF NOT EXISTS members (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL
);

INSERT INTO members (name, email)
VALUES ('홍길동','hong@test.com'), ('김철수','kim@test.com')
ON DUPLICATE KEY UPDATE email=VALUES(email);

 


실습 과제

여태까지 배운 내용을 토대로, PyQt와 MySQL을 사용해 ‘재고관리’ 프로그램을 만들어보세요. 과일가게, 옷가게, 음식점 등 취향에 맞게 가상의 가게를 설정하여 자유롭게 만들어보세요! (+깃허브 원격 저장소 활용)

실습 과제 진행을 위해 할 일! (순서 기반)

1. 무엇을 할지 정리 (시나리오를 그려보면서 정리)
예를 들어 ‘명랑핫도그’를 주제로 한다? 그러면…
- 어떤 메뉴가 있을지
- 각 메뉴의 관리는 어떻게 할지 (일련번호, 상품명, 가격, 재고)
- 테이블 구성은 어떻게 할지
- 그것을 어떻게 보여주고, 어떻게 추가 및 삭제할지


2. MySQL 서버에 테이블 구성해두기 
- 상품 테이블 (예를 들면 item) 
- 조금 자신이 있고 의욕이 있다면… 사용자 테이블 (user)


3. PyQt + PyMySQL 을 이용한 기능 구현 
- 필수사항 : 데이터 목록 보여주기 기능 
- 필수사항 : 데이터 직접 입력해서 추가하기 기능 
- 필수사항 : 지우고 싶은 걸 지울 수 있는 기능 
- 필수사항 : 수정하는 기능 (재고나 상품명 등이 변경될 수 있다!)

 

주제: 삼성 휴대폰 재고 관리

 

전체 구성

검색 상품명 또는 코드 검색
코드   상품명   가격   재고   추가 수정 삭제
ID 코드 상품명 가격 재고
         
         
         
         
         
         
         
         

 

기본 메뉴

(id, code, name, price, stock)

(1, 0001, Z_Fold8, 2000000, 21)

(2, 0002, Z_Flip8, 1500000, 34)

(3, 0003, S26, 1200000, 12)

(4, 0004, S26+, 1300000, 26)

(5, 0005, S26Ultra, 1400000, 19)

(6, 0006, S26FE, 1000000, 9)

 

samsung.sql

DROP TABLE IF EXISTS items;

CREATE TABLE items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    code VARCHAR(10) UNIQUE,
    name VARCHAR(50),
    price INT,
    stock INT
);

INSERT INTO items (code, name, price, stock) VALUES
('0001', 'Z_Fold8', 2000000, 21),
('0002', 'Z_Flip8', 1500000, 34),
('0003', 'S26', 1200000, 12),
('0004', 'S26+', 1300000, 26),
('0005', 'S26_Ultra', 1400000, 19),
('0006', 'S26_FE', 1000000, 9);

CREATE TABLE IF NOT EXISTS users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    password VARCHAR(50)
);

INSERT INTO users (username, password)
VALUES ('admin', '1234');

 

users.sql

USE samsung;
SELECT * FROM users;

DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(50)
);

INSERT INTO users (username, password)
VALUES ('admin', '1234');

 

db_helper.py

import pymysql
from pymysql import IntegrityError

DB_CONFIG = dict(
    host="localhost",
    user="root",
    password="0000",
    database="samsung",
    charset="utf8"
)

class DB:
    def __init__(self, **config):
        self.config = config

    def connect(self):
        return pymysql.connect(**self.config)

    # 로그인 검증
    def verify_user(self, username, password):
        sql = "SELECT COUNT(*) FROM users WHERE username=%s AND password=%s"
        with self.connect() as conn:
            with conn.cursor() as cur:
                cur.execute(sql, (username, password))
                count, = cur.fetchone()
                return count == 1

    #재고 관리용 함수
    def fetch_items(self):
        sql = "SELECT id, code, name, price, stock FROM items ORDER BY id"
        with self.connect() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                return cur.fetchall()

    def insert_item(self, code, name, price, stock):
        sql = "INSERT INTO items (code, name, price, stock) VALUES (%s, %s, %s, %s)"
        with self.connect() as conn:
            try:
                with conn.cursor() as cur:
                    cur.execute(sql, (code, name, price, stock))
                conn.commit()
                return True, None
            except IntegrityError:
                conn.rollback()
                return False, "duplicate"
            except Exception:
                conn.rollback()
                return False, "error"

    def delete_item(self, item_id):
        sql = "DELETE FROM items WHERE id=%s"
        with self.connect() as conn:
            with conn.cursor() as cur:
                cur.execute(sql, (item_id,))
            conn.commit()

    def update_item(self, item_id, code, name, price, stock):
        sql = """
            UPDATE items
            SET code=%s, name=%s, price=%s, stock=%s
            WHERE id=%s
        """
        with self.connect() as conn:
            try:
                with conn.cursor() as cur:
                    cur.execute(sql, (code, name, price, stock, item_id))
                conn.commit()
                return True, None
            except IntegrityError:
                conn.rollback()
                return False, "duplicate"
            except Exception:
                conn.rollback()
                return False, "error"

    def search_items(self, keyword):
        sql = """
            SELECT id, code, name, price, stock
            FROM items
            WHERE code LIKE %s OR name LIKE %s
            ORDER BY id
        """
        with self.connect() as conn:
            with conn.cursor() as cur:
                like_keyword = f"%{keyword}%"
                cur.execute(sql, (like_keyword, like_keyword))
                return cur.fetchall()

login_dialog.py

from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QLineEdit, QPushButton, QMessageBox
from db_helper import DB, DB_CONFIG

class LoginDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("로그인")
        self.db = DB(**DB_CONFIG)

        self.username = QLineEdit()
        self.password = QLineEdit()
        self.password.setEchoMode(QLineEdit.Password)

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

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

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

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

        ok = self.db.verify_user(uid, pw)
        if ok:
            self.accept()
        else:
            QMessageBox.critical(self, "실패", "아이디 또는 비밀번호가 올바르지 않습니다.")

main_window.py

from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QHeaderView
from db_helper import DB, DB_CONFIG


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Samsung")
        self.resize(1000, 550)

        self.db = DB(**DB_CONFIG)

        central = QWidget()
        self.setCentralWidget(central)
        vbox = QVBoxLayout(central)

        # 검색창
        search_layout = QHBoxLayout()
        self.search_box = QLineEdit()
        self.search_box.setPlaceholderText("상품명 또는 코드 검색")
        search_layout.addWidget(QLabel("검색"))
        search_layout.addWidget(self.search_box)
        vbox.addLayout(search_layout)

        # 입력폼
        form = QHBoxLayout()

        self.input_code = QLineEdit()
        self.input_name = QLineEdit()
        self.input_price = QLineEdit()
        self.input_stock = QLineEdit()

        self.input_price.setAlignment(Qt.AlignRight)
        self.input_stock.setAlignment(Qt.AlignRight)

        self.btn_add = QPushButton("추가")
        self.btn_update = QPushButton("수정")
        self.btn_delete = QPushButton("삭제")

        form.addWidget(QLabel("코드"))
        form.addWidget(self.input_code)
        form.addWidget(QLabel("상품명"))
        form.addWidget(self.input_name)
        form.addWidget(QLabel("가격"))
        form.addWidget(self.input_price)
        form.addWidget(QLabel("재고"))
        form.addWidget(self.input_stock)
        form.addWidget(self.btn_add)
        form.addWidget(self.btn_update)
        form.addWidget(self.btn_delete)

        vbox.addLayout(form)

        # 테이블
        self.table = QTableWidget()
        self.table.setColumnCount(5)
        self.table.setHorizontalHeaderLabels(["ID", "코드", "상품명", "가격", "재고"])
        self.table.verticalHeader().setVisible(False)

        self.table.setAlternatingRowColors(True)
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        self.table.setSelectionMode(QTableWidget.SingleSelection)
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)

        header = self.table.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.Stretch)
        header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(4, QHeaderView.ResizeToContents)

        self.table.setSortingEnabled(True)  # 정렬 기능 ON
        self.table.horizontalHeader().setSectionsClickable(True)

        vbox.addWidget(self.table)

        # 이벤트 연결
        self.btn_add.clicked.connect(self.add_item)
        self.btn_delete.clicked.connect(self.delete_item)
        self.btn_update.clicked.connect(self.update_item)
        self.table.cellClicked.connect(self.fill_form)
        self.search_box.textChanged.connect(self.filter_items)

        self.load_items()

    # 데이터 로드
    def load_items(self):
        rows = self.db.fetch_items()
        self.all_rows = rows  # 검색용 저장
        self.display_items(rows)

    def display_items(self, rows):
        self.table.setSortingEnabled(False)

        self.table.setRowCount(len(rows))

        for r, row in enumerate(rows):
            for c, value in enumerate(row):

                if c == 3:
                    value = f"{int(value):,}"

                item = QTableWidgetItem(str(value))

                if c in (3, 4):
                    item.setData(Qt.DisplayRole, int(row[c]))
                    item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)

                self.table.setItem(r, c, item)

        self.table.resizeRowsToContents()
        self.table.setSortingEnabled(True)

    # 검색
    def filter_items(self):
        keyword = self.search_box.text().strip()

        if not keyword:
            self.load_items()
            return

        rows = self.db.search_items(keyword)
        self.display_items(rows)

    # 폼 초기화
    def clear_form(self):
        self.input_code.clear()
        self.input_name.clear()
        self.input_price.clear()
        self.input_stock.clear()

    # 추가
    def add_item(self):
        code = self.input_code.text().strip()
        name = self.input_name.text().strip()
        price = self.input_price.text().strip()
        stock = self.input_stock.text().strip()

        # 빈 값 먼저 검사
        if not code or not name:
            QMessageBox.warning(self, "오류", "코드와 상품명을 입력하세요.")
            return

        # 숫자 검사
        if not price.isdigit() or not stock.isdigit():
            QMessageBox.warning(self, "오류", "가격과 재고는 숫자만 입력하세요.")
            return

        # DB insert
        ok, reason = self.db.insert_item(code, name, int(price), int(stock))

        if not ok:
            if reason == "duplicate":
                QMessageBox.warning(self, "중복 오류", "이미 존재하는 코드입니다.")
            else:
                QMessageBox.critical(self, "오류", "추가 중 문제가 발생했습니다.")
            return

        self.load_items()
        self.clear_form()
        self.search_box.clear()

    # 삭제
    def delete_item(self):
        row = self.table.currentRow()
        if row < 0:
            return

        item_id = self.table.item(row, 0).data(Qt.DisplayRole)
        self.db.delete_item(item_id)
        self.load_items()
        self.clear_form()
        self.search_box.clear()

    # 수정
    def update_item(self):
        row = self.table.currentRow()
        if row < 0:
            return

        item_id = self.table.item(row, 0).data(Qt.DisplayRole)

        code = self.input_code.text().strip()
        name = self.input_name.text().strip()
        price = self.input_price.text().strip()
        stock = self.input_stock.text().strip()

        if not price.isdigit() or not stock.isdigit():
            QMessageBox.warning(self, "오류", "가격과 재고는 숫자만 입력하세요.")
            return

        if not code or not name:
            QMessageBox.warning(self, "오류", "코드와 상품명을 입력하세요.")
            return

        ok, reason = self.db.update_item(item_id, code, name, int(price), int(stock))

        if not ok:
            if reason == "duplicate":
                QMessageBox.warning(self, "중복 오류", "이미 존재하는 코드입니다.")
            else:
                QMessageBox.critical(self, "오류", "수정 중 문제가 발생했습니다.")
            return

        self.load_items()
        self.clear_form()
        self.search_box.clear()

    # 폼 채우기
    def fill_form(self, row, _):
        self.input_code.setText(self.table.item(row, 1).text())
        self.input_name.setText(self.table.item(row, 2).text())
        self.input_price.setText(str(self.table.item(row, 3).data(Qt.DisplayRole)))
        self.input_stock.setText(str(self.table.item(row, 4).data(Qt.DisplayRole)))

app.py

import sys
from PyQt5.QtWidgets import QApplication
from login_dialog import LoginDialog
from main_window import MainWindow

if __name__ == "__main__":
    app = QApplication(sys.argv)

    login = LoginDialog()
    if login.exec_() == LoginDialog.Accepted:
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())
    else:
        sys.exit(0)