From 31c376da0708ffbf432c9e46c2f8a066d18a5dbb Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 16 May 2026 03:39:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(work):=20/work/saju=20+=20input=20+=20resu?= =?UTF-8?q?lt=20=E2=80=94=20=ED=98=84=20/saju=20=EC=BB=A8=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - saju 페이지 + 입력 폼 + 결과 + AI 해석 + 사주 컴포넌트 모두 이동 - depth 변경 → 모든 import @/ 절대 경로 - 내부 Link href + router.push 새 URL로 - 카탈로그 spec(49만 코어 + 11 모듈)은 보류 — 무료 사주 분석만 마이그 - API route /api/saju/* 변경 없음 Co-Authored-By: Claude Opus 4.7 (1M context) --- app/work/saju/components/SajuForm.tsx | 220 +++++++ app/work/saju/input/page.tsx | 41 ++ app/work/saju/layout.tsx | 27 + app/work/saju/page.tsx | 334 +++++++++++ app/work/saju/result/SajuAISection.tsx | 443 ++++++++++++++ app/work/saju/result/SajuFortuneSection.tsx | 323 ++++++++++ app/work/saju/result/page.tsx | 627 ++++++++++++++++++++ 7 files changed, 2015 insertions(+) create mode 100644 app/work/saju/components/SajuForm.tsx create mode 100644 app/work/saju/input/page.tsx create mode 100644 app/work/saju/layout.tsx create mode 100644 app/work/saju/page.tsx create mode 100644 app/work/saju/result/SajuAISection.tsx create mode 100644 app/work/saju/result/SajuFortuneSection.tsx create mode 100644 app/work/saju/result/page.tsx diff --git a/app/work/saju/components/SajuForm.tsx b/app/work/saju/components/SajuForm.tsx new file mode 100644 index 0000000..7a76af1 --- /dev/null +++ b/app/work/saju/components/SajuForm.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { lunarToSolar } from '@/lib/lunar-utils'; + +export default function SajuForm() { + const router = useRouter(); + const [year, setYear] = useState(''); + const [month, setMonth] = useState(''); + const [day, setDay] = useState(''); + const [hour, setHour] = useState(''); + const [calendarType, setCalendarType] = useState<'solar' | 'lunar'>('solar'); + const [gender, setGender] = useState<'male' | 'female'>('male'); + const [isLeapMonth, setIsLeapMonth] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!year || !month || !day) { + alert('생년월일을 모두 입력해주세요.'); + return; + } + + let finalYear = year; + let finalMonth = month; + let finalDay = day; + + // 음력인 경우 양력으로 변환 + if (calendarType === 'lunar') { + const solar = lunarToSolar( + parseInt(year), + parseInt(month), + parseInt(day), + isLeapMonth + ); + finalYear = solar.year.toString(); + finalMonth = solar.month.toString(); + finalDay = solar.day.toString(); + } + + // URL 파라미터로 전달 + const params = new URLSearchParams({ + year: finalYear, + month: finalMonth, + day: finalDay, + gender, + calendarType, + originalYear: year, + originalMonth: month, + originalDay: day, + }); + + if (hour) { + params.append('hour', hour); + } + + if (calendarType === 'lunar') { + params.append('isLeapMonth', isLeapMonth.toString()); + } + + router.push(`/work/saju/result?${params.toString()}`); + }; + + return ( +
+ {/* 생년월일 */} +
+ +
+ setYear(e.target.value)} + required + /> + setMonth(e.target.value)} + required + /> + setDay(e.target.value)} + required + /> +
+
+ + {/* 태어난 시간 */} +
+ + +
+ + {/* 양력/음력 선택 */} +
+ +
+ + +
+ {calendarType === 'lunar' && ( +
+ +
+ )} +
+ + {/* 성별 선택 */} +
+ +
+ + +
+
+ + {/* 제출 버튼 */} + + +

+ * 태어난 시간을 정확히 아시면 더 정확한 사주를 확인할 수 있습니다. +

+
+ ); +} diff --git a/app/work/saju/input/page.tsx b/app/work/saju/input/page.tsx new file mode 100644 index 0000000..5fb5ac0 --- /dev/null +++ b/app/work/saju/input/page.tsx @@ -0,0 +1,41 @@ +import SajuForm from '@/app/work/saju/components/SajuForm'; + +export default function SajuInputPage() { + return ( +
+ {/* Hero */} +
+ +
+
+ + AI 사주 분석 · 생년월일 입력 +
+

+ 생년월일을 입력해주세요 +

+

+ 정확한 생년월일과 태어난 시간을 입력하면
+ 더 정밀한 사주팔자를 계산할 수 있습니다. +

+
+
+ + {/* Form 영역 */} +
+
+
+
+

기본 정보 입력

+
+ +
+ +

+ 입력하신 정보는 사주 계산에만 사용되며 별도로 저장되지 않습니다. +

+
+
+ ); +} diff --git a/app/work/saju/layout.tsx b/app/work/saju/layout.tsx new file mode 100644 index 0000000..860fa23 --- /dev/null +++ b/app/work/saju/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'AI 사주 분석', + description: + '생년월일시를 입력하면 Gemini AI가 사주팔자를 분석합니다. 일간·오행·대운·세운 기반 12개 항목 상세 해석. 재물운·애정운·직업·건강 포함.', + keywords: [ + 'AI 사주', + '사주풀이', + '사주팔자', + '사주 분석', + '오행 분석', + '대운', + '세운', + '사주 운세', + ], + openGraph: { + title: 'AI 사주 분석 | 쟁승메이드', + description: + 'Gemini AI 기반 사주팔자 분석. 일간·오행·대운·세운·재물운·애정운 12개 항목 해석.', + url: 'https://jaengseung-made.com/work/saju', + }, +}; + +export default function SajuLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/work/saju/page.tsx b/app/work/saju/page.tsx new file mode 100644 index 0000000..3032558 --- /dev/null +++ b/app/work/saju/page.tsx @@ -0,0 +1,334 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import PaymentButton from '@/app/components/PaymentButton'; +import { createClient } from '@/lib/supabase/client'; + +const faqItems = [ + { + q: '사주팔자란 무엇인가요?', + a: '사주팔자(四柱八字)는 태어난 년·월·일·시의 네 기둥(四柱)에 각각 천간과 지지 두 글자씩 총 여덟 글자(八字)로 이루어진 동양의 전통 운명 분석 체계입니다.', + }, + { + q: 'AI 해석은 어떻게 동작하나요?', + a: '전통 명리학 계산 로직(오행, 신강/신약, 용신/희신 등)으로 산출된 데이터를 Gemini AI에 전달하여 12개 항목의 상세 해석을 생성합니다. 현재 기본 원국 분석과 AI 상세 해석 모두 무료로 제공됩니다.', + }, + { + q: '태어난 시간을 모르면 어떻게 하나요?', + a: '시간을 모르더라도 년·월·일 세 기둥(三柱)만으로 사주를 계산할 수 있습니다. 다만 시주가 빠지면 세부 분석 정확도가 다소 낮아집니다.', + }, + { + q: '음력으로 입력할 수 있나요?', + a: '네, 양력과 음력 모두 지원합니다. 음력을 선택하면 내부적으로 양력으로 변환하여 정확한 사주를 계산합니다. 윤달도 별도 선택이 가능합니다.', + }, +]; + +interface SajuRecord { + id: number; + created_at: string; + saju_data: { + birth_year: number; + birth_month: number; + birth_day: number; + birth_hour?: number; + gender: string; + }; + interpretation: string | null; + is_paid: boolean; +} + +function buildResultUrl(rec: SajuRecord) { + const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data; + if (!birth_year || !birth_month || !birth_day) return '/work/saju/input'; + let url = `/work/saju/result?year=${birth_year}&month=${birth_month}&day=${birth_day}&gender=${gender}&calendarType=solar`; + if (birth_hour != null) url += `&hour=${birth_hour}`; + return url; +} + +export default function SajuPage() { + const supabase = createClient(); + const [paidRecords, setPaidRecords] = useState([]); + const [hasPaid, setHasPaid] = useState(false); + const [authChecked, setAuthChecked] = useState(false); + + useEffect(() => { + async function fetchRecords() { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { setAuthChecked(true); return; } + + const { data: records } = await supabase + .from('saju_records') + .select('*') + .eq('user_id', user.id) + .eq('is_paid', true) + .order('created_at', { ascending: false }) + .limit(2); + + if (records && records.length > 0) { + setPaidRecords(records); + setHasPaid(true); + } + setAuthChecked(true); + } + fetchRecords(); + }, []); + + return ( +
+ {/* ─── Hero ─── */} +
+ +
+
+ + 전통 명리학 × AI 해석 · 무료 +
+

+ AI가 분석하는
+ 사주팔자 +

+

+ 수천 년의 동양 명리학과 최신 AI 기술의 만남.
+ 태어난 순간의 우주적 에너지를 12가지 항목으로 해석해드립니다. +

+ + {/* 이전 기록 있으면 분기 버튼, 없으면 단일 CTA */} + {authChecked && hasPaid ? ( +
+ + + + + 새로 보기 + + + + + + 이전 내역 다시 보기 + +
+ ) : ( + + + + + 지금 바로 시작하기 + + )} +
+
+ +
+
+ + {/* ─── 이전 기록 섹션 (구매한 유저만) ─── */} + {hasPaid && paidRecords.length > 0 && ( +
+
+

MY RECORDS

+

이전 AI 사주 기록

+

결제한 사주 기록을 다시 확인하세요

+
+
+ {paidRecords.map((rec) => ( +
+
+
+
+ {new Date(rec.created_at).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })} +
+
+ {rec.saju_data.birth_year ?? '?'}년{' '} + {rec.saju_data.birth_month ?? '?'}월{' '} + {rec.saju_data.birth_day ?? '?'}일생 +
+
+ {rec.saju_data.gender === 'male' ? '남성' : '여성'} + {rec.saju_data.birth_hour != null ? ` · ${rec.saju_data.birth_hour}시생` : ''} +
+
+ + AI 해석 완료 + +
+ {rec.interpretation && ( +

+ {rec.interpretation.replace(/[#*]/g, '').substring(0, 80)}... +

+ )} + + 다시 보기 → + +
+ ))} +
+
+ )} + + {/* ─── 바로 시작하기 CTA ─── */} +
+

지금 무료로 시작하세요

+

회원가입 없이, 생년월일만 입력하면 바로 확인 가능합니다

+ + 사주 입력하러 가기 → + +
+ + {/* ─── 무료 vs 유료 비교표 ─── */} +
+
+

PRICING

+

무엇을 분석해드리나요

+

기본 원국은 무료, AI 상세 해석은 1,000원

+
+ +
+ {/* 무료 */} +
+
+
+ + + +
+
+
FREE
+
무료 기본 분석
+
+
+
    + {[ + '사주팔자 원국 (년·월·일·시주)', + '천간·지지·지장간 표', + '십성 및 십이운성', + '오행 분포 차트', + '지지 상호작용 (합·충·형)', + '일간 분석 요약', + ].map((item) => ( +
  • +
    +
    +
    + {item} +
  • + ))} +
+
+
무료
+
회원가입 불필요
+ + 무료로 시작하기 + +
+
+ + {/* AI 해석 (현재 무료) */} +
+
+ 1,000원 +
+
+
+ + + +
+
+
AI PREMIUM
+
AI 상세 해석
+
+
+
    + {[ + '무료 기본 분석 전체 포함', + '신강/신약 정밀 판단', + '용신·희신·기신 추정', + '대운 (10년 주기) 분석', + '올해 세운 흐름', + 'Gemini 2.5 Pro AI 12가지 상세 해석', + ].map((item) => ( +
  • +
    +
    +
    + {item} +
  • + ))} +
+
+
+ 1,000원 + / 1회 +
+
로그인 후 결제 · 12가지 항목 AI 해석
+ + 사주 분석 시작하기 → + +
+
+
+
+ + {/* ─── FAQ ─── */} +
+
+

FAQ

+

자주 묻는 질문

+
+
+ {faqItems.map((item, i) => ( +
+
+
+ Q +
+
+

{item.q}

+

{item.a}

+
+
+
+ ))} +
+
+ +
+
+
+ ); +} diff --git a/app/work/saju/result/SajuAISection.tsx b/app/work/saju/result/SajuAISection.tsx new file mode 100644 index 0000000..4581af3 --- /dev/null +++ b/app/work/saju/result/SajuAISection.tsx @@ -0,0 +1,443 @@ +'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가 생성한 내용입니다. 참고용으로 활용해주세요. +

    + )} +
    +
    + ); +} diff --git a/app/work/saju/result/SajuFortuneSection.tsx b/app/work/saju/result/SajuFortuneSection.tsx new file mode 100644 index 0000000..8607d95 --- /dev/null +++ b/app/work/saju/result/SajuFortuneSection.tsx @@ -0,0 +1,323 @@ +'use client'; + +import { useMemo } from 'react'; + +// ── 천간 / 지지 ─────────────────────────────────────────────────────── +const STEMS = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸']; +const STEMS_KR = ['갑','을','병','정','무','기','경','신','임','계']; +const BRANCHES = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥']; +const BRANCHES_KR= ['자','축','인','묘','진','사','오','미','신','유','술','해']; + +const STEM_ELEM: Record = { '甲':'木','乙':'木','丙':'火','丁':'火','戊':'土','己':'土','庚':'金','辛':'金','壬':'水','癸':'水' }; +const BRANCH_ELEM: Record = { '子':'水','亥':'水','寅':'木','卯':'木','巳':'火','午':'火','申':'金','酉':'金','丑':'土','辰':'土','未':'土','戌':'土' }; + +// 1900-01-01 = 甲戌 (stem=0, branch=10) — CLAUDE.md 검증 완료 +const BASE_MS = Date.UTC(1900, 0, 1); + +function getTodayPillar() { + const now = new Date(); + const todayMs = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()); + const diff = Math.round((todayMs - BASE_MS) / 86400000); + const si = ((0 + diff) % 10 + 10) % 10; + const bi = ((10 + diff) % 12 + 12) % 12; + return { + stem: STEMS[si], stemKr: STEMS_KR[si], + branch: BRANCHES[bi], branchKr: BRANCHES_KR[bi], + stemElem: STEM_ELEM[STEMS[si]] ?? '木', + branchElem: BRANCH_ELEM[BRANCHES[bi]] ?? '水', + year: now.getFullYear(), month: now.getMonth() + 1, date: now.getDate(), + }; +} + +// ── 오행 상생·상극 ──────────────────────────────────────────────────── +const GENERATES: Record = { '木':'火','火':'土','土':'金','金':'水','水':'木' }; +const OVERCOMES: Record = { '木':'土','火':'金','土':'水','金':'木','水':'火' }; + +type Rel = 'same'|'generates'|'generated'|'overcomes'|'overcome'|'neutral'; +function getRelation(a: string, b: string): Rel { + if (a === b) return 'same'; + if (GENERATES[a] === b) return 'generates'; + if (GENERATES[b] === a) return 'generated'; + if (OVERCOMES[a] === b) return 'overcomes'; + if (OVERCOMES[b] === a) return 'overcome'; + return 'neutral'; +} + +// ── 오늘 종합 점수 (0–100) ──────────────────────────────────────────── +function calcOverallScore(stemElem: string, branchElem: string, yongShin: string, heeShin: string) { + let score = 50; + const add = (rel: Rel, weight: number) => { + if (rel === 'same') score += 25 * weight; + else if (rel === 'generates' || rel === 'generated') score += 15 * weight; + else if (rel === 'overcomes') score -= 20 * weight; + else if (rel === 'overcome') score -= 8 * weight; + }; + add(getRelation(stemElem, yongShin), 1); + add(getRelation(branchElem, yongShin), 0.8); + add(getRelation(stemElem, heeShin), 0.3); + add(getRelation(branchElem, heeShin), 0.2); + return Math.round(Math.max(10, Math.min(100, score))); +} + +type Level = 'great'|'good'|'neutral'|'caution'; +function toLevel(s: number): Level { + if (s >= 78) return 'great'; + if (s >= 58) return 'good'; + if (s >= 38) return 'neutral'; + return 'caution'; +} + +// ── 결정론적 랜덤 ──────────────────────────────────────────────────── +function seededRand(seed: number) { + let s = seed; + return () => { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 0xffffffff; }; +} + +// ── 운세 항목 빌드 ──────────────────────────────────────────────────── +type Area = { icon: string; label: string; score: number; desc: string }; + +const DESCS: Record> = { + money: { + great: '재물 흐름이 활발합니다. 작은 투자나 구매 결정에 긍정적인 시기입니다.', + good: '재물 운이 순조롭습니다. 무리하지 않는 범위에서 움직이면 이익이 납니다.', + neutral: '수입·지출이 균형을 이루는 날. 큰 결정은 잠시 미루세요.', + caution: '충동 지출에 주의하세요. 중요한 금전 거래는 신중히 검토하세요.', + }, + love: { + great: '감정 교류가 잘 이루어지는 날. 마음을 전하기 좋은 타이밍입니다.', + good: '관계에 따뜻한 기운이 감돕니다. 오래 연락 못 했던 사람에게 먼저 다가가 보세요.', + neutral: '평온한 관계를 유지하는 날입니다. 억지로 변화를 만들 필요 없습니다.', + caution: '오해가 생기기 쉬운 날입니다. 중요한 대화는 감정이 차분해진 후에 하세요.', + }, + career: { + great: '능력이 잘 발휘되는 날. 중요한 프레젠테이션이나 면담에 최적입니다.', + good: '업무 효율이 올라가는 날입니다. 오늘 마무리한 과제는 좋은 결과로 이어집니다.', + neutral: '꾸준히 하던 일을 이어가는 날. 새 프로젝트보다 마무리에 집중하세요.', + caution: '실수가 생기기 쉬운 날입니다. 중요한 결재·계약은 하루 늦춰보세요.', + }, + health: { + great: '체력·집중력 모두 좋은 날. 평소보다 활동량을 늘려도 괜찮습니다.', + good: '컨디션이 안정적입니다. 가벼운 운동으로 기운을 더 끌어올리세요.', + neutral: '무리하지 않는 것이 최선. 충분한 수분과 수면을 챙겨주세요.', + caution: '피로가 쌓이기 쉬운 날입니다. 무리한 약속은 피하고 충분히 쉬세요.', + }, + social: { + great: '대인관계 운이 열린 날. 중요한 만남·협상에 유리한 시기입니다.', + good: '사교적 기운이 넘칩니다. 새 인맥을 만들거나 협업을 제안해보세요.', + neutral: '조용히 자신의 일에 집중하는 날. 복잡한 인간관계는 잠시 내려놓으세요.', + caution: '갈등이 생기기 쉬운 날입니다. 중요한 협상은 다음 기회로 미루는 것이 현명합니다.', + }, +}; + +function buildAreas( + overall: number, + yongShin: string, heeShin: string, + yearNum: number, monthNum: number, dayNum: number, +): Area[] { + const now = new Date(); + const seed = yearNum * 1_000_000 + monthNum * 10_000 + dayNum * 100 + now.getFullYear() % 100 * 10 + now.getMonth(); + const rand = seededRand(seed); + const roll = () => Math.round(Math.max(15, Math.min(98, rand() * 40 + overall - 20))); + const keys = ['money','love','career','health','social'] as const; + const icons = ['💰','💕','🎯','🌿','🤝']; + const labels = ['재물운','애정운','직업운','건강운','사회운']; + return keys.map((k, i) => { + const s = roll(); + return { icon: icons[i], label: labels[i], score: s, desc: DESCS[k][toLevel(s)] }; + }); +} + +// ── 레벨별 색상/라벨 ───────────────────────────────────────────────── +const LEVEL_META: Record = { + great: { emoji:'🌟', label:'아주 좋은 날', bar:'#f59e0b', bg:'bg-amber-50', border:'border-amber-300', text:'text-amber-800', badge:'bg-amber-100 text-amber-700 border-amber-300' }, + good: { emoji:'✨', label:'좋은 날', bar:'#22c55e', bg:'bg-emerald-50',border:'border-emerald-300',text:'text-emerald-800',badge:'bg-emerald-100 text-emerald-700 border-emerald-300' }, + neutral: { emoji:'🌤️', label:'평온한 날', bar:'#64748b', bg:'bg-slate-50', border:'border-slate-200', text:'text-slate-700', badge:'bg-slate-100 text-slate-600 border-slate-200' }, + caution: { emoji:'⚠️', label:'조심하는 날', bar:'#f97316', bg:'bg-orange-50', border:'border-orange-300',text:'text-orange-800', badge:'bg-orange-100 text-orange-700 border-orange-300' }, +}; + +const REL_DESC: (yongShin: string, yongShinKr: string) => Record = (y, yk) => ({ + same: `오늘 기운이 당신의 용신 ${y}(${yk})과 같은 오행으로 강하게 공명합니다.`, + generates: `오늘 기운이 용신 ${y}(${yk})을 생(生)해줍니다. 순조롭게 힘이 실리는 날.`, + generated: `용신 ${y}(${yk})이 오늘 기운을 생(生)해주고 있어 에너지를 베풀기 좋은 날입니다.`, + overcomes: `오늘 기운이 용신 ${y}(${yk})을 극(克)합니다. 신중하게 움직이는 것이 좋습니다.`, + overcome: `용신 ${y}(${yk})이 오늘 기운을 극(克)합니다. 주도적으로 판단하기 좋은 날.`, + neutral: `오늘 기운과 용신 ${y}(${yk})은 독립적으로 작용합니다. 차분하게 나아가세요.`, +}); + +// ── 점수 바 ────────────────────────────────────────────────────────── +function ScoreBar({ score, color }: { score: number; color: string }) { + return ( +
    +
    +
    +
    + {score} +
    + ); +} + +// ── 메인 컴포넌트 ───────────────────────────────────────────────────── +interface Props { + yongShin: string; + yongShinKr: string; + heeShin: string; + heeShinKr: string; + yearNum: number; + monthNum: number; + dayNum: number; + hasLottoSubscription: boolean; +} + +export default function SajuFortuneSection({ + yongShin, yongShinKr, heeShin, heeShinKr, + yearNum, monthNum, dayNum, + hasLottoSubscription, +}: Props) { + const today = useMemo(getTodayPillar, []); + const overall = useMemo(() => calcOverallScore(today.stemElem, today.branchElem, yongShin, heeShin), [today, yongShin, heeShin]); + const level = toLevel(overall); + const meta = LEVEL_META[level]; + const areas = useMemo(() => buildAreas(overall, yongShin, heeShin, yearNum, monthNum, dayNum), [overall, yongShin, heeShin, yearNum, monthNum, dayNum]); + const stemRel = getRelation(today.stemElem, yongShin); + const relDesc = REL_DESC(yongShin, yongShinKr)[stemRel]; + + return ( + <> + {/* ── 상단 연결 화살표 ── */} +
    +
    +
    + 사주 분석에서 이어지는 오늘의 운세 +
    +
    +
    + + {/* ── 본문 카드 ── */} +
    + {/* 헤더 */} +
    +
    +
    +
    + ☀️ +
    +
    +

    오늘의 운세

    +

    + {today.year}년 {today.month}월 {today.date}일 · 일진 {today.stem}{today.branch} ({today.stemKr}{today.branchKr}) +

    +
    +
    + + {meta.emoji} {meta.label} + +
    +
    + +
    + {/* 일진 × 용신 분석 */} +
    +
    +
    + {today.stem}{today.branch} +
    +
    +
    + 오늘 일진과 당신의 용신 {yongShin}({yongShinKr}) 분석 +
    +

    + {relDesc} +

    +
    +
    + + {/* 종합 점수 바 */} +
    + 오늘 종합 운세 +
    +
    +
    + {overall}점 +
    +
    + + {/* 5대 운세 그리드 */} +
    +

    오늘의 분야별 운세

    +
    + {areas.map((area) => { + const aLevel = toLevel(area.score); + const aMeta = LEVEL_META[aLevel]; + return ( +
    +
    + {area.icon} +
    +
    +
    + {area.label} + {aMeta.emoji} +
    + +

    {area.desc}

    +
    +
    + ); + })} +
    +
    + + {/* 면책 */} +

    + 오늘의 운세는 당신의 사주 용신({yongShinKr}·{yongShin})과 오늘 일진의 오행 상호작용을 기반으로 합니다.
    + 명리학적 참고 자료이며 결과를 보장하지 않습니다. +

    + + {/* 로또 CTA */} +
    +
    +
    +
    + 🎱 + + {level === 'great' ? '오늘 운이 아주 좋습니다! 로또도 한 번 도전해보세요.' : '사주 기반 행운 번호도 확인해보세요.'} + +
    +

    + 용신 {yongShin}({yongShinKr}) 오행이 담긴 + 사주 기반 로또 번호가 아래에 준비되어 있습니다. + {hasLottoSubscription + ? ' 구독 중이신 로또 서비스의 매주 최신 추천 번호도 함께 확인하세요.' + : ' 로또 구독 시 대운 교차 분석으로 더 정밀한 번호를 매주 받을 수 있어요.'} +

    + { + e.preventDefault(); + document.getElementById('saju-lotto-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }} + className="block w-full text-center bg-gradient-to-r from-amber-500 to-amber-400 hover:from-amber-400 hover:to-amber-300 text-[#04102b] text-sm font-extrabold px-4 py-2.5 rounded-xl transition-all shadow-lg cursor-pointer" + > + 오늘의 로또 번호 추천 보기 ↓ + +
    +
    +
    +
    + + {/* 하단 연결 */} +
    +
    +
    + 🎱 오늘의 운세에서 이어지는 사주 로또 추천 +
    +
    +
    + + ); +} diff --git a/app/work/saju/result/page.tsx b/app/work/saju/result/page.tsx new file mode 100644 index 0000000..f0f485a --- /dev/null +++ b/app/work/saju/result/page.tsx @@ -0,0 +1,627 @@ +import { calculateSaju } from '@/lib/saju-calculator'; +import Link from 'next/link'; +import { calculateDaeun, getCurrentDaeun, getDaeunDescription } from '@/lib/daeun-calculator'; +import { getCurrentSolarTerm, getSolarTermName, getSolarTermMonthBranch } from '@/lib/solar-terms'; +import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju-calculator'; +import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation'; +import { createClient } from '@/lib/supabase/server'; +import SajuAISection from './SajuAISection'; +import SajuFortuneSection from './SajuFortuneSection'; + +interface PageProps { + searchParams: Promise<{ + year: string; + month: string; + day: string; + hour?: string; + gender: 'male' | 'female'; + calendarType: 'solar' | 'lunar'; + originalYear?: string; + originalMonth?: string; + originalDay?: string; + isLeapMonth?: string; + }>; +} + +export default async function SajuResultPage({ searchParams }: PageProps) { + const params = await searchParams; + const { year, month, day, hour, gender, calendarType, originalYear, originalMonth, originalDay, isLeapMonth } = params; + + const yearNum = parseInt(year, 10); + const monthNum = parseInt(month, 10); + const dayNum = parseInt(day, 10); + const hourNum = hour ? parseInt(hour, 10) : null; + + if (isNaN(yearNum) || isNaN(monthNum) || isNaN(dayNum)) { + return ( +
    +
    +

    잘못된 접근입니다. 생년월일을 다시 입력해주세요.

    + 사주 입력하기 +
    +
    + ); + } + + const inputYear = originalYear ? parseInt(originalYear) : yearNum; + const inputMonth = originalMonth ? parseInt(originalMonth) : monthNum; + const inputDay = originalDay ? parseInt(originalDay) : dayNum; + const isLunar = calendarType === 'lunar'; + const isLeap = isLeapMonth === 'true'; + + // ── 사주팔자 계산 (TypeScript — lunar-javascript 기반 정밀 절기 계산) ── + const sajuData = calculateSaju(yearNum, monthNum, dayNum, hourNum, gender); + + // 추가 분석 (신강신약, 용신, 오행균형, 세운) + const analysis = performFullAnalysis(sajuData); + const elementScores = analysis.elementScores; + + // 대운 + const currentYear = new Date().getFullYear(); + const daeunList = calculateDaeun( + yearNum, monthNum, dayNum, gender, + sajuData.month.stem, sajuData.month.branch + ); + const currentDaeun = getCurrentDaeun(daeunList, currentYear); + + // 지지 상호작용 / 신살 / 공망 / 지장간 + const branchInteractions = analysis.branchInteractions; + const shinsal = analysis.shinsal; + const gongmang = analysis.gongmang; + const hiddenStems = analysis.hiddenStems; + + // ── 절기 정보 (표시용) ──────────────────────────────────────────────── + const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum); + const solarTermName = getSolarTermName(solarTermIndex); + + // ── 결제 여부 + 저장된 AI 해석 + 로또 구독 확인 ───────────────────── + let hasPaid = false; + let savedInterpretation: string | null = null; + let hasLottoSubscription = false; + try { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (user) { + // 사주 결제 확인 (anon client — 본인 orders는 RLS 허용 가정) + const { data: order } = await supabase + .from('orders').select('id') + .eq('user_id', user.id).eq('product_id', 'saju_detail').eq('status', 'paid') + .maybeSingle(); + hasPaid = !!order; + + if (hasPaid) { + // 1차: birth_hour 포함 정확한 키로 조회 + const birthKey: Record = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender }; + if (hourNum !== null) birthKey.birth_hour = hourNum; + const { data: record } = await supabase + .from('saju_records').select('interpretation') + .eq('user_id', user.id).eq('is_paid', true) + .contains('saju_data', birthKey).maybeSingle(); + savedInterpretation = record?.interpretation ?? null; + + // 2차 폴백: birth_hour 없이 조회 (시간 입력 안 한 케이스 or 불일치 방지) + if (!savedInterpretation) { + const birthKeyNoHour: Record = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender }; + const { data: record2 } = await supabase + .from('saju_records').select('interpretation') + .eq('user_id', user.id).eq('is_paid', true) + .contains('saju_data', birthKeyNoHour).maybeSingle(); + savedInterpretation = record2?.interpretation ?? null; + } + } + + // 로또 구독 확인 — subscriptions 테이블 (세션 클라이언트로 RLS select_own 통과) + const { data: lottoSub } = await supabase + .from('subscriptions') + .select('id') + .eq('user_id', user.id) + .eq('status', 'active') + .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) + .maybeSingle(); + hasLottoSubscription = !!lottoSub; + + // subscriptions에서 못 찾으면 orders 테이블로 폴백 (구독 마이그레이션 전 데이터) + if (!hasLottoSubscription) { + const now = new Date().toISOString(); + const thirtyOneDaysAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString(); + const { data: lottoOrder } = await supabase + .from('orders') + .select('id, created_at') + .eq('user_id', user.id) + .eq('status', 'paid') + .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) + .gte('created_at', thirtyOneDaysAgo) + .maybeSingle(); + hasLottoSubscription = !!lottoOrder; + } + } + } catch { + // 미로그인 시 무시 + } + + // ── 오행 색상 ────────────────────────────────────────────────────────── + const elementColors: { [k: string]: string } = { + '木': 'text-green-700', '火': 'text-red-600', '土': 'text-yellow-700', + '金': 'text-amber-600', '水': 'text-blue-700', + }; + const elementBgColors: { [k: string]: string } = { + '木': 'bg-green-50 border-green-400', '火': 'bg-red-50 border-red-400', + '土': 'bg-yellow-50 border-yellow-400', '金': 'bg-amber-50 border-amber-400', + '水': 'bg-blue-50 border-blue-400', + }; + + // ── 띠 계산 ──────────────────────────────────────────────────────────── + const zodiacAnimals = ['쥐', '소', '호랑이', '토끼', '용', '뱀', '말', '양', '원숭이', '닭', '개', '돼지']; + const zodiacIdx = (yearNum - 4) % 12; + const zodiacAnimal = zodiacAnimals[zodiacIdx >= 0 ? zodiacIdx : zodiacIdx + 12]; + + const engineBadge = TS 계산; + + return ( +
    + {/* 헤더 */} +
    +
    +
    + + 사주팔자 감정서 +
    +

    사주팔자 분석 결과

    +

    전통 명리학과 AI 기술의 만남

    +
    +
    + +
    +
    + + {/* 사이드바 */} + + + {/* 메인 */} +
    + + {/* 사주팔자 표 */} +
    +

    사주팔자 (四柱八字)

    + +
    + + + + + {sajuData.hour && } + + + + + + + {/* 천간 */} + + + {sajuData.hour && ( + + )} + + + + + + {/* 지지 */} + + + {sajuData.hour && ( + + )} + + + + + + {/* 지장간 */} + + + {(() => { + const order = sajuData.hour + ? ['시주', '일주', '월주', '년주'] + : ['일주', '월주', '년주']; + return order.map((pillarName, idx) => { + const h = hiddenStems.find((hs: any) => hs.pillar === pillarName); + return ( + + ); + }); + })()} + + + {/* 십성 */} + + + {sajuData.hour && ( + + )} + + + + + + {/* 십이운성 */} + + + {sajuData.hour && ( + + )} + + + + + +
    구분시주일주월주년주
    천간 +
    {sajuData.hour.stem}
    +
    {sajuData.hour.stemKr}
    +
    +
    {sajuData.day.stem}
    +
    {sajuData.day.stemKr}
    +
    일간
    +
    +
    {sajuData.month.stem}
    +
    {sajuData.month.stemKr}
    +
    +
    {sajuData.year.stem}
    +
    {sajuData.year.stemKr}
    +
    지지 +
    {sajuData.hour.branch}
    +
    {sajuData.hour.branchKr}
    +
    +
    {sajuData.day.branch}
    +
    {sajuData.day.branchKr}
    +
    +
    {sajuData.month.branch}
    +
    {sajuData.month.branchKr}
    +
    +
    {sajuData.year.branch}
    +
    {sajuData.year.branchKr}
    +
    +
    지장간
    +
    숨은 천간
    +
    + {h && ( +
    + {h.stems.map((s: any, si: number) => ( + + {s.stemKr} + + ))} +
    + )} +
    십성 +
    {sajuData.hour.tenGod}
    +
    +
    {sajuData.day.tenGod}
    +
    +
    {sajuData.month.tenGod}
    +
    +
    {sajuData.year.tenGod}
    +
    십이운성 +
    {sajuData.hour.fortune}
    +
    +
    {sajuData.day.fortune}
    +
    +
    {sajuData.month.fortune}
    +
    +
    {sajuData.year.fortune}
    +
    +
    + + {/* 지지 상호작용 */} + {branchInteractions.length > 0 && ( +
    +

    지지 상호작용

    +
    + {branchInteractions.map((inter: any, idx: number) => { + const isPositive = inter.type.includes('합'); + const isNegative = inter.type.includes('충') || inter.type.includes('형'); + const colorClass = isPositive + ? 'bg-emerald-50 border-emerald-400 text-emerald-800' + : isNegative + ? 'bg-red-50 border-red-400 text-red-800' + : 'bg-amber-50 border-amber-400 text-amber-800'; + return ( + + {inter.type} {inter.branchesKr.join('')} + {inter.resultElement && ` → ${FIVE_ELEMENTS_KR[inter.resultElement as keyof typeof FIVE_ELEMENTS_KR]}`} + + ); + })} +
    +
    + )} + + {/* 오행 균형 */} +
    +

    오행 균형

    +
    + {Object.entries(elementScores).map(([element, score]) => ( +
    +
    {element}
    +
    + {FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR]} +
    +
    +
    +
    +
    {score}%
    +
    + ))} +
    +
    +
    + + {/* 분석 카드 그리드 */} +
    + + {/* 신강/신약 + 용신 */} +
    +

    일간 세력 분석

    +
    + + {analysis.dayMasterStrength.result} + + 점수: {analysis.dayMasterStrength.score} +
    +
      + {analysis.dayMasterStrength.reasons.map((r: string, i: number) => ( +
    • + - + {r} +
    • + ))} +
    + +
    +

    용신 / 희신 / 기신

    +
    + + 용신: {analysis.yongShin.yongShinKr} + + + 희신: {analysis.yongShin.heeShinKr} + + + 기신: {analysis.yongShin.giShinKr} + +
    +

    {analysis.yongShin.explanation}

    +
    +
    + + {/* 신살 + 공망 */} +
    +

    신살 (神煞)

    + {shinsal.length > 0 ? ( +
    + {shinsal.map((s: any, i: number) => ( +
    + + {s.name} + +
    +
    + {s.pillar} {s.branchKr} +
    +
    {s.description}
    +
    +
    + ))} +
    + ) : ( +

    특별한 신살이 발견되지 않았습니다.

    + )} + +
    +

    공망 (空亡)

    +
    + {gongmang.branchesKr.map((bk: string, i: number) => ( + + {bk} + + ))} +
    +

    {gongmang.description}

    +
    + + {/* 세운 정보 */} +
    +

    + {analysis.seun.year}년 세운 +

    +
    + + {analysis.seun.stemKr}{analysis.seun.branchKr} + + {analysis.seun.elementKr} 기운 +
    + {analysis.seun.interactions.length > 0 && ( +
    + {analysis.seun.interactions.map((si: any, i: number) => ( + + {si.type} {si.branchesKr.join('')} + + ))} +
    + )} +
    +
    +
    + + {/* AI 상세 해석 섹션 */} + {(() => { + const birthKey = { + birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender, + ...(hourNum !== null ? { birth_hour: hourNum } : {}), + }; + const currentUrl = `/work/saju/result?year=${yearNum}&month=${monthNum}&day=${dayNum}${hourNum !== null ? `&hour=${hourNum}` : ''}&gender=${gender}&calendarType=${calendarType}${originalYear ? `&originalYear=${originalYear}&originalMonth=${originalMonth}&originalDay=${originalDay}` : ''}${isLeap ? '&isLeapMonth=true' : ''}`; + return ( + + ); + })()} + + {/* 오늘의 운세 (사주 결제 시 표시) */} + {hasPaid && ( + + )} + + {/* 대운 */} +
    +

    + 대운 (大運) — 10년 주기 운세 +

    + + {currentDaeun && ( +
    +

    현재 대운

    +
    +
    + {currentDaeun.stem}{currentDaeun.branch} +
    +
    + {currentDaeun.stemKr}{currentDaeun.branchKr} +
    +
    + {currentDaeun.age}세 ~ {currentDaeun.age + 9}세 ({currentDaeun.startYear} ~ {currentDaeun.endYear}년) +
    +
    +

    + {getDaeunDescription(currentDaeun, sajuData.day.stem)} +

    +
    + )} + +
    + {daeunList.map((daeun: any, index: number) => { + const isCurrent = currentDaeun && + daeun.startYear === currentDaeun.startYear && + daeun.endYear === currentDaeun.endYear; + return ( +
    +
    +
    {daeun.stem}{daeun.branch}
    +
    {daeun.stemKr}{daeun.branchKr}
    +
    {daeun.age}세 ~ {daeun.age + 9}세
    +
    {daeun.startYear} ~ {daeun.endYear}
    + {isCurrent && ( +
    + 현재 +
    + )} +
    +
    + ); + })} +
    +
    + +
    +
    +
    +
    + ); +}