8강 실습 중심 ⏱ 약 70분

 

0. 학습 목표

→ 7강에서 만든 조회 기능을 화면에 연결해, 재고 부족 상품을 눈으로 확인할 수 있게 만듭니다.

더보기

0.1 학습 목적과 선수 조건

 

이 강의도 7강과 마찬가지로 어려운 문법이나 복잡한 화면 설계를 다루지 않습니다. 다음 한 가지만 확실히 이해하는 것이 목표입니다.

  • 이미 검증된 inventory_service.py를 화면 코드가 그대로 재사용하도록 만드는 방법

 

이 강의는 다음 상태에서 시작합니다.

  • 7강에서 Claude Codeapp/db.py, inventory_repository.py, inventory_service.py를 완성했습니다.
  • pytest -v 테스트 2개가 모두 통과합니다.
  • inventory DB에 products 8건, stock_logs 16건, operation_requests 0건이 있습니다.

 

 

 

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.pyapp/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건이 출력되어야 합니다. 이 강의는 이 결과가 화면에 그대로 나타나는지를 확인하는 강의입니다.

▶ 지금 해보세요

  1. 가상환경을 활성화하고 비밀번호 환경변수를 설정합니다.
  2. pytest -vpython -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 부족 수량

▶ 지금 해보세요

  1. 화면 요소 세 가지를 문장으로 정리합니다.
  2. 다섯 컬럼의 실제 필드명과 화면 표시 이름을 표로 정리합니다.

✔ 확인 기준: 화면 요소 세 가지와 컬럼 매핑 표가 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로 보여주고 내 승인을 기다려줘.

 

[화면 캡처: VS Code Claude Code 세션이 재사용할 함수와 만들 파일 목록을 제시한 화면]

 

 

승인하기 전에 다음만 확인합니다.

  • 새로 만드는 파일이 inventory_window.py, main.py 두 개인가?
  • 7강에서 만든 세 파일을 수정하지 않고 그대로 재사용한다고 되어 있는가?

▶ 지금 해보세요

  1. pip install로 PySide6를 설치합니다.
  2. 위 요청을 입력하고, 재사용 함수와 파일 목록을 확인한 뒤 계획을 승인합니다.

✔ 확인 기준: 새로 만들 파일이 두 개로 제한되어 있고, 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로 보여주고 승인을 기다려줘.

▶ 지금 해보세요

  1. 5.1 요청을 입력하고 diff를 확인한 뒤 승인합니다.
  2. 5.2 요청을 입력하고 diff를 확인한 뒤 승인합니다.

✔ 확인 기준: app/ui/inventory_window.pyapp/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()는 창이 닫힐 때까지 프로그램을 계속 실행 상태로 유지합니다.

▶ 지금 해보세요

  1. inventory_window.py에서 setRowCount(0)이 어디 있는지 찾아봅니다.
  2. 연결 예외와 조회 예외를 처리하는 두 try 블록의 위치를 확인합니다.

✔ 확인 기준: 표를 비우는 코드가 조회 결과를 채우는 코드보다 먼저 실행되고, 연결 오류와 조회 오류가 각각 다른 메시지로 안내됩니다.

 

7. 실행하여 화면 확인하기

→ 창을 띄우고, 새로고침을 반복해도 표가 정상인지 확인합니다.

더보기

7.1 프로그램 실행하기

 

VS Code 통합 터미널에서 다음을 실행합니다.

python -m app.main

창이 열리면서 상태 문구에 "조회 건수: 2건"이 표시되고, 표에 웹캠(부족 3)이 첫 행, USB-C 허브(부족 2)가 둘째 행으로 나타나야 합니다.

[화면 캡처: 재고 부족 상품 조회 창 — 상태 문구와 표에 웹캠, USB-C 허브가 표시된 화면]

 

 

 

7.2 새로고침 반복해서 확인하기

 

새로고침 버튼을 5번 정도 연속으로 눌러 봅니다. 표의 행 수가 계속 2행으로 유지되면, setRowCount(0)이 의도대로 동작하는 것입니다.

▶ 지금 해보세요

  1. python -m app.main으로 프로그램을 실행합니다.
  2. 새로고침 버튼을 5번 이상 눌러 표의 행 수를 확인합니다.

✔ 확인 기준: 새로고침을 여러 번 눌러도 표가 항상 2행이고, 순서는 웹캠 다음 USB-C 허브입니다.

 

8. 오류 상황 만들어 확인하기

→ 일부러 오류를 만들어, 프로그램이 멈추지 않고 안내 메시지로 처리하는지 확인합니다.

더보기

8.1 비밀번호 환경변수를 잠시 지워보기

 

프로그램을 종료한 뒤, 터미널에서 비밀번호 환경변수를 지우고 다시 실행합니다.

# 오류 테스트
unset INVENTORY_DB_PASSWORD
python -m app.main

창이 뜨면서 "연결 오류" 메시지 창이 함께 표시되고, 프로그램이 강제 종료되지 않아야 합니다.

 

[화면 캡처: 연결 오류 QMessageBox가 표시된 화면]

 

확인 후에는 다시 비밀번호를 설정하고 새로고침 버튼을 눌러 정상 조회로 되돌립니다.

read -s -p "INVENTORY_DB_PASSWORD: " INVENTORY_DB_PASSWORD
export INVENTORY_DB_PASSWORD

▶ 지금 해보세요

  1. unset INVENTORY_DB_PASSWORDpython -m app.main을 실행합니다.
  2. 오류 메시지 창을 확인하고, 프로그램이 종료되지 않는지 확인합니다.
  3. 비밀번호를 다시 설정하고 새로고침으로 정상 상태를 확인합니다.

✔ 확인 기준: 비밀번호가 없을 때 "연결 오류" 메시지가 뜨고, 프로그램은 창이 닫히지 않은 채 유지됩니다.

 

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

▶ 지금 해보세요

  1. MCP로 같은 조건을 조회합니다.
  2. 화면 값과 MCP 결과를 표로 비교합니다.

✔ 확인 기준: 화면에 표시된 조회 건수와 부족 수량이 MCP 조회 결과와 정확히 일치합니다.

 

10. 오류 해결하기

→ 설정을 무작정 다시 만들지 않습니다. 다음 순서로 원인을 좁힙니다.

더보기

10.1 증상별 확인 순서

 

PySide6 설치 확인 → 가상환경 활성화 확인 → 환경변수 확인 → 7강 코드 확인
증상 먼저 확인할 항목 해결 방향
ModuleNotFoundError: No module named 'PySide6' 가상환경이 활성화된 상태에서 설치했는가 source .venv/bin/activatepip 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.pyapp/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 형식)