10. 구현 - 대여 페이지

1. 목표
더보기


- BasePage의 공통 UI 골격을 재사용하여 CirculationPage에서 다음 기능을 구현한다.
- 회원 조회(회원 ID/이름/연락처/이메일 조건 기반)
- 선택 회원의 대출 목록 조회(issues + books JOIN)
- 도서 대출(issues INSERT + books.is_available 갱신)
- 도서 반납(issues DELETE + books.is_available 갱신)
- 대출 연장(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의 두 슬롯 레이아웃을 "의미 재정의"해서 사용합니다.
- 회원 조회 라인: BasePage.form_layout
- 입력 필드 4개: 회원 ID/이름/연락처/이메일
- 버튼 1개: "회원 조회"
- 목표: 조회 결과가 1명일 때 current_member_id 설정 후 대출 목록 로드
- 도서 대출 라인: BasePage.search_layout 재사용
- 입력 필드 2개: 도서 ID/도서 제목
- 버튼 1개: "대출 실행"
- 목표: 회원이 선택되어 있어야 대출 가능
- 대출 목록 테이블
- 5컬럼: ["도서 ID", "제목", "대출일", "연장횟수", "회원ID"]
- load_loan_list()가 issues JOIN books 결과를 채움
- 하단 버튼 재사용
- BasePage의 두 버튼을 "연장", "반납"으로 바꿔 재사용
- 즉, Circulation은 별도의 하단 버튼을 추가 생성하지 않음
ㄴ 2.3 Signal & Slot + 주석
더보기
- 회원 조회
- btn_member_search.clicked.connect(self.search_member)
- 대출 실행
- btn_loan.clicked.connect(self.loan_book)
- 하단 버튼 재사용(핵심)
- 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블록으로 이해하면 됩니다.
- 상태 변수
- self.current_member_id: 현재 조회/선택된 회원의 PK 역할
- 회원 조회 UI + 로직
- 입력 조건으로 members 테이블 조회
- 결과가 1명일 때만 성공 처리(대출 기능 활성화 관점)
- 대출 입력 UI + 로직
- 도서 ID 또는 도서 제목으로 도서 검색
- books.is_available 확인 후 대출 처리
- 대출 목록 조회
- issues와 books를 JOIN하여 현재 회원의 대출 목록 출력
- issues와 books를 JOIN하여 현재 회원의 대출 목록 출력
- 반납
- 선택된 테이블 행 기준으로 issues 삭제 + books.is_available 복구
- 선택된 테이블 행 기준으로 issues 삭제 + books.is_available 복구
- 연장
- 선택된 테이블 행 기준으로 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. 실행 테스트
더보기
- 회원 조회
- 회원 ID에 존재하는 값 입력 후 "회원 조회"
- 성공 시 상태: "상태: 회원 조회 성공(회원ID)"
- 대출 목록이 테이블에 출력
- 대출 실행(정상)
- 도서 ID 입력 후 "대출 실행"
- books.is_available == TRUE인 경우
- 성공 시:
- issues에 레코드 생성
- books.is_available FALSE로 변경
- 상태: "상태: 대출 성공"
- 목록 갱신 확인
- 대출 실행(이미 대출중)
- 동일 도서 다시 대출 시도
- "이미 대출중인 도서" 경고 확인
- 반납
- 테이블에서 대출 도서 선택 후 "반납"
- 확인창 Yes
- 성공 시:
- issues 레코드 삭제
- books.is_available TRUE로 복구
- 상태: "상태: 반납 성공"
- 연장
- 테이블에서 도서 선택 후 "연장"
- issue_date 갱신, renew_count + 1 확인
- 상태: "상태: 연장 성공"
4. 학습 주요 포인트
더보기
- "상속 기반 공통 페이지"에서 중복 UI를 피하는 방법
- BasePage 버튼을 새로 만들지 말고 재사용
- BasePage 버튼을 새로 만들지 말고 재사용
- Signal 재연결 패턴
- BasePage가 버튼을 이미 만들기 때문에, 파생 페이지는 "버튼 생성"이 아니라 "버튼 의미 변경 + 동작 교체"
- BasePage가 버튼을 이미 만들기 때문에, 파생 페이지는 "버튼 생성"이 아니라 "버튼 의미 변경 + 동작 교체"
- 조회 결과 처리 정책(1명만 성공)
- Circulation은 "정확히 1명"을 선택해야 한다.
- 다중 결과를 허용하려면 "검색 결과 목록 테이블"을 별도로 만들어 선택 UI를 제공해야 한다
- 대출/반납의 원자성(트랜잭션)
- 현재 DatabaseManager는 매 쿼리마다 연결/커밋/종료를 수행한다.
- 대출(issues INSERT + books UPDATE) / 반납(issues DELETE + books UPDATE)은 둘 중 하나만 성공하면 데이터 불일치가 발생한다.
- 실무 관점에서는 트랜잭션(한 연결에서 BEGIN/COMMIT/ROLLBACK)으로 묶는 것이 정석이다.
단계별 완성 파일