9. 구현 - 도서 관리 페이지

1. 목표
더보기


- BasePage(공통 템플릿)를 상속하여 MemberManagerPage를 확장한다.
- 기능 범위
- 도서 등록(INSERT)
- 도서 전체 조회(SELECT)
- 도서 조건 검색(WHERE + LIKE)
- 선택 도서 삭제(DELETE)
- BasePage의 공통 버튼(btn_refresh, btn_delete)을 재사용하여 "버튼 중복" 문제를 해결한다.
- 상태 라벨(status)을 활용해 처리 결과(조회 건수, 성공/실패, 취소)를 사용자에게 즉시 피드백한다.
2. 전체 로직
더보기
(1) 이전 단계에서 구현한 파일에 이어 작업합니다.
libraryManagementSystem/
├─ db/
│ ├─ config.py
│ └─ database_manager.py
├─ pages/
│ ├─ base_page.py
│ ├─ member_manager_page.py
│ ├─ book_manager_page.py
│ └─ circulation_page.py
├─ library_system.py
├─ main.py
└─ requirements.txt
(2) 이전 단계에서 사용했던 book_manager_page.py 를사용합니다.
# Pages/book_manager_page.py
from PySide6.QtWidgets import QLineEdit, QPushButton, QMessageBox
from Pages.base_page import BasePage
class BookManagerPage(BasePage):
def __init__(self, db):
super().__init__("도서 관리 (등록 및 조회)")
self.db = db
# 1) 입력(등록) 폼
self.input_id = QLineEdit()
self.input_id.setPlaceholderText("도서 ID")
self.input_title = QLineEdit()
self.input_title.setPlaceholderText("제목")
self.input_author = QLineEdit()
self.input_author.setPlaceholderText("저자")
self.input_pub = QLineEdit()
self.input_pub.setPlaceholderText("출판사")
btn_add = QPushButton("도서 등록")
btn_add.setStyleSheet("background-color: #3498db; color: white; padding: 10px;")
btn_add.clicked.connect(self.add_book)
self.form_layout.addWidget(self.input_id)
self.form_layout.addWidget(self.input_title)
self.form_layout.addWidget(self.input_author)
self.form_layout.addWidget(self.input_pub)
self.form_layout.addWidget(btn_add)
# 2) 검색 폼
self.search_id = QLineEdit()
self.search_id.setPlaceholderText("도서 ID 검색")
self.search_title = QLineEdit()
self.search_title.setPlaceholderText("제목 검색")
self.search_author = QLineEdit()
self.search_author.setPlaceholderText("저자 검색")
self.search_pub = QLineEdit()
self.search_pub.setPlaceholderText("출판사 검색")
btn_search = QPushButton("도서 검색")
btn_search.setStyleSheet("background-color: #27ae60; color: white; padding: 10px;")
btn_search.clicked.connect(self.search_book)
self.search_layout.addWidget(self.search_id)
self.search_layout.addWidget(self.search_title)
self.search_layout.addWidget(self.search_author)
self.search_layout.addWidget(self.search_pub)
self.search_layout.addWidget(btn_search)
# 3) 테이블
self.set_table(5, ["ID", "제목", "저자", "출판사", "상태"])
# 4) 하단 버튼(중복 해결): BasePage 공통 버튼 재사용
self.btn_refresh.setText("새로고침")
self.btn_delete.setText("선택 도서 삭제")
# BasePage 기본 훅 연결 해제 후 페이지 로직 연결
try:
self.btn_refresh.clicked.disconnect()
except TypeError:
pass
try:
self.btn_delete.clicked.disconnect()
except TypeError:
pass
self.btn_refresh.clicked.connect(self.load_books)
self.btn_delete.clicked.connect(self.delete_book)
# 최초 로딩
self.load_books()
def add_book(self):
book_id = self.input_id.text().strip()
title = self.input_title.text().strip()
author = self.input_author.text().strip()
publisher = self.input_pub.text().strip()
data = (book_id, title, author, publisher)
if "" in data:
self.msg_warn("경고", "모든 내용을 입력하세요.")
self.set_status("상태: 등록 실패(입력 누락)")
return
ok = self.db.execute_query(
"INSERT INTO books (book_id, title, author, publisher) VALUES (%s, %s, %s, %s)",
data
)
if ok:
self.msg_info("성공", "등록되었습니다.")
self.set_status("상태: 등록 성공")
self.load_books()
else:
self.msg_warn("실패", "이미 있는 ID입니다.")
self.set_status("상태: 등록 실패(중복)")
def delete_book(self):
row = self.current_row()
if row < 0:
self.msg_warn("안내", "삭제할 도서를 테이블에서 선택하세요.")
self.set_status("상태: 삭제 실패(선택 없음)")
return
item = self.table.item(row, 0)
if item is None:
self.msg_warn("오류", "선택된 행에서 도서 ID를 읽을 수 없습니다.")
self.set_status("상태: 삭제 실패(ID 없음)")
return
book_id = item.text()
reply = QMessageBox.question(
self,
"삭제 확인",
f"도서 ID '{book_id}'를 삭제하시겠습니까?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
self.set_status("상태: 삭제 취소")
return
ok = self.db.execute_query("DELETE FROM books WHERE book_id = %s", (book_id,))
if ok:
self.msg_info("성공", "삭제되었습니다.")
self.set_status("상태: 삭제 성공")
self.load_books()
else:
self.msg_warn("실패", "삭제에 실패했습니다.")
self.set_status("상태: 삭제 실패(DB 오류)")
def load_books(self):
rows = self.db.fetch_data("SELECT * FROM books")
self.fill_table(rows, self._transform_book)
self.set_status(f"상태: 조회 완료({len(rows)}건)")
def _transform_book(self, col, value):
if col == 4:
return "대출 가능" if value else "대출 중"
return value
def search_book(self):
conditions = []
params = []
sid = self.search_id.text().strip()
stitle = self.search_title.text().strip()
sauthor = self.search_author.text().strip()
spub = self.search_pub.text().strip()
if sid:
conditions.append("book_id LIKE %s")
params.append(f"%{sid}%")
if stitle:
conditions.append("title LIKE %s")
params.append(f"%{stitle}%")
if sauthor:
conditions.append("author LIKE %s")
params.append(f"%{sauthor}%")
if spub:
conditions.append("publisher LIKE %s")
params.append(f"%{spub}%")
if not conditions:
self.load_books()
return
query = "SELECT * FROM books WHERE " + " AND ".join(conditions)
rows = self.db.fetch_data(query, tuple(params))
self.fill_table(rows, self._transform_book)
self.set_status(f"상태: 검색 완료({len(rows)}건)")
(3) LibrarySystem.py 수정
...
class MemberManagerPage(BasePage):
def __init__(self, db): # main 에서 db 객체를 넘겨 받습니다.
super().__init__("회원 관리 (등록 및 조회)")
self.db = db
..
self.pages = QStackedWidget()
self.page_member = MemberManagerPage(db)
self.page_book = BookManagerPage(db) # db 객체를 MemberManagerPage 로 전달합니다.
self.page_circulation = CirculationPage()
(4) main.py 수정
# main.py
...
from DB.database_manager import DatabaseManager
if __name__ == "__main__":
...
db = DatabaseManager() # db 객체를 main 에서 생성하고
window = LibrarySystem(db) # LibrarySystem 으로 전달합니다.
ㄴ 2.1 import + 주석
더보기
from PySide6.QtWidgets import QLineEdit, QPushButton, QMessageBox
from Pages.base_page import BasePage
- QLineEdit: 입력/검색 텍스트 박스 구성에 필요
- QPushButton: "회원 등록", "회원 검색" 버튼 구성에 필요
- QMessageBox: 삭제 확인(Yes/No) 대화상자에 필요
- BasePage: 공통 UI 골격(타이틀/상태/테이블/하단버튼/슬롯 레이아웃)을 상속하기 위함
ㄴ 2.2 GUI + 주석
더보기
- 입력(등록) 영역: form_layout에 위젯을 "추가"하여 구성
- BasePage에서 self.form_layout을 미리 만들어 둔 덕분에, 파생 페이지는 addWidget만 하면 된다.
- 검색 영역: search_layout에 위젯을 "추가"하여 구성
- BasePage의 self.search_layout 활용
- 테이블: set_table(5, [...])로 도서 컬럼 구조 확정
- ["ID", "제목", "저자", "출판사", "상태"]
- ["ID", "제목", "저자", "출판사", "상태"]
- 하단 버튼: 새로 만들지 않고 BasePage 버튼 재사용
- self.btn_refresh.setText(...)
- self.btn_delete.setText(...)
ㄴ 2.3 Signal & Slot + 주석
더보기
공통 버튼 재사용
- BasePage 생성자에서 기본 훅이 연결되어 있습니다.
- btn_refresh -> BasePage.on_refresh
- btn_delete -> BasePage.on_delete
- book_manager_page 에서는 이를 끊고 페이지 로직에 다시 연결합니다.
try:
self.btn_refresh.clicked.disconnect()
except TypeError:
pass
try:
self.btn_delete.clicked.disconnect()
except TypeError:
pass
self.btn_refresh.clicked.connect(self.load_books)
self.btn_delete.clicked.connect(self.delete_book)
- BasePage 생성자에서 이미 clicked.connect(self.on_refresh/on_delete)를 수행했다.
- 파생 페이지에서 동일 버튼을 사용하려면 기존 연결을 해제한 뒤, 페이지 로직에 연결해야 한다.
- disconnect()는 연결이 없으면 TypeError가 날 수 있으므로 예외 처리를 둔다.
ㄴ 2.4. <DatabaseManager> 요약
더보기
MemberManagerPage(BasePage)의 구조는 다음 4블록으로 이해하면 됩니다.
- "입력(등록) UI" 구성
- form_layout에 입력란 4개 + 등록 버튼 1개 배치
- 도서 ID/제목/저자/출판사 + "도서 등록" 버튼
- "검색 UI" 구성
- search_layout에 검색란 4개 + 검색 버튼 1개 배치
- ID/제목/저자/출판사 조건 + "도서 검색" 버튼
- "테이블 설정"
- set_table로 컬럼/헤더 세팅
- 조회/검색 결과 출력(5열)
- "하단 공통 버튼 재사용"
- BasePage의 btn_refresh/btn_delete 텍스트 변경
- 기본 연결(disconnect) 후 기능 연결(connect)
- 최초 1회 load_members로 초기 데이터 로드
- "새로고침"(전체 조회), "선택 도서 삭제"(선택 행 삭제)
ㄴ 2.5. <DatabaseManager> 상세 주석
더보기
(1) 초기화 흐름
- UI 위젯 생성 및 레이아웃 배치
- 테이블 헤더 설정
- BasePage 버튼 재사용 설정 + 시그널 재연결
- 최초 load_books() 호출로 초기 데이터 로딩
(2) add_book()
- 입력값 strip()로 공백 제거
- 입력 누락 검사
- INSERT 실행
- 성공 시 reload(load_books), 실패 시 경고 + 상태 업데이트
(3) load_books()
- SELECT * FROM books로 전체 조회
- BasePage의 fill_table(rows, transform_func) 사용
- status에 "조회 완료(n건)" 표시
(4) _transform_book()
- 5번째 컬럼(인덱스 4)을 사람이 읽기 좋은 문자열로 변환
- 예: boolean/tinyint 값 -> "대출 가능"/"대출 중"
(5) search_book()
- 입력된 검색 조건만 동적으로 WHERE 절에 추가
- params는 %keyword% 형태로 LIKE 검색
- 조건이 하나도 없으면 전체 조회로 fallback
- 결과 테이블 출력 + status에 "검색 완료(n건)" 표시
(6) delete_book()
- 선택 행 존재 여부 확인
- 선택 행의 0열(ID) 추출
- QMessageBox로 삭제 확인(Yes/No)
- DELETE 실행 후 갱신(load_books)
3. 실행 테스트
더보기
- 프로그램 실행 후 "도서 관리" 페이지 진입
- status: "조회 완료(n건)" 표시 확인
- 테이블에 목록 출력 확인
- 등록 테스트
- 입력 4개 모두 채우고 "도서 등록" 클릭
- 성공 메시지 + status: "등록 성공" 확인
- 목록에 추가 반영 확인
- 등록 실패 테스트(중복 ID)
- 기존 ID로 등록
- 경고 + status: "등록 실패(중복)" 확인
- 검색 테스트
- 제목 일부 입력 후 "도서 검색"
- 결과 목록 필터링 확인
- status: "검색 완료(n건)" 확인
- 삭제 테스트
- 테이블에서 특정 행 선택 후 "선택 도서 삭제"
- 삭제 확인 창 Yes -> 삭제 후 목록 갱신 확인
- No -> status: "삭제 취소" 확인
4. 학습 주요 포인트
더보기
- "상속 기반 공통 페이지"에서 중복 UI를 피하는 방법
- BasePage 버튼을 새로 만들지 말고 재사용
- BasePage 버튼을 새로 만들지 말고 재사용
- Signal 재연결 패턴
- BasePage에서 미리 connect한 것을 disconnect() 후 원하는 slot으로 재연결
- BasePage에서 미리 connect한 것을 disconnect() 후 원하는 slot으로 재연결
- 동적 검색 쿼리 구성 패턴
- conditions 리스트 + params 리스트
- WHERE " AND ".join(conditions)
- UI 피드백 설계(status 라벨)
- 사용자에게 "무엇이 되었는지" 즉시 보여주는 설계 습관
- 사용자에게 "무엇이 되었는지" 즉시 보여주는 설계 습관
- 변환 함수(transform_func)로 데이터 표시 품질 개선
- DB 원본 값을 UI 친화적인 문자열로 변환
단계별 완성 파일