handOver2/ui/panels/info_bar.py

1080 lines
41 KiB
Python

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