# -*- coding: utf-8 -*- """ 일상검수 위젯 모듈 일상검수 대상 편성을 표시하고 관리하는 위젯입니다. """ from datetime import date from typing import List, Optional from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QFrame, QDialog ) from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QFont, QColor, QPainter, QPen from ui.base.base_widget import BaseWidget, CardWidget from ui.components.custom_button import CustomButton from ui.dialogs.train_input_dialog import TrainInputDialog from database.crud import CRUDManager from database.models import DailyInspection from core.constants import DAILY_INSPECTION_SLOTS, CLEANING_TYPES, SHIFT_TYPES from core.config import ConfigManager from core.signals import GlobalSignals from core.logger import get_logger logger = get_logger(__name__) class TrainSlot(QPushButton): """ 편성 슬롯 위젯 일상검수 편성 하나를 표시하는 슬롯입니다. 청소 유형에 따라 다른 표시를 합니다. """ slot_clicked = Signal(int) # 슬롯 번호 def __init__( self, slot_number: int, parent=None, train_number: str = "", cleaning_type: str = "없음", has_work: bool = False ): super().__init__(parent) self.config = ConfigManager() self.slot_number = slot_number self._train_number = train_number self._cleaning_type = cleaning_type self._has_work = has_work # 기본 설정 - 151A 형식 표시를 위해 크기 조정 self.setFixedSize(70, 50) self.setCursor(Qt.PointingHandCursor) self.setFont(QFont("GmarketSans", 12, QFont.Bold)) self.clicked.connect(lambda: self.slot_clicked.emit(self.slot_number)) self._update_display() def _update_display(self): """표시 업데이트""" if self._train_number: self.setText(self._train_number) else: self.setText("+") self._apply_style() def _apply_style(self): """스타일 적용""" theme = self.config.theme if theme == 'dark': bg = "#334155" bg_hover = "#475569" text = "#f8fafc" empty_bg = "#1e293b" empty_text = "#64748b" else: bg = "#e2e8f0" bg_hover = "#cbd5e1" text = "#1e293b" empty_bg = "#f1f5f9" empty_text = "#94a3b8" if self._train_number: self.setStyleSheet(f""" QPushButton {{ background-color: {bg}; color: {text}; border: none; border-radius: 8px; font-weight: bold; }} QPushButton:hover {{ background-color: {bg_hover}; }} """) else: self.setStyleSheet(f""" QPushButton {{ background-color: {empty_bg}; color: {empty_text}; border: 2px dashed {empty_text}; border-radius: 8px; font-size: 20px; }} QPushButton:hover {{ border-color: {text}; color: {text}; }} """) def paintEvent(self, event): """페인트 이벤트 (청소/작업 표시)""" super().paintEvent(event) if not self._train_number: return painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # 청소 표시 if self._cleaning_type == "중청소": # 파란 네모 painter.setPen(QPen(QColor("#3b82f6"), 2)) painter.setBrush(Qt.NoBrush) painter.drawRect(4, 4, 14, 14) elif self._cleaning_type == "대청소": # 빨간 동그라미 painter.setPen(QPen(QColor("#ef4444"), 2)) painter.setBrush(Qt.NoBrush) painter.drawEllipse(4, 4, 14, 14) # 작업 표시 (노란 느낌표) if self._has_work: painter.setPen(QPen(QColor("#f59e0b"), 2)) painter.setFont(QFont("Arial", 10, QFont.Bold)) painter.drawText(self.width() - 14, 14, "!") def set_data(self, train_number: str, cleaning_type: str, has_work: bool): """데이터 설정""" self._train_number = train_number self._cleaning_type = cleaning_type self._has_work = has_work self._update_display() self.update() def clear(self): """슬롯 비우기""" self._train_number = "" self._cleaning_type = "없음" self._has_work = False self._update_display() self.update() class ShiftInspectionRow(QWidget): """ 근무 유형별 일상검수 행 주간/야간 각각의 5개 슬롯을 표시합니다. """ slot_clicked = Signal(str, int) # 근무유형, 슬롯번호 def __init__(self, shift_type: str, parent=None): super().__init__(parent) self.config = ConfigManager() self.shift_type = shift_type self.slots: List[TrainSlot] = [] self._setup_ui() def _setup_ui(self): """UI 설정""" layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(8) # 근무 유형 라벨 shift_label = QLabel(self.shift_type) shift_label.setFixedWidth(40) shift_label.setFont(QFont("GmarketSans", 11, QFont.Bold)) shift_label.setAlignment(Qt.AlignCenter) theme = self.config.theme text_color = "#f8fafc" if theme == 'dark' else "#1e293b" shift_label.setStyleSheet(f"color: {text_color};") layout.addWidget(shift_label) # 슬롯들 for i in range(DAILY_INSPECTION_SLOTS): slot = TrainSlot(i + 1) slot.slot_clicked.connect( lambda num, st=self.shift_type: self.slot_clicked.emit(st, num) ) self.slots.append(slot) layout.addWidget(slot) layout.addStretch() def set_slot_data(self, slot_number: int, train_number: str, cleaning_type: str, has_work: bool): """슬롯 데이터 설정""" if 1 <= slot_number <= len(self.slots): self.slots[slot_number - 1].set_data(train_number, cleaning_type, has_work) def clear_all(self): """모든 슬롯 비우기""" for slot in self.slots: slot.clear() class DailyInspectionWidget(CardWidget): """ 일상검수 위젯 주간/야간 일상검수 편성을 표시하고 관리합니다. Examples: >>> widget = DailyInspectionWidget() >>> widget.load_data() """ def __init__(self, parent=None): super().__init__(parent, padding=12, radius=12) self.crud = CRUDManager() self._current_date = date.today() self._setup_ui() self._connect_signals() logger.info("일상검수 위젯 초기화 완료") def _setup_ui(self): """UI 설정""" # 헤더 header = QWidget() header_layout = QHBoxLayout(header) header_layout.setContentsMargins(0, 0, 0, 0) title = QLabel("📋 일상검수") title.setFont(QFont("GmarketSans", 14, QFont.Bold)) theme = self.config.theme text_color = "#f8fafc" if theme == 'dark' else "#1e293b" title.setStyleSheet(f"color: {text_color};") header_layout.addWidget(title) header_layout.addStretch() self.layout.addWidget(header) # 구분선 separator = QFrame() separator.setFrameShape(QFrame.HLine) separator.setStyleSheet("color: #334155;" if theme == 'dark' else "color: #e2e8f0;") self.layout.addWidget(separator) # 주간/야간 행 self.day_row = ShiftInspectionRow("주간") self.day_row.slot_clicked.connect(self._on_slot_clicked) self.layout.addWidget(self.day_row) self.night_row = ShiftInspectionRow("야간") self.night_row.slot_clicked.connect(self._on_slot_clicked) self.layout.addWidget(self.night_row) # 범례 self._create_legend() def _create_legend(self): """범례 생성""" legend = QWidget() legend_layout = QHBoxLayout(legend) legend_layout.setContentsMargins(0, 8, 0, 0) legend_layout.setSpacing(16) theme = self.config.theme text_color = "#94a3b8" if theme == 'dark' else "#64748b" items = [ ("□", "#3b82f6", "중청소"), ("○", "#ef4444", "대청소"), ("!", "#f59e0b", "작업"), ] for symbol, color, label in items: item = QLabel(f'{symbol} {label}') item.setFont(QFont("GmarketSans", 10)) item.setStyleSheet(f"color: {text_color};") legend_layout.addWidget(item) legend_layout.addStretch() self.layout.addWidget(legend) def _connect_signals(self): """시그널 연결""" self.signals.daily_inspection_changed.connect(self._on_inspection_changed) def load_data(self): """데이터 로드""" # 주간 데이터 day_inspections = self.crud.get_daily_inspections_by_date( self._current_date, "주간" ) self.day_row.clear_all() for inspection in day_inspections: self.day_row.set_slot_data( inspection.slot_number, inspection.train_number or "", inspection.cleaning_type or "없음", inspection.has_work ) # 야간 데이터 night_inspections = self.crud.get_daily_inspections_by_date( self._current_date, "야간" ) self.night_row.clear_all() for inspection in night_inspections: self.night_row.set_slot_data( inspection.slot_number, inspection.train_number or "", inspection.cleaning_type or "없음", inspection.has_work ) def _on_slot_clicked(self, shift_type: str, slot_number: int): """슬롯 클릭""" # 현재 등록된 모든 편성 조회 (중복 방지용) existing_trains = [] day_inspections = self.crud.get_daily_inspections_by_date(self._current_date, "주간") night_inspections = self.crud.get_daily_inspections_by_date(self._current_date, "야간") for insp in day_inspections + night_inspections: if insp.train_number: existing_trains.append(insp.train_number) dialog = TrainInputDialog( self, shift_type, slot_number, self._current_date, existing_trains=existing_trains ) if dialog.exec() == QDialog.Accepted: data = dialog.get_data() self.crud.upsert_daily_inspection( inspection_date=self._current_date, shift_type=shift_type, slot_number=slot_number, train_number=data.get("train_number", ""), cleaning_type=data.get("cleaning_type", "없음"), has_work=data.get("has_work", False), work_content=data.get("work_content", ""), is_work_completed=data.get("is_work_completed", False), ) self.load_data() self.signals.daily_inspection_changed.emit(shift_type, slot_number, data.get("train_number", "")) def _on_inspection_changed(self, shift_type: str, slot_number: int, train_number: str): """일상검수 변경 시그널""" self.load_data()