'use client'; import { useState, useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import PaymentButton from '@/app/components/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 }) =>
    {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}
    ))}
    AI 상세 해석 받기 — 1,000원

    결제 후 즉시 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가 생성한 내용입니다. 참고용으로 활용해주세요.

    )}
    ); }