From 030367da6c13227f56b59a7e71b21ae964bf98d3 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 26 May 2026 08:02:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(saju-lab):=20monthly=5Fflow.py=20=E2=80=94?= =?UTF-8?q?=2012=EA=B0=9C=EC=9B=94=20=EC=9A=B4=EC=84=B8=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20(4=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 월간(月干)과 월지(月支)의 일간 관계를 이용한 12개월 운세 점수 계산: - 월간 상생(生) 관계: +5~10점 - 월간 상극(剋) 관계: -8점 - 월지 육합(六合) 관계: +10점 - 월지 육충(六衝) 관계: -12점 - 월지 상생/상극: ±4점 점수 범위 0~100, 5단계 레이블 (정체/도전/변동/안정/성장) Co-Authored-By: Claude Opus 4.7 (1M context) --- saju-lab/app/calculator/monthly_flow.py | 91 +++++++++++++++++++++++++ saju-lab/tests/test_monthly_flow.py | 38 +++++++++++ 2 files changed, 129 insertions(+) create mode 100644 saju-lab/app/calculator/monthly_flow.py create mode 100644 saju-lab/tests/test_monthly_flow.py diff --git a/saju-lab/app/calculator/monthly_flow.py b/saju-lab/app/calculator/monthly_flow.py new file mode 100644 index 0000000..bcbad92 --- /dev/null +++ b/saju-lab/app/calculator/monthly_flow.py @@ -0,0 +1,91 @@ +"""12개월 운세 흐름 — 월운(月運) + 일간 관계.""" +from typing import List + +from .constants import ( + HEAVENLY_STEMS, EARTHLY_BRANCHES, + FIVE_ELEMENTS, SHENG_CYCLE, KE_CYCLE, +) + + +# 寅월부터 시작 (1월=寅, 2월=卯, ..., 12월=丑) +MONTH_BRANCHES = ["寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥", "子", "丑"] + +LIU_CHONG = { + frozenset(["子", "午"]), frozenset(["丑", "未"]), + frozenset(["寅", "申"]), frozenset(["卯", "酉"]), + frozenset(["辰", "戌"]), frozenset(["巳", "亥"]), +} +LIU_HE = { + frozenset(["子", "丑"]), frozenset(["寅", "亥"]), + frozenset(["卯", "戌"]), frozenset(["辰", "酉"]), + frozenset(["巳", "申"]), frozenset(["午", "未"]), +} + + +def _month_stem_for_year(year_stem: str, branch_idx: int) -> str: + """월간(月干) — saju-web의 공식: (yearStemIdx * 2 + branchIdx) % 10.""" + year_stem_idx = HEAVENLY_STEMS.index(year_stem) + stem_idx = (year_stem_idx * 2 + branch_idx) % 10 + return HEAVENLY_STEMS[stem_idx] + + +def _label_for(score: int) -> str: + if score >= 80: + return "성장" + if score >= 65: + return "안정" + if score >= 50: + return "변동" + if score >= 35: + return "도전" + return "정체" + + +def calculate_monthly_flow(saju: dict, year: int) -> List[dict]: + """12개월 운세 흐름.""" + day_stem = saju["day_stem"] + day_element = FIVE_ELEMENTS[day_stem] + day_branch = saju["day"]["branch"] + year_stem = saju["year"]["stem"] + + flow: List[dict] = [] + for i, branch in enumerate(MONTH_BRANCHES): + branch_idx = EARTHLY_BRANCHES.index(branch) + stem = _month_stem_for_year(year_stem, branch_idx) + stem_element = FIVE_ELEMENTS[stem] + branch_element = FIVE_ELEMENTS[branch] + + score = 60 + + if SHENG_CYCLE.get(day_element) == stem_element: + score += 5 + elif SHENG_CYCLE.get(stem_element) == day_element: + score += 10 + elif KE_CYCLE.get(day_element) == stem_element: + score += 8 + elif KE_CYCLE.get(stem_element) == day_element: + score -= 8 + elif stem_element == day_element: + score += 3 + + if frozenset([day_branch, branch]) in LIU_HE: + score += 10 + elif frozenset([day_branch, branch]) in LIU_CHONG: + score -= 12 + + if SHENG_CYCLE.get(branch_element) == day_element: + score += 4 + elif KE_CYCLE.get(branch_element) == day_element: + score -= 4 + + score = max(0, min(100, score)) + + flow.append({ + "month": i + 1, + "stem": stem, + "branch": branch, + "score": score, + "label": _label_for(score), + }) + + return flow diff --git a/saju-lab/tests/test_monthly_flow.py b/saju-lab/tests/test_monthly_flow.py new file mode 100644 index 0000000..54091e2 --- /dev/null +++ b/saju-lab/tests/test_monthly_flow.py @@ -0,0 +1,38 @@ +import pytest + +from app.calculator.core import calculate_saju +from app.calculator.monthly_flow import calculate_monthly_flow + + +def test_returns_12_entries(): + saju = calculate_saju(1990, 5, 15, 14, "male") + flow = calculate_monthly_flow(saju, 2026) + assert len(flow) == 12 + + +def test_entries_have_required_keys(): + saju = calculate_saju(1990, 5, 15, 14, "male") + flow = calculate_monthly_flow(saju, 2026) + for i, entry in enumerate(flow): + assert entry["month"] == i + 1 + for k in ("stem", "branch", "score", "label"): + assert k in entry, f"month {i+1} missing {k}" + assert 0 <= entry["score"] <= 100 + + +def test_labels_are_valid(): + saju = calculate_saju(1990, 5, 15, 14, "male") + flow = calculate_monthly_flow(saju, 2026) + valid_labels = {"변동", "성장", "안정", "도전", "정체"} + for entry in flow: + assert entry["label"] in valid_labels + + +def test_different_sajus_different_flows(): + s1 = calculate_saju(1990, 5, 15, 14, "male") + s2 = calculate_saju(1985, 1, 1, 0, "female") + f1 = calculate_monthly_flow(s1, 2026) + f2 = calculate_monthly_flow(s2, 2026) + scores_1 = [e["score"] for e in f1] + scores_2 = [e["score"] for e in f2] + assert scores_1 != scores_2