feat(work): /work/saju + input + result — 현 /saju 컨텐츠 이동
- 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) <noreply@anthropic.com>
This commit is contained in:
220
app/work/saju/components/SajuForm.tsx
Normal file
220
app/work/saju/components/SajuForm.tsx
Normal file
@@ -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 (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* 생년월일 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||||
|
생년월일
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="년 (예: 1990)"
|
||||||
|
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||||
|
min="1900"
|
||||||
|
max="2100"
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => setYear(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="월 (1-12)"
|
||||||
|
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||||
|
min="1"
|
||||||
|
max="12"
|
||||||
|
value={month}
|
||||||
|
onChange={(e) => setMonth(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="일 (1-31)"
|
||||||
|
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||||
|
min="1"
|
||||||
|
max="31"
|
||||||
|
value={day}
|
||||||
|
onChange={(e) => setDay(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 태어난 시간 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||||
|
태어난 시간 (선택)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||||
|
value={hour}
|
||||||
|
onChange={(e) => setHour(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">모름 / 시간 선택 안함</option>
|
||||||
|
<option value="0">자시 (子時) 23:00 - 01:00</option>
|
||||||
|
<option value="1">축시 (丑時) 01:00 - 03:00</option>
|
||||||
|
<option value="3">인시 (寅時) 03:00 - 05:00</option>
|
||||||
|
<option value="5">묘시 (卯時) 05:00 - 07:00</option>
|
||||||
|
<option value="7">진시 (辰時) 07:00 - 09:00</option>
|
||||||
|
<option value="9">사시 (巳時) 09:00 - 11:00</option>
|
||||||
|
<option value="11">오시 (午時) 11:00 - 13:00</option>
|
||||||
|
<option value="13">미시 (未時) 13:00 - 15:00</option>
|
||||||
|
<option value="15">신시 (申時) 15:00 - 17:00</option>
|
||||||
|
<option value="17">유시 (酉時) 17:00 - 19:00</option>
|
||||||
|
<option value="19">술시 (戌時) 19:00 - 21:00</option>
|
||||||
|
<option value="21">해시 (亥時) 21:00 - 23:00</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 양력/음력 선택 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||||
|
생일 구분
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCalendarType('solar')}
|
||||||
|
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||||
|
calendarType === 'solar'
|
||||||
|
? 'bg-[#1a56db] text-white shadow-lg'
|
||||||
|
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
양력
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCalendarType('lunar')}
|
||||||
|
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||||
|
calendarType === 'lunar'
|
||||||
|
? 'bg-[#1a56db] text-white shadow-lg'
|
||||||
|
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
음력
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{calendarType === 'lunar' && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="flex items-center justify-center gap-2 text-sm text-slate-500 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isLeapMonth}
|
||||||
|
onChange={(e) => setIsLeapMonth(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-[#1a56db] border-gray-300 rounded focus:ring-[#1a56db]"
|
||||||
|
/>
|
||||||
|
<span>윤달</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 성별 선택 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||||
|
성별
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGender('male')}
|
||||||
|
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||||
|
gender === 'male'
|
||||||
|
? 'bg-[#1a56db] text-white shadow-lg'
|
||||||
|
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
남성
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGender('female')}
|
||||||
|
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||||
|
gender === 'female'
|
||||||
|
? 'bg-[#1a56db] text-white shadow-lg'
|
||||||
|
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
여성
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 제출 버튼 */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-gradient-to-r from-[#1a56db] to-[#7c3aed] hover:from-[#1e4fc2] hover:to-[#6d28d9] text-white py-4 rounded-xl text-lg font-bold transition shadow-lg hover:shadow-xl hover:scale-[1.02]"
|
||||||
|
>
|
||||||
|
내 사주 보기 →
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-500 text-center">
|
||||||
|
* 태어난 시간을 정확히 아시면 더 정확한 사주를 확인할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
app/work/saju/input/page.tsx
Normal file
41
app/work/saju/input/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import SajuForm from '@/app/work/saju/components/SajuForm';
|
||||||
|
|
||||||
|
export default function SajuInputPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-[#f0f5ff]">
|
||||||
|
{/* Hero */}
|
||||||
|
<div className="relative overflow-hidden px-6 py-12"
|
||||||
|
style={{ background: '#04102b', backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}>
|
||||||
|
|
||||||
|
<div className="relative max-w-xl mx-auto text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-violet-400/10 border border-violet-400/25 text-violet-300 text-xs font-semibold px-4 py-1.5 rounded-full mb-4 tracking-wide">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||||
|
AI 사주 분석 · 생년월일 입력
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-extrabold text-white leading-tight mb-3 tracking-tight">
|
||||||
|
생년월일을 입력해주세요
|
||||||
|
</h1>
|
||||||
|
<p className="text-blue-200/60 text-sm leading-relaxed">
|
||||||
|
정확한 생년월일과 태어난 시간을 입력하면<br />
|
||||||
|
더 정밀한 사주팔자를 계산할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form 영역 */}
|
||||||
|
<div className="px-6 py-10 max-w-2xl mx-auto">
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-8 shadow-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-6">
|
||||||
|
<div className="w-1 h-5 bg-gradient-to-b from-[#1a56db] to-[#7c3aed] rounded-full" />
|
||||||
|
<h2 className="font-bold text-[#04102b] text-base">기본 정보 입력</h2>
|
||||||
|
</div>
|
||||||
|
<SajuForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-xs text-slate-400 mt-6">
|
||||||
|
입력하신 정보는 사주 계산에만 사용되며 별도로 저장되지 않습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
app/work/saju/layout.tsx
Normal file
27
app/work/saju/layout.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
334
app/work/saju/page.tsx
Normal file
334
app/work/saju/page.tsx
Normal file
@@ -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<SajuRecord[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="min-h-full bg-[#f0f5ff]">
|
||||||
|
{/* ─── Hero ─── */}
|
||||||
|
<div className="relative overflow-hidden bg-[#04102b] px-6 py-14 lg:px-12" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
|
||||||
|
|
||||||
|
<div className="relative max-w-3xl mx-auto">
|
||||||
|
<div className="flex items-center gap-2 mb-5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||||
|
<span className="text-violet-300/70 text-xs font-mono tracking-widest uppercase">전통 명리학 × AI 해석 · 무료</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-extrabold text-white leading-tight mb-5 tracking-tight">
|
||||||
|
AI가 분석하는<br />
|
||||||
|
<span className="text-amber-400">사주팔자</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-blue-200/70 text-base md:text-lg leading-relaxed mb-8 max-w-xl mx-auto">
|
||||||
|
수천 년의 동양 명리학과 최신 AI 기술의 만남.<br />
|
||||||
|
태어난 순간의 우주적 에너지를 12가지 항목으로 해석해드립니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 이전 기록 있으면 분기 버튼, 없으면 단일 CTA */}
|
||||||
|
{authChecked && hasPaid ? (
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||||
|
<Link
|
||||||
|
href="/work/saju/input"
|
||||||
|
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-7 py-3.5 rounded-xl font-semibold text-base transition-all"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
새로 보기
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#past-records"
|
||||||
|
className="inline-flex items-center gap-2 bg-white/10 border border-white/20 text-white px-7 py-3.5 rounded-xl font-semibold text-base transition-all hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
이전 내역 다시 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href="/work/saju/input"
|
||||||
|
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#1a56db] to-[#7c3aed] hover:from-[#1e4fc2] hover:to-[#6d28d9] text-white px-8 py-3.5 rounded-xl font-semibold text-base transition-all shadow-lg shadow-violet-900/40"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||||
|
</svg>
|
||||||
|
지금 바로 시작하기
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-12 lg:px-12">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-10">
|
||||||
|
|
||||||
|
{/* ─── 이전 기록 섹션 (구매한 유저만) ─── */}
|
||||||
|
{hasPaid && paidRecords.length > 0 && (
|
||||||
|
<div id="past-records">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<p className="text-violet-600 text-xs font-bold uppercase tracking-widest mb-2">MY RECORDS</p>
|
||||||
|
<h2 className="text-2xl font-extrabold text-[#04102b]">이전 AI 사주 기록</h2>
|
||||||
|
<p className="text-slate-500 text-sm mt-1">결제한 사주 기록을 다시 확인하세요</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{paidRecords.map((rec) => (
|
||||||
|
<div key={rec.id} className="bg-white rounded-2xl border border-[#dbe8ff] p-5 hover:border-violet-300 transition-colors">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-1">
|
||||||
|
{new Date(rec.created_at).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||||
|
</div>
|
||||||
|
<div className="font-bold text-[#04102b] text-base">
|
||||||
|
{rec.saju_data.birth_year ?? '?'}년{' '}
|
||||||
|
{rec.saju_data.birth_month ?? '?'}월{' '}
|
||||||
|
{rec.saju_data.birth_day ?? '?'}일생
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-500 mt-0.5">
|
||||||
|
{rec.saju_data.gender === 'male' ? '남성' : '여성'}
|
||||||
|
{rec.saju_data.birth_hour != null ? ` · ${rec.saju_data.birth_hour}시생` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold px-2 py-1 rounded-lg bg-amber-50 text-amber-600 border border-amber-200 flex-shrink-0">
|
||||||
|
AI 해석 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{rec.interpretation && (
|
||||||
|
<p className="text-xs text-slate-500 bg-slate-50 rounded-lg px-3 py-2 mb-3 line-clamp-2">
|
||||||
|
{rec.interpretation.replace(/[#*]/g, '').substring(0, 80)}...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
href={buildResultUrl(rec)}
|
||||||
|
className="block w-full text-center py-2 rounded-xl text-sm font-bold bg-[#04102b] hover:bg-[#0a1f5c] text-white border border-[#1a3a7a] transition"
|
||||||
|
>
|
||||||
|
다시 보기 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ─── 바로 시작하기 CTA ─── */}
|
||||||
|
<div
|
||||||
|
className="rounded-2xl border border-[#1a3a7a] p-8 text-center"
|
||||||
|
style={{
|
||||||
|
background: '#04102b',
|
||||||
|
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 className="text-2xl font-extrabold text-white mb-2">지금 무료로 시작하세요</h3>
|
||||||
|
<p className="text-blue-200/60 text-sm mb-6">회원가입 없이, 생년월일만 입력하면 바로 확인 가능합니다</p>
|
||||||
|
<Link
|
||||||
|
href="/work/saju/input"
|
||||||
|
className="inline-flex items-center gap-2 bg-amber-400 hover:bg-amber-300 text-[#04102b] px-8 py-3.5 rounded-xl font-bold text-base transition-all"
|
||||||
|
>
|
||||||
|
사주 입력하러 가기 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── 무료 vs 유료 비교표 ─── */}
|
||||||
|
<div>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PRICING</p>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b] tracking-tight">무엇을 분석해드리나요</h2>
|
||||||
|
<p className="text-slate-500 text-sm mt-2">기본 원국은 무료, AI 상세 해석은 1,000원</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* 무료 */}
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-5">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-[#1a56db]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-slate-500 uppercase tracking-wide">FREE</div>
|
||||||
|
<div className="text-lg font-extrabold text-[#04102b]">무료 기본 분석</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{[
|
||||||
|
'사주팔자 원국 (년·월·일·시주)',
|
||||||
|
'천간·지지·지장간 표',
|
||||||
|
'십성 및 십이운성',
|
||||||
|
'오행 분포 차트',
|
||||||
|
'지지 상호작용 (합·충·형)',
|
||||||
|
'일간 분석 요약',
|
||||||
|
].map((item) => (
|
||||||
|
<li key={item} className="flex items-center gap-2.5 text-sm text-slate-700">
|
||||||
|
<div className="w-4 h-4 rounded-full bg-blue-100 border border-blue-200 flex items-center justify-center flex-shrink-0">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-[#1a56db]" />
|
||||||
|
</div>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-6 pt-5 border-t border-slate-100">
|
||||||
|
<div className="text-2xl font-extrabold text-[#04102b]">무료</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">회원가입 불필요</div>
|
||||||
|
<Link
|
||||||
|
href="/work/saju/input"
|
||||||
|
className="mt-4 block w-full text-center py-2.5 rounded-xl text-sm font-bold bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] hover:bg-blue-50 transition"
|
||||||
|
>
|
||||||
|
무료로 시작하기
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI 해석 (현재 무료) */}
|
||||||
|
<div
|
||||||
|
className="rounded-2xl border border-[#1a3a7a] p-6 shadow-lg relative overflow-hidden"
|
||||||
|
style={{
|
||||||
|
background: '#04102b',
|
||||||
|
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute top-4 right-4 bg-amber-400 text-[#04102b] text-xs font-bold px-2 py-0.5 rounded-lg">
|
||||||
|
1,000원
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 mb-5 relative">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-violet-500/20 border border-violet-400/30 flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-violet-300 uppercase tracking-wide">AI PREMIUM</div>
|
||||||
|
<div className="text-lg font-extrabold text-white">AI 상세 해석</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-3 relative">
|
||||||
|
{[
|
||||||
|
'무료 기본 분석 전체 포함',
|
||||||
|
'신강/신약 정밀 판단',
|
||||||
|
'용신·희신·기신 추정',
|
||||||
|
'대운 (10년 주기) 분석',
|
||||||
|
'올해 세운 흐름',
|
||||||
|
'Gemini 2.5 Pro AI 12가지 상세 해석',
|
||||||
|
].map((item) => (
|
||||||
|
<li key={item} className="flex items-center gap-2.5 text-sm text-blue-200">
|
||||||
|
<div className="w-4 h-4 rounded-full bg-amber-400/20 border border-amber-400/40 flex items-center justify-center flex-shrink-0">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
||||||
|
</div>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-6 pt-5 border-t border-white/10 relative">
|
||||||
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
|
<span className="text-2xl font-extrabold text-white">1,000원</span>
|
||||||
|
<span className="text-xs text-blue-300/50">/ 1회</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-300/70 mt-1 mb-4">로그인 후 결제 · 12가지 항목 AI 해석</div>
|
||||||
|
<Link
|
||||||
|
href="/work/saju/input"
|
||||||
|
className="block w-full text-center py-3 rounded-xl text-sm font-bold transition bg-amber-400 text-[#04102b] hover:bg-amber-300"
|
||||||
|
>
|
||||||
|
사주 분석 시작하기 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── FAQ ─── */}
|
||||||
|
<div>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">FAQ</p>
|
||||||
|
<h2 className="text-2xl font-extrabold text-[#04102b]">자주 묻는 질문</h2>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{faqItems.map((item, i) => (
|
||||||
|
<div key={i} className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-full bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
<span className="text-[#1a56db] text-xs font-bold">Q</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-[#04102b] text-sm mb-2">{item.q}</p>
|
||||||
|
<p className="text-slate-600 text-sm leading-relaxed">{item.a}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
443
app/work/saju/result/SajuAISection.tsx
Normal file
443
app/work/saju/result/SajuAISection.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={`rounded-2xl border-2 ${meta.border} bg-white overflow-hidden shadow-sm transition-all`}>
|
||||||
|
{/* 헤더 */}
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-full flex items-center gap-3 p-4 text-left hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* 번호 아이콘 */}
|
||||||
|
<div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${meta.gradient} flex items-center justify-center text-white font-extrabold text-sm flex-shrink-0 shadow-sm`}>
|
||||||
|
{section.number > 0 ? section.number : meta.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className={`text-[11px] font-bold px-2 py-0.5 rounded-full border ${meta.badge}`}>
|
||||||
|
{meta.badgeText}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-extrabold text-[#04102b] text-sm leading-snug">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 토글 화살표 */}
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 text-slate-400 flex-shrink-0 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 내용 (아코디언) */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-5 pb-5 pt-1 border-t border-slate-100">
|
||||||
|
<div className={`text-[11px] font-semibold mb-3 flex items-center gap-1.5 ${meta.badge.includes('violet') ? 'text-violet-400' : 'text-slate-400'}`}>
|
||||||
|
<span className="text-base">{meta.icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="prose prose-sm max-w-none text-slate-700 leading-relaxed">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
h1: ({ children }) => <h1 className="text-base font-extrabold text-[#04102b] mt-4 mb-2">{children}</h1>,
|
||||||
|
h2: ({ children }) => <h2 className="text-sm font-extrabold text-[#04102b] mt-3 mb-1.5">{children}</h2>,
|
||||||
|
h3: ({ children }) => <h3 className="text-sm font-bold text-[#04102b] mt-2 mb-1">{children}</h3>,
|
||||||
|
p: ({ children }) => <p className="mb-3 text-sm leading-relaxed text-slate-700">{children}</p>,
|
||||||
|
strong: ({ children }) => <strong className="font-bold text-[#04102b]">{children}</strong>,
|
||||||
|
em: ({ children }) => <em className="italic text-slate-600">{children}</em>,
|
||||||
|
ul: ({ children }) => <ul className="list-disc list-inside space-y-1.5 mb-3 text-sm text-slate-700 pl-1">{children}</ul>,
|
||||||
|
ol: ({ children }) => <ol className="list-decimal list-inside space-y-1.5 mb-3 text-sm text-slate-700 pl-1">{children}</ol>,
|
||||||
|
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="border-l-4 border-violet-300 pl-4 py-1 my-3 text-slate-600 bg-violet-50 rounded-r-lg text-sm italic">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
hr: () => <hr className="border-slate-200 my-4" />,
|
||||||
|
code: ({ children }) => (
|
||||||
|
<code className="bg-slate-100 text-violet-700 px-1.5 py-0.5 rounded text-xs font-mono">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{section.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Set<number>>(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 (
|
||||||
|
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] rounded-2xl border border-[#1a3a7a] p-7 text-center relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-[0.05]"
|
||||||
|
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '22px 22px' }} />
|
||||||
|
<div className="relative">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-amber-400/10 border border-amber-400/25 text-amber-300 text-xs font-semibold px-3 py-1 rounded-full mb-3">
|
||||||
|
AI PREMIUM
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-extrabold text-white mb-2">AI 상세 해석 (12개 항목)</h3>
|
||||||
|
<p className="text-blue-200/60 text-sm mb-6">
|
||||||
|
성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등<br />
|
||||||
|
Gemini 2.5 Pro가 생성하는 맞춤형 사주 해석을 받아보세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 미리보기 섹션 목록 */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-6 text-left">
|
||||||
|
{SECTION_META.map((meta, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2 py-1.5">
|
||||||
|
<span className="text-sm">{meta.icon}</span>
|
||||||
|
<span className="text-xs text-blue-200/70 font-medium">{meta.badgeText}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PaymentButton
|
||||||
|
productId="saju_detail"
|
||||||
|
className="inline-flex items-center gap-2 bg-amber-400 hover:bg-amber-300 text-[#04102b] font-bold px-7 py-3 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
AI 상세 해석 받기 — 1,000원
|
||||||
|
</PaymentButton>
|
||||||
|
<p className="text-blue-200/40 text-xs mt-3">결제 후 즉시 AI 분석 시작 · 로그인 필요</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 로딩 ──────────────────────────────────────────────────────────
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-8 text-center">
|
||||||
|
<div className="w-10 h-10 border-2 border-violet-600 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-slate-500 text-sm font-medium">AI가 사주를 분석하는 중입니다...</p>
|
||||||
|
<p className="text-slate-400 text-xs mt-1">약 20~30초 소요될 수 있습니다</p>
|
||||||
|
<div className="mt-5 flex flex-wrap justify-center gap-2">
|
||||||
|
{SECTION_META.map((meta, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-1 text-xs text-slate-400 animate-pulse">
|
||||||
|
<span>{meta.icon}</span>{meta.badgeText}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 오류 ──────────────────────────────────────────────────────────
|
||||||
|
if (status === 'error') {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-2xl border border-red-200 p-6 text-center">
|
||||||
|
<p className="text-red-500 text-sm font-medium mb-3">AI 해석 생성에 실패했습니다.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { called.current = false; setStatus('idle'); }}
|
||||||
|
className="text-xs text-blue-600 underline"
|
||||||
|
>
|
||||||
|
다시 시도하기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 해석 완료 ─────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="flex items-center gap-2 px-6 py-4 border-b border-slate-100 bg-gradient-to-r from-[#04102b] to-[#0a1f5c]">
|
||||||
|
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-violet-400 to-amber-400 flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-sm font-extrabold text-white">AI 상세 해석</h2>
|
||||||
|
<p className="text-blue-300/60 text-[11px]">12개 항목 · 클릭해서 펼쳐보세요</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
title="AI 해석 재생성"
|
||||||
|
className="text-[11px] text-blue-300/60 hover:text-blue-200 px-2 py-1 rounded-lg hover:bg-white/10 transition-all flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
재생성
|
||||||
|
</button>
|
||||||
|
<span className="text-xs bg-emerald-400/20 border border-emerald-400/30 text-emerald-300 font-bold px-2.5 py-1 rounded-full">
|
||||||
|
결제 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 섹션 컨트롤 + 목록 */}
|
||||||
|
<div className="p-5">
|
||||||
|
{/* 전체 펼치기/접기 */}
|
||||||
|
{sections.length > 1 && (
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="text-xs text-slate-400 font-medium">
|
||||||
|
총 {sections.length}개 항목
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={expandAll}
|
||||||
|
className="text-xs text-violet-600 hover:text-violet-800 font-semibold px-3 py-1 rounded-lg border border-violet-200 hover:bg-violet-50 transition-colors"
|
||||||
|
>
|
||||||
|
전체 펼치기
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={collapseAll}
|
||||||
|
className="text-xs text-slate-500 hover:text-slate-700 font-semibold px-3 py-1 rounded-lg border border-slate-200 hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
전체 접기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 섹션 카드 목록 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{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 (
|
||||||
|
<SectionCard
|
||||||
|
key={idx}
|
||||||
|
section={section}
|
||||||
|
meta={meta}
|
||||||
|
isOpen={openSections.has(idx)}
|
||||||
|
onToggle={() => toggleSection(idx)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 하단 안내 */}
|
||||||
|
{sections.length > 0 && (
|
||||||
|
<p className="text-center text-xs text-slate-400 mt-5">
|
||||||
|
해석은 사주 데이터를 기반으로 AI가 생성한 내용입니다. 참고용으로 활용해주세요.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
323
app/work/saju/result/SajuFortuneSection.tsx
Normal file
323
app/work/saju/result/SajuFortuneSection.tsx
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
// ── 천간 / 지지 ───────────────────────────────────────────────────────
|
||||||
|
const STEMS = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
|
||||||
|
const STEMS_KR = ['갑','을','병','정','무','기','경','신','임','계'];
|
||||||
|
const BRANCHES = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
|
||||||
|
const BRANCHES_KR= ['자','축','인','묘','진','사','오','미','신','유','술','해'];
|
||||||
|
|
||||||
|
const STEM_ELEM: Record<string,string> = { '甲':'木','乙':'木','丙':'火','丁':'火','戊':'土','己':'土','庚':'金','辛':'金','壬':'水','癸':'水' };
|
||||||
|
const BRANCH_ELEM: Record<string,string> = { '子':'水','亥':'水','寅':'木','卯':'木','巳':'火','午':'火','申':'金','酉':'金','丑':'土','辰':'土','未':'土','戌':'土' };
|
||||||
|
|
||||||
|
// 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<string,string> = { '木':'火','火':'土','土':'金','金':'水','水':'木' };
|
||||||
|
const OVERCOMES: Record<string,string> = { '木':'土','火':'金','土':'水','金':'木','水':'火' };
|
||||||
|
|
||||||
|
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<string, Record<Level, string>> = {
|
||||||
|
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<Level, { emoji: string; label: string; bar: string; bg: string; border: string; text: string; badge: string }> = {
|
||||||
|
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<Rel, string> = (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 (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<div className="flex-1 h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
||||||
|
<div style={{ width: `${score}%`, background: color, transition: 'width 0.8s ease' }} className="h-full rounded-full" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-bold w-6 text-right" style={{ color }}>{score}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 메인 컴포넌트 ─────────────────────────────────────────────────────
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
{/* ── 상단 연결 화살표 ── */}
|
||||||
|
<div className="flex flex-col items-center gap-0 py-1">
|
||||||
|
<div className="w-px h-5 bg-gradient-to-b from-blue-200 to-amber-300" />
|
||||||
|
<div className="flex items-center gap-2 px-4 py-1.5 bg-amber-50 border border-amber-200 rounded-full text-[11px] font-bold text-amber-700">
|
||||||
|
<span>✨</span> 사주 분석에서 이어지는 오늘의 운세
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-5 bg-gradient-to-b from-amber-300 to-amber-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── 본문 카드 ── */}
|
||||||
|
<div id="today-fortune" className="bg-white rounded-2xl border border-amber-200 overflow-hidden shadow-sm">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="bg-gradient-to-r from-[#1a0a00] via-[#3d1a00] to-[#1a0a00] px-6 py-5">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center flex-shrink-0 shadow-md text-xl">
|
||||||
|
☀️
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-extrabold text-white">오늘의 운세</h2>
|
||||||
|
<p className="text-amber-300/70 text-[11px] mt-0.5">
|
||||||
|
{today.year}년 {today.month}월 {today.date}일 · 일진 {today.stem}{today.branch} ({today.stemKr}{today.branchKr})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`text-[11px] font-extrabold px-3 py-1.5 rounded-full border ${meta.badge} flex-shrink-0`}>
|
||||||
|
{meta.emoji} {meta.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 space-y-5">
|
||||||
|
{/* 일진 × 용신 분석 */}
|
||||||
|
<div className={`rounded-xl border p-4 ${meta.bg} ${meta.border}`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center font-bold text-lg flex-shrink-0 ${meta.text}`}
|
||||||
|
style={{ background: 'rgba(255,255,255,0.65)' }}>
|
||||||
|
{today.stem}{today.branch}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className={`text-xs font-extrabold mb-1 ${meta.text}`}>
|
||||||
|
오늘 일진과 당신의 용신 {yongShin}({yongShinKr}) 분석
|
||||||
|
</div>
|
||||||
|
<p className={`text-xs leading-relaxed ${meta.text}`} style={{ opacity: 0.88 }}>
|
||||||
|
{relDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 종합 점수 바 */}
|
||||||
|
<div className="mt-3 flex items-center gap-3">
|
||||||
|
<span className="text-[11px] font-bold text-slate-500">오늘 종합 운세</span>
|
||||||
|
<div className="flex-1 h-2.5 bg-white/70 rounded-full overflow-hidden border border-white/50">
|
||||||
|
<div
|
||||||
|
style={{ width: `${overall}%`, background: `linear-gradient(90deg, ${meta.bar}cc, ${meta.bar})` }}
|
||||||
|
className="h-full rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-extrabold" style={{ color: meta.bar }}>{overall}점</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5대 운세 그리드 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-extrabold text-[#04102b] mb-3">오늘의 분야별 운세</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{areas.map((area) => {
|
||||||
|
const aLevel = toLevel(area.score);
|
||||||
|
const aMeta = LEVEL_META[aLevel];
|
||||||
|
return (
|
||||||
|
<div key={area.label} className="flex gap-3 items-start">
|
||||||
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-sm ${aMeta.bg} border ${aMeta.border}`}>
|
||||||
|
{area.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-0.5">
|
||||||
|
<span className="text-xs font-bold text-[#04102b]">{area.label}</span>
|
||||||
|
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded-full border ${aMeta.badge}`}>{aMeta.emoji}</span>
|
||||||
|
</div>
|
||||||
|
<ScoreBar score={area.score} color={aMeta.bar} />
|
||||||
|
<p className="text-[11px] text-slate-500 mt-1 leading-relaxed">{area.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 면책 */}
|
||||||
|
<p className="text-center text-[11px] text-slate-400 leading-relaxed">
|
||||||
|
오늘의 운세는 당신의 사주 용신({yongShinKr}·{yongShin})과 오늘 일진의 오행 상호작용을 기반으로 합니다.<br />
|
||||||
|
명리학적 참고 자료이며 결과를 보장하지 않습니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 로또 CTA */}
|
||||||
|
<div className="rounded-2xl bg-gradient-to-br from-[#04102b] via-[#0d1f5c] to-[#04102b] border border-[#1a3a7a] p-5 relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-[0.04]"
|
||||||
|
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '20px 20px' }} />
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-base">🎱</span>
|
||||||
|
<span className="text-xs font-extrabold text-amber-300">
|
||||||
|
{level === 'great' ? '오늘 운이 아주 좋습니다! 로또도 한 번 도전해보세요.' : '사주 기반 행운 번호도 확인해보세요.'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-blue-200/70 leading-relaxed mb-4">
|
||||||
|
용신 <strong className="text-amber-300">{yongShin}({yongShinKr})</strong> 오행이 담긴
|
||||||
|
사주 기반 로또 번호가 아래에 준비되어 있습니다.
|
||||||
|
{hasLottoSubscription
|
||||||
|
? ' 구독 중이신 로또 서비스의 매주 최신 추천 번호도 함께 확인하세요.'
|
||||||
|
: ' 로또 구독 시 대운 교차 분석으로 더 정밀한 번호를 매주 받을 수 있어요.'}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#saju-lotto-section"
|
||||||
|
onClick={e => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
오늘의 로또 번호 추천 보기 ↓
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 하단 연결 */}
|
||||||
|
<div className="flex flex-col items-center gap-0 py-1">
|
||||||
|
<div className="w-px h-5 bg-gradient-to-b from-amber-200 to-blue-300" />
|
||||||
|
<div className="flex items-center gap-2 px-4 py-1.5 bg-blue-50 border border-blue-200 rounded-full text-[11px] font-bold text-blue-700">
|
||||||
|
<span>🎱</span> 오늘의 운세에서 이어지는 사주 로또 추천
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-5 bg-gradient-to-b from-blue-200 to-transparent" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
627
app/work/saju/result/page.tsx
Normal file
627
app/work/saju/result/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center">
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<p className="text-slate-500 text-sm mb-4">잘못된 접근입니다. 생년월일을 다시 입력해주세요.</p>
|
||||||
|
<a href="/work/saju/input" className="text-blue-600 underline text-sm">사주 입력하기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown> = { 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<string, unknown> = { 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 = <span className="text-[10px] bg-blue-50 border border-blue-200 text-blue-600 px-2 py-0.5 rounded-full font-semibold">TS 계산</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-[#f0f5ff]">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] px-6 py-10">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-violet-400/10 border border-violet-400/25 text-violet-300 text-xs font-semibold px-4 py-1.5 rounded-full mb-4">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
||||||
|
사주팔자 감정서
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-white mb-2">사주팔자 분석 결과</h1>
|
||||||
|
<p className="text-blue-200/60 text-sm">전통 명리학과 AI 기술의 만남</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-8 max-w-4xl mx-auto">
|
||||||
|
<div className="grid lg:grid-cols-[280px_1fr] gap-6">
|
||||||
|
|
||||||
|
{/* 사이드바 */}
|
||||||
|
<aside className="lg:sticky lg:top-6 h-fit">
|
||||||
|
<div className="bg-[#04102b] rounded-2xl p-6 text-white">
|
||||||
|
<h2 className="text-base font-bold mb-5 text-center pb-4 border-b border-white/10">기본 정보</h2>
|
||||||
|
<div className="space-y-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">생년월일</div>
|
||||||
|
<div className="font-bold">
|
||||||
|
{isLunar ? (
|
||||||
|
<div>
|
||||||
|
<div>음력 {inputYear}.{inputMonth}.{inputDay}{isLeap ? ' (윤달)' : ''}</div>
|
||||||
|
<div className="text-xs text-blue-300/50 mt-0.5">양력 {yearNum}.{monthNum}.{dayNum}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{yearNum}.{monthNum}.{dayNum}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hourNum !== null && (
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">태어난 시간</div>
|
||||||
|
<div className="font-bold">{hourNum}시</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">성별</div>
|
||||||
|
<div className="font-bold">{gender === 'male' ? '남성' : '여성'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">띠</div>
|
||||||
|
<div className="font-bold">{zodiacAnimal}띠</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">태어난 절기</div>
|
||||||
|
<div className="font-bold text-amber-300">{solarTermName} 이후</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-blue-300/60 mb-1">일간</div>
|
||||||
|
<div className="font-bold text-2xl text-amber-400">
|
||||||
|
{sajuData.day.stem} ({sajuData.day.stemKr})
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-300/60 mt-1">
|
||||||
|
{FIVE_ELEMENTS_KR[sajuData.day.element as keyof typeof FIVE_ELEMENTS_KR]}({sajuData.day.element})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-blue-300/60 text-xs">계산 엔진</div>
|
||||||
|
{engineBadge}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 pt-5 border-t border-white/10 space-y-2">
|
||||||
|
<Link href="/work/saju/input"
|
||||||
|
className="block w-full text-center bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||||
|
다시 입력하기
|
||||||
|
</Link>
|
||||||
|
<Link href="/work/saju"
|
||||||
|
className="block w-full text-center bg-violet-500/20 hover:bg-violet-500/30 text-violet-300 px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||||
|
서비스 소개
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* 메인 */}
|
||||||
|
<main className="space-y-6">
|
||||||
|
|
||||||
|
{/* 사주팔자 표 */}
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||||
|
<h2 className="text-xl font-extrabold text-[#04102b] mb-5 text-center">사주팔자 (四柱八字)</h2>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#04102b] text-white">
|
||||||
|
<th className="py-2.5 px-3 text-center font-bold text-xs">구분</th>
|
||||||
|
{sajuData.hour && <th className="py-2.5 px-3 text-center font-bold text-xs">시주</th>}
|
||||||
|
<th className="py-2.5 px-3 text-center font-bold text-xs">일주</th>
|
||||||
|
<th className="py-2.5 px-3 text-center font-bold text-xs">월주</th>
|
||||||
|
<th className="py-2.5 px-3 text-center font-bold text-xs">년주</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{/* 천간 */}
|
||||||
|
<tr className="border-b border-slate-100">
|
||||||
|
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">천간</td>
|
||||||
|
{sajuData.hour && (
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.hour.stem}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.hour.stemKr}</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.day.stem}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.day.stemKr}</div>
|
||||||
|
<div className="text-xs text-amber-600 font-bold mt-0.5">일간</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.month.stem}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.month.stemKr}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.year.stem}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.year.stemKr}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* 지지 */}
|
||||||
|
<tr className="border-b border-slate-100">
|
||||||
|
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">지지</td>
|
||||||
|
{sajuData.hour && (
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.hour.branch}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.hour.branchKr}</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.day.branch}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.day.branchKr}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.month.branch}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.month.branchKr}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b]">{sajuData.year.branch}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sajuData.year.branchKr}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* 지장간 */}
|
||||||
|
<tr className="border-b border-slate-100">
|
||||||
|
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">
|
||||||
|
<div>지장간</div>
|
||||||
|
<div className="text-[10px] text-slate-400 font-normal">숨은 천간</div>
|
||||||
|
</td>
|
||||||
|
{(() => {
|
||||||
|
const order = sajuData.hour
|
||||||
|
? ['시주', '일주', '월주', '년주']
|
||||||
|
: ['일주', '월주', '년주'];
|
||||||
|
return order.map((pillarName, idx) => {
|
||||||
|
const h = hiddenStems.find((hs: any) => hs.pillar === pillarName);
|
||||||
|
return (
|
||||||
|
<td key={idx} className={`py-2 px-2 text-center ${pillarName === '일주' ? 'bg-amber-50' : ''}`}>
|
||||||
|
{h && (
|
||||||
|
<div className="flex flex-wrap justify-center gap-1">
|
||||||
|
{h.stems.map((s: any, si: number) => (
|
||||||
|
<span key={si}
|
||||||
|
className={`inline-block px-1.5 py-0.5 rounded text-xs font-semibold border ${elementBgColors[s.element] || 'bg-gray-100'}`}
|
||||||
|
title={s.role}>
|
||||||
|
{s.stemKr}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* 십성 */}
|
||||||
|
<tr className="border-b border-slate-100">
|
||||||
|
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">십성</td>
|
||||||
|
{sajuData.hour && (
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.hour.tenGod}</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.day.tenGod}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.month.tenGod}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.year.tenGod}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* 십이운성 */}
|
||||||
|
<tr>
|
||||||
|
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">십이운성</td>
|
||||||
|
{sajuData.hour && (
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.hour.fortune}</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.day.fortune}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.month.fortune}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{sajuData.year.fortune}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 지지 상호작용 */}
|
||||||
|
{branchInteractions.length > 0 && (
|
||||||
|
<div className="mt-5 pt-5 border-t border-slate-100">
|
||||||
|
<h3 className="text-sm font-bold text-[#04102b] mb-3 text-center">지지 상호작용</h3>
|
||||||
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
|
{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 (
|
||||||
|
<span key={idx} className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-bold border ${colorClass}`}
|
||||||
|
title={inter.description}>
|
||||||
|
{inter.type} {inter.branchesKr.join('')}
|
||||||
|
{inter.resultElement && ` → ${FIVE_ELEMENTS_KR[inter.resultElement as keyof typeof FIVE_ELEMENTS_KR]}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 오행 균형 */}
|
||||||
|
<div className="mt-5 pt-5 border-t border-slate-100">
|
||||||
|
<h3 className="text-sm font-bold text-[#04102b] mb-4 text-center">오행 균형</h3>
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
{Object.entries(elementScores).map(([element, score]) => (
|
||||||
|
<div key={element} className="text-center">
|
||||||
|
<div className={`text-lg font-bold mb-1 ${elementColors[element] || ''}`}>{element}</div>
|
||||||
|
<div className="text-xs text-slate-500 mb-2">
|
||||||
|
{FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR]}
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-200 rounded-full h-1.5 mb-1">
|
||||||
|
<div
|
||||||
|
className={`h-1.5 rounded-full transition-all ${element === sajuData.day.element
|
||||||
|
? 'bg-gradient-to-r from-[#1a56db] to-[#7c3aed]'
|
||||||
|
: 'bg-slate-400'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${Math.max(score, 5)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-[#04102b]">{score}%</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 분석 카드 그리드 */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{/* 신강/신약 + 용신 */}
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||||
|
<h3 className="text-base font-extrabold text-[#04102b] mb-4">일간 세력 분석</h3>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<span className={`inline-block px-4 py-1.5 rounded-xl text-sm font-bold ${
|
||||||
|
analysis.dayMasterStrength.result === '신강'
|
||||||
|
? 'bg-red-100 text-red-700 border-2 border-red-400'
|
||||||
|
: analysis.dayMasterStrength.result === '신약'
|
||||||
|
? 'bg-blue-100 text-blue-700 border-2 border-blue-400'
|
||||||
|
: 'bg-green-100 text-green-700 border-2 border-green-400'
|
||||||
|
}`}>
|
||||||
|
{analysis.dayMasterStrength.result}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-500 text-xs">점수: {analysis.dayMasterStrength.score}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1 text-xs text-slate-500 mb-5">
|
||||||
|
{analysis.dayMasterStrength.reasons.map((r: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start">
|
||||||
|
<span className="text-amber-500 mr-1.5">-</span>
|
||||||
|
<span>{r}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-100 pt-4">
|
||||||
|
<h4 className="font-bold text-[#04102b] mb-2.5 text-sm">용신 / 희신 / 기신</h4>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.yongShin.yongShin] || 'bg-gray-100'}`}>
|
||||||
|
용신: {analysis.yongShin.yongShinKr}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.yongShin.heeShin] || 'bg-gray-100'}`}>
|
||||||
|
희신: {analysis.yongShin.heeShinKr}
|
||||||
|
</span>
|
||||||
|
<span className="px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 border border-slate-300 text-slate-700">
|
||||||
|
기신: {analysis.yongShin.giShinKr}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">{analysis.yongShin.explanation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 신살 + 공망 */}
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||||
|
<h3 className="text-base font-extrabold text-[#04102b] mb-4">신살 (神煞)</h3>
|
||||||
|
{shinsal.length > 0 ? (
|
||||||
|
<div className="space-y-2 mb-5">
|
||||||
|
{shinsal.map((s: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-start gap-2 p-3 rounded-xl bg-[#f0f5ff]">
|
||||||
|
<span className="inline-block px-2 py-0.5 bg-[#04102b] text-white rounded-lg text-xs font-bold whitespace-nowrap">
|
||||||
|
{s.name}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-semibold text-[#04102b]">
|
||||||
|
{s.pillar} {s.branchKr}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{s.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-slate-500 text-xs mb-5">특별한 신살이 발견되지 않았습니다.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-slate-100 pt-4">
|
||||||
|
<h4 className="font-bold text-[#04102b] mb-2 text-sm">공망 (空亡)</h4>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
{gongmang.branchesKr.map((bk: string, i: number) => (
|
||||||
|
<span key={i} className="px-2.5 py-1 bg-[#04102b] text-white rounded-lg text-xs font-bold">
|
||||||
|
{bk}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">{gongmang.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 세운 정보 */}
|
||||||
|
<div className="border-t border-slate-100 pt-4 mt-4">
|
||||||
|
<h4 className="font-bold text-[#04102b] mb-2 text-sm">
|
||||||
|
{analysis.seun.year}년 세운
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.seun.element] || 'bg-gray-100'}`}>
|
||||||
|
{analysis.seun.stemKr}{analysis.seun.branchKr}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">{analysis.seun.elementKr} 기운</span>
|
||||||
|
</div>
|
||||||
|
{analysis.seun.interactions.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
|
{analysis.seun.interactions.map((si: any, i: number) => (
|
||||||
|
<span key={i} className={`text-xs px-2 py-0.5 rounded-full font-semibold ${
|
||||||
|
si.type.includes('합') ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700'
|
||||||
|
}`}>
|
||||||
|
{si.type} {si.branchesKr.join('')}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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 (
|
||||||
|
<SajuAISection
|
||||||
|
hasPaid={hasPaid}
|
||||||
|
savedInterpretation={savedInterpretation}
|
||||||
|
sajuData={sajuData}
|
||||||
|
daeun={currentDaeun}
|
||||||
|
daeunList={daeunList}
|
||||||
|
gender={gender}
|
||||||
|
birthKey={birthKey}
|
||||||
|
currentUrl={currentUrl}
|
||||||
|
engineData={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* 오늘의 운세 (사주 결제 시 표시) */}
|
||||||
|
{hasPaid && (
|
||||||
|
<SajuFortuneSection
|
||||||
|
yongShin={analysis.yongShin.yongShin}
|
||||||
|
yongShinKr={analysis.yongShin.yongShinKr}
|
||||||
|
heeShin={analysis.yongShin.heeShin}
|
||||||
|
heeShinKr={analysis.yongShin.heeShinKr}
|
||||||
|
yearNum={yearNum}
|
||||||
|
monthNum={monthNum}
|
||||||
|
dayNum={dayNum}
|
||||||
|
hasLottoSubscription={hasLottoSubscription}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 대운 */}
|
||||||
|
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||||
|
<h2 className="text-lg font-extrabold text-[#04102b] mb-5 text-center">
|
||||||
|
대운 (大運) — 10년 주기 운세
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{currentDaeun && (
|
||||||
|
<div className="bg-gradient-to-r from-[#04102b] to-[#0a2060] rounded-2xl p-5 mb-5 text-white">
|
||||||
|
<h3 className="text-sm font-bold mb-3 text-center text-blue-300">현재 대운</h3>
|
||||||
|
<div className="text-center mb-3">
|
||||||
|
<div className="text-3xl font-bold mb-1">
|
||||||
|
{currentDaeun.stem}{currentDaeun.branch}
|
||||||
|
</div>
|
||||||
|
<div className="text-base text-blue-200">
|
||||||
|
{currentDaeun.stemKr}{currentDaeun.branchKr}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-300/70 mt-1">
|
||||||
|
{currentDaeun.age}세 ~ {currentDaeun.age + 9}세 ({currentDaeun.startYear} ~ {currentDaeun.endYear}년)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-center leading-relaxed text-xs text-blue-200/80">
|
||||||
|
{getDaeunDescription(currentDaeun, sajuData.day.stem)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{daeunList.map((daeun: any, index: number) => {
|
||||||
|
const isCurrent = currentDaeun &&
|
||||||
|
daeun.startYear === currentDaeun.startYear &&
|
||||||
|
daeun.endYear === currentDaeun.endYear;
|
||||||
|
return (
|
||||||
|
<div key={index}
|
||||||
|
className={`rounded-xl p-3 border-2 transition ${isCurrent ? 'bg-amber-50 border-amber-400' : 'bg-white border-[#dbe8ff]'}`}>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xl font-bold text-[#04102b] mb-0.5">{daeun.stem}{daeun.branch}</div>
|
||||||
|
<div className="text-xs text-slate-500 mb-1.5">{daeun.stemKr}{daeun.branchKr}</div>
|
||||||
|
<div className="text-xs text-slate-400">{daeun.age}세 ~ {daeun.age + 9}세</div>
|
||||||
|
<div className="text-xs text-slate-400">{daeun.startYear} ~ {daeun.endYear}</div>
|
||||||
|
{isCurrent && (
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<span className="inline-block bg-[#04102b] text-white text-xs px-2.5 py-0.5 rounded-full font-semibold">현재</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user