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