diff --git a/app/saju/result/page.tsx b/app/saju/result/page.tsx index 0d11310..53c61bc 100644 --- a/app/saju/result/page.tsx +++ b/app/saju/result/page.tsx @@ -22,19 +22,55 @@ interface PageProps { }>; } +// Python 사주 엔진 호출 (실패 시 null 반환) +async function fetchFromPythonEngine( + year: number, month: number, day: number, + hour: number | null, gender: string +): Promise<{ + saju: any; daeunList: any[]; currentDaeun: any; + interactions: any[]; shinsal: any[]; gongmang: any; hiddenStems: any[]; +} | null> { + const url = process.env.SAJU_ENGINE_URL; + const secret = process.env.SAJU_ENGINE_SECRET; + if (!url) return null; + + try { + const res = await fetch(`${url}/saju/calculate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(secret ? { 'X-API-Secret': secret } : {}), + }, + body: JSON.stringify({ year, month, day, hour: hour ?? undefined, gender, calendar_type: 'solar' }), + signal: AbortSignal.timeout(10000), + cache: 'no-store', + }); + if (!res.ok) return null; + const data = await res.json(); + return { + saju: data.saju, + daeunList: data.daeunList, + currentDaeun: data.currentDaeun, + interactions: data.interactions, + shinsal: data.shinsal, + gongmang: data.gongmang, + hiddenStems: data.hiddenStems, + }; + } catch { + console.warn('[사주] Python 엔진 연결 실패 — TypeScript 폴백 사용'); + return null; + } +} + export default async function SajuResultPage({ searchParams }: PageProps) { const params = await searchParams; - const { - year, month, day, hour, gender, calendarType, - originalYear, originalMonth, originalDay, isLeapMonth - } = params; + const { year, month, day, hour, gender, calendarType, originalYear, originalMonth, originalDay, isLeapMonth } = params; const yearNum = parseInt(year, 10); const monthNum = parseInt(month, 10); const dayNum = parseInt(day, 10); const hourNum = hour ? parseInt(hour, 10) : null; - // 필수 파라미터 누락 시 안전한 기본값 (NaN 방지) if (isNaN(yearNum) || isNaN(monthNum) || isNaN(dayNum)) { return (
@@ -52,9 +88,35 @@ export default async function SajuResultPage({ searchParams }: PageProps) { const isLunar = calendarType === 'lunar'; const isLeap = isLeapMonth === 'true'; - const sajuData = calculateSaju(yearNum, monthNum, dayNum, hourNum, gender); + // ── Python 엔진 호출 (폴백: TypeScript) ────────────────────────────── + const engineResult = await fetchFromPythonEngine(yearNum, monthNum, dayNum, hourNum, gender); - // 결제 여부 + 저장된 AI 해석 확인 (서버사이드) + // 사주팔자 (Python 엔진 우선, TS 폴백) + const sajuData = engineResult?.saju ?? calculateSaju(yearNum, monthNum, dayNum, hourNum, gender); + + // 추가 분석 (신강신약, 용신, 오행균형, 세운) — TypeScript 계산 유지 + const analysis = performFullAnalysis(sajuData); + const elementScores = analysis.elementScores; + + // 대운 (Python 엔진 우선, TS 폴백) + const currentYear = new Date().getFullYear(); + const daeunList = engineResult?.daeunList ?? calculateDaeun( + yearNum, monthNum, dayNum, gender, + sajuData.month.stem, sajuData.month.branch + ); + const currentDaeun = engineResult?.currentDaeun ?? getCurrentDaeun(daeunList, currentYear); + + // 지지 상호작용 / 신살 / 공망 / 지장간 (Python 엔진 우선, TS 폴백) + const branchInteractions = engineResult?.interactions ?? analysis.branchInteractions; + const shinsal = engineResult?.shinsal ?? analysis.shinsal; + const gongmang = engineResult?.gongmang ?? analysis.gongmang; + const hiddenStems = engineResult?.hiddenStems ?? analysis.hiddenStems; + + // ── 절기 정보 (표시용) ──────────────────────────────────────────────── + const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum); + const solarTermName = getSolarTermName(solarTermIndex); + + // ── 결제 여부 + 저장된 AI 해석 ──────────────────────────────────────── let hasPaid = false; let savedInterpretation: string | null = null; try { @@ -62,11 +124,8 @@ export default async function SajuResultPage({ searchParams }: PageProps) { const { data: { user } } = await supabase.auth.getUser(); if (user) { const { data: order } = await supabase - .from('orders') - .select('id') - .eq('user_id', user.id) - .eq('product_id', 'saju_detail') - .eq('status', 'paid') + .from('orders').select('id') + .eq('user_id', user.id).eq('product_id', 'saju_detail').eq('status', 'paid') .maybeSingle(); hasPaid = !!order; @@ -74,52 +133,35 @@ export default async function SajuResultPage({ searchParams }: PageProps) { const birthKey: Record = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender }; if (hourNum !== null) birthKey.birth_hour = hourNum; const { data: record } = await supabase - .from('saju_records') - .select('interpretation') - .eq('user_id', user.id) - .eq('is_paid', true) - .contains('saju_data', birthKey) - .maybeSingle(); + .from('saju_records').select('interpretation') + .eq('user_id', user.id).eq('is_paid', true) + .contains('saju_data', birthKey).maybeSingle(); savedInterpretation = record?.interpretation ?? null; } } } catch { - // 인증 오류 시 무시 (미로그인) + // 미로그인 시 무시 } - // 절기 정보 - const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum); - const solarTermName = getSolarTermName(solarTermIndex); - const monthBranchIndex = getSolarTermMonthBranch(yearNum, monthNum, dayNum); - const monthBranchName = EARTHLY_BRANCHES_KR[monthBranchIndex]; - - // 종합 분석 수행 - const analysis = performFullAnalysis(sajuData); - const elementScores = analysis.elementScores; - - // 대운 계산 - const daeunList = calculateDaeun( - yearNum, monthNum, dayNum, gender, - sajuData.month.stem, sajuData.month.branch - ); - const currentYear = new Date().getFullYear(); - const currentDaeun = getCurrentDaeun(daeunList, currentYear); - - // 오행 색상 매핑 - const elementColors: { [key: string]: string } = { + // ── 오행 색상 ────────────────────────────────────────────────────────── + const elementColors: { [k: string]: string } = { '木': 'text-green-700', '火': 'text-red-600', '土': 'text-yellow-700', '金': 'text-amber-600', '水': 'text-blue-700', }; - const elementBgColors: { [key: string]: string } = { + const elementBgColors: { [k: string]: string } = { '木': 'bg-green-50 border-green-400', '火': 'bg-red-50 border-red-400', '土': 'bg-yellow-50 border-yellow-400', '金': 'bg-amber-50 border-amber-400', '水': 'bg-blue-50 border-blue-400', }; - // 띠 계산 + // ── 띠 계산 ──────────────────────────────────────────────────────────── const zodiacAnimals = ['쥐', '소', '호랑이', '토끼', '용', '뱀', '말', '양', '원숭이', '닭', '개', '돼지']; - const zodiacIndex = (yearNum - 4) % 12; - const zodiacAnimal = zodiacAnimals[zodiacIndex >= 0 ? zodiacIndex : zodiacIndex + 12]; + const zodiacIdx = (yearNum - 4) % 12; + const zodiacAnimal = zodiacAnimals[zodiacIdx >= 0 ? zodiacIdx : zodiacIdx + 12]; + + const engineBadge = engineResult + ? Python 엔진 + : TS 폴백; return (
@@ -138,12 +180,10 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
- {/* 사이드바 - 기본 정보 */} + {/* 사이드바 */} - {/* 메인 콘텐츠 */} + {/* 메인 */}
{/* 사주팔자 표 */} @@ -273,26 +317,27 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
숨은 천간
{(() => { - const pillars = sajuData.hour - ? [analysis.hiddenStems.find(h => h.pillar === '시주'), analysis.hiddenStems.find(h => h.pillar === '일주'), analysis.hiddenStems.find(h => h.pillar === '월주'), analysis.hiddenStems.find(h => h.pillar === '년주')] - : [analysis.hiddenStems.find(h => h.pillar === '일주'), analysis.hiddenStems.find(h => h.pillar === '월주'), analysis.hiddenStems.find(h => h.pillar === '년주')]; - return pillars.map((h, idx) => ( - - {h && ( -
- {h.stems.map((s, si) => ( - - {s.stemKr} - - ))} -
- )} - - )); + const order = sajuData.hour + ? ['시주', '일주', '월주', '년주'] + : ['일주', '월주', '년주']; + return order.map((pillarName, idx) => { + const h = hiddenStems.find((hs: any) => hs.pillar === pillarName); + return ( + + {h && ( +
+ {h.stems.map((s: any, si: number) => ( + + {s.stemKr} + + ))} +
+ )} + + ); + }); })()} @@ -338,11 +383,11 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
{/* 지지 상호작용 */} - {analysis.branchInteractions.length > 0 && ( + {branchInteractions.length > 0 && (

지지 상호작용

- {analysis.branchInteractions.map((inter, idx) => { + {branchInteractions.map((inter: any, idx: number) => { const isPositive = inter.type.includes('합'); const isNegative = inter.type.includes('충') || inter.type.includes('형'); const colorClass = isPositive @@ -351,7 +396,8 @@ export default async function SajuResultPage({ searchParams }: PageProps) { ? 'bg-red-50 border-red-400 text-red-800' : 'bg-amber-50 border-amber-400 text-amber-800'; return ( - + {inter.type} {inter.branchesKr.join('')} {inter.resultElement && ` → ${FIVE_ELEMENTS_KR[inter.resultElement as keyof typeof FIVE_ELEMENTS_KR]}`} @@ -389,6 +435,7 @@ export default async function SajuResultPage({ searchParams }: PageProps) { {/* 분석 카드 그리드 */}
+ {/* 신강/신약 + 용신 */}

일간 세력 분석

@@ -405,7 +452,7 @@ export default async function SajuResultPage({ searchParams }: PageProps) { 점수: {analysis.dayMasterStrength.score}
    - {analysis.dayMasterStrength.reasons.map((r, i) => ( + {analysis.dayMasterStrength.reasons.map((r: string, i: number) => (
  • - {r} @@ -433,9 +480,9 @@ export default async function SajuResultPage({ searchParams }: PageProps) { {/* 신살 + 공망 */}

    신살 (神煞)

    - {analysis.shinsal.length > 0 ? ( + {shinsal.length > 0 ? (
    - {analysis.shinsal.map((s, i) => ( + {shinsal.map((s: any, i: number) => (
    {s.name} @@ -456,13 +503,13 @@ export default async function SajuResultPage({ searchParams }: PageProps) {

    공망 (空亡)

    - {analysis.gongmang.branchesKr.map((bk, i) => ( + {gongmang.branchesKr.map((bk: string, i: number) => ( {bk} ))}
    -

    {analysis.gongmang.description}

    +

    {gongmang.description}

    {/* 세운 정보 */} @@ -478,7 +525,7 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
    {analysis.seun.interactions.length > 0 && (
    - {analysis.seun.interactions.map((si, i) => ( + {analysis.seun.interactions.map((si: any, i: number) => ( @@ -493,7 +540,10 @@ export default async function SajuResultPage({ searchParams }: PageProps) { {/* AI 상세 해석 섹션 */} {(() => { - const birthKey = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender, ...(hourNum !== null ? { birth_hour: hourNum } : {}) }; + const birthKey = { + birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender, + ...(hourNum !== null ? { birth_hour: hourNum } : {}), + }; const currentUrl = `/saju/result?year=${yearNum}&month=${monthNum}&day=${dayNum}${hourNum !== null ? `&hour=${hourNum}` : ''}&gender=${gender}&calendarType=${calendarType}${originalYear ? `&originalYear=${originalYear}&originalMonth=${originalMonth}&originalDay=${originalDay}` : ''}${isLeap ? '&isLeapMonth=true' : ''}`; return ( - {daeunList.map((daeun, index) => { + {daeunList.map((daeun: any, index: number) => { const isCurrent = currentDaeun && daeun.startYear === currentDaeun.startYear && daeun.endYear === currentDaeun.endYear; - return ( -
    +
    -
    - {daeun.stem}{daeun.branch} -
    -
    - {daeun.stemKr}{daeun.branchKr} -
    -
    - {daeun.age}세 ~ {daeun.age + 9}세 -
    -
    - {daeun.startYear} ~ {daeun.endYear} -
    +
    {daeun.stem}{daeun.branch}
    +
    {daeun.stemKr}{daeun.branchKr}
    +
    {daeun.age}세 ~ {daeun.age + 9}세
    +
    {daeun.startYear} ~ {daeun.endYear}
    {isCurrent && (
    - - 현재 - + 현재
    )}
    diff --git a/saju-engine/main.py b/saju-engine/main.py index a137918..df01d53 100644 --- a/saju-engine/main.py +++ b/saju-engine/main.py @@ -154,13 +154,16 @@ async def calculate_saju_api(request: Request, body: SajuRequest): year, month, day = body.year, body.month, body.day if body.calendar_type == 'lunar': try: - import korean_lunar_calendar - calendar = korean_lunar_calendar.KoreanLunarCalendar() - calendar.setLunarDate(year, month, day, False) - solar = calendar.SolarIsoFormat().split('-') - year, month, day = int(solar[0]), int(solar[1]), int(solar[2]) + from korean_lunar_calendar import KoreanLunarCalendar + cal = KoreanLunarCalendar() + cal.setLunarDate(year, month, day, False) + solar_str = cal.SolarIsoFormat() # 'YYYY-MM-DD' + parts = solar_str.split('-') + year, month, day = int(parts[0]), int(parts[1]), int(parts[2]) + logger.info(f'음력 변환 완료: 음력 {body.year}/{body.month}/{body.day} → 양력 {year}/{month}/{day}') except Exception as e: logger.warning(f'음력 변환 실패, 양력으로 처리: {e}') + raise HTTPException(status_code=400, detail=f'음력 변환 실패: {e}') # 사주팔자 계산 saju = calculate_saju(year, month, day, body.hour, body.gender) diff --git a/saju-engine/requirements.txt b/saju-engine/requirements.txt index 04c8cf8..34d97d9 100644 --- a/saju-engine/requirements.txt +++ b/saju-engine/requirements.txt @@ -4,3 +4,4 @@ ephem==4.1.6 slowapi==0.1.9 python-dotenv==1.0.1 pydantic==2.10.3 +korean-lunar-calendar==0.3.1