- 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>
162 lines
6.1 KiB
TypeScript
162 lines
6.1 KiB
TypeScript
|
|
import { NextResponse } from 'next/server';
|
|
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. 일간 분석과 타고난 기질
|
|
(GEMINI_API_KEY 환경변수를 설정하고 서버를 재시작하면 실제 AI 해석을 받을 수 있습니다.)
|
|
귀하는 **갑목(甲木)** 일간으로 태어나, 마치 곧게 뻗은 소나무와 같은 기상을 지니고 있다. 리더십이 강하고 추진력이 뛰어나며, 한번 마음먹은 일은 끝까지 해내는 뚝심이 있다.
|
|
|
|
## 2. 오행 균형과 용신 기반 개운법
|
|
사주에서 **화(火)** 기운이 부족하여 표현력이 다소 약할 수 있다.
|
|
|
|
## 3. 지지 상호작용 해석
|
|
지지 간의 상호작용을 살펴보면, 특별한 합충형이 발견된다.
|
|
|
|
## 4. 신살이 삶에 미치는 영향
|
|
역마살이 사주에 자리하고 있어 이동과 변동이 많은 삶을 살게 된다.
|
|
|
|
## 5. 재물운과 금전 흐름
|
|
재물창고인 **진토(辰土)**를 깔고 있어 기본적으로 재복은 타고났다.
|
|
|
|
## 6. 직업 적성과 진로
|
|
교육, 출판, 건축, 디자인 등 창조적이고 독립적인 분야에서 두각을 나타낼 수 있다.
|
|
|
|
## 7. 애정운과 결혼
|
|
자존심이 강해 상대방에게 굽히지 않으려는 성향이 있다.
|
|
|
|
## 8. 건강운
|
|
간, 담낭, 신경계 통증에 유의해야 한다.
|
|
|
|
## 9. 현재 대운의 흐름과 기회/위기
|
|
현재 대운은 인생의 전환점이다.
|
|
|
|
## 10. 올해의 세운 분석
|
|
올해는 귀인의 도움을 받을 수 있는 해이다.
|
|
|
|
## 11. 인생의 황금기 예측
|
|
40대 중반부터 50대 초반까지 인생의 가장 화려한 시기를 맞이할 것으로 보인다.
|
|
|
|
## 12. 종합 조언
|
|
"서두르지 않아도 봄은 온다." 조급해하지 말고 때를 기다리는 지혜가 필요하다.
|
|
`;
|
|
|
|
// 모델 우선순위 — 강력한 순서 (이 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();
|
|
|
|
// 종합 분석 수행
|
|
let analysis;
|
|
try {
|
|
analysis = performFullAnalysis(saju);
|
|
} catch (analysisError: any) {
|
|
console.error('[사주] 분석 계산 오류:', analysisError.message);
|
|
return NextResponse.json(
|
|
{ error: '사주 분석 계산 중 오류: ' + analysisError.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
if (!apiKey) {
|
|
console.warn('[사주] GEMINI_API_KEY 미설정 — 예시 데이터 반환');
|
|
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
|
}
|
|
|
|
const genAI = new GoogleGenerativeAI(apiKey);
|
|
|
|
// createSajuPrompt 반환값 = 시스템 지시문 (데이터 + 출력 요구사항 포함)
|
|
const systemInstruction = createSajuPrompt(saju, daeun, gender, analysis, daeunList || [], engineData);
|
|
|
|
// 유저 트리거 메시지 (Gemini는 systemInstruction + user 메시지 구조 필요)
|
|
const userMessage = '위 사주 데이터를 바탕으로 12개 항목의 상세 해석을 작성해주세요. 각 항목은 ## 1. ~ ## 12. 형식으로 작성하세요.';
|
|
|
|
let interpretation: string | null = null;
|
|
|
|
for (const { id: modelId, maxTokens } of MODELS) {
|
|
try {
|
|
console.log(`[사주] ${modelId} 로 해석 생성 중...`);
|
|
|
|
const model = genAI.getGenerativeModel({
|
|
model: modelId,
|
|
systemInstruction, // ← 시스템 프롬프트 분리 (핵심 수정)
|
|
generationConfig: {
|
|
temperature: 0.8,
|
|
topP: 0.95,
|
|
maxOutputTokens: maxTokens,
|
|
},
|
|
});
|
|
|
|
const result = await model.generateContent(userMessage);
|
|
const text = result.response.text();
|
|
|
|
if (!text || text.trim().length < 100) {
|
|
throw new Error('응답이 너무 짧거나 비어있습니다');
|
|
}
|
|
|
|
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} → 다음 모델로 폴백...`);
|
|
}
|
|
}
|
|
|
|
if (!interpretation) {
|
|
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
|
}
|
|
|
|
return NextResponse.json({ interpretation, analysis });
|
|
|
|
} catch (error: any) {
|
|
console.error('[사주] 전체 오류:', error.message || error);
|
|
return NextResponse.json(
|
|
{ error: error.message || 'Failed to generate interpretation' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|