# -*- coding: utf-8 -*- """ 전동차 편성관리 다이얼로그 모듈 편성번호별 전동차 정보를 관리하는 다이얼로그입니다. """ from datetime import date from typing import Optional, Dict, Any from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLineEdit, QComboBox, QDateEdit, QSpinBox, QLabel, QHeaderView, QAbstractItemView, QMessageBox, QButtonGroup ) from PySide6.QtCore import Qt, QDate, Signal from PySide6.QtGui import QFont from ui.base.base_dialog import BaseDialog from ui.components.chips.choice_chip_button import ChoiceChipButton from database.common_db_manager import CommonDatabaseManager from database.models import TrainFormation from core.logger import get_logger logger = get_logger(__name__) class TrainFormationDialog(BaseDialog): """전동차 편성관리 다이얼로그""" def __init__(self, parent=None): super().__init__( parent=parent, title="전동차 편성관리", width=1200, height=700, min_width=1000, min_height=600 ) self.db = CommonDatabaseManager() self.current_formation_id = None self._setup_ui() self._load_formations() self._apply_black_theme() def _setup_ui(self): """UI 설정""" # 검색 및 필터 영역 filter_widget = QWidget() filter_layout = QHBoxLayout(filter_widget) filter_layout.setContentsMargins(0, 0, 0, 0) filter_layout.setSpacing(12) # 검색 입력 self.search_input = QLineEdit() self.search_input.setPlaceholderText("편성번호로 검색...") self.search_input.textChanged.connect(self._on_search_changed) filter_layout.addWidget(QLabel("검색:")) filter_layout.addWidget(self.search_input) # 배속지 필터 (필터 칩 사용) filter_layout.addWidget(QLabel("배속지:")) depot_chip_container = QWidget() depot_chip_layout = QHBoxLayout(depot_chip_container) depot_chip_layout.setContentsMargins(0, 0, 0, 0) depot_chip_layout.setSpacing(8) self.depot_filter_group = QButtonGroup() self.depot_filter_group.setExclusive(True) self.selected_depot = "전체" def create_depot_chip(text: str, key: str): # 초기 상태: 전체가 선택된 상태 is_selected = (key == "전체") bg_color = "#3b82f6" if is_selected else "#404040" chip = ChoiceChipButton(text=text, key=key, bg=bg_color) self.depot_filter_group.addButton(chip) depot_chip_layout.addWidget(chip) if is_selected: chip.setChecked(True) def make_depot_handler(depot_key: str): def handler(): self.selected_depot = depot_key # 선택된 칩 색상 변경 for btn in self.depot_filter_group.buttons(): if isinstance(btn, ChoiceChipButton): if btn.key == depot_key: btn.set_bg("#3b82f6") # 선택됨: 파란색 else: btn.set_bg("#404040") # 선택안됨: 회색 self._on_filter_changed(depot_key) return handler chip.clicked_key.connect(make_depot_handler(key)) return chip create_depot_chip("전체", "전체") create_depot_chip("신평", "신평") create_depot_chip("노포", "노포") filter_layout.addWidget(depot_chip_container) filter_layout.addStretch() # 추가 버튼 self.add_btn = QPushButton("추가") self.add_btn.clicked.connect(self._on_add) filter_layout.addWidget(self.add_btn) self.content_layout.addWidget(filter_widget) # 테이블 self.table = QTableWidget() self.table.setColumnCount(9) self.table.setHorizontalHeaderLabels([ "편성번호", "신차/구차", "제조사", "도입일", "배속지", "별칭", "도입단계", "도입량", "관리" ]) # 테이블 설정 self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setAlternatingRowColors(True) self.table.setShowGrid(False) self.table.verticalHeader().setVisible(False) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) # 헤더 설정 header = self.table.horizontalHeader() header.setStretchLastSection(False) header.setSectionResizeMode(QHeaderView.Interactive) header.setDefaultAlignment(Qt.AlignCenter) # 컬럼 너비 설정 self.table.setColumnWidth(0, 100) # 편성번호 self.table.setColumnWidth(1, 100) # 신차/구차 self.table.setColumnWidth(2, 120) # 제조사 self.table.setColumnWidth(3, 120) # 도입일 self.table.setColumnWidth(4, 100) # 배속지 self.table.setColumnWidth(5, 150) # 별칭 self.table.setColumnWidth(6, 120) # 도입단계 self.table.setColumnWidth(7, 100) # 도입량 self.table.setColumnWidth(8, 150) # 관리 self.content_layout.addWidget(self.table, 1) # 버튼 영역 button_layout = QHBoxLayout() button_layout.addStretch() self.close_btn = QPushButton("닫기") self.close_btn.clicked.connect(self.close) button_layout.addWidget(self.close_btn) self.content_layout.addLayout(button_layout) def _apply_black_theme(self): """블랙 테마 적용""" self.setStyleSheet(""" QDialog { background-color: #0a0a0a; } #dialogContainer { background-color: #1a1a1a; border: 1px solid #333333; border-radius: 16px; } #dialogTitle { color: #ffffff; font-family: 'GmarketSans'; font-size: 18pt; font-weight: bold; } #closeButton { background-color: transparent; border: none; color: #ffffff; font-size: 16px; border-radius: 16px; } #closeButton:hover { background-color: #dc2626; color: white; } QLabel { color: #e0e0e0; font-family: 'GmarketSans'; font-size: 11pt; } QLineEdit, QComboBox, QDateEdit, QSpinBox { background-color: #2a2a2a; color: #ffffff; border: 1px solid #404040; border-radius: 6px; padding: 8px 12px; font-family: 'GmarketSans'; font-size: 11pt; } QLineEdit:focus, QComboBox:focus, QDateEdit:focus, QSpinBox:focus { border-color: #3b82f6; outline: none; } QComboBox::drop-down { border: none; background-color: #404040; width: 30px; } QComboBox::down-arrow { image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #ffffff; width: 0; height: 0; } QComboBox QAbstractItemView { background-color: #2a2a2a; color: #ffffff; border: 1px solid #404040; selection-background-color: #3b82f6; } QDateEdit::drop-down { border: none; background-color: #404040; width: 30px; } QPushButton { background-color: #3b82f6; color: white; border: none; border-radius: 8px; padding: 8px 20px; font-family: 'GmarketSans'; font-size: 11pt; font-weight: bold; } QPushButton:hover { background-color: #2563eb; } QPushButton:pressed { background-color: #1d4ed8; } QPushButton#editBtn { background-color: #64748b; } QPushButton#editBtn:hover { background-color: #475569; } QPushButton#deleteBtn { background-color: #ef4444; } QPushButton#deleteBtn:hover { background-color: #dc2626; } QTableWidget { background-color: #1a1a1a; color: #ffffff; border: 1px solid #333333; border-radius: 8px; gridline-color: #333333; font-family: 'GmarketSans'; font-size: 11pt; } QTableWidget::item { padding: 8px; border: none; } QTableWidget::item:selected { background-color: #3b82f6; color: white; } QTableWidget::item:alternate { background-color: #222222; } QHeaderView::section { background-color: #2a2a2a; color: #ffffff; padding: 10px; border: none; border-bottom: 2px solid #3b82f6; font-family: 'GmarketSans'; font-size: 11pt; font-weight: bold; } QCheckBox { color: #ffffff; font-family: 'GmarketSans'; font-size: 11pt; } QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #404040; border-radius: 4px; background-color: #2a2a2a; } QCheckBox::indicator:checked { background-color: #3b82f6; border-color: #3b82f6; } """) def _load_formations(self, search_text: str = "", depot_filter: str = "전체"): """편성 목록 로드""" self.table.setRowCount(0) query = "SELECT * FROM train_formations WHERE 1=1" params = [] if search_text: query += " AND train_number LIKE ?" params.append(f"%{search_text}%") if depot_filter != "전체": query += " AND depot = ?" params.append(depot_filter) query += " ORDER BY train_number" formations = self.db.fetch_all(query, tuple(params) if params else None) for formation in formations: self._add_table_row(formation) def _add_table_row(self, formation: Dict[str, Any]): """테이블에 행 추가""" row = self.table.rowCount() self.table.insertRow(row) # 편성번호 train_number_item = QTableWidgetItem(formation.get('train_number', '')) train_number_item.setTextAlignment(Qt.AlignCenter) train_number_item.setData(Qt.UserRole, formation.get('id')) self.table.setItem(row, 0, train_number_item) # 신차/구차 is_new = formation.get('is_new_train', 1) new_old_item = QTableWidgetItem("신차" if is_new else "구차") new_old_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 1, new_old_item) # 제조사 manufacturer_item = QTableWidgetItem(formation.get('manufacturer', '')) manufacturer_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 2, manufacturer_item) # 도입일 intro_date = formation.get('introduction_date') if intro_date: if isinstance(intro_date, str): date_str = intro_date else: date_str = intro_date.strftime('%Y-%m-%d') else: date_str = "" date_item = QTableWidgetItem(date_str) date_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 3, date_item) # 배속지 depot_item = QTableWidgetItem(formation.get('depot', '')) depot_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 4, depot_item) # 별칭 alias_item = QTableWidgetItem(formation.get('alias', '')) alias_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 5, alias_item) # 도입단계 stage_item = QTableWidgetItem(formation.get('introduction_stage', '')) stage_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 6, stage_item) # 도입량 count_item = QTableWidgetItem(str(formation.get('introduction_count', 0))) count_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 7, count_item) # 관리 버튼 button_widget = QWidget() button_layout = QHBoxLayout(button_widget) button_layout.setContentsMargins(4, 4, 4, 4) button_layout.setSpacing(4) edit_btn = QPushButton("수정") edit_btn.setObjectName("editBtn") edit_btn.setFixedHeight(32) edit_btn.clicked.connect(lambda checked, fid=formation.get('id'): self._on_edit(fid)) button_layout.addWidget(edit_btn) delete_btn = QPushButton("삭제") delete_btn.setObjectName("deleteBtn") delete_btn.setFixedHeight(32) delete_btn.clicked.connect(lambda checked, fid=formation.get('id'): self._on_delete(fid)) button_layout.addWidget(delete_btn) self.table.setCellWidget(row, 8, button_widget) def _on_search_changed(self, text: str): """검색어 변경""" self._load_formations(text, self.selected_depot) def _on_filter_changed(self, depot_filter: str = None): """필터 변경""" if depot_filter is None: depot_filter = self.selected_depot search_text = self.search_input.text() self._load_formations(search_text, depot_filter) def _on_add(self): """추가 버튼 클릭""" dialog = TrainFormationEditDialog(self) if dialog.exec(): self._load_formations() def _on_edit(self, formation_id: int): """수정 버튼 클릭""" formation = self.db.fetch_one( "SELECT * FROM train_formations WHERE id = ?", (formation_id,) ) if formation: dialog = TrainFormationEditDialog(self, formation) if dialog.exec(): self._load_formations() def _on_delete(self, formation_id: int): """삭제 버튼 클릭""" reply = QMessageBox.question( self, "삭제 확인", "정말 삭제하시겠습니까?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: self.db.execute( "DELETE FROM train_formations WHERE id = ?", (formation_id,) ) self.db.commit() self._load_formations() except Exception as e: logger.error(f"편성 삭제 실패: {e}") QMessageBox.critical(self, "오류", f"삭제 중 오류가 발생했습니다: {e}") class TrainFormationEditDialog(BaseDialog): """편성 편집 다이얼로그""" def __init__(self, parent=None, formation: Optional[Dict[str, Any]] = None): super().__init__( parent=parent, title="편성 정보 편집" if formation else "편성 추가", width=500, height=600, min_width=450, min_height=550 ) self.db = CommonDatabaseManager() self.formation = formation self._setup_ui() self._apply_black_theme() if formation: self._load_formation_data(formation) def _setup_ui(self): """UI 설정""" # 편성번호 self.train_number_input = QLineEdit() self.train_number_input.setPlaceholderText("예: 134a, 134b, 1A") self._add_form_row("편성번호 *", self.train_number_input) # 신차/구차 (필터 칩 사용) new_old_container = QWidget() new_old_layout = QHBoxLayout(new_old_container) new_old_layout.setContentsMargins(0, 0, 0, 0) new_old_layout.setSpacing(8) self.new_old_group = QButtonGroup() self.new_old_group.setExclusive(True) self.is_new_train = True def create_new_old_chip(text: str, key: bool): # 초기 상태: True(신차)가 기본값 is_selected = (key == True) bg_color = "#3b82f6" if is_selected else "#404040" chip = ChoiceChipButton(text=text, key=str(key), bg=bg_color) self.new_old_group.addButton(chip) new_old_layout.addWidget(chip) if is_selected: chip.setChecked(True) def make_handler(is_new: bool): def handler(): self.is_new_train = is_new # 선택된 칩 색상 변경 for btn in self.new_old_group.buttons(): if isinstance(btn, ChoiceChipButton): if btn.key == str(is_new): btn.set_bg("#3b82f6") # 선택됨: 파란색 else: btn.set_bg("#404040") # 선택안됨: 회색 return handler chip.clicked_key.connect(make_handler(key)) return chip create_new_old_chip("신차", True) create_new_old_chip("구차", False) new_old_layout.addStretch() row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(12) label = QLabel("신차/구차") label.setMinimumWidth(100) row_layout.addWidget(label) row_layout.addWidget(new_old_container, 1) self.content_layout.addWidget(row) # 제조사 self.manufacturer_input = QLineEdit() self.manufacturer_input.setPlaceholderText("제조사명 입력") self._add_form_row("제조사", self.manufacturer_input) # 도입일 self.introduction_date_input = QDateEdit() self.introduction_date_input.setCalendarPopup(True) self.introduction_date_input.setDate(QDate.currentDate()) self.introduction_date_input.setDisplayFormat("yyyy-MM-dd") self._add_form_row("도입일", self.introduction_date_input) # 배속지 (필터 칩 사용) depot_container = QWidget() depot_layout = QHBoxLayout(depot_container) depot_layout.setContentsMargins(0, 0, 0, 0) depot_layout.setSpacing(8) self.depot_group = QButtonGroup() self.depot_group.setExclusive(True) self.selected_depot_edit = "신평" # 기본값 def create_depot_chip(text: str, key: str): # 초기 상태: 신평이 기본값 is_selected = (key == "신평") bg_color = "#3b82f6" if is_selected else "#404040" chip = ChoiceChipButton(text=text, key=key, bg=bg_color) self.depot_group.addButton(chip) depot_layout.addWidget(chip) if is_selected: chip.setChecked(True) def make_handler(depot_key: str): def handler(): self.selected_depot_edit = depot_key # 선택된 칩 색상 변경 for btn in self.depot_group.buttons(): if isinstance(btn, ChoiceChipButton): if btn.key == depot_key: btn.set_bg("#3b82f6") # 선택됨: 파란색 else: btn.set_bg("#404040") # 선택안됨: 회색 return handler chip.clicked_key.connect(make_handler(key)) return chip create_depot_chip("신평", "신평") create_depot_chip("노포", "노포") depot_layout.addStretch() row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(12) label = QLabel("배속지") label.setMinimumWidth(100) row_layout.addWidget(label) row_layout.addWidget(depot_container, 1) self.content_layout.addWidget(row) # 별칭 self.alias_input = QLineEdit() self.alias_input.setPlaceholderText("별칭 입력") self._add_form_row("별칭", self.alias_input) # 도입단계 self.stage_input = QLineEdit() self.stage_input.setPlaceholderText("도입단계 입력") self._add_form_row("도입단계", self.stage_input) # 도입량 self.count_input = QSpinBox() self.count_input.setMinimum(0) self.count_input.setMaximum(9999) self.count_input.setValue(0) self._add_form_row("도입량", self.count_input) # 버튼 self.add_confirm_cancel_buttons() def _add_form_row(self, label_text: str, widget: QWidget): """폼 행 추가""" row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(12) label = QLabel(label_text) label.setMinimumWidth(100) row_layout.addWidget(label) row_layout.addWidget(widget, 1) self.content_layout.addWidget(row) def _load_formation_data(self, formation: Dict[str, Any]): """편성 데이터 로드""" self.train_number_input.setText(formation.get('train_number', '')) # 신차/구차 설정 is_new = bool(formation.get('is_new_train', True)) self.is_new_train = is_new for btn in self.new_old_group.buttons(): if isinstance(btn, ChoiceChipButton): if btn.key == str(is_new): btn.setChecked(True) btn.set_bg("#3b82f6") # 선택됨: 파란색 else: btn.setChecked(False) btn.set_bg("#404040") # 선택안됨: 회색 manufacturer = formation.get('manufacturer', '') if manufacturer: self.manufacturer_input.setText(manufacturer) intro_date = formation.get('introduction_date') if intro_date: if isinstance(intro_date, str): date_obj = QDate.fromString(intro_date, "yyyy-MM-dd") else: date_obj = QDate.fromString(intro_date.strftime('%Y-%m-%d'), "yyyy-MM-dd") if date_obj.isValid(): self.introduction_date_input.setDate(date_obj) # 배속지 설정 depot = formation.get('depot', '') self.selected_depot_edit = depot for btn in self.depot_group.buttons(): if isinstance(btn, ChoiceChipButton): if btn.key == depot: btn.setChecked(True) btn.set_bg("#3b82f6") # 선택됨: 파란색 else: btn.setChecked(False) btn.set_bg("#404040") # 선택안됨: 회색 alias = formation.get('alias', '') if alias: self.alias_input.setText(alias) stage = formation.get('introduction_stage', '') if stage: self.stage_input.setText(stage) count = formation.get('introduction_count', 0) self.count_input.setValue(int(count) if count else 0) def _apply_black_theme(self): """블랙 테마 적용""" self.setStyleSheet(""" QDialog { background-color: #0a0a0a; } #dialogContainer { background-color: #1a1a1a; border: 1px solid #333333; border-radius: 16px; } #dialogTitle { color: #ffffff; font-family: 'GmarketSans'; font-size: 18pt; font-weight: bold; } #closeButton { background-color: transparent; border: none; color: #ffffff; font-size: 16px; border-radius: 16px; } #closeButton:hover { background-color: #dc2626; color: white; } QLabel { color: #e0e0e0; font-family: 'GmarketSans'; font-size: 11pt; } QLineEdit, QComboBox, QDateEdit, QSpinBox { background-color: #2a2a2a; color: #ffffff; border: 1px solid #404040; border-radius: 6px; padding: 8px 12px; font-family: 'GmarketSans'; font-size: 11pt; } QLineEdit:focus, QComboBox:focus, QDateEdit:focus, QSpinBox:focus { border-color: #3b82f6; outline: none; } QComboBox::drop-down { border: none; background-color: #404040; width: 30px; } QComboBox::down-arrow { image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #ffffff; width: 0; height: 0; } QComboBox QAbstractItemView { background-color: #2a2a2a; color: #ffffff; border: 1px solid #404040; selection-background-color: #3b82f6; } QDateEdit::drop-down { border: none; background-color: #404040; width: 30px; } QPushButton { background-color: #3b82f6; color: white; border: none; border-radius: 8px; padding: 8px 20px; font-family: 'GmarketSans'; font-size: 11pt; font-weight: bold; } QPushButton:hover { background-color: #2563eb; } QPushButton:pressed { background-color: #1d4ed8; } QPushButton#editBtn { background-color: #64748b; } QPushButton#editBtn:hover { background-color: #475569; } QPushButton#deleteBtn { background-color: #ef4444; } QPushButton#deleteBtn:hover { background-color: #dc2626; } QCheckBox { color: #ffffff; font-family: 'GmarketSans'; font-size: 11pt; } QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #404040; border-radius: 4px; background-color: #2a2a2a; } QCheckBox::indicator:checked { background-color: #3b82f6; border-color: #3b82f6; } """) def _on_confirm(self): """확인 버튼 클릭""" train_number = self.train_number_input.text().strip() if not train_number: QMessageBox.warning(self, "입력 오류", "편성번호를 입력해주세요.") return try: is_new = 1 if self.is_new_train else 0 manufacturer = self.manufacturer_input.text().strip() or None qdate = self.introduction_date_input.date() intro_date = date(qdate.year(), qdate.month(), qdate.day()) if qdate.isValid() else None depot = self.selected_depot_edit or None alias = self.alias_input.text().strip() or None stage = self.stage_input.text().strip() or None count = self.count_input.value() if self.formation: # 수정 self.db.execute(""" UPDATE train_formations SET train_number = ?, is_new_train = ?, manufacturer = ?, introduction_date = ?, depot = ?, alias = ?, introduction_stage = ?, introduction_count = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( train_number, is_new, manufacturer, intro_date, depot, alias, stage, count, self.formation.get('id') )) else: # 추가 self.db.execute(""" INSERT INTO train_formations (train_number, is_new_train, manufacturer, introduction_date, depot, alias, introduction_stage, introduction_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( train_number, is_new, manufacturer, intro_date, depot, alias, stage, count )) self.db.commit() self.accept() except Exception as e: logger.error(f"편성 저장 실패: {e}") QMessageBox.critical(self, "오류", f"저장 중 오류가 발생했습니다: {e}")