'use client';
import { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
// PaymentButton 비활성화 — 토스페이먼츠 결제 일시 중단, 무료 제공 중
interface BirthKey {
birth_year: number;
birth_month: number;
birth_day: number;
birth_hour?: number;
gender: string;
}
interface SajuAISectionProps {
hasPaid: boolean;
savedInterpretation: string | null;
sajuData: object;
daeun: object | null;
daeunList: object[];
gender: string;
birthKey: BirthKey;
currentUrl: string;
engineData?: {
interactions?: any[];
shinsal?: any[];
gongmang?: any;
hiddenStems?: any[];
};
}
// ── 섹션별 메타 (아이콘·색상) ──────────────────────────────────────────
const SECTION_META: {
icon: string;
gradient: string;
border: string;
badge: string;
badgeText: string;
}[] = [
{ icon: '🌟', gradient: 'from-violet-500 to-purple-600', border: 'border-violet-100', badge: 'bg-violet-50 border-violet-200 text-violet-700', badgeText: '기질' },
{ icon: '⚖️', gradient: 'from-emerald-500 to-teal-600', border: 'border-emerald-100', badge: 'bg-emerald-50 border-emerald-200 text-emerald-700', badgeText: '오행' },
{ icon: '🔗', gradient: 'from-blue-500 to-indigo-600', border: 'border-blue-100', badge: 'bg-blue-50 border-blue-200 text-blue-700', badgeText: '지지' },
{ icon: '✨', gradient: 'from-amber-500 to-orange-500', border: 'border-amber-100', badge: 'bg-amber-50 border-amber-200 text-amber-700', badgeText: '신살' },
{ icon: '💰', gradient: 'from-yellow-500 to-amber-600', border: 'border-yellow-100', badge: 'bg-yellow-50 border-yellow-200 text-yellow-700', badgeText: '재물' },
{ icon: '🎯', gradient: 'from-rose-500 to-pink-600', border: 'border-rose-100', badge: 'bg-rose-50 border-rose-200 text-rose-700', badgeText: '직업' },
{ icon: '💕', gradient: 'from-pink-500 to-rose-500', border: 'border-pink-100', badge: 'bg-pink-50 border-pink-200 text-pink-700', badgeText: '애정' },
{ icon: '🌿', gradient: 'from-green-500 to-emerald-600', border: 'border-green-100', badge: 'bg-green-50 border-green-200 text-green-700', badgeText: '건강' },
{ icon: '🗺️', gradient: 'from-cyan-500 to-blue-600', border: 'border-cyan-100', badge: 'bg-cyan-50 border-cyan-200 text-cyan-700', badgeText: '대운' },
{ icon: '📅', gradient: 'from-indigo-500 to-violet-600', border: 'border-indigo-100', badge: 'bg-indigo-50 border-indigo-200 text-indigo-700', badgeText: '세운' },
{ icon: '🏆', gradient: 'from-amber-400 to-yellow-500', border: 'border-amber-100', badge: 'bg-amber-50 border-amber-200 text-amber-700', badgeText: '황금기' },
{ icon: '💌', gradient: 'from-slate-600 to-slate-800', border: 'border-slate-100', badge: 'bg-slate-50 border-slate-200 text-slate-700', badgeText: '종합' },
];
// ── 마크다운 → 섹션 파싱 ──────────────────────────────────────────────
interface ParsedSection {
number: number;
title: string;
content: string;
}
function parseInterpretation(text: string): ParsedSection[] {
// "## 숫자. 제목" 패턴으로 분리
const parts = text.split(/\n(?=##\s+\d+[\.\s])/).filter(Boolean);
const sections: ParsedSection[] = [];
for (const part of parts) {
const lines = part.trim().split('\n');
const headerLine = lines[0] ?? '';
const match = headerLine.match(/^##\s+(\d+)[.\s]\s*(.+)$/);
if (match) {
sections.push({
number: parseInt(match[1], 10),
title: match[2].trim(),
content: lines.slice(1).join('\n').trim(),
});
}
}
// 파싱 실패 시 전체를 하나의 섹션으로
if (sections.length === 0 && text.trim()) {
sections.push({ number: 0, title: 'AI 해석', content: text.trim() });
}
return sections;
}
// ── 섹션 카드 컴포넌트 ────────────────────────────────────────────────
function SectionCard({ section, meta, isOpen, onToggle }: {
section: ParsedSection;
meta: typeof SECTION_META[0];
isOpen: boolean;
onToggle: () => void;
}) {
return (
{/* 헤더 */}
{/* 내용 (아코디언) */}
{isOpen && (
{meta.icon}
{children}
,
h2: ({ children }) => {children}
,
h3: ({ children }) => {children}
,
p: ({ children }) => {children}
,
strong: ({ children }) => {children},
em: ({ children }) => {children},
ul: ({ children }) => ,
ol: ({ children }) => {children}
,
li: ({ children }) => {children},
blockquote: ({ children }) => (
{children}
),
hr: () =>
,
code: ({ children }) => (
{children}
),
}}
>
{section.content}
)}
);
}
// 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,
savedInterpretation,
sajuData,
daeun,
daeunList,
gender,
birthKey,
currentUrl,
engineData,
}: SajuAISectionProps) {
// 저장된 해석이 mock 데이터면 재생성 필요
const isMock = isMockInterpretation(savedInterpretation);
const validSaved = savedInterpretation && !isMock ? savedInterpretation : null;
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>(
validSaved ? 'done' : 'idle'
);
const [interpretation, setInterpretation] = useState(validSaved ?? '');
const [openSections, setOpenSections] = useState>(new Set([0]));
const called = useRef(false);
const sections = parseInterpretation(interpretation);
const toggleSection = (idx: number) => {
setOpenSections(prev => {
const next = new Set(prev);
if (next.has(idx)) next.delete(idx);
else next.add(idx);
return next;
});
};
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 || validSaved || called.current) return;
called.current = true;
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) {
setInterpretation(data.interpretation);
setStatus('done');
// 첫 번째 섹션 자동 열기
setOpenSections(new Set([0]));
const { birth_year, birth_month, birth_day } = birthKey;
if (
typeof birth_year === 'number' && !isNaN(birth_year) &&
typeof birth_month === 'number' && !isNaN(birth_month) &&
typeof birth_day === 'number' && !isNaN(birth_day)
) {
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'));
}, [hasPaid]);
// ── 미결제 ──────────────────────────────────────────────────────────
if (!hasPaid) {
return (
AI PREMIUM
AI 상세 해석 (12개 항목)
성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등
Gemini 2.5 Pro가 생성하는 맞춤형 사주 해석을 받아보세요.
{/* 미리보기 섹션 목록 */}
{SECTION_META.map((meta, i) => (
{meta.icon}
{meta.badgeText}
))}
{/* 결제 일시 중단 — hasPaid=true이므로 이 분기는 표시되지 않음 */}
AI 해석 무료로 보기
);
}
// ── 로딩 ──────────────────────────────────────────────────────────
if (status === 'loading') {
return (
AI가 사주를 분석하는 중입니다...
약 20~30초 소요될 수 있습니다
{SECTION_META.map((meta, i) => (
{meta.icon}{meta.badgeText}
))}
);
}
// ── 오류 ──────────────────────────────────────────────────────────
if (status === 'error') {
return (
AI 해석 생성에 실패했습니다.
);
}
// ── 해석 완료 ─────────────────────────────────────────────────────
return (
{/* 헤더 */}
AI 상세 해석
12개 항목 · 클릭해서 펼쳐보세요
{/* 섹션 컨트롤 + 목록 */}
{/* 전체 펼치기/접기 */}
{sections.length > 1 && (
총 {sections.length}개 항목
)}
{/* 섹션 카드 목록 */}
{sections.map((section, idx) => {
const metaIdx = section.number > 0 ? Math.min(section.number - 1, SECTION_META.length - 1) : idx % SECTION_META.length;
const meta = SECTION_META[metaIdx];
return (
toggleSection(idx)}
/>
);
})}
{/* 하단 안내 */}
{sections.length > 0 && (
해석은 사주 데이터를 기반으로 AI가 생성한 내용입니다. 참고용으로 활용해주세요.
)}
);
}