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 + 주석

더보기
  1. 입력(등록) 영역: form_layout에 위젯을 "추가"하여 구성
    • BasePage에서 self.form_layout을 미리 만들어 둔 덕분에, 파생 페이지는 addWidget만 하면 된다.
  2. 검색 영역: search_layout에 위젯을 "추가"하여 구성
    • BasePage의 self.search_layout 활용
  3. 테이블: set_table(5, [...])로 도서 컬럼 구조 확정
    • ["ID", "제목", "저자", "출판사", "상태"]

  4. 하단 버튼: 새로 만들지 않고 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블록으로 이해하면 됩니다.

  1. "입력(등록) UI" 구성
    • form_layout에 입력란 4개 + 등록 버튼 1개 배치
    • 도서 ID/제목/저자/출판사 + "도서 등록" 버튼

  2. "검색 UI" 구성
    • search_layout에 검색란 4개 + 검색 버튼 1개 배치
    • ID/제목/저자/출판사 조건 + "도서 검색" 버튼

  3. "테이블 설정"
    • set_table로 컬럼/헤더 세팅
    • 조회/검색 결과 출력(5열)
  4. "하단 공통 버튼 재사용"
    • 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. 실행 테스트

더보기
  1. 프로그램 실행 후 "도서 관리" 페이지 진입
    • status: "조회 완료(n건)" 표시 확인
    • 테이블에 목록 출력 확인

  2. 등록 테스트
    • 입력 4개 모두 채우고 "도서 등록" 클릭
    • 성공 메시지 + status: "등록 성공" 확인
    • 목록에 추가 반영 확인

  3. 등록 실패 테스트(중복 ID)
    • 기존 ID로 등록
    • 경고 + status: "등록 실패(중복)" 확인

  4. 검색 테스트
    • 제목 일부 입력 후 "도서 검색"
    • 결과 목록 필터링 확인
    • status: "검색 완료(n건)" 확인

  5. 삭제 테스트
    • 테이블에서 특정 행 선택 후 "선택 도서 삭제"
    • 삭제 확인 창 Yes -> 삭제 후 목록 갱신 확인
    • No -> status: "삭제 취소" 확인

 


4. 학습 주요 포인트

더보기
  1. "상속 기반 공통 페이지"에서 중복 UI를 피하는 방법
    • BasePage 버튼을 새로 만들지 말고 재사용

  2. Signal 재연결 패턴
    • BasePage에서 미리 connect한 것을 disconnect() 후 원하는 slot으로 재연결

  3. 동적 검색 쿼리 구성 패턴
    • conditions 리스트 + params 리스트
    • WHERE " AND ".join(conditions)

  4. UI 피드백 설계(status 라벨)
    • 사용자에게 "무엇이 되었는지" 즉시 보여주는 설계 습관

  5. 변환 함수(transform_func)로 데이터 표시 품질 개선
    • DB 원본 값을 UI 친화적인 문자열로 변환


단계별 완성 파일