handOver2/ui/dialogs/train_formation_dialog.py

903 lines
31 KiB
Python

# -*- 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}")