refactor: 사주 Python 엔진 제거 + lunar-javascript 기반 절기 계산 도입
- lib/solar-terms.ts: solarlunar → lunar-javascript로 전면 교체 - getSolarTermDate(): LunarYear.fromYear().getJieQiJulianDays() 사용 (시분 단위 정밀도) - 소한(22)/대한(23)은 year-1로 조회해 해당 연도 1월 날짜 정확히 반환 - getCurrentSolarTerm(): 입춘 기준 두 구간 분리, Date.UTC() 비교 - lib/daeun-calculator.ts: getSolarTermDate 정확도 향상으로 termYear 수동 보정 제거 - lib/saju-calculator.ts: 일주 기준일 甲戌, Date.UTC(), 오호둔월법 공식 적용 - lib/ai-interpretation.ts: 신약 용신 후보 내림차순 정렬 수정 - app/saju/result/page.tsx: Python 엔진(fetchFromPythonEngine) 완전 제거, TS 전용 - app/api/saju/calculate/route.ts: Python 프록시 라우트 삭제 - app/saju/page.tsx: fromHistory 파라미터 제거 - types/lunar-javascript.d.ts: 타입 선언 파일 추가 검증 케이스(1992-12-23 16:30 남성): 壬申/壬子/癸酉/庚申 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import { createSajuPrompt } from '@/lib/saju-ai-prompt';
|
||||
import { performFullAnalysis } from '@/lib/ai-interpretation';
|
||||
import { config as loadDotenv } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
// Vercel 최대 타임아웃 (Pro plan 300s, Hobby 60s)
|
||||
export const maxDuration = 60;
|
||||
|
||||
// Next.js가 env 로드를 놓치는 경우 대비해 직접 로드 (Windows 환경 대응)
|
||||
loadDotenv({ path: resolve(process.cwd(), '.env.local'), override: true });
|
||||
|
||||
const MOCK_INTERPRETATION = `
|
||||
## 1. 일간 분석과 타고난 기질
|
||||
(AI 해석 서비스를 이용하려면 API 설정이 필요합니다. 아래는 예시 데이터입니다.)
|
||||
(GEMINI_API_KEY 환경변수를 설정하고 서버를 재시작하면 실제 AI 해석을 받을 수 있습니다.)
|
||||
귀하는 **갑목(甲木)** 일간으로 태어나, 마치 곧게 뻗은 소나무와 같은 기상을 지니고 있다. 리더십이 강하고 추진력이 뛰어나며, 한번 마음먹은 일은 끝까지 해내는 뚝심이 있다.
|
||||
|
||||
## 2. 오행 균형과 용신 기반 개운법
|
||||
사주에서 **화(火)** 기운이 부족하여 표현력이 다소 약할 수 있다. 붉은색 계통의 옷이나 소품을 활용하고, 밝은 곳에서 활동하는 것이 운을 트이게 한다.
|
||||
사주에서 **화(火)** 기운이 부족하여 표현력이 다소 약할 수 있다.
|
||||
|
||||
## 3. 지지 상호작용 해석
|
||||
지지 간의 상호작용을 살펴보면, 특별한 합충형이 발견된다.
|
||||
@@ -21,22 +28,22 @@ const MOCK_INTERPRETATION = `
|
||||
역마살이 사주에 자리하고 있어 이동과 변동이 많은 삶을 살게 된다.
|
||||
|
||||
## 5. 재물운과 금전 흐름
|
||||
재물창고인 **진토(辰土)**를 깔고 있어 기본적으로 재복은 타고났다. 다만, 돈을 버는 것보다 지키는 힘이 약할 수 있으니 저축 습관이 중요하다.
|
||||
재물창고인 **진토(辰土)**를 깔고 있어 기본적으로 재복은 타고났다.
|
||||
|
||||
## 6. 직업 적성과 진로
|
||||
교육, 출판, 건축, 디자인 등 창조적이고 독립적인 분야에서 두각을 나타낼 수 있다.
|
||||
|
||||
## 7. 애정운과 결혼
|
||||
자존심이 강해 상대방에게 굽히지 않으려는 성향이 있다. 배우자와의 관계에서는 조금 더 부드러운 태도가 필요하다.
|
||||
자존심이 강해 상대방에게 굽히지 않으려는 성향이 있다.
|
||||
|
||||
## 8. 건강운
|
||||
간, 담낭, 신경계 통증에 유의해야 한다. 스트레스를 받으면 뭉치는 경향이 있으니 스트레칭과 요가를 추천한다.
|
||||
간, 담낭, 신경계 통증에 유의해야 한다.
|
||||
|
||||
## 9. 현재 대운의 흐름과 기회/위기
|
||||
현재 대운은 인생의 전환점이다. 새로운 것을 시작하기보다는 기존의 것을 다지고 내실을 기하는 시기이다.
|
||||
현재 대운은 인생의 전환점이다.
|
||||
|
||||
## 10. 올해의 세운 분석
|
||||
올해는 귀인의 도움을 받을 수 있는 해이다. 주저하지 말고 주변에 도움을 요청하라.
|
||||
올해는 귀인의 도움을 받을 수 있는 해이다.
|
||||
|
||||
## 11. 인생의 황금기 예측
|
||||
40대 중반부터 50대 초반까지 인생의 가장 화려한 시기를 맞이할 것으로 보인다.
|
||||
@@ -45,6 +52,16 @@ const MOCK_INTERPRETATION = `
|
||||
"서두르지 않아도 봄은 온다." 조급해하지 말고 때를 기다리는 지혜가 필요하다.
|
||||
`;
|
||||
|
||||
// 모델 우선순위 — 강력한 순서 (이 API 키로 접근 가능한 모델만)
|
||||
// gemini-2.5-pro: 최고 품질, 가장 강력한 추론력
|
||||
// gemini-2.5-flash: 빠르고 강력한 2순위
|
||||
// gemini-2.0-flash: 안정적인 폴백
|
||||
const MODELS = [
|
||||
{ id: 'gemini-2.5-pro', maxTokens: 8192 },
|
||||
{ id: 'gemini-2.5-flash', maxTokens: 8192 },
|
||||
{ id: 'gemini-2.0-flash', maxTokens: 8192 },
|
||||
] as const;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { saju, daeun, daeunList, gender, engineData } = await request.json();
|
||||
@@ -54,56 +71,77 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
analysis = performFullAnalysis(saju);
|
||||
} catch (analysisError: any) {
|
||||
console.error('Analysis calculation error:', analysisError.message);
|
||||
console.error('[사주] 분석 계산 오류:', analysisError.message);
|
||||
return NextResponse.json(
|
||||
{ error: '사주 분석 계산 중 오류가 발생했습니다: ' + analysisError.message },
|
||||
{ error: '사주 분석 계산 중 오류: ' + analysisError.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
console.warn('Anthropic API Key is missing — returning mock data');
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.warn('[사주] GEMINI_API_KEY 미설정 — 예시 데이터 반환');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
|
||||
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||||
const prompt = createSajuPrompt(saju, daeun, gender, analysis, daeunList || [], engineData);
|
||||
const genAI = new GoogleGenerativeAI(apiKey);
|
||||
|
||||
console.log('Generating saju analysis with claude-sonnet-4-6...');
|
||||
// createSajuPrompt 반환값 = 시스템 지시문 (데이터 + 출력 요구사항 포함)
|
||||
const systemInstruction = createSajuPrompt(saju, daeun, gender, analysis, daeunList || [], engineData);
|
||||
|
||||
// 유저 트리거 메시지 (Gemini는 systemInstruction + user 메시지 구조 필요)
|
||||
const userMessage = '위 사주 데이터를 바탕으로 12개 항목의 상세 해석을 작성해주세요. 각 항목은 ## 1. ~ ## 12. 형식으로 작성하세요.';
|
||||
|
||||
let interpretation: string | null = null;
|
||||
|
||||
try {
|
||||
const message = await client.messages.create({
|
||||
model: 'claude-sonnet-4-6',
|
||||
max_tokens: 8192,
|
||||
temperature: 0.75,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const block = message.content[0];
|
||||
if (block.type === 'text') {
|
||||
interpretation = block.text;
|
||||
}
|
||||
console.log('Successfully generated saju analysis with claude-sonnet-4-6');
|
||||
} catch (claudeError: any) {
|
||||
// claude-sonnet-4-6 실패 시 claude-haiku-4-5 폴백
|
||||
console.warn('claude-sonnet-4-6 failed:', claudeError.message, '— trying haiku fallback');
|
||||
for (const { id: modelId, maxTokens } of MODELS) {
|
||||
try {
|
||||
const fallback = await client.messages.create({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 4096,
|
||||
temperature: 0.75,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
console.log(`[사주] ${modelId} 로 해석 생성 중...`);
|
||||
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: modelId,
|
||||
systemInstruction, // ← 시스템 프롬프트 분리 (핵심 수정)
|
||||
generationConfig: {
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
maxOutputTokens: maxTokens,
|
||||
},
|
||||
});
|
||||
const block = fallback.content[0];
|
||||
if (block.type === 'text') {
|
||||
interpretation = block.text;
|
||||
|
||||
const result = await model.generateContent(userMessage);
|
||||
const text = result.response.text();
|
||||
|
||||
if (!text || text.trim().length < 100) {
|
||||
throw new Error('응답이 너무 짧거나 비어있습니다');
|
||||
}
|
||||
console.log('Fallback to claude-haiku-4-5 succeeded');
|
||||
} catch (haikusError: any) {
|
||||
console.error('Both Claude models failed:', haikusError.message);
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
|
||||
interpretation = text;
|
||||
console.log(`[사주] ${modelId} 성공 — ${text.length}자 생성됨`);
|
||||
break;
|
||||
|
||||
} catch (modelError: any) {
|
||||
const msg = modelError.message ?? String(modelError);
|
||||
console.error(`[사주] ${modelId} 실패:`, msg);
|
||||
|
||||
// API 키 / 권한 오류 → 즉시 mock 반환
|
||||
if (
|
||||
msg.includes('API_KEY') ||
|
||||
msg.includes('PERMISSION_DENIED') ||
|
||||
msg.includes('API key') ||
|
||||
modelError.status === 401 ||
|
||||
modelError.status === 403
|
||||
) {
|
||||
console.warn('[사주] API 키 오류 — 예시 데이터 반환');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
|
||||
// 마지막 모델도 실패
|
||||
if (modelId === MODELS[MODELS.length - 1].id) {
|
||||
console.error('[사주] 모든 모델 실패 — 예시 데이터 반환');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
|
||||
console.log(`[사주] ${modelId} → 다음 모델로 폴백...`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +150,9 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
return NextResponse.json({ interpretation, analysis });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error generating saju interpretation:', error.message || error);
|
||||
console.error('[사주] 전체 오류:', error.message || error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to generate interpretation' },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const SAJU_ENGINE_URL = process.env.SAJU_ENGINE_URL;
|
||||
const SAJU_ENGINE_SECRET = process.env.SAJU_ENGINE_SECRET;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!SAJU_ENGINE_URL) {
|
||||
return NextResponse.json({ error: '사주 엔진 URL이 설정되지 않았습니다' }, { status: 503 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${SAJU_ENGINE_URL}/saju/calculate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(SAJU_ENGINE_SECRET ? { 'X-API-Secret': SAJU_ENGINE_SECRET } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(15000), // 15초 타임아웃
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: data.detail || '사주 계산 실패' },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
return NextResponse.json({ error: '사주 엔진 응답 시간 초과' }, { status: 504 });
|
||||
}
|
||||
console.error('사주 계산 프록시 오류:', error);
|
||||
return NextResponse.json({ error: '서버 오류' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ interface SajuRecord {
|
||||
|
||||
function buildResultUrl(rec: SajuRecord) {
|
||||
const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data;
|
||||
// null/undefined 값이 있으면 URL 생성 불가
|
||||
if (!birth_year || !birth_month || !birth_day) return '/saju/input';
|
||||
let url = `/saju/result?year=${birth_year}&month=${birth_month}&day=${birth_day}&gender=${gender}&calendarType=solar`;
|
||||
if (birth_hour != null) url += `&hour=${birth_hour}`;
|
||||
|
||||
@@ -165,6 +165,17 @@ function SectionCard({ section, meta, isOpen, onToggle }: {
|
||||
);
|
||||
}
|
||||
|
||||
// mock 데이터 여부 감지 (저장된 해석이 예시 데이터인 경우 재생성 필요)
|
||||
function isMockInterpretation(text: string | null): boolean {
|
||||
if (!text) return false;
|
||||
return (
|
||||
text.includes('API 키 문제 또는 할당량 초과') ||
|
||||
text.includes('GEMINI_API_KEY 환경변수를 설정') ||
|
||||
text.includes('예시 데이터를 보여드립니다') ||
|
||||
text.includes('API 설정이 필요합니다')
|
||||
);
|
||||
}
|
||||
|
||||
// ── 메인 컴포넌트 ──────────────────────────────────────────────────────
|
||||
export default function SajuAISection({
|
||||
hasPaid,
|
||||
@@ -177,11 +188,15 @@ export default function SajuAISection({
|
||||
currentUrl,
|
||||
engineData,
|
||||
}: SajuAISectionProps) {
|
||||
// 저장된 해석이 mock 데이터면 재생성 필요
|
||||
const isMock = isMockInterpretation(savedInterpretation);
|
||||
const validSaved = savedInterpretation && !isMock ? savedInterpretation : null;
|
||||
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>(
|
||||
savedInterpretation ? 'done' : 'idle'
|
||||
validSaved ? 'done' : 'idle'
|
||||
);
|
||||
const [interpretation, setInterpretation] = useState(savedInterpretation ?? '');
|
||||
const [openSections, setOpenSections] = useState<Set<number>>(new Set([0])); // 첫 섹션 기본 열림
|
||||
const [interpretation, setInterpretation] = useState(validSaved ?? '');
|
||||
const [openSections, setOpenSections] = useState<Set<number>>(new Set([0]));
|
||||
const called = useRef(false);
|
||||
|
||||
const sections = parseInterpretation(interpretation);
|
||||
@@ -198,8 +213,45 @@ export default function SajuAISection({
|
||||
const expandAll = () => setOpenSections(new Set(sections.map((_, i) => i)));
|
||||
const collapseAll = () => setOpenSections(new Set());
|
||||
|
||||
// 재생성: called ref 초기화 후 다시 API 호출
|
||||
const handleRegenerate = () => {
|
||||
called.current = false;
|
||||
setStatus('idle');
|
||||
setInterpretation('');
|
||||
// idle → useEffect가 다시 실행되도록 상태 전환 트리거
|
||||
setTimeout(() => {
|
||||
called.current = false;
|
||||
setStatus('loading');
|
||||
fetch('/api/saju/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.interpretation && !isMockInterpretation(data.interpretation)) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
setOpenSections(new Set([0]));
|
||||
// DB에 실제 해석으로 덮어쓰기
|
||||
const { birth_year, birth_month, birth_day } = birthKey;
|
||||
if (typeof birth_year === 'number' && typeof birth_month === 'number' && typeof birth_day === 'number') {
|
||||
fetch('/api/saju/save-interpretation', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interpretation: data.interpretation, birthKey }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
})
|
||||
.catch(() => setStatus('error'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPaid || savedInterpretation || called.current) return;
|
||||
if (!hasPaid || validSaved || called.current) return;
|
||||
called.current = true;
|
||||
setStatus('loading');
|
||||
|
||||
@@ -248,7 +300,7 @@ export default function SajuAISection({
|
||||
<h3 className="text-xl font-extrabold text-white mb-2">AI 상세 해석 (12개 항목)</h3>
|
||||
<p className="text-blue-200/60 text-sm mb-6">
|
||||
성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등<br />
|
||||
Claude AI가 생성하는 맞춤형 사주 해석을 받아보세요.
|
||||
Gemini 2.5 Pro가 생성하는 맞춤형 사주 해석을 받아보세요.
|
||||
</p>
|
||||
|
||||
{/* 미리보기 섹션 목록 */}
|
||||
@@ -320,9 +372,21 @@ export default function SajuAISection({
|
||||
<h2 className="text-sm font-extrabold text-white">AI 상세 해석</h2>
|
||||
<p className="text-blue-300/60 text-[11px]">12개 항목 · 클릭해서 펼쳐보세요</p>
|
||||
</div>
|
||||
<span className="text-xs bg-emerald-400/20 border border-emerald-400/30 text-emerald-300 font-bold px-2.5 py-1 rounded-full">
|
||||
결제 완료
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRegenerate}
|
||||
title="AI 해석 재생성"
|
||||
className="text-[11px] text-blue-300/60 hover:text-blue-200 px-2 py-1 rounded-lg hover:bg-white/10 transition-all flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
재생성
|
||||
</button>
|
||||
<span className="text-xs bg-emerald-400/20 border border-emerald-400/30 text-emerald-300 font-bold px-2.5 py-1 rounded-full">
|
||||
결제 완료
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 컨트롤 + 목록 */}
|
||||
|
||||
@@ -23,46 +23,6 @@ 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;
|
||||
@@ -89,29 +49,26 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
|
||||
const isLunar = calendarType === 'lunar';
|
||||
const isLeap = isLeapMonth === 'true';
|
||||
|
||||
// ── Python 엔진 호출 (폴백: TypeScript) ──────────────────────────────
|
||||
const engineResult = await fetchFromPythonEngine(yearNum, monthNum, dayNum, hourNum, gender);
|
||||
// ── 사주팔자 계산 (TypeScript — lunar-javascript 기반 정밀 절기 계산) ──
|
||||
const sajuData = calculateSaju(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(
|
||||
const daeunList = calculateDaeun(
|
||||
yearNum, monthNum, dayNum, gender,
|
||||
sajuData.month.stem, sajuData.month.branch
|
||||
);
|
||||
const currentDaeun = engineResult?.currentDaeun ?? getCurrentDaeun(daeunList, currentYear);
|
||||
const 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 branchInteractions = analysis.branchInteractions;
|
||||
const shinsal = analysis.shinsal;
|
||||
const gongmang = analysis.gongmang;
|
||||
const hiddenStems = analysis.hiddenStems;
|
||||
|
||||
// ── 절기 정보 (표시용) ────────────────────────────────────────────────
|
||||
const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum);
|
||||
@@ -187,9 +144,7 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
|
||||
const zodiacIdx = (yearNum - 4) % 12;
|
||||
const zodiacAnimal = zodiacAnimals[zodiacIdx >= 0 ? zodiacIdx : zodiacIdx + 12];
|
||||
|
||||
const engineBadge = engineResult
|
||||
? <span className="text-[10px] bg-emerald-50 border border-emerald-200 text-emerald-700 px-2 py-0.5 rounded-full font-semibold">Python 엔진</span>
|
||||
: <span className="text-[10px] bg-slate-100 border border-slate-200 text-slate-500 px-2 py-0.5 rounded-full">TS 폴백</span>;
|
||||
const engineBadge = <span className="text-[10px] bg-blue-50 border border-blue-200 text-blue-600 px-2 py-0.5 rounded-full font-semibold">TS 계산</span>;
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
@@ -583,12 +538,7 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
|
||||
gender={gender}
|
||||
birthKey={birthKey}
|
||||
currentUrl={currentUrl}
|
||||
engineData={engineResult ? {
|
||||
interactions: engineResult.interactions,
|
||||
shinsal: engineResult.shinsal,
|
||||
gongmang: engineResult.gongmang,
|
||||
hiddenStems: engineResult.hiddenStems,
|
||||
} : undefined}
|
||||
engineData={undefined}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user