import { calculateSaju } from '@/lib/saju-calculator'; import Link from 'next/link'; import { calculateDaeun, getCurrentDaeun, getDaeunDescription } from '@/lib/daeun-calculator'; import { getCurrentSolarTerm, getSolarTermName, getSolarTermMonthBranch } from '@/lib/solar-terms'; import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju-calculator'; import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation'; import { createClient } from '@/lib/supabase/server'; import SajuAISection from './SajuAISection'; import SajuLottoSection from './SajuLottoSection'; interface PageProps { searchParams: Promise<{ year: string; month: string; day: string; hour?: string; gender: 'male' | 'female'; calendarType: 'solar' | 'lunar'; originalYear?: string; originalMonth?: string; originalDay?: string; isLeapMonth?: string; }>; } // 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 yearNum = parseInt(year, 10); const monthNum = parseInt(month, 10); const dayNum = parseInt(day, 10); const hourNum = hour ? parseInt(hour, 10) : null; if (isNaN(yearNum) || isNaN(monthNum) || isNaN(dayNum)) { return (

잘못된 접근입니다. 생년월일을 다시 입력해주세요.

사주 입력하기
); } const inputYear = originalYear ? parseInt(originalYear) : yearNum; const inputMonth = originalMonth ? parseInt(originalMonth) : monthNum; const inputDay = originalDay ? parseInt(originalDay) : dayNum; const isLunar = calendarType === 'lunar'; const isLeap = isLeapMonth === 'true'; // ── Python 엔진 호출 (폴백: TypeScript) ────────────────────────────── const engineResult = await fetchFromPythonEngine(yearNum, monthNum, dayNum, hourNum, gender); // 사주팔자 (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; let hasLottoSubscription = false; try { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (user) { // 사주 결제 확인 (anon client — 본인 orders는 RLS 허용 가정) const { data: order } = await supabase .from('orders').select('id') .eq('user_id', user.id).eq('product_id', 'saju_detail').eq('status', 'paid') .maybeSingle(); hasPaid = !!order; if (hasPaid) { 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(); savedInterpretation = record?.interpretation ?? null; } // 로또 구독 확인 — subscriptions 테이블 (세션 클라이언트로 RLS select_own 통과) const { data: lottoSub } = await supabase .from('subscriptions') .select('id') .eq('user_id', user.id) .eq('status', 'active') .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) .maybeSingle(); hasLottoSubscription = !!lottoSub; // subscriptions에서 못 찾으면 orders 테이블로 폴백 (구독 마이그레이션 전 데이터) if (!hasLottoSubscription) { const now = new Date().toISOString(); const thirtyOneDaysAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString(); const { data: lottoOrder } = await supabase .from('orders') .select('id, created_at') .eq('user_id', user.id) .eq('status', 'paid') .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) .gte('created_at', thirtyOneDaysAgo) .maybeSingle(); hasLottoSubscription = !!lottoOrder; } } } catch { // 미로그인 시 무시 } // ── 오행 색상 ────────────────────────────────────────────────────────── const elementColors: { [k: string]: string } = { '木': 'text-green-700', '火': 'text-red-600', '土': 'text-yellow-700', '金': 'text-amber-600', '水': 'text-blue-700', }; 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 zodiacIdx = (yearNum - 4) % 12; const zodiacAnimal = zodiacAnimals[zodiacIdx >= 0 ? zodiacIdx : zodiacIdx + 12]; const engineBadge = engineResult ? Python 엔진 : TS 폴백; return (
{/* 헤더 */}
사주팔자 감정서

사주팔자 분석 결과

전통 명리학과 AI 기술의 만남

{/* 사이드바 */} {/* 메인 */}
{/* 사주팔자 표 */}

사주팔자 (四柱八字)

{sajuData.hour && } {/* 천간 */} {sajuData.hour && ( )} {/* 지지 */} {sajuData.hour && ( )} {/* 지장간 */} {(() => { const order = sajuData.hour ? ['시주', '일주', '월주', '년주'] : ['일주', '월주', '년주']; return order.map((pillarName, idx) => { const h = hiddenStems.find((hs: any) => hs.pillar === pillarName); return ( ); }); })()} {/* 십성 */} {sajuData.hour && ( )} {/* 십이운성 */} {sajuData.hour && ( )}
구분시주일주 월주 년주
천간
{sajuData.hour.stem}
{sajuData.hour.stemKr}
{sajuData.day.stem}
{sajuData.day.stemKr}
일간
{sajuData.month.stem}
{sajuData.month.stemKr}
{sajuData.year.stem}
{sajuData.year.stemKr}
지지
{sajuData.hour.branch}
{sajuData.hour.branchKr}
{sajuData.day.branch}
{sajuData.day.branchKr}
{sajuData.month.branch}
{sajuData.month.branchKr}
{sajuData.year.branch}
{sajuData.year.branchKr}
지장간
숨은 천간
{h && (
{h.stems.map((s: any, si: number) => ( {s.stemKr} ))}
)}
십성
{sajuData.hour.tenGod}
{sajuData.day.tenGod}
{sajuData.month.tenGod}
{sajuData.year.tenGod}
십이운성
{sajuData.hour.fortune}
{sajuData.day.fortune}
{sajuData.month.fortune}
{sajuData.year.fortune}
{/* 지지 상호작용 */} {branchInteractions.length > 0 && (

지지 상호작용

{branchInteractions.map((inter: any, idx: number) => { const isPositive = inter.type.includes('합'); const isNegative = inter.type.includes('충') || inter.type.includes('형'); const colorClass = isPositive ? 'bg-emerald-50 border-emerald-400 text-emerald-800' : isNegative ? '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]}`} ); })}
)} {/* 오행 균형 */}

오행 균형

{Object.entries(elementScores).map(([element, score]) => (
{element}
{FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR]}
{score}%
))}
{/* 분석 카드 그리드 */}
{/* 신강/신약 + 용신 */}

일간 세력 분석

{analysis.dayMasterStrength.result} 점수: {analysis.dayMasterStrength.score}
    {analysis.dayMasterStrength.reasons.map((r: string, i: number) => (
  • - {r}
  • ))}

용신 / 희신 / 기신

용신: {analysis.yongShin.yongShinKr} 희신: {analysis.yongShin.heeShinKr} 기신: {analysis.yongShin.giShinKr}

{analysis.yongShin.explanation}

{/* 신살 + 공망 */}

신살 (神煞)

{shinsal.length > 0 ? (
{shinsal.map((s: any, i: number) => (
{s.name}
{s.pillar} {s.branchKr}
{s.description}
))}
) : (

특별한 신살이 발견되지 않았습니다.

)}

공망 (空亡)

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

{gongmang.description}

{/* 세운 정보 */}

{analysis.seun.year}년 세운

{analysis.seun.stemKr}{analysis.seun.branchKr} {analysis.seun.elementKr} 기운
{analysis.seun.interactions.length > 0 && (
{analysis.seun.interactions.map((si: any, i: number) => ( {si.type} {si.branchesKr.join('')} ))}
)}
{/* AI 상세 해석 섹션 */} {(() => { 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 ( ); })()} {/* 사주 연동 로또 번호 추천 (사주 결제 시 표시) */} {hasPaid && ( )} {/* 대운 */}

대운 (大運) — 10년 주기 운세

{currentDaeun && (

현재 대운

{currentDaeun.stem}{currentDaeun.branch}
{currentDaeun.stemKr}{currentDaeun.branchKr}
{currentDaeun.age}세 ~ {currentDaeun.age + 9}세 ({currentDaeun.startYear} ~ {currentDaeun.endYear}년)

{getDaeunDescription(currentDaeun, sajuData.day.stem)}

)}
{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}
{isCurrent && (
현재
)}
); })}
); }