# -*- 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, }