# -*- coding: utf-8 -*- """ 기본 테이블 클래스 모듈 커스텀 테이블 위젯의 기반 클래스를 정의합니다. 이 모듈은 다음 기능을 제공합니다: - 커스텀 스타일링 - 팀 확인 체크박스 - 편성 팝업 지원 - 드래그 앤 드롭 """ from typing import List, Dict, Any, Optional from PySide6.QtWidgets import ( QTableWidget, QTableWidgetItem, QHeaderView, QWidget, QHBoxLayout, QCheckBox, QPushButton, QAbstractItemView, QStyledItemDelegate ) from PySide6.QtCore import Qt, Signal, QPoint, QRect from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen from core.logger import get_logger from core.config import ConfigManager from core.signals import GlobalSignals from core.constants import TEAMS # 로거 설정 logger = get_logger(__name__) class TeamConfirmationWidget(QWidget): """ 팀 확인 체크박스 위젯 각 팀의 확인 상태를 표시하는 체크박스 그룹입니다. """ confirmation_changed = Signal(str, bool) # 팀명, 확인여부 def __init__(self, confirmations: Dict[str, bool] = None, parent=None): super().__init__(parent) self.confirmations = confirmations or {team: False for team in TEAMS} self.checkboxes: Dict[str, QCheckBox] = {} self._setup_ui() def _setup_ui(self): """UI 설정""" layout = QHBoxLayout(self) layout.setContentsMargins(4, 2, 4, 2) layout.setSpacing(2) for team in TEAMS: checkbox = QCheckBox() checkbox.setChecked(self.confirmations.get(team, False)) checkbox.setToolTip(team) checkbox.stateChanged.connect( lambda state, t=team: self._on_state_changed(t, state) ) # 팀 이니셜 라벨 checkbox.setStyleSheet(f""" QCheckBox {{ spacing: 2px; }} QCheckBox::indicator {{ width: 16px; height: 16px; }} QCheckBox::indicator:checked {{ background-color: #22c55e; border-radius: 3px; }} QCheckBox::indicator:unchecked {{ background-color: #64748b; border-radius: 3px; }} """) layout.addWidget(checkbox) self.checkboxes[team] = checkbox def _on_state_changed(self, team: str, state: int): """체크박스 상태 변경""" checked = state == Qt.Checked self.confirmations[team] = checked self.confirmation_changed.emit(team, checked) def set_confirmations(self, confirmations: Dict[str, bool]): """확인 상태 설정""" self.confirmations = confirmations for team, checkbox in self.checkboxes.items(): checkbox.blockSignals(True) checkbox.setChecked(confirmations.get(team, False)) checkbox.blockSignals(False) def get_confirmations(self) -> Dict[str, bool]: """확인 상태 반환""" return self.confirmations.copy() def all_confirmed(self) -> bool: """모든 팀 확인 여부""" return all(self.confirmations.values()) class TrainNumberDelegate(QStyledItemDelegate): """ 편성번호 셀 델리게이트 편성번호 위에 마우스를 올리면 최근 고장 목록 팝업을 표시합니다. """ train_hovered = Signal(str, int, int) # 편성번호, x, y train_left = Signal() def __init__(self, parent=None): super().__init__(parent) self.signals = GlobalSignals() def paint(self, painter: QPainter, option, index): """셀 렌더링""" super().paint(painter, option, index) # 값 가져오기 value = index.data(Qt.DisplayRole) if not value: return # 작업 여부 확인 (노란 느낌표) has_work = index.data(Qt.UserRole + 1) if has_work: self._draw_work_indicator(painter, option.rect) # 청소 유형 확인 cleaning_type = index.data(Qt.UserRole + 2) if cleaning_type == "중청소": self._draw_cleaning_indicator(painter, option.rect, "medium") elif cleaning_type == "대청소": self._draw_cleaning_indicator(painter, option.rect, "large") def _draw_work_indicator(self, painter: QPainter, rect: QRect): """작업 표시 (노란 느낌표)""" painter.save() # 느낌표 위치 (오른쪽 상단) x = rect.right() - 12 y = rect.top() + 4 painter.setPen(QPen(QColor("#f59e0b"), 2)) painter.setFont(QFont("Arial", 10, QFont.Bold)) painter.drawText(x, y + 10, "!") painter.restore() def _draw_cleaning_indicator(self, painter: QPainter, rect: QRect, cleaning_type: str): """청소 표시 (파란 네모 / 빨간 동그라미)""" painter.save() # 표시 위치 (왼쪽 상단) x = rect.left() + 2 y = rect.top() + 2 size = 12 if cleaning_type == "medium": # 파란 네모 painter.setPen(QPen(QColor("#3b82f6"), 2)) painter.setBrush(Qt.NoBrush) painter.drawRect(x, y, size, size) elif cleaning_type == "large": # 빨간 동그라미 painter.setPen(QPen(QColor("#ef4444"), 2)) painter.setBrush(Qt.NoBrush) painter.drawEllipse(x, y, size, size) painter.restore() def editorEvent(self, event, model, option, index): """에디터 이벤트 처리""" from PySide6.QtCore import QEvent if event.type() == QEvent.MouseMove: value = index.data(Qt.DisplayRole) if value: pos = event.globalPosition().toPoint() self.train_hovered.emit(value, pos.x(), pos.y()) return super().editorEvent(event, model, option, index) class BaseTable(QTableWidget): """ 기본 테이블 클래스 모든 커스텀 테이블이 상속받는 기반 클래스입니다. Attributes: config: 설정 관리자 signals: 전역 시그널 """ # 시그널 row_selected = Signal(int) row_double_clicked = Signal(int) cell_edited = Signal(int, str, str) # row, column_name, new_value def __init__(self, parent=None): super().__init__(parent) self.config = ConfigManager() self.signals = GlobalSignals() self._setup_table() self._apply_style() def _setup_table(self): """테이블 설정""" # 기본 설정 self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setAlternatingRowColors(True) self.setShowGrid(False) self.verticalHeader().setVisible(False) # 헤더 설정 header = self.horizontalHeader() header.setStretchLastSection(True) header.setSectionResizeMode(QHeaderView.Interactive) header.setDefaultAlignment(Qt.AlignCenter) # 행 높이 self.verticalHeader().setDefaultSectionSize(40) # 시그널 연결 self.itemSelectionChanged.connect(self._on_selection_changed) self.itemDoubleClicked.connect(self._on_double_clicked) self.itemChanged.connect(self._on_item_changed) def _apply_style(self): """스타일 적용""" theme = self.config.theme if theme == 'dark': bg_color = "#0f172a" alt_bg = "#1e293b" header_bg = "#334155" text_color = "#f8fafc" selected_bg = "#3b82f6" border_color = "#334155" hover_bg = "#1e3a5f" else: bg_color = "#ffffff" alt_bg = "#f8fafc" header_bg = "#e2e8f0" text_color = "#1e293b" selected_bg = "#3b82f6" border_color = "#e2e8f0" hover_bg = "#dbeafe" self.setStyleSheet(f""" QTableWidget {{ background-color: {bg_color}; color: {text_color}; border: 1px solid {border_color}; border-radius: 8px; font-family: 'GmarketSans'; font-size: 13px; }} QTableWidget::item {{ padding: 8px; border-bottom: 1px solid {border_color}; }} QTableWidget::item:selected {{ background-color: {selected_bg}; color: white; }} QTableWidget::item:hover {{ background-color: {hover_bg}; }} QTableWidget::item:alternate {{ background-color: {alt_bg}; }} QHeaderView::section {{ background-color: {header_bg}; color: {text_color}; padding: 12px 8px; border: none; border-bottom: 2px solid {border_color}; font-weight: bold; font-size: 13px; }} QScrollBar:vertical {{ background-color: {bg_color}; width: 10px; border-radius: 5px; }} QScrollBar::handle:vertical {{ background-color: {border_color}; border-radius: 5px; min-height: 20px; }} QScrollBar::handle:vertical:hover {{ background-color: {selected_bg}; }} """) def _on_selection_changed(self): """선택 변경 이벤트""" selected = self.selectedItems() if selected: self.row_selected.emit(selected[0].row()) def _on_double_clicked(self, item: QTableWidgetItem): """더블클릭 이벤트""" self.row_double_clicked.emit(item.row()) def _on_item_changed(self, item: QTableWidgetItem): """아이템 변경 이벤트""" column_name = self.horizontalHeaderItem(item.column()).text() self.cell_edited.emit(item.row(), column_name, item.text()) def set_columns(self, columns: List[Dict[str, Any]]): """ 컬럼 설정 Args: columns: 컬럼 설정 리스트 [{"name": "id", "label": "ID", "width": 50}, ...] """ self.setColumnCount(len(columns)) for i, col in enumerate(columns): header = QTableWidgetItem(col.get("label", col["name"])) self.setHorizontalHeaderItem(i, header) if "width" in col: self.setColumnWidth(i, col["width"]) def add_row(self, data: List[Any], row_data: Any = None): """ 행 추가 Args: data: 셀 데이터 리스트 row_data: 행에 저장할 추가 데이터 (레코드 ID 등) """ row = self.rowCount() self.insertRow(row) for col, value in enumerate(data): item = QTableWidgetItem(str(value) if value else "") item.setTextAlignment(Qt.AlignCenter) if row_data is not None: item.setData(Qt.UserRole, row_data) self.setItem(row, col, item) def get_row_data(self, row: int) -> Any: """ 행의 추가 데이터 반환 Args: row: 행 인덱스 Returns: 저장된 데이터 """ item = self.item(row, 0) if item: return item.data(Qt.UserRole) return None def clear_rows(self): """모든 행 삭제""" self.setRowCount(0) def get_selected_row(self) -> int: """선택된 행 인덱스 반환""" selected = self.selectedItems() if selected: return selected[0].row() return -1 def get_selected_data(self) -> Any: """선택된 행의 데이터 반환""" row = self.get_selected_row() if row >= 0: return self.get_row_data(row) return None