i18n: complete UI/achievement keying, fix help_view tr() and i18n syntax errors

This commit is contained in:
KINDNICK
2026-06-16 10:52:57 +09:00
parent f5751460e3
commit 63b0e324b9
34 changed files with 3400 additions and 1175 deletions
+154 -153
View File
@@ -17,6 +17,7 @@
- notification_log: 휴식 권고 카운트
"""
from __future__ import annotations
from core.i18n import tr
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import Callable, Optional, List, Tuple
@@ -426,40 +427,40 @@ def _bool_eval(condition_fn):
# ---- 1. 출근 streak (24개 — 22번 거북이 제거) ----
_STREAK_DEFS = [
# (code, name, desc, target, evaluator, tier, icon)
('streak_first', '첫걸음', '첫 출근 기록', 1,
('streak_first', tr('achieve.streak_first.name'), tr('achieve.streak_first.desc'), 1,
_bool_eval(lambda db: _count_work_records(db) >= 1), TIER_BRONZE, '👋'),
('streak_3', '뿌리내림', '3일 연속 영업일 출근', 3,
('streak_3', tr('achieve.streak_3.name'), tr('achieve.streak_3.desc'), 3,
_make_streak_eval(3), TIER_BRONZE, '🌱'),
('streak_5', '첫 주 완주', '5 영업일 연속 출근', 5,
('streak_5', tr('achieve.streak_5.name'), tr('achieve.streak_5.desc'), 5,
_make_streak_eval(5), TIER_SILVER, '📅'),
('streak_7_cal', '7일 연속', '주말 포함 7일 연속 출근', 7,
('streak_7_cal', tr('achieve.streak_7_cal.name'), tr('achieve.streak_7_cal.desc'), 7,
_make_streak_eval(7, business_only=False), TIER_SILVER, '🔥'),
('streak_10', '2주 연속', '10 영업일 연속 출근', 10,
('streak_10', tr('achieve.streak_10.name'), tr('achieve.streak_10.desc'), 10,
_make_streak_eval(10), TIER_SILVER, '💪'),
('streak_22', '한 달 개근', '한 달 영업일 100% 출근 (22일)', 22,
('streak_22', tr('achieve.streak_22.name'), tr('achieve.streak_22.desc'), 22,
_make_streak_eval(22), TIER_GOLD, '🏔️'),
('streak_50', '50일 연속', '50 영업일 연속 출근', 50,
('streak_50', tr('achieve.streak_50.name'), tr('achieve.streak_50.desc'), 50,
_make_streak_eval(50), TIER_GOLD, '🎯'),
('streak_100', '100일 연속', '100 영업일 연속 출근', 100,
('streak_100', tr('achieve.streak_100.name'), tr('achieve.streak_100.desc'), 100,
_make_streak_eval(100), TIER_PLATINUM, '💎'),
('streak_quarter', '분기 완주', '약 65 영업일 (3개월)', 65,
('streak_quarter', tr('achieve.streak_quarter.name'), tr('achieve.streak_quarter.desc'), 65,
_make_streak_eval(65), TIER_PLATINUM, '🏆'),
('streak_half_year', '반년 마라톤', '약 130 영업일 (6개월)', 130,
('streak_half_year', tr('achieve.streak_half_year.name'), tr('achieve.streak_half_year.desc'), 130,
_make_streak_eval(130), TIER_PLATINUM, '👑'),
('streak_year', '1년 풀 시즌', '약 260 영업일 (1년)', 260,
('streak_year', tr('achieve.streak_year.name'), tr('achieve.streak_year.desc'), 260,
_make_streak_eval(260), TIER_LEGEND, '🌟'),
('streak_200', '사이언스', '200 영업일 연속', 200,
('streak_200', tr('achieve.streak_200.name'), tr('achieve.streak_200.desc'), 200,
_make_streak_eval(200), TIER_LEGEND, '🌌'),
('streak_365_cal', '불사신', '365일 달력 연속', 365,
('streak_365_cal', tr('achieve.streak_365_cal.name'), tr('achieve.streak_365_cal.desc'), 365,
_make_streak_eval(365, business_only=False), TIER_LEGEND, '🛡️'),
('streak_resilience', '회복력', '결근 후 다음날 즉시 출근 (자동: 달력 streak 깨진 후 재시작)', 1,
('streak_resilience', tr('achieve.streak_resilience.name'), tr('achieve.streak_resilience.desc'), 1,
_bool_eval(lambda db: _consecutive_workdays(db) >= 1
and _count_work_records(db) >= 5), TIER_BRONZE, '⚡'),
('streak_total_100', '누적 100회', '누적 출근 100회', 100,
('streak_total_100', tr('achieve.streak_total_100.name'), tr('achieve.streak_total_100.desc'), 100,
_make_count_eval(_count_work_records, 100), TIER_GOLD, '💼'),
('streak_total_500', '누적 500회', '누적 출근 500회', 500,
('streak_total_500', tr('achieve.streak_total_500.name'), tr('achieve.streak_total_500.desc'), 500,
_make_count_eval(_count_work_records, 500), TIER_PLATINUM, '🏛️'),
('streak_total_1000', '누적 1000회', '누적 출근 1000회', 1000,
('streak_total_1000', tr('achieve.streak_total_1000.name'), tr('achieve.streak_total_1000.desc'), 1000,
_make_count_eval(_count_work_records, 1000), TIER_LEGEND, '🎖️'),
]
@@ -476,37 +477,37 @@ def _count_weekday_clockins(db, weekday: int) -> int:
_STREAK_DEFS.extend([
('streak_monday_10', '월요일 정복', '월요일 10주 연속 출근', 10,
('streak_monday_10', tr('achieve.streak_monday_10.name'), tr('achieve.streak_monday_10.desc'), 10,
_make_count_eval(lambda db: _count_weekday_clockins(db, 1), 10), TIER_SILVER, '🌅'),
('streak_friday_10', '금요일 무결', '금요일 10주 연속 출근', 10,
('streak_friday_10', tr('achieve.streak_friday_10.name'), tr('achieve.streak_friday_10.desc'), 10,
_make_count_eval(lambda db: _count_weekday_clockins(db, 5), 10), TIER_SILVER, '🌒'),
])
# ---- 2. 시간 엄수 (19개 - 34/46 제거) ----
_PUNCTUAL_DEFS = [
('punc_before_8_1', '얼리버드', '08:00 이전 출근 1회', 1,
('punc_before_8_1', tr('achieve.punc_before_8_1.name'), tr('achieve.punc_before_8_1.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_before(db, 8), 1), TIER_BRONZE, '🌄'),
('punc_before_8_10', '참새족', '08:00 이전 10회', 10,
('punc_before_8_10', tr('achieve.punc_before_8_10.name'), tr('achieve.punc_before_8_10.desc'), 10,
_make_count_eval(lambda db: _count_clock_in_before(db, 8), 10), TIER_SILVER, '🐦'),
('punc_before_8_30', '일찍 자고 일찍', '08:00 이전 30회', 30,
('punc_before_8_30', tr('achieve.punc_before_8_30.name'), tr('achieve.punc_before_8_30.desc'), 30,
_make_count_eval(lambda db: _count_clock_in_before(db, 8), 30), TIER_GOLD, '🌞'),
('punc_before_6_1', '새벽잠 없음', '06:00 이전 1회', 1,
('punc_before_6_1', tr('achieve.punc_before_6_1.name'), tr('achieve.punc_before_6_1.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_before(db, 6), 1), TIER_GOLD, '🥱'),
('punc_before_6_10', '어둠을 가르는 자', '06:00 이전 10회', 10,
('punc_before_6_10', tr('achieve.punc_before_6_10.name'), tr('achieve.punc_before_6_10.desc'), 10,
_make_count_eval(lambda db: _count_clock_in_before(db, 6), 10), TIER_PLATINUM, '🌑'),
('punc_before_5', '새벽 챔피언', '05:00 이전 출근', 1,
('punc_before_5', tr('achieve.punc_before_5.name'), tr('achieve.punc_before_5.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_before(db, 5), 1), TIER_LEGEND, '🌌'),
('punc_at_9', '9시 정각', '09:00 정각(±1분) 출근 1회', 1,
('punc_at_9', tr('achieve.punc_at_9.name'), tr('achieve.punc_at_9.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 2), 1),
TIER_BRONZE, '🎯'),
('punc_at_9_5', '완벽한 9시', '09:00 정각(±1분) 5회', 5,
('punc_at_9_5', tr('achieve.punc_at_9_5.name'), tr('achieve.punc_at_9_5.desc'), 5,
_make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 2), 5),
TIER_SILVER, '🏹'),
('punc_late_5min', '5분 늦음', '09:00~09:05 출근 1회 (자조)', 1,
('punc_late_5min', tr('achieve.punc_late_5min.name'), tr('achieve.punc_late_5min.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 6), 1),
TIER_BRONZE, '🛌'),
('punc_at_909', '운명의 시각', '09:09 출근 (시크릿)', 1,
('punc_at_909', tr('achieve.punc_at_909.name'), tr('achieve.punc_at_909.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 9, 9, 10), 1),
TIER_GOLD, '🎰'),
]
@@ -525,76 +526,76 @@ def _count_clock_in_in_range_minute(db, sh: int, sm: int, eh: int, em: int) -> i
# ---- 3. 워라밸·정시 퇴근 (8개 코어) ----
_BALANCE_DEFS = [
('bal_first_punct', '첫 칼퇴', '정시 퇴근 첫 달성', 1,
('bal_first_punct', tr('achieve.bal_first_punct.name'), tr('achieve.bal_first_punct.desc'), 1,
_make_count_eval(_count_punctual_clockouts, 1), TIER_BRONZE, '🚪'),
('bal_punct_10', '칼퇴러', '정시 퇴근 10회', 10,
('bal_punct_10', tr('achieve.bal_punct_10.name'), tr('achieve.bal_punct_10.desc'), 10,
_make_count_eval(_count_punctual_clockouts, 10), TIER_SILVER, '🎉'),
('bal_punct_30', '칼퇴 챔프', '정시 퇴근 30회', 30,
('bal_punct_30', tr('achieve.bal_punct_30.name'), tr('achieve.bal_punct_30.desc'), 30,
_make_count_eval(_count_punctual_clockouts, 30), TIER_GOLD, '🏃'),
('bal_punct_100', '진정한 자유', '정시 퇴근 100회', 100,
('bal_punct_100', tr('achieve.bal_punct_100.name'), tr('achieve.bal_punct_100.desc'), 100,
_make_count_eval(_count_punctual_clockouts, 100), TIER_LEGEND, '🏖️'),
('bal_punct_300', '워라밸 마스터', '정시 퇴근 300회', 300,
('bal_punct_300', tr('achieve.bal_punct_300.name'), tr('achieve.bal_punct_300.desc'), 300,
_make_count_eval(_count_punctual_clockouts, 300), TIER_LEGEND, '🪐'),
]
# ---- 4. 연장근무 적립 ----
_OT_BANK_DEFS = [
('ot_first_30m', '첫 30분', '첫 연장 적립', 30,
('ot_first_30m', tr('achieve.ot_first_30m.name'), tr('achieve.ot_first_30m.desc'), 30,
_make_count_eval(_ot_total_earned, 30), TIER_BRONZE, '💰'),
('ot_total_60m', '1시간 적금', '누적 1시간 적립', 60,
('ot_total_60m', tr('achieve.ot_total_60m.name'), tr('achieve.ot_total_60m.desc'), 60,
_make_count_eval(_ot_total_earned, 60), TIER_BRONZE, '💵'),
('ot_total_5h', '5시간 적립', '누적 5시간', 300,
('ot_total_5h', tr('achieve.ot_total_5h.name'), tr('achieve.ot_total_5h.desc'), 300,
_make_count_eval(_ot_total_earned, 300), TIER_SILVER, '🏦'),
('ot_total_10h', '10시간 적립', '누적 10시간', 600,
('ot_total_10h', tr('achieve.ot_total_10h.name'), tr('achieve.ot_total_10h.desc'), 600,
_make_count_eval(_ot_total_earned, 600), TIER_SILVER, '💎'),
('ot_total_25h', '25시간 적립', '누적 25시간', 1500,
('ot_total_25h', tr('achieve.ot_total_25h.name'), tr('achieve.ot_total_25h.desc'), 1500,
_make_count_eval(_ot_total_earned, 1500), TIER_GOLD, '🏆'),
('ot_total_50h', '50시간 적립', '누적 50시간', 3000,
('ot_total_50h', tr('achieve.ot_total_50h.name'), tr('achieve.ot_total_50h.desc'), 3000,
_make_count_eval(_ot_total_earned, 3000), TIER_GOLD, '🎯'),
('ot_total_100h', '마라토너', '누적 100시간 (걱정 메시지)', 6000,
('ot_total_100h', tr('achieve.ot_total_100h.name'), tr('achieve.ot_total_100h.desc'), 6000,
_make_count_eval(_ot_total_earned, 6000), TIER_PLATINUM, '🏔️'),
('ot_total_200h', '워크홀릭 경고', '누적 200시간 (경고)', 12000,
('ot_total_200h', tr('achieve.ot_total_200h.name'), tr('achieve.ot_total_200h.desc'), 12000,
_make_count_eval(_ot_total_earned, 12000), TIER_PLATINUM, '🌑'),
('ot_total_300h', '위험 신호', '누적 300시간 (강한 경고)', 18000,
('ot_total_300h', tr('achieve.ot_total_300h.name'), tr('achieve.ot_total_300h.desc'), 18000,
_make_count_eval(_ot_total_earned, 18000), TIER_LEGEND, '⚠️'),
('ot_total_500h', '응급실 단골', '누적 500시간 (자조)', 30000,
('ot_total_500h', tr('achieve.ot_total_500h.name'), tr('achieve.ot_total_500h.desc'), 30000,
_make_count_eval(_ot_total_earned, 30000), TIER_LEGEND, '🚑'),
]
# ---- 5. 연장근무 사용 ----
_OT_USE_DEFS = [
('use_first', '첫 휴식', '적립 첫 사용', 1,
('use_first', tr('achieve.use_first.name'), tr('achieve.use_first.desc'), 1,
_bool_eval(lambda db: _ot_total_used(db) > 0), TIER_BRONZE, '🛌'),
('use_total_5h', '선물 사용', '누적 5시간 사용', 300,
('use_total_5h', tr('achieve.use_total_5h.name'), tr('achieve.use_total_5h.desc'), 300,
_make_count_eval(_ot_total_used, 300), TIER_SILVER, '🎁'),
('use_total_25h', '휴식의 가치', '누적 25시간 사용', 1500,
('use_total_25h', tr('achieve.use_total_25h.name'), tr('achieve.use_total_25h.desc'), 1500,
_make_count_eval(_ot_total_used, 1500), TIER_GOLD, '🛀'),
('use_total_50h', '회복 마스터', '누적 50시간 사용', 3000,
('use_total_50h', tr('achieve.use_total_50h.name'), tr('achieve.use_total_50h.desc'), 3000,
_make_count_eval(_ot_total_used, 3000), TIER_GOLD, '🏖️'),
('use_total_100h', '마사지', '누적 100시간 사용', 6000,
('use_total_100h', tr('achieve.use_total_100h.name'), tr('achieve.use_total_100h.desc'), 6000,
_make_count_eval(_ot_total_used, 6000), TIER_PLATINUM, '💆'),
]
# ---- 6. 연차 ----
_LEAVE_DEFS = [
('leave_first', '첫 연차', '첫 연차 사용', 1,
('leave_first', tr('achieve.leave_first.name'), tr('achieve.leave_first.desc'), 1,
_make_count_eval(_count_leave_records, 1), TIER_BRONZE, '🌴'),
('leave_half', '첫 반차', '0.5일 연차 사용', 1,
('leave_half', tr('achieve.leave_half.name'), tr('achieve.leave_half.desc'), 1,
_bool_eval(lambda db: _has_leave_with_days(db, 0.5)), TIER_BRONZE, '🍃'),
('leave_quarter', '시간 연차', '0.25일 연차 사용', 1,
('leave_quarter', tr('achieve.leave_quarter.name'), tr('achieve.leave_quarter.desc'), 1,
_bool_eval(lambda db: _has_leave_with_days(db, 0.25)), TIER_BRONZE, '⏱️'),
('leave_streak_3', '미니 휴가', '연속 3일 연차', 3,
('leave_streak_3', tr('achieve.leave_streak_3.name'), tr('achieve.leave_streak_3.desc'), 3,
_make_count_eval(_consecutive_leave_days, 3), TIER_SILVER, '🏝️'),
('leave_streak_5', '본격 휴가', '연속 5일 연차', 5,
('leave_streak_5', tr('achieve.leave_streak_5.name'), tr('achieve.leave_streak_5.desc'), 5,
_make_count_eval(_consecutive_leave_days, 5), TIER_GOLD, '🌅'),
('leave_streak_7', '장거리 휴가', '연속 7일 이상 연차', 7,
('leave_streak_7', tr('achieve.leave_streak_7.name'), tr('achieve.leave_streak_7.desc'), 7,
_make_count_eval(_consecutive_leave_days, 7), TIER_PLATINUM, '🛬'),
('leave_total_10', '연차 10회', '연차 기록 10건', 10,
('leave_total_10', tr('achieve.leave_total_10.name'), tr('achieve.leave_total_10.desc'), 10,
_make_count_eval(_count_leave_records, 10), TIER_SILVER, '🌊'),
('leave_sick', '병가', 'sick 타입 연차 사용', 1,
('leave_sick', tr('achieve.leave_sick.name'), tr('achieve.leave_sick.desc'), 1,
_make_count_eval(lambda db: _count_leave_records(db, 'sick'), 1),
TIER_BRONZE, '🏥'),
]
@@ -602,22 +603,22 @@ _LEAVE_DEFS = [
# ---- 7. 식사 (점심/저녁) ----
_MEAL_DEFS = [
('meal_lunch_first', '첫 점심 등록', '점심 첫 토글', 1,
('meal_lunch_first', tr('achieve.meal_lunch_first.name'), tr('achieve.meal_lunch_first.desc'), 1,
_make_count_eval(_count_lunch_registrations, 1), TIER_BRONZE, '🍱'),
('meal_lunch_30', '점심 마스터', '점심 등록 30회', 30,
('meal_lunch_30', tr('achieve.meal_lunch_30.name'), tr('achieve.meal_lunch_30.desc'), 30,
_make_count_eval(_count_lunch_registrations, 30), TIER_SILVER, '🥢'),
('meal_lunch_100', '점심 챔프', '점심 등록 100회', 100,
('meal_lunch_100', tr('achieve.meal_lunch_100.name'), tr('achieve.meal_lunch_100.desc'), 100,
_make_count_eval(_count_lunch_registrations, 100), TIER_GOLD, '🍜'),
('meal_dinner_first', '첫 저녁 등록', '저녁 첫 토글', 1,
('meal_dinner_first', tr('achieve.meal_dinner_first.name'), tr('achieve.meal_dinner_first.desc'), 1,
_make_count_eval(_count_dinner_registrations, 1), TIER_BRONZE, '🍽️'),
('meal_dinner_10', '저녁 단골', '저녁 등록 10회 (경고)', 10,
('meal_dinner_10', tr('achieve.meal_dinner_10.name'), tr('achieve.meal_dinner_10.desc'), 10,
_make_count_eval(_count_dinner_registrations, 10), TIER_SILVER, '🍛'),
('meal_dinner_30', '야식 단골', '저녁 등록 30회 (경고)', 30,
('meal_dinner_30', tr('achieve.meal_dinner_30.name'), tr('achieve.meal_dinner_30.desc'), 30,
_make_count_eval(_count_dinner_registrations, 30), TIER_GOLD, '🌃'),
('meal_lunch_actual', '실측 점심', '실제 점심 시각 입력', 1,
('meal_lunch_actual', tr('achieve.meal_lunch_actual.name'), tr('achieve.meal_lunch_actual.desc'), 1,
_make_count_eval(lambda db: _count_break_records_type(db, 'lunch'), 1),
TIER_BRONZE, '⏱️'),
('meal_dinner_actual', '실측 저녁', '실제 저녁 시각 입력', 1,
('meal_dinner_actual', tr('achieve.meal_dinner_actual.name'), tr('achieve.meal_dinner_actual.desc'), 1,
_make_count_eval(lambda db: _count_break_records_type(db, 'dinner'), 1),
TIER_BRONZE, '⏰'),
]
@@ -625,13 +626,13 @@ _MEAL_DEFS = [
# ---- 8. 외출 ----
_BREAK_DEFS = [
('break_first', '첫 외출', '첫 외출 시작', 1,
('break_first', tr('achieve.break_first.name'), tr('achieve.break_first.desc'), 1,
_make_count_eval(lambda db: _count_break_records_type(db, 'break'), 1),
TIER_BRONZE, '🚶'),
('break_10', '외출 챔프', '외출 10회', 10,
('break_10', tr('achieve.break_10.name'), tr('achieve.break_10.desc'), 10,
_make_count_eval(lambda db: _count_break_records_type(db, 'break'), 10),
TIER_SILVER, '🚪'),
('break_50', '산책러', '외출 50회', 50,
('break_50', tr('achieve.break_50.name'), tr('achieve.break_50.desc'), 50,
_make_count_eval(lambda db: _count_break_records_type(db, 'break'), 50),
TIER_GOLD, '🚶‍♂️'),
]
@@ -639,39 +640,39 @@ _BREAK_DEFS = [
# ---- 9. 시간대별 ----
_TIME_SLOT_DEFS = [
('slot_in_06', '06시대 출근', '06:00-06:59 출근 1회', 1,
('slot_in_06', tr('achieve.slot_in_06.name'), tr('achieve.slot_in_06.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range(db, 6, 7), 1),
TIER_BRONZE, '🌅'),
('slot_in_07', '07시대 출근', '07:00-07:59 출근 1회', 1,
('slot_in_07', tr('achieve.slot_in_07.name'), tr('achieve.slot_in_07.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range(db, 7, 8), 1),
TIER_BRONZE, '🌄'),
('slot_in_08', '08시대 출근', '08:00-08:59 출근 1회', 1,
('slot_in_08', tr('achieve.slot_in_08.name'), tr('achieve.slot_in_08.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range(db, 8, 9), 1),
TIER_BRONZE, '☀️'),
('slot_in_10', '10시대 출근', '10시대 출근 (지각/유연근무)', 1,
('slot_in_10', tr('achieve.slot_in_10.name'), tr('achieve.slot_in_10.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range(db, 10, 11), 1),
TIER_BRONZE, '🕙'),
('slot_in_11', '11시대 출근', '11시대 출근 (자조)', 1,
('slot_in_11', tr('achieve.slot_in_11.name'), tr('achieve.slot_in_11.desc'), 1,
_make_count_eval(lambda db: _count_clock_in_in_range(db, 11, 12), 1),
TIER_SILVER, '🕦'),
('slot_out_19', '19시대 퇴근', '19시대 퇴근 10회 (경고)', 10,
('slot_out_19', tr('achieve.slot_out_19.name'), tr('achieve.slot_out_19.desc'), 10,
_make_count_eval(lambda db: _count_clockouts_in_hour(db, 19), 10),
TIER_SILVER, '🌆'),
('slot_out_20', '20시대 퇴근', '20시대 퇴근 10회 (경고)', 10,
('slot_out_20', tr('achieve.slot_out_20.name'), tr('achieve.slot_out_20.desc'), 10,
_make_count_eval(lambda db: _count_clockouts_in_hour(db, 20), 10),
TIER_GOLD, '🌌'),
('slot_out_21', '21시대 퇴근', '21시대 퇴근 5회 (경고)', 5,
('slot_out_21', tr('achieve.slot_out_21.name'), tr('achieve.slot_out_21.desc'), 5,
_make_count_eval(lambda db: _count_clockouts_in_hour(db, 21), 5),
TIER_GOLD, '🌑'),
('slot_out_22', '22시대 퇴근', '22시대 퇴근 1회 (경고)', 1,
('slot_out_22', tr('achieve.slot_out_22.name'), tr('achieve.slot_out_22.desc'), 1,
_make_count_eval(lambda db: _count_clockouts_in_hour(db, 22), 1),
TIER_PLATINUM, '🦉'),
('slot_out_23', '23시대 퇴근', '23시대 퇴근 1회 (경고)', 1,
('slot_out_23', tr('achieve.slot_out_23.name'), tr('achieve.slot_out_23.desc'), 1,
_make_count_eval(lambda db: _count_clockouts_in_hour(db, 23), 1),
TIER_PLATINUM, '🦇'),
('slot_midnight', '자정 퇴근', '자정 이후 퇴근 (경고)', 1,
('slot_midnight', tr('achieve.slot_midnight.name'), tr('achieve.slot_midnight.desc'), 1,
_make_count_eval(_count_clock_out_after_midnight, 1), TIER_LEGEND, '🌚'),
('slot_midnight_3', '올빼미 트리오', '자정 이후 퇴근 3회 (경고)', 3,
('slot_midnight_3', tr('achieve.slot_midnight_3.name'), tr('achieve.slot_midnight_3.desc'), 3,
_make_count_eval(_count_clock_out_after_midnight, 3), TIER_LEGEND, '🌌'),
]
@@ -692,50 +693,50 @@ def _count_clockouts_in_hour(db, hour: int) -> int:
# ---- 10. 공휴일·주말 ----
_SPECIAL_DAY_DEFS = [
('weekend_1', '주말 출근 1회', '토/일 출근 1회', 1,
('weekend_1', tr('achieve.weekend_1.name'), tr('achieve.weekend_1.desc'), 1,
_make_count_eval(_count_weekend_clockins, 1), TIER_SILVER, '🌃'),
('weekend_5', '주말 워커', '주말 출근 5회 (경고)', 5,
('weekend_5', tr('achieve.weekend_5.name'), tr('achieve.weekend_5.desc'), 5,
_make_count_eval(_count_weekend_clockins, 5), TIER_GOLD, '🌑'),
('weekend_20', '진짜 워크홀릭', '주말 출근 20회 (강한 자조)', 20,
('weekend_20', tr('achieve.weekend_20.name'), tr('achieve.weekend_20.desc'), 20,
_make_count_eval(_count_weekend_clockins, 20), TIER_PLATINUM, '💀'),
('holiday_1', '공휴일 출근', '한국 공휴일 출근 1회', 1,
('holiday_1', tr('achieve.holiday_1.name'), tr('achieve.holiday_1.desc'), 1,
_make_count_eval(_count_holiday_clockins, 1), TIER_GOLD, '📆'),
('holiday_5', '공휴일 워커홀릭', '한국 공휴일 출근 5회 (경고)', 5,
('holiday_5', tr('achieve.holiday_5.name'), tr('achieve.holiday_5.desc'), 5,
_make_count_eval(_count_holiday_clockins, 5), TIER_LEGEND, '⚠️'),
('day_christmas', '크리스마스 출근', '12/25 출근 (자조)', 1,
('day_christmas', tr('achieve.day_christmas.name'), tr('achieve.day_christmas.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '12-25')), TIER_GOLD, '🎄'),
('day_newyear', '신정 출근', '1/1 출근 (자조)', 1,
('day_newyear', tr('achieve.day_newyear.name'), tr('achieve.day_newyear.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '01-01')), TIER_GOLD, '🎊'),
('day_liberation', '광복절 출근', '8/15 출근', 1,
('day_liberation', tr('achieve.day_liberation.name'), tr('achieve.day_liberation.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '08-15')), TIER_SILVER, '🎆'),
('day_children', '어린이날 출근', '5/5 출근 (자조)', 1,
('day_children', tr('achieve.day_children.name'), tr('achieve.day_children.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '05-05')), TIER_GOLD, '🎀'),
('day_hangul', '한글날 출근', '10/9 출근', 1,
('day_hangul', tr('achieve.day_hangul.name'), tr('achieve.day_hangul.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '10-09')), TIER_SILVER, '🎤'),
('day_valentine', '발렌타인데이 출근', '2/14 출근', 1,
('day_valentine', tr('achieve.day_valentine.name'), tr('achieve.day_valentine.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '02-14')), TIER_BRONZE, '💝'),
('day_white', '화이트데이 출근', '3/14 출근', 1,
('day_white', tr('achieve.day_white.name'), tr('achieve.day_white.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '03-14')), TIER_BRONZE, '🌹'),
('day_pepero', '빼빼로데이', '11/11 출근', 1,
('day_pepero', tr('achieve.day_pepero.name'), tr('achieve.day_pepero.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '11-11')), TIER_SILVER, '🍫'),
('day_halloween', '핼러윈 출근', '10/31 출근', 1,
('day_halloween', tr('achieve.day_halloween.name'), tr('achieve.day_halloween.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '10-31')), TIER_BRONZE, '🎃'),
('day_aprilfools', '만우절 출근', '4/1 출근', 1,
('day_aprilfools', tr('achieve.day_aprilfools.name'), tr('achieve.day_aprilfools.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '04-01')), TIER_BRONZE, '🃏'),
('day_77', '칠월칠석', '7/7 출근', 1,
('day_77', tr('achieve.day_77.name'), tr('achieve.day_77.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '07-07')), TIER_SILVER, '🎋'),
('day_dongji', '동지 출근', '12/22 출근', 1,
('day_dongji', tr('achieve.day_dongji.name'), tr('achieve.day_dongji.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '12-22')), TIER_BRONZE, '🎇'),
('day_parents', '어버이날 정시 퇴근', '5/8 정시 퇴근', 1,
('day_parents', tr('achieve.day_parents.name'), tr('achieve.day_parents.desc'), 1,
_bool_eval(lambda db: _has_punctual_clockout_on(db, '05-08')),
TIER_SILVER, '🪅'),
('day_teacher', '스승의 날 정시 퇴근', '5/15 정시 퇴근', 1,
('day_teacher', tr('achieve.day_teacher.name'), tr('achieve.day_teacher.desc'), 1,
_bool_eval(lambda db: _has_punctual_clockout_on(db, '05-15')),
TIER_BRONZE, '🎂'),
('day_xmas_eve', '크리스마스이브 정시 퇴근', '12/24 정시 퇴근', 1,
('day_xmas_eve', tr('achieve.day_xmas_eve.name'), tr('achieve.day_xmas_eve.desc'), 1,
_bool_eval(lambda db: _has_punctual_clockout_on(db, '12-24')),
TIER_SILVER, '🎁'),
('day_earth', '지구의 날', '4/22 출근 (시크릿)', 1,
('day_earth', tr('achieve.day_earth.name'), tr('achieve.day_earth.desc'), 1,
_bool_eval(lambda db: _has_clockin_on(db, '04-22')), TIER_GOLD, '🌏'),
]
@@ -760,79 +761,79 @@ def _make_month_first_eval(month: int):
_SEASON_DEFS = [
('season_jan', '1월 정착', '1월 한 달 출근', 1,
('season_jan', tr('achieve.season_jan.name'), tr('achieve.season_jan.desc'), 1,
_make_month_first_eval(1), TIER_BRONZE, '⛄'),
('season_feb', '2월 정착', '2월 영업일 모두 출근', 1,
('season_feb', tr('achieve.season_feb.name'), tr('achieve.season_feb.desc'), 1,
_make_month_full_attendance_eval(2), TIER_SILVER, '🌨️'),
('season_mar', '봄을 맞이', '3월 첫 출근', 1,
('season_mar', tr('achieve.season_mar.name'), tr('achieve.season_mar.desc'), 1,
_make_month_first_eval(3), TIER_BRONZE, '🌸'),
('season_apr', '4월 정착', '4월 한 달 출근', 1,
('season_apr', tr('achieve.season_apr.name'), tr('achieve.season_apr.desc'), 1,
_make_month_full_attendance_eval(4), TIER_BRONZE, '🌷'),
('season_may', '5월 정착', '5월 영업일 모두 출근', 1,
('season_may', tr('achieve.season_may.name'), tr('achieve.season_may.desc'), 1,
_make_month_full_attendance_eval(5), TIER_SILVER, '🌺'),
('season_jun', '여름의 시작', '6월 첫 출근', 1,
('season_jun', tr('achieve.season_jun.name'), tr('achieve.season_jun.desc'), 1,
_make_month_first_eval(6), TIER_BRONZE, '☀️'),
('season_jul', '7월 정착', '7월 한 달 출근', 1,
('season_jul', tr('achieve.season_jul.name'), tr('achieve.season_jul.desc'), 1,
_make_month_full_attendance_eval(7), TIER_BRONZE, '🌻'),
('season_aug', '8월 정착', '8월 영업일 모두 출근', 1,
('season_aug', tr('achieve.season_aug.name'), tr('achieve.season_aug.desc'), 1,
_make_month_full_attendance_eval(8), TIER_SILVER, '🍦'),
('season_sep', '가을의 시작', '9월 첫 출근', 1,
('season_sep', tr('achieve.season_sep.name'), tr('achieve.season_sep.desc'), 1,
_make_month_first_eval(9), TIER_BRONZE, '🍂'),
('season_oct', '10월 정착', '10월 한 달 출근', 1,
('season_oct', tr('achieve.season_oct.name'), tr('achieve.season_oct.desc'), 1,
_make_month_full_attendance_eval(10), TIER_BRONZE, '🌾'),
('season_nov', '11월 단풍', '11월 영업일 모두 출근', 1,
('season_nov', tr('achieve.season_nov.name'), tr('achieve.season_nov.desc'), 1,
_make_month_full_attendance_eval(11), TIER_SILVER, '🍁'),
('season_dec', '겨울의 시작', '12월 첫 출근', 1,
('season_dec', tr('achieve.season_dec.name'), tr('achieve.season_dec.desc'), 1,
_make_month_first_eval(12), TIER_BRONZE, '❄️'),
]
# ---- 12. 앱 사용 마일스톤 ----
_MILESTONE_DEFS = [
('mile_first', 'Hello, World!', '앱 첫 실행', 1,
('mile_first', tr('achieve.mile_first.name'), tr('achieve.mile_first.desc'), 1,
_bool_eval(lambda db: _count_work_records(db) >= 1 or _days_since_first_work(db) >= 0),
TIER_BRONZE, '👋'),
('mile_7days', '일주일 사용', '7일 사용', 7,
('mile_7days', tr('achieve.mile_7days.name'), tr('achieve.mile_7days.desc'), 7,
_make_count_eval(_days_since_first_work, 7), TIER_BRONZE, '🗓️'),
('mile_30days', '한 달 사용', '30일 사용', 30,
('mile_30days', tr('achieve.mile_30days.name'), tr('achieve.mile_30days.desc'), 30,
_make_count_eval(_days_since_first_work, 30), TIER_SILVER, '📚'),
('mile_365days', '1주년', '365일 사용', 365,
('mile_365days', tr('achieve.mile_365days.name'), tr('achieve.mile_365days.desc'), 365,
_make_count_eval(_days_since_first_work, 365), TIER_PLATINUM, '💎'),
('mile_730days', '2주년', '730일 사용', 730,
('mile_730days', tr('achieve.mile_730days.name'), tr('achieve.mile_730days.desc'), 730,
_make_count_eval(_days_since_first_work, 730), TIER_LEGEND, '🌟'),
('mile_1095days', '3주년', '3년 사용', 1095,
('mile_1095days', tr('achieve.mile_1095days.name'), tr('achieve.mile_1095days.desc'), 1095,
_make_count_eval(_days_since_first_work, 1095), TIER_LEGEND, '🎖️'),
('mile_5years', '5년 사용자', '5년 사용', 1825,
('mile_5years', tr('achieve.mile_5years.name'), tr('achieve.mile_5years.desc'), 1825,
_make_count_eval(_days_since_first_work, 1825), TIER_LEGEND, '🏆'),
('mile_10years', '10년 사용자', '10년 사용', 3650,
('mile_10years', tr('achieve.mile_10years.name'), tr('achieve.mile_10years.desc'), 3650,
_make_count_eval(_days_since_first_work, 3650), TIER_LEGEND, '🎖️'),
]
# ---- 13. 통계·분석 (view counter 기반) ----
_STATS_DEFS = [
('stat_weekly_10', '주간 통계러', '주간 탭 10회 조회', 10,
('stat_weekly_10', tr('achieve.stat_weekly_10.name'), tr('achieve.stat_weekly_10.desc'), 10,
_make_count_eval(lambda db: _setting_int(db, 'stat_weekly_view_count'), 10),
TIER_BRONZE, '📊'),
('stat_monthly_10', '월간 통계러', '월간 탭 10회', 10,
('stat_monthly_10', tr('achieve.stat_monthly_10.name'), tr('achieve.stat_monthly_10.desc'), 10,
_make_count_eval(lambda db: _setting_int(db, 'stat_monthly_view_count'), 10),
TIER_BRONZE, '📈'),
('stat_pattern_10', '패턴 분석가', '패턴 탭 10회', 10,
('stat_pattern_10', tr('achieve.stat_pattern_10.name'), tr('achieve.stat_pattern_10.desc'), 10,
_make_count_eval(lambda db: _setting_int(db, 'stat_pattern_view_count'), 10),
TIER_SILVER, '🔍'),
('stat_calendar_30', '캘린더 챔프', '캘린더 30회 조회', 30,
('stat_calendar_30', tr('achieve.stat_calendar_30.name'), tr('achieve.stat_calendar_30.desc'), 30,
_make_count_eval(lambda db: _setting_int(db, 'calendar_view_count'), 30),
TIER_SILVER, '📅'),
('stat_report_first', '일일 보고서 첫 생성', '일일 보고 1회', 1,
('stat_report_first', tr('achieve.stat_report_first.name'), tr('achieve.stat_report_first.desc'), 1,
_make_count_eval(lambda db: _setting_int(db, 'daily_report_count'), 1),
TIER_BRONZE, '📋'),
('stat_report_30', '보고서 챔프', '일일 보고 30회', 30,
('stat_report_30', tr('achieve.stat_report_30.name'), tr('achieve.stat_report_30.desc'), 30,
_make_count_eval(lambda db: _setting_int(db, 'daily_report_count'), 30),
TIER_SILVER, '📰'),
('stat_chart_hover', '차트 호버 발견', '차트 hover 첫 발견', 1,
('stat_chart_hover', tr('achieve.stat_chart_hover.name'), tr('achieve.stat_chart_hover.desc'), 1,
_bool_eval(lambda db: db.get_setting('chart_hover_discovered', 'false').lower() == 'true'),
TIER_BRONZE, '🎨'),
('stat_achievements_open', '도전과제 박물관', '도전과제 뷰 50회', 50,
('stat_achievements_open', tr('achieve.stat_achievements_open.name'), tr('achieve.stat_achievements_open.desc'), 50,
_make_count_eval(lambda db: _setting_int(db, 'achievements_view_count'), 50),
TIER_BRONZE, '🦄'),
]
@@ -957,23 +958,23 @@ def _has_500_anniv_clockin(db) -> bool:
_SECRET_DEFS = [
('secret_palindrome', '회문 시각', '출근 시각이 회문', 1,
('secret_palindrome', tr('achieve.secret_palindrome.name'), tr('achieve.secret_palindrome.desc'), 1,
_bool_eval(_has_clock_in_palindrome), TIER_GOLD, '🪞'),
('secret_jackpot', '잭팟 시각', '출근 시각 모든 자릿수 동일', 1,
('secret_jackpot', tr('achieve.secret_jackpot.name'), tr('achieve.secret_jackpot.desc'), 1,
_bool_eval(_has_clock_in_jackpot), TIER_PLATINUM, '🎰'),
('secret_fri13', '13일 금요일', '13일 금요일 출근', 1,
('secret_fri13', tr('achieve.secret_fri13.name'), tr('achieve.secret_fri13.desc'), 1,
_bool_eval(_has_friday_13th_clockin), TIER_GOLD, '🌑'),
('secret_777', '7-7-7', '7월 7일 7시 7분 출근', 1,
('secret_777', tr('achieve.secret_777.name'), tr('achieve.secret_777.desc'), 1,
_bool_eval(_has_777), TIER_LEGEND, '🔮'),
('secret_exact_8h', '정확 8시간', '정확히 8h 0m 근무', 1,
('secret_exact_8h', tr('achieve.secret_exact_8h.name'), tr('achieve.secret_exact_8h.desc'), 1,
_bool_eval(_has_exact_8h), TIER_PLATINUM, '🎯'),
('secret_pi_day', '파이 데이', '3/14 01:59 출근', 1,
('secret_pi_day', tr('achieve.secret_pi_day.name'), tr('achieve.secret_pi_day.desc'), 1,
_bool_eval(_has_pi_day), TIER_LEGEND, '🥧'),
('secret_fibonacci', '피보나치', '출근 분이 피보나치 수', 1,
('secret_fibonacci', tr('achieve.secret_fibonacci.name'), tr('achieve.secret_fibonacci.desc'), 1,
_bool_eval(_has_fibonacci_minute), TIER_SILVER, '🔢'),
('secret_double_six', '더블 식스', '6/6 18:06 출근', 1,
('secret_double_six', tr('achieve.secret_double_six.name'), tr('achieve.secret_double_six.desc'), 1,
_bool_eval(_has_double_six), TIER_LEGEND, '🎲'),
('secret_anniversary', '마법사', '가입 후 정확히 365일 후 출근', 1,
('secret_anniversary', tr('achieve.secret_anniversary.name'), tr('achieve.secret_anniversary.desc'), 1,
_bool_eval(_has_500_anniv_clockin), TIER_LEGEND, '🧙'),
]
@@ -984,30 +985,30 @@ def _setting_changed_from_default(db, key: str, default_value: str) -> bool:
_SETTINGS_DEFS = [
('set_dark', '다크 사이드', '다크 테마 1회 사용', 1,
('set_dark', tr('achieve.set_dark.name'), tr('achieve.set_dark.desc'), 1,
_bool_eval(lambda db: _setting_changed_from_default(db, 'theme', 'light')),
TIER_BRONZE, '🌗'),
('set_lang', '이중언어', '언어 변경 (en 사용)', 1,
('set_lang', tr('achieve.set_lang.name'), tr('achieve.set_lang.desc'), 1,
_bool_eval(lambda db: db.get_setting('language', 'ko') == 'en'),
TIER_BRONZE, '🌐'),
('set_a11y', '접근성 활용', '글꼴 크기≠100% 또는 고대비 ON', 1,
('set_a11y', tr('achieve.set_a11y.name'), tr('achieve.set_a11y.desc'), 1,
_bool_eval(lambda db: db.get_setting('font_scale', '1.0') != '1.0'
or db.get_setting('high_contrast', 'false').lower() == 'true'),
TIER_BRONZE, '♿'),
('set_overtime_unit', '단위 변경', 'overtime_unit 변경', 1,
('set_overtime_unit', tr('achieve.set_overtime_unit.name'), tr('achieve.set_overtime_unit.desc'), 1,
_bool_eval(lambda db: db.get_setting('overtime_unit', '30') != '30'),
TIER_BRONZE, '⏱️'),
('set_goal_full', '목표 마스터', '월 연장+일평균 둘 다 설정', 1,
('set_goal_full', tr('achieve.set_goal_full.name'), tr('achieve.set_goal_full.desc'), 1,
_bool_eval(lambda db: _setting_int(db, 'goal_overtime_max_monthly') > 0
and float(db.get_setting('goal_avg_hours_daily', '0') or 0) > 0),
TIER_SILVER, '🎯'),
('set_discord_full', '풀 셋업', 'Discord URL + 모든 알림 ON', 1,
('set_discord_full', tr('achieve.set_discord_full.name'), tr('achieve.set_discord_full.desc'), 1,
_bool_eval(lambda db: bool(db.get_setting('discord_webhook_url', '') or '')
and all(db.get_setting(k, 'true').lower() == 'true' for k in
('notification_clock_out', 'notification_lunch',
'notification_overtime', 'notification_health'))),
TIER_SILVER, '🔔'),
('set_cloud', '클라우드 동기화', 'DB 경로 변경', 1,
('set_cloud', tr('achieve.set_cloud.name'), tr('achieve.set_cloud.desc'), 1,
_bool_eval(lambda db: bool(db.get_setting('db_path_override', '') or '')),
TIER_SILVER, '☁️'),
]
@@ -1032,21 +1033,21 @@ def _earned_secret_count(db) -> int:
_META_DEFS = [
('meta_first', '첫 도전과제', '첫 도전과제 획득', 1,
('meta_first', tr('achieve.meta_first.name'), tr('achieve.meta_first.desc'), 1,
_make_count_eval(_earned_count, 1), TIER_BRONZE, '🏆'),
('meta_10', '10개 달성', '10개 보유', 10,
('meta_10', tr('achieve.meta_10.name'), tr('achieve.meta_10.desc'), 10,
_make_count_eval(_earned_count, 10), TIER_BRONZE, '🎖️'),
('meta_25', '25개 달성', '25개 보유', 25,
('meta_25', tr('achieve.meta_25.name'), tr('achieve.meta_25.desc'), 25,
_make_count_eval(_earned_count, 25), TIER_SILVER, '🥈'),
('meta_50', '50개 달성', '50개 보유', 50,
('meta_50', tr('achieve.meta_50.name'), tr('achieve.meta_50.desc'), 50,
_make_count_eval(_earned_count, 50), TIER_GOLD, '🥇'),
('meta_75', '75개 달성', '75개 보유', 75,
('meta_75', tr('achieve.meta_75.name'), tr('achieve.meta_75.desc'), 75,
_make_count_eval(_earned_count, 75), TIER_PLATINUM, '💎'),
('meta_100', '100개 달성', '100개 보유', 100,
('meta_100', tr('achieve.meta_100.name'), tr('achieve.meta_100.desc'), 100,
_make_count_eval(_earned_count, 100), TIER_LEGEND, '🌟'),
('meta_secret_1', '시크릿 발견', '첫 시크릿 발견', 1,
('meta_secret_1', tr('achieve.meta_secret_1.name'), tr('achieve.meta_secret_1.desc'), 1,
_make_count_eval(_earned_secret_count, 1), TIER_SILVER, '🔍'),
('meta_secret_5', '시크릿 헌터', '시크릿 5개 발견', 5,
('meta_secret_5', tr('achieve.meta_secret_5.name'), tr('achieve.meta_secret_5.desc'), 5,
_make_count_eval(_earned_secret_count, 5), TIER_GOLD, '🌑'),
]
+28 -18
View File
@@ -12,6 +12,7 @@ from core.settings_keys import (
WORK_HOURS, WORK_MINUTES, ANNUAL_LEAVE_TOTAL, ANNUAL_LEAVE_DAYS,
INITIAL_OVERTIME_MINUTES, INITIAL_LEAVE_USED_HOURS,
)
from utils.debug_log import dlog
class Database:
@@ -53,8 +54,8 @@ class Database:
finally:
try:
conn.close()
except Exception:
pass
except Exception as e:
dlog(f"connection close failed: {e}")
def _enable_concurrency(self):
"""WAL 모드 활성화 — 동시 읽기 + 쓰기 가능, 클라우드 동기화 친화."""
@@ -118,7 +119,8 @@ class Database:
sentinel = self.get_setting('holidays_synced_date', '')
if sentinel == today:
return
except Exception:
except Exception as e:
dlog(f"holiday sync precheck failed: {e}")
return
cur_year = _dt.now().year
@@ -131,8 +133,8 @@ class Database:
added = db.add_korean_holidays_auto(cur_year, include_next_year=True)
if added >= 0:
db.set_setting('holidays_synced_date', today)
except Exception:
pass
except Exception as e:
dlog(f"holiday sync worker failed: {e}")
t = threading.Thread(target=_worker, daemon=True, name='holiday-sync')
t.start()
@@ -1070,7 +1072,8 @@ class Database:
target = _dt.strptime(date_str, '%Y-%m-%d').date()
recs = self.get_recurring_leaves(active_on=date_str)
recurring_days = sum(o.days for o in expand_for_date(recs, target))
except Exception:
except Exception as e:
dlog(f"recurring leave expansion failed: {e}")
recurring_days = 0.0
return concrete_days + recurring_days
@@ -1356,17 +1359,21 @@ class Database:
for row in rows:
key = row['key']
value = row['value']
# 타입 변환
if value.lower() in ['true', 'false']:
settings[key] = value.lower() == 'true'
else:
# 타입 변환 (bool 우선, 숫자, 문자열 순)
lower = (value or '').lower()
if lower in ('1', 'true', 'yes', 'on'):
settings[key] = True
continue
if lower in ('0', 'false', 'no', 'off', ''):
settings[key] = False
continue
try:
settings[key] = int(value)
except ValueError:
try:
settings[key] = int(value)
settings[key] = float(value)
except ValueError:
try:
settings[key] = float(value)
except ValueError:
settings[key] = value
settings[key] = value
return settings
def save_settings(self, settings: Dict):
@@ -1388,7 +1395,8 @@ class Database:
pass
elif 'work_hours' in synced and 'work_minutes' not in synced:
try:
synced['work_minutes'] = int(round(float(synced['work_hours']) * 60))
# 은행 반올림 회피: 명시적 반올림
synced['work_minutes'] = int(float(synced['work_hours']) * 60 + 0.5)
except (ValueError, TypeError):
pass
@@ -1674,7 +1682,8 @@ class Database:
''', (date, leave_type, days, memo))
conn.commit()
# 잔여 개수 차감 (별도 트랜잭션 — set_leave_balance 내부 commit)
self.set_leave_balance(current_balance - days)
# get_leave_balance()가 leave_records를 실시간으로 계산하므로
# 별도로 leave_balance 설정값을 갱신할 필요가 없음.
# ===== 공휴일 관련 메서드 =====
@@ -1838,7 +1847,8 @@ class Database:
try:
import holidays as _holidays
kr = _holidays.country_holidays('KR', years=y)
except Exception:
except Exception as e:
dlog(f"holidays package fallback failed for {y}: {e}")
continue # 둘 다 실패면 해당 연도만 스킵
for d, name in kr.items():
date_str = d.isoformat()
+1830 -4
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -15,6 +15,8 @@ from dataclasses import dataclass
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional
from core.i18n import tr
_WEEKDAY_MAP = {
'mon': 0, 'monday': 0,
@@ -25,6 +27,7 @@ _WEEKDAY_MAP = {
'sat': 5, 'saturday': 5,
'sun': 6, 'sunday': 6,
}
_WEEKDAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
@dataclass
@@ -135,19 +138,16 @@ def _parse_date(s: Optional[str]) -> Optional[date]:
return None
_KO_WEEKDAY_NAMES = ['월', '화', '수', '목', '금', '토', '일']
def describe_pattern(pattern: str) -> str:
"""사용자에게 보여줄 패턴 설명. ko."""
"""사용자에게 보여줄 패턴 설명."""
parsed = _parse_pattern(pattern)
if parsed is None:
return pattern
kind, info = parsed
if kind in ('weekly', 'biweekly'):
names = [_KO_WEEKDAY_NAMES[w] for w in info]
prefix = '매주' if kind == 'weekly' else '격주'
return f"{prefix} {','.join(names)}요일"
names = [tr(f'label.weekday_{_WEEKDAY_KEYS[w]}') for w in info]
prefix = tr('recurring.weekly') if kind == 'weekly' else tr('recurring.biweekly')
return tr('recurring.pattern_weekly', prefix=prefix, weekdays=','.join(names))
if kind == 'monthly':
return f"매월 {info}일"
return tr('recurring.pattern_monthly', day=info)
return pattern
+2 -1
View File
@@ -21,7 +21,8 @@ class TimeCalculator:
if work_minutes is not None:
self.work_minutes = int(work_minutes)
elif work_hours is not None:
self.work_minutes = int(round(float(work_hours) * 60))
# 은행 반올림(banker's rounding) 회피: 6.5시간 → 390분이 되도록 명시적 반올림
self.work_minutes = int(float(work_hours) * 60 + 0.5)
else:
self.work_minutes = 480