i18n: complete UI/achievement keying, fix help_view tr() and i18n syntax errors
This commit is contained in:
+154
-153
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user