# -*- coding: utf-8 -*- """ 인포바 모듈 화면 상단의 정보 표시 바입니다. 오늘 날짜, 현재 팀, 근무 유형, 날씨 정보 등을 표시합니다. """ from datetime import datetime, date import webbrowser from PySide6.QtWidgets import ( QWidget, QHBoxLayout, QVBoxLayout, QLabel, QFrame, QMenu, QMessageBox ) from PySide6.QtCore import Qt, Signal, QTimer, QPoint from PySide6.QtGui import QFont, QAction, QMouseEvent, QEnterEvent, QCursor from ui.widgets.clickableLabel import ClickableLabel from ui.base.base_widget import BaseWidget from ui.components.custom_button import CustomButton from ui.dialogs.duty_dialog import DutyChangeDialog from ui.dialogs.weather_detail_dialog import WeatherDetailDialog from ui.dialogs.weather_location_dialog import WeatherLocationDialog from ui.dialogs.settings_dialog import SettingsDialog from core.constants import TEAMS from core.logger import get_logger from database.crud import CRUDManager from ui.dialogs.handover_dialog import HandoverDialog from services.weather_service import WeatherService logger = get_logger(__name__) class InfoBar(BaseWidget): """ 인포바 위젯 화면 상단 10% 영역에 표시되는 정보 바입니다. 날짜, 팀, 근무 유형, 팀 변경 버튼, 당무 정보, 날씨 정보 등을 포함합니다. Signals: team_change_requested: 팀 변경 요청 시그널 duty_change_requested: 당무 변경 요청 시그널 team_settings_requested: 팀 인원 설정 요청 시그널 Examples: >>> info_bar = InfoBar() >>> info_bar.update_weather({"temp": 15, "condition": "맑음"}) """ team_change_requested = Signal() duty_change_requested = Signal() team_settings_requested = Signal() def __init__(self, parent=None): super().__init__(parent) self._current_date = date.today() self._current_team = self.config.current_team self._current_shift = self.config.current_shift self._weather_data = {} self.crud = CRUDManager() self.duty_dialog = DutyChangeDialog() self.handover_dialog = HandoverDialog() self.weather_service = WeatherService() # 당무 정보 self._duty_vice_leader = "" # 당무 부팀장 self._duty_operator = "" # 당무 운용 self._setup_ui() self._connect_signals() self._start_clock() self._load_duty_info() self._update_weather_time() # 초기 날씨 시간 표시 logger.info("인포바 초기화 완료") def _setup_ui(self): """UI 설정""" layout = QHBoxLayout(self) layout.setContentsMargins(24, 12, 24, 12) layout.setSpacing(24) # 날짜 정보 self._create_date_section(layout) # 구분선 self._add_separator(layout) # 팀/근무 통합 정보 self._create_team_shift_section(layout) # 구분선 self._add_separator(layout) # 당무 정보 섹션 self._create_duty_section(layout) # 늘어남 영역 layout.addStretch() # 인수/인계 버튼 self._create_handover_button(layout) # 구분선 self._add_separator(layout) # 팀 인원 설정 버튼 self._create_team_settings_button(layout) # 구분선 self._add_separator(layout) # 시계 self._create_clock(layout) # 구분선 self._add_separator(layout) # 날씨 정보 self._create_weather_section(layout) # 스타일 적용 self._apply_style() def _create_date_section(self, layout: QHBoxLayout): """날짜 섹션 생성""" date_widget = QWidget() date_layout = QVBoxLayout(date_widget) date_layout.setContentsMargins(0, 0, 0, 0) date_layout.setSpacing(2) # 년월일 self.date_label = QLabel() self.date_label.setObjectName("dateLabel") self.date_label.setFont(QFont("GmarketSans", 18, QFont.Bold)) # 요일 self.weekday_label = QLabel() self.weekday_label.setObjectName("weekdayLabel") self.weekday_label.setFont(QFont("GmarketSans", 12)) self._update_date() date_layout.addWidget(self.date_label) date_layout.addWidget(self.weekday_label) layout.addWidget(date_widget) def _create_team_shift_section(self, layout: QHBoxLayout): """팀/근무 통합 섹션 생성""" team_shift_widget = QWidget() team_shift_layout = QVBoxLayout(team_shift_widget) team_shift_layout.setContentsMargins(0, 0, 0, 0) team_shift_layout.setSpacing(2) team_shift_title = QLabel("현재 팀/근무") team_shift_title.setObjectName("sectionTitle") team_shift_title.setFont(QFont("GmarketSans", 11)) # 팀/근무 통합 레이블 (더블클릭 가능) shift_icon = "☀️" if self._current_shift == "주간" else "🌙" self.team_shift_label = ClickableLabel(f"{self._current_team} {self._current_shift} {shift_icon}") self.team_shift_label.setObjectName("teamShiftLabel") self.team_shift_label.setFont(QFont("GmarketSans", 20, QFont.Bold)) # 더블클릭 이벤트 연결 self.team_shift_label.clicked.connect(self._on_team_shift_clicked) self.team_shift_label.hover_entered.connect(lambda: self._on_team_shift_hover_entered(shift_icon)) self.team_shift_label.hover_left.connect(lambda: self._on_team_shift_hover_left()) team_shift_layout.addWidget(team_shift_title) team_shift_layout.addWidget(self.team_shift_label) layout.addWidget(team_shift_widget) def _on_team_shift_hover_entered(self, shift_icon: str): """팀/근무 레이블 호버 진입""" self.team_shift_label.set_hover_text(f"팀 / 근무 변경하기") def _on_team_shift_hover_left(self): """팀/근무 레이블 호버 이탈""" self.team_shift_label.clear_hover_text() def _create_duty_section(self, layout: QHBoxLayout): """당무 정보 섹션 생성""" duty_widget = QWidget() duty_layout = QHBoxLayout(duty_widget) duty_layout.setContentsMargins(0, 0, 0, 0) duty_layout.setSpacing(16) # 부팀장 당무 vice_leader_widget = QWidget() vice_leader_layout = QVBoxLayout(vice_leader_widget) vice_leader_layout.setContentsMargins(0, 0, 0, 0) vice_leader_layout.setSpacing(2) vice_leader_title = QLabel("부팀장 당무") vice_leader_title.setObjectName("sectionTitle") vice_leader_title.setFont(QFont("GmarketSans", 11)) self.vice_leader_label = ClickableLabel("미지정") self.vice_leader_label.setObjectName("dutyLabel") self.vice_leader_label.setFont(QFont("GmarketSans", 14, QFont.Bold)) self.vice_leader_label.clicked.connect(lambda: self._on_duty_label_clicked("vice_leader")) self.vice_leader_label.hover_entered.connect(lambda: self._on_duty_label_hover_entered(self.vice_leader_label,"부팀장 당무 변경하기")) self.vice_leader_label.hover_left.connect(lambda: self._on_duty_label_hover_left(self.vice_leader_label)) vice_leader_layout.addWidget(vice_leader_title) vice_leader_layout.addWidget(self.vice_leader_label) duty_layout.addWidget(vice_leader_widget) # 운용 당무 operator_widget = QWidget() operator_layout = QVBoxLayout(operator_widget) operator_layout.setContentsMargins(0, 0, 0, 0) operator_layout.setSpacing(2) operator_title = QLabel("운용 당무") operator_title.setObjectName("sectionTitle") operator_title.setFont(QFont("GmarketSans", 11)) self.operator_label = ClickableLabel("미지정") self.operator_label.setObjectName("dutyLabel") self.operator_label.setFont(QFont("GmarketSans", 14, QFont.Bold)) self.operator_label.clicked.connect(lambda: self._on_duty_label_clicked("operator")) self.operator_label.hover_entered.connect(lambda: self._on_duty_label_hover_entered(self.operator_label, "운용 당무 변경하기")) self.operator_label.hover_left.connect(lambda: self._on_duty_label_hover_left(self.operator_label)) operator_layout.addWidget(operator_title) operator_layout.addWidget(self.operator_label) duty_layout.addWidget(operator_widget) layout.addWidget(duty_widget) def _on_duty_label_hover_entered(self, label: ClickableLabel, hover_text: str): """당무 레이블 호버 진입""" label.set_hover_text(hover_text) def _on_duty_label_hover_left(self, label: ClickableLabel): """당무 레이블 호버 이탈""" label.clear_hover_text() def _create_handover_button(self, layout: QHBoxLayout): """인수/인계 버튼 생성""" self.handover_btn = CustomButton( "📋 인수/인계", style_type="primary", fixed_height=36 ) self.handover_btn.clicked.connect(self._on_handover_clicked) layout.addWidget(self.handover_btn) def _create_team_settings_button(self, layout: QHBoxLayout): """팀 인원 설정 버튼 생성""" self.team_settings_btn = CustomButton( "⚙️ 인원 설정", style_type="text", fixed_height=36 ) self.team_settings_btn.clicked.connect(self._on_team_settings_clicked) layout.addWidget(self.team_settings_btn) def _create_clock(self, layout: QHBoxLayout): """시계 생성""" self.clock_label = QLabel() self.clock_label.setObjectName("clockLabel") self.clock_label.setFont(QFont("GmarketSans", 24, QFont.Bold)) self.clock_label.setAlignment(Qt.AlignCenter) self._update_clock() layout.addWidget(self.clock_label) def _create_weather_section(self, layout: QHBoxLayout): """날씨 섹션 생성""" weather_widget = QWidget() weather_layout = QHBoxLayout(weather_widget) weather_layout.setContentsMargins(0, 0, 0, 0) weather_layout.setSpacing(12) # 날씨 아이콘 self.weather_icon = QLabel("🌤") self.weather_icon.setFont(QFont("Segoe UI Emoji", 24)) self.weather_icon.setCursor(Qt.PointingHandCursor) # 날씨 정보 weather_info = QWidget() weather_info_layout = QVBoxLayout(weather_info) weather_info_layout.setContentsMargins(0, 0, 0, 0) weather_info_layout.setSpacing(2) self.temp_label = QLabel("--°C") self.temp_label.setObjectName("tempLabel") self.temp_label.setFont(QFont("GmarketSans", 16, QFont.Bold)) self.temp_label.setCursor(Qt.PointingHandCursor) self.weather_desc_label = QLabel("날씨 정보 없음") self.weather_desc_label.setObjectName("weatherDesc") self.weather_desc_label.setFont(QFont("GmarketSans", 11)) self.weather_desc_label.setCursor(Qt.PointingHandCursor) # 주간조 추가 정보 레이블 self.weather_extra_label = QLabel("") self.weather_extra_label.setObjectName("weatherExtra") self.weather_extra_label.setFont(QFont("GmarketSans", 9)) self.weather_extra_label.setCursor(Qt.PointingHandCursor) self.weather_extra_label.hide() # 초기에는 숨김 # 가져온 시간 레이블 self.weather_time_label = QLabel("") self.weather_time_label.setObjectName("weatherTime") self.weather_time_label.setFont(QFont("GmarketSans", 9)) self.weather_time_label.setCursor(Qt.PointingHandCursor) weather_info_layout.addWidget(self.temp_label) weather_info_layout.addWidget(self.weather_desc_label) weather_info_layout.addWidget(self.weather_extra_label) weather_info_layout.addWidget(self.weather_time_label) weather_layout.addWidget(self.weather_icon) weather_layout.addWidget(weather_info) # 새로고침 버튼 self.weather_refresh_btn = CustomButton( "🔄", style_type="text", fixed_height=32, fixed_width=32 ) self.weather_refresh_btn.setToolTip("날씨 새로고침") self.weather_refresh_btn.clicked.connect(self._on_weather_refresh_clicked) weather_layout.addWidget(self.weather_refresh_btn) # 날씨 위젯 전체에 이벤트 연결 weather_widget.setCursor(Qt.PointingHandCursor) weather_widget.mousePressEvent = self._on_weather_clicked weather_widget.mouseDoubleClickEvent = self._on_weather_double_clicked weather_widget.setContextMenuPolicy(Qt.CustomContextMenu) weather_widget.customContextMenuRequested.connect(self._on_weather_right_clicked) self.weather_widget = weather_widget # 참조 저장 layout.addWidget(weather_widget) def _add_separator(self, layout: QHBoxLayout): """구분선 추가""" separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setObjectName("separator") layout.addWidget(separator) def _apply_style(self): """스타일 적용""" theme = self.config.theme if theme == 'dark': bg = "#0f172a" text = "#f8fafc" secondary = "#94a3b8" accent = "#3b82f6" separator = "#334155" else: bg = "#ffffff" text = "#1e293b" secondary = "#64748b" accent = "#3b82f6" separator = "#e2e8f0" self.setStyleSheet(f""" InfoBar {{ background-color: {bg}; border-bottom: 1px solid {separator}; }} #dateLabel, #clockLabel, #tempLabel {{ color: {text}; }} #weekdayLabel, #sectionTitle, #weatherDesc, #weatherExtra, #weatherTime {{ color: {secondary}; }} #teamShiftLabel {{ color: {accent}; }} #dutyLabel {{ color: {text}; }} #separator {{ color: {separator}; }} """) def _connect_signals(self): """시그널 연결""" self.signals.team_changed.connect(self._on_team_changed) self.signals.shift_changed.connect(self._on_shift_changed) self.signals.weather_updated.connect(self._on_weather_updated) self.signals.weather_refresh_requested.connect(self._on_weather_refresh_requested) def _start_clock(self): """시계 타이머 시작""" self._clock_timer = QTimer() self._clock_timer.timeout.connect(self._update_clock) self._clock_timer.start(1000) # 1초마다 업데이트 def _update_date(self): """날짜 업데이트""" today = date.today() self._current_date = today # 한국어 요일 weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] weekday = weekdays[today.weekday()] self.date_label.setText(today.strftime("%Y년 %m월 %d일")) self.weekday_label.setText(weekday) def _update_clock(self): """시계 업데이트""" now = datetime.now() self.clock_label.setText(now.strftime("%H:%M:%S")) # 날짜가 바뀌었는지 확인 if now.date() != self._current_date: self._update_date() def _on_team_shift_clicked(self): """팀/근무 레이블 더블클릭""" self.team_change_requested.emit() self._show_team_selector() def _on_team_change_clicked(self): """팀 변경 버튼 클릭 (호환성 유지)""" self.team_change_requested.emit() self._show_team_selector() def _show_team_selector(self): """팀 선택 메뉴 표시 (서브메뉴로 주/야 선택)""" # 메뉴 스타일 theme = self.config.theme if theme == 'dark': menu_style = """ QMenu { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; } QMenu::item:checked { background-color: #22c55e; } QMenu::separator { height: 1px; background-color: #334155; margin: 4px 8px; } """ else: menu_style = """ QMenu { background-color: #ffffff; color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; color: white; } QMenu::item:checked { background-color: #22c55e; color: white; } QMenu::separator { height: 1px; background-color: #e2e8f0; margin: 4px 8px; } """ menu = QMenu(self) menu.setStyleSheet(menu_style) # 각 팀마다 서브메뉴로 주간/야간 선택 for team in TEAMS: team_menu = menu.addMenu(f"🏢 {team}") team_menu.setStyleSheet(menu_style) # 주간 day_action = QAction("☀️ 주간", self) day_action.triggered.connect( lambda checked, t=team, s="주간": self._select_team_and_shift(t, s) ) # 현재 선택된 팀/근무 표시 if team == self._current_team and self._current_shift == "주간": day_action.setCheckable(True) day_action.setChecked(True) team_menu.addAction(day_action) # 야간 night_action = QAction("🌙 야간", self) night_action.triggered.connect( lambda checked, t=team, s="야간": self._select_team_and_shift(t, s) ) if team == self._current_team and self._current_shift == "야간": night_action.setCheckable(True) night_action.setChecked(True) team_menu.addAction(night_action) # 레이블 위치에 메뉴 표시 pos = self.team_shift_label.mapToGlobal( self.team_shift_label.rect().bottomLeft() ) menu.exec(pos) def _select_team_and_shift(self, team: str, shift: str): """팀과 근무 유형 동시 선택""" # 팀 변경 self._current_team = team self.config.current_team = team # 근무 유형 변경 self._current_shift = shift self.config.current_shift = shift # 통합 레이블 업데이트 shift_icon = "☀️" if shift == "주간" else "🌙" self.team_shift_label.setText(f"{team} {shift} {shift_icon}") self.config.save() # 당무 정보 업데이트 (팀 변경 시 메시지 표시) self._load_duty_info(show_message=True) # 시그널 발생 (시그널 핸들러에서는 메시지 표시 안 함) self.signals.team_changed.emit(team) self.signals.shift_changed.emit(shift) logger.info("팀/근무 변경: %s %s", team, shift) def _select_team(self, team: str): """팀 선택""" self._current_team = team self.team_label.setText(team) self.config.current_team = team self.config.save() self.signals.team_changed.emit(team) logger.info("팀 변경: %s", team) def _select_shift(self, shift: str): """근무 유형 선택""" self._current_shift = shift self.shift_label.setText(shift) self.config.current_shift = shift self.config.save() self.signals.shift_changed.emit(shift) logger.info("근무 유형 변경: %s", shift) def _on_team_changed(self, team: str): """팀 변경 시그널 수신""" self._current_team = team shift_icon = "☀️" if self._current_shift == "주간" else "🌙" self.team_shift_label.setText(f"{team} {self._current_shift} {shift_icon}") # 당무 정보 업데이트 (시그널로 인한 호출이므로 메시지 표시 안 함) self._load_duty_info(show_message=False) def _on_shift_changed(self, shift: str): """근무 유형 변경 시그널 수신""" self._current_shift = shift shift_icon = "☀️" if shift == "주간" else "🌙" self.team_shift_label.setText(f"{self._current_team} {shift} {shift_icon}") # 당무 정보 업데이트 (시그널로 인한 호출이므로 메시지 표시 안 함) self._load_duty_info(show_message=False) # 날씨 정보 업데이트 (근무 형태 변경으로 인해 표시되는 통계가 바뀔 수 있음) if self._weather_data: self.update_weather(self._weather_data) def _on_weather_updated(self, weather_json: str): """날씨 업데이트 시그널 수신""" import json try: data = json.loads(weather_json) self.update_weather(data) except json.JSONDecodeError: pass def update_weather(self, data: dict): """ 날씨 정보 업데이트 Args: data: 날씨 데이터 {"temp": 15, "condition": "맑음", "icon": "☀️", ...} """ self._weather_data = data # 근무 형태에 따른 날씨 정보 가져오기 try: shift_weather_data = self.weather_service.get_weather_for_shift(self._current_shift) except Exception as e: logger.error(f"근무 형태 날씨 정보 조회 실패: {e}") shift_weather_data = None # 현재 온도 표시 current_temp = data.get("temp", "--") current_condition = data.get("condition", "정보 없음") current_icon = data.get("icon", "🌤") wind_speed = data.get("wind_speed", "--") precipitation_prob = data.get("precipitation_prob") # 온도 표시 (현재 기온 표시) if current_temp != "--" and current_temp is not None: temp_text = f"{current_temp}°C" else: temp_text = "--°C" self.temp_label.setText(temp_text) self.weather_icon.setText(current_icon) # 날씨 설명 레이블에 날씨 상태, 풍속, 강수 정보 표시 desc_parts = [current_condition] # 풍속 정보 추가 if wind_speed and wind_speed != "--" and wind_speed != "-": desc_parts.append(f"바람 {wind_speed}") # 강수확률 정보 추가 if precipitation_prob is not None and precipitation_prob != "--": desc_parts.append(f"강수 {precipitation_prob}%") self.weather_desc_label.setText(" | ".join(desc_parts)) # 근무 시간대의 통계 정보 표시 if shift_weather_data and shift_weather_data.get("data_points", 0) > 0: extra_parts = [] # 최저/최고 기온 temp_min = shift_weather_data.get("temp_min") temp_max = shift_weather_data.get("temp_max") if temp_min is not None and temp_max is not None: extra_parts.append(f"기온 최저 {temp_min}°C / 최고 {temp_max}°C") # 최저/최고 체감온도 feels_min = shift_weather_data.get("feels_like_min") feels_max = shift_weather_data.get("feels_like_max") if feels_min is not None and feels_max is not None and (feels_min != temp_min or feels_max != temp_max): extra_parts.append(f"체감 최저 {feels_min}°C / 최고 {feels_max}°C") # 최대 강수확률 max_precip = shift_weather_data.get("max_precipitation_prob") if max_precip is not None and max_precip > 0: extra_parts.append(f"강수 최대 {max_precip}%") if extra_parts: self.weather_extra_label.setText(" | ".join(extra_parts)) self.weather_extra_label.show() else: self.weather_extra_label.hide() else: # 데이터가 없을 경우 기존 로직으로 폴백 self.weather_extra_label.hide() # 가져온 시간 표시 fetched_at = data.get("fetched_at") if fetched_at: try: fetched_time = datetime.fromisoformat(fetched_at) time_str = fetched_time.strftime("%H:%M") self.weather_time_label.setText(f"업데이트: {time_str}") except (ValueError, TypeError): self._update_weather_time() else: self._update_weather_time() def _update_weather_time(self): """가져온 시간 업데이트""" try: from services.weather_service import WEATHER_TIMESTAMP_FILE if WEATHER_TIMESTAMP_FILE.exists(): with open(WEATHER_TIMESTAMP_FILE, 'r', encoding='utf-8') as f: timestamp_str = f.read().strip() fetched_time = datetime.fromisoformat(timestamp_str) time_str = fetched_time.strftime("%H:%M") self.weather_time_label.setText(f"업데이트: {time_str}") else: self.weather_time_label.setText("") except Exception as e: logger.error(f"날씨 시간 업데이트 실패: {e}") self.weather_time_label.setText("") # ======================================================================== # 당무 관련 메서드 # ======================================================================== def _load_duty_info(self, show_message: bool = False): """ 당무 정보 로드 Args: show_message: 저장되지 않은 경우 메시지를 표시할지 여부 """ try: today = date.today() # 오늘 날짜, 현재 팀, 현재 근무 유형의 당무 정보 조회 duty = self.crud.get_duty_schedule( today, self._current_team, self._current_shift ) if duty: self._duty_vice_leader = duty.vice_leader_name or "미지정" self._duty_operator = duty.operator_name or "미지정" else: self._duty_vice_leader = "미지정" self._duty_operator = "미지정" self.vice_leader_label.setText(self._duty_vice_leader) self.operator_label.setText(self._duty_operator) # 저장되지 않은 경우 사용자에게 알림 (팀 변경 시에만) if show_message and (not duty or (not duty.vice_leader_name and not duty.operator_name)): logger.info("당무 정보가 저장되지 않음: 팀=%s, 근무=%s", self._current_team, self._current_shift) # 사용자에게 알림 메시지 표시 QMessageBox.information( self, "당무 정보 없음", f"{self._current_team} {self._current_shift}의 당무 정보가 저장되지 않았습니다.\n\n" f"부팀장 당무와 운용 당무를 클릭하여 설정해주세요." ) except Exception as e: logger.error("당무 정보 로드 실패: %s", e) self._duty_vice_leader = "미지정" self._duty_operator = "미지정" self.vice_leader_label.setText("미지정") self.operator_label.setText("미지정") def _on_duty_label_clicked(self, duty_type: str): """당무 레이블 클릭""" # 간단한 선택 메뉴 표시 menu = QMenu(self) theme = self.config.theme if theme == 'dark': menu_style = """ QMenu { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; } """ else: menu_style = """ QMenu { background-color: #ffffff; color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; color: white; } """ menu.setStyleSheet(menu_style) # 팀 멤버 목록 가져오기 try: position = "부팀장" if duty_type == "vice_leader" else "운용" # 디버깅: 조회 파라미터 로그 logger.debug("당무 멤버 조회: 팀=%s, 직책=%s, 활성화만=%s", self._current_team, position, True) # 팀 멤버 목록 가져오기 members = self.crud.get_team_members_by_team( self._current_team, position, active_only=True ) current_name = self._duty_vice_leader if duty_type == "vice_leader" else self._duty_operator # 디버깅: 조회 결과 로그 logger.debug("조회된 멤버 수: %d", len(members)) if members: logger.debug("멤버 목록: %s", [m.name for m in members]) else: # 전체 멤버 조회해서 문제 확인 all_members = self.crud.get_team_members_by_team( self._current_team, position=None, active_only=False ) logger.debug("전체 멤버 수 (비활성 포함): %d", len(all_members)) active_members = self.crud.get_team_members_by_team( self._current_team, position=None, active_only=True ) logger.debug("활성 멤버 수: %d", len(active_members)) if active_members: logger.debug("활성 멤버 직책: %s", [f"{m.name}({m.position})" for m in active_members]) # 멤버 목록이 비어있으면 사용자에게 알림 if not members: logger.warning("당무 선택 가능한 멤버가 없습니다: 팀=%s, 직책=%s, 타입=%s", self._current_team, position, duty_type) # 사용자에게 안내 메시지 표시 QMessageBox.information( self, "멤버 없음", f"{self._current_team}의 {position} 직책 멤버가 없습니다.\n\n" f"인원 설정에서 {self._current_team}에 {position} 직책의 멤버를 추가해주세요." ) return # 멤버 목록 추가 for member in members: action = QAction(member.name, self) if member.name == current_name: action.setCheckable(True) action.setChecked(True) action.triggered.connect( lambda checked, m=member, dt=duty_type: self._select_duty_member(m.name, dt) ) menu.addAction(action) # 마우스 커서 위치에 메뉴 표시 menu.exec(QCursor.pos()) except Exception as e: logger.error("당무 메뉴 표시 실패: %s", e) def _select_duty_member(self, name: str, duty_type: str): """당무 멤버 선택""" today = date.today() # 기존 당무 정보 가져오기 existing_duty = self.crud.get_duty_schedule(today, self._current_team, self._current_shift) if duty_type == "vice_leader": # 부팀장 선택 members = self.crud.get_team_members_by_team(self._current_team, "부팀장", active_only=True) member = next((m for m in members if m.name == name), None) if member: self.crud.upsert_duty_schedule( duty_date=today, team=self._current_team, shift_type=self._current_shift, vice_leader_id=member.id, vice_leader_name=name, operator_id=existing_duty.operator_id if existing_duty else None, operator_name=existing_duty.operator_name if existing_duty else "" ) self._duty_vice_leader = name self.vice_leader_label.setText(name) else: # 운용 선택 members = self.crud.get_team_members_by_team(self._current_team, "운용", active_only=True) member = next((m for m in members if m.name == name), None) if member: self.crud.upsert_duty_schedule( duty_date=today, team=self._current_team, shift_type=self._current_shift, operator_id=member.id, operator_name=name, vice_leader_id=existing_duty.vice_leader_id if existing_duty else None, vice_leader_name=existing_duty.vice_leader_name if existing_duty else "" ) self._duty_operator = name self.operator_label.setText(name) logger.info("당무 선택: %s - %s", duty_type, name) def _on_duty_change_clicked(self): """당무 변경 버튼 클릭 (호환성 유지)""" self.duty_change_requested.emit() self._show_duty_selector() def _show_duty_selector(self): """당무 선택 다이얼로그 표시""" self.duty_dialog.change_UI_Info(self._current_team, self._current_shift) if self.duty_dialog.exec(): # 당무 정보 다시 로드 self._load_duty_info() def _on_handover_clicked(self): """인수/인계 버튼 클릭""" self.handover_dialog.change_UI_Info(self._current_team, self._current_shift) # dialog = HandoverDialog( # self, # current_team=self._current_team, # current_shift=self._current_shift # ) if self.handover_dialog.exec(): # 인수인계 완료 시 팀/근무 변경 new_team = self.handover_dialog.receiving_team new_shift = self.handover_dialog.receiving_shift self._select_team_and_shift(new_team, new_shift) # 당무 정보 다시 로드 self._load_duty_info() def _on_team_settings_clicked(self): """팀 인원 설정 버튼 클릭""" self.team_settings_requested.emit() self._show_team_settings() def _show_team_settings(self): """팀 인원 설정 다이얼로그 표시""" from ui.dialogs.team_settings_dialog import TeamSettingsDialog dialog = TeamSettingsDialog(self) dialog.exec() def update_duty_info(self, vice_leader: str, operator: str): """ 당무 정보 업데이트 Args: vice_leader: 부팀장 당무 operator: 운용 당무 """ self._duty_vice_leader = vice_leader self._duty_operator = operator self.vice_leader_label.setText(vice_leader or "미지정") self.operator_label.setText(operator or "미지정") def _on_weather_clicked(self, event): """날씨 클릭 - 상세 다이얼로그 표시""" # 더블클릭이 아닌 경우에만 실행 if event.button() == Qt.LeftButton: dialog = WeatherDetailDialog(self) dialog.exec() def _on_weather_double_clicked(self, event): """날씨 더블클릭 - 기상청 홈페이지 열기""" # 현재 설정된 지역 가져오기 location_name = self.config.get('weather', 'location_name', '부산') # 기상청 홈페이지 URL (지역별) # 부산: stn=159 city_urls = { '부산': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=159&x=24&y=5", '서울': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=108&x=60&y=127", '대구': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=143&x=89&y=90", '인천': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=112&x=55&y=124", '광주': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=156&x=58&y=74", '대전': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=133&x=68&y=100", '울산': "https://www.weather.go.kr/w/weather/forecast/short-term.do?stn=152&x=102&y=84", } url = city_urls.get(location_name, "https://www.weather.go.kr/w/index.do") webbrowser.open(url) logger.info("기상청 홈페이지 열기: %s - %s", location_name, url) def _on_weather_right_clicked(self, pos): """날씨 우클릭 - 지역 설정 메뉴""" menu = QMenu(self) theme = self.config.theme if theme == 'dark': menu_style = """ QMenu { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; } """ else: menu_style = """ QMenu { background-color: #ffffff; color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 24px; border-radius: 4px; } QMenu::item:selected { background-color: #3b82f6; color: white; } """ menu.setStyleSheet(menu_style) # 지역 설정 액션 location_action = QAction("지역 설정", self) location_action.triggered.connect(self._show_weather_location_settings) menu.addAction(location_action) # 날씨 설정 액션 settings_action = QAction("날씨 설정", self) settings_action.triggered.connect(self._show_weather_settings) menu.addAction(settings_action) menu.exec(self.weather_widget.mapToGlobal(pos)) def _show_weather_location_settings(self): """날씨 지역 설정 다이얼로그 표시""" dialog = WeatherLocationDialog(self) if dialog.exec(): # 날씨 정보 다시 로드 (시그널을 통해) self.signals.weather_location_changed.emit() def _show_weather_settings(self): """날씨 설정 다이얼로그 표시""" dialog = SettingsDialog(self) # 날씨 탭으로 이동 dialog.tabs.setCurrentIndex(4) # 날씨 탭 인덱스 dialog.exec() def _on_weather_refresh_clicked(self): """날씨 새로고침 버튼 클릭""" # 시그널을 통해 새로고침 요청 self.signals.weather_refresh_requested.emit() logger.info("날씨 새로고침 요청") def _on_weather_refresh_requested(self): """날씨 새로고침 요청 시그널 수신 (외부에서 호출)""" # 실제 새로고침은 MainWindow에서 처리 pass