8강. PySide6 재고 목록 완성하기

0. 학습 목표
→ 7강에서 만든 조회 기능을 화면에 연결해, 재고 부족 상품을 눈으로 확인할 수 있게 만듭니다.
0.1 학습 목적과 선수 조건
이 강의도 7강과 마찬가지로 어려운 문법이나 복잡한 화면 설계를 다루지 않습니다. 다음 한 가지만 확실히 이해하는 것이 목표입니다.
- 이미 검증된
inventory_service.py를 화면 코드가 그대로 재사용하도록 만드는 방법
이 강의는 다음 상태에서 시작합니다.
- 7강에서 Claude Code로
app/db.py,inventory_repository.py,inventory_service.py를 완성했습니다. pytest -v테스트 2개가 모두 통과합니다.inventoryDB에products8건,stock_logs16건,operation_requests0건이 있습니다.
0.2 진행 순서
7강 실습 상태 확인
↓
화면에 무엇을 보여줄지 정하기
↓
PySide6 기본 개념 알아보기
↓
Claude Code에 구현 계획 요청하기
↓
화면 코드 생성 요청하기
↓
완성된 코드 확인하기
↓
실행하여 화면 확인하기
↓
오류 상황 만들어 확인하기
↓
MCP 조회 결과와 화면 비교하기
↓
오류 해결하기
↓
완료 기준 확인하기
0.3 완성할 결과물
inventory-mysql-app/
├── app/
│ ├── db.py (7강에서 생성, 그대로 사용)
│ ├── repositories/
│ │ └── inventory_repository.py (7강에서 생성, 그대로 사용)
│ ├── services/
│ │ └── inventory_service.py (7강에서 생성, 그대로 사용)
│ ├── ui/
│ │ ├── __init__.py (신규)
│ │ └── inventory_window.py (신규)
│ └── main.py (신규)
이번 강의에서 새로 만드는 파일은 app/ui/inventory_window.py와 app/main.py 두 개뿐입니다. 7강에서 만든 세 파일은 코드를 한 줄도 고치지 않고 그대로 불러와 사용합니다.
0.4 학습 목표
이 강의를 마치면 다음을 할 수 있습니다.
- PySide6 창(window)에 표를 띄우고, 버튼을 눌러 데이터를 채울 수 있습니다.
- 화면 코드가 SQL을 직접 쓰지 않고 서비스 함수만 호출하도록 만들 수 있습니다.
- 조회 결과가 없을 때와 오류가 났을 때를 구분해서 안내할 수 있습니다.
- 새로고침을 여러 번 눌러도 표에 행이 쌓이지 않게 만들 수 있습니다.
- 화면에 보이는 값을 MCP 조회 결과와 대조해서 검증할 수 있습니다.
1. 7강 실습 상태 확인
→ 조회 모듈과 테스트가 그대로 남아 있는지 확인하고 시작합니다.
1.1 VS Code와 프로젝트 열기
VS Code에서 프로젝트 폴더를 열고, 통합 터미널에서 가상환경을 활성화합니다.
source .venv/bin/activate
비밀번호 환경변수도 다시 설정합니다. 새 터미널을 열었다면 이전에 설정한 값이 사라져 있습니다.
read -s -p "INVENTORY_DB_PASSWORD: " INVENTORY_DB_PASSWORD
export INVENTORY_DB_PASSWORD
1.2 조회 모듈 재확인하기
7강에서 완성한 코드가 그대로 동작하는지 두 가지로 확인합니다.

pytest -v
python -m app.services.inventory_service
두 번째 명령은 웹캠(부족 3), USB-C 허브(부족 2) 순서로 2건이 출력되어야 합니다. 이 강의는 이 결과가 화면에 그대로 나타나는지를 확인하는 강의입니다.
▶ 지금 해보세요
- 가상환경을 활성화하고 비밀번호 환경변수를 설정합니다.
pytest -v와python -m app.services.inventory_service를 각각 실행합니다.
✔ 확인 기준: 테스트 2개가 모두 PASSED이고, 조회 결과가 웹캠(부족 3), USB-C 허브(부족 2) 순서로 2건 출력됩니다.
2. 화면에 무엇을 보여줄지 정하기
→ 화면 요소와 표시할 컬럼을 문장으로 정한 뒤 schema.md와 대조합니다.
2.1 화면 구성 요소 정리하기
7강에서 조건·정렬·결과를 문장으로 정한 것처럼, 이번에도 화면에 무엇이 있어야 하는지 먼저 문장으로 정합니다.
| 화면 요소 | 동작 |
| 새로고침 버튼 | 누르면 get_low_stock_report를 다시 호출해 표를 갱신한다 |
| 상태 표시 문구 | 조회 건수, 빈 결과 안내, 오류 안내를 문장으로 보여준다 |
| 표(테이블) | 상품 번호, 상품명, 현재 수량, 재주문 기준, 부족 수량을 행마다 표시한다 |
이 화면에는 창고를 선택하는 요소를 두지 않습니다. 1강부터 사용해 온 products 테이블에는 창고를 구분하는 컬럼이 없기 때문입니다.
2.2 표시할 컬럼과 논리명 확인하기
docs/database/schema.md를 열어, 표에 표시할 다섯 컬럼의 논리명(화면에 보여줄 한글 이름)을 확인합니다.
| 실제 필드명 (inventory_service 반환값) | 화면 표시 이름 |
product_id |
상품 번호 |
product_name |
상품명 |
stock_quantity |
현재 수량 |
reorder_level |
재주문 기준 |
shortage_quantity |
부족 수량 |
▶ 지금 해보세요
- 화면 요소 세 가지를 문장으로 정리합니다.
- 다섯 컬럼의 실제 필드명과 화면 표시 이름을 표로 정리합니다.
✔ 확인 기준: 화면 요소 세 가지와 컬럼 매핑 표가 inventory_service.py가 실제로 반환하는 필드명과 일치합니다.
3. PySide6 기본 개념 알아보기
→ 화면 코드를 읽기 전에 필요한 최소한의 용어만 확인합니다.
3.1 위젯과 창
PySide6는 Qt라는 화면 프레임워크를 Python에서 사용할 수 있게 만든 라이브러리입니다. 이번 강의에서는 다음 네 가지 요소만 사용합니다.
- QMainWindow: 버튼, 표 등을 담는 하나의 창(window)
- QPushButton: 누를 수 있는 버튼
- QTableWidget: 행과 열로 데이터를 보여주는 표
- QLabel: 짧은 문구를 보여주는 이름표
이 네 가지를 위젯(widget)이라고 부릅니다. 위젯은 화면에 보이는 부품 하나하나를 뜻합니다.
3.2 시그널과 슬롯
용어: 시그널(signal) — 버튼 클릭처럼 "무언가 일어났다"는 신호입니다.
용어: 슬롯(slot) — 그 신호가 발생했을 때 실행할 함수입니다.
예를 들어 버튼을 눌렀을 때(시그널) 표를 다시 채우는 함수(슬롯)를 실행하려면 다음처럼 연결합니다.
self.refresh_button.clicked.connect(self.load_data)
clicked가 시그널이고, self.load_data가 슬롯입니다. "버튼이 클릭되면 load_data를 실행하라"는 뜻입니다. 이 강의에서 직접 만들 코드에서 이 문장을 다시 확인합니다.
4. Claude Code에 구현 계획 요청하기
→ PySide6를 설치하고, 화면 코드를 만들기 전에 계획부터 확인합니다.
4.1 PySide6 설치하기
VS Code 통합 터미널에서 가상환경이 활성화된 상태로 다음을 실행합니다.

pip install "PySide6>=6.6,<6.8"
requirements.txt에도 같은 줄을 추가합니다.
# 자동 추가
pip freeze > requirements.txt
# 수동 추가
PySide6>=6.6,<6.8
4.2 구현 계획 요청하기
Claude Code 세션에 다음과 같이 요청합니다.
app/services/inventory_service.py의 get_low_stock_report 함수를
PySide6 화면에 연결할 계획을 세워줘.
조건:
- 새 파일은 app/ui/inventory_window.py와 app/main.py 두 개만 만든다.
- app/db.py, inventory_repository.py, inventory_service.py는
수정하지 않고 그대로 불러와 사용한다.
- UI 파일에는 SQL이나 MySQL 연결 코드를 새로 쓰지 않는다.
만들기 전에 재사용할 함수와 새로 만들 파일 목록을 먼저 보여줘.
파일을 한 번에 다 만들지 말고, 하나 만들 때마다
diff로 보여주고 내 승인을 기다려줘.

승인하기 전에 다음만 확인합니다.
- 새로 만드는 파일이
inventory_window.py,main.py두 개인가? - 7강에서 만든 세 파일을 수정하지 않고 그대로 재사용한다고 되어 있는가?
▶ 지금 해보세요
pip install로 PySide6를 설치합니다.- 위 요청을 입력하고, 재사용 함수와 파일 목록을 확인한 뒤 계획을 승인합니다.
✔ 확인 기준: 새로 만들 파일이 두 개로 제한되어 있고, 7강 파일 세 개는 수정 대상에서 빠져 있습니다.
5. 화면 코드 생성 요청하기
→ 조회, 빈 결과 안내, 오류 안내, 새로고침을 한 번에 조건으로 요청합니다.
5.1 inventory_window.py 생성 요청하기
Claude Code 세션에 다음과 같이 요청합니다.
app/ui/inventory_window.py를 만들어줘.
조건:
- QMainWindow를 상속하는 InventoryWindow 클래스를 만든다.
- 새로고침 버튼(QPushButton), 상태 문구(QLabel),
표(QTableWidget)를 세로로 배치한다.
- 표의 열은 상품 번호, 상품명, 현재 수량, 재주문 기준, 부족 수량이다.
- 새로고침 버튼을 누르면 load_data 메서드를 실행한다.
- load_data는 app.db.create_connection으로 연결을 얻고,
app.services.inventory_service.get_low_stock_report를 호출한다.
- 연결 생성 중 RuntimeError가 나면 QMessageBox로 안내하고 함수를 끝낸다.
- 조회 중 예외가 나면 QMessageBox로 안내하고, connection은
finally에서 반드시 닫는다.
- 결과를 표에 채우기 전에 표의 기존 행을 모두 지운다.
- 결과가 없으면 표를 비운 채 상태 문구에 안내 문장을 표시한다.
- 결과가 있으면 상태 문구에 건수를 표시하고, 창이 열릴 때도
자동으로 한 번 조회한다.
diff로 보여주고 승인을 기다려줘.
5.2 main.py 생성 요청하기
Claude Code 세션에 다음과 같이 요청합니다.
app/main.py를 만들어줘.
조건:
- QApplication을 만들고 InventoryWindow를 띄운다.
- 프로그램 종료 코드를 sys.exit(app.exec())로 반환한다.
diff로 보여주고 승인을 기다려줘.
▶ 지금 해보세요
- 5.1 요청을 입력하고 diff를 확인한 뒤 승인합니다.
- 5.2 요청을 입력하고 diff를 확인한 뒤 승인합니다.
✔ 확인 기준: app/ui/inventory_window.py와 app/main.py가 생성되어 있고, 두 파일 어디에도 SQL 문장이 없습니다.
6. 완성된 코드 확인하기
→ 표를 지우고 다시 채우는 순서와, 오류를 구분해서 처리하는 부분을 확인합니다.
6.1 inventory_window.py 코드 확인하기
승인하면 다음과 비슷한 코드가 만들어집니다.
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QLabel, QMessageBox,
)
from app.db import create_connection
from app.services.inventory_service import get_low_stock_report
COLUMNS = ["상품 번호", "상품명", "현재 수량", "재주문 기준", "부족 수량"]
class InventoryWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("재고 부족 상품 조회")
self.resize(600, 400)
central = QWidget()
layout = QVBoxLayout(central)
self.refresh_button = QPushButton("새로고침")
self.refresh_button.clicked.connect(self.load_data)
layout.addWidget(self.refresh_button)
self.status_label = QLabel("")
layout.addWidget(self.status_label)
self.table = QTableWidget(0, len(COLUMNS))
self.table.setHorizontalHeaderLabels(COLUMNS)
layout.addWidget(self.table)
self.setCentralWidget(central)
self.load_data()
def load_data(self):
try:
connection = create_connection()
except RuntimeError as error:
QMessageBox.critical(self, "연결 오류", str(error))
return
try:
report = get_low_stock_report(connection)
except Exception as error:
QMessageBox.critical(self, "조회 오류", f"데이터를 불러오지 못했습니다.\n{error}")
return
finally:
connection.close()
self.table.setRowCount(0)
if not report:
self.status_label.setText("재주문 대상 상품이 없습니다.")
return
self.status_label.setText(f"조회 건수: {len(report)}건")
for row_index, item in enumerate(report):
self.table.insertRow(row_index)
values = [
item["product_id"],
item["product_name"],
item["stock_quantity"],
item["reorder_level"],
item["shortage_quantity"],
]
for col_index, value in enumerate(values):
self.table.setItem(row_index, col_index, QTableWidgetItem(str(value)))
self.table.setRowCount(0)이 이번 코드의 핵심입니다. 새 데이터를 채우기 전에 표를 먼저 비우기 때문에, 새로고침 버튼을 여러 번 눌러도 이전 행 위에 새 행이 쌓이지 않습니다.
연결을 만드는 try와 조회를 실행하는 try를 나눈 이유는, 두 단계에서 서로 다른 예외가 날 수 있기 때문입니다. 연결 자체가 실패하면 RuntimeError(비밀번호 없음)이거나 connection이 아직 없는 상태이므로, 조회 단계의 finally: connection.close()까지 가지 않고 먼저 함수를 끝냅니다.
6.2 main.py 코드 확인하기
import sys
from PySide6.QtWidgets import QApplication
from app.ui.inventory_window import InventoryWindow
def main():
app = QApplication(sys.argv)
window = InventoryWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
QApplication은 화면 프로그램 전체를 관리하는 객체로, 창을 하나 이상 띄우려면 반드시 먼저 만들어야 합니다. app.exec()는 창이 닫힐 때까지 프로그램을 계속 실행 상태로 유지합니다.
▶ 지금 해보세요
inventory_window.py에서setRowCount(0)이 어디 있는지 찾아봅니다.- 연결 예외와 조회 예외를 처리하는 두
try블록의 위치를 확인합니다.
✔ 확인 기준: 표를 비우는 코드가 조회 결과를 채우는 코드보다 먼저 실행되고, 연결 오류와 조회 오류가 각각 다른 메시지로 안내됩니다.
7. 실행하여 화면 확인하기
→ 창을 띄우고, 새로고침을 반복해도 표가 정상인지 확인합니다.
7.1 프로그램 실행하기
VS Code 통합 터미널에서 다음을 실행합니다.
python -m app.main
창이 열리면서 상태 문구에 "조회 건수: 2건"이 표시되고, 표에 웹캠(부족 3)이 첫 행, USB-C 허브(부족 2)가 둘째 행으로 나타나야 합니다.

7.2 새로고침 반복해서 확인하기
새로고침 버튼을 5번 정도 연속으로 눌러 봅니다. 표의 행 수가 계속 2행으로 유지되면, setRowCount(0)이 의도대로 동작하는 것입니다.
▶ 지금 해보세요
python -m app.main으로 프로그램을 실행합니다.- 새로고침 버튼을 5번 이상 눌러 표의 행 수를 확인합니다.
✔ 확인 기준: 새로고침을 여러 번 눌러도 표가 항상 2행이고, 순서는 웹캠 다음 USB-C 허브입니다.
8. 오류 상황 만들어 확인하기
→ 일부러 오류를 만들어, 프로그램이 멈추지 않고 안내 메시지로 처리하는지 확인합니다.
8.1 비밀번호 환경변수를 잠시 지워보기
프로그램을 종료한 뒤, 터미널에서 비밀번호 환경변수를 지우고 다시 실행합니다.
# 오류 테스트
unset INVENTORY_DB_PASSWORD
python -m app.main
창이 뜨면서 "연결 오류" 메시지 창이 함께 표시되고, 프로그램이 강제 종료되지 않아야 합니다.

확인 후에는 다시 비밀번호를 설정하고 새로고침 버튼을 눌러 정상 조회로 되돌립니다.
read -s -p "INVENTORY_DB_PASSWORD: " INVENTORY_DB_PASSWORD
export INVENTORY_DB_PASSWORD
▶ 지금 해보세요
unset INVENTORY_DB_PASSWORD후python -m app.main을 실행합니다.- 오류 메시지 창을 확인하고, 프로그램이 종료되지 않는지 확인합니다.
- 비밀번호를 다시 설정하고 새로고침으로 정상 상태를 확인합니다.
✔ 확인 기준: 비밀번호가 없을 때 "연결 오류" 메시지가 뜨고, 프로그램은 창이 닫히지 않은 채 유지됩니다.
9. MCP 조회 결과와 화면 비교하기
→ 화면에 보이는 값이 실제 데이터베이스 값과 같은지 마지막으로 대조합니다.
9.1 MCP로 같은 조건 조회하기
화면이 정상으로 보인다고 해서 값이 반드시 맞다는 뜻은 아닙니다. Claude Code 세션에 다음과 같이 요청합니다.
mysql-inventory MCP로 products 테이블에서
stock_quantity가 reorder_level 이하인 상품을 조회해줘.
조건:
- SELECT만 사용하고, 결과를 표로 보여줘.
내가 결과를 직접 확인할 것이므로, 결론을 먼저 단정하지 마.
9.2 화면 값과 비교하기
| 비교 항목 | 화면(PySide6) 값 | MCP 조회 결과 |
| 조회 건수 | 2건 | 2건 |
| 대상 상품과 부족 수량 | 웹캠 3, USB-C 허브 2 | 웹캠 3, USB-C 허브 2 |
▶ 지금 해보세요
- MCP로 같은 조건을 조회합니다.
- 화면 값과 MCP 결과를 표로 비교합니다.
✔ 확인 기준: 화면에 표시된 조회 건수와 부족 수량이 MCP 조회 결과와 정확히 일치합니다.
10. 오류 해결하기
→ 설정을 무작정 다시 만들지 않습니다. 다음 순서로 원인을 좁힙니다.
10.1 증상별 확인 순서
PySide6 설치 확인 → 가상환경 활성화 확인 → 환경변수 확인 → 7강 코드 확인
| 증상 | 먼저 확인할 항목 | 해결 방향 |
ModuleNotFoundError: No module named 'PySide6' |
가상환경이 활성화된 상태에서 설치했는가 | source .venv/bin/activate 후 pip install "PySide6>=6.6,<6.8" 재실행 |
| 창은 뜨지만 표가 비어 있고 오류 메시지도 없음 | 7강의 python -m app.services.inventory_service 결과가 정상인가 |
7강 코드부터 먼저 확인, 서비스 함수가 빈 리스트를 반환하는지 점검 |
| "연결 오류" 메시지 창이 뜸 | 현재 터미널에 INVENTORY_DB_PASSWORD가 설정되어 있는가 |
export INVENTORY_DB_PASSWORD를 같은 터미널에서 다시 실행 |
| 새로고침을 누를 때마다 표 행이 늘어남 | load_data에서 setRowCount(0)이 조회보다 먼저 실행되는가 |
Claude Code에 diff로 순서를 다시 확인시키고 승인 |
| 화면 값과 MCP 조회 결과의 건수가 다름 | 화면이 7강의 get_low_stock_report를 그대로 호출하는가 |
UI 코드 안에 별도의 조건이나 SQL이 추가되지 않았는지 확인 |
💡 팁: 화면에 문제가 생기면 먼저 7강 명령(pytest -v, python -m app.services.inventory_service)으로 데이터 쪽이 정상인지부터 좁혀서 확인합니다.
11. 이번 강의 완료 기준
→ 완성한 코드와 확인한 내용을 체크리스트로 정리합니다.
11.1 최종 체크리스트
☐ 화면 요소 세 가지(새로고침 버튼, 상태 문구, 표)와 컬럼 매핑을 inventory_service.py 반환값과 맞춰 확인했다
☐ app/ui/inventory_window.py와 app/main.py가 생성되어 있고, 두 파일 어디에도 SQL이 없다
☐ python -m app.main 실행 결과 표에 웹캠(부족 3), USB-C 허브(부족 2) 순서로 2건이 표시된다
☐ 새로고침을 여러 번 눌러도 표가 항상 2행으로 유지된다
☐ 비밀번호가 없을 때 "연결 오류" 메시지가 뜨고 프로그램이 종료되지 않는다
☐ 화면에 표시된 조회 건수·부족 수량이 MCP 조회 결과와 일치한다
11.2 완성 구조
inventory-mysql-app/
├── app/
│ ├── __init__.py
│ ├── db.py
│ ├── main.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ └── inventory_repository.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── inventory_service.py
│ └── ui/
│ ├── __init__.py
│ └── inventory_window.py
├── tests/
│ └── test_inventory_service.py
├── requirements.txt (PySide6 항목 추가)
└── pytest.ini
| 파일 | 처음 생성 | 내용을 채움 |
app/db.py |
7강 | 7강 |
inventory_repository.py |
7강 | 7강 |
inventory_service.py |
7강 | 7강 |
inventory_window.py |
8강 | 8강 |
main.py |
8강 | 8강 |
11.3 이번 강의에서 아직 하지 않는 작업
이번 강의는 조회 결과를 화면 표로 보여주는 데까지만 다룹니다. 다음 작업은 아직 하지 않습니다.
- 재고 수량을 바꾸는 출고 기능(
UPDATE) — 이번 화면은SELECT결과만 표시 - pytest-qt 등을 이용한 화면 자동 테스트 — 이번 강의는 직접 실행하여 눈으로 확인하는 방식만 사용
- 검색 조건 입력, 정렬 기준 변경 등 화면에서의 조작 기능
11.4 참고 문서
[OG 카드 자리: PySide6 공식 문서 — Qt for Python, QMainWindow]
시그널과 슬롯의 자세한 동작 방식은 PySide6 공식 문서의 "Signals and Slots" 페이지에서 확인할 수 있습니다.
→ 다음 강의 (9강): 이번 강의의 조회 화면과 별도로, Python 트랜잭션을 사용해 재고를 실제로 줄이는 출고 기능을 구현합니다.
12. 실습 과제
→ 화면 요소를 하나 더 추가해 보면서 위젯과 서비스 함수 연결 흐름을 스스로 반복합니다.
12.1 마지막 조회 시각 표시하기
Claude Code에 "새로고침할 때마다 현재 시각을 상태 문구 옆에 QLabel로 표시해줘"라고 요청하고, diff로 inventory_window.py가 어떻게 바뀌는지 확인합니다.
12.2 오류 문구 다시 확인하기
8.1에서 했던 것처럼 비밀번호 환경변수를 지운 상태를 재현하고, 이번에는 오류 메시지 창의 문구를 그대로 옮겨 적습니다.
12.3 제출물
다음 세 가지를 캡처하거나 정리하여 제출합니다.
- 마지막 조회 시각이 표시된 화면 캡처
- 새로고침을 5회 이상 반복한 뒤에도 표가 2행으로 유지된 화면 캡처
- 화면 값과 MCP 조회 결과 비교표(9.2 형식)