Files
web-page-backend/saju-lab/app/calculator/compatibility.py
gahusb f4f518fc80 feat(saju-lab): compatibility.py — 두 사주 궁합 점수 + breakdown
- saju-web/app/compatibility/result/page.tsx의 calculateCompatibility() 1:1 매핑
- 알고리즘: base 50 ± (일간 오행 same/produce/overcome) ± (일지 6합/3합/충), clamp [0,100]
- breakdown: day_master_element + branch_interaction (delta + relation/flags + description)
- 17 unit tests passed (헬퍼 9개 + 통합 8개), 438/438 total

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:14:47 +09:00

171 lines
6.2 KiB
Python

"""사주 궁합 — 두 사주 일주(일간 오행 + 일지) 매칭 → 점수 + breakdown.
saju-web/app/compatibility/result/page.tsx의 `calculateCompatibility()` 로직과 1:1 매핑.
알고리즘 (saju-web TS):
score = 50 (base)
# 일간 오행 관계
if element1 == element2: score += 10 # same
elif relation in (produce, produced): score += 25 # 상생
else: score -= 10 # 상극 (overcome/overcomed)
# 일지 관계
if 六合 (sixHarmony): score += 20
if 三合 (threeHarmony): score += 15
if 沖 (conflict): score -= 20
score = clamp(score, 0, 100)
breakdown은 saju-lab 자체 확장 — 각 카테고리별 (score_delta, description) 제공.
"""
from __future__ import annotations
from typing import Any, Dict
from .constants import FIVE_ELEMENTS
# ─── 오행 상생/상극 맵 (saju-web과 동일) ──────────────────────────────
_PRODUCE_MAP = {"": "", "": "", "": "", "": "", "": ""}
_OVERCOME_MAP = {"": "", "": "", "": "", "": "", "": ""}
# ─── 지지 6합/6충 (saju-web과 동일) ───────────────────────────────────
_SIX_HARMONY: dict[str, str] = {
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
}
# 삼합 그룹 (saju-web threeHarmonyGroups)
_THREE_HARMONY_GROUPS = [
{"", "", ""},
{"", "", ""},
{"", "", ""},
{"", "", ""},
]
_CONFLICT: dict[str, str] = {
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
"": "", "": "",
}
# ─── 헬퍼 ─────────────────────────────────────────────────────────
def _get_element_relation(el1: str, el2: str) -> str:
"""오행 관계 판별 — 'same' | 'produce' | 'produced' | 'overcome' | 'overcomed'.
saju-web getElementRelation()과 1:1 매핑.
"""
if el1 == el2:
return "same"
if _PRODUCE_MAP.get(el1) == el2:
return "produce" # el1 → el2 (el1이 el2를 생함)
if _PRODUCE_MAP.get(el2) == el1:
return "produced" # el2 → el1 (el1이 el2로부터 생을 받음)
if _OVERCOME_MAP.get(el1) == el2:
return "overcome" # el1 → el2 (el1이 el2를 극함)
return "overcomed" # el2 → el1 (el1이 el2로부터 극을 받음)
def _get_branch_relation(b1: str, b2: str) -> dict:
"""지지 관계 — {six_harmony, three_harmony, conflict}.
saju-web getBranchRelation()과 1:1 매핑.
"""
is_three = any(b1 in g and b2 in g and b1 != b2 for g in _THREE_HARMONY_GROUPS)
return {
"six_harmony": _SIX_HARMONY.get(b1) == b2,
"three_harmony": is_three,
"conflict": _CONFLICT.get(b1) == b2,
}
def _day_master_element_delta(el1: str, el2: str) -> tuple[int, str, str]:
"""일간 오행 매칭 delta + relation tag + description."""
relation = _get_element_relation(el1, el2)
if relation == "same":
return 10, "same", f"일간 동일 오행 ({el1}={el2}) — 안정적이나 자극은 약함"
if relation in ("produce", "produced"):
return 25, relation, f"일간 상생 관계 ({el1}{el2}) — 서로 도움"
# overcome / overcomed
return -10, relation, f"일간 상극 관계 ({el1}{el2}) — 갈등 가능"
def _branch_interaction_delta(b1: str, b2: str) -> tuple[int, dict, str]:
"""일지 6합/3합/충 delta + flags + description."""
rel = _get_branch_relation(b1, b2)
delta = 0
parts: list[str] = []
if rel["six_harmony"]:
delta += 20
parts.append(f"일지 6합 ({b1}+{b2})")
if rel["three_harmony"]:
delta += 15
parts.append(f"일지 3합 ({b1}+{b2})")
if rel["conflict"]:
delta -= 20
parts.append(f"일지 충 ({b1}{b2})")
if not parts:
parts.append(f"일지 중립 ({b1}, {b2})")
return delta, rel, " · ".join(parts)
# ─── 메인 ─────────────────────────────────────────────────────────
def calculate_compatibility(saju_a: Dict[str, Any], saju_b: Dict[str, Any]) -> Dict[str, Any]:
"""두 사주 궁합 점수 (0~100) + breakdown.
Args:
saju_a: calculate_saju() 결과 — `saju["day"]["stem"|"branch"|"element"]` 사용
saju_b: 두 번째 사주
Returns:
{
"score": int (0~100),
"breakdown": {
"day_master_element": {"score": int, "relation": str, "description": str},
"branch_interaction": {"score": int, "flags": dict, "description": str},
"base": 50,
}
}
"""
day_a = saju_a["day"]
day_b = saju_b["day"]
el1 = day_a.get("element") or FIVE_ELEMENTS[day_a["stem"]]
el2 = day_b.get("element") or FIVE_ELEMENTS[day_b["stem"]]
b1 = day_a["branch"]
b2 = day_b["branch"]
elem_delta, elem_relation, elem_desc = _day_master_element_delta(el1, el2)
branch_delta, branch_flags, branch_desc = _branch_interaction_delta(b1, b2)
raw = 50 + elem_delta + branch_delta
score = max(0, min(100, raw))
return {
"score": score,
"breakdown": {
"base": 50,
"day_master_element": {
"score": elem_delta,
"relation": elem_relation,
"element_a": el1,
"element_b": el2,
"description": elem_desc,
},
"branch_interaction": {
"score": branch_delta,
"flags": branch_flags,
"branch_a": b1,
"branch_b": b2,
"description": branch_desc,
},
},
}