diff --git a/app/saju/result/SajuAISection.tsx b/app/saju/result/SajuAISection.tsx index 5e7e070..b1474cb 100644 --- a/app/saju/result/SajuAISection.tsx +++ b/app/saju/result/SajuAISection.tsx @@ -1,6 +1,8 @@ '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 { @@ -20,7 +22,6 @@ interface SajuAISectionProps { gender: string; birthKey: BirthKey; currentUrl: string; - // Python 엔진 데이터 (더 정밀한 절기 계산 결과) engineData?: { interactions?: any[]; shinsal?: any[]; @@ -29,6 +30,142 @@ interface SajuAISectionProps { }; } +// ── 섹션별 메타 (아이콘·색상) ────────────────────────────────────────── +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} +
    +
    +
    + )} +
    + ); +} + +// ── 메인 컴포넌트 ────────────────────────────────────────────────────── export default function SajuAISection({ hasPaid, savedInterpretation, @@ -44,8 +181,23 @@ export default function SajuAISection({ savedInterpretation ? 'done' : 'idle' ); const [interpretation, setInterpretation] = useState(savedInterpretation ?? ''); + 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()); + useEffect(() => { if (!hasPaid || savedInterpretation || called.current) return; called.current = true; @@ -54,20 +206,16 @@ export default function SajuAISection({ fetch('/api/saju/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - saju: sajuData, - daeun, - daeunList, - gender, - engineData, // Python 엔진 데이터 전달 - }), + body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }), }) - .then((r) => r.json()) - .then((data) => { + .then(r => r.json()) + .then(data => { if (data.interpretation) { setInterpretation(data.interpretation); setStatus('done'); - // birthKey 유효성 검사 후 저장 (NaN/null 방지) + // 첫 번째 섹션 자동 열기 + setOpenSections(new Set([0])); + const { birth_year, birth_month, birth_day } = birthKey; if ( typeof birth_year === 'number' && !isNaN(birth_year) && @@ -87,7 +235,7 @@ export default function SajuAISection({ .catch(() => setStatus('error')); }, [hasPaid]); - // 미결제 상태 + // ── 미결제 ────────────────────────────────────────────────────────── if (!hasPaid) { return (
    @@ -98,10 +246,21 @@ export default function SajuAISection({ AI PREMIUM

    AI 상세 해석 (12개 항목)

    -

    +

    성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등
    GPT-4o가 생성하는 맞춤형 사주 해석을 받아보세요.

    + + {/* 미리보기 섹션 목록 */} +
    + {SECTION_META.map((meta, i) => ( +
    + {meta.icon} + {meta.badgeText} +
    + ))} +
    +

    AI가 사주를 분석하는 중입니다...

    약 20~30초 소요될 수 있습니다

    +
    + {SECTION_META.map((meta, i) => ( + + {meta.icon}{meta.badgeText} + + ))} +
    ); } - // 오류 + // ── 오류 ────────────────────────────────────────────────────────── if (status === 'error') { return (
    @@ -140,22 +306,73 @@ export default function SajuAISection({ ); } - // AI 해석 완료 + // ── 해석 완료 ───────────────────────────────────────────────────── return ( -
    -
    -
    +
    + {/* 헤더 */} +
    +
    -

    AI 상세 해석

    - +
    +

    AI 상세 해석

    +

    12개 항목 · 클릭해서 펼쳐보세요

    +
    + 결제 완료
    -
    - {interpretation} + + {/* 섹션 컨트롤 + 목록 */} +
    + {/* 전체 펼치기/접기 */} + {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가 생성한 내용입니다. 참고용으로 활용해주세요. +

    + )}
    );