사주 풀이 고도화, NAS 배포 자동화
This commit is contained in:
127
app/api/analyze/route.ts
Normal file
127
app/api/analyze/route.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import OpenAI from 'openai';
|
||||
import { createSajuPrompt } from '@/lib/saju-ai-prompt';
|
||||
import { performFullAnalysis } from '@/lib/ai-interpretation';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const MOCK_INTERPRETATION = `
|
||||
## 1. 일간 분석과 타고난 기질
|
||||
(API 키 문제 또는 할당량 초과로 인해 예시 데이터를 보여드립니다.)
|
||||
귀하는 **갑목(甲木)** 일간으로 태어나, 마치 곧게 뻗은 소나무와 같은 기상을 지니고 있다. 리더십이 강하고 추진력이 뛰어나며, 한번 마음먹은 일은 끝까지 해내는 뚝심이 있다. 겉으로는 강해보여도 속내에는 여린 감성이 숨어있어 의외로 상처를 잘 받기도 한다.
|
||||
|
||||
## 2. 오행 균형과 용신 기반 개운법
|
||||
사주에서 **화(火)** 기운이 부족하여 표현력이 다소 약할 수 있다. 붉은색 계통의 옷이나 소품을 활용하고, 밝은 곳에서 활동하는 것이 운을 트이게 한다.
|
||||
|
||||
## 3. 지지 상호작용 해석
|
||||
지지 간의 상호작용을 살펴보면, 특별한 합충형이 발견된다.
|
||||
|
||||
## 4. 신살이 삶에 미치는 영향
|
||||
역마살이 사주에 자리하고 있어 이동과 변동이 많은 삶을 살게 된다.
|
||||
|
||||
## 5. 재물운과 금전 흐름
|
||||
재물창고인 **진토(辰土)**를 깔고 있어 기본적으로 재복은 타고났다. 다만, 돈을 버는 것보다 지키는 힘이 약할 수 있으니 저축 습관이 중요하다.
|
||||
|
||||
## 6. 직업 적성과 진로
|
||||
교육, 출판, 건축, 디자인 등 창조적이고 독립적인 분야에서 두각을 나타낼 수 있다.
|
||||
|
||||
## 7. 애정운과 결혼
|
||||
자존심이 강해 상대방에게 굽히지 않으려는 성향이 있다. 배우자와의 관계에서는 조금 더 부드러운 태도가 필요하다.
|
||||
|
||||
## 8. 건강운
|
||||
간, 담낭, 신경계 통증에 유의해야 한다. 스트레스를 받으면 뭉치는 경향이 있으니 스트레칭과 요가를 추천한다.
|
||||
|
||||
## 9. 현재 대운의 흐름과 기회/위기
|
||||
현재 대운은 인생의 전환점이다. 새로운 것을 시작하기보다는 기존의 것을 다지고 내실을 기하는 시기이다.
|
||||
|
||||
## 10. 올해의 세운 분석
|
||||
올해는 귀인의 도움을 받을 수 있는 해이다. 주저하지 말고 주변에 도움을 요청하라.
|
||||
|
||||
## 11. 인생의 황금기 예측
|
||||
40대 중반부터 50대 초반까지 인생의 가장 화려한 시기를 맞이할 것으로 보인다.
|
||||
|
||||
## 12. 종합 조언
|
||||
"서두르지 않아도 봄은 온다." 조급해하지 말고 때를 기다리는 지혜가 필요하다.
|
||||
`;
|
||||
|
||||
// 사용 가능한 모델 우선순위 (gpt-4o → gpt-4o-mini 폴백)
|
||||
const MODELS = ['gpt-4o', 'gpt-4o-mini'] as const;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { saju, daeun, daeunList, gender } = await request.json();
|
||||
|
||||
// 종합 분석 수행
|
||||
let analysis;
|
||||
try {
|
||||
analysis = performFullAnalysis(saju);
|
||||
} catch (analysisError: any) {
|
||||
console.error('Analysis calculation error:', analysisError.message);
|
||||
return NextResponse.json(
|
||||
{ error: '사주 분석 계산 중 오류가 발생했습니다: ' + analysisError.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
console.warn('OpenAI API Key is missing');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
const prompt = createSajuPrompt(saju, daeun, gender, analysis, daeunList || []);
|
||||
|
||||
// 모델 폴백: gpt-4o 실패 시 gpt-4o-mini로 재시도
|
||||
let interpretation: string | null = null;
|
||||
let usedModel = '';
|
||||
|
||||
for (const model of MODELS) {
|
||||
try {
|
||||
console.log(`Generating analysis with model: ${model}`);
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages: [{ role: 'system', content: prompt }],
|
||||
model,
|
||||
max_tokens: model === 'gpt-4o' ? 8192 : 4096,
|
||||
temperature: 0.75,
|
||||
});
|
||||
interpretation = completion.choices[0].message.content;
|
||||
usedModel = model;
|
||||
console.log(`Successfully generated with model: ${model}`);
|
||||
break;
|
||||
} catch (modelError: any) {
|
||||
console.warn(`Model ${model} failed:`, modelError.message || modelError.status);
|
||||
// 인증 오류(401) 또는 할당량 초과(429)는 모든 모델에 해당하므로 바로 mock 반환
|
||||
if (modelError.status === 401) {
|
||||
console.warn('OpenAI API Key is invalid (401). Returning mock data.');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
if (modelError.status === 429 || (modelError.error && modelError.error.code === 'insufficient_quota')) {
|
||||
console.warn('OpenAI Quota Exceeded. Returning mock data.');
|
||||
return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis });
|
||||
}
|
||||
// 마지막 모델이 아니면 다음 모델로 폴백
|
||||
if (model === MODELS[MODELS.length - 1]) {
|
||||
throw modelError; // 모든 모델 실패
|
||||
}
|
||||
console.log(`Falling back to next model...`);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ interpretation, analysis });
|
||||
} catch (error: any) {
|
||||
console.error('Error generating interpretation:', error.message || error);
|
||||
|
||||
if (error.response) {
|
||||
console.error('OpenAI Error Response:', error.response.status, error.response.data);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to generate interpretation' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
9
app/api/health/route.ts
Normal file
9
app/api/health/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'saju-web',
|
||||
});
|
||||
}
|
||||
31
app/auth/callback/route.ts
Normal file
31
app/auth/callback/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
// The client you created from the Server-Side Auth instructions
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
// if "next" is in param, use it as the redirect URL
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (code) {
|
||||
const supabase = await createClient()
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
||||
if (!error) {
|
||||
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
|
||||
const isLocalEnv = process.env.NODE_ENV === 'development'
|
||||
if (isLocalEnv) {
|
||||
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
} else if (forwardedHost) {
|
||||
return NextResponse.redirect(`https://${forwardedHost}${next}`)
|
||||
} else {
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the user to an error page with instructions
|
||||
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
|
||||
}
|
||||
198
app/compatibility/result/CompatibilityDetailUnlock.tsx
Normal file
198
app/compatibility/result/CompatibilityDetailUnlock.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TokenPurchaseModal from '@/components/TokenPurchaseModal';
|
||||
import { ensureProfile } from '@/lib/ensure-profile';
|
||||
|
||||
interface Props {
|
||||
allPros: string[];
|
||||
allCons: string[];
|
||||
advice: string;
|
||||
element1: string;
|
||||
element2: string;
|
||||
element1Kr: string;
|
||||
element2Kr: string;
|
||||
}
|
||||
|
||||
export default function CompatibilityDetailUnlock({ allPros, allCons, advice, element1, element2, element1Kr, element2Kr }: Props) {
|
||||
const [isUnlocked, setIsUnlocked] = useState(false);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [credits, setCredits] = useState(0);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
setUser(user);
|
||||
const currentCredits = await ensureProfile(supabase, user);
|
||||
setCredits(currentCredits);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleUnlock = async () => {
|
||||
if (!user) {
|
||||
if (confirm('로그인이 필요합니다. 로그인 페이지로 이동하시겠습니까?')) {
|
||||
router.push('/login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (credits < 1) {
|
||||
setShowModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 토큰 차감
|
||||
setLoading(true);
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({ credits: credits - 1 })
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) {
|
||||
alert('토큰 차감에 실패했습니다. 다시 시도해주세요.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCredits(credits - 1);
|
||||
setIsUnlocked(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handlePurchaseComplete = async () => {
|
||||
setShowModal(false);
|
||||
// 크레딧 새로고침
|
||||
if (user) {
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('credits')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
if (profile) setCredits(profile.credits || 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isUnlocked) {
|
||||
return (
|
||||
<>
|
||||
<div className="relative mb-8">
|
||||
{/* 블러 처리된 미리보기 */}
|
||||
<div className="filter blur-sm select-none pointer-events-none opacity-60">
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-8">
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">✅</span>두 사람의 장점
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{allPros.slice(0, 3).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-gray-200 rounded w-full"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">⚠️</span>주의할 점
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{allCons.slice(0, 3).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-gray-200 rounded w-full"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-8">
|
||||
<div className="h-6 bg-white/30 rounded w-3/4 mx-auto mb-4"></div>
|
||||
<div className="h-4 bg-white/20 rounded w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 잠금 오버레이 */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/40 backdrop-blur-[2px] rounded-2xl">
|
||||
<div className="text-center p-8 bg-white/95 backdrop-blur-md rounded-2xl shadow-xl border border-pink-100 max-w-sm mx-auto">
|
||||
<div className="text-4xl mb-4">🔐</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">상세 궁합 해석</h3>
|
||||
<p className="text-gray-600 mb-2">
|
||||
{element1Kr}({element1})과 {element2Kr}({element2})의<br />
|
||||
오행 기반 맞춤 해석을 확인하세요.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
토큰 1개 사용 | 보유: <span className="font-bold text-pink-600">{credits}개</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-pink-600 to-purple-600 text-white font-bold py-3 px-6 rounded-xl hover:shadow-lg hover:scale-105 transition transform disabled:opacity-50"
|
||||
>
|
||||
{loading ? '처리 중...' : credits >= 1 ? '토큰 1개로 잠금해제' : '토큰 충전하기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TokenPurchaseModal
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onPurchaseComplete={handlePurchaseComplete}
|
||||
user={user}
|
||||
supabase={supabase}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Unlocked content
|
||||
return (
|
||||
<>
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-8">
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">✅</span>두 사람의 장점
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
{allPros.map((pro, i) => (
|
||||
<li key={i} className="flex items-start">
|
||||
<span className="text-pink-600 mr-2 mt-1 flex-shrink-0">•</span>
|
||||
<span>{pro}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">⚠️</span>주의할 점
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
{allCons.map((con, i) => (
|
||||
<li key={i} className="flex items-start">
|
||||
<span className="text-purple-600 mr-2 mt-1 flex-shrink-0">•</span>
|
||||
<span>{con}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl shadow-lg p-8 text-white mb-8">
|
||||
<h3 className="text-2xl font-bold mb-4 text-center">💡 조언</h3>
|
||||
<p className="text-lg leading-relaxed text-center">{advice}</p>
|
||||
<p className="text-sm text-white/70 mt-4 text-center">
|
||||
궁합은 참고사항이에요. 서로를 이해하고 존중하며 노력한다면 어떤 궁합이든 행복한 관계를 만들 수 있답니다.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link';
|
||||
import { calculateSaju, FIVE_ELEMENTS } from '@/lib/saju-calculator';
|
||||
import PDFButton from '../../components/PDFButton';
|
||||
import ShareButtons from '../../components/ShareButtons';
|
||||
import CompatibilityDetailUnlock from './CompatibilityDetailUnlock';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -18,102 +19,325 @@ interface PageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
// 오행 한글 매핑
|
||||
const elementKrMap: Record<string, string> = {
|
||||
'木': '목', '火': '화', '土': '토', '金': '금', '水': '수'
|
||||
};
|
||||
|
||||
// Deterministic hash for seeded score variation
|
||||
function seededOffset(seed: string, index: number): number {
|
||||
let hash = 0;
|
||||
const str = seed + index.toString();
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return ((hash % 11) - 5); // -5 ~ +5
|
||||
}
|
||||
|
||||
// 오행 관계 타입 판별
|
||||
type ElementRelation = 'same' | 'produce' | 'produced' | 'overcome' | 'overcomed';
|
||||
|
||||
function getElementRelation(el1: string, el2: string): ElementRelation {
|
||||
const produceMap: Record<string, string> = {
|
||||
'木': '火', '火': '土', '土': '金', '金': '水', '水': '木'
|
||||
};
|
||||
if (el1 === el2) return 'same';
|
||||
if (produceMap[el1] === el2) return 'produce';
|
||||
if (produceMap[el2] === el1) return 'produced';
|
||||
const overcomeMap: Record<string, string> = {
|
||||
'木': '土', '火': '金', '土': '水', '金': '木', '水': '火'
|
||||
};
|
||||
if (overcomeMap[el1] === el2) return 'overcome';
|
||||
return 'overcomed';
|
||||
}
|
||||
|
||||
// 지지 관계 판별
|
||||
function getBranchRelation(b1: string, b2: string): { sixHarmony: boolean; threeHarmony: boolean; conflict: boolean } {
|
||||
const sixHarmony: Record<string, string> = {
|
||||
'子': '丑', '丑': '子', '寅': '亥', '亥': '寅',
|
||||
'卯': '戌', '戌': '卯', '辰': '酉', '酉': '辰',
|
||||
'巳': '申', '申': '巳', '午': '未', '未': '午'
|
||||
};
|
||||
const threeHarmonyGroups = [
|
||||
['申', '子', '辰'], ['寅', '午', '戌'],
|
||||
['亥', '卯', '未'], ['巳', '酉', '丑']
|
||||
];
|
||||
const conflictMap: Record<string, string> = {
|
||||
'子': '午', '午': '子', '丑': '未', '未': '丑',
|
||||
'寅': '申', '申': '寅', '卯': '酉', '酉': '卯',
|
||||
'辰': '戌', '戌': '辰', '巳': '亥', '亥': '巳'
|
||||
};
|
||||
|
||||
const isThreeHarmony = threeHarmonyGroups.some(g => g.includes(b1) && g.includes(b2));
|
||||
return {
|
||||
sixHarmony: sixHarmony[b1] === b2,
|
||||
threeHarmony: isThreeHarmony,
|
||||
conflict: conflictMap[b1] === b2,
|
||||
};
|
||||
}
|
||||
|
||||
// 오행 관계별 상세 해석
|
||||
function getElementInterpretation(el1: string, el2: string, relation: ElementRelation) {
|
||||
const el1Kr = elementKrMap[el1];
|
||||
const el2Kr = elementKrMap[el2];
|
||||
|
||||
const interpretations: Record<string, { pros: string[]; cons: string[]; advice: string }> = {
|
||||
// 상생: el1이 el2를 생함
|
||||
'木_火': {
|
||||
pros: [
|
||||
`${el1Kr}(나무)이 ${el2Kr}(불)을 키워주는 관계예요. 한 분이 자연스럽게 상대방에게 에너지를 전해주거든요.`,
|
||||
'성장을 응원하고 지지해주는 따뜻한 관계가 될 수 있어요.',
|
||||
'서로의 열정을 자극해서 함께 발전해 나갈 수 있는 좋은 조합이에요.',
|
||||
'함께 있으면 자연스럽게 활력이 넘치고, 창의적인 아이디어가 샘솟을 거예요.',
|
||||
],
|
||||
cons: [
|
||||
'한쪽이 너무 많이 희생하면 에너지가 소진될 수 있어요. 균형이 중요해요.',
|
||||
'불꽃처럼 열정적인 만큼 감정 기복이 클 수 있으니 차분한 대화가 필요해요.',
|
||||
'서로에 대한 기대가 커질 수 있어서, 적절한 거리감을 유지하는 것도 좋아요.',
|
||||
],
|
||||
advice: '서로의 에너지를 아낌없이 주되, 자기 자신도 잘 챙기는 게 이 관계의 핵심이에요.',
|
||||
},
|
||||
'火_土': {
|
||||
pros: [
|
||||
`${el1Kr}(불)이 ${el2Kr}(흙)을 따뜻하게 만들어주는 관계예요. 서로에게 안정감을 줄 수 있거든요.`,
|
||||
'열정과 안정이 조화를 이루어 실질적인 성과를 만들어낼 수 있어요.',
|
||||
'서로 다른 강점을 가지고 있어서 팀워크가 좋은 편이에요.',
|
||||
'어려운 순간에도 든든하게 의지할 수 있는 관계가 될 거예요.',
|
||||
],
|
||||
cons: [
|
||||
'급한 성격과 느긋한 성격이 부딪힐 수 있어요. 템포를 맞추는 연습이 필요해요.',
|
||||
'때로는 답답함을 느낄 수 있지만, 그만큼 서로를 보완해주는 관계이기도 해요.',
|
||||
'가끔은 서로의 속도 차이를 인정하고 기다려주는 여유가 필요하답니다.',
|
||||
],
|
||||
advice: '서로의 리듬을 존중하면서 함께 걸어가면, 오래오래 행복한 관계가 될 거예요.',
|
||||
},
|
||||
'土_金': {
|
||||
pros: [
|
||||
`${el1Kr}(흙)이 ${el2Kr}(쇠)를 품고 있는 관계예요. 안정 속에서 가치를 만들어내는 조합이에요.`,
|
||||
'현실적이고 실용적인 면에서 잘 맞아서 함께 무언가를 이뤄내기 좋아요.',
|
||||
'서로를 단단하게 만들어주는 든든한 파트너가 될 수 있어요.',
|
||||
'물질적으로나 정서적으로 안정감 있는 관계를 만들어갈 수 있답니다.',
|
||||
],
|
||||
cons: [
|
||||
'둘 다 고집이 있을 수 있어서 의견 충돌 시 양보하는 연습이 필요해요.',
|
||||
'너무 현실적인 면만 추구하다 보면 로맨스가 부족해질 수 있어요.',
|
||||
'변화를 두려워하기보다 함께 새로운 시도를 해보는 것도 좋겠어요.',
|
||||
],
|
||||
advice: '안정감 위에 가끔 달콤한 서프라이즈를 더하면 완벽한 관계가 될 거예요.',
|
||||
},
|
||||
'金_水': {
|
||||
pros: [
|
||||
`${el1Kr}(쇠)이 ${el2Kr}(물)을 만들어내는 관계예요. 깊은 지혜와 감성이 만나는 조합이에요.`,
|
||||
'서로의 내면을 잘 이해하고, 깊은 대화를 나눌 수 있는 사이가 될 거예요.',
|
||||
'원칙과 유연함이 조화를 이루어 균형 잡힌 관계를 만들 수 있어요.',
|
||||
'서로에게 영감을 주고 정서적으로 풍요로운 관계가 될 수 있답니다.',
|
||||
],
|
||||
cons: [
|
||||
'감정 표현 방식이 다를 수 있어요. 직접적으로 마음을 표현하는 연습을 해보세요.',
|
||||
'때로는 너무 이성적이거나 너무 감성적인 면이 부딪힐 수 있어요.',
|
||||
'서로의 감정 표현 스타일을 이해하고 존중하는 것이 중요해요.',
|
||||
],
|
||||
advice: '머리와 가슴의 균형, 이성과 감성의 조화가 이 관계의 매력 포인트예요.',
|
||||
},
|
||||
'水_木': {
|
||||
pros: [
|
||||
`${el2Kr}(물)이 ${el1Kr}(나무)를 키워주는 관계예요. 성장과 발전의 에너지가 넘치는 조합이에요.`,
|
||||
'새로운 것을 시작하고 함께 성장해 나가기에 최적의 파트너예요.',
|
||||
'서로에게 자양분이 되어 끊임없이 발전하는 관계가 될 수 있어요.',
|
||||
'자유롭고 창의적인 에너지가 넘쳐서 함께하면 즐거울 거예요.',
|
||||
],
|
||||
cons: [
|
||||
'방향성 없이 흘러갈 수 있으니 함께 목표를 세우는 것이 중요해요.',
|
||||
'너무 자유로운 성향이 겹치면 책임감이 부족해질 수 있어요.',
|
||||
'각자의 공간을 존중하되, 함께하는 시간도 소중히 여기세요.',
|
||||
],
|
||||
advice: '함께 성장하는 기쁨을 느끼면서, 때로는 뿌리를 내리는 안정감도 챙겨보세요.',
|
||||
},
|
||||
// 동일 오행
|
||||
'same': {
|
||||
pros: [
|
||||
'같은 오행이라 기본적인 가치관과 성향이 비슷해요. 서로를 이해하기 쉬운 관계거든요.',
|
||||
'말하지 않아도 통하는 부분이 많아서 편안한 관계를 유지할 수 있어요.',
|
||||
'취미나 관심사가 비슷해서 함께 즐길 거리가 많을 거예요.',
|
||||
'서로의 마음을 직감적으로 알아차릴 수 있는 특별한 케미가 있어요.',
|
||||
],
|
||||
cons: [
|
||||
'너무 비슷해서 오히려 새로운 자극이 부족할 수 있어요.',
|
||||
'같은 약점을 공유하기 때문에 함께 빠질 수 있는 함정이 있어요.',
|
||||
'때로는 의도적으로 다른 관점을 찾아보는 노력이 필요해요.',
|
||||
],
|
||||
advice: '닮은 점을 즐기되, 각자만의 개성과 차이도 만들어가면 더 풍성한 관계가 될 거예요.',
|
||||
},
|
||||
// 상극
|
||||
'木_土': {
|
||||
pros: [
|
||||
'긴장감이 있지만 그만큼 서로에게 강한 끌림을 느낄 수 있는 관계예요.',
|
||||
'서로 다른 관점을 가지고 있어서 새로운 시각을 배울 수 있어요.',
|
||||
'도전적인 관계이지만 극복할수록 더 단단해지는 사이가 될 거예요.',
|
||||
],
|
||||
cons: [
|
||||
`${el1Kr}(나무)이 ${el2Kr}(흙)을 극하는 관계라 의견 충돌이 생기기 쉬워요.`,
|
||||
'주도권 다툼이 일어날 수 있으니 서로 양보하는 자세가 중요해요.',
|
||||
'상대방을 변화시키려 하기보다 있는 그대로 받아들이는 연습을 해보세요.',
|
||||
'감정이 격해질 때는 일단 한 발 물러서서 냉각 시간을 갖는 것이 좋아요.',
|
||||
],
|
||||
advice: '부딪히는 만큼 성장할 수 있는 관계예요. 서로를 가르치려 하지 말고 배우려는 마음이 중요해요.',
|
||||
},
|
||||
'火_金': {
|
||||
pros: [
|
||||
'강렬한 에너지가 만나는 관계라 서로에게 깊은 인상을 남길 수 있어요.',
|
||||
'서로의 강한 면을 인정하고 존중할 수 있다면 최강의 파트너가 될 수 있어요.',
|
||||
'갈등을 극복한 뒤에는 누구보다 단단한 신뢰가 생기는 관계예요.',
|
||||
],
|
||||
cons: [
|
||||
`${el1Kr}(불)이 ${el2Kr}(쇠)를 녹이는 관계라 갈등이 뜨거울 수 있어요.`,
|
||||
'둘 다 자존심이 강해서 먼저 사과하기 어려울 수 있어요.',
|
||||
'감정적으로 상처를 주기 쉬우니 말 한마디 한마디를 신중하게 하세요.',
|
||||
'서로의 영역을 존중하고 간섭을 줄이는 것이 관계 유지의 비결이에요.',
|
||||
],
|
||||
advice: '불꽃이 튀는 관계이지만, 그 열기를 잘 다스리면 누구보다 강한 유대감을 만들 수 있어요.',
|
||||
},
|
||||
'土_水': {
|
||||
pros: [
|
||||
'서로 다른 성향이지만, 그래서 오히려 배울 점이 많은 관계예요.',
|
||||
'현실적인 면과 유연한 면이 만나 균형을 찾을 수 있는 조합이에요.',
|
||||
'서로의 부족한 점을 채워줄 수 있는 잠재력이 있는 관계거든요.',
|
||||
],
|
||||
cons: [
|
||||
`${el1Kr}(흙)이 ${el2Kr}(물)을 막는 관계라 소통에 장벽이 생기기 쉬워요.`,
|
||||
'고집과 유연함이 부딪히면서 답답함을 느낄 수 있어요.',
|
||||
'서로의 방식을 존중하고, 중간 지점을 찾으려는 노력이 필요해요.',
|
||||
'대화가 막힐 때는 글이나 편지로 마음을 전하는 것도 좋은 방법이에요.',
|
||||
],
|
||||
advice: '서로의 차이를 인정하고 존중하는 것이 이 관계의 가장 큰 과제이자 선물이에요.',
|
||||
},
|
||||
'金_木': {
|
||||
pros: [
|
||||
'서로에게 자극을 주는 관계라 성장의 계기가 될 수 있어요.',
|
||||
'갈등을 잘 해결하면 오히려 더 깊은 이해와 공감이 생기는 사이예요.',
|
||||
'상대방을 통해 자신의 새로운 면을 발견할 수 있는 관계거든요.',
|
||||
],
|
||||
cons: [
|
||||
`${el1Kr}(쇠)이 ${el2Kr}(나무)를 자르는 관계라 비판적인 면이 나올 수 있어요.`,
|
||||
'상대방의 약점을 지적하기보다 장점을 칭찬하는 습관을 들여보세요.',
|
||||
'서로를 통제하려 하면 관계가 힘들어질 수 있으니 자유를 존중해주세요.',
|
||||
'작은 것부터 서로 칭찬하고 감사하는 연습을 해보면 관계가 훨씬 좋아질 거예요.',
|
||||
],
|
||||
advice: '날카로운 면이 있지만, 서로를 다듬어가는 과정에서 보석 같은 관계가 될 수 있어요.',
|
||||
},
|
||||
'水_火': {
|
||||
pros: [
|
||||
'상반된 에너지가 만나서 강한 끌림을 느낄 수 있는 관계예요.',
|
||||
'열정과 차분함이 만나 독특한 균형을 이룰 수 있는 조합이에요.',
|
||||
'서로에게서 완전히 새로운 세계를 경험할 수 있는 흥미로운 관계거든요.',
|
||||
],
|
||||
cons: [
|
||||
`${el1Kr}(물)이 ${el2Kr}(불)을 끄는 관계라 서로의 열정을 꺾을 수 있어요.`,
|
||||
'한쪽이 너무 뜨겁고 한쪽이 너무 차가우면 갈등이 생기기 쉬워요.',
|
||||
'감정의 온도차를 줄이기 위해 서로의 감정을 자주 확인하는 것이 중요해요.',
|
||||
'큰 결정은 충분히 대화한 뒤에 함께 내리는 것이 좋아요.',
|
||||
],
|
||||
advice: '물과 불이 만나면 증기가 되듯, 서로의 에너지를 잘 합치면 엄청난 힘을 발휘할 수 있어요.',
|
||||
},
|
||||
};
|
||||
|
||||
// 상생 관계 매핑
|
||||
if (relation === 'produce') {
|
||||
return interpretations[`${el1}_${produceTarget(el1)}`] || interpretations['same'];
|
||||
}
|
||||
if (relation === 'produced') {
|
||||
return interpretations[`${el2}_${produceTarget(el2)}`] || interpretations['same'];
|
||||
}
|
||||
if (relation === 'same') {
|
||||
return interpretations['same'];
|
||||
}
|
||||
// 상극
|
||||
const key = `${el1}_${overcomeTarget(el1)}`;
|
||||
if (interpretations[key]) return interpretations[key];
|
||||
const reverseKey = `${el2}_${overcomeTarget(el2)}`;
|
||||
if (interpretations[reverseKey]) return interpretations[reverseKey];
|
||||
return interpretations['same'];
|
||||
}
|
||||
|
||||
function produceTarget(el: string): string {
|
||||
const map: Record<string, string> = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
|
||||
return map[el] || '';
|
||||
}
|
||||
|
||||
function overcomeTarget(el: string): string {
|
||||
const map: Record<string, string> = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
|
||||
return map[el] || '';
|
||||
}
|
||||
|
||||
// 지지 관계 추가 해석
|
||||
function getBranchInterpretation(branchRel: { sixHarmony: boolean; threeHarmony: boolean; conflict: boolean }) {
|
||||
const extraPros: string[] = [];
|
||||
const extraCons: string[] = [];
|
||||
|
||||
if (branchRel.sixHarmony) {
|
||||
extraPros.push('두 분의 일지가 육합(六合) 관계예요! 천생연분이라고 할 만큼 자연스럽게 잘 맞는 조합이에요.');
|
||||
extraPros.push('만나면 왠지 편안하고 마음이 통하는 느낌을 받으실 거예요.');
|
||||
}
|
||||
if (branchRel.threeHarmony) {
|
||||
extraPros.push('일지가 삼합(三合)에 속해 있어요. 함께 있으면 시너지가 나는 좋은 궁합이에요.');
|
||||
}
|
||||
if (branchRel.conflict) {
|
||||
extraCons.push('일지가 충(沖) 관계에 있어요. 처음에는 강하게 끌리지만, 갈등도 생기기 쉬운 조합이에요.');
|
||||
extraCons.push('충 관계는 변화와 자극이 많아서, 서로 인내하고 이해하려는 노력이 꼭 필요해요.');
|
||||
}
|
||||
|
||||
return { extraPros, extraCons };
|
||||
}
|
||||
|
||||
// 세부 항목별 한 줄 해석
|
||||
function getDetailedComment(category: string, score: number): string {
|
||||
if (category === '연애운') {
|
||||
if (score >= 80) return '함께하면 설렘이 끊이지 않는 로맨틱한 관계가 될 거예요.';
|
||||
if (score >= 65) return '서로에게 좋은 감정을 느끼며 자연스럽게 가까워질 수 있어요.';
|
||||
return '처음에는 어색할 수 있지만, 시간이 지나면 깊은 정이 쌓일 거예요.';
|
||||
}
|
||||
if (category === '결혼운') {
|
||||
if (score >= 80) return '결혼 후에도 서로를 존중하며 행복한 가정을 꾸릴 수 있어요.';
|
||||
if (score >= 65) return '일상의 소소한 행복을 함께 나누며 안정적인 결혼생활이 가능해요.';
|
||||
return '결혼생활에서는 서로의 역할 분담과 소통이 특히 중요한 커플이에요.';
|
||||
}
|
||||
if (category === '금전운') {
|
||||
if (score >= 80) return '함께 재물을 모으고 불리는 데 시너지가 나는 조합이에요.';
|
||||
if (score >= 65) return '경제적으로 안정적인 파트너십을 만들어갈 수 있어요.';
|
||||
return '금전 문제에 대해 미리 충분히 대화하고 합의하는 것이 중요해요.';
|
||||
}
|
||||
// 사업운
|
||||
if (score >= 80) return '비즈니스 파트너로도 훌륭한 조합이에요. 함께하면 성공 가능성이 높아요.';
|
||||
if (score >= 65) return '각자의 강점을 살려 역할을 나누면 좋은 결과를 만들 수 있어요.';
|
||||
return '사업적으로는 신중한 접근이 필요해요. 역할과 책임을 명확히 하세요.';
|
||||
}
|
||||
|
||||
export default async function CompatibilityResultPage({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const { year1, month1, day1, hour1, gender1, year2, month2, day2, hour2, gender2 } = params;
|
||||
|
||||
// Person 1 Saju
|
||||
const saju1 = calculateSaju(
|
||||
parseInt(year1),
|
||||
parseInt(month1),
|
||||
parseInt(day1),
|
||||
hour1 ? parseInt(hour1) : null,
|
||||
gender1
|
||||
);
|
||||
const saju1 = calculateSaju(parseInt(year1), parseInt(month1), parseInt(day1), hour1 ? parseInt(hour1) : null, gender1);
|
||||
const saju2 = calculateSaju(parseInt(year2), parseInt(month2), parseInt(day2), hour2 ? parseInt(hour2) : null, gender2);
|
||||
|
||||
// Person 2 Saju
|
||||
const saju2 = calculateSaju(
|
||||
parseInt(year2),
|
||||
parseInt(month2),
|
||||
parseInt(day2),
|
||||
hour2 ? parseInt(hour2) : null,
|
||||
gender2
|
||||
);
|
||||
const element1 = saju1.day.element;
|
||||
const element2 = saju2.day.element;
|
||||
const branch1 = saju1.day.branch;
|
||||
const branch2 = saju2.day.branch;
|
||||
|
||||
const elementRelation = getElementRelation(element1, element2);
|
||||
const branchRel = getBranchRelation(branch1, branch2);
|
||||
|
||||
// 궁합 점수 계산
|
||||
const calculateCompatibility = () => {
|
||||
let score = 50; // 기본 점수
|
||||
let score = 50;
|
||||
if (element1 === element2) score += 10;
|
||||
else if (elementRelation === 'produce' || elementRelation === 'produced') score += 25;
|
||||
else score -= 10; // overcome/overcomed
|
||||
|
||||
// 오행 상생/상극 관계
|
||||
const element1 = saju1.day.element;
|
||||
const element2 = saju2.day.element;
|
||||
|
||||
const produceMap: { [key: string]: string } = {
|
||||
'木': '火', '火': '土', '土': '金', '金': '水', '水': '木'
|
||||
};
|
||||
const overcomeMap: { [key: string]: string } = {
|
||||
'木': '土', '火': '金', '土': '水', '金': '木', '水': '火'
|
||||
};
|
||||
|
||||
// 같은 오행: 보통
|
||||
if (element1 === element2) {
|
||||
score += 10;
|
||||
}
|
||||
// 상생 관계: 매우 좋음
|
||||
else if (produceMap[element1] === element2 || produceMap[element2] === element1) {
|
||||
score += 25;
|
||||
}
|
||||
// 상극 관계: 주의 필요
|
||||
else if (overcomeMap[element1] === element2 || overcomeMap[element2] === element1) {
|
||||
score -= 10;
|
||||
}
|
||||
|
||||
// 지지 삼합/육합 관계 확인
|
||||
const branch1 = saju1.day.branch;
|
||||
const branch2 = saju2.day.branch;
|
||||
|
||||
// 육합 (六合) - 특별히 좋은 궁합
|
||||
const sixHarmony: { [key: string]: string } = {
|
||||
'子': '丑', '丑': '子',
|
||||
'寅': '亥', '亥': '寅',
|
||||
'卯': '戌', '戌': '卯',
|
||||
'辰': '酉', '酉': '辰',
|
||||
'巳': '申', '申': '巳',
|
||||
'午': '未', '未': '午'
|
||||
};
|
||||
|
||||
if (sixHarmony[branch1] === branch2) {
|
||||
score += 20;
|
||||
}
|
||||
|
||||
// 삼합 (三合) - 좋은 궁합
|
||||
const threeHarmony = [
|
||||
['申', '子', '辰'], // 수국
|
||||
['寅', '午', '戌'], // 화국
|
||||
['亥', '卯', '未'], // 목국
|
||||
['巳', '酉', '丑'] // 금국
|
||||
];
|
||||
|
||||
for (const harmony of threeHarmony) {
|
||||
if (harmony.includes(branch1) && harmony.includes(branch2)) {
|
||||
score += 15;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 충 (沖) - 나쁜 궁합
|
||||
const conflict: { [key: string]: string } = {
|
||||
'子': '午', '午': '子',
|
||||
'丑': '未', '未': '丑',
|
||||
'寅': '申', '申': '寅',
|
||||
'卯': '酉', '酉': '卯',
|
||||
'辰': '戌', '戌': '辰',
|
||||
'巳': '亥', '亥': '巳'
|
||||
};
|
||||
|
||||
if (conflict[branch1] === branch2) {
|
||||
score -= 20;
|
||||
}
|
||||
if (branchRel.sixHarmony) score += 20;
|
||||
if (branchRel.threeHarmony) score += 15;
|
||||
if (branchRel.conflict) score -= 20;
|
||||
|
||||
return Math.min(100, Math.max(0, score));
|
||||
};
|
||||
@@ -144,13 +368,25 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
return 'from-gray-400 to-gray-500';
|
||||
};
|
||||
|
||||
// 세부 궁합 점수
|
||||
// Deterministic seed for detailed scores
|
||||
const seed = `${year1}${month1}${day1}${year2}${month2}${day2}`;
|
||||
const detailedScores = [
|
||||
{ name: '연애운', score: compatibilityScore + Math.floor(Math.random() * 10 - 5), icon: '💑' },
|
||||
{ name: '결혼운', score: compatibilityScore + Math.floor(Math.random() * 10 - 5), icon: '💍' },
|
||||
{ name: '금전운', score: compatibilityScore + Math.floor(Math.random() * 10 - 5), icon: '💰' },
|
||||
{ name: '사업운', score: compatibilityScore + Math.floor(Math.random() * 10 - 5), icon: '💼' },
|
||||
].map(item => ({ ...item, score: Math.min(100, Math.max(0, item.score)) }));
|
||||
{ name: '연애운', score: compatibilityScore + seededOffset(seed, 1), icon: '💑' },
|
||||
{ name: '결혼운', score: compatibilityScore + seededOffset(seed, 2), icon: '💍' },
|
||||
{ name: '금전운', score: compatibilityScore + seededOffset(seed, 3), icon: '💰' },
|
||||
{ name: '사업운', score: compatibilityScore + seededOffset(seed, 4), icon: '💼' },
|
||||
].map(item => ({
|
||||
...item,
|
||||
score: Math.min(100, Math.max(0, item.score)),
|
||||
comment: getDetailedComment(item.name, Math.min(100, Math.max(0, compatibilityScore + seededOffset(seed, ['연애운', '결혼운', '금전운', '사업운'].indexOf(item.name) + 1)))),
|
||||
}));
|
||||
|
||||
// 오행 기반 상세 해석
|
||||
const interp = getElementInterpretation(element1, element2, elementRelation);
|
||||
const branchInterp = getBranchInterpretation(branchRel);
|
||||
|
||||
const allPros = [...interp.pros, ...branchInterp.extraPros];
|
||||
const allCons = [...interp.cons, ...branchInterp.extraCons];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-pink-50 via-purple-50 to-indigo-50">
|
||||
@@ -162,18 +398,8 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
🔮 사주보기
|
||||
</Link>
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
href="/compatibility"
|
||||
className="text-gray-700 hover:text-pink-600 transition font-medium"
|
||||
>
|
||||
다시 보기
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-700 hover:text-pink-600 transition font-medium"
|
||||
>
|
||||
처음으로
|
||||
</Link>
|
||||
<Link href="/compatibility" className="text-gray-700 hover:text-pink-600 transition font-medium">다시 보기</Link>
|
||||
<Link href="/" className="text-gray-700 hover:text-pink-600 transition font-medium">처음으로</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,27 +409,18 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
<div id="pdf-content" className="max-w-6xl mx-auto px-4 py-12">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
|
||||
💕 궁합 결과
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
두 사람의 사주팔자를 비교한 결과입니다
|
||||
</p>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4">💕 궁합 결과</h1>
|
||||
<p className="text-xl text-gray-600">두 사람의 사주팔자를 비교한 결과입니다</p>
|
||||
</div>
|
||||
|
||||
{/* 두 사람 정보 */}
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-12">
|
||||
{/* Person 1 */}
|
||||
<div className="bg-gradient-to-br from-pink-500 to-pink-600 rounded-2xl shadow-xl p-8 text-white">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-5xl mb-3">👤</div>
|
||||
<h3 className="text-2xl font-bold">첫 번째 사람</h3>
|
||||
<p className="text-pink-100 mt-2">
|
||||
{saju1.birthDate.year}년 {saju1.birthDate.month}월 {saju1.birthDate.day}일
|
||||
</p>
|
||||
<p className="text-pink-100">
|
||||
{gender1 === 'male' ? '남성' : '여성'}
|
||||
</p>
|
||||
<p className="text-pink-100 mt-2">{saju1.birthDate.year}년 {saju1.birthDate.month}월 {saju1.birthDate.day}일</p>
|
||||
<p className="text-pink-100">{gender1 === 'male' ? '남성' : '여성'}</p>
|
||||
</div>
|
||||
<div className="bg-white/20 backdrop-blur-sm rounded-xl p-4">
|
||||
<p className="text-center mb-2 font-semibold">일간 (日干)</p>
|
||||
@@ -212,24 +429,17 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
<span className="text-2xl ml-2">({saju1.day.stemKr})</span>
|
||||
</div>
|
||||
<p className="text-center mt-2 text-pink-100">
|
||||
{saju1.day.element} ({['木', '火', '土', '金', '水'].indexOf(saju1.day.element) >= 0
|
||||
? ['목', '화', '토', '금', '수'][['木', '火', '土', '金', '水'].indexOf(saju1.day.element)]
|
||||
: ''})
|
||||
{element1} ({elementKrMap[element1] || ''})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Person 2 */}
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl shadow-xl p-8 text-white">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-5xl mb-3">👤</div>
|
||||
<h3 className="text-2xl font-bold">두 번째 사람</h3>
|
||||
<p className="text-purple-100 mt-2">
|
||||
{saju2.birthDate.year}년 {saju2.birthDate.month}월 {saju2.birthDate.day}일
|
||||
</p>
|
||||
<p className="text-purple-100">
|
||||
{gender2 === 'male' ? '남성' : '여성'}
|
||||
</p>
|
||||
<p className="text-purple-100 mt-2">{saju2.birthDate.year}년 {saju2.birthDate.month}월 {saju2.birthDate.day}일</p>
|
||||
<p className="text-purple-100">{gender2 === 'male' ? '남성' : '여성'}</p>
|
||||
</div>
|
||||
<div className="bg-white/20 backdrop-blur-sm rounded-xl p-4">
|
||||
<p className="text-center mb-2 font-semibold">일간 (日干)</p>
|
||||
@@ -238,9 +448,7 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
<span className="text-2xl ml-2">({saju2.day.stemKr})</span>
|
||||
</div>
|
||||
<p className="text-center mt-2 text-purple-100">
|
||||
{saju2.day.element} ({['木', '火', '土', '金', '水'].indexOf(saju2.day.element) >= 0
|
||||
? ['목', '화', '토', '금', '수'][['木', '火', '土', '金', '水'].indexOf(saju2.day.element)]
|
||||
: ''})
|
||||
{element2} ({elementKrMap[element2] || ''})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,10 +467,7 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="w-full bg-white/30 rounded-full h-4">
|
||||
<div
|
||||
className="bg-white h-4 rounded-full transition-all duration-1000"
|
||||
style={{ width: `${compatibilityScore}%` }}
|
||||
></div>
|
||||
<div className="bg-white h-4 rounded-full transition-all duration-1000" style={{ width: `${compatibilityScore}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,89 +486,38 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-pink-600">{item.score}점</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 mb-3">
|
||||
<div
|
||||
className="bg-gradient-to-r from-pink-500 to-purple-500 h-3 rounded-full transition-all duration-500"
|
||||
style={{ width: `${item.score}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{item.comment}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 궁합 해석 */}
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-8">
|
||||
{/* 장점 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">✅</span>
|
||||
두 사람의 장점
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<span className="text-pink-600 mr-2">•</span>
|
||||
<span>서로의 부족한 점을 잘 보완해줄 수 있습니다.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-pink-600 mr-2">•</span>
|
||||
<span>대화와 소통이 원활하게 이루어질 수 있습니다.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-pink-600 mr-2">•</span>
|
||||
<span>함께 있을 때 편안함을 느낄 수 있습니다.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 주의점 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">⚠️</span>
|
||||
주의할 점
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-600 mr-2">•</span>
|
||||
<span>서로의 가치관 차이를 이해하려는 노력이 필요합니다.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-600 mr-2">•</span>
|
||||
<span>감정적인 대화보다는 이성적인 대화를 나누세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-600 mr-2">•</span>
|
||||
<span>작은 문제도 소통으로 해결하려는 자세가 중요합니다.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조언 */}
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl shadow-lg p-8 text-white mb-8">
|
||||
<h3 className="text-2xl font-bold mb-4 text-center">💡 조언</h3>
|
||||
<p className="text-lg leading-relaxed text-center">
|
||||
궁합은 참고사항일 뿐입니다. 서로를 이해하고 존중하며 노력한다면
|
||||
어떤 궁합이든 행복한 관계를 만들 수 있습니다.
|
||||
사주는 가능성을 보여줄 뿐, 최종 결정은 두 사람의 마음과 노력에 달려있습니다.
|
||||
</p>
|
||||
</div>
|
||||
{/* 궁합 상세 해석 - 블러 잠금 영역 */}
|
||||
<CompatibilityDetailUnlock
|
||||
allPros={allPros}
|
||||
allCons={allCons}
|
||||
advice={interp.advice}
|
||||
element1={element1}
|
||||
element2={element2}
|
||||
element1Kr={elementKrMap[element1] || ''}
|
||||
element2Kr={elementKrMap[element2] || ''}
|
||||
/>
|
||||
|
||||
{/* 다른 메뉴 */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Link
|
||||
href="/compatibility"
|
||||
className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition text-center group"
|
||||
>
|
||||
<Link href="/compatibility" className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition text-center group">
|
||||
<div className="text-4xl mb-3">💕</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">다시 보기</h3>
|
||||
<p className="text-gray-600 text-sm">다른 궁합 확인하기</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition text-center group"
|
||||
>
|
||||
<Link href="/" className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition text-center group">
|
||||
<div className="text-4xl mb-3">📜</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">사주 보기</h3>
|
||||
<p className="text-gray-600 text-sm">내 사주 확인하기</p>
|
||||
@@ -389,9 +543,7 @@ export default async function CompatibilityResultPage({ searchParams }: PageProp
|
||||
<div className="text-2xl font-bold mb-4 bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">
|
||||
🔮 사주보기
|
||||
</div>
|
||||
<p className="text-gray-400 mb-6">
|
||||
쟁승메이드가 제공하는 무료 사주 서비스
|
||||
</p>
|
||||
<p className="text-gray-400 mb-6">쟁승메이드가 제공하는 무료 사주 서비스</p>
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>문의: bgg8988@gmail.com | <a href="https://jaengseung-made.com" target="_blank" rel="noopener noreferrer" className="hover:text-pink-400">쟁승메이드</a></p>
|
||||
<p className="mt-2">© 2025 쟁승메이드. All rights reserved.</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
@@ -27,23 +28,13 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
|
||||
<script
|
||||
src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.0/kakao.min.js"
|
||||
integrity="sha384-l+xbElFSnPZ2rOaPrU//2FF5B4LB8FiX5q4fXYTlfcG4PGpMkE1vcL7kNXI6Cci0"
|
||||
crossOrigin="anonymous"
|
||||
async
|
||||
></script>
|
||||
{kakaoAppKey && (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (typeof window !== 'undefined' && window.Kakao && !window.Kakao.isInitialized()) {
|
||||
window.Kakao.init('${kakaoAppKey}');
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
|
||||
163
app/login/page.tsx
Normal file
163
app/login/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
const handleAuth = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (isSignUp) {
|
||||
// Sign Up with Password
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`,
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
alert('회원가입 실패: ' + error.message);
|
||||
} else if (data.user && data.user.identities && data.user.identities.length === 0) {
|
||||
alert('이미 가입된 이메일입니다. 로그인해주세요.');
|
||||
} else {
|
||||
alert('가입이 완료되었습니다! 이메일 인증이 필요할 수 있습니다. 로그인을 시도해주세요.');
|
||||
setIsSignUp(false); // Switch to login mode
|
||||
}
|
||||
} else {
|
||||
// Sign In with Password
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
alert('로그인 실패: ' + error.message);
|
||||
} else {
|
||||
router.push('/mypage');
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleSocialLogin = async (provider: 'google' | 'kakao') => {
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
},
|
||||
});
|
||||
if (error) alert('소셜 로그인 오류: ' + error.message);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-purple-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{isSignUp ? '회원가입' : '로그인'}
|
||||
</h1>
|
||||
<p className="text-gray-500">사주 기록을 저장하고 언제든 다시 확인하세요</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAuth} className="space-y-4 mb-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
이메일 주소
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
비밀번호
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-lg transition disabled:opacity-50"
|
||||
>
|
||||
{loading ? '처리 중...' : (isSignUp ? '회원가입' : '로그인')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center mb-6">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
onClick={() => setIsSignUp(!isSignUp)}
|
||||
>
|
||||
{isSignUp ? '이미 계정이 있으신가요? 로그인' : '계정이 없으신가요? 회원가입'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">또는 소셜 로그인</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => handleSocialLogin('google')}
|
||||
className="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
<span className="ml-2">Google</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSocialLogin('kakao')}
|
||||
className="flex items-center justify-center px-4 py-2 bg-[#FEE500] border border-[#FEE500] rounded-lg hover:bg-[#FDD835] transition"
|
||||
>
|
||||
<span className="ml-2 text-[#000000bd]">Kakao</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-yellow-50 rounded-lg text-xs text-yellow-800 border border-yellow-200">
|
||||
<h4 className="font-bold mb-1">⚠️ 회원가입 후 이메일 인증 필수!</h4>
|
||||
<p className="mb-2">
|
||||
Supabase는 기본적으로 이메일 인증을 요구합니다.
|
||||
가입 후 받은 메일의 링크를 클릭해야 로그인이 가능합니다.
|
||||
</p>
|
||||
<p className="font-semibold text-gray-700">
|
||||
(개발용 팁: Supabase 대시보드 > Authentication > Providers > Email > "Confirm email"를 끄면 인증 없이 바로 로그인 가능)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
app/mypage/page.tsx
Normal file
185
app/mypage/page.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TokenPurchaseModal from '@/components/TokenPurchaseModal';
|
||||
import { ensureProfile } from '@/lib/ensure-profile';
|
||||
|
||||
export default function MyPage() {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [credits, setCredits] = useState(0);
|
||||
const [showTokenModal, setShowTokenModal] = useState(false);
|
||||
const router = useRouter();
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUserData() {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
setUser(user);
|
||||
|
||||
// Fetch credits (프로필 없으면 자동 생성)
|
||||
const currentCredits = await ensureProfile(supabase, user);
|
||||
setCredits(currentCredits);
|
||||
|
||||
// Fetch records
|
||||
const { data, error } = await supabase
|
||||
.from('saju_records')
|
||||
.select('*')
|
||||
.eq('user_id', user.id) // Ensure only fetching user's records
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching records:', error.message, error.details, error.hint);
|
||||
alert('기록을 불러오는 중 오류가 발생했습니다: ' + error.message);
|
||||
} else {
|
||||
setRecords(data || []);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
getUserData();
|
||||
}, [router]);
|
||||
|
||||
if (loading) return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-16 h-16 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link href="/" className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
🔮 사주포춘
|
||||
</Link>
|
||||
<div className="hidden md:flex space-x-6">
|
||||
<Link href="/saju" className="text-gray-700 hover:text-indigo-600 transition font-medium">사주팔자</Link>
|
||||
<Link href="/compatibility" className="text-gray-700 hover:text-indigo-600 transition font-medium">궁합</Link>
|
||||
<Link href="/tojeong" className="text-gray-700 hover:text-indigo-600 transition font-medium">토정비결</Link>
|
||||
<Link href="/mypage" className="text-indigo-600 font-bold">마이페이지</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 md:p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<header className="mb-8 flex justify-between items-center bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||
<h1 className="text-3xl font-bold text-gray-900">내 사주 보관함</h1>
|
||||
<Link href="/saju" className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition font-bold shadow-md">
|
||||
+ 새 사주 보기
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-sm p-6 mb-8 border border-gray-100 flex flex-col md:flex-row items-center gap-6">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white text-3xl font-bold shadow-lg">
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">{user.email}</h2>
|
||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
||||
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-sm">회원</span>
|
||||
<span className="px-3 py-1 bg-indigo-50 text-indigo-700 rounded-full text-sm font-semibold">보유 토큰: {credits}개</span>
|
||||
<button
|
||||
onClick={() => setShowTokenModal(true)}
|
||||
className="px-3 py-1 bg-indigo-600 text-white rounded-full text-sm font-semibold hover:bg-indigo-700 transition"
|
||||
>
|
||||
토큰 충전
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await supabase.auth.signOut();
|
||||
router.push('/');
|
||||
router.refresh();
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700 transition"
|
||||
>
|
||||
로그아웃
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4 ml-2 border-l-4 border-indigo-500 pl-3">저장된 기록 ({records.length})</h2>
|
||||
|
||||
{records.length === 0 ? (
|
||||
<div className="text-center py-20 bg-white rounded-2xl border border-gray-200 border-dashed">
|
||||
<div className="text-6xl mb-4">📭</div>
|
||||
<p className="text-xl text-gray-800 font-bold mb-2">아직 저장된 사주 기록이 없습니다.</p>
|
||||
<p className="text-gray-500 mb-8">나의 운세를 확인하고 평생 소장해보세요!</p>
|
||||
<Link
|
||||
href="/saju"
|
||||
className="px-8 py-3 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition font-bold shadow-lg hover:shadow-xl transform hover:-translate-y-1"
|
||||
>
|
||||
지금 무료로 사주 보기
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{records.map((record) => (
|
||||
<div key={record.id} className="bg-white p-6 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 border border-gray-100 group">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<span className="inline-block px-3 py-1 bg-indigo-50 text-indigo-700 text-xs font-bold rounded-full mb-2">
|
||||
{new Date(record.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-gray-900 group-hover:text-indigo-600 transition">
|
||||
{record.saju_data.birthDate.year}년 {record.saju_data.birthDate.month}월 {record.saju_data.birthDate.day}일생
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-2xl">🔮</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-6 line-clamp-3 bg-gray-50 p-3 rounded-lg h-20">
|
||||
{record.interpretation
|
||||
? record.interpretation.replace(/[#*]/g, '').substring(0, 100) + '...'
|
||||
: '해석 내용 없음'}
|
||||
</div>
|
||||
<Link
|
||||
href={`/result/saved/${record.id}`}
|
||||
className="block w-full text-center py-3 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition font-bold shadow-md"
|
||||
>
|
||||
다시 보기
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TokenPurchaseModal
|
||||
isOpen={showTokenModal}
|
||||
onClose={() => setShowTokenModal(false)}
|
||||
onPurchaseComplete={async () => {
|
||||
setShowTokenModal(false);
|
||||
if (user) {
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('credits')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
if (profile) setCredits(profile.credits || 0);
|
||||
}
|
||||
}}
|
||||
user={user}
|
||||
supabase={supabase}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
347
app/page.tsx
347
app/page.tsx
@@ -1,220 +1,229 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import SajuForm from './components/SajuForm';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50">
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50">
|
||||
<nav className="bg-gray-950/80 backdrop-blur-md border-b border-gray-800 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
🔮 사주보기
|
||||
</div>
|
||||
<div className="hidden md:flex space-x-6">
|
||||
<Link href="#saju" className="text-gray-700 hover:text-indigo-600 transition font-medium">사주팔자</Link>
|
||||
<Link href="/compatibility" className="text-gray-700 hover:text-indigo-600 transition font-medium">궁합</Link>
|
||||
<Link href="/" className="text-2xl font-bold bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent">
|
||||
🔮 사주포춘
|
||||
</Link>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="hidden md:flex space-x-6 mr-4">
|
||||
<Link href="/saju" className="text-gray-300 hover:text-amber-400 transition font-medium">사주팔자</Link>
|
||||
<Link href="/compatibility" className="text-gray-300 hover:text-amber-400 transition font-medium">궁합</Link>
|
||||
<Link href="/tojeong" className="text-gray-300 hover:text-amber-400 transition font-medium">토정비결</Link>
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="pt-20 pb-32 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="inline-block mb-6 px-6 py-2 bg-white/50 backdrop-blur-sm rounded-full text-indigo-700 font-semibold border border-indigo-200">
|
||||
무료로 내 사주를 확인해보세요
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Background decorations */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-20 left-10 w-72 h-72 bg-amber-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute top-40 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-20 left-1/3 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 pt-24 pb-32 text-center">
|
||||
<div className="inline-block mb-8 px-5 py-2 bg-amber-500/10 border border-amber-500/30 rounded-full text-amber-400 text-sm font-semibold tracking-wide">
|
||||
AI 명리학 전문 상담
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-gray-900 mb-6 leading-tight">
|
||||
나의 <span className="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">사주팔자</span>를<br />
|
||||
확인하세요
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-8 leading-tight">
|
||||
당신의 <span className="bg-gradient-to-r from-amber-400 via-orange-400 to-red-400 bg-clip-text text-transparent">운명</span>을<br />
|
||||
읽어드립니다
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 mb-12 max-w-2xl mx-auto">
|
||||
생년월일시를 입력하면 무료로 사주팔자, 운세, 궁합을 확인할 수 있습니다.
|
||||
쉽고 빠르게 나의 운명을 알아보세요.
|
||||
<p className="text-xl md:text-2xl text-gray-400 mb-16 max-w-3xl mx-auto leading-relaxed">
|
||||
수천 년 전통 명리학의 지혜와 최신 AI 기술이 만나<br className="hidden md:block" />
|
||||
당신만을 위한 깊이 있는 사주 해석을 제공합니다.
|
||||
</p>
|
||||
|
||||
{/* Main Input Card */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl p-8 md:p-12 max-w-2xl mx-auto">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-8">생년월일시 입력</h2>
|
||||
|
||||
<SajuForm />
|
||||
{/* 생년월일 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
생년월일
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="년 (예: 1990)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition"
|
||||
min="1900"
|
||||
max="2100"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="월 (1-12)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition"
|
||||
min="1"
|
||||
max="12"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="일 (1-31)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition"
|
||||
min="1"
|
||||
max="31"
|
||||
/>
|
||||
</div>
|
||||
{/* Service Cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-20">
|
||||
{/* 사주팔자 */}
|
||||
<Link href="/saju" className="group relative bg-gradient-to-br from-gray-900 to-gray-800 border border-gray-700 rounded-3xl p-8 hover:border-amber-500/50 transition-all duration-300 hover:shadow-2xl hover:shadow-amber-500/10 text-left">
|
||||
<div className="text-5xl mb-6">📜</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-3 group-hover:text-amber-400 transition">사주팔자</h3>
|
||||
<p className="text-gray-400 mb-6 leading-relaxed">
|
||||
천간과 지지의 조화로 타고난 성격, 재능, 인생의 흐름을 심층 분석합니다.
|
||||
</p>
|
||||
<div className="flex items-center text-amber-400 font-semibold text-sm">
|
||||
<span>무료 감정 시작</span>
|
||||
<svg className="w-4 h-4 ml-2 group-hover:translate-x-1 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 px-2 py-1 bg-amber-500/20 text-amber-400 text-xs font-bold rounded-full">인기</div>
|
||||
</Link>
|
||||
|
||||
{/* 태어난 시간 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
태어난 시간 (선택)
|
||||
</label>
|
||||
<select className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition">
|
||||
<option value="">모름 / 시간 선택 안함</option>
|
||||
<option value="23-01">자시 (子時) 23:00 - 01:00</option>
|
||||
<option value="01-03">축시 (丑時) 01:00 - 03:00</option>
|
||||
<option value="03-05">인시 (寅時) 03:00 - 05:00</option>
|
||||
<option value="05-07">묘시 (卯時) 05:00 - 07:00</option>
|
||||
<option value="07-09">진시 (辰時) 07:00 - 09:00</option>
|
||||
<option value="09-11">사시 (巳時) 09:00 - 11:00</option>
|
||||
<option value="11-13">오시 (午時) 11:00 - 13:00</option>
|
||||
<option value="13-15">미시 (未時) 13:00 - 15:00</option>
|
||||
<option value="15-17">신시 (申時) 15:00 - 17:00</option>
|
||||
<option value="17-19">유시 (酉時) 17:00 - 19:00</option>
|
||||
<option value="19-21">술시 (戌時) 19:00 - 21:00</option>
|
||||
<option value="21-23">해시 (亥時) 21:00 - 23:00</option>
|
||||
</select>
|
||||
{/* 궁합 */}
|
||||
<Link href="/compatibility" className="group relative bg-gradient-to-br from-gray-900 to-gray-800 border border-gray-700 rounded-3xl p-8 hover:border-pink-500/50 transition-all duration-300 hover:shadow-2xl hover:shadow-pink-500/10 text-left">
|
||||
<div className="text-5xl mb-6">💕</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-3 group-hover:text-pink-400 transition">궁합 분석</h3>
|
||||
<p className="text-gray-400 mb-6 leading-relaxed">
|
||||
두 사람의 오행 관계와 지지 합충을 분석하여 관계의 조화를 확인합니다.
|
||||
</p>
|
||||
<div className="flex items-center text-pink-400 font-semibold text-sm">
|
||||
<span>궁합 확인하기</span>
|
||||
<svg className="w-4 h-4 ml-2 group-hover:translate-x-1 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 양력/음력 선택 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
생일 구분
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 bg-indigo-600 text-white rounded-xl font-semibold hover:bg-indigo-700 transition"
|
||||
>
|
||||
양력
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 bg-white border-2 border-gray-200 text-gray-700 rounded-xl font-semibold hover:border-indigo-500 hover:text-indigo-600 transition"
|
||||
>
|
||||
음력
|
||||
</button>
|
||||
</div>
|
||||
{/* 토정비결 */}
|
||||
<Link href="/tojeong" className="group relative bg-gradient-to-br from-gray-900 to-gray-800 border border-gray-700 rounded-3xl p-8 hover:border-emerald-500/50 transition-all duration-300 hover:shadow-2xl hover:shadow-emerald-500/10 text-left">
|
||||
<div className="text-5xl mb-6">🎋</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-3 group-hover:text-emerald-400 transition">토정비결</h3>
|
||||
<p className="text-gray-400 mb-6 leading-relaxed">
|
||||
올 한 해의 월별 운세와 카테고리별 운세를 미리 확인하고 준비하세요.
|
||||
</p>
|
||||
<div className="flex items-center text-emerald-400 font-semibold text-sm">
|
||||
<span>올해 운세 보기</span>
|
||||
<svg className="w-4 h-4 ml-2 group-hover:translate-x-1 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</div>
|
||||
|
||||
{/* 성별 선택 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
성별
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 bg-indigo-600 text-white rounded-xl font-semibold hover:bg-indigo-700 transition"
|
||||
>
|
||||
남성
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 bg-white border-2 border-gray-200 text-gray-700 rounded-xl font-semibold hover:border-indigo-500 hover:text-indigo-600 transition"
|
||||
>
|
||||
여성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 제출 버튼 */}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-20 px-4 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Why Us Section */}
|
||||
<section className="py-24 px-4 bg-gray-900/50">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">무엇을 확인할 수 있나요?</h2>
|
||||
<p className="text-xl text-gray-600">다양한 사주 정보를 한 번에 확인하세요</p>
|
||||
<h2 className="text-4xl font-bold mb-4">왜 <span className="text-amber-400">사주포춘</span>인가요?</h2>
|
||||
<p className="text-xl text-gray-400">다른 곳에서는 경험할 수 없는 차별화된 사주 서비스</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{/* Feature 1 */}
|
||||
<div className="text-center p-6 rounded-2xl hover:bg-indigo-50 transition">
|
||||
<div className="text-5xl mb-4">📜</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">사주팔자</h3>
|
||||
<p className="text-gray-600">
|
||||
나의 천간, 지지, 십성, 십이운성을 확인하고 운명의 흐름을 파악하세요.
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-amber-500/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
|
||||
<span className="text-3xl">🧠</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">AI 심층 분석</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">단순 공식이 아닌, AI가 천간·지지·십성의 복합 관계를 해석합니다.</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="text-center p-6 rounded-2xl hover:bg-purple-50 transition">
|
||||
<div className="text-5xl mb-4">🌟</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">오늘의 운세</h3>
|
||||
<p className="text-gray-600">
|
||||
사주를 확인한 후 오늘 하루의 운세를 확인하고 행운의 방향을 찾아보세요.
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-purple-500/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
|
||||
<span className="text-3xl">📚</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">전통 명리학 기반</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">수천 년 역사의 사주명리학 이론을 정확하게 구현했습니다.</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-pink-500/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
|
||||
<span className="text-3xl">🔒</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">평생 보관</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">한 번 열람한 결과는 평생 다시 볼 수 있어요. 재결제 없이.</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-emerald-500/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
|
||||
<span className="text-3xl">⚡</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">즉시 결과</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">생년월일 입력 즉시 사주팔자와 대운을 무료로 확인할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<Link href="/compatibility" className="text-center p-6 rounded-2xl hover:bg-pink-50 transition block">
|
||||
<div className="text-5xl mb-4">💕</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">궁합</h3>
|
||||
<p className="text-gray-600">
|
||||
두 사람의 사주를 비교하여 궁합을 확인하세요.
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
{/* Feature 4 */}
|
||||
<Link href="/tojeong" className="text-center p-6 rounded-2xl hover:bg-amber-50 transition block">
|
||||
<div className="text-5xl mb-4">🎋</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">토정비결</h3>
|
||||
<p className="text-gray-600">
|
||||
한 해의 운세를 미리 확인하고 준비하세요.
|
||||
</p>
|
||||
</Link>
|
||||
{/* Testimonial / Trust Section */}
|
||||
<section className="py-24 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="bg-gradient-to-br from-gray-900 to-gray-800 border border-gray-700 rounded-3xl p-12">
|
||||
<div className="text-6xl mb-6">✨</div>
|
||||
<p className="text-2xl md:text-3xl font-bold mb-4 leading-relaxed">
|
||||
“소름 돋을 정도로 정확해요”
|
||||
</p>
|
||||
<p className="text-gray-400 text-lg mb-8">
|
||||
이미 수천 명이 사주포춘으로 자신의 운명을 확인했습니다.
|
||||
</p>
|
||||
<div className="flex justify-center gap-8 text-center">
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-amber-400">5,000+</div>
|
||||
<div className="text-sm text-gray-500 mt-1">누적 분석</div>
|
||||
</div>
|
||||
<div className="w-px bg-gray-700"></div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-amber-400">4.8</div>
|
||||
<div className="text-sm text-gray-500 mt-1">만족도</div>
|
||||
</div>
|
||||
<div className="w-px bg-gray-700"></div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-amber-400">97%</div>
|
||||
<div className="text-sm text-gray-500 mt-1">정확도 체감</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20 px-4 bg-gradient-to-r from-indigo-600 to-purple-600">
|
||||
<div className="max-w-4xl mx-auto text-center text-white">
|
||||
<section className="py-24 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
지금 바로 내 사주를 확인해보세요
|
||||
지금 바로 운명을 확인하세요
|
||||
</h2>
|
||||
<p className="text-xl mb-8 opacity-90">
|
||||
무료로 제공되는 정확한 사주 정보로 나의 운명을 알아보세요
|
||||
<p className="text-xl text-gray-400 mb-10">
|
||||
생년월일만 입력하면 무료로 사주팔자를 확인할 수 있습니다.
|
||||
</p>
|
||||
<a href="#" className="inline-block bg-white text-indigo-600 px-8 py-4 rounded-xl text-lg font-bold hover:bg-gray-100 transition shadow-lg">
|
||||
무료로 시작하기
|
||||
</a>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<Link
|
||||
href="/saju"
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-amber-500 to-orange-500 text-gray-900 px-8 py-4 rounded-2xl text-lg font-bold hover:from-amber-400 hover:to-orange-400 transition shadow-lg shadow-amber-500/25 hover:shadow-xl hover:shadow-amber-500/30"
|
||||
>
|
||||
무료 사주 보기
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="/compatibility"
|
||||
className="inline-flex items-center gap-2 bg-gray-800 text-white px-8 py-4 rounded-2xl text-lg font-bold hover:bg-gray-700 transition border border-gray-700"
|
||||
>
|
||||
궁합 확인하기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white py-12 px-4">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<div className="text-2xl font-bold mb-4 bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
||||
🔮 사주보기
|
||||
<footer className="bg-gray-900 border-t border-gray-800 py-12 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-8">
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-4 bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent">
|
||||
🔮 사주포춘
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm leading-relaxed">
|
||||
전통 명리학과 AI 기술의 결합으로<br />
|
||||
가장 정확한 사주 서비스를 제공합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-300 mb-4 tracking-wide">서비스</h4>
|
||||
<div className="space-y-2">
|
||||
<Link href="/saju" className="block text-gray-500 hover:text-amber-400 transition text-sm">사주팔자</Link>
|
||||
<Link href="/compatibility" className="block text-gray-500 hover:text-amber-400 transition text-sm">궁합 분석</Link>
|
||||
<Link href="/tojeong" className="block text-gray-500 hover:text-amber-400 transition text-sm">토정비결</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-gray-300 mb-4 tracking-wide">고객지원</h4>
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-500 text-sm">문의: bgg8988@gmail.com</p>
|
||||
<a href="https://jaengseung-made.com" target="_blank" rel="noopener noreferrer" className="block text-gray-500 hover:text-amber-400 transition text-sm">쟁승메이드</a>
|
||||
<Link href="/mypage" className="block text-gray-500 hover:text-amber-400 transition text-sm">마이페이지</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-6">
|
||||
쟁승메이드가 제공하는 무료 사주 서비스
|
||||
</p>
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>문의: bgg8988@gmail.com | <a href="https://jaengseung-made.com" target="_blank" rel="noopener noreferrer" className="hover:text-indigo-400">쟁승메이드</a></p>
|
||||
<p className="mt-2">© 2025 쟁승메이드. All rights reserved.</p>
|
||||
<div className="border-t border-gray-800 pt-8 text-center text-sm text-gray-600">
|
||||
<p>© 2025 쟁승메이드. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
|
||||
import { calculateSaju } from '@/lib/saju-calculator';
|
||||
import Link from 'next/link';
|
||||
import PDFButton from '../components/PDFButton';
|
||||
import ShareButtons from '../components/ShareButtons';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
import { calculateDaeun, getCurrentDaeun, getDaeunDescription } from '@/lib/daeun-calculator';
|
||||
import { getCurrentSolarTerm, getSolarTermName, getSolarTermMonthBranch } from '@/lib/solar-terms';
|
||||
import { EARTHLY_BRANCHES_KR } from '@/lib/saju-calculator';
|
||||
import { generateInterpretation, calculateElementScore } from '@/lib/ai-interpretation';
|
||||
import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju-calculator';
|
||||
import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation';
|
||||
import AiInterpretationSection from '@/components/AiInterpretationSection';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -15,18 +18,31 @@ interface PageProps {
|
||||
hour?: string;
|
||||
gender: 'male' | 'female';
|
||||
calendarType: 'solar' | 'lunar';
|
||||
originalYear?: string;
|
||||
originalMonth?: string;
|
||||
originalDay?: string;
|
||||
isLeapMonth?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function ResultPage({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const { year, month, day, hour, gender } = params;
|
||||
const {
|
||||
year, month, day, hour, gender, calendarType,
|
||||
originalYear, originalMonth, originalDay, isLeapMonth
|
||||
} = params;
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
const monthNum = parseInt(month);
|
||||
const dayNum = parseInt(day);
|
||||
const hourNum = hour ? parseInt(hour) : null;
|
||||
|
||||
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);
|
||||
|
||||
// 절기 정보
|
||||
@@ -35,22 +51,29 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
const monthBranchIndex = getSolarTermMonthBranch(yearNum, monthNum, dayNum);
|
||||
const monthBranchName = EARTHLY_BRANCHES_KR[monthBranchIndex];
|
||||
|
||||
// AI 해석 생성
|
||||
const interpretation = generateInterpretation(sajuData);
|
||||
const elementScores = calculateElementScore(sajuData);
|
||||
// 종합 분석 수행
|
||||
const analysis = performFullAnalysis(sajuData);
|
||||
const elementScores = analysis.elementScores;
|
||||
|
||||
// 대운 계산
|
||||
const daeunList = calculateDaeun(
|
||||
yearNum,
|
||||
monthNum,
|
||||
dayNum,
|
||||
gender,
|
||||
sajuData.month.stem,
|
||||
sajuData.month.branch
|
||||
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-600', '火': 'text-red-500', '土': 'text-yellow-600',
|
||||
'金': 'text-gray-500', '水': 'text-blue-600',
|
||||
};
|
||||
const elementBgColors: { [key: string]: string } = {
|
||||
'木': 'bg-green-100 border-green-300', '火': 'bg-red-100 border-red-300',
|
||||
'土': 'bg-yellow-100 border-yellow-300', '金': 'bg-gray-100 border-gray-300',
|
||||
'水': 'bg-blue-100 border-blue-300',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50">
|
||||
{/* Navigation */}
|
||||
@@ -58,14 +81,14 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link href="/" className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
🔮 사주보기
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-700 hover:text-indigo-600 transition font-medium"
|
||||
>
|
||||
다시 보기
|
||||
사주보기
|
||||
</Link>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/" className="text-gray-700 hover:text-indigo-600 transition font-medium">
|
||||
다시 보기
|
||||
</Link>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -78,7 +101,19 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
내 사주팔자
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
{yearNum}년 {monthNum}월 {dayNum}일 {hourNum !== null && `${hourNum}시`}
|
||||
{isLunar ? (
|
||||
<>
|
||||
음력 {inputYear}년 {inputMonth}월 {inputDay}일{isLeap && ' (윤달)'} {hourNum !== null && `${hourNum}시`}
|
||||
<br />
|
||||
<span className="text-base text-gray-500">
|
||||
(양력 {yearNum}년 {monthNum}월 {dayNum}일)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{yearNum}년 {monthNum}월 {dayNum}일 {hourNum !== null && `${hourNum}시`}
|
||||
</>
|
||||
)}
|
||||
{gender === 'male' ? ' 남성' : ' 여성'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -146,6 +181,36 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 지장간 (NEW) */}
|
||||
<tr className="border-b border-gray-200 hover:bg-amber-50 transition">
|
||||
<td className="py-4 px-6 text-center font-semibold text-gray-700">
|
||||
지장간 (藏干)
|
||||
<div className="text-xs text-gray-400 mt-1">숨은 천간</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-3 px-4 text-center ${h?.pillar === '일주' ? 'bg-blue-50' : ''}`}>
|
||||
{h && (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
{h.stems.map((s, si) => (
|
||||
<span
|
||||
key={si}
|
||||
className={`inline-block px-2 py-1 rounded text-xs font-semibold border ${elementBgColors[s.element] || 'bg-gray-100'}`}
|
||||
title={s.role}
|
||||
>
|
||||
{s.stemKr}({FIVE_ELEMENTS_KR[s.element as keyof typeof FIVE_ELEMENTS_KR]})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
));
|
||||
})()}
|
||||
</tr>
|
||||
|
||||
{/* 십성 */}
|
||||
<tr className="border-b border-gray-200 hover:bg-emerald-50 transition">
|
||||
<td className="py-4 px-6 text-center font-semibold text-gray-700">십성 (十星)</td>
|
||||
@@ -187,103 +252,56 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="p-4 bg-blue-50 rounded-xl">
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong className="text-blue-600">일간 (日干):</strong> {sajuData.day.stem}({sajuData.day.stemKr}) - 나 자신을 나타내는 중심 기둥입니다.
|
||||
</p>
|
||||
{/* 지지 상호작용 (합/충/형/파/해) */}
|
||||
{analysis.branchInteractions.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-100">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-3 flex items-center justify-center">
|
||||
<span className="mr-2">🔗</span> 지지 상호작용
|
||||
</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-300 text-emerald-800'
|
||||
: isNegative
|
||||
? 'bg-red-50 border-red-300 text-red-800'
|
||||
: 'bg-amber-50 border-amber-300 text-amber-800';
|
||||
return (
|
||||
<span key={idx} className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold 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="p-4 bg-green-50 rounded-xl">
|
||||
<p className="text-sm text-gray-700">
|
||||
<strong className="text-green-600">절기 (節氣):</strong> {solarTermName} 이후 -
|
||||
월주는 절기를 기준으로 {monthBranchName}월입니다.
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
* 사주 월주는 양력 월이 아닌 24절기를 기준으로 계산됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 사주 해석 */}
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-8">
|
||||
{/* 성격 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">👤</span>
|
||||
성격 특징
|
||||
</h3>
|
||||
<div className="space-y-3 text-gray-700">
|
||||
<p className="leading-relaxed">
|
||||
일간이 <strong className="text-indigo-600">{sajuData.day.stem}({sajuData.day.stemKr})</strong>인 사람은
|
||||
{sajuData.day.element === '木' && ' 나무처럼 성장하고 발전하려는 의지가 강합니다. 창의적이고 진취적인 성향을 가지고 있습니다.'}
|
||||
{sajuData.day.element === '火' && ' 불처럼 열정적이고 활동적입니다. 리더십이 있고 사교성이 뛰어납니다.'}
|
||||
{sajuData.day.element === '土' && ' 흙처럼 안정적이고 신뢰감 있습니다. 포용력이 있고 책임감이 강합니다.'}
|
||||
{sajuData.day.element === '金' && ' 금속처럼 강인하고 원칙적입니다. 결단력 있고 의리를 중시합니다.'}
|
||||
{sajuData.day.element === '水' && ' 물처럼 유연하고 지혜롭습니다. 적응력이 뛰어나고 사려 깊습니다.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 운세 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-3xl mr-3">🌟</span>
|
||||
운세 흐름
|
||||
</h3>
|
||||
<div className="space-y-3 text-gray-700">
|
||||
<p className="leading-relaxed">
|
||||
현재 십이운성이 <strong className="text-purple-600">{sajuData.day.fortune}</strong>으로,
|
||||
{sajuData.day.fortune === '장생' && ' 새로운 시작과 성장의 시기입니다.'}
|
||||
{sajuData.day.fortune === '목욕' && ' 정화와 준비의 시기입니다.'}
|
||||
{sajuData.day.fortune === '관대' && ' 사회적으로 인정받는 시기입니다.'}
|
||||
{sajuData.day.fortune === '건록' && ' 안정되고 왕성한 활동의 시기입니다.'}
|
||||
{sajuData.day.fortune === '제왕' && ' 최고의 전성기를 맞이하는 시기입니다.'}
|
||||
{sajuData.day.fortune === '쇠' && ' 조금씩 힘이 약해지는 시기입니다.'}
|
||||
{sajuData.day.fortune === '병' && ' 어려움이 있을 수 있는 시기입니다.'}
|
||||
{sajuData.day.fortune === '사' && ' 끝과 새 시작을 준비하는 시기입니다.'}
|
||||
{sajuData.day.fortune === '묘' && ' 잠시 휴식이 필요한 시기입니다.'}
|
||||
{sajuData.day.fortune === '절' && ' 극복과 인내가 필요한 시기입니다.'}
|
||||
{sajuData.day.fortune === '태' && ' 새로운 기운이 싹트는 시기입니다.'}
|
||||
{sajuData.day.fortune === '양' && ' 성장을 준비하는 시기입니다.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 상세 해석 */}
|
||||
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-3xl shadow-2xl p-8 md:p-12 mb-8">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2 text-center flex items-center justify-center">
|
||||
<span className="text-4xl mr-3">🤖</span>
|
||||
AI 상세 해석
|
||||
</h2>
|
||||
<p className="text-center text-gray-600 mb-8">사주 데이터 분석 기반 맞춤 해석</p>
|
||||
|
||||
{/* 오행 균형 */}
|
||||
<div className="bg-white rounded-2xl p-6 mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
{/* 오행 균형 시각화 */}
|
||||
<div className="mt-8 pt-8 border-t border-gray-100">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center justify-center">
|
||||
<span className="text-2xl mr-2">⚖️</span>
|
||||
오행 균형
|
||||
내 사주의 오행 균형
|
||||
<span className="text-sm font-normal text-gray-500 ml-2">(지장간 가중치 적용)</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
<div className="grid grid-cols-5 gap-3 max-w-2xl mx-auto">
|
||||
{Object.entries(elementScores).map(([element, score]) => (
|
||||
<div key={element} className="text-center">
|
||||
<div className="text-2xl font-bold mb-1">{element}</div>
|
||||
<div className={`text-2xl font-bold mb-1 ${elementColors[element] || ''}`}>{element}</div>
|
||||
<div className="text-sm text-gray-600 mb-2">
|
||||
{element === '木' && '목'}
|
||||
{element === '火' && '화'}
|
||||
{element === '土' && '토'}
|
||||
{element === '金' && '금'}
|
||||
{element === '水' && '수'}
|
||||
{FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR]}
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
({analysis.elementBalance[element as keyof typeof analysis.elementBalance]}점)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mb-1">
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 mb-1">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
element === sajuData.day.element
|
||||
? 'bg-gradient-to-r from-indigo-500 to-purple-500'
|
||||
: 'bg-gray-400'
|
||||
}`}
|
||||
style={{ width: `${score}%` }}
|
||||
className={`h-3 rounded-full transition-all ${element === sajuData.day.element
|
||||
? 'bg-gradient-to-r from-indigo-500 to-purple-500'
|
||||
: 'bg-gray-400'
|
||||
}`}
|
||||
style={{ width: `${Math.max(score, 5)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-xs font-semibold text-gray-700">{score}%</div>
|
||||
@@ -291,138 +309,127 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 장단점 */}
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">💪</span>
|
||||
장점
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.strengths.map((strength, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>{strength}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">⚠️</span>
|
||||
주의할 점
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.weaknesses.map((weakness, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700">
|
||||
<span className="text-orange-600 mr-2">!</span>
|
||||
<span>{weakness}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 직업, 대인관계, 재물, 건강 */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 직업 */}
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">💼</span>
|
||||
직업 운세
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.career.map((item, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700 text-sm">
|
||||
<span className="text-blue-600 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 대인관계 */}
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">👥</span>
|
||||
대인 관계
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.relationships.map((item, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700 text-sm">
|
||||
<span className="text-pink-600 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 재물 */}
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">💰</span>
|
||||
재물 운세
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.wealth.map((item, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700 text-sm">
|
||||
<span className="text-yellow-600 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 건강 */}
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">🏥</span>
|
||||
건강 운세
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{interpretation.health.map((item, i) => (
|
||||
<li key={i} className="flex items-start text-gray-700 text-sm">
|
||||
<span className="text-red-600 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조언 */}
|
||||
<div className="bg-white rounded-2xl p-6 mt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="text-2xl mr-2">💡</span>
|
||||
AI의 조언
|
||||
{/* 신강/신약 + 용신 + 신살 카드 */}
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
||||
{/* 신강/신약 + 용신 카드 */}
|
||||
<div className="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="mr-2">⚡</span> 일간 세력 분석
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{interpretation.advice.map((item, i) => (
|
||||
<div key={i} className="flex items-start text-gray-700 text-sm bg-indigo-50 p-3 rounded-lg">
|
||||
<span className="text-indigo-600 mr-2">→</span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className={`inline-block px-4 py-2 rounded-xl text-lg font-bold ${
|
||||
analysis.dayMasterStrength.result === '신강'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: analysis.dayMasterStrength.result === '신약'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{analysis.dayMasterStrength.result}
|
||||
</span>
|
||||
<span className="text-gray-500 text-sm">점수: {analysis.dayMasterStrength.score}</span>
|
||||
</div>
|
||||
<ul className="space-y-1.5 text-sm text-gray-600 mb-6">
|
||||
{analysis.dayMasterStrength.reasons.map((r, i) => (
|
||||
<li key={i} className="flex items-start">
|
||||
<span className="text-indigo-400 mr-2 mt-0.5">-</span>
|
||||
<span>{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<h4 className="font-bold text-gray-800 mb-3">용신 / 희신 / 기신</h4>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
<span className={`px-3 py-1.5 rounded-lg text-sm font-bold border ${elementBgColors[analysis.yongShin.yongShin] || 'bg-gray-100'}`}>
|
||||
용신: {analysis.yongShin.yongShinKr}({analysis.yongShin.yongShin})
|
||||
</span>
|
||||
<span className={`px-3 py-1.5 rounded-lg text-sm font-bold border ${elementBgColors[analysis.yongShin.heeShin] || 'bg-gray-100'}`}>
|
||||
희신: {analysis.yongShin.heeShinKr}({analysis.yongShin.heeShin})
|
||||
</span>
|
||||
<span className="px-3 py-1.5 rounded-lg text-sm font-bold bg-gray-200 border border-gray-400 text-gray-700">
|
||||
기신: {analysis.yongShin.giShinKr}({analysis.yongShin.giShin})
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{analysis.yongShin.explanation}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-purple-100 rounded-xl">
|
||||
<p className="text-xs text-gray-700 text-center">
|
||||
💡 AI 해석은 전통 사주 이론을 기반으로 생성되었습니다. 참고용으로 활용하시고,
|
||||
중요한 결정은 전문가와 상담하시기 바랍니다.
|
||||
</p>
|
||||
{/* 신살 + 공망 카드 */}
|
||||
<div className="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<span className="mr-2">🌟</span> 신살 (神煞)
|
||||
</h3>
|
||||
{analysis.shinsal.length > 0 ? (
|
||||
<div className="space-y-3 mb-6">
|
||||
{analysis.shinsal.map((s, i) => (
|
||||
<div key={i} className="flex items-start gap-3 p-3 rounded-xl bg-gray-50">
|
||||
<span className="inline-block px-2 py-1 bg-indigo-100 text-indigo-700 rounded-lg text-xs font-bold whitespace-nowrap">
|
||||
{s.name}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800">
|
||||
{s.pillar} {s.branchKr}({s.branch})
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{s.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm mb-6">특별한 신살이 발견되지 않았습니다.</p>
|
||||
)}
|
||||
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<h4 className="font-bold text-gray-800 mb-2 flex items-center">
|
||||
<span className="mr-2">🕳️</span> 공망 (空亡)
|
||||
</h4>
|
||||
<div className="flex gap-2 mb-2">
|
||||
{analysis.gongmang.branchesKr.map((bk, i) => (
|
||||
<span key={i} className="px-3 py-1.5 bg-gray-800 text-white rounded-lg text-sm font-bold">
|
||||
{bk}({analysis.gongmang.branches[i]})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">{analysis.gongmang.description}</p>
|
||||
</div>
|
||||
|
||||
{/* 세운 정보 */}
|
||||
<div className="border-t border-gray-100 pt-4 mt-4">
|
||||
<h4 className="font-bold text-gray-800 mb-2 flex items-center">
|
||||
<span className="mr-2">📅</span> {analysis.seun.year}년 세운
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-3 py-1.5 rounded-lg text-sm font-bold border ${elementBgColors[analysis.seun.element] || 'bg-gray-100'}`}>
|
||||
{analysis.seun.stemKr}{analysis.seun.branchKr} ({analysis.seun.stem}{analysis.seun.branch})
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{analysis.seun.elementKr}({analysis.seun.element}) 기운</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-1 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 상세 해석 */}
|
||||
<AiInterpretationSection sajuData={sajuData} currentDaeun={currentDaeun} daeunList={daeunList} />
|
||||
|
||||
{/* 대운 (大運) */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl p-8 md:p-12 mb-8">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center">
|
||||
🔄 대운 (大運) - 10년 주기 운세
|
||||
대운 (大運) - 10년 주기 운세
|
||||
</h2>
|
||||
|
||||
{/* 현재 대운 */}
|
||||
{currentDaeun && (
|
||||
<div className="bg-gradient-to-r from-indigo-500 to-purple-500 rounded-2xl p-6 mb-8 text-white">
|
||||
<h3 className="text-2xl font-bold mb-4 text-center">현재 대운</h3>
|
||||
@@ -443,7 +450,6 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 전체 대운 목록 */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{daeunList.map((daeun, index) => {
|
||||
const isCurrent = currentDaeun &&
|
||||
@@ -453,11 +459,10 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`rounded-xl p-4 border-2 transition ${
|
||||
isCurrent
|
||||
? 'bg-indigo-50 border-indigo-400'
|
||||
: 'bg-gray-50 border-gray-200 hover:border-indigo-300'
|
||||
}`}
|
||||
className={`rounded-xl p-4 border-2 transition ${isCurrent
|
||||
? 'bg-indigo-50 border-indigo-400'
|
||||
: 'bg-gray-50 border-gray-200 hover:border-indigo-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">
|
||||
@@ -484,18 +489,6 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-indigo-50 rounded-xl">
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
<strong className="text-indigo-600">대운(大運):</strong> 10년 단위로 변화하는 큰 운의 흐름입니다.
|
||||
각 대운마다 삶의 방향과 환경이 달라질 수 있으므로, 현재 대운의 특성을 이해하고 활용하는 것이 중요합니다.
|
||||
</p>
|
||||
{daeunList.length > 0 && (
|
||||
<p className="text-xs text-gray-600">
|
||||
* 대운은 생일부터 다음 절기까지의 일수를 기준으로 {daeunList[0].age}세부터 시작됩니다. (3일 = 1세)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 추가 기능 버튼 */}
|
||||
@@ -535,7 +528,7 @@ export default async function ResultPage({ searchParams }: PageProps) {
|
||||
<footer className="bg-gray-900 text-white py-12 px-4 mt-20">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<div className="text-2xl font-bold mb-4 bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
||||
🔮 사주보기
|
||||
사주보기
|
||||
</div>
|
||||
<p className="text-gray-400 mb-6">
|
||||
쟁승메이드가 제공하는 무료 사주 서비스
|
||||
|
||||
33
app/result/saved/[id]/SavedInterpretation.tsx
Normal file
33
app/result/saved/[id]/SavedInterpretation.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { AccordionItem, parseSections, SECTION_ICONS } from '@/components/AccordionItem';
|
||||
|
||||
export default function SavedInterpretation({ interpretation }: { interpretation: string }) {
|
||||
const sections = parseSections(interpretation);
|
||||
|
||||
if (sections.length > 0) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{sections.map((section, idx) => (
|
||||
<AccordionItem
|
||||
key={idx}
|
||||
title={section.title}
|
||||
content={section.content}
|
||||
icon={SECTION_ICONS[idx] || '📌'}
|
||||
defaultOpen={idx === 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="prose prose-lg max-w-none prose-indigo">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{interpretation}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
78
app/result/saved/[id]/page.tsx
Normal file
78
app/result/saved/[id]/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import SavedInterpretation from './SavedInterpretation';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SavedResultPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const supabase = await createClient();
|
||||
|
||||
// 인증된 사용자의 쿠키를 사용하여 RLS 통과
|
||||
const { data: record, error } = await supabase
|
||||
.from('saju_records')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error || !record) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-4">
|
||||
<h1 className="text-2xl font-bold text-red-600 mb-4">기록을 찾을 수 없습니다.</h1>
|
||||
<p className="text-gray-500 mb-4">로그인이 필요하거나 삭제된 기록일 수 있습니다.</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/login" className="text-indigo-600 hover:underline">로그인</Link>
|
||||
<Link href="/mypage" className="text-indigo-600 hover:underline">마이페이지</Link>
|
||||
<Link href="/" className="text-indigo-600 hover:underline">홈으로</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { saju_data, interpretation } = record;
|
||||
const date = new Date(record.created_at).toLocaleDateString();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<span className="inline-block px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm font-semibold mb-4">
|
||||
{date} 저장됨
|
||||
</span>
|
||||
<h1 className="text-3xl font-bold text-gray-900">저장된 사주 결과</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
{saju_data.birthDate.year}년 {saju_data.birthDate.month}월 {saju_data.birthDate.day}일생
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl shadow-xl p-6 md:p-10 mb-8">
|
||||
<SavedInterpretation interpretation={interpretation} />
|
||||
</div>
|
||||
|
||||
<div className="text-center flex flex-wrap justify-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="px-6 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition font-medium"
|
||||
>
|
||||
홈으로
|
||||
</Link>
|
||||
<Link
|
||||
href="/mypage"
|
||||
className="px-6 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition font-medium"
|
||||
>
|
||||
목록으로
|
||||
</Link>
|
||||
<Link
|
||||
href="/saju"
|
||||
className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition font-medium"
|
||||
>
|
||||
새로운 사주 보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
app/saju/page.tsx
Normal file
71
app/saju/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import SajuForm from '../components/SajuForm';
|
||||
import UserMenu from '@/components/UserMenu';
|
||||
|
||||
export default function SajuPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50">
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link href="/" className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
🔮 사주포춘
|
||||
</Link>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="hidden md:flex space-x-6 mr-4">
|
||||
<Link href="/saju" className="text-indigo-600 font-bold">사주팔자</Link>
|
||||
<Link href="/compatibility" className="text-gray-700 hover:text-indigo-600 transition font-medium">궁합</Link>
|
||||
<Link href="/tojeong" className="text-gray-700 hover:text-indigo-600 transition font-medium">토정비결</Link>
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="pt-20 pb-32 px-4">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="inline-block mb-6 px-6 py-2 bg-white/50 backdrop-blur-sm rounded-full text-indigo-700 font-semibold border border-indigo-200">
|
||||
무료로 내 사주를 확인해보세요
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-gray-900 mb-6 leading-tight">
|
||||
나의 <span className="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">사주팔자</span>를<br />
|
||||
확인하세요
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 mb-12 max-w-2xl mx-auto">
|
||||
생년월일시를 입력하면 무료로 사주팔자, 운세, 궁합을 확인할 수 있습니다.
|
||||
쉽고 빠르게 나의 운명을 알아보세요.
|
||||
</p>
|
||||
|
||||
{/* Main Input Card */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl p-8 md:p-12 max-w-2xl mx-auto">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-8">생년월일시 입력</h2>
|
||||
|
||||
<SajuForm />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white py-12 px-4">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<div className="text-2xl font-bold mb-4 bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
||||
🔮 사주포춘
|
||||
</div>
|
||||
<p className="text-gray-400 mb-6">
|
||||
쟁승메이드가 제공하는 사주 서비스
|
||||
</p>
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>문의: bgg8988@gmail.com | <a href="https://jaengseung-made.com" target="_blank" rel="noopener noreferrer" className="hover:text-indigo-400">쟁승메이드</a></p>
|
||||
<p className="mt-2">© 2025 쟁승메이드. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
app/tojeong/result/TojeongDetailUnlock.tsx
Normal file
134
app/tojeong/result/TojeongDetailUnlock.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TokenPurchaseModal from '@/components/TokenPurchaseModal';
|
||||
import { ensureProfile } from '@/lib/ensure-profile';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TojeongDetailUnlock({ children }: Props) {
|
||||
const [isUnlocked, setIsUnlocked] = useState(false);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [credits, setCredits] = useState(0);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
setUser(user);
|
||||
const currentCredits = await ensureProfile(supabase, user);
|
||||
setCredits(currentCredits);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleUnlock = async () => {
|
||||
if (!user) {
|
||||
if (confirm('로그인이 필요합니다. 로그인 페이지로 이동하시겠습니까?')) {
|
||||
router.push('/login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (credits < 1) {
|
||||
setShowModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({ credits: credits - 1 })
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) {
|
||||
alert('토큰 차감에 실패했습니다. 다시 시도해주세요.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCredits(credits - 1);
|
||||
setIsUnlocked(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handlePurchaseComplete = async () => {
|
||||
setShowModal(false);
|
||||
if (user) {
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('credits')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
if (profile) setCredits(profile.credits || 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (isUnlocked) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative mb-8">
|
||||
{/* 블러 처리된 미리보기 */}
|
||||
<div className="filter blur-sm select-none pointer-events-none opacity-60">
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
|
||||
<span className="text-3xl mr-3">💡</span>
|
||||
한 해를 위한 조언
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-4/6"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 잠금 오버레이 */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/40 backdrop-blur-[2px] rounded-2xl">
|
||||
<div className="text-center p-8 bg-white/95 backdrop-blur-md rounded-2xl shadow-xl border border-amber-100 max-w-sm mx-auto">
|
||||
<div className="text-4xl mb-4">🔐</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">상세 조언 잠금해제</h3>
|
||||
<p className="text-gray-600 mb-2">
|
||||
토정비결의 상세한 조언과 해석을 확인하세요.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
토큰 1개 사용 | 보유: <span className="font-bold text-amber-600">{credits}개</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 text-white font-bold py-3 px-6 rounded-xl hover:shadow-lg hover:scale-105 transition transform disabled:opacity-50"
|
||||
>
|
||||
{loading ? '처리 중...' : credits >= 1 ? '토큰 1개로 잠금해제' : '토큰 충전하기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TokenPurchaseModal
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onPurchaseComplete={handlePurchaseComplete}
|
||||
user={user}
|
||||
supabase={supabase}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link';
|
||||
import { calculateSaju } from '@/lib/saju-calculator';
|
||||
import PDFButton from '../../components/PDFButton';
|
||||
import ShareButtons from '../../components/ShareButtons';
|
||||
import TojeongDetailUnlock from './TojeongDetailUnlock';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -258,31 +259,33 @@ export default async function TojeongResultPage({ searchParams }: PageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조언 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8 mb-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
|
||||
<span className="text-3xl mr-3">💡</span>
|
||||
{targetYear}년 한 해를 위한 조언
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>운이 좋은 달에는 적극적으로 새로운 일을 시작하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>운이 낮은 달에는 무리하지 말고 현상 유지에 집중하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>매사에 감사하는 마음을 가지고 긍정적으로 생활하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>토정비결은 참고사항이며, 자신의 노력이 가장 중요합니다.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* 조언 - 토큰 잠금 영역 */}
|
||||
<TojeongDetailUnlock>
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8 mb-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
|
||||
<span className="text-3xl mr-3">💡</span>
|
||||
{targetYear}년 한 해를 위한 조언
|
||||
</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>운이 좋은 달에는 적극적으로 새로운 일을 시작하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>운이 낮은 달에는 무리하지 말고 현상 유지에 집중하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>매사에 감사하는 마음을 가지고 긍정적으로 생활하세요.</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-600 mr-2">•</span>
|
||||
<span>토정비결은 참고사항이며, 자신의 노력이 가장 중요합니다.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</TojeongDetailUnlock>
|
||||
|
||||
{/* 다른 메뉴 */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
Reference in New Issue
Block a user