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 (
+
+ );
+}
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원
+
+
+
+ {/* 무료 */}
+
+
+
+ {[
+ '사주팔자 원국 (년·월·일·시주)',
+ '천간·지지·지장간 표',
+ '십성 및 십이운성',
+ '오행 분포 차트',
+ '지지 상호작용 (합·충·형)',
+ '일간 분석 요약',
+ ].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 ─── */}
+
+
+
+ {faqItems.map((item, i) => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
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 (
+
+ {/* 헤더 */}
+
+ {/* 번호 아이콘 */}
+
+ {section.number > 0 ? section.number : meta.icon}
+
+
+
+
+
+ {meta.badgeText}
+
+
+ {section.title}
+
+
+
+
+ {/* 토글 화살표 */}
+
+
+
+
+
+ {/* 내용 (아코디언) */}
+ {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}
+
+ ))}
+
+
+
+ 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 해석 생성에 실패했습니다.
+
{ called.current = false; setStatus('idle'); }}
+ className="text-xs text-blue-600 underline"
+ >
+ 다시 시도하기
+
+
+ );
+ }
+
+ // ── 해석 완료 ─────────────────────────────────────────────────────
+ 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 (
+
+ );
+}
+
+// ── 메인 컴포넌트 ─────────────────────────────────────────────────────
+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 */}
+
+
+
+
+ {/* 하단 연결 */}
+
+
+
+ 🎱 오늘의 운세에서 이어지는 사주 로또 추천
+
+
+
+ >
+ );
+}
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.stem}
+ {sajuData.hour.stemKr}
+
+ )}
+
+ {sajuData.day.stem}
+ {sajuData.day.stemKr}
+ 일간
+
+
+ {sajuData.month.stem}
+ {sajuData.month.stemKr}
+
+
+ {sajuData.year.stem}
+ {sajuData.year.stemKr}
+
+
+
+ {/* 지지 */}
+
+ 지지
+ {sajuData.hour && (
+
+ {sajuData.hour.branch}
+ {sajuData.hour.branchKr}
+
+ )}
+
+ {sajuData.day.branch}
+ {sajuData.day.branchKr}
+
+
+ {sajuData.month.branch}
+ {sajuData.month.branchKr}
+
+
+ {sajuData.year.branch}
+ {sajuData.year.branchKr}
+
+
+
+ {/* 지장간 */}
+
+
+ 지장간
+ 숨은 천간
+
+ {(() => {
+ const order = sajuData.hour
+ ? ['시주', '일주', '월주', '년주']
+ : ['일주', '월주', '년주'];
+ return order.map((pillarName, idx) => {
+ const h = hiddenStems.find((hs: any) => hs.pillar === pillarName);
+ return (
+
+ {h && (
+
+ {h.stems.map((s: any, si: number) => (
+
+ {s.stemKr}
+
+ ))}
+
+ )}
+
+ );
+ });
+ })()}
+
+
+ {/* 십성 */}
+
+ 십성
+ {sajuData.hour && (
+
+ {sajuData.hour.tenGod}
+
+ )}
+
+ {sajuData.day.tenGod}
+
+
+ {sajuData.month.tenGod}
+
+
+ {sajuData.year.tenGod}
+
+
+
+ {/* 십이운성 */}
+
+ 십이운성
+ {sajuData.hour && (
+
+ {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 && (
+
+ 현재
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+}