사주 기능 이식 & 로그인, 유저 페이지 Supabase 연동 & 토스 페이먼츠 결제 연동 & 사주 심층 분석을 위한 기능 분리
This commit is contained in:
0
saju-engine/calculator/__init__.py
Normal file
0
saju-engine/calculator/__init__.py
Normal file
109
saju-engine/calculator/daeun_calculator.py
Normal file
109
saju-engine/calculator/daeun_calculator.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
대운 (大運) 계산 모듈
|
||||
양남음녀 순행, 음남양녀 역행, 절기 기준 대운 시작 나이
|
||||
"""
|
||||
|
||||
from calculator.saju_calculator import HEAVENLY_STEMS, EARTHLY_BRANCHES, HEAVENLY_STEMS_KR, EARTHLY_BRANCHES_KR
|
||||
from calculator.solar_terms import get_days_to_next_solar_term, get_days_from_prev_solar_term
|
||||
|
||||
|
||||
def _calculate_daeun_start_age(
|
||||
birth_year: int,
|
||||
birth_month: int,
|
||||
birth_day: int,
|
||||
gender: str,
|
||||
is_yang_year: bool,
|
||||
) -> int:
|
||||
"""절기 기준 대운 시작 나이 계산"""
|
||||
is_forward = (gender == 'male' and is_yang_year) or (gender == 'female' and not is_yang_year)
|
||||
|
||||
if is_forward:
|
||||
# 순행: 생일부터 다음 절기까지의 일수
|
||||
days = get_days_to_next_solar_term(birth_year, birth_month, birth_day)
|
||||
else:
|
||||
# 역행: 이전 절기부터 생일까지의 일수
|
||||
days = get_days_from_prev_solar_term(birth_year, birth_month, birth_day)
|
||||
|
||||
# 3일 = 1세
|
||||
start_age = days // 3
|
||||
return max(1, min(10, start_age))
|
||||
|
||||
|
||||
def calculate_daeun(
|
||||
birth_year: int,
|
||||
birth_month: int,
|
||||
birth_day: int,
|
||||
gender: str,
|
||||
month_stem: str,
|
||||
month_branch: str,
|
||||
) -> list[dict]:
|
||||
"""대운 계산 (10년 단위, 8개 대운)"""
|
||||
if month_stem not in HEAVENLY_STEMS or month_branch not in EARTHLY_BRANCHES:
|
||||
return []
|
||||
|
||||
month_stem_idx = HEAVENLY_STEMS.index(month_stem)
|
||||
month_branch_idx = EARTHLY_BRANCHES.index(month_branch)
|
||||
|
||||
year_stem_idx = (birth_year - 1900 + 6) % 10
|
||||
is_yang_year = year_stem_idx % 2 == 0
|
||||
|
||||
is_forward = (gender == 'male' and is_yang_year) or (gender == 'female' and not is_yang_year)
|
||||
|
||||
start_age = _calculate_daeun_start_age(birth_year, birth_month, birth_day, gender, is_yang_year)
|
||||
|
||||
daeun_list = []
|
||||
for i in range(8):
|
||||
age = start_age + (i * 10)
|
||||
start_year = birth_year + age
|
||||
end_year = start_year + 9
|
||||
|
||||
if is_forward:
|
||||
stem_idx = (month_stem_idx + i + 1) % 10
|
||||
branch_idx = (month_branch_idx + i + 1) % 12
|
||||
else:
|
||||
stem_idx = (month_stem_idx - i - 1 + 100) % 10
|
||||
branch_idx = (month_branch_idx - i - 1 + 120) % 12
|
||||
|
||||
daeun_list.append({
|
||||
'age': age,
|
||||
'startYear': start_year,
|
||||
'endYear': end_year,
|
||||
'stem': HEAVENLY_STEMS[stem_idx],
|
||||
'branch': EARTHLY_BRANCHES[branch_idx],
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
|
||||
})
|
||||
|
||||
return daeun_list
|
||||
|
||||
|
||||
def get_current_daeun(daeun_list: list[dict], current_year: int) -> dict | None:
|
||||
"""현재 대운 찾기"""
|
||||
for daeun in daeun_list:
|
||||
if daeun['startYear'] <= current_year <= daeun['endYear']:
|
||||
return daeun
|
||||
return None
|
||||
|
||||
|
||||
def get_daeun_description(daeun: dict, day_stem: str) -> str:
|
||||
"""대운 기본 해석"""
|
||||
age = daeun['age']
|
||||
ganzi = f"{daeun['stem']}{daeun['branch']}"
|
||||
desc = f"{age}세부터 {age + 9}세까지의 10년은 {daeun['stemKr']}{daeun['branchKr']}({ganzi}) 대운입니다. "
|
||||
|
||||
if age < 20:
|
||||
desc += '청소년기로 학업과 기초를 다지는 시기입니다. '
|
||||
elif age < 40:
|
||||
desc += '성장과 발전의 시기로 사회활동이 왕성한 때입니다. '
|
||||
elif age < 60:
|
||||
desc += '안정과 성숙의 시기로 경험이 쌓이는 때입니다. '
|
||||
else:
|
||||
desc += '원숙한 시기로 인생의 지혜를 나누는 때입니다. '
|
||||
|
||||
stem_idx = HEAVENLY_STEMS.index(daeun['stem'])
|
||||
if stem_idx % 2 == 0:
|
||||
desc += '적극적이고 외향적인 활동이 유리합니다.'
|
||||
else:
|
||||
desc += '차분하고 내실을 다지는 것이 좋습니다.'
|
||||
|
||||
return desc
|
||||
267
saju-engine/calculator/lotto_generator.py
Normal file
267
saju-engine/calculator/lotto_generator.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
사주 기반 로또 번호 생성 모듈
|
||||
오행 균형, 천간/지지 고유 숫자, 신살 등을 반영
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from calculator.saju_calculator import FIVE_ELEMENTS
|
||||
|
||||
# 오행별 로또 번호 후보
|
||||
_ELEMENT_NUMBERS: dict[str, list[int]] = {
|
||||
'木': [1, 2, 11, 12, 21, 22, 31, 32, 41, 42],
|
||||
'火': [3, 4, 13, 14, 23, 24, 33, 34, 43, 44],
|
||||
'土': [5, 6, 15, 16, 25, 26, 35, 36, 45],
|
||||
'金': [7, 8, 17, 18, 27, 28, 37, 38],
|
||||
'水': [9, 10, 19, 20, 29, 30, 39, 40],
|
||||
}
|
||||
|
||||
# 천간 고유 숫자 (각 천간에 대응하는 행운 숫자)
|
||||
_STEM_NUMBERS: dict[str, list[int]] = {
|
||||
'甲': [1, 11, 21, 31, 41],
|
||||
'乙': [2, 12, 22, 32, 42],
|
||||
'丙': [3, 13, 23, 33, 43],
|
||||
'丁': [4, 14, 24, 34, 44],
|
||||
'戊': [5, 15, 25, 35, 45],
|
||||
'己': [6, 16, 26, 36],
|
||||
'庚': [7, 17, 27, 37],
|
||||
'辛': [8, 18, 28, 38],
|
||||
'壬': [9, 19, 29, 39],
|
||||
'癸': [10, 20, 30, 40],
|
||||
}
|
||||
|
||||
# 지지 고유 숫자
|
||||
_BRANCH_NUMBERS: dict[str, list[int]] = {
|
||||
'子': [9, 19, 29, 39],
|
||||
'丑': [6, 15, 25, 36],
|
||||
'寅': [1, 11, 31, 41],
|
||||
'卯': [2, 12, 22, 42],
|
||||
'辰': [5, 16, 26, 35],
|
||||
'巳': [3, 14, 24, 43],
|
||||
'午': [4, 13, 23, 44],
|
||||
'未': [6, 16, 26, 45],
|
||||
'申': [7, 18, 27, 37],
|
||||
'酉': [8, 17, 28, 38],
|
||||
'戌': [5, 15, 25, 35],
|
||||
'亥': [10, 20, 30, 40],
|
||||
}
|
||||
|
||||
# 신살별 보너스 숫자
|
||||
_SHINSAL_BONUS: dict[str, list[int]] = {
|
||||
'역마살': [7, 17, 27, 37],
|
||||
'도화살': [3, 13, 23, 33, 43],
|
||||
'화개살': [11, 22, 33, 44],
|
||||
'천을귀인': [1, 7, 14, 21, 28, 35, 42],
|
||||
'문창귀인': [4, 16, 25, 36],
|
||||
'천덕귀인': [6, 12, 24, 36],
|
||||
}
|
||||
|
||||
|
||||
def _seed_from_saju(saju: dict) -> str:
|
||||
"""사주 데이터에서 결정론적 시드 생성"""
|
||||
bd = saju.get('birthDate', {})
|
||||
key = (
|
||||
f"{bd.get('year')}-{bd.get('month')}-{bd.get('day')}-"
|
||||
f"{bd.get('hour', 'X')}-{saju.get('gender', 'X')}"
|
||||
)
|
||||
return hashlib.sha256(key.encode()).hexdigest()
|
||||
|
||||
|
||||
def _get_dominant_elements(saju: dict) -> list[str]:
|
||||
"""사주에서 강한 오행 추출 (빈도 기준 정렬)"""
|
||||
count: dict[str, int] = {'木': 0, '火': 0, '土': 0, '金': 0, '水': 0}
|
||||
pillars = ['year', 'month', 'day', 'hour']
|
||||
for p in pillars:
|
||||
pillar = saju.get(p)
|
||||
if not pillar:
|
||||
continue
|
||||
for key in ['stem', 'branch']:
|
||||
char = pillar.get(key, '')
|
||||
elem = FIVE_ELEMENTS.get(char)
|
||||
if elem:
|
||||
count[elem] = count.get(elem, 0) + 1
|
||||
|
||||
return sorted(count, key=lambda e: count[e], reverse=True)
|
||||
|
||||
|
||||
def generate_lotto_numbers(
|
||||
saju: dict,
|
||||
shinsal: Optional[list[dict]] = None,
|
||||
count: int = 6,
|
||||
) -> dict:
|
||||
"""
|
||||
사주 기반 로또 번호 생성
|
||||
|
||||
Returns:
|
||||
{
|
||||
'numbers': [int, ...], # 추천 번호 (오름차순)
|
||||
'basis': str, # 생성 근거 설명
|
||||
'elementBalance': dict, # 오행별 번호 분포
|
||||
}
|
||||
"""
|
||||
seed_hex = _seed_from_saju(saju)
|
||||
rng = random.Random(int(seed_hex, 16) % (2**32))
|
||||
|
||||
# 1. 후보 풀 구성 (우선순위 점수)
|
||||
scores: dict[int, float] = {n: 0.0 for n in range(1, 46)}
|
||||
|
||||
# 오행 비중 (강한 오행 우선)
|
||||
dominant_elements = _get_dominant_elements(saju)
|
||||
for rank, elem in enumerate(dominant_elements):
|
||||
weight = 5.0 - rank # 1위=5점, 2위=4점, ...
|
||||
for n in _ELEMENT_NUMBERS.get(elem, []):
|
||||
if n in scores:
|
||||
scores[n] += weight
|
||||
|
||||
# 일간 비중
|
||||
day_stem = saju.get('dayStem', '')
|
||||
for n in _STEM_NUMBERS.get(day_stem, []):
|
||||
if n in scores:
|
||||
scores[n] += 4.0
|
||||
|
||||
# 일지 비중
|
||||
day_branch = saju.get('day', {}).get('branch', '')
|
||||
for n in _BRANCH_NUMBERS.get(day_branch, []):
|
||||
if n in scores:
|
||||
scores[n] += 3.0
|
||||
|
||||
# 월지 비중
|
||||
month_branch = saju.get('month', {}).get('branch', '')
|
||||
for n in _BRANCH_NUMBERS.get(month_branch, []):
|
||||
if n in scores:
|
||||
scores[n] += 2.0
|
||||
|
||||
# 신살 보너스
|
||||
shinsal_names = []
|
||||
if shinsal:
|
||||
for s in shinsal:
|
||||
name = s.get('name', '')
|
||||
shinsal_names.append(name)
|
||||
for n in _SHINSAL_BONUS.get(name, []):
|
||||
if n in scores:
|
||||
scores[n] += 2.5
|
||||
|
||||
# 2. 점수 기반 확률 가중 샘플링
|
||||
numbers_pool = list(scores.keys())
|
||||
weights = [scores[n] + 1.0 for n in numbers_pool] # 최소 1.0 보장
|
||||
|
||||
selected: list[int] = []
|
||||
remaining_pool = list(zip(numbers_pool, weights))
|
||||
|
||||
while len(selected) < count and remaining_pool:
|
||||
total = sum(w for _, w in remaining_pool)
|
||||
pick = rng.uniform(0, total)
|
||||
cumulative = 0
|
||||
picked_n = None
|
||||
for n, w in remaining_pool:
|
||||
cumulative += w
|
||||
if pick <= cumulative:
|
||||
picked_n = n
|
||||
break
|
||||
if picked_n is None:
|
||||
picked_n = remaining_pool[-1][0]
|
||||
|
||||
selected.append(picked_n)
|
||||
remaining_pool = [(n, w) for n, w in remaining_pool if n != picked_n]
|
||||
|
||||
selected.sort()
|
||||
|
||||
# 3. 오행 분포 계산
|
||||
def _number_to_element(n: int) -> str:
|
||||
for elem, nums in _ELEMENT_NUMBERS.items():
|
||||
if n in nums:
|
||||
return elem
|
||||
return '土'
|
||||
|
||||
element_balance = {}
|
||||
for n in selected:
|
||||
elem = _number_to_element(n)
|
||||
element_balance[elem] = element_balance.get(elem, [])
|
||||
element_balance[elem].append(n)
|
||||
|
||||
# 4. 근거 설명 생성
|
||||
basis_parts = [
|
||||
f"일간 {saju.get('dayStem', '')}({day_stem}) 기반",
|
||||
f"강한 오행: {', '.join(dominant_elements[:2])}",
|
||||
]
|
||||
if shinsal_names:
|
||||
basis_parts.append(f"신살 반영: {', '.join(set(shinsal_names))}")
|
||||
|
||||
basis = ' / '.join(basis_parts)
|
||||
|
||||
return {
|
||||
'numbers': selected,
|
||||
'basis': basis,
|
||||
'elementBalance': element_balance,
|
||||
}
|
||||
|
||||
|
||||
def generate_multiple_sets(
|
||||
saju: dict,
|
||||
shinsal: Optional[list[dict]] = None,
|
||||
sets: int = 5,
|
||||
) -> list[dict]:
|
||||
"""여러 세트의 로또 번호 생성 (시드 변형)"""
|
||||
results = []
|
||||
seed_hex = _seed_from_saju(saju)
|
||||
base_seed = int(seed_hex, 16) % (2**32)
|
||||
|
||||
for i in range(sets):
|
||||
# 세트별 시드 변형
|
||||
modified_saju = dict(saju)
|
||||
modified_saju['_set_index'] = i # 내부 변형용
|
||||
|
||||
rng = random.Random(base_seed + i * 997)
|
||||
|
||||
dominant_elements = _get_dominant_elements(saju)
|
||||
|
||||
# 각 세트는 조금씩 다른 오행 강조
|
||||
scores: dict[int, float] = {n: rng.random() * 2 for n in range(1, 46)}
|
||||
|
||||
elem_to_emphasize = dominant_elements[i % len(dominant_elements)]
|
||||
for n in _ELEMENT_NUMBERS.get(elem_to_emphasize, []):
|
||||
if n in scores:
|
||||
scores[n] += 5.0
|
||||
|
||||
day_stem = saju.get('dayStem', '')
|
||||
for n in _STEM_NUMBERS.get(day_stem, []):
|
||||
if n in scores:
|
||||
scores[n] += 3.0
|
||||
|
||||
if shinsal:
|
||||
for s in shinsal:
|
||||
for n in _SHINSAL_BONUS.get(s.get('name', ''), []):
|
||||
if n in scores:
|
||||
scores[n] += 2.0
|
||||
|
||||
pool = list(scores.keys())
|
||||
weights = [scores[n] for n in pool]
|
||||
|
||||
selected: list[int] = []
|
||||
remaining = list(zip(pool, weights))
|
||||
|
||||
while len(selected) < 6 and remaining:
|
||||
total = sum(w for _, w in remaining)
|
||||
pick = rng.uniform(0, total)
|
||||
cumulative = 0.0
|
||||
picked_n = None
|
||||
for n, w in remaining:
|
||||
cumulative += w
|
||||
if pick <= cumulative:
|
||||
picked_n = n
|
||||
break
|
||||
if picked_n is None:
|
||||
picked_n = remaining[-1][0]
|
||||
selected.append(picked_n)
|
||||
remaining = [(n, w) for n, w in remaining if n != picked_n]
|
||||
|
||||
selected.sort()
|
||||
results.append({
|
||||
'set': i + 1,
|
||||
'numbers': selected,
|
||||
'emphasis': elem_to_emphasize,
|
||||
})
|
||||
|
||||
return results
|
||||
596
saju-engine/calculator/saju_calculator.py
Normal file
596
saju-engine/calculator/saju_calculator.py
Normal file
@@ -0,0 +1,596 @@
|
||||
"""
|
||||
사주팔자 계산 모듈
|
||||
천간, 지지, 오행, 십성, 십이운성, 신살, 공망, 지장간, 지지 상호작용
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
from calculator.solar_terms import get_solar_term_month_branch
|
||||
|
||||
# ============================================================
|
||||
# 기본 상수
|
||||
# ============================================================
|
||||
|
||||
HEAVENLY_STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']
|
||||
HEAVENLY_STEMS_KR = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계']
|
||||
|
||||
EARTHLY_BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥']
|
||||
EARTHLY_BRANCHES_KR = ['자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해']
|
||||
|
||||
FIVE_ELEMENTS: dict[str, str] = {
|
||||
'甲': '木', '乙': '木',
|
||||
'丙': '火', '丁': '火',
|
||||
'戊': '土', '己': '土',
|
||||
'庚': '金', '辛': '金',
|
||||
'壬': '水', '癸': '水',
|
||||
'寅': '木', '卯': '木',
|
||||
'巳': '火', '午': '火',
|
||||
'辰': '土', '戌': '土', '丑': '土', '未': '土',
|
||||
'申': '金', '酉': '金',
|
||||
'子': '水', '亥': '水',
|
||||
}
|
||||
|
||||
FIVE_ELEMENTS_KR = {'木': '목', '火': '화', '土': '토', '金': '금', '水': '수'}
|
||||
|
||||
TWELVE_FORTUNES = ['장생', '목욕', '관대', '건록', '제왕', '쇠', '병', '사', '묘', '절', '태', '양']
|
||||
|
||||
# 기준년: 1900 = 庚子년
|
||||
BASE_YEAR = 1900
|
||||
BASE_YEAR_STEM = 6 # 庚
|
||||
BASE_YEAR_BRANCH = 0 # 子
|
||||
|
||||
# 기준일: 1900-01-01 = 丙寅일
|
||||
BASE_DAY_STEM = 2 # 丙
|
||||
BASE_DAY_BRANCH = 2 # 寅
|
||||
|
||||
# ============================================================
|
||||
# 간지 계산
|
||||
# ============================================================
|
||||
|
||||
def get_year_ganzi(year: int) -> dict:
|
||||
year_diff = year - BASE_YEAR
|
||||
stem_idx = (BASE_YEAR_STEM + year_diff) % 10
|
||||
branch_idx = (BASE_YEAR_BRANCH + year_diff) % 12
|
||||
return {
|
||||
'stem': HEAVENLY_STEMS[stem_idx],
|
||||
'branch': EARTHLY_BRANCHES[branch_idx],
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
|
||||
}
|
||||
|
||||
|
||||
def get_month_ganzi(year: int, month: int, day: int) -> dict:
|
||||
branch_idx = get_solar_term_month_branch(year, month, day)
|
||||
year_stem = get_year_ganzi(year)['stem']
|
||||
year_stem_idx = HEAVENLY_STEMS.index(year_stem)
|
||||
stem_idx = (year_stem_idx * 2 + branch_idx) % 10
|
||||
return {
|
||||
'stem': HEAVENLY_STEMS[stem_idx],
|
||||
'branch': EARTHLY_BRANCHES[branch_idx],
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
|
||||
}
|
||||
|
||||
|
||||
def get_day_ganzi(year: int, month: int, day: int) -> dict:
|
||||
base = date(1900, 1, 1)
|
||||
target = date(year, month, day)
|
||||
days_diff = (target - base).days
|
||||
|
||||
stem_idx = (BASE_DAY_STEM + days_diff) % 10
|
||||
branch_idx = (BASE_DAY_BRANCH + days_diff) % 12
|
||||
|
||||
# 음수 처리 (1900년 이전)
|
||||
if stem_idx < 0:
|
||||
stem_idx += 10
|
||||
if branch_idx < 0:
|
||||
branch_idx += 12
|
||||
|
||||
return {
|
||||
'stem': HEAVENLY_STEMS[stem_idx],
|
||||
'branch': EARTHLY_BRANCHES[branch_idx],
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
|
||||
}
|
||||
|
||||
|
||||
def get_hour_ganzi(day_stem: str, hour: int) -> dict:
|
||||
if hour >= 23 or hour < 1:
|
||||
branch_idx = 0 # 子
|
||||
elif hour < 3:
|
||||
branch_idx = 1 # 丑
|
||||
elif hour < 5:
|
||||
branch_idx = 2 # 寅
|
||||
elif hour < 7:
|
||||
branch_idx = 3 # 卯
|
||||
elif hour < 9:
|
||||
branch_idx = 4 # 辰
|
||||
elif hour < 11:
|
||||
branch_idx = 5 # 巳
|
||||
elif hour < 13:
|
||||
branch_idx = 6 # 午
|
||||
elif hour < 15:
|
||||
branch_idx = 7 # 未
|
||||
elif hour < 17:
|
||||
branch_idx = 8 # 申
|
||||
elif hour < 19:
|
||||
branch_idx = 9 # 酉
|
||||
elif hour < 21:
|
||||
branch_idx = 10 # 戌
|
||||
else:
|
||||
branch_idx = 11 # 亥
|
||||
|
||||
day_stem_idx = HEAVENLY_STEMS.index(day_stem)
|
||||
stem_idx = (day_stem_idx * 2 + branch_idx) % 10
|
||||
return {
|
||||
'stem': HEAVENLY_STEMS[stem_idx],
|
||||
'branch': EARTHLY_BRANCHES[branch_idx],
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 십성 계산
|
||||
# ============================================================
|
||||
|
||||
_PRODUCE_MAP = {'木': '火', '火': '土', '土': '金', '金': '水', '水': '木'}
|
||||
_OVERCOME_MAP = {'木': '土', '火': '金', '土': '水', '金': '木', '水': '火'}
|
||||
|
||||
|
||||
def get_ten_god(day_stem: str, target_stem: str, is_yang: bool) -> str:
|
||||
day_elem = FIVE_ELEMENTS.get(day_stem, '')
|
||||
target_elem = FIVE_ELEMENTS.get(target_stem, '')
|
||||
|
||||
if day_elem == target_elem:
|
||||
return '비견' if is_yang else '겁재'
|
||||
if _PRODUCE_MAP.get(day_elem) == target_elem:
|
||||
return '식신' if is_yang else '상관'
|
||||
if _OVERCOME_MAP.get(day_elem) == target_elem:
|
||||
return '편재' if is_yang else '정재'
|
||||
if _OVERCOME_MAP.get(target_elem) == day_elem:
|
||||
return '편관' if is_yang else '정관'
|
||||
if _PRODUCE_MAP.get(target_elem) == day_elem:
|
||||
return '편인' if is_yang else '정인'
|
||||
return '비견'
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 십이운성 계산
|
||||
# ============================================================
|
||||
|
||||
_FORTUNE_MAP: dict[str, dict[str, int]] = {
|
||||
'甲': {'亥': 11, '子': 0, '丑': 1, '寅': 2, '卯': 3, '辰': 4, '巳': 5, '午': 6, '未': 7, '申': 8, '酉': 9, '戌': 10},
|
||||
'乙': {'午': 11, '未': 0, '申': 1, '酉': 2, '戌': 3, '亥': 4, '子': 5, '丑': 6, '寅': 7, '卯': 8, '辰': 9, '巳': 10},
|
||||
'丙': {'寅': 11, '卯': 0, '辰': 1, '巳': 2, '午': 3, '未': 4, '申': 5, '酉': 6, '戌': 7, '亥': 8, '子': 9, '丑': 10},
|
||||
'丁': {'酉': 11, '戌': 0, '亥': 1, '子': 2, '丑': 3, '寅': 4, '卯': 5, '辰': 6, '巳': 7, '午': 8, '未': 9, '申': 10},
|
||||
'戊': {'寅': 11, '卯': 0, '辰': 1, '巳': 2, '午': 3, '未': 4, '申': 5, '酉': 6, '戌': 7, '亥': 8, '子': 9, '丑': 10},
|
||||
'己': {'酉': 11, '戌': 0, '亥': 1, '子': 2, '丑': 3, '寅': 4, '卯': 5, '辰': 6, '巳': 7, '午': 8, '未': 9, '申': 10},
|
||||
'庚': {'巳': 11, '午': 0, '未': 1, '申': 2, '酉': 3, '戌': 4, '亥': 5, '子': 6, '丑': 7, '寅': 8, '卯': 9, '辰': 10},
|
||||
'辛': {'子': 11, '丑': 0, '寅': 1, '卯': 2, '辰': 3, '巳': 4, '午': 5, '未': 6, '申': 7, '酉': 8, '戌': 9, '亥': 10},
|
||||
'壬': {'申': 11, '酉': 0, '戌': 1, '亥': 2, '子': 3, '丑': 4, '寅': 5, '卯': 6, '辰': 7, '巳': 8, '午': 9, '未': 10},
|
||||
'癸': {'卯': 11, '辰': 0, '巳': 1, '午': 2, '未': 3, '申': 4, '酉': 5, '戌': 6, '亥': 7, '子': 8, '丑': 9, '寅': 10},
|
||||
}
|
||||
|
||||
|
||||
def get_twelve_fortune(day_stem: str, branch: str) -> str:
|
||||
idx = _FORTUNE_MAP.get(day_stem, {}).get(branch, 0)
|
||||
return TWELVE_FORTUNES[idx]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 지장간 (藏干)
|
||||
# ============================================================
|
||||
|
||||
HIDDEN_STEMS: dict[str, list[str]] = {
|
||||
'子': ['癸'],
|
||||
'丑': ['己', '癸', '辛'],
|
||||
'寅': ['甲', '丙', '戊'],
|
||||
'卯': ['乙'],
|
||||
'辰': ['戊', '乙', '癸'],
|
||||
'巳': ['丙', '庚', '戊'],
|
||||
'午': ['丁', '己'],
|
||||
'未': ['己', '丁', '乙'],
|
||||
'申': ['庚', '壬', '戊'],
|
||||
'酉': ['辛'],
|
||||
'戌': ['戊', '辛', '丁'],
|
||||
'亥': ['壬', '甲'],
|
||||
}
|
||||
|
||||
_ROLE_NAMES = ['정기(본기)', '중기', '여기']
|
||||
|
||||
|
||||
def get_hidden_stems(branch: str) -> list[str]:
|
||||
return HIDDEN_STEMS.get(branch, [])
|
||||
|
||||
|
||||
def get_all_hidden_stems(saju: dict) -> list[dict]:
|
||||
pillars = [
|
||||
{'pillar': '년주', 'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr']},
|
||||
{'pillar': '월주', 'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr']},
|
||||
{'pillar': '일주', 'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr']},
|
||||
]
|
||||
if saju.get('hour'):
|
||||
pillars.append({'pillar': '시주', 'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr']})
|
||||
|
||||
result = []
|
||||
for p in pillars:
|
||||
hidden = get_hidden_stems(p['branch'])
|
||||
stems_info = []
|
||||
for idx, stem in enumerate(hidden):
|
||||
stem_idx = HEAVENLY_STEMS.index(stem)
|
||||
stems_info.append({
|
||||
'stem': stem,
|
||||
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
|
||||
'element': FIVE_ELEMENTS.get(stem, ''),
|
||||
'role': _ROLE_NAMES[idx] if idx < len(_ROLE_NAMES) else '여기',
|
||||
})
|
||||
result.append({
|
||||
'pillar': p['pillar'],
|
||||
'branch': p['branch'],
|
||||
'branchKr': p['branchKr'],
|
||||
'stems': stems_info,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 지지 상호작용
|
||||
# ============================================================
|
||||
|
||||
_YUKAP_PAIRS = [
|
||||
('子', '丑', '土'), ('寅', '亥', '木'), ('卯', '戌', '火'),
|
||||
('辰', '酉', '金'), ('巳', '申', '水'), ('午', '未', '火'),
|
||||
]
|
||||
|
||||
_SAMHAP_GROUPS = [
|
||||
('申', '子', '辰', '水'), ('亥', '卯', '未', '木'),
|
||||
('寅', '午', '戌', '火'), ('巳', '酉', '丑', '金'),
|
||||
]
|
||||
|
||||
_BANGHAP_GROUPS = [
|
||||
('寅', '卯', '辰', '木'), ('巳', '午', '未', '火'),
|
||||
('申', '酉', '戌', '金'), ('亥', '子', '丑', '水'),
|
||||
]
|
||||
|
||||
_CHUNG_PAIRS = [
|
||||
('子', '午'), ('丑', '未'), ('寅', '申'),
|
||||
('卯', '酉'), ('辰', '戌'), ('巳', '亥'),
|
||||
]
|
||||
|
||||
_HYUNG_GROUPS = [
|
||||
{'branches': ['寅', '巳', '申'], 'name': '무은지형(無恩之刑)'},
|
||||
{'branches': ['丑', '戌', '未'], 'name': '지세지형(恃勢之刑)'},
|
||||
{'branches': ['子', '卯'], 'name': '무례지형(無禮之刑)'},
|
||||
]
|
||||
|
||||
_JAHYUNG_BRANCHES = ['辰', '午', '酉', '亥']
|
||||
|
||||
_PA_PAIRS = [
|
||||
('子', '酉'), ('丑', '辰'), ('寅', '亥'),
|
||||
('卯', '午'), ('巳', '申'), ('未', '戌'),
|
||||
]
|
||||
|
||||
_HAE_PAIRS = [
|
||||
('子', '未'), ('丑', '午'), ('寅', '巳'),
|
||||
('卯', '辰'), ('申', '亥'), ('酉', '戌'),
|
||||
]
|
||||
|
||||
_ELEM_KR = {'木': '목', '火': '화', '土': '토', '金': '금', '水': '수'}
|
||||
|
||||
|
||||
def analyze_branch_interactions(saju: dict) -> list[dict]:
|
||||
pillar_branches = [
|
||||
{'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr'], 'pillar': '년주'},
|
||||
{'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr'], 'pillar': '월주'},
|
||||
{'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr'], 'pillar': '일주'},
|
||||
]
|
||||
if saju.get('hour'):
|
||||
pillar_branches.append({'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr'], 'pillar': '시주'})
|
||||
|
||||
branches = [p['branch'] for p in pillar_branches]
|
||||
interactions = []
|
||||
|
||||
# 육합
|
||||
for a, b, elem in _YUKAP_PAIRS:
|
||||
if a in branches and b in branches:
|
||||
idx_a = branches.index(a)
|
||||
idx_b = branches.index(b)
|
||||
interactions.append({
|
||||
'type': '육합(六合)',
|
||||
'branches': [a, b],
|
||||
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
|
||||
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
|
||||
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 육합 → {_ELEM_KR.get(elem, '')}({elem}) 기운 생성. 조화와 화합의 관계.",
|
||||
'resultElement': 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
|
||||
interactions.append({
|
||||
'type': '삼합(三合)' if is_complete else '반삼합(半三合)',
|
||||
'branches': found,
|
||||
'branchesKr': [p['branchKr'] for p in found_pillars],
|
||||
'pillars': [p['pillar'] for p in found_pillars],
|
||||
'description': f"{''.join(p['branchKr'] for p in found_pillars)} {'삼합' if is_complete else '반삼합'} → {_ELEM_KR.get(elem, '')}({elem})국.",
|
||||
'resultElement': 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]
|
||||
interactions.append({
|
||||
'type': '방합(方合)',
|
||||
'branches': found,
|
||||
'branchesKr': [p['branchKr'] for p in found_pillars],
|
||||
'pillars': [p['pillar'] for p in found_pillars],
|
||||
'description': f"{''.join(p['branchKr'] for p in found_pillars)} 방합 → {_ELEM_KR.get(elem, '')}({elem}) 방국. 매우 강한 오행 기운.",
|
||||
'resultElement': elem,
|
||||
})
|
||||
|
||||
# 충
|
||||
for a, b in _CHUNG_PAIRS:
|
||||
if a in branches and b in branches:
|
||||
idx_a = branches.index(a)
|
||||
idx_b = branches.index(b)
|
||||
interactions.append({
|
||||
'type': '충(沖)',
|
||||
'branches': [a, b],
|
||||
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
|
||||
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
|
||||
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 충 → 변동, 갈등, 변화의 에너지.",
|
||||
})
|
||||
|
||||
# 형
|
||||
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]
|
||||
interactions.append({
|
||||
'type': '형(刑)',
|
||||
'branches': found,
|
||||
'branchesKr': [p['branchKr'] for p in found_pillars],
|
||||
'pillars': [p['pillar'] for p in found_pillars],
|
||||
'description': f"{''.join(p['branchKr'] for p in found_pillars)} {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],
|
||||
'branchesKr': [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:
|
||||
idx_a = branches.index(a)
|
||||
idx_b = branches.index(b)
|
||||
interactions.append({
|
||||
'type': '파(破)',
|
||||
'branches': [a, b],
|
||||
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
|
||||
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
|
||||
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 파 → 관계의 균열, 계획의 차질 가능성.",
|
||||
})
|
||||
|
||||
# 해
|
||||
for a, b in _HAE_PAIRS:
|
||||
if a in branches and b in branches:
|
||||
idx_a = branches.index(a)
|
||||
idx_b = branches.index(b)
|
||||
interactions.append({
|
||||
'type': '해(害)',
|
||||
'branches': [a, b],
|
||||
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
|
||||
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
|
||||
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 해 → 은근한 방해, 원망의 기운.",
|
||||
})
|
||||
|
||||
return interactions
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 신살 (神煞)
|
||||
# ============================================================
|
||||
|
||||
_SAMHAP_GROUP_MAP: dict[str, str] = {
|
||||
'申': '申子辰', '子': '申子辰', '辰': '申子辰',
|
||||
'寅': '寅午戌', '午': '寅午戌', '戌': '寅午戌',
|
||||
'巳': '巳酉丑', '酉': '巳酉丑', '丑': '巳酉丑',
|
||||
'亥': '亥卯未', '卯': '亥卯未', '未': '亥卯未',
|
||||
}
|
||||
|
||||
_YEOKMA_MAP = {'申子辰': '寅', '寅午戌': '申', '巳酉丑': '亥', '亥卯未': '巳'}
|
||||
_DOHWA_MAP = {'申子辰': '酉', '寅午戌': '卯', '巳酉丑': '午', '亥卯未': '子'}
|
||||
_HWAGAE_MAP = {'申子辰': '辰', '寅午戌': '戌', '巳酉丑': '丑', '亥卯未': '未'}
|
||||
|
||||
_CHEONUL_MAP: dict[str, list[str]] = {
|
||||
'甲': ['丑', '未'], '乙': ['子', '申'], '丙': ['亥', '酉'], '丁': ['亥', '酉'],
|
||||
'戊': ['丑', '未'], '己': ['子', '申'], '庚': ['丑', '未'], '辛': ['寅', '午'],
|
||||
'壬': ['卯', '巳'], '癸': ['卯', '巳'],
|
||||
}
|
||||
|
||||
_MUNCHANG_MAP: dict[str, str] = {
|
||||
'甲': '巳', '乙': '午', '丙': '申', '丁': '酉',
|
||||
'戊': '申', '己': '酉', '庚': '亥', '辛': '子',
|
||||
'壬': '寅', '癸': '卯',
|
||||
}
|
||||
|
||||
_CHEONDUK_MAP: dict[str, str] = {
|
||||
'寅': '丁', '卯': '申', '辰': '壬', '巳': '辛',
|
||||
'午': '亥', '未': '甲', '申': '癸', '酉': '寅',
|
||||
'戌': '丙', '亥': '乙', '子': '巳', '丑': '庚',
|
||||
}
|
||||
|
||||
|
||||
def calculate_shinsal(saju: dict) -> list[dict]:
|
||||
result = []
|
||||
day_branch = saju['day']['branch']
|
||||
day_stem = saju['dayStem']
|
||||
month_branch = saju['month']['branch']
|
||||
|
||||
pillar_branches = [
|
||||
{'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr'], 'pillar': '년주'},
|
||||
{'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr'], 'pillar': '월주'},
|
||||
{'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr'], 'pillar': '일주'},
|
||||
]
|
||||
if saju.get('hour'):
|
||||
pillar_branches.append({'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr'], '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': '역마살', 'nameHanja': '驛馬殺',
|
||||
'branch': yeokma, 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
|
||||
'description': '이동, 변동, 해외, 출장이 많은 기운. 활동적이고 한 곳에 머물지 못하는 성향.',
|
||||
})
|
||||
|
||||
# 도화살
|
||||
dohwa = _DOHWA_MAP[group]
|
||||
for pb in pillar_branches:
|
||||
if pb['branch'] == dohwa and pb['pillar'] != '일주':
|
||||
result.append({
|
||||
'name': '도화살', 'nameHanja': '桃花殺',
|
||||
'branch': dohwa, 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
|
||||
'description': '매력, 인기, 예술적 감각. 이성에게 끌리는 기운이 강하며 대인관계가 화려함.',
|
||||
})
|
||||
|
||||
# 화개살
|
||||
hwagae = _HWAGAE_MAP[group]
|
||||
for pb in pillar_branches:
|
||||
if pb['branch'] == hwagae and pb['pillar'] != '일주':
|
||||
result.append({
|
||||
'name': '화개살', 'nameHanja': '華蓋殺',
|
||||
'branch': hwagae, 'branchKr': pb['branchKr'], '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': '천을귀인', 'nameHanja': '天乙貴人',
|
||||
'branch': pb['branch'], 'branchKr': pb['branchKr'], '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': '문창귀인', 'nameHanja': '文昌貴人',
|
||||
'branch': pb['branch'], 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
|
||||
'description': '학문, 시험, 문서에 유리한 기운. 공부를 잘하며 시험운이 좋음.',
|
||||
})
|
||||
|
||||
# 천덕귀인 (월지 기준, 천간에서 확인)
|
||||
cheonduk_stem = _CHEONDUK_MAP.get(month_branch)
|
||||
if cheonduk_stem:
|
||||
all_stems = [
|
||||
{'stem': saju['year']['stem'], 'pillar': '년주'},
|
||||
{'stem': saju['day']['stem'], 'pillar': '일주'},
|
||||
]
|
||||
if saju.get('hour'):
|
||||
all_stems.append({'stem': saju['hour']['stem'], 'pillar': '시주'})
|
||||
for ps in all_stems:
|
||||
if ps['stem'] == cheonduk_stem:
|
||||
result.append({
|
||||
'name': '천덕귀인', 'nameHanja': '天德貴人',
|
||||
'branch': month_branch,
|
||||
'branchKr': EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.index(month_branch)],
|
||||
'pillar': ps['pillar'],
|
||||
'description': '하늘의 덕을 받는 기운. 재난을 피하고 복을 받는 길신 중의 길신.',
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 공망 (空亡)
|
||||
# ============================================================
|
||||
|
||||
def calculate_gongmang(day_stem: str, day_branch: str) -> dict:
|
||||
stem_idx = HEAVENLY_STEMS.index(day_stem)
|
||||
branch_idx = EARTHLY_BRANCHES.index(day_branch)
|
||||
|
||||
start_branch_idx = (branch_idx - stem_idx + 120) % 12
|
||||
gm1 = (start_branch_idx + 10) % 12
|
||||
gm2 = (start_branch_idx + 11) % 12
|
||||
|
||||
branch1 = EARTHLY_BRANCHES[gm1]
|
||||
branch2 = EARTHLY_BRANCHES[gm2]
|
||||
br_kr1 = EARTHLY_BRANCHES_KR[gm1]
|
||||
br_kr2 = EARTHLY_BRANCHES_KR[gm2]
|
||||
|
||||
return {
|
||||
'branches': [branch1, branch2],
|
||||
'branchesKr': [br_kr1, br_kr2],
|
||||
'description': f'{br_kr1}({branch1})·{br_kr2}({branch2}) 공망 → 해당 지지의 기운이 비어있어 허무하거나 집착이 없는 영역. 오히려 초월적 능력이 될 수 있음.',
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 사주팔자 전체 계산
|
||||
# ============================================================
|
||||
|
||||
def calculate_saju(
|
||||
year: int,
|
||||
month: int,
|
||||
day: int,
|
||||
hour: Optional[int],
|
||||
gender: str,
|
||||
) -> dict:
|
||||
year_ganzi = get_year_ganzi(year)
|
||||
month_ganzi = get_month_ganzi(year, month, day)
|
||||
day_ganzi = get_day_ganzi(year, month, day)
|
||||
hour_ganzi = get_hour_ganzi(day_ganzi['stem'], hour) if hour is not None else None
|
||||
|
||||
day_stem = day_ganzi['stem']
|
||||
day_stem_idx = HEAVENLY_STEMS.index(day_stem)
|
||||
is_day_yang = day_stem_idx % 2 == 0
|
||||
|
||||
def enrich(ganzi: dict, is_day_pillar: bool = False) -> dict:
|
||||
stem = ganzi['stem']
|
||||
branch = ganzi['branch']
|
||||
stem_idx = HEAVENLY_STEMS.index(stem)
|
||||
is_yang = (stem_idx % 2 == 0) == is_day_yang
|
||||
return {
|
||||
**ganzi,
|
||||
'element': FIVE_ELEMENTS.get(stem, ''),
|
||||
'tenGod': '일간' if is_day_pillar else get_ten_god(day_stem, stem, is_yang),
|
||||
'fortune': get_twelve_fortune(day_stem, branch),
|
||||
}
|
||||
|
||||
saju: dict = {
|
||||
'year': enrich(year_ganzi),
|
||||
'month': enrich(month_ganzi),
|
||||
'day': enrich(day_ganzi, is_day_pillar=True),
|
||||
'dayStem': day_stem,
|
||||
'birthDate': {'year': year, 'month': month, 'day': day, 'hour': hour},
|
||||
'gender': gender,
|
||||
}
|
||||
|
||||
if hour_ganzi:
|
||||
saju['hour'] = enrich(hour_ganzi)
|
||||
|
||||
return saju
|
||||
191
saju-engine/calculator/solar_terms.py
Normal file
191
saju-engine/calculator/solar_terms.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
24절기 계산 모듈
|
||||
ephem 라이브러리를 사용한 정밀한 절기 날짜 계산
|
||||
"""
|
||||
|
||||
import ephem
|
||||
import math
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Optional
|
||||
|
||||
# 24절기 이름 (한글)
|
||||
SOLAR_TERMS = [
|
||||
'입춘', '우수', '경칩', '춘분', '청명', '곡우',
|
||||
'입하', '소만', '망종', '하지', '소서', '대서',
|
||||
'입추', '처서', '백로', '추분', '한로', '상강',
|
||||
'입동', '소설', '대설', '동지', '소한', '대한'
|
||||
]
|
||||
|
||||
# 각 절기에 대응하는 태양황경 (도)
|
||||
SOLAR_TERM_ANGLES = [
|
||||
315, 330, 345, 0, 15, 30,
|
||||
45, 60, 75, 90, 105, 120,
|
||||
135, 150, 165, 180, 195, 210,
|
||||
225, 240, 255, 270, 285, 300
|
||||
]
|
||||
|
||||
# 절기별 대략적인 월
|
||||
SOLAR_TERM_BASE_MONTHS = [
|
||||
2, 2, 3, 3, 4, 4,
|
||||
5, 5, 6, 6, 7, 7,
|
||||
8, 8, 9, 9, 10, 10,
|
||||
11, 11, 12, 12, 1, 1
|
||||
]
|
||||
|
||||
# 절기별 대략적인 일
|
||||
SOLAR_TERM_BASE_DAYS = [
|
||||
4, 19, 5, 20, 4, 20,
|
||||
5, 21, 6, 21, 7, 23,
|
||||
7, 23, 8, 23, 8, 23,
|
||||
7, 22, 7, 22, 5, 20
|
||||
]
|
||||
|
||||
# 절기 → 월지지 인덱스 매핑
|
||||
# 입춘(0) → 인월(2), 우수(1) → 인월(2), ...
|
||||
TERM_TO_MONTH_BRANCH = [
|
||||
2, # 입춘 → 인월
|
||||
2, # 우수 → 인월
|
||||
3, # 경칩 → 묘월
|
||||
3, # 춘분 → 묘월
|
||||
4, # 청명 → 진월
|
||||
4, # 곡우 → 진월
|
||||
5, # 입하 → 사월
|
||||
5, # 소만 → 사월
|
||||
6, # 망종 → 오월
|
||||
6, # 하지 → 오월
|
||||
7, # 소서 → 미월
|
||||
7, # 대서 → 미월
|
||||
8, # 입추 → 신월
|
||||
8, # 처서 → 신월
|
||||
9, # 백로 → 유월
|
||||
9, # 추분 → 유월
|
||||
10, # 한로 → 술월
|
||||
10, # 상강 → 술월
|
||||
11, # 입동 → 해월
|
||||
11, # 소설 → 해월
|
||||
0, # 대설 → 자월
|
||||
0, # 동지 → 자월
|
||||
1, # 소한 → 축월
|
||||
1, # 대한 → 축월
|
||||
]
|
||||
|
||||
|
||||
def _get_solar_longitude(dt: datetime) -> float:
|
||||
"""주어진 날짜시간의 태양황경 계산 (ephem 사용)"""
|
||||
sun = ephem.Sun()
|
||||
sun.compute(dt.strftime('%Y/%m/%d %H:%M:%S'))
|
||||
ecl = ephem.Ecliptic(sun)
|
||||
return math.degrees(ecl.lon) % 360
|
||||
|
||||
|
||||
def _get_solar_term_date_ephem(year: int, term_index: int) -> Optional[date]:
|
||||
"""ephem을 사용해 특정 절기 날짜 계산"""
|
||||
target_angle = SOLAR_TERM_ANGLES[term_index]
|
||||
base_month = SOLAR_TERM_BASE_MONTHS[term_index]
|
||||
base_day = SOLAR_TERM_BASE_DAYS[term_index]
|
||||
|
||||
# 소한(22), 대한(23)은 1월이지만 기준 년도에서 검색
|
||||
search_year = year if term_index < 22 else year
|
||||
|
||||
try:
|
||||
start_day = max(1, base_day - 5)
|
||||
start_dt = datetime(search_year, base_month, start_day)
|
||||
except ValueError:
|
||||
start_dt = datetime(search_year, base_month, 1)
|
||||
|
||||
# 20일 범위에서 절기 날짜 탐색
|
||||
prev_diff = None
|
||||
for i in range(20):
|
||||
check_dt = start_dt + timedelta(days=i)
|
||||
lon = _get_solar_longitude(check_dt)
|
||||
|
||||
# 황경 차이 계산 (0° 교차 처리)
|
||||
diff = (lon - target_angle + 360) % 360
|
||||
if diff > 180:
|
||||
diff -= 360
|
||||
|
||||
if abs(diff) < 2.0:
|
||||
return check_dt.date()
|
||||
|
||||
# 부호가 바뀌면 직전 날짜가 절기
|
||||
if prev_diff is not None and prev_diff * diff < 0:
|
||||
return (check_dt - timedelta(days=1)).date()
|
||||
|
||||
prev_diff = diff
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_solar_term_date(year: int, term_index: int) -> date:
|
||||
"""특정 년도의 특정 절기 날짜 반환"""
|
||||
try:
|
||||
result = _get_solar_term_date_ephem(year, term_index)
|
||||
if result:
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 폴백: 근사값 사용
|
||||
base_month = SOLAR_TERM_BASE_MONTHS[term_index]
|
||||
base_day = SOLAR_TERM_BASE_DAYS[term_index]
|
||||
try:
|
||||
return date(year, base_month, base_day)
|
||||
except ValueError:
|
||||
return date(year, base_month, min(28, base_day))
|
||||
|
||||
|
||||
def get_current_solar_term(year: int, month: int, day: int) -> int:
|
||||
"""주어진 날짜가 어느 절기 이후인지 반환 (0~23)"""
|
||||
target = date(year, month, day)
|
||||
|
||||
# 역순으로 확인 (가장 최근 절기 찾기)
|
||||
for i in range(23, -1, -1):
|
||||
term_date = get_solar_term_date(year, i)
|
||||
|
||||
# 소한, 대한의 경우 년도 조정
|
||||
if i >= 22:
|
||||
if month >= 2:
|
||||
term_date = date(year, term_date.month, term_date.day)
|
||||
else:
|
||||
term_date = date(year - 1, term_date.month, term_date.day)
|
||||
|
||||
if target >= term_date:
|
||||
return i
|
||||
|
||||
return 23 # 입춘 이전 → 전년도 대한 이후
|
||||
|
||||
|
||||
def get_solar_term_month_branch(year: int, month: int, day: int) -> int:
|
||||
"""절기 기준 월주 지지 인덱스 계산 (0=자, 1=축, 2=인, ...)"""
|
||||
term_index = get_current_solar_term(year, month, day)
|
||||
return TERM_TO_MONTH_BRANCH[term_index]
|
||||
|
||||
|
||||
def get_days_to_next_solar_term(year: int, month: int, day: int) -> int:
|
||||
"""다음 절기까지 남은 일수 계산"""
|
||||
current_term = get_current_solar_term(year, month, day)
|
||||
next_term_index = (current_term + 1) % 24
|
||||
|
||||
next_year = year + 1 if current_term == 23 else year
|
||||
next_term_date = get_solar_term_date(next_year, next_term_index)
|
||||
|
||||
current_date = date(year, month, day)
|
||||
diff = (next_term_date - current_date).days
|
||||
return max(1, diff)
|
||||
|
||||
|
||||
def get_days_from_prev_solar_term(year: int, month: int, day: int) -> int:
|
||||
"""이전 절기부터 주어진 날짜까지의 일수 계산"""
|
||||
current_term = get_current_solar_term(year, month, day)
|
||||
term_date = get_solar_term_date(year, current_term)
|
||||
|
||||
# 소한, 대한 년도 조정
|
||||
if current_term >= 22:
|
||||
if month >= 2:
|
||||
term_date = date(year, term_date.month, term_date.day)
|
||||
else:
|
||||
term_date = date(year - 1, term_date.month, term_date.day)
|
||||
|
||||
target = date(year, month, day)
|
||||
diff = (target - term_date).days
|
||||
return max(1, diff)
|
||||
Reference in New Issue
Block a user