handOver2/ui/sections/instruction_section.py

941 lines
36 KiB
Python

# -*- coding: utf-8 -*-
"""
지시 섹션 모듈
상위부서나 상급자의 지시사항을 관리하는 섹션입니다.
"""
from datetime import date
from typing import List, Optional, Any
from PySide6.QtWidgets import QDialog, QPushButton
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 Instruction
from database.crud import CRUDManager
from core.logger import get_logger
logger = get_logger(__name__)
class InstructionSection(BaseSection):
"""
지시 섹션
상위부서나 상급자의 지시사항을 표시하고 관리합니다.
필드:
- 생성일, 생성팀, 지시자, 지시내용, 지시일자, 지속여부, 팀확인, 완료
"""
def __init__(self, parent=None):
super().__init__(parent, "instructions", Instruction)
# 필드 설정
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("created_date", "생성일", width=80, required=True, editable=False, field_type="date"),
FieldConfig("created_team", "생성팀", width=60, required=True, editable=False),
FieldConfig("instructor", "지시자", width=80),
FieldConfig("instruction_content", "지시내용", width=350),
FieldConfig("instruction_date", "지시일자", width=80, field_type="date"),
FieldConfig("is_continuous", "지속", width=50, field_type="checkbox"),
FieldConfig("team_confirmations", "확인팀", width=110),
FieldConfig("is_completed", "완료", width=40, field_type="checkbox"),
]
def _fetch_data(self, **filters) -> List[Instruction]:
"""데이터 조회"""
# 오늘 날짜의 지시 + 지속 지시 조회
today = date.today()
return self.crud.get_instructions_by_date(today, include_continuous=True)
def on_add_clicked(self):
"""추가 버튼 클릭"""
dialog = InstructionInputDialog(self)
if dialog.exec() == QDialog.Accepted:
data = dialog.get_data()
self.crud.create_instruction(**data)
self.refresh_data()
def on_edit_clicked(self, record_id: int):
"""편집 버튼 클릭"""
record = self.crud.get_instruction(record_id)
if record:
dialog = InstructionInputDialog(self, record)
if dialog.exec() == QDialog.Accepted:
data = dialog.get_data()
self.crud.update_instruction(record_id, **data)
self.refresh_data()
def _delete_record(self, record_id: int):
"""레코드 삭제"""
self.crud.delete_instruction(record_id)
self.refresh_data()
def on_search_changed(self, text: str):
"""검색어 변경"""
if text:
# 검색 결과로 필터링
results = self.crud._search(
"instructions",
Instruction,
["instructor", "instruction_content"],
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_instruction(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_instruction(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_instruction(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: Instruction):
"""인라인 필드 편집"""
if field.name == "instructor":
self._edit_instructor(row, col, record)
elif field.name == "instruction_date":
self._edit_instruction_date(row, col, record)
elif field.name == "is_continuous":
self._edit_is_continuous(row, col, record)
elif field.name == "instruction_content":
self._edit_instruction_content(row, col, record)
elif field.name == "team_confirmations":
self._edit_team_confirmations(row, col, record)
elif field.name == "created_team":
self._edit_created_team(row, col, record)
else:
# 기본 편집 모드
self._enable_cell_editing(row, col, field)
def _edit_instructor(self, row: int, col: int, record: Instruction):
"""지시자 편집"""
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.instructor:
input_field.setText(record.instructor)
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:
instructor = input_field.text()
self.crud.update_instruction(record.id, instructor=instructor)
self.refresh_data()
def _edit_instruction_date(self, row: int, col: int, record: Instruction):
"""지시일자 편집 (캘린더 팝업)"""
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.instruction_date:
qdate = QDate.fromString(record.instruction_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_instruction(record.id, instruction_date=selected_date)
self.refresh_data()
def _edit_is_continuous(self, row: int, col: int, record: Instruction):
"""지속여부 토글"""
new_value = not record.is_continuous
self.crud.update_instruction(record.id, is_continuous=new_value)
self.refresh_data()
def _edit_instruction_content(self, row: int, col: int, record: Instruction):
"""지시내용 편집"""
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.instruction_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_instruction(record.id, instruction_content=new_content)
self.refresh_data()
def _edit_team_confirmations(self, row: int, col: int, record: Instruction):
"""확인팀 편집 (다중선택)"""
widget = self.table.cellWidget(row, col)
if widget:
label = widget.findChild(ClickableLabel)
if label:
self._show_team_confirmations_popup(record, label)
def _edit_created_team(self, row: int, col: int, record: Instruction):
"""생성팀 편집"""
widget = self.table.cellWidget(row, col)
if widget:
label = widget.findChild(ClickableLabel)
if label:
self._show_created_team_popup(record, label)
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 == "team_confirmations":
self._set_team_label(row, col, field, value, record)
# 생성팀은 ClickableLabel로 표시
elif field.name == "created_team":
self._set_clickable_label(row, col, field, value, record)
# 지시자는 ClickableLabel로 표시
elif field.name == "instructor":
self._set_clickable_label(row, col, field, value, record)
# 지시일자는 ClickableLabel로 표시
elif field.name in ["instruction_date", "created_date"]:
self._set_clickable_label(row, col, field, value, record)
# 지속여부는 ClickableLabel로 표시
elif field.name == "is_continuous":
self._set_continuous_label(row, col, field, value, record)
# 지시내용은 줄바꿈이 가능한 위젯으로 표시
elif field.name == "instruction_content":
content_height = self._set_content_label(row, col, field, value, 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)
# 행 높이 설정
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: Instruction):
"""ClickableLabel 설정"""
from PySide6.QtWidgets import QWidget, QHBoxLayout
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);
}}
""")
# 더블클릭 시 편집
def on_double_clicked():
self._edit_field_inline(row, col, field, record)
label.double_clicked.connect(on_double_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)
def _set_continuous_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Instruction):
"""지속여부 라벨 설정 (토글 가능)"""
from PySide6.QtWidgets import QWidget, QHBoxLayout
is_continuous = bool(value)
display_value = "" if is_continuous else "-"
# ClickableLabel 생성
label = ClickableLabel(display_value, enable_hover=True)
label.setAlignment(Qt.AlignCenter)
theme = self.config.theme
# 지속이면 초록색, 아니면 회색
if is_continuous:
bg_color = "#22c55e"
text_color = "#ffffff"
else:
bg_color = "#64748b"
text_color = "#94a3b8" if theme == 'dark' else "#cbd5e1"
if theme == 'dark':
label.setStyleSheet(f"""
QLabel {{
background-color: {bg_color};
color: {text_color};
border: 1px solid rgba(255,255,255,0.2);
border-radius: 0px;
padding: 0px;
margin: 0px;
font-weight: 600;
font-size: 14px;
}}
QLabel:hover {{
background-color: {bg_color};
border-color: rgba(255,255,255,0.4);
}}
""")
else:
label.setStyleSheet(f"""
QLabel {{
background-color: {bg_color};
color: {text_color};
border: 1px solid rgba(0,0,0,0.1);
border-radius: 0px;
padding: 0px;
margin: 0px;
font-weight: 600;
font-size: 14px;
}}
QLabel:hover {{
background-color: {bg_color};
border-color: rgba(0,0,0,0.2);
}}
""")
# 클릭 시 토글
def on_clicked():
self._edit_is_continuous(row, col, record)
label.clicked.connect(on_clicked)
label.setToolTip("클릭하여 지속 여부 토글")
# 레코드 정보 저장
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_team_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Instruction):
"""확인팀 라벨 설정 (공간 효율 최대화)"""
from PySide6.QtWidgets import QWidget, QHBoxLayout
import json
theme = self.config.theme
unified_color = "#64748b"
# 확인팀: 확인된 팀 숫자만 표시 (예: "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():
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_content_label(self, row: int, col: int, field: FieldConfig, value: Any, record: Instruction) -> int:
"""지시내용 라벨 설정 (줄바꿈 지원)
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
text_width = font_metrics.horizontalAdvance(display_value)
if text_width > available_width:
needs_wrap = True
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 _show_team_confirmations_popup(self, record: Instruction, 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))
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_created_team_popup(self, record: Instruction, 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.created_team or ""
# 팝업 생성
popup = PopupWidget(self, title="생성팀 선택", width=400, auto_hide=False)
self._current_team_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(8)
def on_team_selected(team_key: str):
self.crud.update_instruction(record.id, created_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))
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)
class InstructionInputDialog(SectionInputDialog):
"""지시 입력 다이얼로그"""
def __init__(self, parent=None, record: Instruction = None):
super().__init__(
parent,
title="지시 추가" if record is None else "지시 편집",
record=record
)
self._setup_instruction_fields()
if record:
self._load_record(record)
def _setup_instruction_fields(self):
"""지시 필드 설정"""
from ui.components.custom_input import CustomLineEdit, CustomTextEdit, LabeledInput
from ui.components.toggle_switch import LabeledToggle
from ui.components.custom_calendar import CustomCalendar
from PySide6.QtWidgets import QDateEdit
from PySide6.QtCore import QDate
from datetime import date
# 지시자
self.instructor_input = CustomLineEdit(placeholder="지시자 이름")
self.content_layout.addWidget(LabeledInput("지시자", self.instructor_input))
# 지시내용
self.content_input = CustomTextEdit(placeholder="지시 내용을 입력하세요", min_height=80)
self.content_layout.addWidget(LabeledInput("지시내용", self.content_input, required=True))
# 지시일자
self.date_input = QDateEdit()
self.date_input.setDate(QDate.currentDate())
self.date_input.setCalendarPopup(True)
self.content_layout.addWidget(LabeledInput("지시일자", self.date_input))
# 지속여부
self.continuous_toggle = LabeledToggle("지속 지시", initial_state=False)
self.content_layout.addWidget(self.continuous_toggle)
def _load_record(self, record: Instruction):
"""레코드 데이터 로드"""
self.instructor_input.setText(record.instructor or "")
self.content_input.set_text(record.instruction_content or "")
if record.instruction_date:
from PySide6.QtCore import QDate
if isinstance(record.instruction_date, date):
self.date_input.setDate(QDate(
record.instruction_date.year,
record.instruction_date.month,
record.instruction_date.day
))
self.continuous_toggle.set_state(record.is_continuous)
def get_data(self) -> dict:
"""입력 데이터 반환"""
from datetime import date
qdate = self.date_input.date()
instruction_date = date(qdate.year(), qdate.month(), qdate.day())
return {
"created_date": date.today().isoformat(),
"created_team": self.config.current_team,
"instructor": self.instructor_input.text(),
"instruction_content": self.content_input.get_text(),
"instruction_date": instruction_date.isoformat(),
"is_continuous": self.continuous_toggle.is_on,
}