483 lines
20 KiB
Python
483 lines
20 KiB
Python
"""신살 + 지장간 + 공망 + 지지 상호작용.
|
|
|
|
saju-web/lib/saju-calculator.ts 의 다음 함수와 1:1 매핑:
|
|
- getHiddenStems / getAllHiddenStems (지장간)
|
|
- analyzeBranchInteractions (육합/삼합/방합/충/형/자형/파/해)
|
|
- calculateShinsal (역마/도화/화개/천을귀인/문창귀인/천덕귀인)
|
|
- calculateGongmang (공망)
|
|
|
|
입력 saju dict는 core.calculate_saju 의 snake_case 출력 (year/month/day/hour pillar
|
|
각각 stem, branch, stem_kr, branch_kr, element, ten_god, fortune. day_stem 별도).
|
|
출력 키는 snake_case (branch_kr, branches_kr, result_element 등). 단, `pillar` 값은
|
|
TS와 동일하게 한글(`년주`/`월주`/`일주`/`시주`)을 사용.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from .constants import (
|
|
HEAVENLY_STEMS,
|
|
HEAVENLY_STEMS_KR,
|
|
EARTHLY_BRANCHES,
|
|
EARTHLY_BRANCHES_KR,
|
|
FIVE_ELEMENTS,
|
|
HIDDEN_STEMS,
|
|
)
|
|
|
|
|
|
# ─── 지장간 ────────────────────────────────────────────────────────
|
|
def get_hidden_stems(branch: str) -> list[str]:
|
|
"""지지의 지장간(숨은 천간) — [본기, 중기, 여기]."""
|
|
return list(HIDDEN_STEMS.get(branch, []))
|
|
|
|
|
|
def get_all_hidden_stems(saju: dict) -> list[dict]:
|
|
"""4주 전체의 지장간 정보 반환.
|
|
|
|
출력 구조:
|
|
[{
|
|
"pillar": "년주",
|
|
"branch": "午",
|
|
"branch_kr": "오",
|
|
"stems": [
|
|
{"stem": "丁", "stem_kr": "정", "element": "火", "role": "정기(본기)"},
|
|
{"stem": "己", "stem_kr": "기", "element": "土", "role": "중기"},
|
|
],
|
|
}, ...]
|
|
"""
|
|
pillars: list[dict] = [
|
|
{"pillar": "년주", "branch": saju["year"]["branch"], "branch_kr": saju["year"]["branch_kr"]},
|
|
{"pillar": "월주", "branch": saju["month"]["branch"], "branch_kr": saju["month"]["branch_kr"]},
|
|
{"pillar": "일주", "branch": saju["day"]["branch"], "branch_kr": saju["day"]["branch_kr"]},
|
|
]
|
|
if saju.get("hour") is not None:
|
|
pillars.append({
|
|
"pillar": "시주",
|
|
"branch": saju["hour"]["branch"],
|
|
"branch_kr": saju["hour"]["branch_kr"],
|
|
})
|
|
|
|
result: list[dict] = []
|
|
for p in pillars:
|
|
hidden = get_hidden_stems(p["branch"])
|
|
stems = []
|
|
for idx, stem in enumerate(hidden):
|
|
stem_index = HEAVENLY_STEMS.index(stem)
|
|
if idx == 0:
|
|
role = "정기(본기)"
|
|
elif idx == 1:
|
|
role = "중기"
|
|
else:
|
|
role = "여기"
|
|
stems.append({
|
|
"stem": stem,
|
|
"stem_kr": HEAVENLY_STEMS_KR[stem_index],
|
|
"element": FIVE_ELEMENTS[stem],
|
|
"role": role,
|
|
})
|
|
result.append({
|
|
"pillar": p["pillar"],
|
|
"branch": p["branch"],
|
|
"branch_kr": p["branch_kr"],
|
|
"stems": stems,
|
|
})
|
|
return result
|
|
|
|
|
|
# ─── 지지 상호작용 ──────────────────────────────────────────────────
|
|
# 육합 (六合): 子丑(土), 寅亥(木), 卯戌(火), 辰酉(金), 巳申(水), 午未(火)
|
|
_YUKAP_PAIRS: list[tuple[str, str, str]] = [
|
|
("子", "丑", "土"), ("寅", "亥", "木"), ("卯", "戌", "火"),
|
|
("辰", "酉", "金"), ("巳", "申", "水"), ("午", "未", "火"),
|
|
]
|
|
|
|
# 삼합 (三合)
|
|
_SAMHAP_GROUPS: list[tuple[str, str, str, str]] = [
|
|
("申", "子", "辰", "水"), ("亥", "卯", "未", "木"),
|
|
("寅", "午", "戌", "火"), ("巳", "酉", "丑", "金"),
|
|
]
|
|
|
|
# 방합 (方合)
|
|
_BANGHAP_GROUPS: list[tuple[str, str, str, str]] = [
|
|
("寅", "卯", "辰", "木"), ("巳", "午", "未", "火"),
|
|
("申", "酉", "戌", "金"), ("亥", "子", "丑", "水"),
|
|
]
|
|
|
|
# 충 (沖)
|
|
_CHUNG_PAIRS: list[tuple[str, str]] = [
|
|
("子", "午"), ("丑", "未"), ("寅", "申"),
|
|
("卯", "酉"), ("辰", "戌"), ("巳", "亥"),
|
|
]
|
|
|
|
# 형 (刑)
|
|
_HYUNG_GROUPS: list[dict] = [
|
|
{"branches": ["寅", "巳", "申"], "name": "무은지형(無恩之刑)"},
|
|
{"branches": ["丑", "戌", "未"], "name": "지세지형(恃勢之刑)"},
|
|
{"branches": ["子", "卯"], "name": "무례지형(無禮之刑)"},
|
|
]
|
|
_JAHYUNG_BRANCHES = ["辰", "午", "酉", "亥"]
|
|
|
|
# 파 (破)
|
|
_PA_PAIRS: list[tuple[str, str]] = [
|
|
("子", "酉"), ("丑", "辰"), ("寅", "亥"),
|
|
("卯", "午"), ("巳", "申"), ("未", "戌"),
|
|
]
|
|
|
|
# 해 (害)
|
|
_HAE_PAIRS: list[tuple[str, str]] = [
|
|
("子", "未"), ("丑", "午"), ("寅", "巳"),
|
|
("卯", "辰"), ("申", "亥"), ("酉", "戌"),
|
|
]
|
|
|
|
_ELEMENT_NAMES_KR: dict[str, str] = {
|
|
"木": "목", "火": "화", "土": "토", "金": "금", "水": "수",
|
|
}
|
|
|
|
|
|
def _collect_pillar_branches(saju: dict) -> list[dict]:
|
|
"""4주 지지 리스트 — TS analyzeBranchInteractions 내부 변수와 동일 순서."""
|
|
out: list[dict] = [
|
|
{"branch": saju["year"]["branch"], "pillar": "년주", "branch_kr": saju["year"]["branch_kr"]},
|
|
{"branch": saju["month"]["branch"], "pillar": "월주", "branch_kr": saju["month"]["branch_kr"]},
|
|
{"branch": saju["day"]["branch"], "pillar": "일주", "branch_kr": saju["day"]["branch_kr"]},
|
|
]
|
|
if saju.get("hour") is not None:
|
|
out.append({
|
|
"branch": saju["hour"]["branch"],
|
|
"pillar": "시주",
|
|
"branch_kr": saju["hour"]["branch_kr"],
|
|
})
|
|
return out
|
|
|
|
|
|
def analyze_branch_interactions(saju: dict) -> list[dict]:
|
|
"""지지 상호작용 분석 (육합/삼합/반삼합/방합/충/형/자형/파/해).
|
|
|
|
TS analyzeBranchInteractions 와 1:1 동일. branches는 첫 번째 발견 인덱스만 본다
|
|
(중복 지지가 있을 때 TS의 indexOf 동작과 동일).
|
|
"""
|
|
interactions: list[dict] = []
|
|
pillar_branches = _collect_pillar_branches(saju)
|
|
branches = [pb["branch"] for pb in pillar_branches]
|
|
|
|
# 육합 (六合)
|
|
for a, b, elem in _YUKAP_PAIRS:
|
|
if a in branches and b in branches:
|
|
ia = branches.index(a)
|
|
ib = branches.index(b)
|
|
interactions.append({
|
|
"type": "육합(六合)",
|
|
"branches": [a, b],
|
|
"branches_kr": [pillar_branches[ia]["branch_kr"], pillar_branches[ib]["branch_kr"]],
|
|
"pillars": [pillar_branches[ia]["pillar"], pillar_branches[ib]["pillar"]],
|
|
"description": (
|
|
f"{pillar_branches[ia]['branch_kr']}{pillar_branches[ib]['branch_kr']} 육합 "
|
|
f"→ {_ELEMENT_NAMES_KR[elem]}({elem}) 기운 생성. 조화와 화합의 관계."
|
|
),
|
|
"result_element": elem,
|
|
})
|
|
|
|
# 삼합 (三合) / 반삼합 (半三合)
|
|
for a, b, c, elem in _SAMHAP_GROUPS:
|
|
found = [x for x in (a, b, c) if x in branches]
|
|
if len(found) >= 2:
|
|
found_pillars = [pillar_branches[branches.index(x)] for x in found]
|
|
is_complete = len(found) == 3
|
|
type_kr = "삼합(三合)" if is_complete else "반삼합(半三合)"
|
|
kr_str = "".join(p["branch_kr"] for p in found_pillars)
|
|
samhap_label = "삼합" if is_complete else "반삼합"
|
|
tail = "강력한 합의 기운." if is_complete else "삼합의 기운이 부분적으로 작용."
|
|
interactions.append({
|
|
"type": type_kr,
|
|
"branches": found,
|
|
"branches_kr": [p["branch_kr"] for p in found_pillars],
|
|
"pillars": [p["pillar"] for p in found_pillars],
|
|
"description": (
|
|
f"{kr_str} {samhap_label} → {_ELEMENT_NAMES_KR[elem]}({elem})국. {tail}"
|
|
),
|
|
"result_element": elem,
|
|
})
|
|
|
|
# 방합 (方合)
|
|
for a, b, c, elem in _BANGHAP_GROUPS:
|
|
found = [x for x in (a, b, c) if x in branches]
|
|
if len(found) == 3:
|
|
found_pillars = [pillar_branches[branches.index(x)] for x in found]
|
|
kr_str = "".join(p["branch_kr"] for p in found_pillars)
|
|
interactions.append({
|
|
"type": "방합(方合)",
|
|
"branches": found,
|
|
"branches_kr": [p["branch_kr"] for p in found_pillars],
|
|
"pillars": [p["pillar"] for p in found_pillars],
|
|
"description": (
|
|
f"{kr_str} 방합 → {_ELEMENT_NAMES_KR[elem]}({elem}) 방국. 매우 강한 오행 기운."
|
|
),
|
|
"result_element": elem,
|
|
})
|
|
|
|
# 충 (沖)
|
|
for a, b in _CHUNG_PAIRS:
|
|
if a in branches and b in branches:
|
|
ia = branches.index(a)
|
|
ib = branches.index(b)
|
|
interactions.append({
|
|
"type": "충(沖)",
|
|
"branches": [a, b],
|
|
"branches_kr": [pillar_branches[ia]["branch_kr"], pillar_branches[ib]["branch_kr"]],
|
|
"pillars": [pillar_branches[ia]["pillar"], pillar_branches[ib]["pillar"]],
|
|
"description": (
|
|
f"{pillar_branches[ia]['branch_kr']}{pillar_branches[ib]['branch_kr']} 충 "
|
|
f"→ 변동, 갈등, 변화의 에너지. "
|
|
f"{pillar_branches[ia]['pillar']}와 {pillar_branches[ib]['pillar']} 사이의 긴장 관계."
|
|
),
|
|
})
|
|
|
|
# 형 (刑)
|
|
for group in _HYUNG_GROUPS:
|
|
found = [x for x in group["branches"] if x in branches]
|
|
if len(found) >= 2:
|
|
found_pillars = [pillar_branches[branches.index(x)] for x in found]
|
|
kr_str = "".join(p["branch_kr"] for p in found_pillars)
|
|
interactions.append({
|
|
"type": "형(刑)",
|
|
"branches": found,
|
|
"branches_kr": [p["branch_kr"] for p in found_pillars],
|
|
"pillars": [p["pillar"] for p in found_pillars],
|
|
"description": f"{kr_str} {group['name']} → 시련과 갈등의 기운. 주의가 필요한 관계.",
|
|
})
|
|
|
|
# 자형 (自刑)
|
|
for jb in _JAHYUNG_BRANCHES:
|
|
count = branches.count(jb)
|
|
if count >= 2:
|
|
br_kr = EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.index(jb)]
|
|
interactions.append({
|
|
"type": "자형(自刑)",
|
|
"branches": [jb, jb],
|
|
"branches_kr": [br_kr, br_kr],
|
|
"pillars": [p["pillar"] for p in pillar_branches if p["branch"] == jb],
|
|
"description": f"{br_kr}{br_kr} 자형 → 자기 자신과의 갈등, 내면의 갈등 기운.",
|
|
})
|
|
|
|
# 파 (破)
|
|
for a, b in _PA_PAIRS:
|
|
if a in branches and b in branches:
|
|
ia = branches.index(a)
|
|
ib = branches.index(b)
|
|
interactions.append({
|
|
"type": "파(破)",
|
|
"branches": [a, b],
|
|
"branches_kr": [pillar_branches[ia]["branch_kr"], pillar_branches[ib]["branch_kr"]],
|
|
"pillars": [pillar_branches[ia]["pillar"], pillar_branches[ib]["pillar"]],
|
|
"description": (
|
|
f"{pillar_branches[ia]['branch_kr']}{pillar_branches[ib]['branch_kr']} 파 "
|
|
f"→ 관계의 균열, 계획의 차질 가능성."
|
|
),
|
|
})
|
|
|
|
# 해 (害)
|
|
for a, b in _HAE_PAIRS:
|
|
if a in branches and b in branches:
|
|
ia = branches.index(a)
|
|
ib = branches.index(b)
|
|
interactions.append({
|
|
"type": "해(害)",
|
|
"branches": [a, b],
|
|
"branches_kr": [pillar_branches[ia]["branch_kr"], pillar_branches[ib]["branch_kr"]],
|
|
"pillars": [pillar_branches[ia]["pillar"], pillar_branches[ib]["pillar"]],
|
|
"description": (
|
|
f"{pillar_branches[ia]['branch_kr']}{pillar_branches[ib]['branch_kr']} 해 "
|
|
f"→ 은근한 방해, 원망의 기운."
|
|
),
|
|
})
|
|
|
|
return interactions
|
|
|
|
|
|
# ─── 신살 ──────────────────────────────────────────────────────────
|
|
# 일지 삼합국 기준 신살 매핑
|
|
_SAMHAP_GROUP_MAP: dict[str, str] = {
|
|
"申": "申子辰", "子": "申子辰", "辰": "申子辰",
|
|
"寅": "寅午戌", "午": "寅午戌", "戌": "寅午戌",
|
|
"巳": "巳酉丑", "酉": "巳酉丑", "丑": "巳酉丑",
|
|
"亥": "亥卯未", "卯": "亥卯未", "未": "亥卯未",
|
|
}
|
|
_YEOKMA_MAP: dict[str, str] = {
|
|
"申子辰": "寅", "寅午戌": "申", "巳酉丑": "亥", "亥卯未": "巳",
|
|
}
|
|
_DOHWA_MAP: dict[str, str] = {
|
|
"申子辰": "酉", "寅午戌": "卯", "巳酉丑": "午", "亥卯未": "子",
|
|
}
|
|
_HWAGAE_MAP: dict[str, str] = {
|
|
"申子辰": "辰", "寅午戌": "戌", "巳酉丑": "丑", "亥卯未": "未",
|
|
}
|
|
|
|
# 천을귀인 (天乙貴人) — 일간 기준
|
|
_CHEONUL_MAP: dict[str, list[str]] = {
|
|
"甲": ["丑", "未"], "乙": ["子", "申"], "丙": ["亥", "酉"], "丁": ["亥", "酉"],
|
|
"戊": ["丑", "未"], "己": ["子", "申"], "庚": ["丑", "未"], "辛": ["寅", "午"],
|
|
"壬": ["卯", "巳"], "癸": ["卯", "巳"],
|
|
}
|
|
|
|
# 문창귀인 (文昌貴人) — 일간 기준
|
|
_MUNCHANG_MAP: dict[str, str] = {
|
|
"甲": "巳", "乙": "午", "丙": "申", "丁": "酉",
|
|
"戊": "申", "己": "酉", "庚": "亥", "辛": "子",
|
|
"壬": "寅", "癸": "卯",
|
|
}
|
|
|
|
# 천덕귀인 (天德貴人) — 월지 기준 천간
|
|
_CHEONDUK_MAP: dict[str, str] = {
|
|
"寅": "丁", "卯": "申", "辰": "壬", "巳": "辛",
|
|
"午": "亥", "未": "甲", "申": "癸", "酉": "寅",
|
|
"戌": "丙", "亥": "乙", "子": "巳", "丑": "庚",
|
|
}
|
|
|
|
|
|
def calculate_shinsal(saju: dict) -> list[dict]:
|
|
"""신살 계산 (역마/도화/화개/천을귀인/문창귀인/천덕귀인).
|
|
|
|
TS calculateShinsal 와 1:1 동일.
|
|
"""
|
|
result: list[dict] = []
|
|
day_branch = saju["day"]["branch"]
|
|
day_stem = saju["day_stem"]
|
|
month_branch = saju["month"]["branch"]
|
|
|
|
pillar_branches: list[dict] = [
|
|
{"branch": saju["year"]["branch"], "branch_kr": saju["year"]["branch_kr"], "pillar": "년주"},
|
|
{"branch": saju["month"]["branch"], "branch_kr": saju["month"]["branch_kr"], "pillar": "월주"},
|
|
{"branch": saju["day"]["branch"], "branch_kr": saju["day"]["branch_kr"], "pillar": "일주"},
|
|
]
|
|
if saju.get("hour") is not None:
|
|
pillar_branches.append({
|
|
"branch": saju["hour"]["branch"],
|
|
"branch_kr": saju["hour"]["branch_kr"],
|
|
"pillar": "시주",
|
|
})
|
|
|
|
group = _SAMHAP_GROUP_MAP.get(day_branch)
|
|
|
|
if group:
|
|
# 역마살
|
|
yeokma = _YEOKMA_MAP[group]
|
|
for pb in pillar_branches:
|
|
if pb["branch"] == yeokma and pb["pillar"] != "일주":
|
|
result.append({
|
|
"name": "역마살",
|
|
"name_hanja": "驛馬殺",
|
|
"branch": yeokma,
|
|
"branch_kr": pb["branch_kr"],
|
|
"pillar": pb["pillar"],
|
|
"description": "이동, 변동, 해외, 출장이 많은 기운. 활동적이고 한 곳에 머물지 못하는 성향.",
|
|
})
|
|
|
|
# 도화살
|
|
dohwa = _DOHWA_MAP[group]
|
|
for pb in pillar_branches:
|
|
if pb["branch"] == dohwa and pb["pillar"] != "일주":
|
|
result.append({
|
|
"name": "도화살",
|
|
"name_hanja": "桃花殺",
|
|
"branch": dohwa,
|
|
"branch_kr": pb["branch_kr"],
|
|
"pillar": pb["pillar"],
|
|
"description": "매력, 인기, 예술적 감각. 이성에게 끌리는 기운이 강하며 대인관계가 화려함.",
|
|
})
|
|
|
|
# 화개살
|
|
hwagae = _HWAGAE_MAP[group]
|
|
for pb in pillar_branches:
|
|
if pb["branch"] == hwagae and pb["pillar"] != "일주":
|
|
result.append({
|
|
"name": "화개살",
|
|
"name_hanja": "華蓋殺",
|
|
"branch": hwagae,
|
|
"branch_kr": pb["branch_kr"],
|
|
"pillar": pb["pillar"],
|
|
"description": "학문, 종교, 예술에 심취하는 기운. 고독을 즐기며 정신적 세계에 몰두하는 성향.",
|
|
})
|
|
|
|
# 천을귀인 — 일간 기준
|
|
cheonul_branches = _CHEONUL_MAP.get(day_stem, [])
|
|
for pb in pillar_branches:
|
|
if pb["branch"] in cheonul_branches and pb["pillar"] != "일주":
|
|
result.append({
|
|
"name": "천을귀인",
|
|
"name_hanja": "天乙貴人",
|
|
"branch": pb["branch"],
|
|
"branch_kr": pb["branch_kr"],
|
|
"pillar": pb["pillar"],
|
|
"description": "위기에서 귀인의 도움을 받는 길한 기운. 어려울 때 도움을 주는 사람이 나타남.",
|
|
})
|
|
|
|
# 문창귀인 — 일간 기준
|
|
munchang_branch = _MUNCHANG_MAP.get(day_stem)
|
|
if munchang_branch:
|
|
for pb in pillar_branches:
|
|
if pb["branch"] == munchang_branch and pb["pillar"] != "일주":
|
|
result.append({
|
|
"name": "문창귀인",
|
|
"name_hanja": "文昌貴人",
|
|
"branch": pb["branch"],
|
|
"branch_kr": pb["branch_kr"],
|
|
"pillar": pb["pillar"],
|
|
"description": "학문, 시험, 문서에 유리한 기운. 공부를 잘하며 시험운이 좋음.",
|
|
})
|
|
|
|
# 천덕귀인 — 월지 기준 천간이 4주 천간에 있으면
|
|
cheonduk_stem = _CHEONDUK_MAP.get(month_branch)
|
|
if cheonduk_stem:
|
|
all_stems: list[dict] = [
|
|
{"stem": saju["year"]["stem"], "pillar": "년주"},
|
|
{"stem": saju["day"]["stem"], "pillar": "일주"},
|
|
]
|
|
if saju.get("hour") is not None:
|
|
all_stems.append({"stem": saju["hour"]["stem"], "pillar": "시주"})
|
|
for ps in all_stems:
|
|
if ps["stem"] == cheonduk_stem:
|
|
br_kr = EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.index(month_branch)]
|
|
result.append({
|
|
"name": "천덕귀인",
|
|
"name_hanja": "天德貴人",
|
|
"branch": month_branch,
|
|
"branch_kr": br_kr,
|
|
"pillar": ps["pillar"],
|
|
"description": "하늘의 덕을 받는 기운. 재난을 피하고 복을 받는 길신 중의 길신.",
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
# ─── 공망 ──────────────────────────────────────────────────────────
|
|
def calculate_gongmang(day_stem: str, day_branch: str) -> dict:
|
|
"""공망(空亡) — 일주의 旬(60갑자 10단위)에서 빠지는 2개 지지.
|
|
|
|
TS calculateGongmang 와 1:1 동일.
|
|
"""
|
|
stem_idx = HEAVENLY_STEMS.index(day_stem)
|
|
branch_idx = EARTHLY_BRANCHES.index(day_branch)
|
|
|
|
# 60갑자에서 해당 旬의 시작점 = 천간이 甲인 지점
|
|
# 旬의 시작 지지 인덱스 = (branchIdx - stemIdx + 120) % 12
|
|
start_branch_idx = (branch_idx - stem_idx + 120) % 12
|
|
|
|
# 공망 = 旬 시작 + 10, +11
|
|
gongmang1_idx = (start_branch_idx + 10) % 12
|
|
gongmang2_idx = (start_branch_idx + 11) % 12
|
|
|
|
branch1 = EARTHLY_BRANCHES[gongmang1_idx]
|
|
branch2 = EARTHLY_BRANCHES[gongmang2_idx]
|
|
branch_kr1 = EARTHLY_BRANCHES_KR[gongmang1_idx]
|
|
branch_kr2 = EARTHLY_BRANCHES_KR[gongmang2_idx]
|
|
|
|
return {
|
|
"branches": [branch1, branch2],
|
|
"branches_kr": [branch_kr1, branch_kr2],
|
|
"description": (
|
|
f"{branch_kr1}({branch1})·{branch_kr2}({branch2}) 공망 "
|
|
f"→ 해당 지지의 기운이 비어있어 허무하거나 집착이 없는 영역. "
|
|
f"오히려 초월적 능력이 될 수 있음."
|
|
),
|
|
}
|