사주 기능 이식 & 로그인, 유저 페이지 Supabase 연동 & 토스 페이먼츠 결제 연동 & 사주 심층 분석을 위한 기능 분리
This commit is contained in:
220
app/saju/components/SajuForm.tsx
Normal file
220
app/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(`/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>
|
||||
);
|
||||
}
|
||||
43
app/saju/input/page.tsx
Normal file
43
app/saju/input/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import SajuForm from '../components/SajuForm';
|
||||
|
||||
export default function SajuInputPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
{/* Hero */}
|
||||
<div className="relative overflow-hidden bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] px-6 py-12">
|
||||
<div className="absolute inset-0 opacity-[0.05]"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '28px 28px' }} />
|
||||
<div className="absolute right-0 top-0 w-72 h-72 rounded-full bg-violet-500/10 blur-3xl -translate-y-1/2 translate-x-1/3" />
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
341
app/saju/page.tsx
Normal file
341
app/saju/page.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PaymentButton from '../components/PaymentButton';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
q: '사주팔자란 무엇인가요?',
|
||||
a: '사주팔자(四柱八字)는 태어난 년·월·일·시의 네 기둥(四柱)에 각각 천간과 지지 두 글자씩 총 여덟 글자(八字)로 이루어진 동양의 전통 운명 분석 체계입니다.',
|
||||
},
|
||||
{
|
||||
q: 'AI 해석은 어떻게 동작하나요?',
|
||||
a: '전통 명리학 계산 로직(오행, 신강/신약, 용신/희신 등)으로 산출된 데이터를 GPT-4o에 전달하여 12개 항목의 상세 해석을 생성합니다. 기본 원국 분석은 무료이며, AI 상세 해석은 유료(₩4,900)로 제공됩니다.',
|
||||
},
|
||||
{
|
||||
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;
|
||||
// null/undefined 값이 있으면 URL 생성 불가
|
||||
if (!birth_year || !birth_month || !birth_day) return '/saju/input';
|
||||
let url = `/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-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] px-6 py-14 lg:px-12">
|
||||
<div className="absolute inset-0 opacity-[0.06]"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '30px 30px' }} />
|
||||
<div className="absolute right-0 top-0 w-96 h-96 rounded-full bg-violet-500/10 blur-3xl -translate-y-1/2 translate-x-1/3" />
|
||||
<div className="absolute left-1/3 bottom-0 w-64 h-64 rounded-full bg-amber-400/8 blur-3xl translate-y-1/2" />
|
||||
|
||||
<div className="relative max-w-3xl 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-5 tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||
전통 명리학 × AI 해석 · 무료 기본 분석 제공
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-white leading-tight mb-5 tracking-tight">
|
||||
AI가 분석하는<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#c4b5fd] to-[#fbbf24]">
|
||||
사주팔자
|
||||
</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="/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-7 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="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="/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-gradient-to-r from-[#04102b] to-[#0a2060] text-white hover:from-[#0a1f5c] hover:to-[#1a3a7a] transition"
|
||||
>
|
||||
다시 보기 →
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── 바로 시작하기 CTA ─── */}
|
||||
<div className="bg-gradient-to-r from-[#04102b] via-[#0a1f5c] to-[#0d2d8a] rounded-2xl border border-[#1a3a7a] p-8 text-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-[0.04]"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '25px 25px' }} />
|
||||
<div className="relative">
|
||||
<div className="text-3xl mb-3">✨</div>
|
||||
<h3 className="text-2xl font-extrabold text-white mb-2">지금 무료로 시작하세요</h3>
|
||||
<p className="text-blue-200/60 text-sm mb-6">회원가입 없이, 생년월일만 입력하면 바로 확인 가능합니다</p>
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#1a56db] to-[#7c3aed] text-white px-8 py-3.5 rounded-xl font-semibold text-base hover:from-[#1e4fc2] hover:to-[#6d28d9] transition-all shadow-lg shadow-violet-900/40"
|
||||
>
|
||||
사주 입력하러 가기 →
|
||||
</Link>
|
||||
</div>
|
||||
</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">무료 vs 유료 비교</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">기본 원국은 무료로, AI 상세 해석은 단 ₩4,900에</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="/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>
|
||||
|
||||
{/* 유료 */}
|
||||
<div className="bg-gradient-to-br from-[#04102b] to-[#0a2060] rounded-2xl border border-[#1a3a7a] p-6 shadow-lg relative overflow-hidden">
|
||||
<div className="absolute top-4 right-4 bg-amber-400 text-[#04102b] text-xs font-bold px-2 py-0.5 rounded-lg">
|
||||
₩4,900
|
||||
</div>
|
||||
<div className="absolute bottom-0 right-0 w-32 h-32 rounded-full bg-violet-500/10 blur-2xl" />
|
||||
<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년 주기) 분석',
|
||||
'올해 세운 흐름',
|
||||
'GPT-4o 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="text-2xl font-extrabold text-amber-400">₩4,900</div>
|
||||
<div className="text-xs text-blue-300/70 mt-1 mb-4">1회 결제 · 영구 열람</div>
|
||||
{hasPaid ? (
|
||||
<Link
|
||||
href="/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>
|
||||
) : (
|
||||
<PaymentButton
|
||||
productId="saju_detail"
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold transition bg-amber-400 text-[#04102b] hover:bg-amber-300"
|
||||
>
|
||||
AI 상세 해석 구매하기
|
||||
</PaymentButton>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
148
app/saju/result/SajuAISection.tsx
Normal file
148
app/saju/result/SajuAISection.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
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;
|
||||
}
|
||||
|
||||
export default function SajuAISection({
|
||||
hasPaid,
|
||||
savedInterpretation,
|
||||
sajuData,
|
||||
daeun,
|
||||
daeunList,
|
||||
gender,
|
||||
birthKey,
|
||||
currentUrl,
|
||||
}: SajuAISectionProps) {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>(
|
||||
savedInterpretation ? 'done' : 'idle'
|
||||
);
|
||||
const [interpretation, setInterpretation] = useState(savedInterpretation ?? '');
|
||||
const called = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPaid || savedInterpretation || 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 }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (data.interpretation) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
// birthKey 유효성 검사 후 저장 (NaN/null 방지)
|
||||
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-5">
|
||||
성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등<br />
|
||||
GPT-4o가 생성하는 맞춤형 사주 해석을 받아보세요.
|
||||
</p>
|
||||
<PaymentButton
|
||||
productId="saju_detail"
|
||||
returnUrl={currentUrl}
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-amber-500 to-amber-400 hover:from-amber-400 hover:to-amber-300 text-[#04102b] font-bold px-7 py-3 rounded-xl transition-all shadow-lg"
|
||||
>
|
||||
AI 해석 구매하기 · ₩4,900
|
||||
</PaymentButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// AI 생성 중
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// 오류
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// AI 해석 완료
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<div className="flex items-center gap-2 mb-5 pb-4 border-b border-slate-100">
|
||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-violet-500 to-amber-500 flex items-center justify-center">
|
||||
<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>
|
||||
<h2 className="text-lg font-extrabold text-[#04102b]">AI 상세 해석</h2>
|
||||
<span className="ml-auto text-xs bg-emerald-50 border border-emerald-200 text-emerald-700 font-bold px-2 py-0.5 rounded-full">
|
||||
결제 완료
|
||||
</span>
|
||||
</div>
|
||||
<div className="prose prose-sm max-w-none text-slate-700 leading-relaxed whitespace-pre-wrap">
|
||||
{interpretation}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
584
app/saju/result/page.tsx
Normal file
584
app/saju/result/page.tsx
Normal file
@@ -0,0 +1,584 @@
|
||||
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';
|
||||
|
||||
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;
|
||||
|
||||
// 필수 파라미터 누락 시 안전한 기본값 (NaN 방지)
|
||||
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="/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';
|
||||
|
||||
const sajuData = calculateSaju(yearNum, monthNum, dayNum, hourNum, gender);
|
||||
|
||||
// 결제 여부 + 저장된 AI 해석 확인 (서버사이드)
|
||||
let hasPaid = false;
|
||||
let savedInterpretation: string | null = null;
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 인증 오류 시 무시 (미로그인)
|
||||
}
|
||||
|
||||
// 절기 정보
|
||||
const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum);
|
||||
const solarTermName = getSolarTermName(solarTermIndex);
|
||||
const monthBranchIndex = getSolarTermMonthBranch(yearNum, monthNum, dayNum);
|
||||
const monthBranchName = EARTHLY_BRANCHES_KR[monthBranchIndex];
|
||||
|
||||
// 종합 분석 수행
|
||||
const analysis = performFullAnalysis(sajuData);
|
||||
const elementScores = analysis.elementScores;
|
||||
|
||||
// 대운 계산
|
||||
const daeunList = calculateDaeun(
|
||||
yearNum, monthNum, dayNum, gender,
|
||||
sajuData.month.stem, sajuData.month.branch
|
||||
);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentDaeun = getCurrentDaeun(daeunList, currentYear);
|
||||
|
||||
// 오행 색상 매핑
|
||||
const elementColors: { [key: string]: string } = {
|
||||
'木': 'text-green-700', '火': 'text-red-600', '土': 'text-yellow-700',
|
||||
'金': 'text-amber-600', '水': 'text-blue-700',
|
||||
};
|
||||
const elementBgColors: { [key: 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 zodiacIndex = (yearNum - 4) % 12;
|
||||
const zodiacAnimal = zodiacAnimals[zodiacIndex >= 0 ? zodiacIndex : zodiacIndex + 12];
|
||||
|
||||
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-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>
|
||||
|
||||
<div className="mt-5 pt-5 border-t border-white/10 space-y-2">
|
||||
<Link
|
||||
href="/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="/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 pillars = sajuData.hour
|
||||
? [analysis.hiddenStems.find(h => h.pillar === '시주'), analysis.hiddenStems.find(h => h.pillar === '일주'), analysis.hiddenStems.find(h => h.pillar === '월주'), analysis.hiddenStems.find(h => h.pillar === '년주')]
|
||||
: [analysis.hiddenStems.find(h => h.pillar === '일주'), analysis.hiddenStems.find(h => h.pillar === '월주'), analysis.hiddenStems.find(h => h.pillar === '년주')];
|
||||
return pillars.map((h, idx) => (
|
||||
<td key={idx} className={`py-2 px-2 text-center ${h?.pillar === '일주' ? 'bg-amber-50' : ''}`}>
|
||||
{h && (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
{h.stems.map((s, si) => (
|
||||
<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>
|
||||
|
||||
{/* 지지 상호작용 */}
|
||||
{analysis.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">
|
||||
{analysis.branchInteractions.map((inter, idx) => {
|
||||
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}`}>
|
||||
{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, i) => (
|
||||
<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>
|
||||
{analysis.shinsal.length > 0 ? (
|
||||
<div className="space-y-2 mb-5">
|
||||
{analysis.shinsal.map((s, i) => (
|
||||
<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">
|
||||
{analysis.gongmang.branchesKr.map((bk, i) => (
|
||||
<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">{analysis.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, i) => (
|
||||
<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 = `/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}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 대운 */}
|
||||
<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, index) => {
|
||||
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