3118 lines
122 KiB
Python
3118 lines
122 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
고장 섹션 모듈
|
|
전동차 고장 정보를 관리하는 섹션입니다.
|
|
"""
|
|
|
|
from datetime import date, time, datetime
|
|
from typing import List, Optional, Any
|
|
|
|
from PySide6.QtWidgets import QDialog, QPushButton, QLabel, QWidget
|
|
from PySide6.QtCore import Qt, QPoint
|
|
|
|
from ui.base.base_section import BaseSection, FieldConfig
|
|
from ui.dialogs.input_dialog import SectionInputDialog
|
|
from ui.widgets.clickableLabel import ClickableLabel
|
|
from ui.components.popup_widget import PopupWidget
|
|
from database.models import Fault
|
|
from database.db_manager import DatabaseManager
|
|
from core.constants import DEVICE_CATEGORIES, FAULT_SOURCES
|
|
from core.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class FaultSection(BaseSection):
|
|
"""
|
|
고장 섹션
|
|
|
|
전동차 고장 정보를 표시하고 관리합니다.
|
|
|
|
필드:
|
|
- 발생일자, 편성, 호차, 고장코드, 장치분류, 발생역, 발생시간,
|
|
고장내용, 조치내용, 조치팀, 팀확인, 완료
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent, "faults", Fault)
|
|
|
|
# 필드 설정
|
|
self._setup_fields()
|
|
|
|
# 초기화 후 처리 (설정 로드)
|
|
self._post_init()
|
|
|
|
# 팝업 추적용
|
|
self._current_popup: Optional[PopupWidget] = None
|
|
self._current_team_popup: Optional[PopupWidget] = None # 조치팀/확인팀 팝업 추적
|
|
|
|
# 초기 데이터 로드
|
|
self.load_data()
|
|
|
|
logger.info("고장 섹션 초기화 완료")
|
|
|
|
def _setup_fields(self):
|
|
"""필드 설정"""
|
|
self.fields = [
|
|
FieldConfig("occurrence_date", "발생일", width=70, field_type="date"),
|
|
FieldConfig("column_number", "열번", width=60),
|
|
FieldConfig("train_number", "편성", width=50),
|
|
FieldConfig("car_number", "호차", width=50),
|
|
FieldConfig("fault_code", "고장코드", width=70),
|
|
FieldConfig("device_category", "장치분류", width=110),
|
|
FieldConfig("occurrence_station", "발생역", width=90),
|
|
FieldConfig("occurrence_time", "발생시간", width=90, field_type="time"),
|
|
FieldConfig("fault_content", "고장내용", width=300),
|
|
FieldConfig("action_content", "조치내용", width=300),
|
|
FieldConfig("fault_source", "고장출처", width=100),
|
|
FieldConfig("attachments", "자료", width=50), # 관련자료
|
|
FieldConfig("action_team", "조치팀", width=90),
|
|
FieldConfig("team_confirmations", "확인팀", width=110),
|
|
FieldConfig("is_completed", "완료", width=40, field_type="checkbox"),
|
|
]
|
|
|
|
def _fetch_data(self, **filters) -> List[Fault]:
|
|
"""데이터 조회"""
|
|
return self.crud.get_all_faults()
|
|
|
|
def on_add_clicked(self):
|
|
"""추가 버튼 클릭"""
|
|
dialog = FaultInputDialog(self)
|
|
if dialog.exec() == QDialog.Accepted:
|
|
data = dialog.get_data()
|
|
fault = self.crud.create_fault(**data)
|
|
# 조치 단계 저장
|
|
if fault and fault.id and hasattr(dialog, '_save_action_steps'):
|
|
dialog._save_action_steps(fault.id)
|
|
self.refresh_data()
|
|
|
|
def on_edit_clicked(self, record_id: int):
|
|
"""편집 버튼 클릭"""
|
|
record = self.crud.get_fault(record_id)
|
|
if record:
|
|
dialog = FaultInputDialog(self, record)
|
|
if dialog.exec() == QDialog.Accepted:
|
|
data = dialog.get_data()
|
|
self.crud.update_fault(record_id, **data)
|
|
# 조치 단계 저장
|
|
if hasattr(dialog, '_save_action_steps'):
|
|
dialog._save_action_steps(record_id)
|
|
self.refresh_data()
|
|
|
|
def _delete_record(self, record_id: int):
|
|
"""레코드 삭제"""
|
|
self.crud.delete_fault(record_id)
|
|
self.refresh_data()
|
|
|
|
def on_search_changed(self, text: str):
|
|
"""검색어 변경"""
|
|
if text:
|
|
results = self.crud.search_faults(text)
|
|
self.current_records = results
|
|
self._update_table()
|
|
else:
|
|
self.load_data()
|
|
|
|
def _update_team_confirmations(self, record_id: int, confirmations: dict):
|
|
"""팀확인 상태 업데이트"""
|
|
import json
|
|
self.crud.update_fault(record_id, team_confirmations=json.dumps(confirmations, ensure_ascii=False))
|
|
self.signals.data_changed.emit(self.table_name)
|
|
|
|
def _mark_as_completed(self, record_id: int):
|
|
"""레코드를 완료로 표시"""
|
|
from datetime import datetime
|
|
self.crud.update_fault(record_id, is_completed=True, completed_at=datetime.now())
|
|
self.signals.data_changed.emit(self.table_name)
|
|
|
|
def _on_double_clicked(self, item):
|
|
"""더블클릭 이벤트 오버라이드"""
|
|
row = item.row()
|
|
col = item.column()
|
|
|
|
# 위젯인 경우 (ClickableLabel 등)
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
record_id = widget.property("record_id")
|
|
if not record_id:
|
|
# 첫 번째 셀에서 레코드 ID 가져오기
|
|
first_item = self.table.item(row, 0)
|
|
if first_item:
|
|
record_id = first_item.data(Qt.UserRole)
|
|
|
|
if record_id:
|
|
visible_fields = [f for f in self.fields if f.visible]
|
|
if col < len(visible_fields):
|
|
field = visible_fields[col]
|
|
record = self.crud.get_fault(record_id)
|
|
if record:
|
|
self._edit_field_inline(row, col, field, record)
|
|
return
|
|
|
|
# 일반 아이템인 경우 부모 클래스 처리
|
|
super()._on_double_clicked(item)
|
|
|
|
def _edit_field_inline(self, row: int, col: int, field: FieldConfig, record: Fault):
|
|
"""인라인 필드 편집"""
|
|
if field.name == "train_number":
|
|
self._edit_train_number(row, col, record)
|
|
elif field.name == "column_number":
|
|
self._edit_column_number(row, col, record)
|
|
elif field.name == "device_category":
|
|
self._edit_device_category(row, col, record)
|
|
elif field.name == "fault_code":
|
|
self._edit_fault_code(row, col, record)
|
|
elif field.name == "occurrence_station":
|
|
self._edit_occurrence_station(row, col, record)
|
|
elif field.name == "car_number":
|
|
self._edit_car_number(row, col, record)
|
|
elif field.name == "occurrence_date":
|
|
self._edit_occurrence_date(row, col, record)
|
|
elif field.name == "occurrence_time":
|
|
self._edit_occurrence_time(row, col, record)
|
|
elif field.name == "fault_source":
|
|
self._edit_fault_source(row, col, record)
|
|
elif field.name == "action_team":
|
|
self._edit_action_team(row, col, record)
|
|
elif field.name == "team_confirmations":
|
|
self._edit_team_confirmations(row, col, record)
|
|
elif field.name == "fault_content":
|
|
self._edit_fault_content(row, col, record)
|
|
elif field.name == "action_content":
|
|
self._edit_action_content(row, col, record)
|
|
else:
|
|
# 기본 편집 모드
|
|
self._enable_cell_editing(row, col, field)
|
|
|
|
def _edit_train_number(self, row: int, col: int, record: Fault):
|
|
"""편성번호 편집"""
|
|
from ui.dialogs.train_input_dialog import TrainInputDialog
|
|
|
|
dialog = TrainInputDialog(self, "", 0, date.today())
|
|
dialog.sinpyeong_toggle.set_state(True)
|
|
|
|
# 현재 편성번호 설정
|
|
if record.train_number:
|
|
# 편성번호에서 숫자 추출 (예: "151A" -> 51)
|
|
try:
|
|
train_num_str = record.train_number.replace("1", "").rstrip("AB")
|
|
train_num = int(train_num_str)
|
|
dialog._selected_train = train_num # type: ignore
|
|
dialog._selected_train_display = record.train_number # type: ignore
|
|
except Exception:
|
|
pass
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
data = dialog.get_data()
|
|
train_number = data.get("train_number", "")
|
|
if train_number:
|
|
self.crud.update_fault(record.id, train_number=train_number)
|
|
self.refresh_data()
|
|
|
|
def _edit_device_category(self, row: int, col: int, record: Fault):
|
|
"""장치분류 편집"""
|
|
from ui.components.custom_input import CustomComboBox
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("장치분류 선택")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
combo = CustomComboBox(items=DEVICE_CATEGORIES, placeholder="장치분류 선택")
|
|
if record.device_category:
|
|
combo.set_selected_value(record.device_category)
|
|
layout.addWidget(combo)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
device_category = combo.get_selected_value() or ""
|
|
self.crud.update_fault(record.id, device_category=device_category)
|
|
self.refresh_data()
|
|
|
|
def _edit_fault_code(self, row: int, col: int, record: Fault):
|
|
"""고장코드 편집"""
|
|
from ui.components.custom_input import CustomLineEdit
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("고장코드 입력")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
input_field = CustomLineEdit(placeholder="고장코드")
|
|
if record.fault_code:
|
|
input_field.setText(record.fault_code)
|
|
layout.addWidget(input_field)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
fault_code = input_field.text()
|
|
self.crud.update_fault(record.id, fault_code=fault_code)
|
|
self.refresh_data()
|
|
|
|
def _edit_occurrence_station(self, row: int, col: int, record: Fault):
|
|
"""발생역 편집"""
|
|
from ui.components.custom_input import CustomLineEdit
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("발생역 입력")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
input_field = CustomLineEdit(placeholder="발생역")
|
|
if record.occurrence_station:
|
|
input_field.setText(record.occurrence_station)
|
|
layout.addWidget(input_field)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
occurrence_station = input_field.text()
|
|
self.crud.update_fault(record.id, occurrence_station=occurrence_station)
|
|
self.refresh_data()
|
|
|
|
def _edit_column_number(self, row: int, col: int, record: Fault):
|
|
"""열번 편집 (4개 스핀박스: 1000자리, 100자리, 10자리, 1자리)"""
|
|
from ui.components.popup_widget import PopupWidget
|
|
from PySide6.QtWidgets import QSpinBox, QHBoxLayout, QWidget, QLabel, QPushButton
|
|
|
|
# 기존 팝업이 있으면 닫기
|
|
if self._current_team_popup:
|
|
self._current_team_popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
# 현재 열번 파싱 (4자리 숫자)
|
|
current_column = record.column_number or ""
|
|
d1000, d100, d10, d1 = 1, 0, 0, 1 # 기본값
|
|
if current_column and len(current_column) == 4 and current_column.isdigit():
|
|
d1000 = int(current_column[0])
|
|
d100 = int(current_column[1])
|
|
d10 = int(current_column[2])
|
|
d1 = int(current_column[3])
|
|
|
|
# 팝업 생성
|
|
popup = PopupWidget(self, title="열번 선택", width=280, auto_hide=False)
|
|
self._current_team_popup = popup
|
|
|
|
# 스핀박스 컨테이너
|
|
container = QWidget()
|
|
layout = QHBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(4)
|
|
|
|
theme = self.config.theme
|
|
|
|
spinbox_style_dark = """
|
|
QSpinBox {
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
border: 1px solid #475569;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #475569;
|
|
border: none;
|
|
width: 14px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background-color: #64748b;
|
|
}
|
|
"""
|
|
|
|
spinbox_style_light = """
|
|
QSpinBox {
|
|
background-color: #f8fafc;
|
|
color: #1e293b;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #e2e8f0;
|
|
border: none;
|
|
width: 14px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background-color: #cbd5e1;
|
|
}
|
|
"""
|
|
|
|
spinbox_style = spinbox_style_dark if theme == 'dark' else spinbox_style_light
|
|
|
|
# 1000자리 스핀박스 (1~9: 정기, 회송, 시운전, 구간, 임시열차)
|
|
spin_1000 = QSpinBox()
|
|
spin_1000.setRange(1, 9)
|
|
spin_1000.setValue(d1000)
|
|
spin_1000.setFixedWidth(45)
|
|
spin_1000.setStyleSheet(spinbox_style)
|
|
spin_1000.setToolTip(
|
|
"[1호선] 열차종별\n"
|
|
"정기: 1(상), 2(하)\n"
|
|
"회송: 3(상), 4(하)\n"
|
|
"시운전: 5(상), 6(하)\n"
|
|
"구간: 7(상), 8(하)\n"
|
|
"임시: 9"
|
|
)
|
|
|
|
# 100자리 스핀박스 (0~8)
|
|
spin_100 = QSpinBox()
|
|
spin_100.setRange(0, 8)
|
|
spin_100.setValue(d100)
|
|
spin_100.setFixedWidth(45)
|
|
spin_100.setStyleSheet(spinbox_style)
|
|
spin_100.setToolTip("[1호선] 100자리")
|
|
|
|
# 10자리 스핀박스 (0~9)
|
|
spin_10 = QSpinBox()
|
|
spin_10.setRange(0, 9)
|
|
spin_10.setValue(d10)
|
|
spin_10.setFixedWidth(45)
|
|
spin_10.setStyleSheet(spinbox_style)
|
|
spin_10.setToolTip("[1호선] 10자리")
|
|
|
|
# 1자리 스핀박스 (0~9)
|
|
spin_1 = QSpinBox()
|
|
spin_1.setRange(0, 9)
|
|
spin_1.setValue(d1)
|
|
spin_1.setFixedWidth(45)
|
|
spin_1.setStyleSheet(spinbox_style)
|
|
spin_1.setToolTip(
|
|
"[1호선] 1자리\n"
|
|
"홀수: 상행\n"
|
|
"짝수: 하행"
|
|
)
|
|
|
|
layout.addWidget(spin_1000)
|
|
layout.addWidget(spin_100)
|
|
layout.addWidget(spin_10)
|
|
layout.addWidget(spin_1)
|
|
|
|
popup.content_layout.addWidget(container)
|
|
|
|
# 확인 버튼
|
|
btn_container = QWidget()
|
|
btn_layout = QHBoxLayout(btn_container)
|
|
btn_layout.setContentsMargins(0, 8, 0, 0)
|
|
btn_layout.setSpacing(8)
|
|
|
|
confirm_btn = QPushButton("확인")
|
|
confirm_btn.setCursor(Qt.PointingHandCursor)
|
|
confirm_btn.setFixedWidth(60)
|
|
|
|
if theme == 'dark':
|
|
confirm_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2979ff;
|
|
color: #ffffff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 6px 12px;
|
|
font-weight: 600;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #1e40af;
|
|
}
|
|
""")
|
|
else:
|
|
confirm_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2979ff;
|
|
color: #ffffff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 6px 12px;
|
|
font-weight: 600;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #1e40af;
|
|
}
|
|
""")
|
|
|
|
def on_confirm():
|
|
# 4자리 열번 생성
|
|
column_number = f"{spin_1000.value()}{spin_100.value()}{spin_10.value()}{spin_1.value()}"
|
|
self.crud.update_fault(record.id, column_number=column_number)
|
|
self.refresh_data()
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
confirm_btn.clicked.connect(on_confirm)
|
|
|
|
btn_layout.addStretch()
|
|
btn_layout.addWidget(confirm_btn)
|
|
|
|
popup.content_layout.addWidget(btn_container)
|
|
|
|
# 팝업이 마우스 밖으로 나가면 닫기
|
|
original_leave_event = popup.leaveEvent
|
|
def on_popup_leave(event):
|
|
if self._current_team_popup == popup:
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
original_leave_event(event)
|
|
popup.leaveEvent = on_popup_leave
|
|
|
|
# 마우스 추적 활성화
|
|
popup.setMouseTracking(True)
|
|
popup.container.setMouseTracking(True)
|
|
|
|
# 셀 위치 기준으로 팝업 위치 계산
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
widget_pos = widget.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(widget_pos.x(), widget_pos.y() + widget.height() + 5)
|
|
else:
|
|
rect = self.table.visualItemRect(self.table.item(row, col))
|
|
table_pos = self.table.viewport().mapToGlobal(rect.topLeft())
|
|
popup_pos = QPoint(table_pos.x(), table_pos.y() + rect.height() + 5)
|
|
|
|
popup.show_at(popup_pos)
|
|
|
|
def _edit_car_number(self, row: int, col: int, record: Fault):
|
|
"""호차 편집 (팝업 스핀박스)"""
|
|
from ui.components.popup_widget import PopupWidget
|
|
from PySide6.QtWidgets import QSpinBox, QHBoxLayout, QWidget, QLabel
|
|
|
|
# 기존 팝업이 있으면 닫기
|
|
if self._current_team_popup:
|
|
self._current_team_popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
current_car = int(record.car_number) if record.car_number else 1
|
|
|
|
# 팝업 생성
|
|
popup = PopupWidget(self, title="호차 선택", width=120, auto_hide=False)
|
|
self._current_team_popup = popup
|
|
|
|
# 스핀박스 컨테이너
|
|
container = QWidget()
|
|
layout = QHBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(8)
|
|
|
|
# 스핀박스 생성 (1~8)
|
|
spinbox = QSpinBox()
|
|
spinbox.setRange(1, 8)
|
|
spinbox.setValue(current_car)
|
|
spinbox.setFixedWidth(60)
|
|
|
|
theme = self.config.theme
|
|
if theme == 'dark':
|
|
spinbox.setStyleSheet("""
|
|
QSpinBox {
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
border: 1px solid #475569;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 14px;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #475569;
|
|
border: none;
|
|
width: 16px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background-color: #64748b;
|
|
}
|
|
""")
|
|
else:
|
|
spinbox.setStyleSheet("""
|
|
QSpinBox {
|
|
background-color: #f8fafc;
|
|
color: #1e293b;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 14px;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #e2e8f0;
|
|
border: none;
|
|
width: 16px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background_color: #cbd5e1;
|
|
}
|
|
""")
|
|
|
|
# 값 변경 시 즉시 저장 (팝업은 닫지 않음)
|
|
def on_value_changed(value: int):
|
|
self.crud.update_fault(record.id, car_number=value)
|
|
# refresh_data()를 호출하면 테이블이 다시 그려지면서 팝업이 닫힐 수 있음
|
|
# 대신 시그널만 발생시켜 다른 곳에서 필요시 반영하도록 함
|
|
self.signals.data_changed.emit(self.table_name)
|
|
|
|
spinbox.valueChanged.connect(on_value_changed)
|
|
|
|
label = QLabel("호차")
|
|
if theme == 'dark':
|
|
label.setStyleSheet("color: #94a3b8;")
|
|
else:
|
|
label.setStyleSheet("color: #64748b;")
|
|
|
|
layout.addWidget(spinbox)
|
|
layout.addWidget(label)
|
|
layout.addStretch()
|
|
|
|
popup.content_layout.addWidget(container)
|
|
|
|
# 팝업이 마우스 밖으로 나가면 닫기
|
|
original_leave_event = popup.leaveEvent
|
|
def on_popup_leave(event):
|
|
if self._current_team_popup == popup:
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
# 팝업 닫힐 때 테이블 새로고침
|
|
self.refresh_data()
|
|
original_leave_event(event)
|
|
popup.leaveEvent = on_popup_leave
|
|
|
|
# 마우스 추적 활성화
|
|
popup.setMouseTracking(True)
|
|
popup.container.setMouseTracking(True)
|
|
|
|
# 셀 위치 기준으로 팝업 위치 계산
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
widget_pos = widget.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(widget_pos.x(), widget_pos.y() + widget.height() + 5)
|
|
else:
|
|
rect = self.table.visualItemRect(self.table.item(row, col))
|
|
table_pos = self.table.viewport().mapToGlobal(rect.topLeft())
|
|
popup_pos = QPoint(table_pos.x(), table_pos.y() + rect.height() + 5)
|
|
|
|
popup.show_at(popup_pos)
|
|
|
|
def _edit_occurrence_date(self, row: int, col: int, record: Fault):
|
|
"""발생일 편집 (캘린더 팝업)"""
|
|
from ui.components.custom_calendar import CustomCalendar
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
from PySide6.QtCore import QDate
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("발생일 선택")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# 캘린더 위젯 생성
|
|
calendar = CustomCalendar(show_range_toggle=False, show_time=False)
|
|
if record.occurrence_date:
|
|
qdate = QDate.fromString(record.occurrence_date.isoformat(), Qt.ISODate)
|
|
calendar.calendar.setSelectedDate(qdate)
|
|
layout.addWidget(calendar)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
selected_date = calendar.get_selected_date()
|
|
if selected_date:
|
|
self.crud.update_fault(record.id, occurrence_date=selected_date)
|
|
self.refresh_data()
|
|
|
|
def _edit_occurrence_time(self, row: int, col: int, record: Fault):
|
|
"""발생시간 편집 (시간 피커 팝업)"""
|
|
from ui.components.custom_calendar import TimeSelector
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox, QLabel
|
|
from datetime import time as dt_time
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("발생시간 선택")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# 시간 선택 위젯 생성 (1분 단위로 스크롤 가능)
|
|
time_selector = TimeSelector(minute_step=1)
|
|
if record.occurrence_time:
|
|
if isinstance(record.occurrence_time, dt_time):
|
|
time_selector.set_time(record.occurrence_time)
|
|
elif isinstance(record.occurrence_time, str):
|
|
try:
|
|
t = dt_time.fromisoformat(record.occurrence_time)
|
|
time_selector.set_time(t)
|
|
except Exception:
|
|
pass
|
|
|
|
layout.addWidget(time_selector)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
selected_time = time_selector.get_time()
|
|
self.crud.update_fault(record.id, occurrence_time=selected_time)
|
|
self.refresh_data()
|
|
|
|
def _edit_fault_source(self, row: int, col: int, record: Fault):
|
|
"""고장출처 편집"""
|
|
# ClickableLabel에서 클릭 이벤트로 처리
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
label = widget.findChild(ClickableLabel)
|
|
if label:
|
|
self._show_fault_source_popup(record, label)
|
|
|
|
def _edit_action_team(self, row: int, col: int, record: Fault):
|
|
"""조치팀 편집"""
|
|
# ClickableLabel에서 클릭 이벤트로 처리
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
label = widget.findChild(ClickableLabel)
|
|
if label:
|
|
self._show_action_team_popup(record, label)
|
|
|
|
def _edit_team_confirmations(self, row: int, col: int, record: Fault):
|
|
"""확인팀 편집 (다중선택)"""
|
|
# ClickableLabel에서 클릭 이벤트로 처리
|
|
widget = self.table.cellWidget(row, col)
|
|
if widget:
|
|
label = widget.findChild(ClickableLabel)
|
|
if label:
|
|
self._show_team_confirmations_popup(record, label)
|
|
|
|
def _edit_fault_content(self, row: int, col: int, record: Fault):
|
|
"""고장내용 편집"""
|
|
from ui.components.custom_input import CustomTextEdit, LabeledInput
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("고장내용 편집")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# 텍스트 입력 필드
|
|
text_input = CustomTextEdit(placeholder="고장 내용을 입력하세요", min_height=150)
|
|
text_input.set_text(record.fault_content or "")
|
|
layout.addWidget(LabeledInput("고장내용", text_input, required=True))
|
|
|
|
# 버튼
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
new_content = text_input.get_text()
|
|
self.crud.update_fault(record.id, fault_content=new_content)
|
|
self.refresh_data()
|
|
|
|
def _edit_action_content(self, row: int, col: int, record: Fault):
|
|
"""조치내용 편집"""
|
|
from ui.components.custom_input import CustomTextEdit, LabeledInput
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("조치내용 편집")
|
|
dialog.setModal(True)
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# 텍스트 입력 필드
|
|
text_input = CustomTextEdit(placeholder="조치 내용을 입력하세요", min_height=150)
|
|
text_input.set_text(record.action_content or "")
|
|
layout.addWidget(LabeledInput("조치내용", text_input))
|
|
|
|
# 버튼
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
new_content = text_input.get_text()
|
|
self.crud.update_fault(record.id, action_content=new_content)
|
|
self.refresh_data()
|
|
|
|
def _update_table(self):
|
|
"""테이블 업데이트 (오버라이드)"""
|
|
self.table.setRowCount(0)
|
|
|
|
# 표시할 필드만 필터링
|
|
visible_fields = [f for f in self.fields if f.visible]
|
|
|
|
# 컬럼 설정
|
|
self.table.setColumnCount(len(visible_fields))
|
|
self.table.setHorizontalHeaderLabels([f.label for f in visible_fields])
|
|
|
|
# 컬럼 너비 설정
|
|
for i, field in enumerate(visible_fields):
|
|
self.table.setColumnWidth(i, field.width)
|
|
|
|
# 데이터 채우기 (완료된 레코드는 제외)
|
|
for record in self.current_records:
|
|
# 완료된 레코드는 섹션에서 숨김 (DB에는 유지)
|
|
if hasattr(record, 'is_completed') and record.is_completed:
|
|
continue
|
|
|
|
row = self.table.rowCount()
|
|
self.table.insertRow(row)
|
|
|
|
max_height = int(40 * 1.15) # 기본 높이 15% 증가 (46px)
|
|
for col, field in enumerate(visible_fields):
|
|
value = getattr(record, field.name, "")
|
|
|
|
# 완료 필드는 버튼 위젯으로 표시
|
|
if field.name == "is_completed":
|
|
self._set_completion_button(row, col, record)
|
|
# 관련자료 필드는 ClickableLabel로 표시
|
|
elif field.name == "attachments":
|
|
self._set_attachment_label(row, col, record)
|
|
# 편성, 열번, 장치분류, 고장코드, 발생역, 호차, 발생일, 발생시간, 고장출처는 ClickableLabel로 표시
|
|
elif field.name in ["train_number", "column_number", "device_category", "fault_code", "occurrence_station", "car_number", "occurrence_date", "occurrence_time", "fault_source"]:
|
|
self._set_clickable_label(row, col, field, value, record)
|
|
# 조치팀, 확인팀은 ClickableLabel로 표시
|
|
elif field.name in ["action_team", "team_confirmations"]:
|
|
self._set_team_label(row, col, field, value, record)
|
|
# 고장내용은 줄바꿈이 가능한 위젯으로 표시
|
|
elif field.name == "fault_content":
|
|
content_height = self._set_content_label(row, col, field, value, record)
|
|
if content_height:
|
|
max_height = max(max_height, content_height)
|
|
# 조치내용은 조치 단계 요약으로 표시
|
|
elif field.name == "action_content":
|
|
content_height = self._set_action_content_label(row, col, field, record)
|
|
if content_height:
|
|
max_height = max(max_height, content_height)
|
|
else:
|
|
item = self._create_table_item(field, value, record)
|
|
self.table.setItem(row, col, item)
|
|
|
|
# 셀 내용에 따른 높이 계산
|
|
if item.text():
|
|
text = item.text()
|
|
lines = text.count('\n') + 1
|
|
|
|
font_metrics = self.table.fontMetrics()
|
|
text_width = font_metrics.horizontalAdvance(text)
|
|
col_width = self.table.columnWidth(col)
|
|
|
|
is_content_field = field.name in ['action_taken', 'instruction_content',
|
|
'work_details', 'report_content', 'content']
|
|
|
|
if is_content_field:
|
|
if col_width > 0:
|
|
chars_per_line = max(1, col_width // font_metrics.averageCharWidth())
|
|
lines = max(lines, (len(text) // chars_per_line) + 1)
|
|
elif text_width > col_width and col_width > 0:
|
|
lines = max(lines, (text_width // col_width) + 1)
|
|
|
|
line_height = font_metrics.height() + 16
|
|
calculated_height = max(int(40 * 1.15), lines * line_height)
|
|
max_height = max(max_height, calculated_height)
|
|
|
|
# 행 높이 설정
|
|
self.table.setRowHeight(row, max_height)
|
|
|
|
# 레코드 ID 저장 (첫 번째 셀에)
|
|
first_item = self.table.item(row, 0)
|
|
if first_item:
|
|
first_item.setData(Qt.UserRole, record.id)
|
|
else:
|
|
# 첫 번째 셀이 위젯인 경우 (완료 버튼 등)
|
|
widget = self.table.cellWidget(row, 0)
|
|
if widget:
|
|
widget.setProperty("record_id", record.id)
|
|
|
|
def _set_clickable_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Fault):
|
|
"""ClickableLabel 설정"""
|
|
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
|
|
|
# 편성번호는 중간 2글자만 표시 (예: 132B -> 32)
|
|
if field.name == "train_number" and value:
|
|
import re
|
|
train_str = str(value).strip()
|
|
# 숫자 부분에서 마지막 2자리 추출 (예: 132B -> 32, 101A -> 01)
|
|
match = re.match(r'^\d*(\d{2})[AB]?$', train_str)
|
|
if match:
|
|
display_value = match.group(1)
|
|
else:
|
|
# 다른 형식인 경우 원본 표시
|
|
display_value = train_str
|
|
else:
|
|
display_value = self._format_value(field, value) if value else ""
|
|
|
|
# ClickableLabel 생성
|
|
label = ClickableLabel(display_value or "미지정", enable_hover=True)
|
|
label.setAlignment(Qt.AlignCenter) # 중앙 정렬
|
|
label.setWordWrap(True) # 텍스트 잘림 방지 (줄바꿈 허용)
|
|
|
|
# 통일된 색상 적용 (알록달록하지 않게)
|
|
theme = self.config.theme
|
|
# 모든 필드에 동일한 색상 사용
|
|
unified_color = "#64748b" # 통일된 회색 계열
|
|
|
|
if theme == 'dark':
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {unified_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {unified_color};
|
|
border-color: rgba(255,255,255,0.4);
|
|
}}
|
|
""")
|
|
else:
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {unified_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {unified_color};
|
|
border-color: rgba(0,0,0,0.2);
|
|
}}
|
|
""")
|
|
|
|
# 호버 시 팝업 표시 (편성, 장치분류, 고장코드만, 고장출처는 제외)
|
|
if field.name in ["train_number", "device_category", "fault_code"]:
|
|
def on_hover_enter():
|
|
if value: # 값이 있을 때만 팝업 표시
|
|
self._show_fault_history_popup(field.name, value, record, label)
|
|
|
|
def on_hover_leave():
|
|
if self._current_popup:
|
|
self._current_popup.hide_popup()
|
|
self._current_popup = None
|
|
|
|
label.hover_entered.connect(on_hover_enter)
|
|
label.hover_left.connect(on_hover_leave)
|
|
|
|
# 더블클릭 시 편집
|
|
def on_double_clicked():
|
|
self._edit_field_inline(row, col, field, record)
|
|
|
|
label.double_clicked.connect(on_double_clicked)
|
|
|
|
# 고장출처는 클릭으로도 편집 가능
|
|
if field.name == "fault_source":
|
|
def on_clicked():
|
|
self._edit_field_inline(row, col, field, record)
|
|
label.clicked.connect(on_clicked)
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
label.setProperty("field_name", field.name)
|
|
label.setProperty("field_value", value or "")
|
|
|
|
# 컨테이너 위젯으로 감싸서 구분선 적용 (여백 없이)
|
|
container = QWidget()
|
|
container_layout = QHBoxLayout(container)
|
|
container_layout.setContentsMargins(0, 0, 0, 0) # 여백 완전 제거
|
|
container_layout.setSpacing(0)
|
|
container_layout.addWidget(label)
|
|
|
|
# 구분선 스타일 적용
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
# 기본 높이 반환
|
|
return int(40 * 1.15)
|
|
|
|
def _set_action_content_label(self, row: int, col: int, field: FieldConfig, record: Fault) -> int:
|
|
"""조치내용 라벨 설정 (조치 단계 요약 표시)
|
|
|
|
Returns:
|
|
계산된 셀 높이 (픽셀)
|
|
"""
|
|
from PySide6.QtWidgets import QWidget, QVBoxLayout
|
|
|
|
# 조치 단계 조회
|
|
db = DatabaseManager()
|
|
steps = db.fetch_all(
|
|
"SELECT step_number, action_content, action_team, created_at FROM action_steps WHERE fault_id = ? ORDER BY step_number",
|
|
(record.id,)
|
|
)
|
|
|
|
if not steps:
|
|
# 조치 단계가 없으면 기존 action_content 표시
|
|
display_value = record.action_content or ""
|
|
return self._set_content_label(row, col, field, display_value, record)
|
|
|
|
# 조치 단계 요약 생성
|
|
step_texts = []
|
|
for step in steps:
|
|
step_num = step.get('step_number', 0)
|
|
content = step.get('action_content', '')
|
|
team = step.get('action_team', '')
|
|
created_at = step.get('created_at', '')
|
|
|
|
# 시간 포맷팅
|
|
if isinstance(created_at, str):
|
|
try:
|
|
dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
|
|
time_str = dt.strftime("%m/%d %H:%M")
|
|
except:
|
|
time_str = ""
|
|
else:
|
|
time_str = ""
|
|
|
|
# 요약 텍스트 생성
|
|
summary = f"[{step_num}]"
|
|
if team:
|
|
summary += f" {team}"
|
|
if time_str:
|
|
summary += f" ({time_str})"
|
|
if content:
|
|
# 내용이 길면 잘라서 표시
|
|
content_preview = content[:50] + "..." if len(content) > 50 else content
|
|
summary += f": {content_preview}"
|
|
|
|
step_texts.append(summary)
|
|
|
|
display_value = "\n".join(step_texts)
|
|
|
|
# ClickableLabel 생성
|
|
label = ClickableLabel(display_value, enable_hover=False, enable_double_click=True)
|
|
label.setWordWrap(True)
|
|
label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
label.setProperty("field_name", field.name)
|
|
|
|
# 더블클릭 시 조치 단계 다이얼로그 열기
|
|
def on_double_clicked():
|
|
dialog = FaultInputDialog(self, record)
|
|
dialog.exec()
|
|
self.refresh_data()
|
|
|
|
label.double_clicked.connect(on_double_clicked)
|
|
|
|
# 컨테이너 위젯으로 감싸기
|
|
container = QWidget()
|
|
container_layout = QVBoxLayout(container)
|
|
container_layout.setContentsMargins(8, 4, 8, 4)
|
|
container_layout.setSpacing(0)
|
|
container_layout.addWidget(label)
|
|
|
|
# 스타일 적용
|
|
theme = self.config.theme
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
text_color = "#f8fafc" if theme == 'dark' else "#1e293b"
|
|
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {text_color};
|
|
font-size: 11pt;
|
|
background-color: transparent;
|
|
}}
|
|
""")
|
|
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
# 높이 계산
|
|
font_metrics = self.table.fontMetrics()
|
|
lines = len(step_texts)
|
|
line_height = font_metrics.height() + 8
|
|
calculated_height = max(int(40 * 1.15), lines * line_height)
|
|
|
|
return calculated_height
|
|
|
|
def _set_content_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Fault) -> int:
|
|
"""고장내용 라벨 설정 (줄바꿈 지원, ClickableLabel 사용)
|
|
|
|
Returns:
|
|
계산된 셀 높이 (픽셀)
|
|
"""
|
|
from PySide6.QtWidgets import QWidget, QVBoxLayout
|
|
|
|
display_value = self._format_value(field, value) if value else ""
|
|
|
|
# ClickableLabel 생성 (줄바꿈 지원)
|
|
label = ClickableLabel(display_value or "", enable_hover=False, enable_double_click=True, enable_right_click=True)
|
|
label.setWordWrap(True) # 자동 줄바꿈 활성화
|
|
label.setAlignment(Qt.AlignLeft | Qt.AlignTop) # 왼쪽 위 정렬
|
|
label.setTextInteractionFlags(Qt.TextSelectableByMouse) # 텍스트 선택 가능
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
label.setProperty("field_name", field.name)
|
|
label.setProperty("field_value", value or "")
|
|
|
|
# 더블클릭 시 편집
|
|
def on_double_clicked():
|
|
self._edit_field_inline(row, col, field, record)
|
|
|
|
label.double_clicked.connect(on_double_clicked)
|
|
|
|
# 우클릭 컨텍스트 메뉴
|
|
def on_right_clicked(pos):
|
|
from PySide6.QtWidgets import QMenu
|
|
menu = QMenu(self)
|
|
|
|
edit_action = menu.addAction("편집")
|
|
edit_action.triggered.connect(lambda: self._edit_field_inline(row, col, field, record))
|
|
|
|
menu.exec_(label.mapToGlobal(pos))
|
|
|
|
label.right_clicked.connect(on_right_clicked)
|
|
|
|
# 컨테이너 위젯으로 감싸서 구분선 적용
|
|
container = QWidget()
|
|
container_layout = QVBoxLayout(container)
|
|
container_layout.setContentsMargins(8, 4, 8, 4)
|
|
container_layout.setSpacing(0)
|
|
container_layout.addWidget(label)
|
|
|
|
# 구분선 및 텍스트 색상 스타일 적용
|
|
theme = self.config.theme
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
# 텍스트 색상: 다크 테마는 밝은 색, 라이트 테마는 어두운 색
|
|
text_color = "#f8fafc" if theme == 'dark' else "#1e293b"
|
|
|
|
# 높이 계산 및 줄바꿈 여부 확인 (텍스트 길이에 따라)
|
|
font_metrics = self.table.fontMetrics()
|
|
base_font_size = 13 # 기본 폰트 크기 (테이블 기본값)
|
|
col_width = self.table.columnWidth(col)
|
|
needs_wrap = False
|
|
calculated_height = int(40 * 1.15) # 기본 높이
|
|
|
|
if display_value and col_width > 0:
|
|
# 컬럼 너비에 맞춰 줄 수 계산 (패딩 고려)
|
|
available_width = col_width - 16 # 좌우 패딩 8px씩
|
|
text_width = font_metrics.horizontalAdvance(display_value)
|
|
|
|
# 줄바꿈이 필요한지 확인
|
|
if text_width > available_width:
|
|
needs_wrap = True
|
|
# 줄바꿈 시 폰트 크기를 2포인트 작게 (11px)
|
|
wrap_font_size = base_font_size - 2
|
|
|
|
# 작은 폰트로 다시 계산
|
|
from PySide6.QtGui import QFont, QFontMetrics
|
|
small_font = QFont(self.table.font())
|
|
small_font.setPointSize(wrap_font_size)
|
|
small_font_metrics = QFontMetrics(small_font)
|
|
|
|
# 작은 폰트 기준으로 줄 수 계산
|
|
chars_per_line = max(1, available_width // small_font_metrics.averageCharWidth())
|
|
lines = max(1, (len(display_value) // chars_per_line) + 1)
|
|
line_height = small_font_metrics.height() + 6 # 줄 간격 (작은 폰트 고려)
|
|
calculated_height = max(int(40 * 1.15), lines * line_height + 8) # 상하 패딩
|
|
else:
|
|
# 한 줄로 표시 가능
|
|
calculated_height = int(40 * 1.15)
|
|
|
|
# 라벨에 텍스트 색상 및 폰트 크기 적용
|
|
if needs_wrap:
|
|
wrap_font_size = base_font_size - 2
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {text_color};
|
|
background-color: transparent;
|
|
font-size: {wrap_font_size}px;
|
|
}}
|
|
""")
|
|
else:
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {text_color};
|
|
background-color: transparent;
|
|
}}
|
|
""")
|
|
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
return calculated_height
|
|
|
|
def _set_fault_source_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Fault):
|
|
"""고장출처 라벨 설정"""
|
|
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
|
|
|
display_value = value if value else ""
|
|
chip_color = "#8b5cf6" # 고장출처 보라색
|
|
|
|
# ClickableLabel 생성
|
|
if not display_value:
|
|
# 미지정이면 빈칸처럼 보이게
|
|
label = ClickableLabel(" ", enable_hover=False) # 공백 하나
|
|
else:
|
|
label = ClickableLabel(display_value or "미지정", enable_hover=False)
|
|
|
|
label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
|
|
# 칩 스타일 배경색 적용
|
|
theme = self.config.theme
|
|
|
|
# 고장출처가 미지정이면 투명하게
|
|
if not display_value:
|
|
bg_color = "transparent"
|
|
text_color = "transparent"
|
|
border_color = "transparent"
|
|
else:
|
|
bg_color = chip_color
|
|
text_color = "#ffffff"
|
|
border_color = "rgba(255,255,255,0.2)" if theme == 'dark' else "rgba(0,0,0,0.1)"
|
|
|
|
if theme == 'dark':
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {bg_color};
|
|
color: {text_color};
|
|
border: 1px solid {border_color};
|
|
border-radius: 14px;
|
|
padding: 4px 10px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {bg_color if bg_color != 'transparent' else chip_color};
|
|
border-color: rgba(255,255,255,0.4);
|
|
}}
|
|
""")
|
|
else:
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {bg_color};
|
|
color: {text_color};
|
|
border: 1px solid {border_color};
|
|
border-radius: 14px;
|
|
padding: 4px 10px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {bg_color if bg_color != 'transparent' else chip_color};
|
|
border-color: rgba(0,0,0,0.2);
|
|
}}
|
|
""")
|
|
|
|
# 클릭/더블클릭 이벤트 연결
|
|
def on_clicked():
|
|
self._show_fault_source_popup(record, label)
|
|
|
|
def on_double_clicked():
|
|
self._edit_field_inline(row, col, field, record)
|
|
|
|
label.clicked.connect(on_clicked)
|
|
label.double_clicked.connect(on_double_clicked)
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
label.setProperty("field_name", field.name)
|
|
|
|
# 컨테이너 위젯으로 감싸서 구분선 적용
|
|
container = QWidget()
|
|
container_layout = QHBoxLayout(container)
|
|
container_layout.setContentsMargins(8, 0, 8, 0)
|
|
container_layout.setSpacing(4)
|
|
container_layout.addWidget(label)
|
|
|
|
# 고장출처인 경우 리셋 버튼 추가
|
|
if display_value:
|
|
from PySide6.QtWidgets import QToolButton
|
|
reset_btn = QToolButton()
|
|
reset_btn.setText("↻")
|
|
reset_btn.setToolTip("고장출처 리셋")
|
|
reset_btn.setFixedSize(20, 20)
|
|
reset_btn.setCursor(Qt.PointingHandCursor)
|
|
|
|
# 리셋 버튼 스타일
|
|
if theme == 'dark':
|
|
reset_btn.setStyleSheet("""
|
|
QToolButton {
|
|
background-color: transparent;
|
|
color: #94a3b8;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
}
|
|
QToolButton:hover {
|
|
background-color: #475569;
|
|
color: #f8fafc;
|
|
}
|
|
""")
|
|
else:
|
|
reset_btn.setStyleSheet("""
|
|
QToolButton {
|
|
background-color: transparent;
|
|
color: #64748b;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
}
|
|
QToolButton:hover {
|
|
background-color: #e2e8f0;
|
|
color: #1e293b;
|
|
}
|
|
""")
|
|
|
|
# 리셋 버튼 클릭 시 고장출처 리셋
|
|
def on_reset_clicked():
|
|
self.crud.update_fault(record.id, fault_source="")
|
|
self.refresh_data()
|
|
|
|
reset_btn.clicked.connect(on_reset_clicked)
|
|
container_layout.addWidget(reset_btn)
|
|
|
|
# 구분선 스타일 적용
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
def _set_team_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Fault):
|
|
"""조치팀/확인팀 라벨 설정 (공간 효율 최대화)"""
|
|
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
|
import json
|
|
|
|
theme = self.config.theme
|
|
unified_color = "#64748b" # 통일된 회색 계열 (다른 필드와 동일)
|
|
|
|
if field.name == "action_team":
|
|
# 조치팀: 숫자만 표시 (예: "1팀" -> "1")
|
|
if value:
|
|
display_value = value.replace("팀", "")
|
|
else:
|
|
display_value = "-"
|
|
else: # team_confirmations
|
|
# 확인팀: 확인된 팀 숫자만 표시 (예: "1 2 4")
|
|
try:
|
|
confirmations = json.loads(value) if isinstance(value, str) else value
|
|
if isinstance(confirmations, dict):
|
|
# 팀 이름에서 숫자만 추출하여 정렬
|
|
confirmed_nums = sorted([
|
|
team.replace("팀", "")
|
|
for team, confirmed in confirmations.items()
|
|
if confirmed
|
|
])
|
|
display_value = " ".join(confirmed_nums) if confirmed_nums else "-"
|
|
else:
|
|
display_value = "-"
|
|
except Exception:
|
|
display_value = "-"
|
|
|
|
# ClickableLabel 생성
|
|
label = ClickableLabel(display_value, enable_hover=False)
|
|
label.setAlignment(Qt.AlignCenter)
|
|
|
|
# 통일된 스타일 적용 (다른 필드와 동일하게)
|
|
if theme == 'dark':
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {unified_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {unified_color};
|
|
border-color: rgba(255,255,255,0.4);
|
|
}}
|
|
""")
|
|
else:
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {unified_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {unified_color};
|
|
border-color: rgba(0,0,0,0.2);
|
|
}}
|
|
""")
|
|
|
|
# 클릭/더블클릭 이벤트 연결
|
|
def on_clicked():
|
|
if field.name == "action_team":
|
|
self._show_action_team_popup(record, label)
|
|
elif field.name == "team_confirmations":
|
|
self._show_team_confirmations_popup(record, label)
|
|
|
|
def on_double_clicked():
|
|
self._edit_field_inline(row, col, field, record)
|
|
|
|
label.clicked.connect(on_clicked)
|
|
label.double_clicked.connect(on_double_clicked)
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
label.setProperty("field_name", field.name)
|
|
|
|
# 컨테이너 위젯으로 감싸서 구분선 적용 (여백 없이)
|
|
container = QWidget()
|
|
container_layout = QHBoxLayout(container)
|
|
container_layout.setContentsMargins(0, 0, 0, 0)
|
|
container_layout.setSpacing(0)
|
|
container_layout.addWidget(label)
|
|
|
|
# 구분선 스타일 적용
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
def _set_attachment_label(self, row: int, col: int, record: Fault):
|
|
"""관련자료 라벨 설정 (공간 효율 최대화)"""
|
|
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
|
from services.storage_service import get_storage_service
|
|
|
|
theme = self.config.theme
|
|
storage = get_storage_service()
|
|
|
|
# 첨부파일 개수 조회
|
|
count = storage.get_attachment_count("faults", record.id)
|
|
|
|
# 표시 텍스트: 첨부 파일 개수 또는 "+"
|
|
if count > 0:
|
|
display_value = str(count)
|
|
# 파일이 있으면 강조 색상
|
|
bg_color = "#3b82f6" # 파란색
|
|
else:
|
|
display_value = "+"
|
|
bg_color = "#64748b" # 통일된 회색
|
|
|
|
# ClickableLabel 생성
|
|
label = ClickableLabel(display_value, enable_hover=True)
|
|
label.setAlignment(Qt.AlignCenter)
|
|
|
|
# 스타일 적용
|
|
if theme == 'dark':
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {bg_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {bg_color};
|
|
border-color: rgba(255,255,255,0.4);
|
|
}}
|
|
""")
|
|
else:
|
|
label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {bg_color};
|
|
color: #ffffff;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 0px;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: {bg_color};
|
|
border-color: rgba(0,0,0,0.2);
|
|
}}
|
|
""")
|
|
|
|
# 클릭 시 첨부파일 다이얼로그 열기
|
|
def on_clicked():
|
|
self._show_attachment_dialog(record)
|
|
|
|
label.clicked.connect(on_clicked)
|
|
|
|
# 툴팁
|
|
if count > 0:
|
|
label.setToolTip(f"첨부파일 {count}개 (클릭하여 보기)")
|
|
else:
|
|
label.setToolTip("클릭하여 첨부파일 추가")
|
|
|
|
# 레코드 정보 저장
|
|
label.setProperty("record_id", record.id)
|
|
|
|
# 컨테이너 위젯으로 감싸기 (여백 없이)
|
|
container = QWidget()
|
|
container_layout = QHBoxLayout(container)
|
|
container_layout.setContentsMargins(0, 0, 0, 0)
|
|
container_layout.setSpacing(0)
|
|
container_layout.addWidget(label)
|
|
|
|
# 구분선 스타일 적용
|
|
gridline_color = "#475569" if theme == 'dark' else "#cbd5e1"
|
|
container.setStyleSheet(f"border-right: 1px solid {gridline_color};")
|
|
|
|
# 셀에 위젯 설정
|
|
self.table.setCellWidget(row, col, container)
|
|
|
|
def _show_attachment_dialog(self, record: Fault):
|
|
"""첨부파일 다이얼로그 표시"""
|
|
from ui.dialogs.attachment_dialog import AttachmentDialog
|
|
|
|
device_category = record.device_category or ""
|
|
|
|
# 레코드 정보 (파일명 생성용)
|
|
record_info = {
|
|
"occurrence_date": record.occurrence_date,
|
|
"column_number": record.column_number or "",
|
|
"train_number": record.train_number or "",
|
|
"car_number": record.car_number or "",
|
|
"device_category": device_category,
|
|
"fault_content": record.fault_content or "",
|
|
}
|
|
|
|
dialog = AttachmentDialog(
|
|
self,
|
|
record_type="faults",
|
|
record_id=record.id,
|
|
device_category=device_category,
|
|
record_info=record_info
|
|
)
|
|
dialog.exec()
|
|
|
|
# 다이얼로그 닫힌 후 테이블 새로고침 (첨부파일 개수 업데이트)
|
|
self.refresh_data()
|
|
|
|
def _show_fault_source_popup(self, record: Fault, label: ClickableLabel):
|
|
"""고장출처 선택 팝업 표시 (단일선택)"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.components.popup_widget import PopupWidget
|
|
from PySide6.QtWidgets import QVBoxLayout, QWidget, QButtonGroup
|
|
|
|
# 기존 팝업이 있으면 닫기
|
|
if self._current_team_popup:
|
|
self._current_team_popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
current_source = record.fault_source or ""
|
|
|
|
# 팝업 생성 (세로 배치에 맞게 너비 조정)
|
|
popup = PopupWidget(self, title="고장출처 선택", width=200, auto_hide=False)
|
|
self._current_team_popup = popup
|
|
|
|
# 칩 그룹 생성 (단일선택용 QButtonGroup)
|
|
button_group = QButtonGroup()
|
|
button_group.setExclusive(True) # 단일선택
|
|
|
|
chip_container = QWidget()
|
|
chip_layout = QVBoxLayout(chip_container) # 세로 배치
|
|
chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
chip_layout.setSpacing(6)
|
|
|
|
def on_source_selected(source_key: str):
|
|
"""출처 선택 시 바로 처리하고 팝업 닫기"""
|
|
self.crud.update_fault(record.id, fault_source=source_key)
|
|
self.refresh_data()
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
for source in FAULT_SOURCES:
|
|
# 선택된 출처는 회색, 선택되지 않은 출처는 파란색
|
|
chip_bg = "#64748b" if source == current_source else "#2979ff"
|
|
|
|
chip = ChoiceChipButton(
|
|
text=source,
|
|
key=source,
|
|
bg=chip_bg
|
|
)
|
|
chip.setMinimumWidth(160) # 최소 너비 설정으로 텍스트 전체 표시
|
|
button_group.addButton(chip)
|
|
chip_layout.addWidget(chip)
|
|
|
|
# 현재 선택된 출처 설정
|
|
if source == current_source:
|
|
chip.setChecked(True)
|
|
|
|
# 칩 클릭 시 색상 변경 및 처리
|
|
def make_chip_handler(source_key: str):
|
|
def handler():
|
|
# 모든 칩 색상 업데이트
|
|
for btn in button_group.buttons():
|
|
if isinstance(btn, ChoiceChipButton):
|
|
if btn.key == source_key:
|
|
btn.set_bg("#64748b") # 선택된 출처는 회색
|
|
else:
|
|
btn.set_bg("#2979ff") # 선택되지 않은 출처는 파란색
|
|
on_source_selected(source_key)
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_chip_handler(source))
|
|
|
|
popup.content_layout.addWidget(chip_container)
|
|
|
|
# 팝업 크기 자동 조정 (세로 배치에 맞게)
|
|
chip_container.adjustSize()
|
|
popup_width = 200 # 고정 너비
|
|
popup.container.setFixedWidth(popup_width)
|
|
|
|
# 팝업이 마우스 밖으로 나가면 닫기
|
|
original_leave_event = popup.leaveEvent
|
|
def on_popup_leave(event):
|
|
if self._current_team_popup == popup:
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
original_leave_event(event)
|
|
popup.leaveEvent = on_popup_leave
|
|
|
|
# 마우스 추적 활성화
|
|
popup.setMouseTracking(True)
|
|
popup.container.setMouseTracking(True)
|
|
|
|
# 라벨 위치 기준으로 팝업 위치 계산
|
|
label_pos = label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _show_action_team_popup(self, record: Fault, label: ClickableLabel):
|
|
"""조치팀 선택 팝업 표시 (단일선택)"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.components.popup_widget import PopupWidget
|
|
from core.constants import TEAMS
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QButtonGroup
|
|
|
|
# 기존 팝업이 있으면 닫기
|
|
if self._current_team_popup:
|
|
self._current_team_popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
current_team = record.action_team or ""
|
|
|
|
# 팝업 생성
|
|
popup = PopupWidget(self, title="조치팀 선택", width=400, auto_hide=False)
|
|
self._current_team_popup = popup
|
|
|
|
# 칩 그룹 생성 (단일선택용 QButtonGroup)
|
|
button_group = QButtonGroup()
|
|
button_group.setExclusive(True) # 단일선택
|
|
|
|
chip_container = QWidget()
|
|
chip_layout = QHBoxLayout(chip_container)
|
|
chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
chip_layout.setSpacing(8)
|
|
|
|
def on_team_selected(team_key: str):
|
|
"""팀 선택 시 바로 처리하고 팝업 닫기"""
|
|
self.crud.update_fault(record.id, action_team=team_key)
|
|
self.refresh_data()
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
for team in TEAMS:
|
|
# 선택된 팀은 회색, 선택되지 않은 팀은 파란색
|
|
chip_bg = "#64748b" if team == current_team else "#2979ff"
|
|
|
|
chip = ChoiceChipButton(
|
|
text=team,
|
|
key=team,
|
|
bg=chip_bg
|
|
)
|
|
button_group.addButton(chip)
|
|
chip_layout.addWidget(chip)
|
|
|
|
# 현재 선택된 팀 설정
|
|
if team == current_team:
|
|
chip.setChecked(True)
|
|
|
|
# 칩 클릭 시 색상 변경 및 처리
|
|
def make_chip_handler(team_key: str):
|
|
def handler():
|
|
# 모든 칩 색상 업데이트
|
|
for btn in button_group.buttons():
|
|
if isinstance(btn, ChoiceChipButton):
|
|
if btn.key == team_key:
|
|
btn.set_bg("#64748b") # 선택된 팀은 회색
|
|
else:
|
|
btn.set_bg("#2979ff") # 선택되지 않은 팀은 파란색
|
|
on_team_selected(team_key)
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_chip_handler(team))
|
|
|
|
chip_layout.addStretch()
|
|
popup.content_layout.addWidget(chip_container)
|
|
|
|
# 팝업 크기 자동 조정 (칩 개수에 맞게)
|
|
chip_container.adjustSize()
|
|
chip_width = chip_container.sizeHint().width()
|
|
popup_width = max(200, min(400, chip_width + 40)) # 최소 200, 최대 400, 여백 40
|
|
popup.container.setFixedWidth(popup_width)
|
|
|
|
# 팝업이 마우스 밖으로 나가면 닫기
|
|
original_leave_event = popup.leaveEvent
|
|
def on_popup_leave(event):
|
|
if self._current_team_popup == popup:
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
original_leave_event(event)
|
|
popup.leaveEvent = on_popup_leave
|
|
|
|
# 마우스 추적 활성화
|
|
popup.setMouseTracking(True)
|
|
popup.container.setMouseTracking(True)
|
|
|
|
# 라벨 위치 기준으로 팝업 위치 계산
|
|
label_pos = label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _show_team_confirmations_popup(self, record: Fault, label: ClickableLabel):
|
|
"""확인팀 선택 팝업 표시 (다중선택)"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.components.popup_widget import PopupWidget
|
|
from core.constants import TEAMS
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget
|
|
import json
|
|
|
|
# 기존 팝업이 있으면 닫기
|
|
if self._current_team_popup:
|
|
self._current_team_popup.hide_popup()
|
|
self._current_team_popup = None
|
|
|
|
# 현재 확인된 팀 찾기
|
|
current_confirmations = {}
|
|
try:
|
|
confirmations = json.loads(record.team_confirmations) if isinstance(record.team_confirmations, str) else record.team_confirmations
|
|
if isinstance(confirmations, dict):
|
|
current_confirmations = confirmations
|
|
except Exception:
|
|
pass
|
|
|
|
# 팝업 생성
|
|
popup = PopupWidget(self, title="확인팀 선택", width=320, auto_hide=False)
|
|
self._current_team_popup = popup
|
|
|
|
# 칩 그룹 생성 (다중선택용)
|
|
chip_group = {}
|
|
chip_container = QWidget()
|
|
chip_layout = QHBoxLayout(chip_container)
|
|
chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
chip_layout.setSpacing(8)
|
|
|
|
def update_confirmations():
|
|
"""확인 상태 업데이트"""
|
|
confirmations = {team: chip.isChecked() for team, chip in chip_group.items()}
|
|
self._update_team_confirmations(record.id, confirmations)
|
|
self.refresh_data()
|
|
|
|
for team in TEAMS:
|
|
# 선택된 팀은 회색, 선택되지 않은 팀은 청록색
|
|
is_checked = current_confirmations.get(team, False)
|
|
chip_bg = "#64748b" if is_checked else "#00bfa5"
|
|
|
|
chip = ChoiceChipButton(
|
|
text=team,
|
|
key=team,
|
|
bg=chip_bg
|
|
)
|
|
chip_group[team] = chip
|
|
chip_layout.addWidget(chip)
|
|
|
|
# 현재 확인된 팀 설정
|
|
if is_checked:
|
|
chip.setChecked(True)
|
|
|
|
# 칩 토글 시 색상 변경 및 업데이트
|
|
def make_toggle_handler(team_key: str):
|
|
def handler(_key: str, checked: bool):
|
|
chip = chip_group[team_key]
|
|
if checked:
|
|
chip.set_bg("#64748b") # 선택된 팀은 회색
|
|
else:
|
|
chip.set_bg("#00bfa5") # 선택되지 않은 팀은 청록색
|
|
update_confirmations()
|
|
return handler
|
|
|
|
chip.toggled_key.connect(make_toggle_handler(team))
|
|
|
|
chip_layout.addStretch()
|
|
popup.content_layout.addWidget(chip_container)
|
|
|
|
# 팝업 크기 자동 조정 (칩 개수에 맞게)
|
|
chip_container.adjustSize()
|
|
chip_width = chip_container.sizeHint().width()
|
|
popup_width = max(200, min(400, chip_width + 40)) # 최소 200, 최대 400, 여백 40
|
|
popup.container.setFixedWidth(popup_width)
|
|
|
|
# 팝업이 마우스 밖으로 나가면 닫기
|
|
original_leave_event = popup.leaveEvent
|
|
def on_popup_leave(event):
|
|
if self._current_team_popup == popup:
|
|
popup.hide_popup()
|
|
self._current_team_popup = None
|
|
original_leave_event(event)
|
|
popup.leaveEvent = on_popup_leave
|
|
|
|
# 마우스 추적 활성화
|
|
popup.setMouseTracking(True)
|
|
popup.container.setMouseTracking(True)
|
|
|
|
# 라벨 위치 기준으로 팝업 위치 계산
|
|
label_pos = label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _create_fault_history_popup(self, field_name: str, field_value: str, current_record: Fault) -> PopupWidget:
|
|
"""고장 기록 팝업 생성"""
|
|
faults: List[Fault] = []
|
|
title = ""
|
|
|
|
if field_name == "train_number" and field_value:
|
|
faults = self.crud.get_faults_by_train(field_value, limit=20, months_back=3)
|
|
title = f"편성 {field_value} 고장 기록"
|
|
elif field_name == "device_category" and field_value:
|
|
faults = self.crud.get_faults_by_device(field_value, limit=20, months_back=3)
|
|
title = f"장치분류 {field_value} 고장 기록"
|
|
elif field_name == "fault_code" and field_value:
|
|
faults = self.crud.get_faults_by_code(field_value, limit=20, months_back=3)
|
|
title = f"고장코드 {field_value} 고장 기록"
|
|
|
|
# 현재 레코드 제외
|
|
faults = [f for f in faults if f.id != current_record.id]
|
|
|
|
# 팝업 생성
|
|
popup = PopupWidget(self, title=title, width=320, auto_hide=False)
|
|
|
|
if not faults:
|
|
popup.add_text("최근 3달 이내 관련 기록이 없습니다.", is_secondary=True)
|
|
else:
|
|
popup.add_text(f"총 {len(faults)}건의 기록이 있습니다.", is_secondary=True)
|
|
|
|
# 각 고장 기록을 버튼으로 표시
|
|
for fault in faults[:10]: # 최대 10개만 표시
|
|
# 날짜 포맷팅 (date 객체 또는 문자열 처리)
|
|
if fault.occurrence_date:
|
|
if isinstance(fault.occurrence_date, date):
|
|
date_str = fault.occurrence_date.strftime("%Y-%m-%d")
|
|
elif isinstance(fault.occurrence_date, str):
|
|
date_str = fault.occurrence_date[:10] if len(fault.occurrence_date) >= 10 else fault.occurrence_date
|
|
else:
|
|
date_str = str(fault.occurrence_date)
|
|
else:
|
|
date_str = "-"
|
|
|
|
# 시간 포맷팅 (time 객체 또는 문자열 처리)
|
|
if fault.occurrence_time:
|
|
if hasattr(fault.occurrence_time, 'strftime'):
|
|
time_str = fault.occurrence_time.strftime("%H:%M")
|
|
elif isinstance(fault.occurrence_time, str):
|
|
# 문자열인 경우 (예: "14:30:00" 또는 "14:30")
|
|
time_str = fault.occurrence_time[:5] if len(fault.occurrence_time) >= 5 else fault.occurrence_time
|
|
else:
|
|
time_str = str(fault.occurrence_time)
|
|
else:
|
|
time_str = "-"
|
|
|
|
summary = f"{date_str} {time_str} | {fault.fault_content[:30] if fault.fault_content else '-'}"
|
|
|
|
btn = QPushButton(summary)
|
|
btn.setCursor(Qt.PointingHandCursor)
|
|
btn.setProperty("fault_id", fault.id)
|
|
btn.setProperty("filter_type", field_name)
|
|
btn.setProperty("filter_value", field_value)
|
|
btn.clicked.connect(lambda checked=False, f=fault, ft=field_name, fv=field_value:
|
|
self._open_fault_dialog_with_filter(f.id, ft, fv))
|
|
|
|
# 버튼 스타일
|
|
theme = self.config.theme
|
|
if theme == 'dark':
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
border: 1px solid #475569;
|
|
border-radius: 6px;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #475569;
|
|
border-color: #64748b;
|
|
}
|
|
""")
|
|
else:
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #f8fafc;
|
|
color: #1e293b;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #e2e8f0;
|
|
border-color: #cbd5e1;
|
|
}
|
|
""")
|
|
|
|
popup.content_layout.addWidget(btn)
|
|
|
|
return popup
|
|
|
|
def _show_fault_history_popup(self, field_name: str, field_value: str, record: Fault, label: ClickableLabel):
|
|
"""고장 기록 팝업 표시"""
|
|
if not field_value:
|
|
return
|
|
|
|
popup = self._create_fault_history_popup(field_name, field_value, record)
|
|
|
|
# 라벨 위치 기준으로 팝업 위치 계산
|
|
label_pos = label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x() + label.width() + 10, label_pos.y())
|
|
|
|
popup.show_at(popup_pos)
|
|
|
|
# 팝업을 라벨과 연결하여 마우스 이탈 시 닫기
|
|
self._current_popup = popup
|
|
|
|
def _open_fault_dialog_with_filter(self, fault_id: int, filter_type: str = "", filter_value: str = ""):
|
|
"""필터가 적용된 상태로 고장 다이얼로그 열기"""
|
|
# 사용하지 않는 매개변수 무시
|
|
_ = (filter_type, filter_value)
|
|
|
|
# 고장 다이얼로그 열기
|
|
record = self.crud.get_fault(fault_id)
|
|
if record:
|
|
dialog = FaultInputDialog(self, record)
|
|
dialog.exec()
|
|
|
|
# 팝업 닫기
|
|
self._close_current_popup()
|
|
|
|
|
|
class FaultInputDialog(SectionInputDialog):
|
|
"""
|
|
고장 입력 다이얼로그
|
|
|
|
개선된 레이아웃:
|
|
- 상단: 고장내용/조치내용을 크게 좌우 배치
|
|
- 하단: 날짜, 편성, 호차 등 기타 정보
|
|
- 키보드 탭 네비게이션 지원
|
|
- ClickableLabel 팝업 방식의 편성 선택
|
|
"""
|
|
|
|
def __init__(self, parent=None, record: Fault = None):
|
|
super().__init__(
|
|
parent,
|
|
title="고장 추가" if record is None else "고장 편집",
|
|
record=record,
|
|
width=900,
|
|
height=700
|
|
)
|
|
|
|
self._selected_train: Optional[str] = None
|
|
self._selected_source: Optional[str] = None
|
|
self._current_popup = None # 현재 열린 팝업 추적
|
|
|
|
self._setup_improved_layout()
|
|
self._setup_tab_order()
|
|
|
|
if record:
|
|
self._load_record(record)
|
|
|
|
def _setup_improved_layout(self):
|
|
"""개선된 레이아웃 설정"""
|
|
from ui.components.custom_input import CustomLineEdit, CustomTextEdit, CustomComboBox, LabeledInput
|
|
from PySide6.QtWidgets import (
|
|
QHBoxLayout, QVBoxLayout, QWidget, QGridLayout,
|
|
QSpinBox, QFrame, QSizePolicy
|
|
)
|
|
from PySide6.QtCore import QDate, QTime
|
|
from PySide6.QtGui import QFont
|
|
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
# =====================================================================
|
|
# 상단 영역: 고장내용 / 조치내용 (좌우 크게 배치)
|
|
# =====================================================================
|
|
content_area = QWidget()
|
|
content_layout = QHBoxLayout(content_area)
|
|
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
content_layout.setSpacing(16)
|
|
|
|
# 고장내용 (좌측)
|
|
fault_container = QWidget()
|
|
fault_layout = QVBoxLayout(fault_container)
|
|
fault_layout.setContentsMargins(0, 0, 0, 0)
|
|
fault_layout.setSpacing(4)
|
|
|
|
fault_label = self._create_section_label("고장내용 *", is_dark)
|
|
fault_layout.addWidget(fault_label)
|
|
|
|
self.fault_content_input = CustomTextEdit(
|
|
placeholder="고장 내용을 입력하세요\n예: TCU 고장 발생. 추진력 감소 현상.",
|
|
min_height=180
|
|
)
|
|
fault_layout.addWidget(self.fault_content_input)
|
|
content_layout.addWidget(fault_container, 1)
|
|
|
|
# 조치내용 (우측) - 단계별 리스트
|
|
action_container = QWidget()
|
|
action_layout = QVBoxLayout(action_container)
|
|
action_layout.setContentsMargins(0, 0, 0, 0)
|
|
action_layout.setSpacing(4)
|
|
|
|
# 헤더 (라벨 + 추가 버튼)
|
|
action_header = QWidget()
|
|
action_header_layout = QHBoxLayout(action_header)
|
|
action_header_layout.setContentsMargins(0, 0, 0, 0)
|
|
action_header_layout.setSpacing(8)
|
|
|
|
action_label = self._create_section_label("조치내용", is_dark)
|
|
action_header_layout.addWidget(action_label)
|
|
|
|
from PySide6.QtWidgets import QPushButton
|
|
add_action_btn = QPushButton("+ 추가")
|
|
add_action_btn.setFixedHeight(28)
|
|
add_action_btn.setCursor(Qt.PointingHandCursor)
|
|
add_action_btn.clicked.connect(self._add_action_step)
|
|
add_action_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 4px 12px;
|
|
font-size: 11pt;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2563eb;
|
|
}
|
|
""")
|
|
action_header_layout.addWidget(add_action_btn)
|
|
action_header_layout.addStretch()
|
|
|
|
action_layout.addWidget(action_header)
|
|
|
|
# 조치 단계 리스트 스크롤 영역
|
|
from PySide6.QtWidgets import QScrollArea
|
|
self.action_steps_scroll = QScrollArea()
|
|
self.action_steps_scroll.setWidgetResizable(True)
|
|
self.action_steps_scroll.setMinimumHeight(180)
|
|
self.action_steps_scroll.setMaximumHeight(400)
|
|
|
|
# 다크모드 배경색 설정
|
|
theme = self.config.theme
|
|
scroll_bg = "#0f172a" if theme == 'dark' else "#ffffff"
|
|
self.action_steps_scroll.setStyleSheet(f"""
|
|
QScrollArea {{
|
|
border: none;
|
|
background-color: {scroll_bg};
|
|
}}
|
|
""")
|
|
|
|
self.action_steps_container = QWidget()
|
|
# 컨테이너 배경색도 설정
|
|
self.action_steps_container.setStyleSheet(f"""
|
|
QWidget {{
|
|
background-color: {scroll_bg};
|
|
}}
|
|
""")
|
|
self.action_steps_layout = QVBoxLayout(self.action_steps_container)
|
|
self.action_steps_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.action_steps_layout.setSpacing(8)
|
|
self.action_steps_layout.addStretch()
|
|
|
|
self.action_steps_scroll.setWidget(self.action_steps_container)
|
|
action_layout.addWidget(self.action_steps_scroll)
|
|
|
|
# 조치 단계 리스트 저장
|
|
self.action_steps = [] # [(step_number, content, team, created_at), ...]
|
|
|
|
content_layout.addWidget(action_container, 1)
|
|
|
|
self.content_layout.addWidget(content_area)
|
|
|
|
# =====================================================================
|
|
# 구분선
|
|
# =====================================================================
|
|
separator = QFrame()
|
|
separator.setFrameShape(QFrame.HLine)
|
|
separator.setStyleSheet(f"background-color: {'#475569' if is_dark else '#e2e8f0'};")
|
|
separator.setFixedHeight(1)
|
|
self.content_layout.addWidget(separator)
|
|
|
|
# =====================================================================
|
|
# 하단 영역: 날짜, 시간, 편성, 호차 등 기타 정보
|
|
# =====================================================================
|
|
info_area = QWidget()
|
|
info_layout = QGridLayout(info_area)
|
|
info_layout.setContentsMargins(0, 8, 0, 0)
|
|
info_layout.setSpacing(12)
|
|
info_layout.setColumnStretch(0, 1)
|
|
info_layout.setColumnStretch(1, 1)
|
|
info_layout.setColumnStretch(2, 1)
|
|
info_layout.setColumnStretch(3, 1)
|
|
|
|
row = 0
|
|
|
|
# 첫 번째 행: 발생일자, 발생시간, 열번, 편성
|
|
# 발생일자 (클릭 시 캘린더)
|
|
self.date_label = self._create_clickable_field(
|
|
"발생일자", date.today().strftime("%Y-%m-%d"), self._show_date_picker
|
|
)
|
|
info_layout.addWidget(self._wrap_with_label("발생일자", self.date_label), row, 0)
|
|
|
|
# 발생시간 (스핀박스)
|
|
time_widget = self._create_time_spinbox()
|
|
info_layout.addWidget(self._wrap_with_label("발생시간", time_widget), row, 1)
|
|
|
|
# 열번 (4개 스핀박스)
|
|
column_widget = self._create_column_spinbox()
|
|
info_layout.addWidget(self._wrap_with_label("열번", column_widget), row, 2)
|
|
|
|
# 편성 (클릭 시 팝업 칩 선택)
|
|
self.train_label = self._create_clickable_field(
|
|
"편성 선택", "선택", self._show_train_popup
|
|
)
|
|
info_layout.addWidget(self._wrap_with_label("편성 *", self.train_label), row, 3)
|
|
|
|
row += 1
|
|
|
|
# 두 번째 행: 호차, 발생역, 장치분류, 고장코드
|
|
# 호차 (스핀박스 1~8)
|
|
car_widget = self._create_car_spinbox()
|
|
info_layout.addWidget(self._wrap_with_label("호차", car_widget), row, 0)
|
|
|
|
# 발생역
|
|
self.station_input = CustomLineEdit(placeholder="발생역")
|
|
self.station_input.setMinimumHeight(36)
|
|
self.station_input.textChanged.connect(self._estimate_time_from_column_station)
|
|
info_layout.addWidget(self._wrap_with_label("발생역", self.station_input), row, 1)
|
|
|
|
# 장치분류
|
|
self.device_input = CustomComboBox(items=DEVICE_CATEGORIES, placeholder="장치분류")
|
|
self.device_input.setMinimumHeight(36)
|
|
info_layout.addWidget(self._wrap_with_label("장치분류", self.device_input), row, 2)
|
|
|
|
# 고장코드
|
|
self.code_input = CustomLineEdit(placeholder="고장코드")
|
|
self.code_input.setMinimumHeight(36)
|
|
info_layout.addWidget(self._wrap_with_label("고장코드", self.code_input), row, 3)
|
|
|
|
row += 1
|
|
|
|
# 세 번째 행: 고장출처, 조치팀
|
|
# 고장출처 (클릭 시 팝업 칩 선택)
|
|
self.source_label = self._create_clickable_field(
|
|
"고장출처 선택", "선택", self._show_source_popup
|
|
)
|
|
info_layout.addWidget(self._wrap_with_label("고장출처", self.source_label), row, 0)
|
|
|
|
# 조치팀 (클릭 시 팝업 칩 선택)
|
|
self.team_label = self._create_clickable_field(
|
|
"조치팀 선택", "선택", self._show_team_popup
|
|
)
|
|
info_layout.addWidget(self._wrap_with_label("조치팀", self.team_label), row, 1)
|
|
|
|
# 빈 공간
|
|
info_layout.addWidget(QWidget(), row, 2)
|
|
info_layout.addWidget(QWidget(), row, 3)
|
|
|
|
self.content_layout.addWidget(info_area)
|
|
|
|
# 스트레치 추가
|
|
self.content_layout.addStretch()
|
|
|
|
def _create_section_label(self, text: str, is_dark: bool) -> QLabel:
|
|
"""섹션 라벨 생성"""
|
|
from PySide6.QtWidgets import QLabel
|
|
from PySide6.QtGui import QFont
|
|
|
|
label = QLabel(text)
|
|
label.setFont(QFont("GmarketSans", 11, QFont.Bold))
|
|
label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'}; min-height: 24px;")
|
|
return label
|
|
|
|
def _wrap_with_label(self, label_text: str, widget) -> QWidget:
|
|
"""라벨과 위젯을 감싸는 컨테이너 생성"""
|
|
from PySide6.QtWidgets import QVBoxLayout, QWidget, QLabel
|
|
from PySide6.QtGui import QFont
|
|
|
|
container = QWidget()
|
|
layout = QVBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(2)
|
|
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
label = QLabel(label_text)
|
|
label.setFont(QFont("GmarketSans", 9))
|
|
label.setStyleSheet(f"color: {'#94a3b8' if is_dark else '#64748b'}; min-height: 16px;")
|
|
layout.addWidget(label)
|
|
layout.addWidget(widget)
|
|
|
|
return container
|
|
|
|
def _create_clickable_field(self, placeholder: str, default_text: str, click_handler) -> ClickableLabel:
|
|
"""클릭 가능한 필드 생성"""
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
label = ClickableLabel(default_text, enable_hover=True)
|
|
label.setMinimumHeight(36)
|
|
label.setAlignment(Qt.AlignCenter)
|
|
|
|
if is_dark:
|
|
label.setStyleSheet("""
|
|
QLabel {
|
|
border: 1px solid #475569;
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
font-size: 12px;
|
|
}
|
|
QLabel:hover {
|
|
background-color: #475569;
|
|
border-color: #64748b;
|
|
}
|
|
""")
|
|
else:
|
|
label.setStyleSheet("""
|
|
QLabel {
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
background-color: #f8fafc;
|
|
color: #1e293b;
|
|
font-size: 12px;
|
|
}
|
|
QLabel:hover {
|
|
background-color: #e2e8f0;
|
|
border-color: #94a3b8;
|
|
}
|
|
""")
|
|
|
|
label.clicked.connect(click_handler)
|
|
return label
|
|
|
|
def _create_time_spinbox(self) -> QWidget:
|
|
"""시간 입력 스핀박스 생성 (시:분)"""
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QSpinBox, QLabel
|
|
from datetime import datetime
|
|
|
|
container = QWidget()
|
|
layout = QHBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(4)
|
|
|
|
theme = self.config.theme
|
|
spinbox_style = self._get_spinbox_style()
|
|
|
|
# 시간 스핀박스
|
|
self.hour_spin = QSpinBox()
|
|
self.hour_spin.setRange(0, 23)
|
|
self.hour_spin.setValue(datetime.now().hour)
|
|
self.hour_spin.setFixedWidth(50)
|
|
self.hour_spin.setStyleSheet(spinbox_style)
|
|
layout.addWidget(self.hour_spin)
|
|
|
|
# 구분자
|
|
colon = QLabel(":")
|
|
colon.setStyleSheet(f"color: {'#f8fafc' if theme == 'dark' else '#1e293b'};")
|
|
layout.addWidget(colon)
|
|
|
|
# 분 스핀박스
|
|
self.minute_spin = QSpinBox()
|
|
self.minute_spin.setRange(0, 59)
|
|
self.minute_spin.setValue(datetime.now().minute)
|
|
self.minute_spin.setFixedWidth(50)
|
|
self.minute_spin.setStyleSheet(spinbox_style)
|
|
layout.addWidget(self.minute_spin)
|
|
|
|
layout.addStretch()
|
|
return container
|
|
|
|
def _create_column_spinbox(self) -> QWidget:
|
|
"""열번 입력 스핀박스 생성 (4자리)"""
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QSpinBox
|
|
|
|
container = QWidget()
|
|
layout = QHBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(2)
|
|
|
|
spinbox_style = self._get_spinbox_style()
|
|
|
|
# 1000자리 (1~9)
|
|
self.column_1000 = QSpinBox()
|
|
self.column_1000.setRange(1, 9)
|
|
self.column_1000.setValue(1)
|
|
self.column_1000.setFixedWidth(40)
|
|
self.column_1000.setStyleSheet(spinbox_style)
|
|
self.column_1000.setToolTip("열차종별: 1,2=정기, 3,4=회송, 5,6=시운전, 7,8=구간, 9=임시")
|
|
layout.addWidget(self.column_1000)
|
|
|
|
# 100자리 (0~8)
|
|
self.column_100 = QSpinBox()
|
|
self.column_100.setRange(0, 8)
|
|
self.column_100.setValue(0)
|
|
self.column_100.setFixedWidth(40)
|
|
self.column_100.setStyleSheet(spinbox_style)
|
|
layout.addWidget(self.column_100)
|
|
|
|
# 10자리 (0~9)
|
|
self.column_10 = QSpinBox()
|
|
self.column_10.setRange(0, 9)
|
|
self.column_10.setValue(0)
|
|
self.column_10.setFixedWidth(40)
|
|
self.column_10.setStyleSheet(spinbox_style)
|
|
layout.addWidget(self.column_10)
|
|
|
|
# 1자리 (0~9)
|
|
self.column_1 = QSpinBox()
|
|
self.column_1.setRange(0, 9)
|
|
self.column_1.setValue(1)
|
|
self.column_1.setFixedWidth(40)
|
|
self.column_1.setStyleSheet(spinbox_style)
|
|
self.column_1.setToolTip("홀수=상행, 짝수=하행")
|
|
layout.addWidget(self.column_1)
|
|
|
|
# 열번 변경 시 발생시각 자동 추정
|
|
def on_column_changed():
|
|
self._estimate_time_from_column_station()
|
|
|
|
self.column_1000.valueChanged.connect(on_column_changed)
|
|
self.column_100.valueChanged.connect(on_column_changed)
|
|
self.column_10.valueChanged.connect(on_column_changed)
|
|
self.column_1.valueChanged.connect(on_column_changed)
|
|
|
|
layout.addStretch()
|
|
return container
|
|
|
|
def _create_car_spinbox(self) -> QWidget:
|
|
"""호차 입력 스핀박스 생성 (1~8)"""
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QSpinBox, QLabel
|
|
|
|
container = QWidget()
|
|
layout = QHBoxLayout(container)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(4)
|
|
|
|
spinbox_style = self._get_spinbox_style()
|
|
|
|
self.car_spin = QSpinBox()
|
|
self.car_spin.setRange(1, 8)
|
|
self.car_spin.setValue(1)
|
|
self.car_spin.setFixedWidth(50)
|
|
self.car_spin.setStyleSheet(spinbox_style)
|
|
layout.addWidget(self.car_spin)
|
|
|
|
theme = self.config.theme
|
|
suffix = QLabel("호차")
|
|
suffix.setStyleSheet(f"color: {'#94a3b8' if theme == 'dark' else '#64748b'};")
|
|
layout.addWidget(suffix)
|
|
|
|
layout.addStretch()
|
|
return container
|
|
|
|
def _get_spinbox_style(self) -> str:
|
|
"""스핀박스 스타일 반환"""
|
|
theme = self.config.theme
|
|
|
|
if theme == 'dark':
|
|
return """
|
|
QSpinBox {
|
|
background-color: #334155;
|
|
color: #f8fafc;
|
|
border: 1px solid #475569;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
font-size: 12px;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #475569;
|
|
border: none;
|
|
width: 14px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background-color: #64748b;
|
|
}
|
|
"""
|
|
else:
|
|
return """
|
|
QSpinBox {
|
|
background-color: #f8fafc;
|
|
color: #1e293b;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
font-size: 12px;
|
|
}
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
background-color: #e2e8f0;
|
|
border: none;
|
|
width: 14px;
|
|
}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
background-color: #cbd5e1;
|
|
}
|
|
"""
|
|
|
|
def _setup_tab_order(self):
|
|
"""탭 순서 설정"""
|
|
# 고장내용 -> 날짜 -> 시간 -> 열번 -> 편성 -> 호차 -> 발생역 -> 장치분류 -> 고장코드
|
|
self.setTabOrder(self.fault_content_input, self.hour_spin)
|
|
self.setTabOrder(self.hour_spin, self.minute_spin)
|
|
self.setTabOrder(self.minute_spin, self.column_1000)
|
|
self.setTabOrder(self.column_1000, self.column_100)
|
|
self.setTabOrder(self.column_100, self.column_10)
|
|
self.setTabOrder(self.column_10, self.column_1)
|
|
self.setTabOrder(self.column_1, self.car_spin)
|
|
self.setTabOrder(self.car_spin, self.station_input)
|
|
self.setTabOrder(self.station_input, self.device_input)
|
|
self.setTabOrder(self.device_input, self.code_input)
|
|
|
|
def _close_current_popup(self):
|
|
"""현재 열린 팝업 닫기"""
|
|
if self._current_popup:
|
|
# BaseDialog인 경우 accept() 또는 close() 사용
|
|
if hasattr(self._current_popup, 'accept'):
|
|
self._current_popup.accept()
|
|
elif hasattr(self._current_popup, 'hide_popup'):
|
|
self._current_popup.hide_popup()
|
|
else:
|
|
self._current_popup.close()
|
|
self._current_popup = None
|
|
|
|
def _show_date_picker(self):
|
|
"""날짜 선택기 표시"""
|
|
from ui.components.custom_calendar import CustomCalendar
|
|
from ui.components.popup_widget import PopupWidget
|
|
|
|
self._close_current_popup()
|
|
|
|
popup = PopupWidget(self, title="발생일 선택", width=300, auto_hide=False)
|
|
self._current_popup = popup
|
|
|
|
calendar = CustomCalendar(show_range_toggle=False, show_time=False)
|
|
|
|
# 현재 선택된 날짜 설정
|
|
try:
|
|
current_date_str = self.date_label.text()
|
|
current_date = datetime.strptime(current_date_str, "%Y-%m-%d").date()
|
|
calendar.set_date(current_date)
|
|
except Exception:
|
|
calendar.set_date(date.today())
|
|
|
|
def on_date_selected(selected_date):
|
|
self.date_label.setText(selected_date.strftime("%Y-%m-%d"))
|
|
# 날짜 변경 시 시간 재추정
|
|
self._estimate_time_from_column_station()
|
|
if hasattr(popup, 'hide_popup'):
|
|
popup.hide_popup()
|
|
else:
|
|
popup.close()
|
|
self._current_popup = None
|
|
|
|
calendar.date_selected.connect(on_date_selected)
|
|
popup.content_layout.addWidget(calendar)
|
|
|
|
# 팝업 위치
|
|
label_pos = self.date_label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + self.date_label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _estimate_time_from_column_station(self):
|
|
"""열번과 역명으로 발생시각 자동 추정"""
|
|
try:
|
|
from core.dia_data_loader import estimate_time_by_column_station
|
|
|
|
# 열번 조합
|
|
column_number = f"{self.column_1000.value()}{self.column_100.value()}{self.column_10.value()}{self.column_1.value()}"
|
|
|
|
# 발생역 가져오기
|
|
station = self.station_input.text().strip()
|
|
|
|
if not station:
|
|
return
|
|
|
|
# 발생일자 가져오기
|
|
occurrence_date = None
|
|
try:
|
|
date_str = self.date_label.text()
|
|
occurrence_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
except Exception:
|
|
occurrence_date = date.today()
|
|
|
|
# 시간 추정
|
|
estimated_time = estimate_time_by_column_station(
|
|
column_number, station, occurrence_date
|
|
)
|
|
|
|
if estimated_time:
|
|
self.hour_spin.setValue(estimated_time.hour)
|
|
self.minute_spin.setValue(estimated_time.minute)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"시간 추정 실패: {e}")
|
|
|
|
def _show_train_popup(self):
|
|
"""편성 선택 팝업 표시 (칩 방식) - train_formations 테이블 사용"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.base.base_dialog import BaseDialog
|
|
from PySide6.QtWidgets import (
|
|
QGridLayout, QWidget, QButtonGroup, QScrollArea,
|
|
QHBoxLayout, QVBoxLayout, QLabel
|
|
)
|
|
|
|
self._close_current_popup()
|
|
|
|
popup = BaseDialog(
|
|
self,
|
|
title="편성 선택",
|
|
width=500,
|
|
height=500,
|
|
min_width=400,
|
|
min_height=400,
|
|
modal=True,
|
|
frameless=True,
|
|
resizable=True
|
|
)
|
|
self._current_popup = popup
|
|
|
|
# 데이터베이스에서 편성 목록 가져오기
|
|
from database.common_db_manager import CommonDatabaseManager
|
|
db = CommonDatabaseManager()
|
|
all_formations = db.fetch_all(
|
|
"SELECT train_number, depot FROM train_formations ORDER BY train_number"
|
|
)
|
|
|
|
# 배속지 필터 영역 (필터 칩 사용)
|
|
filter_widget = QWidget()
|
|
filter_layout = QVBoxLayout(filter_widget)
|
|
filter_layout.setContentsMargins(0, 0, 0, 0)
|
|
filter_layout.setSpacing(8)
|
|
|
|
filter_label = QLabel("배속지:")
|
|
filter_label.setStyleSheet("color: #e0e0e0; font-size: 11pt; font-weight: bold;")
|
|
filter_layout.addWidget(filter_label)
|
|
|
|
depot_chip_container = QWidget()
|
|
depot_chip_layout = QHBoxLayout(depot_chip_container)
|
|
depot_chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
depot_chip_layout.setSpacing(8)
|
|
|
|
depot_filter_group = QButtonGroup()
|
|
depot_filter_group.setExclusive(True)
|
|
|
|
selected_depot = ["전체"] # 선택된 배속지 저장 (리스트로 참조 전달)
|
|
|
|
def create_depot_chip(text: str, key: str):
|
|
chip = ChoiceChipButton(text=text, key=key, bg="#3b82f6")
|
|
depot_filter_group.addButton(chip)
|
|
depot_chip_layout.addWidget(chip)
|
|
|
|
if key == "전체":
|
|
chip.setChecked(True)
|
|
|
|
def make_depot_handler(depot_key: str):
|
|
def handler():
|
|
selected_depot[0] = depot_key
|
|
# 선택된 칩 색상 변경
|
|
for btn in depot_filter_group.buttons():
|
|
if isinstance(btn, ChoiceChipButton):
|
|
if btn.key == depot_key:
|
|
btn.set_bg("#64748b")
|
|
else:
|
|
btn.set_bg("#3b82f6")
|
|
update_chips(depot_key)
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_depot_handler(key))
|
|
return chip
|
|
|
|
create_depot_chip("전체", "전체")
|
|
create_depot_chip("신평", "신평")
|
|
create_depot_chip("노포", "노포")
|
|
|
|
depot_chip_layout.addStretch()
|
|
filter_layout.addWidget(depot_chip_container)
|
|
|
|
popup.content_layout.addWidget(filter_widget)
|
|
|
|
# 스크롤 가능한 영역
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setMaximumHeight(350)
|
|
scroll.setStyleSheet("QScrollArea { border: none; background-color: transparent; }")
|
|
|
|
chip_container = QWidget()
|
|
chip_layout = QGridLayout(chip_container)
|
|
chip_layout.setContentsMargins(8, 8, 8, 8)
|
|
chip_layout.setSpacing(4)
|
|
|
|
current_train = self._selected_train or ""
|
|
|
|
def update_chips(depot_filter_value: str = "전체"):
|
|
"""칩 목록 업데이트"""
|
|
# 기존 칩 제거
|
|
while chip_layout.count():
|
|
item = chip_layout.takeAt(0)
|
|
if item:
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
|
|
# 버튼 그룹 초기화
|
|
button_group = QButtonGroup()
|
|
button_group.setExclusive(True)
|
|
|
|
# 필터링된 편성 목록
|
|
if depot_filter_value == "전체":
|
|
filtered_formations = all_formations
|
|
else:
|
|
filtered_formations = [
|
|
f for f in all_formations
|
|
if f.get('depot') == depot_filter_value
|
|
]
|
|
|
|
# 선택된 편성을 앞쪽으로 정렬
|
|
if current_train:
|
|
selected_formations = [
|
|
f for f in filtered_formations
|
|
if f.get('train_number') == current_train
|
|
]
|
|
unselected_formations = [
|
|
f for f in filtered_formations
|
|
if f.get('train_number') != current_train
|
|
]
|
|
# 선택된 편성을 앞에, 나머지를 뒤에 배치
|
|
sorted_formations = selected_formations + unselected_formations
|
|
else:
|
|
sorted_formations = filtered_formations
|
|
|
|
# 편성번호를 그리드로 배치 (5열)
|
|
col_count = 5
|
|
for idx, formation in enumerate(sorted_formations):
|
|
train_number = formation.get('train_number', '')
|
|
if not train_number:
|
|
continue
|
|
|
|
row_idx = idx // col_count
|
|
col_idx = idx % col_count
|
|
|
|
is_selected = train_number == current_train
|
|
chip_bg = "#64748b" if is_selected else "#3b82f6"
|
|
|
|
chip = ChoiceChipButton(text=train_number, key=train_number, bg=chip_bg)
|
|
chip.setMinimumWidth(70)
|
|
button_group.addButton(chip)
|
|
chip_layout.addWidget(chip, row_idx, col_idx)
|
|
|
|
if is_selected:
|
|
chip.setChecked(True)
|
|
|
|
# 클로저 문제 해결: train_number를 명시적으로 캡처
|
|
def make_handler(tn: str):
|
|
def handler():
|
|
self._selected_train = tn
|
|
self.train_label.setText(tn)
|
|
popup.accept()
|
|
self._current_popup = None
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_handler(train_number))
|
|
|
|
# 빈 공간이 있으면 stretch 추가
|
|
chip_layout.setRowStretch(chip_layout.rowCount(), 1)
|
|
|
|
# 초기 칩 로드
|
|
update_chips()
|
|
|
|
scroll.setWidget(chip_container)
|
|
popup.content_layout.addWidget(scroll, 1)
|
|
|
|
# 다이얼로그 스타일 적용
|
|
popup.setStyleSheet("""
|
|
QDialog {
|
|
background-color: #0a0a0a;
|
|
}
|
|
|
|
#dialogContainer {
|
|
background-color: #1a1a1a;
|
|
border: 1px solid #333333;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
QLabel {
|
|
color: #e0e0e0;
|
|
font-family: 'GmarketSans';
|
|
font-size: 11pt;
|
|
}
|
|
|
|
QScrollArea {
|
|
border: none;
|
|
background-color: transparent;
|
|
}
|
|
""")
|
|
|
|
# 팝업 위치 (화면 중앙)
|
|
popup.exec()
|
|
|
|
def _show_source_popup(self):
|
|
"""고장출처 선택 팝업 표시"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.components.popup_widget import PopupWidget
|
|
from PySide6.QtWidgets import QVBoxLayout, QWidget, QButtonGroup
|
|
|
|
self._close_current_popup()
|
|
|
|
popup = PopupWidget(self, title="고장출처 선택", width=180, auto_hide=False)
|
|
self._current_popup = popup
|
|
|
|
button_group = QButtonGroup()
|
|
button_group.setExclusive(True)
|
|
|
|
chip_container = QWidget()
|
|
chip_layout = QVBoxLayout(chip_container)
|
|
chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
chip_layout.setSpacing(4)
|
|
|
|
current_source = self._selected_source or ""
|
|
|
|
for source in FAULT_SOURCES:
|
|
is_selected = source == current_source
|
|
chip_bg = "#64748b" if is_selected else "#8b5cf6"
|
|
|
|
chip = ChoiceChipButton(text=source, key=source, bg=chip_bg)
|
|
chip.setMinimumWidth(150)
|
|
button_group.addButton(chip)
|
|
chip_layout.addWidget(chip)
|
|
|
|
if is_selected:
|
|
chip.setChecked(True)
|
|
|
|
def make_handler(source_key: str):
|
|
def handler():
|
|
self._selected_source = source_key
|
|
self.source_label.setText(source_key)
|
|
popup.hide_popup()
|
|
self._current_popup = None
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_handler(source))
|
|
|
|
popup.content_layout.addWidget(chip_container)
|
|
|
|
label_pos = self.source_label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + self.source_label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _show_team_popup(self):
|
|
"""조치팀 선택 팝업 표시"""
|
|
from ui.components.chips.choice_chip_button import ChoiceChipButton
|
|
from ui.components.popup_widget import PopupWidget
|
|
from core.constants import TEAMS
|
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QButtonGroup
|
|
|
|
self._close_current_popup()
|
|
|
|
popup = PopupWidget(self, title="조치팀 선택", width=280, auto_hide=False)
|
|
self._current_popup = popup
|
|
|
|
button_group = QButtonGroup()
|
|
button_group.setExclusive(True)
|
|
|
|
chip_container = QWidget()
|
|
chip_layout = QHBoxLayout(chip_container)
|
|
chip_layout.setContentsMargins(0, 0, 0, 0)
|
|
chip_layout.setSpacing(4)
|
|
|
|
current_team = self.team_label.text() if self.team_label.text() != "선택" else ""
|
|
|
|
for team in TEAMS:
|
|
is_selected = team == current_team
|
|
chip_bg = "#64748b" if is_selected else "#10b981"
|
|
|
|
chip = ChoiceChipButton(text=team, key=team, bg=chip_bg)
|
|
button_group.addButton(chip)
|
|
chip_layout.addWidget(chip)
|
|
|
|
if is_selected:
|
|
chip.setChecked(True)
|
|
|
|
def make_handler(team_key: str):
|
|
def handler():
|
|
self.team_label.setText(team_key)
|
|
popup.hide_popup()
|
|
self._current_popup = None
|
|
return handler
|
|
|
|
chip.clicked_key.connect(make_handler(team))
|
|
|
|
popup.content_layout.addWidget(chip_container)
|
|
|
|
label_pos = self.team_label.mapToGlobal(QPoint(0, 0))
|
|
popup_pos = QPoint(label_pos.x(), label_pos.y() + self.team_label.height() + 5)
|
|
popup.show_at(popup_pos)
|
|
|
|
def _load_record(self, record: Fault):
|
|
"""레코드 데이터 로드"""
|
|
# 날짜
|
|
if record.occurrence_date:
|
|
if isinstance(record.occurrence_date, date):
|
|
self.date_label.setText(record.occurrence_date.strftime("%Y-%m-%d"))
|
|
|
|
# 시간
|
|
if record.occurrence_time:
|
|
if isinstance(record.occurrence_time, time):
|
|
self.hour_spin.setValue(record.occurrence_time.hour)
|
|
self.minute_spin.setValue(record.occurrence_time.minute)
|
|
|
|
# 열번
|
|
column = record.column_number or ""
|
|
if len(column) == 4 and column.isdigit():
|
|
self.column_1000.setValue(int(column[0]))
|
|
self.column_100.setValue(int(column[1]))
|
|
self.column_10.setValue(int(column[2]))
|
|
self.column_1.setValue(int(column[3]))
|
|
|
|
# 편성번호
|
|
if record.train_number:
|
|
self._selected_train = record.train_number
|
|
self.train_label.setText(record.train_number)
|
|
|
|
# 호차
|
|
if record.car_number:
|
|
try:
|
|
self.car_spin.setValue(int(record.car_number))
|
|
except ValueError:
|
|
pass
|
|
|
|
# 발생역
|
|
self.station_input.setText(record.occurrence_station or "")
|
|
|
|
# 장치분류
|
|
if record.device_category:
|
|
self.device_input.set_selected_value(record.device_category)
|
|
|
|
# 고장코드
|
|
self.code_input.setText(record.fault_code or "")
|
|
|
|
# 고장출처
|
|
if record.fault_source:
|
|
self._selected_source = record.fault_source
|
|
self.source_label.setText(record.fault_source)
|
|
|
|
# 조치팀
|
|
if record.action_team:
|
|
self.team_label.setText(record.action_team)
|
|
|
|
# 고장내용
|
|
self.fault_content_input.set_text(record.fault_content or "")
|
|
|
|
# 조치 단계 로드
|
|
self._load_action_steps(record.id if record.id else None)
|
|
|
|
def get_data(self) -> dict:
|
|
"""입력 데이터 반환"""
|
|
# 날짜
|
|
try:
|
|
occurrence_date = datetime.strptime(self.date_label.text(), "%Y-%m-%d").date()
|
|
except ValueError:
|
|
occurrence_date = date.today()
|
|
|
|
# 시간
|
|
occurrence_time = time(self.hour_spin.value(), self.minute_spin.value())
|
|
|
|
# 열번
|
|
column_number = f"{self.column_1000.value()}{self.column_100.value()}{self.column_10.value()}{self.column_1.value()}"
|
|
|
|
# 조치팀
|
|
action_team = self.team_label.text() if self.team_label.text() != "선택" else ""
|
|
|
|
return {
|
|
"created_date": date.today().isoformat(),
|
|
"created_team": self.config.current_team,
|
|
"occurrence_date": occurrence_date.isoformat(),
|
|
"occurrence_time": occurrence_time.isoformat(),
|
|
"column_number": column_number,
|
|
"train_number": self._selected_train or "",
|
|
"car_number": str(self.car_spin.value()),
|
|
"device_category": self.device_input.get_selected_value() or "",
|
|
"fault_code": self.code_input.text(),
|
|
"occurrence_station": self.station_input.text(),
|
|
"fault_source": self._selected_source or "",
|
|
"action_team": action_team,
|
|
"fault_content": self.fault_content_input.get_text(),
|
|
"action_content": "", # 조치 단계로 관리하므로 빈 문자열
|
|
}
|
|
|
|
def validate(self) -> bool:
|
|
"""입력 유효성 검사"""
|
|
if not self._selected_train:
|
|
self.signals.status_message.emit("편성번호를 선택해주세요.", 3000)
|
|
return False
|
|
if not self.fault_content_input.get_text().strip():
|
|
self.signals.status_message.emit("고장내용을 입력해주세요.", 3000)
|
|
return False
|
|
return True
|
|
|
|
def _add_action_step(self):
|
|
"""조치 단계 추가"""
|
|
from ui.dialogs.action_step_dialog import ActionStepDialog
|
|
|
|
dialog = ActionStepDialog(self)
|
|
if dialog.exec():
|
|
step_data = dialog.get_data()
|
|
step_number = len(self.action_steps) + 1
|
|
self.action_steps.append({
|
|
'step_number': step_number,
|
|
'action_content': step_data.get('action_content', ''),
|
|
'action_team': step_data.get('action_team', ''),
|
|
'created_at': datetime.now()
|
|
})
|
|
self._refresh_action_steps_display()
|
|
|
|
def _edit_action_step(self, step_index: int):
|
|
"""조치 단계 편집"""
|
|
from ui.dialogs.action_step_dialog import ActionStepDialog
|
|
|
|
if step_index < 0 or step_index >= len(self.action_steps):
|
|
return
|
|
|
|
step_data = self.action_steps[step_index]
|
|
dialog = ActionStepDialog(self, step_data)
|
|
if dialog.exec():
|
|
updated_data = dialog.get_data()
|
|
self.action_steps[step_index].update({
|
|
'action_content': updated_data.get('action_content', ''),
|
|
'action_team': updated_data.get('action_team', ''),
|
|
})
|
|
self._refresh_action_steps_display()
|
|
|
|
def _delete_action_step(self, step_index: int):
|
|
"""조치 단계 삭제"""
|
|
if step_index < 0 or step_index >= len(self.action_steps):
|
|
return
|
|
|
|
from PySide6.QtWidgets import QMessageBox
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"삭제 확인",
|
|
"정말 이 조치 단계를 삭제하시겠습니까?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.action_steps.pop(step_index)
|
|
# 단계 번호 재정렬
|
|
for i, step in enumerate(self.action_steps):
|
|
step['step_number'] = i + 1
|
|
self._refresh_action_steps_display()
|
|
|
|
def _refresh_action_steps_display(self):
|
|
"""조치 단계 표시 새로고침"""
|
|
# 기존 위젯 제거
|
|
while self.action_steps_layout.count() > 1: # stretch 제외
|
|
item = self.action_steps_layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
# 조치 단계 위젯 추가
|
|
for idx, step in enumerate(self.action_steps):
|
|
step_widget = self._create_action_step_widget(idx, step)
|
|
self.action_steps_layout.insertWidget(idx, step_widget)
|
|
|
|
def _create_action_step_widget(self, step_index: int, step_data: dict) -> QWidget:
|
|
"""조치 단계 위젯 생성"""
|
|
from PySide6.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QPushButton, QLabel
|
|
|
|
step_widget = QFrame()
|
|
step_widget.setFrameShape(QFrame.Box)
|
|
step_layout = QVBoxLayout(step_widget)
|
|
step_layout.setContentsMargins(12, 8, 12, 8)
|
|
step_layout.setSpacing(4)
|
|
|
|
theme = self.config.theme
|
|
bg_color = "#1e293b" if theme == 'dark' else "#f8fafc"
|
|
border_color = "#334155" if theme == 'dark' else "#e2e8f0"
|
|
text_color = "#f8fafc" if theme == 'dark' else "#1e293b"
|
|
secondary_color = "#94a3b8" if theme == 'dark' else "#64748b"
|
|
|
|
step_widget.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {bg_color};
|
|
border: 1px solid {border_color};
|
|
border-radius: 8px;
|
|
}}
|
|
""")
|
|
|
|
# 헤더 (단계 번호, 조치팀, 시간, 버튼)
|
|
header_layout = QHBoxLayout()
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(8)
|
|
|
|
step_number_label = QLabel(f"단계 {step_data.get('step_number', step_index + 1)}")
|
|
step_number_label.setStyleSheet(f"color: {text_color}; font-weight: bold; font-size: 11pt;")
|
|
header_layout.addWidget(step_number_label)
|
|
|
|
team_label = QLabel(step_data.get('action_team', ''))
|
|
team_label.setStyleSheet(f"color: {secondary_color}; font-size: 10pt;")
|
|
header_layout.addWidget(team_label)
|
|
|
|
created_at = step_data.get('created_at')
|
|
if isinstance(created_at, datetime):
|
|
time_str = created_at.strftime("%Y-%m-%d %H:%M")
|
|
elif isinstance(created_at, str):
|
|
time_str = created_at
|
|
else:
|
|
time_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
|
|
time_label = QLabel(time_str)
|
|
time_label.setStyleSheet(f"color: {secondary_color}; font-size: 9pt;")
|
|
header_layout.addWidget(time_label)
|
|
header_layout.addStretch()
|
|
|
|
# 편집/삭제 버튼 (ClickableLabel로 변경)
|
|
edit_label = ClickableLabel("편집", enable_hover=True)
|
|
edit_label.setCursor(Qt.PointingHandCursor)
|
|
edit_label.clicked.connect(lambda: self._edit_action_step(step_index))
|
|
edit_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: #64748b;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
font-size: 10pt;
|
|
font-weight: 500;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: #475569;
|
|
}}
|
|
""")
|
|
header_layout.addWidget(edit_label)
|
|
|
|
delete_label = ClickableLabel("삭제", enable_hover=True)
|
|
delete_label.setCursor(Qt.PointingHandCursor)
|
|
delete_label.clicked.connect(lambda: self._delete_action_step(step_index))
|
|
delete_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: #ef4444;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
font-size: 10pt;
|
|
font-weight: 500;
|
|
}}
|
|
QLabel:hover {{
|
|
background-color: #dc2626;
|
|
}}
|
|
""")
|
|
header_layout.addWidget(delete_label)
|
|
|
|
step_layout.addLayout(header_layout)
|
|
|
|
# 조치 내용
|
|
content_label = QLabel(step_data.get('action_content', ''))
|
|
content_label.setWordWrap(True)
|
|
content_label.setStyleSheet(f"color: {text_color}; font-size: 11pt; padding: 4px 0;")
|
|
step_layout.addWidget(content_label)
|
|
|
|
return step_widget
|
|
|
|
def _load_action_steps(self, fault_id: Optional[int]):
|
|
"""조치 단계 로드"""
|
|
self.action_steps = []
|
|
|
|
if not fault_id:
|
|
return
|
|
|
|
db = DatabaseManager()
|
|
steps = db.fetch_all(
|
|
"SELECT * FROM action_steps WHERE fault_id = ? ORDER BY step_number",
|
|
(fault_id,)
|
|
)
|
|
|
|
for step in steps:
|
|
created_at_str = step.get('created_at')
|
|
if isinstance(created_at_str, str):
|
|
try:
|
|
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
|
except:
|
|
created_at = datetime.now()
|
|
else:
|
|
created_at = datetime.now()
|
|
|
|
self.action_steps.append({
|
|
'id': step.get('id'),
|
|
'step_number': step.get('step_number', 0),
|
|
'action_content': step.get('action_content', ''),
|
|
'action_team': step.get('action_team', ''),
|
|
'created_at': created_at
|
|
})
|
|
|
|
self._refresh_action_steps_display()
|
|
|
|
def _save_action_steps(self, fault_id: int):
|
|
"""조치 단계 저장"""
|
|
db = DatabaseManager()
|
|
|
|
# 기존 조치 단계 삭제
|
|
db.execute("DELETE FROM action_steps WHERE fault_id = ?", (fault_id,))
|
|
|
|
# 새로운 조치 단계 저장
|
|
for step in self.action_steps:
|
|
db.execute("""
|
|
INSERT INTO action_steps (fault_id, step_number, action_content, action_team)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (
|
|
fault_id,
|
|
step.get('step_number', 0),
|
|
step.get('action_content', ''),
|
|
step.get('action_team', '')
|
|
))
|
|
|
|
|