1. 목표

더보기
  • 3개 페이지가 공통으로 사용하는 "페이지 템플릿(BasePage)"를 만든다.
    • UI 일관성 확보
    • 페이지 구현 난이도 하향

  • 공통 템플릿에 다음 UI 패턴을 표준화한다.
    • 상단 제목(title)
    • 상태 표시(status)
    • 공통 버튼 영역(예: "새로고침", "삭제")
    • 공통 테이블(QTableWidget) 설정(Stretch, 행 선택 등)

  • 다음 단계(7~9단계)에서 Member/Book/Circulation 페이지를 만들 때
    "페이지를 만드는 표준 템플릿"을 그대로 재사용할 수 있도록 기반을 만든다.

  • 모든 페이지가 공유하는 UI 구성 패턴(레이아웃 → 입력 → 테이블 → 버튼) 구현.
  • Page에서 DB 처리 로직을 공통 흐름(입력값 → SQL 생성 → db 호출 → 테이블 갱신) 구현


2. 전체 로직

더보기

(1) 이전 단계에서 구현한 파일에 이어 작업합니다.

libraryManagementSystem/
├─ db/
│  ├─ config.py
│  └─ database_manager.py
├─ library_system.py
├─ main.py
└─ requirements.txt

 

 

(2) 이전 단계에서 임시로 사용했던 simple_page.py를 base_page.py 로 대체하여 사용합니다.

# base_page.py # pages/base_page.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QLabel, QTableWidget,
    QAbstractItemView, QHeaderView,
    QHBoxLayout, QPushButton,
    QMessageBox, QTableWidgetItem
)


class BasePage(QWidget):
    def __init__(self, title_text, headers=None):
        super().__init__()

        root = QVBoxLayout(self)
        root.setContentsMargins(30, 30, 30, 30)
        root.setSpacing(12)

        # title
        self.title = QLabel(title_text)
        self.title.setStyleSheet("font-size: 22px; font-weight: bold;")
        root.addWidget(self.title)

        # status
        self.status = QLabel("상태: 대기")
        self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.status.setStyleSheet("color: #555;")
        root.addWidget(self.status)

        # input / search slots
        self.form_layout = QHBoxLayout()
        root.addLayout(self.form_layout)

        self.search_layout = QHBoxLayout()
        root.addLayout(self.search_layout)

        # table
        self.table = QTableWidget()
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        root.addWidget(self.table)

        if headers:
            self.set_headers(headers)

        # bottom slot
        self.bottom_layout = QHBoxLayout()
        self.bottom_layout.addStretch()
        root.addLayout(self.bottom_layout)

        # common buttons
        self.btn_refresh = QPushButton("새로고침")
        self.btn_delete = QPushButton("삭제(선택)")

        self.btn_refresh.setStyleSheet("background-color: #7f8c8d; color: white; padding: 10px;")
        self.btn_delete.setStyleSheet("background-color: #c0392b; color: white; padding: 10px;")

        self.bottom_layout.addWidget(self.btn_refresh)
        self.bottom_layout.addWidget(self.btn_delete)

        self.btn_refresh.clicked.connect(self.on_refresh)
        self.btn_delete.clicked.connect(self.on_delete)

    # common helpers
    def msg_info(self, title, text):
        QMessageBox.information(self, title, text)

    def msg_warn(self, title, text):
        QMessageBox.warning(self, title, text)

    def set_status(self, text):
        self.status.setText(text)

    def set_headers(self, headers):
        self.table.setRowCount(0)
        self.table.setColumnCount(len(headers))
        self.table.setHorizontalHeaderLabels(headers)

    def set_table(self, col_count, headers):
        self.table.setRowCount(0)
        self.table.setColumnCount(col_count)
        self.table.setHorizontalHeaderLabels(headers)

    def fill_rows(self, rows):
        self.table.setRowCount(0)
        for r, row in enumerate(rows):
            self.table.insertRow(r)
            for c, value in enumerate(row):
                self.table.setItem(r, c, QTableWidgetItem(str(value)))

    def fill_table(self, rows, transform_func=None):
        self.table.setRowCount(0)
        for r, row in enumerate(rows):
            self.table.insertRow(r)
            values = list(row)
            if transform_func:
                for c in range(len(values)):
                    values[c] = transform_func(c, values[c])
            for c, value in enumerate(values):
                self.table.setItem(r, c, QTableWidgetItem(str(value)))

    def current_row(self):
        return self.table.currentRow()

    # default hooks
    def on_refresh(self):
        QMessageBox.information(self, "안내", "아직 구현되지 않았습니다.")
        self.set_status("상태: 새로고침 미구현")

    def on_delete(self):
        QMessageBox.information(self, "안내", "아직 구현되지 않았습니다.")
        self.set_status("상태: 삭제 미구현")


 

(3) Pages 관련 파일을 폴더로 묶어 관리합니다.

  1. Pages 디렉토리 생성
  2. base_page.py 파일 선택 
    오른쪽 클릭 >> Refactor >> Move File.. >> Pages 디렉토리 선택
  3. 기존 import 경로 변경 확인 or 수동 경로 변경
libraryManagementSystem/
├─ db/
│  ├─ config.py
│  └─ database_manager.py
├─ pages/
│  └─ base_page.py
├─ main.py
└─ requirements.txt

 

 

(3) book_manager_page.py, circulation_page.py, member_manage_page.py추가

# Pages/book_manager_page.py

from Pages.base_page import BasePage

class BookManagerPage(BasePage):
    def __init__(self):
        super().__init__("도서 관리 (공통 골격)", ["ID", "제목", "저자", "출판사", "상태"])
# Pages/circulation_page.py

from Pages.base_page import BasePage

class CirculationPage(BasePage):
    def __init__(self):
        super().__init__("대출 및 반납 (공통 골격)", ["도서 ID", "제목", "대출일", "연장횟수", "회원ID"])
# Page/book_manger_page.py

from Pages.base_page import BasePage

class MemberManagerPage(BasePage):
    def __init__(self):
        super().__init__("회원 관리 (공통 골격)", ["ID", "이름", "연락처", "이메일"])
libraryManagementSystem/
├─ db/
│  ├─ config.py
│  └─ database_manager.py
├─ pages/
│  ├─ base_page.py
│  ├─ member_manager_page.py
│  ├─ book_manager_page.py
│  └─ circulation_page.py
├─ main.py
└─ requirements.txt

 

 

(7) LibrarySystem.py 수정

# library_system.py

from PySide6.QtWidgets import (
    QMainWindow, QWidget,
    QHBoxLayout, QVBoxLayout,
    QPushButton, QLabel, QFrame, QStackedWidget
)
from PySide6.QtCore import Qt

from Pages.BookManagerPage import BookManagerPagem # 페이지 추가
from Pages.CirculationPage import CirculationPage
from Pages.MemberManagerPage import MemberManagerPage

class LibrarySystem(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("도서 관리 프로그램")
        self.resize(1200, 800)

        container = QWidget()
        self.setCentralWidget(container)
        main_layout = QHBoxLayout(container)
        main_layout.setContentsMargins(0, 0, 0, 0)

        # 사이드바
        self.sidebar = QFrame()
        self.sidebar.setStyleSheet("background-color: #2c3e50; min-width: 200px;")
        sidebar_layout = QVBoxLayout(self.sidebar)

        title_label = QLabel("도서관 시스템")
        title_label.setStyleSheet("color: white; font-size: 24px; font-weight: bold; padding: 20px;")
        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        sidebar_layout.addWidget(title_label)

        self.btn_1 = QPushButton("대출 / 반납")
        self.btn_2 = QPushButton("도서 관리")
        self.btn_3 = QPushButton("회원 관리")

        btn_style = "color: white; background-color: transparent; padding: 15px; text-align: left; font-size: 16px;"
        self.btn_1.setStyleSheet(btn_style)
        self.btn_2.setStyleSheet(btn_style)
        self.btn_3.setStyleSheet(btn_style)

        sidebar_layout.addWidget(self.btn_1)
        sidebar_layout.addWidget(self.btn_2)
        sidebar_layout.addWidget(self.btn_3)
        sidebar_layout.addStretch()

        self.pages = QStackedWidget()

        self.pages = QStackedWidget()
        self.page_circulation = CirculationPage()
        self.page_book = BookManagerPage()
        self.page_member = MemberManagerPage()

        self.pages.addWidget(self.page_circulation)
        self.pages.addWidget(self.page_book)
        self.pages.addWidget(self.page_member)

        main_layout.addWidget(self.sidebar)
        main_layout.addWidget(self.pages)

        self.btn_1.clicked.connect(lambda: self.pages.setCurrentIndex(0))
        self.btn_2.clicked.connect(lambda: self.pages.setCurrentIndex(1))
        self.btn_3.clicked.connect(lambda: self.pages.setCurrentIndex(2))

 

 

ㄴ 2.1 import + 주석

더보기
추가된 import 없음


ㄴ 2.2 GUI + 주석

더보기
# pages/base_page.py

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QLabel, QTableWidget, QAbstractItemView, QHeaderView,
    QHBoxLayout, QPushButton, QMessageBox, QTableWidgetItem
)


class BasePage(QWidget):
    def __init__(self, title_text, headers=None):
        super().__init__()

        root = QVBoxLayout(self)
        root.setContentsMargins(30, 30, 30, 30)
        root.setSpacing(12)

        # 1) 제목
        self.title = QLabel(title_text)
        self.title.setStyleSheet("font-size: 22px; font-weight: bold;")
        root.addWidget(self.title)

        # 2) 상태 라벨
        self.status = QLabel("상태: 대기")
        self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.status.setStyleSheet("color: #555;")
        root.addWidget(self.status)

        # 3) 입력(등록) / 검색 레이아웃 슬롯 (BookManagerPage에서 사용)
        self.form_layout = QHBoxLayout()
        root.addLayout(self.form_layout)

        self.search_layout = QHBoxLayout()
        root.addLayout(self.search_layout)

        # 4) 테이블
        self.table = QTableWidget()
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        root.addWidget(self.table)

        # headers가 넘어오면 즉시 설정 (이전 단계와의 호환)
        if headers:
            self.set_headers(headers)

        # 5) 하단 버튼 레이아웃 슬롯 (BookManagerPage에서 사용)
        self.bottom_layout = QHBoxLayout()
        self.bottom_layout.addStretch()
        root.addLayout(self.bottom_layout)

        # 6) (옵션) 공통 버튼 유지: 이전 단계 호환용
        # - 필요 없으면 파생 페이지에서 숨기거나, BasePage에서 제거해도 됨
        self.btn_refresh = QPushButton("새로고침")
        self.btn_delete = QPushButton("삭제(선택)")

        self.btn_refresh.setStyleSheet("background-color: #7f8c8d; color: white; padding: 10px;")
        self.btn_delete.setStyleSheet("background-color: #c0392b; color: white; padding: 10px;")

        self.bottom_layout.addWidget(self.btn_refresh)
        self.bottom_layout.addWidget(self.btn_delete)

        self.btn_refresh.clicked.connect(self.on_refresh)
        self.btn_delete.clicked.connect(self.on_delete)

    # -------------------------------------------------
    # 공통 유틸 (BookManagerPage가 호출하는 메서드들)
    # -------------------------------------------------
    def msg_info(self, title, text):
        QMessageBox.information(self, title, text)

    def msg_warn(self, title, text):
        QMessageBox.warning(self, title, text)

    def set_status(self, text):
        self.status.setText(text)

    def set_headers(self, headers):
        self.table.setRowCount(0)
        self.table.setColumnCount(len(headers))
        self.table.setHorizontalHeaderLabels(headers)

    # BookManagerPage에서 사용하는 set_table 형태 지원
    def set_table(self, col_count, headers):
        self.table.setRowCount(0)
        self.table.setColumnCount(col_count)
        self.table.setHorizontalHeaderLabels(headers)

    def fill_rows(self, rows):
        self.table.setRowCount(0)
        for r, row_data in enumerate(rows):
            self.table.insertRow(r)
            for c, data in enumerate(row_data):
                self.table.setItem(r, c, QTableWidgetItem(str(data)))

    # BookManagerPage에서 사용하는 fill_table(rows, transform_func) 지원
    def fill_table(self, rows, transform_func=None):
        self.table.setRowCount(0)
        for r, row_data in enumerate(rows):
            self.table.insertRow(r)
            row_list = list(row_data)

            # 변환 함수가 있으면 컬럼별 값 변환
            if transform_func:
                for c in range(len(row_list)):
                    row_list[c] = transform_func(c, row_list[c])

            for c, data in enumerate(row_list):
                self.table.setItem(r, c, QTableWidgetItem(str(data)))

    def current_row(self):
        return self.table.currentRow()

    # -------------------------------------------------
    # 기본 훅(파생 페이지에서 override 가능)
    # -------------------------------------------------
    def on_refresh(self):
        QMessageBox.information(self, "안내", "아직 구현되지 않았습니다. (7~9단계에서 구현)")
        self.set_status("상태: 새로고침 미구현")

    def on_delete(self):
        QMessageBox.information(self, "안내", "아직 구현되지 않았습니다. (7~9단계에서 구현)")
        self.set_status("상태: 삭제 미구현")

 

MemberManagerPage / BookManagerPage / CirculationPage 에 공통으로 적용되는 형태 입니다.

 

ㄴ 2.3 Signal & Slot + 주석

더보기
self.btn_refresh.clicked.connect(self.on_refresh)  # [1] 새로고침 버튼 클릭 -> 공통 슬롯 호출
self.btn_delete.clicked.connect(self.on_delete)    # [2] 삭제 버튼 클릭 -> 공통 슬롯 호출
  • 현재 단계에서는 "동작 미구현 안내"만 수행합니다.
  • 7~9단계에서 각 페이지가 on_refresh, on_delete를 오버라이드하여 실제 CRUD 로직으로 교체합니다.

 

ㄴ 2.4 <BasePage> 요약

더보기
BasePage(QWidget)
│
├─ layout : QVBoxLayout (root_layout)
│   ├─ title : QLabel                          # 페이지 제목
│   │
│   ├─ form_layout : QHBoxLayout               # 입력/등록 영역 슬롯 (비어 있음)
│   │   └─ (페이지별 위젯 추가)
│   │
│   ├─ search_layout : QHBoxLayout             # 검색 영역 슬롯 (비어 있음)
│   │   └─ (페이지별 위젯 추가)
│   │
│   ├─ table : QTableWidget                    # 공통 데이터 리스트 영역
│   │   ├─ horizontalHeader : QHeaderView
│   │   │   └─ ResizeMode.Stretch
│   │   └─ selectionBehavior
│   │       └─ SelectRows
│   │
│   └─ bottom_layout : QHBoxLayout             # 하단 버튼 영역 슬롯
│       ├─ stretch                             # 우측 정렬 기준
│       └─ (페이지별 버튼 추가)
│
├─ [공통 유틸 메서드]
│   ├─ set_table(col_count, headers)
│   ├─ clear_table()
│   ├─ add_table_row(row_data)
│   ├─ fill_table(rows, transform_func=None)
│   ├─ current_row()
│   ├─ msg_info(title, text)
│   └─ msg_warn(title, text)
│
└─ [책임 경계]
    ├─ BasePage
    │   ├─ UI 골격 제공
    │   ├─ 테이블 표시/갱신
    │   └─ 메시지 다이얼로그
    │
    └─ Page별 클래스
        ├─ form_layout / search_layout 구성
        ├─ bottom_layout 버튼 추가
        └─ DB 조회/등록/삭제 로직 수행

 

ㄴ 2.4 <BasePage> 상세 주석

더보기
  1. 공통 UI를 BasePage에 몰아넣기
    • title/status/table/bottom buttons를 BasePage에서 고정 구현

  2. 페이지별 차이는 데이터만 주입
    • title_text, headers만 페이지별로 다르게 주입

  3. 공통 슬롯은 기본 구현, 다음 단계에서 교체
    • on_refresh, on_delete를 공통으로 만들어 두고
      실제 기능은 페이지별로 override


3. 실행 테스트

더보기
  1. 사이드바 버튼 클릭 테스트
    • "대출/반납", "도서 관리", "회원 관리" 버튼을 번갈아 클릭
    • 우측 페이지가 전환되고, 각 페이지 제목/헤더가 달라지는지 확인

  2. 공통 버튼 테스트
    • 각 페이지에서 "새로고침", "삭제(선택)" 클릭
    • "미구현 안내 메시지"가 뜨고, 상태 라벨이 바뀌는지 확인


4.학습 주요 포인트

더보기
  • Page는 하나의 기능 단위 화면
  • 모든 Page는 같은 설계 패턴 기반
    • "페이지는 공통 골격 + 페이지별 데이터/로직"으로 분리한다.
    • "시그널-슬롯도 공통 버튼은 공통 슬롯으로 연결하고, 다음 단계에서 override로 확장한다."
  • UI 이벤트 → Page 메서드 → DBManager → DB
  • QStackedWidget은 Page를 전환만 담당
  • 기능 추가 시 Page 하나만 추가하면 됨


단계별 완성 파일