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