1080 lines
41 KiB
Python
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
|