1. 목표

더보기

 

  • BasePage(공통 템플릿)를 상속하여 MemberManagerPage를 확장한다.
  • 회원 CRUD 중 최소 기능(등록/조회/검색/삭제)을 구현한다.
  • DatabaseManager를 통해 MySQL 테이블 members에 대해 INSERT/SELECT/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) 이전 단계에서 사용했던 member_manager_page.py를 사용합니다.

# Page/member_manager_page.py
from PySide6.QtWidgets import QLineEdit, QPushButton, QMessageBox
from Pages.base_page import BasePage

class MemberManagerPage(BasePage):
    def __init__(self, db):
        super().__init__("회원 관리 (등록 및 조회)")
        self.db = db

        # 1) 입력(등록) 폼
        self.input_id = QLineEdit(); self.input_id.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_add = QPushButton("회원 등록")
        btn_add.setStyleSheet("background-color: #8e44ad; color: white; padding: 10px;")
        btn_add.clicked.connect(self.add_member)

        self.form_layout.addWidget(self.input_id)
        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_add)

        # 2) 검색 폼
        self.search_id = QLineEdit(); self.search_id.setPlaceholderText("회원 ID 검색")
        self.search_name = QLineEdit(); self.search_name.setPlaceholderText("이름 검색")
        self.search_phone = QLineEdit(); self.search_phone.setPlaceholderText("연락처 검색")
        self.search_email = QLineEdit(); self.search_email.setPlaceholderText("이메일 검색")

        btn_search = QPushButton("회원 검색")
        btn_search.setStyleSheet("background-color: #27ae60; color: white; padding: 10px;")
        btn_search.clicked.connect(self.search_member)

        self.search_layout.addWidget(self.search_id)
        self.search_layout.addWidget(self.search_name)
        self.search_layout.addWidget(self.search_phone)
        self.search_layout.addWidget(self.search_email)
        self.search_layout.addWidget(btn_search)

        # 3) 테이블
        self.set_table(4, ["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_members)
        self.btn_delete.clicked.connect(self.delete_member)

        # 최초 로딩
        self.load_members()

    def add_member(self):
        member_id = self.input_id.text().strip()
        name = self.input_name.text().strip()
        phone = self.input_phone.text().strip()
        email = self.input_email.text().strip()

        data = (member_id, name, phone, email)

        if "" in data:
            self.msg_warn("경고", "모든 정보를 입력하세요.")
            self.set_status("상태: 등록 실패(입력 누락)")
            return

        ok = self.db.execute_query(
            "INSERT INTO members (member_id, name, mobile, email) VALUES (%s, %s, %s, %s)",
            data
        )

        if ok:
            self.msg_info("성공", "등록되었습니다.")
            self.set_status("상태: 등록 성공")
            self.load_members()
        else:
            self.msg_warn("실패", "이미 있는 ID입니다.")
            self.set_status("상태: 등록 실패(중복)")

    def delete_member(self):
        row = self.current_row()
        if row < 0:
            self.msg_warn("안내", "삭제할 회원을 테이블에서 선택하세요.")
            self.set_status("상태: 삭제 실패(선택 없음)")
            return

        mid_item = self.table.item(row, 0)
        if mid_item is None:
            self.msg_warn("오류", "선택된 행에서 회원 ID를 읽을 수 없습니다.")
            self.set_status("상태: 삭제 실패(ID 없음)")
            return

        mid = mid_item.text()

        reply = QMessageBox.question(
            self,
            "삭제 확인",
            f"회원 ID '{mid}'를 삭제하시겠습니까?",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        )
        if reply != QMessageBox.StandardButton.Yes:
            self.set_status("상태: 삭제 취소")
            return

        ok = self.db.execute_query("DELETE FROM members WHERE member_id = %s", (mid,))
        if ok:
            self.msg_info("성공", "삭제되었습니다.")
            self.set_status("상태: 삭제 성공")
            self.load_members()
        else:
            self.msg_warn("실패", "삭제에 실패했습니다.")
            self.set_status("상태: 삭제 실패(DB 오류)")

    def load_members(self):
        rows = self.db.fetch_data("SELECT * FROM members")
        self.fill_table(rows)
        self.set_status(f"상태: 조회 완료({len(rows)}건)")

    def search_member(self):
        conditions = []
        params = []

        sid = self.search_id.text().strip()
        sname = self.search_name.text().strip()
        sphone = self.search_phone.text().strip()
        semail = self.search_email.text().strip()

        if sid:
            conditions.append("member_id LIKE %s")
            params.append(f"%{sid}%")
        if sname:
            conditions.append("name LIKE %s")
            params.append(f"%{sname}%")
        if sphone:
            conditions.append("mobile LIKE %s")
            params.append(f"%{sphone}%")
        if semail:
            conditions.append("email LIKE %s")
            params.append(f"%{semail}%")

        if not conditions:
            self.load_members()
            return

        query = "SELECT * FROM members WHERE " + " AND ".join(conditions)
        rows = self.db.fetch_data(query, tuple(params))
        self.fill_table(rows)
        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) # db 객체를 MemberManagerPage 로 전달합니다.
        self.page_book = BookManagerPage()
        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 + 주석

더보기

 

  • BasePage에 이미 마련된 슬롯(form_layout, search_layout, table, bottom_layout, btn_refresh, btn_delete)을 활용한다.
  • 하단 버튼을 새로 만들지 않고, BasePage 공통 버튼을 재사용한다.
  • DB 작업은 db.execute_query, db.fetch_data로 처리한다.
  • 결과 메시지는 msg_info/msg_warn, 진행 상태는 set_status로 표현한다.

 

 

ㄴ 2.3 Signal & Slot + 주석

더보기

(1) 등록 버튼

btn_add.clicked.connect(self.add_member)

 

  • 클릭 시 add_member() 실행
  • INSERT 후 성공하면 load_members()로 테이블 갱신

 

 

 

(2) 검색 버튼

btn_search.clicked.connect(self.search_member)
  • 클릭 시 search_member() 실행
  • 입력된 검색 조건으로 SELECT WHERE 동적 생성


 

 

(3) 공통 버튼 재사용

  • BasePage 생성자에서 기본 훅이 연결되어 있습니다.
    • btn_refresh -> BasePage.on_refresh
    • btn_delete -> BasePage.on_delete
  • MemberManagerPage에서는 이를 끊고 페이지 로직에 다시 연결합니다.
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_members)
self.btn_delete.clicked.connect(self.delete_member)
 

 

ㄴ 2.4. <DatabaseManager> 요약

더보기

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

  1. "입력(등록) UI" 구성
    • form_layout에 입력란 4개 + 등록 버튼 1개 배치
  2. "검색 UI" 구성
    • search_layout에 검색란 4개 + 검색 버튼 1개 배치

  3. "테이블 설정"
    • set_table로 컬럼/헤더 세팅

  4. "하단 공통 버튼 재사용"
    • BasePage의 btn_refresh/btn_delete 텍스트 변경
    • 기본 연결(disconnect) 후 기능 연결(connect)
    • 최초 1회 load_members로 초기 데이터 로드


ㄴ 2.5. <DatabaseManager> 상세 주석

더보기

(A) 생성자 __init__의 책임

  • "화면 구성"과 "이벤트 연결"과 "초기 로드"를 담당합니다.

흐름:

  1. super().__init__("회원 관리 (등록 및 조회)")로 공통 UI 골격 생성
  2. 입력 영역 구성 + 등록 버튼 연결
  3. 검색 영역 구성 + 검색 버튼 연결
  4. 테이블 헤더 설정
  5. 하단 공통 버튼 재사용 세팅(중복 방지)
  6. load_members()로 초기 데이터 표시

 

(B) add_member: 등록 로직

  • 입력값을 strip()으로 정리한 뒤 빈 값 검증
  • INSERT 성공 시:
    • 성공 메시지
    • status 업데이트
    • 목록 새로고침(load_members)
  • 실패 시:
    • 실패 메시지(중복 ID 가능성)
    • status 업데이트
  •  

(C) delete_member: 삭제 로직

  • 현재 선택 행이 있는지 검사
  • 선택 행의 첫 컬럼(ID)을 읽어 member_id로 사용
  • 삭제 confirm(Yes/No)
  • DELETE 성공 시 목록 갱신

 

(D) load_members: 전체 조회

  • SELECT * FROM members 결과를 fill_table(rows)로 테이블에 채움
  • status에 조회 건수 표시

 

(E) search_member: 조건 검색

  • 입력된 조건만 conditions에 누적
  • AND로 조합해 WHERE 절 생성
  • 조건이 없으면 전체 조회(load_members)
  • 결과는 fill_table + status 업데이트


3. 실행 테스트

더보기

 

  1. 프로그램 실행 → 메인 창 표시
  2. "회원 관리" 페이지 이동
  3. 상단 입력폼에 값 입력 → "회원 등록" 클릭
  4. 테이블에 등록 데이터가 표시되는지 확인
  5. 검색 폼에 조건 입력 → "회원 검색" 클릭
  6. 검색 결과가 필터링되는지 확인
  7. 테이블에서 한 행 선택 → 하단 "선택 회원 삭제" 클릭
  8. 삭제 확인창 Yes 선택 → 행이 삭제되고 목록 갱신되는지 확인

 


4. 학습 주요 포인트

더보기

 

  • BasePage 공통 슬롯 사용법
    • form_layout/search_layout/bottom_layout에 위젯을 "추가"해서 페이지 완성
  • 버튼 중복 해결 패턴
    • "공통 버튼을 새로 만들지 말고 재사용"
    • disconnect 후 connect로 기능만 교체
  • DB 로직의 역할 분리
    • UI(Page)에서는 SQL 호출만
    • 실제 연결/커밋/조회는 DatabaseManager가 담당
  • 동적 WHERE 조건 생성
    • 입력값이 있는 조건만 누적하고 AND로 결합
  • 사용자 경험(UX)
    • 삭제 확인(Yes/No)
    • status 라벨로 현재 상태/건수 표시

 


단계별 완성 파일