diff --git a/saju-lab/app/calculator/lucky.py b/saju-lab/app/calculator/lucky.py new file mode 100644 index 0000000..e8d4867 --- /dev/null +++ b/saju-lab/app/calculator/lucky.py @@ -0,0 +1,84 @@ +"""오늘의 럭키 컬러/숫자/방향 + 행운/위험 알림.""" +from datetime import date as _date +from typing import Dict, List + +import sxtwl + +from .constants import HEAVENLY_STEMS, EARTHLY_BRANCHES, FIVE_ELEMENTS, KE_CYCLE + + +LUCKY_COLOR_BY_ELEMENT: Dict[str, List[str]] = { + "木": ["청록", "녹색"], + "火": ["빨강", "주황"], + "土": ["황색", "베이지"], + "金": ["흰색", "은색"], + "水": ["파랑", "검정"], +} + +LUCKY_DIRECTION_BY_ELEMENT: Dict[str, str] = { + "木": "동쪽", + "火": "남쪽", + "土": "중앙", + "金": "서쪽", + "水": "북쪽", +} + + +def _day_stem_branch_idx(target: _date) -> tuple[int, int]: + """양력 → sxtwl 일주 천간/지지 인덱스.""" + day_obj = sxtwl.fromSolar(target.year, target.month, target.day) + try: + gz = day_obj.getDayGZ() + return gz.tg, gz.dz + except AttributeError: + return day_obj.getDayTG(), day_obj.getDayDZ() + + +def calculate_lucky(saju: dict, analysis: dict, target_date: _date) -> dict: + """오늘의 럭키 정보.""" + yongshin = analysis["yong_shin"]["yong_shin"] + color = LUCKY_COLOR_BY_ELEMENT.get(yongshin, ["흰색"]) + direction = LUCKY_DIRECTION_BY_ELEMENT.get(yongshin, "중앙") + + # 럭키 숫자: 오늘 일진 천간+지지 인덱스 합 % 9 + 1 → 1~9 + day_stem_idx, day_branch_idx = _day_stem_branch_idx(target_date) + number = (day_stem_idx + day_branch_idx) % 9 + 1 + + # 행운/위험 + good_signs: List[str] = [] + warnings: List[str] = [] + + today_stem = HEAVENLY_STEMS[day_stem_idx] + today_branch = EARTHLY_BRANCHES[day_branch_idx] + day_master = saju["day_stem"] + + day_elem = FIVE_ELEMENTS[day_master] + today_elem = FIVE_ELEMENTS[today_stem] + if KE_CYCLE.get(day_elem) == today_elem: + good_signs.append("재물 기회가 다가옵니다") + if KE_CYCLE.get(today_elem) == day_elem: + warnings.append("강한 압박이 있을 수 있어요") + + LIU_CHONG = { + frozenset(["子", "午"]), frozenset(["丑", "未"]), + frozenset(["寅", "申"]), frozenset(["卯", "酉"]), + frozenset(["辰", "戌"]), frozenset(["巳", "亥"]), + } + LIU_HE = { + frozenset(["子", "丑"]), frozenset(["寅", "亥"]), + frozenset(["卯", "戌"]), frozenset(["辰", "酉"]), + frozenset(["巳", "申"]), frozenset(["午", "未"]), + } + day_branch = saju["day"]["branch"] + if frozenset([day_branch, today_branch]) in LIU_CHONG: + warnings.append("대인 갈등에 주의하세요") + if frozenset([day_branch, today_branch]) in LIU_HE: + good_signs.append("좋은 인연을 만날 수 있어요") + + return { + "color": color, + "number": number, + "direction": direction, + "good_signs": good_signs, + "warnings": warnings, + } diff --git a/saju-lab/tests/test_lucky.py b/saju-lab/tests/test_lucky.py new file mode 100644 index 0000000..59d1e88 --- /dev/null +++ b/saju-lab/tests/test_lucky.py @@ -0,0 +1,63 @@ +from datetime import date + +import pytest + +from app.calculator.core import calculate_saju +from app.calculator.analysis import perform_full_analysis +from app.calculator.lucky import calculate_lucky + + +def _saju_for(year, month, day, hour, gender): + saju = calculate_saju(year, month, day, hour, gender) + analysis = perform_full_analysis(saju, 2026) + return saju, analysis + + +def test_lucky_keys(): + saju, analysis = _saju_for(1990, 5, 15, 14, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + for k in ("color", "number", "direction", "good_signs", "warnings"): + assert k in r, f"missing {k}" + + +def test_lucky_number_range(): + saju, analysis = _saju_for(1990, 5, 15, 14, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + assert 1 <= r["number"] <= 9 + + +def test_lucky_color_from_yongshin(): + saju, analysis = _saju_for(1990, 5, 15, 14, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + assert isinstance(r["color"], list) + assert len(r["color"]) >= 1 + yongshin = analysis["yong_shin"]["yong_shin"] + valid_colors_by_element = { + "木": {"청록", "녹색"}, + "火": {"빨강", "주황"}, + "土": {"황색", "베이지"}, + "金": {"흰색", "은색"}, + "水": {"파랑", "검정"}, + } + expected_set = valid_colors_by_element[yongshin] + assert all(c in expected_set for c in r["color"]) + + +def test_lucky_direction_from_yongshin(): + saju, analysis = _saju_for(1990, 5, 15, 14, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + valid_dirs = {"동쪽", "남쪽", "중앙", "서쪽", "북쪽"} + assert r["direction"] in valid_dirs + + +def test_good_signs_and_warnings_are_lists(): + saju, analysis = _saju_for(1990, 5, 15, 14, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + assert isinstance(r["good_signs"], list) + assert isinstance(r["warnings"], list) + + +def test_handles_missing_hour(): + saju, analysis = _saju_for(1990, 5, 15, None, "male") + r = calculate_lucky(saju, analysis, date(2026, 5, 26)) + assert 1 <= r["number"] <= 9