852 lines
30 KiB
Python
852 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
커스텀 캘린더 모듈
|
|
날짜 및 기간 선택을 위한 커스텀 캘린더 위젯입니다.
|
|
|
|
기능:
|
|
- 단일 날짜 선택
|
|
- 기간(시작~종료) 선택 토글
|
|
- 시간 선택 (시/분, 30분 단위)
|
|
- 날짜 강조 표시
|
|
- 현대적인 UI 스타일
|
|
"""
|
|
|
|
from datetime import date, datetime, time
|
|
from typing import Optional, Tuple
|
|
|
|
from PySide6.QtWidgets import (
|
|
QCalendarWidget, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QPushButton, QLabel, QSpinBox, QComboBox
|
|
)
|
|
from PySide6.QtCore import Qt, Signal, QDate, QTime
|
|
from PySide6.QtGui import QFont, QColor, QTextCharFormat, QPainter, QPen, QBrush
|
|
|
|
from core.config import ConfigManager
|
|
from core.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class RangeCalendarWidget(QCalendarWidget):
|
|
"""
|
|
기간 선택을 지원하는 캘린더 위젯
|
|
|
|
paintCell을 오버라이드하여 기간 범위를 시각적으로 표시합니다.
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.config = ConfigManager()
|
|
self._start_date: Optional[QDate] = None
|
|
self._end_date: Optional[QDate] = None
|
|
self._range_mode = False
|
|
|
|
# 기본 설정
|
|
self.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
|
|
self.setHorizontalHeaderFormat(QCalendarWidget.ShortDayNames)
|
|
self.setGridVisible(False)
|
|
self.setNavigationBarVisible(True)
|
|
|
|
self._apply_style()
|
|
|
|
def set_range_mode(self, enabled: bool):
|
|
"""기간 선택 모드 설정"""
|
|
self._range_mode = enabled
|
|
if not enabled:
|
|
self._start_date = None
|
|
self._end_date = None
|
|
self.updateCells()
|
|
|
|
def set_range(self, start: Optional[QDate], end: Optional[QDate]):
|
|
"""기간 설정"""
|
|
self._start_date = start
|
|
self._end_date = end
|
|
self.updateCells()
|
|
|
|
def get_range(self) -> Tuple[Optional[date], Optional[date]]:
|
|
"""선택된 기간 반환"""
|
|
start = None
|
|
end = None
|
|
|
|
if self._start_date:
|
|
start = date(self._start_date.year(), self._start_date.month(), self._start_date.day())
|
|
if self._end_date:
|
|
end = date(self._end_date.year(), self._end_date.month(), self._end_date.day())
|
|
|
|
return start, end
|
|
|
|
def paintCell(self, painter: QPainter, rect, qdate: QDate):
|
|
"""셀 그리기 (기간 범위 표시)"""
|
|
painter.save()
|
|
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
# 색상 정의
|
|
if is_dark:
|
|
text_color = QColor("#f8fafc")
|
|
range_bg = QColor("#3b82f6")
|
|
range_bg.setAlpha(60)
|
|
start_end_bg = QColor("#3b82f6")
|
|
today_border = QColor("#22c55e")
|
|
other_month_text = QColor("#64748b")
|
|
else:
|
|
text_color = QColor("#1e293b")
|
|
range_bg = QColor("#3b82f6")
|
|
range_bg.setAlpha(40)
|
|
start_end_bg = QColor("#3b82f6")
|
|
today_border = QColor("#22c55e")
|
|
other_month_text = QColor("#94a3b8")
|
|
|
|
# 현재 보이는 달인지 확인
|
|
is_current_month = qdate.month() == self.monthShown() and qdate.year() == self.yearShown()
|
|
|
|
# 기간 모드에서 범위 내 날짜인지 확인
|
|
in_range = False
|
|
is_start = False
|
|
is_end = False
|
|
|
|
if self._range_mode and self._start_date and self._end_date:
|
|
start = self._start_date
|
|
end = self._end_date
|
|
if start > end:
|
|
start, end = end, start
|
|
|
|
in_range = start <= qdate <= end
|
|
is_start = qdate == start
|
|
is_end = qdate == end
|
|
elif self._range_mode and self._start_date:
|
|
is_start = qdate == self._start_date
|
|
|
|
# 배경 그리기
|
|
if is_start or is_end:
|
|
# 시작/종료 날짜: 원형 배경
|
|
painter.setBrush(QBrush(start_end_bg))
|
|
painter.setPen(Qt.NoPen)
|
|
center = rect.center()
|
|
radius = min(rect.width(), rect.height()) // 2 - 4
|
|
painter.drawEllipse(center, radius, radius)
|
|
text_color = QColor("#ffffff")
|
|
elif in_range:
|
|
# 범위 내 날짜: 연한 배경
|
|
painter.fillRect(rect, range_bg)
|
|
|
|
# 오늘 날짜 테두리
|
|
if qdate == QDate.currentDate():
|
|
painter.setPen(QPen(today_border, 2))
|
|
painter.setBrush(Qt.NoBrush)
|
|
center = rect.center()
|
|
radius = min(rect.width(), rect.height()) // 2 - 4
|
|
painter.drawEllipse(center, radius, radius)
|
|
|
|
# 텍스트 그리기
|
|
if not is_current_month:
|
|
painter.setPen(other_month_text)
|
|
elif is_start or is_end:
|
|
painter.setPen(QColor("#ffffff"))
|
|
else:
|
|
painter.setPen(text_color)
|
|
|
|
painter.setFont(QFont("GmarketSans", 11))
|
|
painter.drawText(rect, Qt.AlignCenter, str(qdate.day()))
|
|
|
|
painter.restore()
|
|
|
|
def _apply_style(self):
|
|
"""스타일 적용"""
|
|
theme = self.config.theme
|
|
|
|
if theme == 'dark':
|
|
bg = "#1e293b"
|
|
header_bg = "#334155"
|
|
text = "#f8fafc"
|
|
hover_bg = "#475569"
|
|
nav_bg = "#0f172a"
|
|
border = "#334155"
|
|
else:
|
|
bg = "#ffffff"
|
|
header_bg = "#f1f5f9"
|
|
text = "#1e293b"
|
|
hover_bg = "#e2e8f0"
|
|
nav_bg = "#f8fafc"
|
|
border = "#e2e8f0"
|
|
|
|
self.setStyleSheet(f"""
|
|
QCalendarWidget {{
|
|
background-color: {bg};
|
|
font-family: 'GmarketSans';
|
|
border: 1px solid {border};
|
|
border-radius: 8px;
|
|
}}
|
|
|
|
QCalendarWidget QToolButton {{
|
|
background-color: {nav_bg};
|
|
color: {text};
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
margin: 2px;
|
|
}}
|
|
|
|
QCalendarWidget QToolButton:hover {{
|
|
background-color: {hover_bg};
|
|
}}
|
|
|
|
QCalendarWidget QToolButton::menu-indicator {{
|
|
image: none;
|
|
}}
|
|
|
|
QCalendarWidget QWidget#qt_calendar_navigationbar {{
|
|
background-color: {nav_bg};
|
|
padding: 4px;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
}}
|
|
|
|
QCalendarWidget QTableView {{
|
|
background-color: {bg};
|
|
outline: none;
|
|
selection-background-color: transparent;
|
|
selection-color: {text};
|
|
}}
|
|
|
|
QCalendarWidget QHeaderView::section {{
|
|
background-color: {header_bg};
|
|
color: {text};
|
|
padding: 6px;
|
|
border: none;
|
|
font-weight: bold;
|
|
font-size: 11px;
|
|
}}
|
|
""")
|
|
|
|
|
|
class TimeSelector(QWidget):
|
|
"""
|
|
시간 선택 위젯
|
|
|
|
시간과 분을 선택할 수 있습니다.
|
|
분은 30분 단위 (00, 30)로 기본 설정됩니다.
|
|
|
|
Signals:
|
|
time_changed: 시간이 변경되었을 때 (QTime)
|
|
"""
|
|
|
|
time_changed = Signal(object) # QTime
|
|
|
|
def __init__(self, parent=None, minute_step: int = 30):
|
|
"""
|
|
Args:
|
|
parent: 부모 위젯
|
|
minute_step: 분 단위 (기본 30분)
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
self.config = ConfigManager()
|
|
self._minute_step = minute_step
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""UI 설정"""
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(4)
|
|
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
# 시간 선택
|
|
self.hour_spin = QSpinBox()
|
|
self.hour_spin.setRange(0, 23)
|
|
self.hour_spin.setSuffix("시")
|
|
self.hour_spin.setFont(QFont("GmarketSans", 12))
|
|
self.hour_spin.valueChanged.connect(self._on_time_changed)
|
|
layout.addWidget(self.hour_spin)
|
|
|
|
# 구분자
|
|
colon = QLabel(":")
|
|
colon.setFont(QFont("GmarketSans", 14, QFont.Bold))
|
|
colon.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'};")
|
|
layout.addWidget(colon)
|
|
|
|
# 분 선택 (스텝이 1이면 스핀박스, 아니면 드롭다운)
|
|
if self._minute_step == 1:
|
|
self.minute_spin = QSpinBox()
|
|
self.minute_spin.setRange(0, 59)
|
|
self.minute_spin.setSuffix("분")
|
|
self.minute_spin.setFont(QFont("GmarketSans", 12))
|
|
self.minute_spin.valueChanged.connect(self._on_time_changed)
|
|
self.minute_combo = None
|
|
layout.addWidget(self.minute_spin)
|
|
else:
|
|
self.minute_spin = None
|
|
self.minute_combo = QComboBox()
|
|
self.minute_combo.setFont(QFont("GmarketSans", 12))
|
|
|
|
# 분 옵션 생성 (30분 단위)
|
|
minutes = []
|
|
for m in range(0, 60, self._minute_step):
|
|
minutes.append(f"{m:02d}분")
|
|
self.minute_combo.addItems(minutes)
|
|
self.minute_combo.currentIndexChanged.connect(self._on_time_changed)
|
|
layout.addWidget(self.minute_combo)
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""스타일 적용"""
|
|
theme = self.config.theme
|
|
|
|
if theme == 'dark':
|
|
bg = "#1e293b"
|
|
text = "#f8fafc"
|
|
border = "#475569"
|
|
hover = "#334155"
|
|
else:
|
|
bg = "#ffffff"
|
|
text = "#1e293b"
|
|
border = "#e2e8f0"
|
|
hover = "#f1f5f9"
|
|
|
|
style = f"""
|
|
QSpinBox, QComboBox {{
|
|
background-color: {bg};
|
|
color: {text};
|
|
border: 1px solid {border};
|
|
border-radius: 6px;
|
|
padding: 6px 10px;
|
|
min-width: 70px;
|
|
}}
|
|
QSpinBox:hover, QComboBox:hover {{
|
|
border-color: #3b82f6;
|
|
}}
|
|
QSpinBox::up-button, QSpinBox::down-button {{
|
|
width: 20px;
|
|
background-color: {hover};
|
|
border: none;
|
|
}}
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {{
|
|
background-color: #3b82f6;
|
|
}}
|
|
QComboBox::drop-down {{
|
|
border: none;
|
|
width: 25px;
|
|
}}
|
|
QComboBox::down-arrow {{
|
|
image: none;
|
|
border-left: 5px solid transparent;
|
|
border-right: 5px solid transparent;
|
|
border-top: 6px solid {text};
|
|
margin-right: 5px;
|
|
}}
|
|
QComboBox QAbstractItemView {{
|
|
background-color: {bg};
|
|
color: {text};
|
|
border: 1px solid {border};
|
|
selection-background-color: #3b82f6;
|
|
}}
|
|
"""
|
|
self.setStyleSheet(style)
|
|
|
|
def _on_time_changed(self):
|
|
"""시간 변경 시"""
|
|
current_time = self.get_time()
|
|
self.time_changed.emit(current_time)
|
|
|
|
def get_time(self) -> time:
|
|
"""현재 선택된 시간 반환"""
|
|
hour = self.hour_spin.value()
|
|
if self._minute_step == 1 and self.minute_spin:
|
|
minute = self.minute_spin.value()
|
|
else:
|
|
minute_idx = self.minute_combo.currentIndex()
|
|
minute = minute_idx * self._minute_step
|
|
return time(hour, minute)
|
|
|
|
def set_time(self, t: time):
|
|
"""시간 설정"""
|
|
self.hour_spin.setValue(t.hour)
|
|
if self._minute_step == 1 and self.minute_spin:
|
|
self.minute_spin.setValue(t.minute)
|
|
else:
|
|
# 가장 가까운 분 단위로 반올림
|
|
minute_idx = round(t.minute / self._minute_step)
|
|
if minute_idx >= self.minute_combo.count():
|
|
minute_idx = 0
|
|
self.minute_combo.setCurrentIndex(minute_idx)
|
|
|
|
def get_qtime(self) -> QTime:
|
|
"""QTime으로 반환"""
|
|
t = self.get_time()
|
|
return QTime(t.hour, t.minute)
|
|
|
|
|
|
class CustomCalendar(QWidget):
|
|
"""
|
|
커스텀 캘린더 위젯
|
|
|
|
날짜 또는 기간 선택 기능을 제공합니다.
|
|
'기간' 토글을 활성화하면 시작일과 종료일을 선택할 수 있습니다.
|
|
시간 선택 옵션을 활성화하면 시/분도 선택할 수 있습니다.
|
|
|
|
Signals:
|
|
date_selected: 단일 날짜 선택 시그널 (date)
|
|
range_selected: 기간 선택 시그널 (start: date, end: date)
|
|
datetime_selected: 날짜+시간 선택 시그널 (datetime)
|
|
range_datetime_selected: 기간+시간 선택 시그널 (start: datetime, end: datetime)
|
|
|
|
Examples:
|
|
>>> calendar = CustomCalendar(show_time=True)
|
|
>>> calendar.date_selected.connect(self.on_date_selected)
|
|
>>> calendar.datetime_selected.connect(self.on_datetime_selected)
|
|
"""
|
|
|
|
date_selected = Signal(object) # date
|
|
range_selected = Signal(object, object) # start_date, end_date
|
|
datetime_selected = Signal(object) # datetime
|
|
range_datetime_selected = Signal(object, object) # start_datetime, end_datetime
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
show_range_toggle: bool = True,
|
|
show_time: bool = False,
|
|
minute_step: int = 30
|
|
):
|
|
"""
|
|
Args:
|
|
parent: 부모 위젯
|
|
show_range_toggle: 기간 토글 버튼 표시 여부
|
|
show_time: 시간 선택 표시 여부
|
|
minute_step: 분 단위 (기본 30분)
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
self.config = ConfigManager()
|
|
self._show_range_toggle = show_range_toggle
|
|
self._show_time = show_time
|
|
self._minute_step = minute_step
|
|
self._range_mode = False
|
|
self._first_click = True # 기간 모드에서 첫 번째 클릭인지
|
|
self._start_date: Optional[QDate] = None
|
|
self._end_date: Optional[QDate] = None
|
|
|
|
self._setup_ui()
|
|
self._connect_signals()
|
|
|
|
def _setup_ui(self):
|
|
"""UI 설정"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(8)
|
|
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
# 상단 툴바 (기간 토글, 날짜 표시)
|
|
toolbar = QWidget()
|
|
toolbar_layout = QHBoxLayout(toolbar)
|
|
toolbar_layout.setContentsMargins(8, 4, 8, 4)
|
|
toolbar_layout.setSpacing(8)
|
|
|
|
# 선택된 날짜/기간 표시
|
|
self.date_label = QLabel("날짜를 선택하세요")
|
|
self.date_label.setFont(QFont("GmarketSans", 11))
|
|
self.date_label.setStyleSheet(f"color: {'#94a3b8' if is_dark else '#64748b'};")
|
|
toolbar_layout.addWidget(self.date_label)
|
|
|
|
toolbar_layout.addStretch()
|
|
|
|
# 기간 토글 버튼
|
|
if self._show_range_toggle:
|
|
self.range_btn = QPushButton("📅 기간")
|
|
self.range_btn.setCheckable(True)
|
|
self.range_btn.setFont(QFont("GmarketSans", 11))
|
|
self.range_btn.setCursor(Qt.PointingHandCursor)
|
|
self.range_btn.clicked.connect(self._toggle_range_mode)
|
|
|
|
btn_style = f"""
|
|
QPushButton {{
|
|
background-color: {'#334155' if is_dark else '#e2e8f0'};
|
|
color: {'#f8fafc' if is_dark else '#1e293b'};
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {'#475569' if is_dark else '#cbd5e1'};
|
|
}}
|
|
QPushButton:checked {{
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
}}
|
|
"""
|
|
self.range_btn.setStyleSheet(btn_style)
|
|
toolbar_layout.addWidget(self.range_btn)
|
|
else:
|
|
self.range_btn = None
|
|
|
|
layout.addWidget(toolbar)
|
|
|
|
# 캘린더
|
|
self.calendar = RangeCalendarWidget()
|
|
layout.addWidget(self.calendar)
|
|
|
|
# 시간 선택 영역 (옵션)
|
|
if self._show_time:
|
|
time_container = QWidget()
|
|
time_layout = QHBoxLayout(time_container)
|
|
time_layout.setContentsMargins(8, 8, 8, 8)
|
|
time_layout.setSpacing(16)
|
|
|
|
# 시작 시간 (기간 모드)
|
|
self.start_time_label = QLabel("시작 시간:")
|
|
self.start_time_label.setFont(QFont("GmarketSans", 11))
|
|
self.start_time_label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'};")
|
|
self.start_time_label.setVisible(False)
|
|
time_layout.addWidget(self.start_time_label)
|
|
|
|
self.start_time_selector = TimeSelector(minute_step=self._minute_step)
|
|
self.start_time_selector.time_changed.connect(self._on_time_changed)
|
|
time_layout.addWidget(self.start_time_selector)
|
|
|
|
# 종료 시간 (기간 모드)
|
|
self.end_time_label = QLabel("→ 종료 시간:")
|
|
self.end_time_label.setFont(QFont("GmarketSans", 11))
|
|
self.end_time_label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'};")
|
|
self.end_time_label.setVisible(False)
|
|
time_layout.addWidget(self.end_time_label)
|
|
|
|
self.end_time_selector = TimeSelector(minute_step=self._minute_step)
|
|
self.end_time_selector.time_changed.connect(self._on_time_changed)
|
|
self.end_time_selector.setVisible(False)
|
|
time_layout.addWidget(self.end_time_selector)
|
|
|
|
time_layout.addStretch()
|
|
layout.addWidget(time_container)
|
|
else:
|
|
self.start_time_selector = None
|
|
self.end_time_selector = None
|
|
|
|
# 하단 버튼
|
|
btn_container = QWidget()
|
|
btn_layout = QHBoxLayout(btn_container)
|
|
btn_layout.setContentsMargins(8, 4, 8, 8)
|
|
btn_layout.setSpacing(8)
|
|
|
|
# 오늘 버튼
|
|
self.today_btn = QPushButton("오늘")
|
|
self.today_btn.setFont(QFont("GmarketSans", 11))
|
|
self.today_btn.setCursor(Qt.PointingHandCursor)
|
|
self.today_btn.clicked.connect(self._go_to_today)
|
|
self.today_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {'#334155' if is_dark else '#e2e8f0'};
|
|
color: {'#f8fafc' if is_dark else '#1e293b'};
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 6px 16px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {'#475569' if is_dark else '#cbd5e1'};
|
|
}}
|
|
""")
|
|
btn_layout.addWidget(self.today_btn)
|
|
|
|
btn_layout.addStretch()
|
|
|
|
# 초기화 버튼
|
|
self.clear_btn = QPushButton("초기화")
|
|
self.clear_btn.setFont(QFont("GmarketSans", 11))
|
|
self.clear_btn.setCursor(Qt.PointingHandCursor)
|
|
self.clear_btn.clicked.connect(self._clear_selection)
|
|
self.clear_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {'#334155' if is_dark else '#e2e8f0'};
|
|
color: {'#f8fafc' if is_dark else '#1e293b'};
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 6px 16px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {'#475569' if is_dark else '#cbd5e1'};
|
|
}}
|
|
""")
|
|
btn_layout.addWidget(self.clear_btn)
|
|
|
|
layout.addWidget(btn_container)
|
|
|
|
def _connect_signals(self):
|
|
"""시그널 연결"""
|
|
self.calendar.clicked.connect(self._on_date_clicked)
|
|
|
|
def _toggle_range_mode(self):
|
|
"""기간 선택 모드 토글"""
|
|
if self.range_btn:
|
|
self._range_mode = self.range_btn.isChecked()
|
|
else:
|
|
self._range_mode = not self._range_mode
|
|
|
|
self._first_click = True
|
|
self._start_date = None
|
|
self._end_date = None
|
|
self.calendar.set_range_mode(self._range_mode)
|
|
|
|
# 시간 선택기 표시/숨김
|
|
if self._show_time:
|
|
self.start_time_label.setVisible(self._range_mode)
|
|
self.end_time_label.setVisible(self._range_mode)
|
|
self.end_time_selector.setVisible(self._range_mode)
|
|
|
|
self._update_date_label()
|
|
|
|
def set_range_mode(self, enabled: bool):
|
|
"""외부에서 기간 모드 설정"""
|
|
if self.range_btn:
|
|
self.range_btn.setChecked(enabled)
|
|
self._range_mode = enabled
|
|
self.calendar.set_range_mode(enabled)
|
|
|
|
if self._show_time:
|
|
self.start_time_label.setVisible(enabled)
|
|
self.end_time_label.setVisible(enabled)
|
|
self.end_time_selector.setVisible(enabled)
|
|
|
|
self._update_date_label()
|
|
|
|
def _on_date_clicked(self, qdate: QDate):
|
|
"""날짜 클릭 이벤트"""
|
|
if self._range_mode:
|
|
# 기간 선택 모드
|
|
if self._first_click:
|
|
# 첫 번째 클릭: 시작일 설정
|
|
self._start_date = qdate
|
|
self._end_date = None
|
|
self._first_click = False
|
|
self.calendar.set_range(self._start_date, None)
|
|
else:
|
|
# 두 번째 클릭: 종료일 설정
|
|
self._end_date = qdate
|
|
self._first_click = True
|
|
|
|
# 시작일이 종료일보다 크면 스왑
|
|
if self._start_date > self._end_date:
|
|
self._start_date, self._end_date = self._end_date, self._start_date
|
|
|
|
self.calendar.set_range(self._start_date, self._end_date)
|
|
|
|
# 시그널 발생
|
|
start = date(self._start_date.year(), self._start_date.month(), self._start_date.day())
|
|
end = date(self._end_date.year(), self._end_date.month(), self._end_date.day())
|
|
|
|
if self._show_time:
|
|
start_time = self.start_time_selector.get_time()
|
|
end_time = self.end_time_selector.get_time()
|
|
start_dt = datetime.combine(start, start_time)
|
|
end_dt = datetime.combine(end, end_time)
|
|
self.range_datetime_selected.emit(start_dt, end_dt)
|
|
else:
|
|
self.range_selected.emit(start, end)
|
|
else:
|
|
# 단일 날짜 선택 모드
|
|
selected_date = date(qdate.year(), qdate.month(), qdate.day())
|
|
|
|
if self._show_time:
|
|
selected_time = self.start_time_selector.get_time()
|
|
selected_dt = datetime.combine(selected_date, selected_time)
|
|
self.datetime_selected.emit(selected_dt)
|
|
else:
|
|
self.date_selected.emit(selected_date)
|
|
|
|
self._update_date_label()
|
|
|
|
def _on_time_changed(self, _time):
|
|
"""시간 변경 시"""
|
|
# 날짜가 선택된 상태에서 시간만 변경되면 시그널 발생
|
|
if self._range_mode:
|
|
if self._start_date and self._end_date:
|
|
start = date(self._start_date.year(), self._start_date.month(), self._start_date.day())
|
|
end = date(self._end_date.year(), self._end_date.month(), self._end_date.day())
|
|
start_time = self.start_time_selector.get_time()
|
|
end_time = self.end_time_selector.get_time()
|
|
start_dt = datetime.combine(start, start_time)
|
|
end_dt = datetime.combine(end, end_time)
|
|
self.range_datetime_selected.emit(start_dt, end_dt)
|
|
else:
|
|
qdate = self.calendar.selectedDate()
|
|
selected_date = date(qdate.year(), qdate.month(), qdate.day())
|
|
selected_time = self.start_time_selector.get_time()
|
|
selected_dt = datetime.combine(selected_date, selected_time)
|
|
self.datetime_selected.emit(selected_dt)
|
|
|
|
self._update_date_label()
|
|
|
|
def _update_date_label(self):
|
|
"""날짜 라벨 업데이트"""
|
|
theme = self.config.theme
|
|
is_dark = theme == 'dark'
|
|
|
|
if self._range_mode:
|
|
if self._start_date and self._end_date:
|
|
start = self._start_date
|
|
end = self._end_date
|
|
text = f"{start.year()}.{start.month():02d}.{start.day():02d}"
|
|
|
|
if self._show_time:
|
|
st = self.start_time_selector.get_time()
|
|
text += f" {st.hour:02d}:{st.minute:02d}"
|
|
|
|
text += f" ~ {end.year()}.{end.month():02d}.{end.day():02d}"
|
|
|
|
if self._show_time:
|
|
et = self.end_time_selector.get_time()
|
|
text += f" {et.hour:02d}:{et.minute:02d}"
|
|
|
|
self.date_label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'}; font-weight: bold;")
|
|
elif self._start_date:
|
|
text = f"{self._start_date.year()}.{self._start_date.month():02d}.{self._start_date.day():02d} ~ ?"
|
|
self.date_label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'};")
|
|
else:
|
|
text = "시작일을 선택하세요"
|
|
self.date_label.setStyleSheet(f"color: {'#94a3b8' if is_dark else '#64748b'};")
|
|
else:
|
|
qdate = self.calendar.selectedDate()
|
|
text = f"{qdate.year()}.{qdate.month():02d}.{qdate.day():02d}"
|
|
|
|
if self._show_time:
|
|
t = self.start_time_selector.get_time()
|
|
text += f" {t.hour:02d}:{t.minute:02d}"
|
|
|
|
self.date_label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'}; font-weight: bold;")
|
|
|
|
self.date_label.setText(text)
|
|
|
|
def _go_to_today(self):
|
|
"""오늘 날짜로 이동"""
|
|
self.calendar.setSelectedDate(QDate.currentDate())
|
|
|
|
if not self._range_mode:
|
|
today = date.today()
|
|
if self._show_time:
|
|
now = datetime.now()
|
|
self.start_time_selector.set_time(now.time())
|
|
self.datetime_selected.emit(now)
|
|
else:
|
|
self.date_selected.emit(today)
|
|
|
|
self._update_date_label()
|
|
|
|
def _clear_selection(self):
|
|
"""선택 초기화"""
|
|
self._first_click = True
|
|
self._start_date = None
|
|
self._end_date = None
|
|
self.calendar.set_range(None, None)
|
|
|
|
if self._show_time:
|
|
self.start_time_selector.set_time(time(0, 0))
|
|
if self.end_time_selector:
|
|
self.end_time_selector.set_time(time(0, 0))
|
|
|
|
self._update_date_label()
|
|
|
|
def set_date(self, d: date):
|
|
"""날짜 설정"""
|
|
self.calendar.setSelectedDate(QDate(d.year, d.month, d.day))
|
|
self._update_date_label()
|
|
|
|
def get_date(self) -> date:
|
|
"""선택된 날짜 반환"""
|
|
qdate = self.calendar.selectedDate()
|
|
return date(qdate.year(), qdate.month(), qdate.day())
|
|
|
|
def set_datetime(self, dt: datetime):
|
|
"""날짜와 시간 설정"""
|
|
self.calendar.setSelectedDate(QDate(dt.year, dt.month, dt.day))
|
|
if self._show_time:
|
|
self.start_time_selector.set_time(dt.time())
|
|
self._update_date_label()
|
|
|
|
def get_datetime(self) -> datetime:
|
|
"""선택된 날짜+시간 반환"""
|
|
qdate = self.calendar.selectedDate()
|
|
d = date(qdate.year(), qdate.month(), qdate.day())
|
|
|
|
if self._show_time:
|
|
t = self.start_time_selector.get_time()
|
|
else:
|
|
t = time(0, 0)
|
|
|
|
return datetime.combine(d, t)
|
|
|
|
def set_range(self, start: date, end: date):
|
|
"""기간 설정"""
|
|
self._range_mode = True
|
|
if self.range_btn:
|
|
self.range_btn.setChecked(True)
|
|
|
|
self._start_date = QDate(start.year, start.month, start.day)
|
|
self._end_date = QDate(end.year, end.month, end.day)
|
|
self._first_click = True
|
|
|
|
self.calendar.set_range_mode(True)
|
|
self.calendar.set_range(self._start_date, self._end_date)
|
|
|
|
if self._show_time:
|
|
self.start_time_label.setVisible(True)
|
|
self.end_time_label.setVisible(True)
|
|
self.end_time_selector.setVisible(True)
|
|
|
|
self._update_date_label()
|
|
|
|
def set_range_datetime(self, start: datetime, end: datetime):
|
|
"""기간 + 시간 설정"""
|
|
self.set_range(start.date(), end.date())
|
|
|
|
if self._show_time:
|
|
self.start_time_selector.set_time(start.time())
|
|
self.end_time_selector.set_time(end.time())
|
|
|
|
self._update_date_label()
|
|
|
|
def get_range(self) -> Tuple[Optional[date], Optional[date]]:
|
|
"""선택된 기간 반환"""
|
|
return self.calendar.get_range()
|
|
|
|
def get_range_datetime(self) -> Tuple[Optional[datetime], Optional[datetime]]:
|
|
"""선택된 기간 + 시간 반환"""
|
|
start_date, end_date = self.calendar.get_range()
|
|
|
|
if start_date and end_date:
|
|
if self._show_time:
|
|
start_time = self.start_time_selector.get_time()
|
|
end_time = self.end_time_selector.get_time()
|
|
else:
|
|
start_time = time(0, 0)
|
|
end_time = time(23, 59)
|
|
|
|
return (
|
|
datetime.combine(start_date, start_time),
|
|
datetime.combine(end_date, end_time)
|
|
)
|
|
|
|
return None, None
|
|
|
|
def highlight_dates(self, dates: list, color: str = "#3b82f6"):
|
|
"""
|
|
여러 날짜 강조 표시
|
|
|
|
Args:
|
|
dates: date 리스트
|
|
color: 강조 색상
|
|
"""
|
|
highlight_format = QTextCharFormat()
|
|
highlight_format.setBackground(QColor(color))
|
|
highlight_format.setForeground(QColor("#ffffff"))
|
|
|
|
for d in dates:
|
|
self.calendar.setDateTextFormat(QDate(d.year, d.month, d.day), highlight_format)
|