1. 목표

더보기
  • BasePage의 공통 UI 골격을 재사용하여 CirculationPage에서 다음 기능을 구현한다.
    1. 회원 조회(회원 ID/이름/연락처/이메일 조건 기반)
    2. 선택 회원의 대출 목록 조회(issues + books JOIN)
    3. 도서 대출(issues INSERT + books.is_available 갱신)
    4. 도서 반납(issues DELETE + books.is_available 갱신)
    5. 대출 연장(issues issue_date 갱신 + renew_count 증가)
  • BasePage의 btn_refresh, btn_delete를 "연장", "반납" 버튼으로 의미 변경하고,
    기존 Signal을 해제 후 페이지 전용 Slot으로 재연결한다.


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) 이전 단계에서 사용했던 circulation_page.py 사용합니다.

# Pages/circulation_page.py

from PySide6.QtWidgets import QLineEdit, QPushButton, QMessageBox
from Pages.base_page import BasePage

class CirculationPage(BasePage):
    def __init__(self, db):
        super().__init__("대출 및 반납")
        self.db = db
        self.current_member_id = None

        # 1) 회원 조회 라인 (BasePage.form_layout 사용)
        self.input_mid = QLineEdit()
        self.input_mid.setPlaceholderText("회원 ID")

        self.input_name = QLineEdit()
        self.input_name.setPlaceholderText("이름")

        self.input_phone = QLineEdit()
        self.input_phone.setPlaceholderText("연락처")

        self.input_email = QLineEdit()
        self.input_email.setPlaceholderText("이메일")

        btn_member_search = QPushButton("회원 조회")
        btn_member_search.setStyleSheet("background-color: #3498db; color: white; padding: 10px;")
        btn_member_search.clicked.connect(self.search_member)

        self.form_layout.addWidget(self.input_mid)
        self.form_layout.addWidget(self.input_name)
        self.form_layout.addWidget(self.input_phone)
        self.form_layout.addWidget(self.input_email)
        self.form_layout.addWidget(btn_member_search)

        # 2) 도서 대출 라인 (BasePage.search_layout 재사용)
        self.input_book_id = QLineEdit()
        self.input_book_id.setPlaceholderText("대출할 도서 ID")

        self.input_book_title = QLineEdit()
        self.input_book_title.setPlaceholderText("대출할 도서 제목")

        btn_loan = QPushButton("대출 실행")
        btn_loan.setStyleSheet("background-color: #27ae60; color: white; padding: 10px;")
        btn_loan.clicked.connect(self.loan_book)

        self.search_layout.addWidget(self.input_book_id)
        self.search_layout.addWidget(self.input_book_title)
        self.search_layout.addWidget(btn_loan)

        # 3) 테이블
        self.set_table(5, ["도서 ID", "제목", "대출일", "연장횟수", "회원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.renew_book)
        self.btn_delete.clicked.connect(self.return_book)

        self.set_status("상태: 회원 조회 대기")

    # ---------------------------
    # 회원 조회 -> 대출 목록 로드
    # ---------------------------
    def search_member(self):
        conditions = []
        params = []

        mid = self.input_mid.text().strip()
        name = self.input_name.text().strip()
        phone = self.input_phone.text().strip()
        email = self.input_email.text().strip()

        if mid:
            conditions.append("member_id = %s")
            params.append(mid)
        if name:
            conditions.append("name = %s")
            params.append(name)
        if phone:
            conditions.append("mobile = %s")
            params.append(phone)
        if email:
            conditions.append("email = %s")
            params.append(email)

        if not conditions:
            self.msg_warn("경고", "조회할 회원 정보를 하나 이상 입력해주세요.")
            self.set_status("상태: 회원 조회 실패(입력 없음)")
            return

        query = "SELECT member_id, name FROM members WHERE " + " AND ".join(conditions)
        rows = self.db.fetch_data(query, tuple(params))

        if len(rows) == 1:
            self.current_member_id = rows[0][0]
            self.msg_info("성공", f"회원 '{rows[0][1]}'님이 조회되었습니다.")
            self.set_status(f"상태: 회원 조회 성공({self.current_member_id})")
            self.load_loan_list()
        elif len(rows) > 1:
            self.msg_warn("알림", "검색 결과가 여러 명입니다. 더 구체적인 정보로 검색해주세요.")
            self.set_status("상태: 회원 조회 실패(다중 결과)")
            self.current_member_id = None
            self.table.setRowCount(0)
        else:
            self.msg_warn("실패", "회원을 찾을 수 없습니다.")
            self.set_status("상태: 회원 조회 실패(0건)")
            self.current_member_id = None
            self.table.setRowCount(0)

    def load_loan_list(self):
        if not self.current_member_id:
            return

        query = """
            SELECT b.book_id, b.title, i.issue_date, i.renew_count, i.member_id
            FROM issues i
            JOIN books b ON i.book_id = b.book_id
            WHERE i.member_id = %s
        """
        rows = self.db.fetch_data(query, (self.current_member_id,))
        self.fill_table(rows)
        self.set_status(f"상태: 대출 목록 조회 완료({len(rows)}건)")

    # ---------------------------
    # 대출/반납/연장
    # ---------------------------
    def loan_book(self):
        if not self.current_member_id:
            self.msg_warn("경고", "먼저 회원을 조회해주세요.")
            self.set_status("상태: 대출 실패(회원 미조회)")
            return

        book_id = self.input_book_id.text().strip()
        book_title = self.input_book_title.text().strip()

        if not book_id and not book_title:
            self.msg_warn("경고", "도서 ID 또는 제목을 입력해주세요.")
            self.set_status("상태: 대출 실패(도서 입력 없음)")
            return

        target_book = None

        if book_id:
            rows = self.db.fetch_data(
                "SELECT book_id, is_available FROM books WHERE book_id = %s",
                (book_id,)
            )
            if rows:
                target_book = rows[0]

        else:
            rows = self.db.fetch_data(
                "SELECT book_id, is_available FROM books WHERE title = %s",
                (book_title,)
            )
            if len(rows) > 1:
                self.msg_warn("알림", "동일한 제목의 도서가 여러 권입니다. ID로 대출해주세요.")
                self.set_status("상태: 대출 실패(동일 제목 다중)")
                return
            if rows:
                target_book = rows[0]

        if not target_book:
            self.msg_warn("실패", "도서를 찾을 수 없습니다.")
            self.set_status("상태: 대출 실패(도서 없음)")
            return

        real_book_id, is_available = target_book[0], target_book[1]

        if not is_available:
            self.msg_warn("불가", "이미 대출중인 도서입니다.")
            self.set_status("상태: 대출 실패(대출중)")
            return

        # 대출 등록 -> 도서 상태 변경(트랜잭션은 아니므로 성공 여부 체크를 강화)
        ok_issue = self.db.execute_query(
            "INSERT INTO issues (book_id, member_id) VALUES (%s, %s)",
            (real_book_id, self.current_member_id)
        )
        if not ok_issue:
            self.msg_warn("실패", "대출 등록에 실패했습니다(DB 오류).")
            self.set_status("상태: 대출 실패(DB 오류)")
            return

        ok_update = self.db.execute_query(
            "UPDATE books SET is_available = FALSE WHERE book_id = %s",
            (real_book_id,)
        )
        if not ok_update:
            # 이 경우 데이터 불일치 위험이 있으므로 강하게 알림
            self.msg_warn("경고", "대출은 등록되었으나 도서 상태 갱신에 실패했습니다. 관리자 확인이 필요합니다.")
            self.set_status("상태: 대출 경고(상태 갱신 실패)")
        else:
            self.msg_info("성공", f"도서({real_book_id})가 대출되었습니다.")
            self.set_status("상태: 대출 성공")

        self.input_book_id.clear()
        self.input_book_title.clear()
        self.load_loan_list()

    def return_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_delete = self.db.execute_query("DELETE FROM issues WHERE book_id = %s", (book_id,))
        if not ok_delete:
            self.msg_warn("실패", "반납 처리(대출기록 삭제)에 실패했습니다.")
            self.set_status("상태: 반납 실패(DB 오류)")
            return

        ok_update = self.db.execute_query("UPDATE books SET is_available = TRUE WHERE book_id = %s", (book_id,))
        if not ok_update:
            self.msg_warn("경고", "반납은 처리되었으나 도서 상태 갱신에 실패했습니다. 관리자 확인이 필요합니다.")
            self.set_status("상태: 반납 경고(상태 갱신 실패)")
        else:
            self.msg_info("성공", "반납되었습니다.")
            self.set_status("상태: 반납 성공")

        self.load_loan_list()

    def renew_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()

        ok = self.db.execute_query(
            "UPDATE issues SET issue_date = NOW(), renew_count = renew_count + 1 WHERE book_id = %s",
            (book_id,)
        )
        if ok:
            self.msg_info("성공", "연장되었습니다.")
            self.set_status("상태: 연장 성공")
            self.load_loan_list()
        else:
            self.msg_warn("실패", "연장에 실패했습니다(DB 오류).")
            self.set_status("상태: 연장 실패(DB 오류)")

 

 

(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) 
        self.page_circulation = CirculationPage(db)  # db 객체를 MemberManagerPage 로 전달합니다.

 

 

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


ㄴ 2.2 GUI + 주석

더보기

CirculationPage는 BasePage의 두 슬롯 레이아웃을 "의미 재정의"해서 사용합니다.

  1. 회원 조회 라인: BasePage.form_layout
    • 입력 필드 4개: 회원 ID/이름/연락처/이메일
    • 버튼 1개: "회원 조회"
    • 목표: 조회 결과가 1명일 때 current_member_id 설정 후 대출 목록 로드

  2. 도서 대출 라인: BasePage.search_layout 재사용
    • 입력 필드 2개: 도서 ID/도서 제목
    • 버튼 1개: "대출 실행"
    • 목표: 회원이 선택되어 있어야 대출 가능

  3. 대출 목록 테이블
    • 5컬럼: ["도서 ID", "제목", "대출일", "연장횟수", "회원ID"]
    • load_loan_list()가 issues JOIN books 결과를 채움

  4. 하단 버튼 재사용
    • BasePage의 두 버튼을 "연장", "반납"으로 바꿔 재사용
    • 즉, Circulation은 별도의 하단 버튼을 추가 생성하지 않음

 

ㄴ 2.3 Signal & Slot + 주석

더보기
  1. 회원 조회
    • btn_member_search.clicked.connect(self.search_member)
  2. 대출 실행
    • btn_loan.clicked.connect(self.loan_book)
  3. 하단 버튼 재사용(핵심)
    • BasePage에서 이미 연결된 on_refresh, on_delete를 해제하고,
    • CirculationPage 전용 동작으로 교체
self.btn_refresh.setText("연장")
self.btn_delete.setText("반납")

self.btn_refresh.clicked.disconnect()
self.btn_delete.clicked.disconnect()

self.btn_refresh.clicked.connect(self.renew_book)
self.btn_delete.clicked.connect(self.return_book)
 

 

ㄴ 2.4. <circulation_page> 요약

더보기

MemberManagerPage(BasePage)의 구조는 다음 6블록으로 이해하면 됩니다.

  1. 상태 변수
    • self.current_member_id: 현재 조회/선택된 회원의 PK 역할
  2. 회원 조회 UI + 로직
    • 입력 조건으로 members 테이블 조회
    • 결과가 1명일 때만 성공 처리(대출 기능 활성화 관점)

  3. 대출 입력 UI + 로직
    • 도서 ID 또는 도서 제목으로 도서 검색
    • books.is_available 확인 후 대출 처리

  4. 대출 목록 조회
    • issues와 books를 JOIN하여 현재 회원의 대출 목록 출력

  5. 반납
    • 선택된 테이블 행 기준으로 issues 삭제 + books.is_available 복구

  6. 연장
    • 선택된 테이블 행 기준으로 issues 갱신(날짜 NOW, renew_count + 1)


ㄴ 2.5. <circulation_page> 상세 주석

더보기
from PySide6.QtWidgets import QLineEdit, QPushButton, QMessageBox
from .base_page import BasePage


class CirculationPage(BasePage):
    def __init__(self, db):
        # BasePage 공통 골격을 먼저 구성
        super().__init__("대출 및 반납")
        self.db = db

        # 현재 선택된(조회 성공한) 회원 ID를 저장
        # - 대출/목록 조회는 이 값이 있어야 가능
        self.current_member_id = None

        # ---------------------------
        # 1) 회원 조회 라인 (form_layout)
        # ---------------------------
        self.input_mid = QLineEdit()
        self.input_mid.setPlaceholderText("회원 ID")

        self.input_name = QLineEdit()
        self.input_name.setPlaceholderText("이름")

        self.input_phone = QLineEdit()
        self.input_phone.setPlaceholderText("연락처")

        self.input_email = QLineEdit()
        self.input_email.setPlaceholderText("이메일")

        btn_member_search = QPushButton("회원 조회")
        btn_member_search.setStyleSheet("background-color: #3498db; color: white; padding: 10px;")
        btn_member_search.clicked.connect(self.search_member)

        self.form_layout.addWidget(self.input_mid)
        self.form_layout.addWidget(self.input_name)
        self.form_layout.addWidget(self.input_phone)
        self.form_layout.addWidget(self.input_email)
        self.form_layout.addWidget(btn_member_search)

        # ---------------------------
        # 2) 도서 대출 라인 (search_layout 재사용)
        # ---------------------------
        self.input_book_id = QLineEdit()
        self.input_book_id.setPlaceholderText("대출할 도서 ID")

        self.input_book_title = QLineEdit()
        self.input_book_title.setPlaceholderText("대출할 도서 제목")

        btn_loan = QPushButton("대출 실행")
        btn_loan.setStyleSheet("background-color: #27ae60; color: white; padding: 10px;")
        btn_loan.clicked.connect(self.loan_book)

        self.search_layout.addWidget(self.input_book_id)
        self.search_layout.addWidget(self.input_book_title)
        self.search_layout.addWidget(btn_loan)

        # ---------------------------
        # 3) 테이블: 대출 목록 표시
        # ---------------------------
        self.set_table(5, ["도서 ID", "제목", "대출일", "연장횟수", "회원ID"])

        # ---------------------------
        # 4) 하단 공통 버튼 재사용
        #    - refresh/delete의 의미를 "연장/반납"으로 바꾼다.
        # ---------------------------
        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.renew_book)
        self.btn_delete.clicked.connect(self.return_book)

        self.set_status("상태: 회원 조회 대기")

    # ---------------------------
    # 회원 조회
    # - 조건을 조합해 members를 조회
    # - 결과가 1명일 때만 current_member_id 확정
    # ---------------------------
    def search_member(self):
        conditions = []
        params = []

        mid = self.input_mid.text().strip()
        name = self.input_name.text().strip()
        phone = self.input_phone.text().strip()
        email = self.input_email.text().strip()

        if mid:
            conditions.append("member_id = %s")
            params.append(mid)
        if name:
            conditions.append("name = %s")
            params.append(name)
        if phone:
            conditions.append("mobile = %s")
            params.append(phone)
        if email:
            conditions.append("email = %s")
            params.append(email)

        if not conditions:
            self.msg_warn("경고", "조회할 회원 정보를 하나 이상 입력해주세요.")
            self.set_status("상태: 회원 조회 실패(입력 없음)")
            return

        query = "SELECT member_id, name FROM members WHERE " + " AND ".join(conditions)
        rows = self.db.fetch_data(query, tuple(params))

        # 조회 결과가 정확히 1명일 때만 성공 처리
        if len(rows) == 1:
            self.current_member_id = rows[0][0]
            self.msg_info("성공", f"회원 '{rows[0][1]}'님이 조회되었습니다.")
            self.set_status(f"상태: 회원 조회 성공({self.current_member_id})")
            self.load_loan_list()
        elif len(rows) > 1:
            self.msg_warn("알림", "검색 결과가 여러 명입니다. 더 구체적인 정보로 검색해주세요.")
            self.set_status("상태: 회원 조회 실패(다중 결과)")
            self.current_member_id = None
            self.table.setRowCount(0)
        else:
            self.msg_warn("실패", "회원을 찾을 수 없습니다.")
            self.set_status("상태: 회원 조회 실패(0건)")
            self.current_member_id = None
            self.table.setRowCount(0)

    # ---------------------------
    # 대출 목록 조회
    # - issues와 books를 JOIN해서 화면에 보여준다
    # ---------------------------
    def load_loan_list(self):
        if not self.current_member_id:
            return

        query = """
            SELECT b.book_id, b.title, i.issue_date, i.renew_count, i.member_id
            FROM issues i
            JOIN books b ON i.book_id = b.book_id
            WHERE i.member_id = %s
        """
        rows = self.db.fetch_data(query, (self.current_member_id,))
        self.fill_table(rows)
        self.set_status(f"상태: 대출 목록 조회 완료({len(rows)}건)")

    # ---------------------------
    # 대출 실행
    # - 회원이 조회되어 있어야 함
    # - 도서 ID 또는 제목으로 도서 찾기
    # - is_available 확인 후 issues INSERT + books UPDATE
    # ---------------------------
    def loan_book(self):
        if not self.current_member_id:
            self.msg_warn("경고", "먼저 회원을 조회해주세요.")
            self.set_status("상태: 대출 실패(회원 미조회)")
            return

        book_id = self.input_book_id.text().strip()
        book_title = self.input_book_title.text().strip()

        if not book_id and not book_title:
            self.msg_warn("경고", "도서 ID 또는 제목을 입력해주세요.")
            self.set_status("상태: 대출 실패(도서 입력 없음)")
            return

        target_book = None

        if book_id:
            rows = self.db.fetch_data(
                "SELECT book_id, is_available FROM books WHERE book_id = %s",
                (book_id,)
            )
            if rows:
                target_book = rows[0]
        else:
            rows = self.db.fetch_data(
                "SELECT book_id, is_available FROM books WHERE title = %s",
                (book_title,)
            )
            if len(rows) > 1:
                self.msg_warn("알림", "동일한 제목의 도서가 여러 권입니다. ID로 대출해주세요.")
                self.set_status("상태: 대출 실패(동일 제목 다중)")
                return
            if rows:
                target_book = rows[0]

        if not target_book:
            self.msg_warn("실패", "도서를 찾을 수 없습니다.")
            self.set_status("상태: 대출 실패(도서 없음)")
            return

        real_book_id, is_available = target_book[0], target_book[1]

        if not is_available:
            self.msg_warn("불가", "이미 대출중인 도서입니다.")
            self.set_status("상태: 대출 실패(대출중)")
            return

        # 중요: 아래 2개 쿼리는 "원자적으로" 묶이는 것이 이상적(트랜잭션).
        ok_issue = self.db.execute_query(
            "INSERT INTO issues (book_id, member_id) VALUES (%s, %s)",
            (real_book_id, self.current_member_id)
        )
        if not ok_issue:
            self.msg_warn("실패", "대출 등록에 실패했습니다(DB 오류).")
            self.set_status("상태: 대출 실패(DB 오류)")
            return

        ok_update = self.db.execute_query(
            "UPDATE books SET is_available = FALSE WHERE book_id = %s",
            (real_book_id,)
        )
        if not ok_update:
            self.msg_warn("경고", "대출은 등록되었으나 도서 상태 갱신에 실패했습니다. 관리자 확인이 필요합니다.")
            self.set_status("상태: 대출 경고(상태 갱신 실패)")
        else:
            self.msg_info("성공", f"도서({real_book_id})가 대출되었습니다.")
            self.set_status("상태: 대출 성공")

        self.input_book_id.clear()
        self.input_book_title.clear()
        self.load_loan_list()

    # ---------------------------
    # 반납
    # - 테이블에서 선택한 도서를 기준으로 issues 삭제 + books 상태 복구
    # ---------------------------
    def return_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_delete = self.db.execute_query("DELETE FROM issues WHERE book_id = %s", (book_id,))
        if not ok_delete:
            self.msg_warn("실패", "반납 처리(대출기록 삭제)에 실패했습니다.")
            self.set_status("상태: 반납 실패(DB 오류)")
            return

        ok_update = self.db.execute_query("UPDATE books SET is_available = TRUE WHERE book_id = %s", (book_id,))
        if not ok_update:
            self.msg_warn("경고", "반납은 처리되었으나 도서 상태 갱신에 실패했습니다. 관리자 확인이 필요합니다.")
            self.set_status("상태: 반납 경고(상태 갱신 실패)")
        else:
            self.msg_info("성공", "반납되었습니다.")
            self.set_status("상태: 반납 성공")

        self.load_loan_list()

    # ---------------------------
    # 연장
    # - 선택한 도서의 대출일 갱신 + 연장횟수 증가
    # ---------------------------
    def renew_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()

        ok = self.db.execute_query(
            "UPDATE issues SET issue_date = NOW(), renew_count = renew_count + 1 WHERE book_id = %s",
            (book_id,)
        )
        if ok:
            self.msg_info("성공", "연장되었습니다.")
            self.set_status("상태: 연장 성공")
            self.load_loan_list()
        else:
            self.msg_warn("실패", "연장에 실패했습니다(DB 오류).")
            self.set_status("상태: 연장 실패(DB 오류)")


3. 실행 테스트

더보기
  1. 회원 조회
    1. 회원 ID에 존재하는 값 입력 후 "회원 조회"
    2. 성공 시 상태: "상태: 회원 조회 성공(회원ID)"
    3. 대출 목록이 테이블에 출력
  2. 대출 실행(정상)
    • 도서 ID 입력 후 "대출 실행"
    • books.is_available == TRUE인 경우
    • 성공 시:
      • issues에 레코드 생성
      • books.is_available FALSE로 변경
      • 상태: "상태: 대출 성공"
      • 목록 갱신 확인

  3. 대출 실행(이미 대출중)
    • 동일 도서 다시 대출 시도
    • "이미 대출중인 도서" 경고 확인

  4. 반납
    • 테이블에서 대출 도서 선택 후 "반납"
    • 확인창 Yes
    • 성공 시:
      • issues 레코드 삭제
      • books.is_available TRUE로 복구
      • 상태: "상태: 반납 성공"

  5. 연장
    • 테이블에서 도서 선택 후 "연장"
    • issue_date 갱신, renew_count + 1 확인
    • 상태: "상태: 연장 성공"

 


4. 학습 주요 포인트

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

  2. Signal 재연결 패턴
    • BasePage가 버튼을 이미 만들기 때문에, 파생 페이지는 "버튼 생성"이 아니라 "버튼 의미 변경 + 동작 교체"

  3. 조회 결과 처리 정책(1명만 성공)
    • Circulation은 "정확히 1명"을 선택해야 한다.
    • 다중 결과를 허용하려면 "검색 결과 목록 테이블"을 별도로 만들어 선택 UI를 제공해야 한다

  4. 대출/반납의 원자성(트랜잭션)
    • 현재 DatabaseManager는 매 쿼리마다 연결/커밋/종료를 수행한다.
    • 대출(issues INSERT + books UPDATE) / 반납(issues DELETE + books UPDATE)은 둘 중 하나만 성공하면 데이터 불일치가 발생한다.
    • 실무 관점에서는 트랜잭션(한 연결에서 BEGIN/COMMIT/ROLLBACK)으로 묶는 것이 정석이다.


단계별 완성 파일