7. 구현 - 공통 페이지

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 관련 파일을 폴더로 묶어 관리합니다.
- Pages 디렉토리 생성
- base_page.py 파일 선택
오른쪽 클릭 >> Refactor >> Move File.. >> Pages 디렉토리 선택 - 기존 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> 상세 주석
더보기
- 공통 UI를 BasePage에 몰아넣기
- title/status/table/bottom buttons를 BasePage에서 고정 구현
- title/status/table/bottom buttons를 BasePage에서 고정 구현
- 페이지별 차이는 데이터만 주입
- title_text, headers만 페이지별로 다르게 주입
- title_text, headers만 페이지별로 다르게 주입
- 공통 슬롯은 기본 구현, 다음 단계에서 교체
- on_refresh, on_delete를 공통으로 만들어 두고
실제 기능은 페이지별로 override
- on_refresh, on_delete를 공통으로 만들어 두고
3. 실행 테스트
더보기
- 사이드바 버튼 클릭 테스트
- "대출/반납", "도서 관리", "회원 관리" 버튼을 번갈아 클릭
- 우측 페이지가 전환되고, 각 페이지 제목/헤더가 달라지는지 확인
- 공통 버튼 테스트
- 각 페이지에서 "새로고침", "삭제(선택)" 클릭
- "미구현 안내 메시지"가 뜨고, 상태 라벨이 바뀌는지 확인
4.학습 주요 포인트
더보기
- Page는 하나의 기능 단위 화면
- 모든 Page는 같은 설계 패턴 기반
- "페이지는 공통 골격 + 페이지별 데이터/로직"으로 분리한다.
- "시그널-슬롯도 공통 버튼은 공통 슬롯으로 연결하고, 다음 단계에서 override로 확장한다."
- UI 이벤트 → Page 메서드 → DBManager → DB
- QStackedWidget은 Page를 전환만 담당
- 기능 추가 시 Page 하나만 추가하면 됨
단계별 완성 파일