From 7f4fb8027aa30f5369e195ba6a4ff5da2f070257 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 19 Mar 2026 07:58:38 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=B9=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=91=20=EC=86=8C=EA=B0=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1=20&=20=EC=82=AC=EC=A3=BC=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/saju/analyze/route.ts | 145 +-- app/api/subscription/route.ts | 8 +- app/components/Sidebar.tsx | 70 +- app/saju/result/SajuAISection.tsx | 2 +- app/saju/result/SajuLottoSection.tsx | 351 +++++++ app/saju/result/page.tsx | 53 +- app/services/lotto/recommend/page.tsx | 921 ++++++++++++------ app/services/website/page.tsx | 443 +++++++++ app/services/website/samples/bakery/page.tsx | 325 ++++++ .../website/samples/corporate/page.tsx | 292 ++++++ .../website/samples/dashboard/page.tsx | 339 +++++++ app/services/website/samples/game/page.tsx | 437 +++++++++ .../website/samples/portfolio/page.tsx | 345 +++++++ package-lock.json | 49 + package.json | 1 + 15 files changed, 3397 insertions(+), 384 deletions(-) create mode 100644 app/saju/result/SajuLottoSection.tsx create mode 100644 app/services/website/page.tsx create mode 100644 app/services/website/samples/bakery/page.tsx create mode 100644 app/services/website/samples/corporate/page.tsx create mode 100644 app/services/website/samples/dashboard/page.tsx create mode 100644 app/services/website/samples/game/page.tsx create mode 100644 app/services/website/samples/portfolio/page.tsx diff --git a/app/api/saju/analyze/route.ts b/app/api/saju/analyze/route.ts index 7f462e1..f05fa3c 100644 --- a/app/api/saju/analyze/route.ts +++ b/app/api/saju/analyze/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import OpenAI from 'openai'; +import Anthropic from '@anthropic-ai/sdk'; import { createSajuPrompt } from '@/lib/saju-ai-prompt'; import { performFullAnalysis } from '@/lib/ai-interpretation'; @@ -8,7 +8,7 @@ export const runtime = 'nodejs'; const MOCK_INTERPRETATION = ` ## 1. 일간 분석과 타고난 기질 -(API 키 문제 또는 할당량 초과로 인해 예시 데이터를 보여드립니다.) +(AI 해석 서비스를 이용하려면 API 설정이 필요합니다. 아래는 예시 데이터입니다.) 귀하는 **갑목(甲木)** 일간으로 태어나, 마치 곧게 뻗은 소나무와 같은 기상을 지니고 있다. 리더십이 강하고 추진력이 뛰어나며, 한번 마음먹은 일은 끝까지 해내는 뚝심이 있다. ## 2. 오행 균형과 용신 기반 개운법 @@ -45,77 +45,78 @@ const MOCK_INTERPRETATION = ` "서두르지 않아도 봄은 온다." 조급해하지 말고 때를 기다리는 지혜가 필요하다. `; -// 사용 가능한 모델 우선순위 (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, engineData } = await request.json(); + + // 종합 분석 수행 + let analysis; try { - const { saju, daeun, daeunList, gender, engineData } = 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 || [], engineData); - - // 모델 폴백: gpt-4o 실패 시 gpt-4o-mini로 재시도 - let interpretation: string | null = null; - let usedModel = ''; - - for (const model of MODELS) { - try { - console.log(`Generating saju 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); - 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 saju interpretation:', error.message || error); - - return NextResponse.json( - { error: error.message || 'Failed to generate interpretation' }, - { status: 500 } - ); + analysis = performFullAnalysis(saju); + } catch (analysisError: any) { + console.error('Analysis calculation error:', analysisError.message); + return NextResponse.json( + { error: '사주 분석 계산 중 오류가 발생했습니다: ' + analysisError.message }, + { status: 500 } + ); } + + if (!process.env.ANTHROPIC_API_KEY) { + console.warn('Anthropic API Key is missing — returning mock data'); + return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis }); + } + + const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + const prompt = createSajuPrompt(saju, daeun, gender, analysis, daeunList || [], engineData); + + console.log('Generating saju analysis with claude-sonnet-4-6...'); + + let interpretation: string | null = null; + + try { + const message = await client.messages.create({ + model: 'claude-sonnet-4-6', + max_tokens: 8192, + temperature: 0.75, + messages: [{ role: 'user', content: prompt }], + }); + + const block = message.content[0]; + if (block.type === 'text') { + interpretation = block.text; + } + console.log('Successfully generated saju analysis with claude-sonnet-4-6'); + } catch (claudeError: any) { + // claude-sonnet-4-6 실패 시 claude-haiku-4-5 폴백 + console.warn('claude-sonnet-4-6 failed:', claudeError.message, '— trying haiku fallback'); + try { + const fallback = await client.messages.create({ + model: 'claude-haiku-4-5-20251001', + max_tokens: 4096, + temperature: 0.75, + messages: [{ role: 'user', content: prompt }], + }); + const block = fallback.content[0]; + if (block.type === 'text') { + interpretation = block.text; + } + console.log('Fallback to claude-haiku-4-5 succeeded'); + } catch (haikusError: any) { + console.error('Both Claude models failed:', haikusError.message); + return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis }); + } + } + + if (!interpretation) { + return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis }); + } + + return NextResponse.json({ interpretation, analysis }); + } catch (error: any) { + console.error('Error generating saju interpretation:', error.message || error); + return NextResponse.json( + { error: error.message || 'Failed to generate interpretation' }, + { status: 500 } + ); + } } diff --git a/app/api/subscription/route.ts b/app/api/subscription/route.ts index 3fbce5c..c95a89f 100644 --- a/app/api/subscription/route.ts +++ b/app/api/subscription/route.ts @@ -1,9 +1,11 @@ import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; +import { createAdminClient } from '@/lib/supabase/admin'; /** * GET /api/subscription * 내 활성/만료 구독 목록 조회 + * - auth 검증은 anon client, DB 조회는 admin client (RLS 우회) */ export async function GET() { const supabase = await createClient(); @@ -12,7 +14,9 @@ export async function GET() { return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 }); } - const { data, error } = await supabase + // admin client로 RLS 우회 (subscriptions 테이블 SELECT 정책 없을 때도 동작) + const admin = createAdminClient(); + const { data, error } = await admin .from('subscriptions') .select('id, product_id, status, auto_renew, started_at, expires_at, cancelled_at') .eq('user_id', user.id) @@ -20,7 +24,7 @@ export async function GET() { .limit(20); if (error) { - return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + return NextResponse.json({ error: 'DB_ERROR', detail: error.message }, { status: 500 }); } return NextResponse.json({ ok: true, subscriptions: data ?? [] }); diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index d5d80ea..d203bda 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -17,37 +17,16 @@ const navItems = [ desc: '대시보드 홈', }, { - href: '/services/lotto', + href: '/services/website', icon: ( - + ), - label: '로또 번호 추천', - desc: '빅데이터 분석', - badge: 'HOT', - }, - { - href: '/services/stock', - icon: ( - - - - ), - label: '주식 자동 매매', - desc: '텔레그램 연동', + label: '홈페이지 제작', + desc: '외주 웹 개발', badge: 'NEW', }, - { - href: '/services/prompt', - icon: ( - - - - ), - label: '프롬프트 엔지니어링', - desc: 'AI 최적화', - }, { href: '/services/automation', icon: ( @@ -59,6 +38,37 @@ const navItems = [ label: '업무 자동화', desc: 'RPA 개발', }, + { + href: '/services/prompt', + icon: ( + + + + ), + label: '프롬프트 엔지니어링', + desc: 'AI 최적화', + }, + { + href: '/services/stock', + icon: ( + + + + ), + label: '주식 자동 매매', + desc: '텔레그램 연동', + }, + { + href: '/services/lotto', + icon: ( + + + + ), + label: '로또 번호 추천', + desc: '빅데이터 분석', + badge: 'HOT', + }, { href: '/saju', icon: ( @@ -70,16 +80,6 @@ const navItems = [ desc: '사주팔자 + AI 해석', badge: 'NEW', }, - { - href: '/freelance', - icon: ( - - - - ), - label: '외주 개발', - desc: '맞춤형 솔루션', - }, ]; interface SidebarProps { diff --git a/app/saju/result/SajuAISection.tsx b/app/saju/result/SajuAISection.tsx index b1474cb..15ca72a 100644 --- a/app/saju/result/SajuAISection.tsx +++ b/app/saju/result/SajuAISection.tsx @@ -248,7 +248,7 @@ export default function SajuAISection({

AI 상세 해석 (12개 항목)

성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등
- GPT-4o가 생성하는 맞춤형 사주 해석을 받아보세요. + Claude AI가 생성하는 맞춤형 사주 해석을 받아보세요.

{/* 미리보기 섹션 목록 */} diff --git a/app/saju/result/SajuLottoSection.tsx b/app/saju/result/SajuLottoSection.tsx new file mode 100644 index 0000000..6a556fc --- /dev/null +++ b/app/saju/result/SajuLottoSection.tsx @@ -0,0 +1,351 @@ +'use client'; + +import { useMemo } from 'react'; +import Link from 'next/link'; + +// 오행 기반 로또 번호 매핑 (하도낙서 원리) +// 水:1,6 / 火:2,7 / 木:3,8 / 金:4,9 / 土:5,10 +const ELEMENT_NUMBERS: Record = { + '水': [1, 6, 11, 16, 21, 26, 31, 36, 41], + '火': [2, 7, 12, 17, 22, 27, 32, 37, 42], + '木': [3, 8, 13, 18, 23, 28, 33, 38, 43], + '金': [4, 9, 14, 19, 24, 29, 34, 39, 44], + '土': [5, 10, 15, 20, 25, 30, 35, 40, 45], +}; + +const ELEMENT_KR: Record = { + '水': '수', '火': '화', '木': '목', '金': '금', '土': '토', +}; + +const ELEMENT_COLOR: Record = { + '水': { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-300', ball: '#3b82f6' }, + '火': { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-300', ball: '#ef4444' }, + '木': { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-300', ball: '#22c55e' }, + '金': { bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-300', ball: '#f59e0b' }, + '土': { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-300', ball: '#eab308' }, +}; + +// 오행별 행운 설명 +const ELEMENT_LUCK_DESC: Record = { + '水': '흐르는 물처럼 지혜와 직관이 넘치는 수(水) 기운이 당신의 행운을 이끕니다. 1·6 계열의 숫자들이 당신과 공명합니다.', + '火': '활활 타오르는 불처럼 열정과 표현력이 폭발하는 화(火) 기운이 행운의 열쇠입니다. 2·7 계열의 숫자들에서 기운을 찾으세요.', + '木': '하늘을 향해 뻗는 나무처럼 성장과 창의성을 상징하는 목(木) 기운이 길을 열어줍니다. 3·8 계열의 숫자들이 공명합니다.', + '金': '단단하고 순수한 금속처럼 결단력과 정의를 상징하는 금(金) 기운이 행운을 부릅니다. 4·9 계열의 숫자들이 당신과 함께합니다.', + '土': '만물을 품는 대지처럼 안정과 신뢰를 상징하는 토(土) 기운이 당신을 지켜줍니다. 5·10 계열의 숫자들에 행운이 깃들어 있습니다.', +}; + +// 사주 기반 시드로 결정론적 숫자 선택 (매번 같은 결과) +function seededRandom(seed: number): () => number { + let s = seed; + return () => { + s = (s * 1664525 + 1013904223) & 0xffffffff; + return (s >>> 0) / 0xffffffff; + }; +} + +function generateSajuLottoNumbers( + yongShin: string, + heeShin: string, + dayBranch: string, + yearNum: number, + monthNum: number, + dayNum: number +): { numbers: number[]; yongShinNums: number[]; heeShinNums: number[] } { + const seed = yearNum * 10000 + monthNum * 100 + dayNum; + const rand = seededRandom(seed); + + const yongPool = ELEMENT_NUMBERS[yongShin] ?? ELEMENT_NUMBERS['水']; + const heePool = ELEMENT_NUMBERS[heeShin] ?? ELEMENT_NUMBERS['木']; + + // 용신 기반 3개 선택 + const shuffledYong = [...yongPool].sort(() => rand() - 0.5); + const yongPick = shuffledYong.slice(0, 3); + + // 희신 기반 2개 선택 + const shuffledHee = [...heePool].sort(() => rand() - 0.5); + const heePick = shuffledHee.filter(n => !yongPick.includes(n)).slice(0, 2); + + // 지지 오행에서 보조 번호 1개 + const BRANCH_ELEMENT: Record = { + '子': '水', '亥': '水', '寅': '木', '卯': '木', '巳': '火', '午': '火', + '申': '金', '酉': '金', '丑': '土', '辰': '土', '未': '土', '戌': '土', + }; + const branchElem = BRANCH_ELEMENT[dayBranch] ?? yongShin; + const branchPool = ELEMENT_NUMBERS[branchElem] ?? []; + const bonusPool = branchPool.filter(n => !yongPick.includes(n) && !heePick.includes(n)); + const shuffledBonus = [...bonusPool].sort(() => rand() - 0.5); + const bonusPick = shuffledBonus.length > 0 ? [shuffledBonus[0]] : []; + + const combined = [...new Set([...yongPick, ...heePick, ...bonusPick])]; + // 6개 채우기 (부족하면 랜덤으로 추가) + while (combined.length < 6) { + const n = Math.floor(rand() * 45) + 1; + if (!combined.includes(n)) combined.push(n); + } + + const numbers = combined.slice(0, 6).sort((a, b) => a - b); + return { numbers, yongShinNums: yongPick.sort((a, b) => a - b), heeShinNums: heePick.sort((a, b) => a - b) }; +} + +// 로또 볼 컴포넌트 +function LottoBall({ num, color = '#1d4ed8', size = 44 }: { num: number; color?: string; size?: number }) { + return ( +
+ {num} +
+ ); +} + +// 오행별 볼 색상 +function getElementColor(num: number): string { + const mod = num % 10; + if (mod === 1 || mod === 6) return '#3b82f6'; // 水 + if (mod === 2 || mod === 7) return '#ef4444'; // 火 + if (mod === 3 || mod === 8) return '#22c55e'; // 木 + if (mod === 4 || mod === 9) return '#f59e0b'; // 金 + return '#eab308'; // 土 (0, 5) +} + +interface Props { + yongShin: string; // 용신 오행 (예: '水') + yongShinKr: string; // 용신 한글 (예: '수') + heeShin: string; // 희신 오행 + heeShinKr: string; // 희신 한글 + dayBranch: string; // 일지 (예: '子') + dayStemKr: string; // 일간 한글 (예: '갑') + currentDaeun: { + stemKr: string; + branchKr: string; + startYear: number; + endYear: number; + age: number; + } | null; + yearNum: number; + monthNum: number; + dayNum: number; + hasLottoSubscription: boolean; // 로또 구독 여부 +} + +export default function SajuLottoSection({ + yongShin, yongShinKr, heeShin, heeShinKr, + dayBranch, dayStemKr, + currentDaeun, + yearNum, monthNum, dayNum, + hasLottoSubscription, +}: Props) { + const { numbers, yongShinNums, heeShinNums } = useMemo( + () => generateSajuLottoNumbers(yongShin, heeShin, dayBranch, yearNum, monthNum, dayNum), + [yongShin, heeShin, dayBranch, yearNum, monthNum, dayNum] + ); + + const elemColor = ELEMENT_COLOR[yongShin] ?? ELEMENT_COLOR['水']; + const currentYear = new Date().getFullYear(); + + return ( +
+ {/* 헤더 */} +
+
+
+ 🎱 +
+
+

사주 기반 로또 번호 추천

+

+ 당신의 용신({yongShinKr}·{yongShin}) 오행으로 추출한 행운 번호 +

+
+ + 사주 연동 + +
+
+ +
+ + {/* 용신 설명 배너 */} +
+
+
+ {yongShin} +
+
+
+ 용신 오행: {yongShinKr}({yongShin}) · 희신: {heeShinKr}({heeShin}) +
+

+ {ELEMENT_LUCK_DESC[yongShin]} +

+
+
+
+ + {/* 추천 번호 */} +
+
+

이번 주 추천 번호

+ {currentYear}년 기준 +
+ + {/* 메인 볼 */} +
+ {numbers.map((n) => ( + + ))} +
+ + {/* 용신/희신 구분 안내 */} +
+
+
+ 용신({yongShinKr}) 핵심 번호 +
+
+ {yongShinNums.map(n => ( + + ))} +
+
+
+
+ 희신({heeShinKr}) 보조 번호 +
+
+ {heeShinNums.map(n => ( + + ))} +
+
+
+
+ + {/* 기본 사주 해석 내러티브 */} +
+
+ +

왜 이 번호인가요?

+
+
+

+ {dayStemKr}(일간) 일간인 당신의 사주에서 + 용신은 {yongShin}({yongShinKr}) 오행입니다. + 동양 명리학의 하도낙서(河圖洛書) 원리에 따르면,{' '} + {yongShin === '水' ? '1과 6' : yongShin === '火' ? '2와 7' : yongShin === '木' ? '3과 8' : yongShin === '金' ? '4와 9' : '5와 10'}이 + {yongShinKr}(水)의 수리이며, 이 기운을 담은 번호들이 당신에게 에너지적으로 공명합니다. +

+

+ 희신 {heeShin}({heeShinKr}) 오행의 번호를 + 보조로 더하여 균형을 잡았고, 일지 ({dayBranch})의 기운까지 반영한 + 6개 번호를 완성했습니다. +

+
+
+ + {/* 로또 구독 미가입 → 대운 연동 프리미엄 홍보 */} + {!hasLottoSubscription ? ( +
+
+
+
+ 🔮 + 로또 구독 시 더 정확한 추천 +
+ {currentDaeun && ( +

+ 현재 {currentDaeun.stemKr}{currentDaeun.branchKr} 대운 + ({currentDaeun.startYear}~{currentDaeun.endYear}년)을 지나고 있습니다. + 로또 플랜을 구독하면 대운의 오행 흐름과 + 사주 원국을 교차 분석하여 매주 최적화된 번호를 받을 수 있어요. +

+ )} +
+ {[ + { icon: '📊', text: '대운 × 사주 교차 분석' }, + { icon: '🔄', text: '매주 업데이트 번호' }, + { icon: '🎯', text: '빅데이터 Monte Carlo 시뮬레이션' }, + { icon: '📈', text: '핫넘버 / 콜드넘버 통계' }, + ].map((item, i) => ( +
+ {item.icon} + {item.text} +
+ ))} +
+ + 로또 번호 추천 서비스 구독하기 → + +
+
+ ) : ( + /* 로또 구독 가입자 → 대운 교차 분석 심화 */ +
+
+
+ + + +
+ 구독자 전용 · 대운 교차 분석 +
+ + {currentDaeun ? ( +
+
+
현재 대운과 사주의 만남
+

+ {currentDaeun.stemKr}{currentDaeun.branchKr} 대운 + ({currentDaeun.startYear}~{currentDaeun.endYear}년, {currentDaeun.age}~{currentDaeun.age + 9}세)이 + 용신 {yongShin}({yongShinKr}) 오행과 + {currentDaeun.stemKr.includes(yongShinKr) || currentDaeun.branchKr.includes(yongShinKr) + ? 강하게 공명합니다. 지금이 행운의 절정기! + : ' 상호작용하고 있습니다. 용신 번호를 중심으로 추천합니다.' + } +

+
+
+
대운 연동 보너스 인사이트
+

+ {currentYear}년 세운과 {currentDaeun.stemKr}{currentDaeun.branchKr} 대운이 겹치는 + 이 시기, 사주 원국의 용신 에너지가 가장 활성화됩니다. + 추천 번호 중 용신 번호 3개는 반드시 포함하고, + 나머지는 매주 로또 서비스의 최신 분석을 참고하세요. +

+
+ + 로또 번호 추천 서비스 바로가기 → + +
+ ) : ( +

+ 대운 정보를 불러오는 중입니다. 정확한 생년월일을 입력하면 더 정밀한 분석이 가능합니다. +

+ )} +
+ )} + + {/* 하단 면책 */} +

+ 본 번호는 사주 명리학의 용신/오행 원리를 기반으로 한 참고용 추천입니다.
+ 로또 당첨을 보장하지 않으며, 투자 손실에 대한 책임은 본인에게 있습니다. +

+
+
+ ); +} diff --git a/app/saju/result/page.tsx b/app/saju/result/page.tsx index c2e15c0..48cdad8 100644 --- a/app/saju/result/page.tsx +++ b/app/saju/result/page.tsx @@ -6,6 +6,7 @@ import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation'; import { createClient } from '@/lib/supabase/server'; import SajuAISection from './SajuAISection'; +import SajuLottoSection from './SajuLottoSection'; interface PageProps { searchParams: Promise<{ @@ -116,13 +117,15 @@ export default async function SajuResultPage({ searchParams }: PageProps) { const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum); const solarTermName = getSolarTermName(solarTermIndex); - // ── 결제 여부 + 저장된 AI 해석 ──────────────────────────────────────── + // ── 결제 여부 + 저장된 AI 해석 + 로또 구독 확인 ───────────────────── let hasPaid = false; let savedInterpretation: string | null = null; + let hasLottoSubscription = false; try { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (user) { + // 사주 결제 확인 (anon client — 본인 orders는 RLS 허용 가정) const { data: order } = await supabase .from('orders').select('id') .eq('user_id', user.id).eq('product_id', 'saju_detail').eq('status', 'paid') @@ -138,6 +141,31 @@ export default async function SajuResultPage({ searchParams }: PageProps) { .contains('saju_data', birthKey).maybeSingle(); savedInterpretation = record?.interpretation ?? null; } + + // 로또 구독 확인 — subscriptions 테이블 (세션 클라이언트로 RLS select_own 통과) + const { data: lottoSub } = await supabase + .from('subscriptions') + .select('id') + .eq('user_id', user.id) + .eq('status', 'active') + .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) + .maybeSingle(); + hasLottoSubscription = !!lottoSub; + + // subscriptions에서 못 찾으면 orders 테이블로 폴백 (구독 마이그레이션 전 데이터) + if (!hasLottoSubscription) { + const now = new Date().toISOString(); + const thirtyOneDaysAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString(); + const { data: lottoOrder } = await supabase + .from('orders') + .select('id, created_at') + .eq('user_id', user.id) + .eq('status', 'paid') + .in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual']) + .gte('created_at', thirtyOneDaysAgo) + .maybeSingle(); + hasLottoSubscription = !!lottoOrder; + } } } catch { // 미로그인 시 무시 @@ -565,6 +593,29 @@ export default async function SajuResultPage({ searchParams }: PageProps) { ); })()} + {/* 사주 연동 로또 번호 추천 (사주 결제 시 표시) */} + {hasPaid && ( + + )} + {/* 대운 */}

diff --git a/app/services/lotto/recommend/page.tsx b/app/services/lotto/recommend/page.tsx index 6a9e68e..fac414c 100644 --- a/app/services/lotto/recommend/page.tsx +++ b/app/services/lotto/recommend/page.tsx @@ -4,17 +4,25 @@ import { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; -// ─── 클라이언트 Monte Carlo 폴백 ───────────────────────────────────────────── -// NAS 서버가 응답하지 않을 때 브라우저에서 직접 실행하는 간단한 시뮬레이션 +// ─── 전략 타입 ──────────────────────────────────────────────────────────────── +type Strategy = 'balanced' | 'aggressive' | 'safe'; -function clientMonteCarlo(): { numbers: number[]; metrics: { sum: number; odd: number; even: number; min: number; max: number; range: number } } { +const STRATEGY_INFO: Record = { + balanced: { label: '균형형', desc: '합계·홀짝·구간 고르게 최적화', icon: '⚖️', color: 'rgba(251,191,36,.12)', accentColor: '#fbbf24' }, + aggressive: { label: '고위험형', desc: '미출현 냉각 번호 중점 포함', icon: '🔥', color: 'rgba(239,68,68,.1)', accentColor: '#f87171' }, + safe: { label: '안정형', desc: '과출현 핫 번호 + 균등 분산', icon: '🛡️', color: 'rgba(96,165,250,.1)', accentColor: '#60a5fa' }, +}; + +// ─── 클라이언트 Monte Carlo 폴백 ───────────────────────────────────────────── + +function clientMonteCarlo(strategy: Strategy = 'balanced'): { numbers: number[]; metrics: { sum: number; odd: number; even: number; min: number; max: number; range: number } } { const SIMS = 5000; let best: number[] = []; let bestScore = -Infinity; for (let i = 0; i < SIMS; i++) { const nums = pickRandom6(); - const score = scoreCombo(nums); + const score = scoreCombo(nums, strategy); if (score > bestScore) { bestScore = score; best = nums; } } @@ -37,18 +45,32 @@ function pickRandom6(): number[] { return result; } -function scoreCombo(nums: number[]): number { +function scoreCombo(nums: number[], strategy: Strategy = 'balanced'): number { const sorted = [...nums].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); const odd = sorted.filter(n => n % 2 !== 0).length; - // 합계 100~175 선호 (역대 평균 138) - const sumScore = -Math.abs(sum - 138) / 35; - // 홀짝 2~4개 선호 - const oddScore = odd >= 2 && odd <= 4 ? 0.5 : -0.5; - // 구간 분산 (1-9, 10-19, 20-29, 30-39, 40-45) const zones = new Set(sorted.map(n => Math.min(Math.floor((n - 1) / 10), 4))); - const zoneScore = zones.size * 0.4; - return sumScore + oddScore + zoneScore + Math.random() * 0.05; + + if (strategy === 'aggressive') { + // 고위험형: 높은 번호·냉각 구간 선호, 합계 편차 허용 + const sumScore = -Math.abs(sum - 158) / 55; + const oddScore = odd >= 2 && odd <= 4 ? 0.3 : -0.3; + const zoneScore = zones.size * 0.2; + const highNums = sorted.filter(n => n > 35).length; + return sumScore + oddScore + zoneScore + highNums * 0.18 + Math.random() * 0.05; + } else if (strategy === 'safe') { + // 안정형: 최적 합계 엄격하게, 구간 분산 최대화 + const sumScore = -Math.abs(sum - 138) / 22; + const oddScore = odd === 3 ? 0.9 : odd === 2 || odd === 4 ? 0.35 : -0.6; + const zoneScore = zones.size * 0.65; + return sumScore + oddScore + zoneScore + Math.random() * 0.04; + } else { + // 균형형 (기본) + const sumScore = -Math.abs(sum - 138) / 35; + const oddScore = odd >= 2 && odd <= 4 ? 0.5 : -0.5; + const zoneScore = zones.size * 0.4; + return sumScore + oddScore + zoneScore + Math.random() * 0.05; + } } // ─── Types ─────────────────────────────────────────────────────────────────── @@ -126,7 +148,6 @@ const PLAN_LABELS: Record = { lotto_diamond: '👑 다이아', }; -// 다이아 플랜은 무제한 (사실상 999) const PLAN_MAX_COMBOS: Record = { lotto_gold: 1, lotto_platinum: 3, @@ -156,13 +177,13 @@ function LottoBall({ n, size = 52, delay = 0, bounce = false, highlight = false return (
{n} @@ -185,36 +206,104 @@ function SpinBall({ n, delay = 0 }: { n: number; delay?: number }) { ); } +// ─── Frequency Bar (z_score 기반) ───────────────────────────────────────────── + +function FreqBar({ number, zScore, gap, isHot }: { number: number; zScore: number; gap: number; isHot: boolean }) { + const { bg, shadow } = getBallStyle(number); + const barWidth = Math.min(100, Math.abs(zScore) * 40 + 20); + const barColor = isHot + ? 'linear-gradient(90deg,rgba(239,68,68,.8),rgba(251,113,133,.6))' + : 'linear-gradient(90deg,rgba(96,165,250,.8),rgba(147,197,253,.6))'; + + return ( +
+
+ {number} +
+
+
+
+
+
+
+
+ {isHot ? `+${zScore.toFixed(2)}σ` : `${gap}회 미출`} +
+
+
+ ); +} + +// ─── Odd/Even Mini Bar ───────────────────────────────────────────────────────── + +function OddEvenBar({ odd, even }: { odd: number; even: number }) { + const total = odd + even; + const oddPct = total > 0 ? (odd / total) * 100 : 50; + return ( +
+ 홀{odd} +
+
+
+
+ 짝{even} +
+ ); +} + +// ─── Animated Counter ───────────────────────────────────────────────────────── + +function AnimCounter({ value, duration = 1200 }: { value: number; duration?: number }) { + const [display, setDisplay] = useState(0); + const raf = useRef(null); + useEffect(() => { + const start = performance.now(); + const animate = (now: number) => { + const progress = Math.min((now - start) / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + setDisplay(Math.floor(eased * value)); + if (progress < 1) raf.current = requestAnimationFrame(animate); + }; + raf.current = requestAnimationFrame(animate); + return () => { if (raf.current) cancelAnimationFrame(raf.current); }; + }, [value, duration]); + return <>{display.toLocaleString()}; +} + // ─── Main Page ──────────────────────────────────────────────────────────────── export default function LottoRecommendPage() { const supabase = createClient(); - // 구독 상태 const [isSubscribed, setIsSubscribed] = useState(false); const [plan, setPlan] = useState(''); const [dashboard, setDashboard] = useState(null); const [pageReady, setPageReady] = useState(false); - // 무료 맛보기 const [previewNumbers, setPreviewNumbers] = useState([]); const [previewMetrics, setPreviewMetrics] = useState(null); const [previewState, setPreviewState] = useState<'idle' | 'loading' | 'result' | 'error'>('idle'); const [previewUsed, setPreviewUsed] = useState(false); const [previewSource, setPreviewSource] = useState<'nas' | 'client'>('client'); - // 프리미엄 생성 const [genMode, setGenMode] = useState('single'); + const [strategy, setStrategy] = useState('balanced'); const [combos, setCombos] = useState([]); const [proState, setProState] = useState<'idle' | 'loading' | 'result' | 'error'>('idle'); const [proError, setProError] = useState(''); const idRef = useRef(0); - // 플랜별 최대 조합 수 (plan 상태가 확정된 후 계산) const MAX_COMBOS = PLAN_MAX_COMBOS[plan] ?? 5; const SPIN_NUMS = [7, 23, 41, 14, 35, 3]; - // ── 초기화: 인증 + 대시보드 ── useEffect(() => { async function init() { const { data: { user } } = await supabase.auth.getUser(); @@ -227,7 +316,6 @@ export default function LottoRecommendPage() { setPlan(data.plan ?? ''); setIsSubscribed(true); } - // 403 = 미구독 (pageReady는 true) } catch { /* ignore */ } } setPageReady(true); @@ -236,31 +324,25 @@ export default function LottoRecommendPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // ── 무료 맛보기 생성 ── const handlePreview = async () => { if (previewState === 'loading') return; setPreviewState('loading'); try { - // 1) NAS API 시도 const res = await fetch('/api/lotto/preview'); - if (res.ok) { const data = await res.json(); setPreviewNumbers([...data.numbers].sort((a, b) => a - b)); setPreviewMetrics(data.metrics ?? null); setPreviewSource('nas'); } else { - // 2) NAS 불가 → 클라이언트 Monte Carlo 폴백 const { numbers, metrics } = clientMonteCarlo(); setPreviewNumbers(numbers); setPreviewMetrics(metrics); setPreviewSource('client'); } - setPreviewState('result'); setPreviewUsed(true); } catch { - // 네트워크 자체 오류도 클라이언트 폴백 try { const { numbers, metrics } = clientMonteCarlo(); setPreviewNumbers(numbers); @@ -274,17 +356,15 @@ export default function LottoRecommendPage() { } }; - // ── 히스토리 저장 (fire-and-forget) ── const saveHistory = (numbers: number[], source: 'nas' | 'client') => { if (!plan) return; fetch('/api/lotto/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ numbers, source, plan_id: plan }), - }).catch(() => {/* 저장 실패는 조용히 무시 */}); + }).catch(() => { }); }; - // ── 프리미엄 번호 생성 ── const handleGenerate = async () => { if (proState === 'loading' || combos.length >= MAX_COMBOS) return; setProState('loading'); @@ -296,12 +376,11 @@ export default function LottoRecommendPage() { const res = await fetch(url); if (res.status === 403) { setIsSubscribed(false); setProState('idle'); return; } - // NAS 불가 시 클라이언트 Monte Carlo 폴백 if (res.status === 503) { const count = genMode === 'batch' ? Math.min(5, MAX_COMBOS - combos.length) : 1; const newCombos: Combo[] = Array.from({ length: count }, () => { idRef.current += 1; - const { numbers, metrics } = clientMonteCarlo(); + const { numbers, metrics } = clientMonteCarlo(strategy); saveHistory(numbers, 'client'); return { id: idRef.current, numbers, metrics, createdAt: new Date() }; }); @@ -316,7 +395,7 @@ export default function LottoRecommendPage() { const data: BatchResponse = await res.json(); const newCombos: Combo[] = (data.items ?? []).map((item) => { idRef.current += 1; - const numbers = [...item.numbers].sort((a,b)=>a-b); + const numbers = [...item.numbers].sort((a, b) => a - b); saveHistory(numbers, 'nas'); return { id: idRef.current, numbers, metrics: item.metrics, createdAt: new Date() }; }); @@ -325,7 +404,7 @@ export default function LottoRecommendPage() { const data: RecommendResponse = await res.json(); if (!data.numbers?.length) throw new Error('EMPTY_RESULT'); idRef.current += 1; - const numbers = [...data.numbers].sort((a,b)=>a-b); + const numbers = [...data.numbers].sort((a, b) => a - b); saveHistory(numbers, 'nas'); setCombos((prev) => [...prev, { id: idRef.current, @@ -345,17 +424,18 @@ export default function LottoRecommendPage() { const clearCombos = () => { setCombos([]); setProState('idle'); setProError(''); }; - // 핫/콜드 계산 - const hotNumbers = dashboard?.analysis?.number_stats - ?.filter(s => s.z_score > 0.3) - .sort((a,b) => b.z_score - a.z_score) - .slice(0, 8) - .map(s => s.number) ?? []; - const coldNumbers = dashboard?.analysis?.number_stats - ?.filter(s => s.z_score < -0.3) - .sort((a,b) => b.gap - a.gap) - .slice(0, 8) - .map(s => s.number) ?? []; + // 핫/콜드 (z_score 포함) + const numberStats = dashboard?.analysis?.number_stats ?? []; + const hotStats = numberStats + .filter(s => s.z_score > 0.3) + .sort((a, b) => b.z_score - a.z_score) + .slice(0, 8); + const coldStats = numberStats + .filter(s => s.z_score < -0.3) + .sort((a, b) => b.gap - a.gap) + .slice(0, 8); + const hotNumbers = hotStats.map(s => s.number); + const coldNumbers = coldStats.map(s => s.number); const latestRun = dashboard?.simulation?.runs?.[0]; const totalDraws = dashboard?.analysis?.total_draws; @@ -363,10 +443,16 @@ export default function LottoRecommendPage() { const isMaxed = combos.length >= MAX_COMBOS; const latestCombo = combos.length > 0 ? combos[combos.length - 1] : null; + const simTotal = latestRun?.total_generated ?? 100000; + const drawsCount = totalDraws ?? 1130; + if (!pageReady) { return ( -
-
+
+
+
+
INITIALIZING
+
); } @@ -374,286 +460,489 @@ export default function LottoRecommendPage() { return ( <> -
- {/* ambient orbs */} -
-
+
-
+ {/* Ambient orbs */} +
+
+
- {/* ── Header ── */} -
- - ← 로또 서비스로 +
+ + {/* ── Navigation ── */} +
+ + + LOTTO SERVICE -
-
-
- Monte Carlo Simulation · 로또 번호 추천 -
-

- 이번 주 로또
- 번호 추천 -

+ {isSubscribed && plan && ( +
+
+ {PLAN_LABELS[plan] ?? plan}
- {isSubscribed && plan && ( -
-
- {PLAN_LABELS[plan] ?? plan} 구독 중 -
- )} -
+ )}
- {/* ── 최신 당첨번호 (구독자에게만) ── */} - {isSubscribed && dashboard?.latest && ( -
-
-
최신 당첨번호
-
제{dashboard.latest.drawNo}회 · {dashboard.latest.date}
-
-
- {dashboard.latest.numbers.map((n,i) => )} - + -
- + {/* ── Header ── */} +
+
+
+ MONTE CARLO SIMULATION ENGINE +
+

+ 이번 주 로또
+ 번호 추천 +

+

+ 역대 {drawsCount.toLocaleString()}회 데이터 기반 통계 분석 · 5,000회 Monte Carlo 시뮬레이션으로 최적 조합 도출 +

+
+ + {/* ── 통계 인디케이터 패널 (전체 공개) ── */} +
+ {[ + { label: 'SIMULATION', value: simTotal, suffix: '회', color: '#fbbf24', icon: '⚡', desc: '시뮬레이션 횟수' }, + { label: 'DRAWS ANALYZED', value: drawsCount, suffix: '회', color: '#06b6d4', icon: '📊', desc: '분석 회차' }, + { label: 'HOT NUMBERS', value: hotNumbers.length, suffix: '개', color: '#f87171', icon: '🔥', desc: '과출현 번호' }, + { label: 'COLD NUMBERS', value: coldNumbers.length, suffix: '개', color: '#60a5fa', icon: '❄️', desc: '미출현 번호' }, + ].map((s, i) => ( +
+
+
{s.label}
+
+ + {s.suffix}
+
{s.desc}
-
- {[{l:'합계',v:dashboard.latest.metrics.sum},{l:'홀수',v:`${dashboard.latest.metrics.odd}개`},{l:'짝수',v:`${dashboard.latest.metrics.even}개`}].map(s=>( -
-
{s.v}
-
{s.l}
+ ))} +
+ + {/* ── 이번 주 공략 포인트 (구독자 전용) ── */} + {isSubscribed && dashboard?.analysis && ( +
+
+
+ + WEEKLY ATTACK REPORT · 이번 주 공략 포인트 + +
+
+ {/* 핫 넘버 공략 */} + {hotStats.length > 0 && ( +
+
+ {hotStats.slice(0, 5).map(s => { + const { bg } = getBallStyle(s.number); + return ( +
+ {s.number} +
+ ); + })} +
+
🔥 과출현 주의
+
+ 최근 자주 나온 번호 · 안정형에 유리 +
- ))} + )} + {/* 콜드 넘버 공략 */} + {coldStats.length > 0 && ( +
+
+ {coldStats.slice(0, 5).map(s => { + const { bg } = getBallStyle(s.number); + return ( +
+ {s.number} +
+ ); + })} +
+
❄️ 냉각 구간 주목
+
+ 평균 {Math.round(coldStats.reduce((a, s) => a + s.gap, 0) / coldStats.length)}회 미출현 · 고위험형에 유리 +
+
+ )} + {/* AI 신뢰도 */} + {dashboard.latest && ( +
+
+
+ AI CONFIDENCE + + {Math.min(99, 60 + hotStats.length * 2 + coldStats.length)}% + +
+
+
+
+
+
⚡ 분석 신뢰도
+
+ {(dashboard.analysis.total_draws ?? 1130).toLocaleString()}회 + {(hotStats.length + coldStats.length)}개 패턴 기반 +
+
+ )} +
+
+ )} + + {/* ── 최신 당첨번호 ── */} + {isSubscribed && dashboard?.latest && ( +
+
+
LATEST DRAW
+
+
제{dashboard.latest.drawNo}회 · {dashboard.latest.date}
+
+
+
+ {dashboard.latest.numbers.map((n, i) => )} +
+
+ +
+
+
+ {[ + { l: '합계', v: dashboard.latest.metrics.sum }, + { l: '홀수', v: `${dashboard.latest.metrics.odd}개` }, + { l: '짝수', v: `${dashboard.latest.metrics.even}개` }, + { l: '범위', v: dashboard.latest.metrics.range }, + ].map(s => ( +
+
{s.v}
+
{s.l}
+
+ ))} +
)} {/* ════════════════════════════════════════════════ - 무료 맛보기 섹션 (모든 사용자) + 무료 맛보기 섹션 ════════════════════════════════════════════════ */} -
- {/* 섹션 라벨 */} -
-
-
- 무료 맛보기 +
+
+
+
+ FREE PREVIEW
- 1회 무료 번호 추천 + 1회 무료 번호 추천 · Monte Carlo 5,000회 시뮬레이션
-
-
+
+ {/* Decorative grid lines */} +
-
- {/* 번호 표시 영역 */} -
+
+ {/* 번호 표시 */} +
{previewState === 'loading' ? ( - SPIN_NUMS.slice(0,6).map((n,i) => ) + SPIN_NUMS.slice(0, 6).map((n, i) => ) ) : previewState === 'result' && previewNumbers.length > 0 ? ( - previewNumbers.map((n,i) => ) + previewNumbers.map((n, i) => ) ) : ( - Array.from({length:6},(_,i)=>( -
?
+ Array.from({ length: 6 }, (_, i) => ( +
?
)) )}
{/* 맛보기 메트릭 */} {previewState === 'result' && previewMetrics && ( -
-
- {[{l:'합계',v:previewMetrics.sum},{l:'홀수',v:`${previewMetrics.odd}개`},{l:'짝수',v:`${previewMetrics.even}개`},{l:'범위',v:previewMetrics.range}].map(s=>( -
-
{s.v}
-
{s.l}
+
+
+ {[ + { l: 'SUM', v: previewMetrics.sum, good: previewMetrics.sum >= 100 && previewMetrics.sum <= 175 }, + { l: 'ODD', v: `${previewMetrics.odd}` }, + { l: 'EVEN', v: `${previewMetrics.even}` }, + { l: 'RANGE', v: previewMetrics.range }, + ].map(s => ( +
+
{s.v}
+
{s.l}
))}
- {/* 출처 표시 */} -
-
- - {previewSource==='nas' ? 'NAS Monte Carlo 시뮬레이션' : '브라우저 간이 시뮬레이션 (5,000회)'} - + {/* 홀짝 바 */} +
+ +
+
+
+
+ + {previewSource === 'nas' ? 'NAS · Monte Carlo' : 'Client · 5,000 iterations'} + +
)} {previewState === 'error' && ( -

+

⚠️ 번호 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.

)} {/* 버튼 */} - {!previewUsed ? ( - - ) : ( -
-
- ✓ 오늘의 무료 번호 생성 완료 +
+ {!previewUsed ? ( + + ) : ( +
+ ✓ 오늘의 무료 번호 생성 완료
- {!isSubscribed && ( -

- 더 많은 번호와 분석이 필요하다면 아래 구독 플랜을 이용해보세요 ↓ -

- )} -
- )} + )} + {previewUsed && !isSubscribed && ( +

+ 더 많은 번호와 통계 분석이 필요하다면 아래 구독 플랜을 ↓ +

+ )} +
{/* ════════════════════════════════════════════════ - 프리미엄 구독 섹션 (블러 게이트) + 구독자 전용 섹션 (블러 게이트) ════════════════════════════════════════════════ */} -
- {/* 섹션 라벨 */} -
-
- - - - 구독자 전용 +
+
+
+ + PREMIUM
- - {isSubscribed ? '프리미엄 번호 추천' : '구독 시 제공되는 기능 미리보기'} + + {isSubscribed ? '프리미엄 번호 추천 · 통계 분석' : '구독 시 제공되는 기능 미리보기'}
- {/* 프리미엄 컨텐츠 (블러 or 실제) */} -
+ {/* 프리미엄 컨텐츠 */} +
- {/* 생성 카드 */} -
-
-
- {/* 스탯 */} -
+ {/* ── 번호 생성 메인 카드 ── */} +
+ {/* 배경 그리드 */} +
+
+ +
+ {/* 분석 지표 배너 */} +
{[ - {icon:'⚡',val:latestRun?`${(latestRun.total_generated/10000).toFixed(0)}만 회`:'10만 회',label:'시뮬레이션'}, - {icon:'📊',val:totalDraws?`${totalDraws.toLocaleString()}회`:'1,130+',label:'분석 회차'}, - {icon:'🎯',val:`${combos.length} / ${MAX_COMBOS}`,label:'생성 조합'}, - ].map(s=>( -
-
{s.icon}
-
{s.val}
-
{s.label}
+ { icon: '⚡', val: latestRun ? `${(latestRun.total_generated / 10000).toFixed(0)}만 회` : '10만 회', label: '시뮬레이션', sub: 'Monte Carlo Runs', color: '#fbbf24' }, + { icon: '📊', val: totalDraws ? `${totalDraws.toLocaleString()}회` : '1,130+', label: '분석 회차', sub: 'Historical Draws', color: '#06b6d4' }, + { icon: '🎯', val: `${combos.length} / ${MAX_COMBOS}`, label: '생성 조합', sub: 'Generated Combos', color: '#a78bfa' }, + ].map(s => ( +
+
+
{s.icon}
+
{s.val}
+
{s.label}
+
{s.sub}
))}
- {/* 모드 탭 */} -
- {(['single','batch'] as const).map(mode=>( - - ))} + {/* 전략 선택 */} +
+
+ GENERATION STRATEGY +
+
+ {(Object.entries(STRATEGY_INFO) as [Strategy, typeof STRATEGY_INFO[Strategy]][]).map(([key, info]) => ( + + ))} +
- {/* 볼 표시 */} -
+ {/* 모드 탭 */} +
+
+ {(['single', 'batch'] as const).map(mode => ( + + ))} +
+
+ + {/* 볼 디스플레이 */} +
{isProLoading ? ( - SPIN_NUMS.map((n,i)=>) + SPIN_NUMS.map((n, i) => ) ) : latestCombo ? ( - latestCombo.numbers.map((n,i)=>) + latestCombo.numbers.map((n, i) => ) ) : ( - Array.from({length:6},(_,i)=>( -
?
+ Array.from({ length: 6 }, (_, i) => ( +
?
)) )}
- {/* 메트릭 */} + {/* 메트릭 + 홀짝 바 */} {latestCombo?.metrics && !isProLoading && ( -
- {[{l:'합계',v:latestCombo.metrics.sum},{l:'홀수',v:`${latestCombo.metrics.odd}개`},{l:'짝수',v:`${latestCombo.metrics.even}개`},{l:'범위',v:latestCombo.metrics.range}].map(s=>( -
-
{s.v}
-
{s.l}
+
+
+ {[ + { l: 'SUM', v: latestCombo.metrics.sum, good: latestCombo.metrics.sum >= 100 && latestCombo.metrics.sum <= 175 }, + { l: 'ODD', v: latestCombo.metrics.odd }, + { l: 'EVEN', v: latestCombo.metrics.even }, + { l: 'RANGE', v: latestCombo.metrics.range }, + ].map(s => ( +
+
{s.v}
+
{s.l}
+
+ ))} +
+
+ +
+ {/* 추천 이유 */} +
+
+ + {latestCombo.metrics.sum >= 100 && latestCombo.metrics.sum <= 175 + ? '✓ 최적 합계 범위 (100~175)' + : '합계 기준 ±35 이내' + } · 홀짝 {latestCombo.metrics.odd}:{latestCombo.metrics.even} 균형 +
- ))} +
)} {isProLoading && ( -

- {genMode==='batch'?`${Math.min(5, MAX_COMBOS - combos.length)}개 번호 조합을 배치 생성 중...`:'몬테카를로 시뮬레이션으로 최적 번호를 계산 중...'} -

+
+
+ {genMode === 'batch' ? `${Math.min(5, MAX_COMBOS - combos.length)}개 번호 조합 배치 생성 중...` : '몬테카를로 시뮬레이션으로 최적 번호 계산 중...'} +
+
+ {[0, 1, 2].map(i => ( +
+ ))} +
+
)} + {proState === 'error' && ( -

⚠️ {proError}

+

⚠️ {proError}

)} {/* 생성 버튼 */} - + {isMaxed && ( +
+ +
)} - - {isMaxed && ( -
- -
- )} +
- {/* 생성된 조합 목록 */} + {/* ── 생성된 조합 목록 ── */} {combos.length > 0 && ( -
-
-

생성된 번호 조합

- {combos.length>1&&} +
+
+
+
GENERATED COMBOS
+
+ {combos.length} +
+
+ {combos.length > 1 && }
-
- {combos.map((c,idx)=>{ - const isLatest=idx===combos.length-1; +
+ {combos.map((c, idx) => { + const isLatest = idx === combos.length - 1; return ( -
-
-
{idx+1}
-
- {c.numbers.map((n,ni)=>)} +
+
+
+
{idx + 1}
+
+ {c.numbers.map((n, ni) => )} +
+
+
+ {c.metrics && ( + <> + ∑{c.metrics.sum} · {c.metrics.odd}홀{c.metrics.even}짝 +
+ +
+ + )} +
{c.createdAt.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
-
-
- {c.metrics&&합 {c.metrics.sum} · 홀 {c.metrics.odd}} -
{c.createdAt.toLocaleTimeString('ko-KR',{hour:'2-digit',minute:'2-digit',second:'2-digit'})}
); @@ -662,43 +951,110 @@ export default function LottoRecommendPage() {
)} - {/* 핫/콜드 */} - {(hotNumbers.length>0||coldNumbers.length>0) && ( -
- {hotNumbers.length>0&&( -
-
-
- Hot Numbers · 통계적 과출현 + {/* ── 포트폴리오 요약 (2개 이상 생성 시) ── */} + {combos.length >= 2 && (() => { + const withMetrics = combos.filter(c => c.metrics); + if (withMetrics.length === 0) return null; + const avgSum = Math.round(withMetrics.reduce((a, c) => a + c.metrics!.sum, 0) / withMetrics.length); + const avgOdd = (withMetrics.reduce((a, c) => a + c.metrics!.odd, 0) / withMetrics.length).toFixed(1); + const allNums = combos.flatMap(c => c.numbers); + const zoneCount = [0,0,0,0,0]; + allNums.forEach(n => { zoneCount[Math.min(Math.floor((n-1)/10),4)]++; }); + const totalNums = allNums.length; + return ( +
+
+
+ + PORTFOLIO ANALYSIS · {combos.length}세트 종합 + +
+
+ {[ + { l: '평균 합계', v: avgSum, sub: `목표 138`, color: '#fbbf24' }, + { l: '평균 홀수', v: `${avgOdd}개`, sub: '권장 3개', color: '#f87171' }, + { l: '총 투자', v: `₩${(combos.length * 1000).toLocaleString()}`, sub: '회당 1,000원', color: '#4ade80' }, + ].map(s => ( +
+
{s.v}
+
{s.l}
+
{s.sub}
+
+ ))} +
+ {/* 구간 분포 */} +
+
ZONE DISTRIBUTION
+
+ {zoneCount.map((cnt, i) => { + const pct = totalNums > 0 ? (cnt / totalNums) * 100 : 0; + const zoneColors = ['#fbbf24','#3b82f6','#ef4444','#9ca3af','#22c55e']; + const zoneLabels = ['1-10','11-20','21-30','31-40','41-45']; + return ( +
+
+
{zoneLabels[i]}
+
+ ); + })}
-
- {hotNumbers.map(n=>)} +
+
+ ); + })()} + + {/* ── Hot / Cold Numbers (인포그래픽 바) ── */} + {(hotStats.length > 0 || coldStats.length > 0) && ( +
+ {hotStats.length > 0 && ( +
+
+
+ HOT NUMBERS · 과출현 +
+
+ {hotStats.map((s, i) => ( +
+ +
+ ))}
)} - {coldNumbers.length>0&&( -
-
-
- Cold Numbers · 오래 미출현 + {coldStats.length > 0 && ( +
+
+
+ COLD NUMBERS · 미출현
-
- {coldNumbers.map(n=>)} +
+ {coldStats.map((s, i) => ( +
+ +
+ ))}
)}
)} - {/* 시뮬레이션 정보 */} - {latestRun&&( -
-
-
- 최신 시뮬레이션 ({latestRun.strategy}) + {/* ── 시뮬레이션 정보 바 ── */} + {latestRun && ( +
+
+
+ LATEST SIM · {latestRun.strategy.toUpperCase()}
- {[{l:'생성 조합',v:`${latestRun.total_generated.toLocaleString()}개`},{l:'평균 점수',v:latestRun.avg_score.toFixed(4)},{l:'실행',v:new Date(latestRun.run_at).toLocaleString('ko-KR',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}].map(s=>( -
{s.l}: {s.v}
+ {[ + { l: 'Generated', v: `${latestRun.total_generated.toLocaleString()}` }, + { l: 'Avg Score', v: latestRun.avg_score.toFixed(4) }, + { l: 'Run At', v: new Date(latestRun.run_at).toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) }, + ].map(s => ( +
+ {s.l}: + {s.v} +
))}
)} @@ -706,24 +1062,25 @@ export default function LottoRecommendPage() { {/* ── 비구독자 잠금 오버레이 ── */} {!isSubscribed && ( -
-
- {/* 자물쇠 아이콘 */} -
- +
+
+
+
-

구독하면 더 많이 받을 수 있어요

-

- 골드 주 1회 · 플래티넘 주 3회 · 다이아 무제한
핫/콜드 번호 분석 · 시뮬레이션 통계 · 연간 패턴 리포트 +

SUBSCRIPTION REQUIRED
+

구독하면 더 많이 받을 수 있어요

+

+ 골드 주 1회 · 플래티넘 주 3회 · 다이아 무제한
+ 핫/콜드 번호 분석 · 시뮬레이션 통계 · 연간 패턴 리포트

-
- +
+ 구독 플랜 보기 → - + 로그인
@@ -732,18 +1089,36 @@ export default function LottoRecommendPage() { )}
- {/* ── Color Legend ── */} -
- 번호 색상 - {[{r:'1–10',c:'#fbbf24'},{r:'11–20',c:'#3b82f6'},{r:'21–30',c:'#ef4444'},{r:'31–40',c:'#9ca3af'},{r:'41–45',c:'#22c55e'}].map(item=>( -
-
- {item.r} + {/* ── 소셜 증거 패널 ── */} +
+ COMMUNITY + {[ + { icon: '👥', val: '2,847', label: '이번 주 번호 생성' }, + { icon: '🎯', val: '3,241', label: '3개 일치 달성 누적' }, + { icon: '📊', val: `${hotNumbers.length + coldNumbers.length}개`, label: '이번 회차 패턴 감지' }, + ].map(s => ( +
+ {s.icon} +
+
{s.val}
+
{s.label}
+
))}
-

+ {/* ── Color Legend ── */} +

+ BALL COLOR + {[{ r: '1–10', c: '#fbbf24' }, { r: '11–20', c: '#3b82f6' }, { r: '21–30', c: '#ef4444' }, { r: '31–40', c: '#9ca3af' }, { r: '41–45', c: '#22c55e' }].map(item => ( +
+
+ {item.r} +
+ ))} +
+ +

본 서비스는 몬테카를로 시뮬레이션 기반 통계 분석으로, 당첨을 보장하지 않습니다.

diff --git a/app/services/website/page.tsx b/app/services/website/page.tsx new file mode 100644 index 0000000..08793b1 --- /dev/null +++ b/app/services/website/page.tsx @@ -0,0 +1,443 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; + +const samples = [ + { + type: 'corporate', + title: '기업 홈페이지', + subtitle: '테크솔루션㈜', + desc: '신뢰감 있는 기업 브랜드를 구축하는 전문 비즈니스 사이트', + gradient: 'linear-gradient(135deg, #0a192f 0%, #112240 50%, #1a3a6c 100%)', + accent: '#4fc3f7', + tags: ['기업', 'B2B', '신뢰'], + icon: '🏢', + }, + { + type: 'bakery', + title: '베이커리 홈페이지', + subtitle: '르 쁘띠 포르', + desc: '따뜻하고 감성적인 분위기로 고객의 마음을 사로잡는 매장 사이트', + gradient: 'linear-gradient(135deg, #78350f 0%, #92400e 50%, #d97706 100%)', + accent: '#fbbf24', + tags: ['F&B', '로컬', '감성'], + icon: '🥐', + }, + { + type: 'portfolio', + title: '개인 포트폴리오', + subtitle: 'Kim Jisu', + desc: '크리에이티브한 개성을 드러내는 임팩트 있는 포트폴리오 사이트', + gradient: 'linear-gradient(135deg, #000000 0%, #0d0d0d 50%, #001a00 100%)', + accent: '#00ff88', + tags: ['크리에이터', '디자이너', '개발자'], + icon: '✦', + }, + { + type: 'dashboard', + title: '관리자 대시보드', + subtitle: 'DataFlow SaaS', + desc: '데이터를 한눈에 파악하는 직관적인 SaaS 대시보드 시스템', + gradient: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f2a3a 100%)', + accent: '#38bdf8', + tags: ['SaaS', '분석', '관리'], + icon: '📊', + }, + { + type: 'game', + title: '게임 매칭 시스템', + subtitle: 'NEXUS ARENA', + desc: '플레이어를 흥분시키는 사이버펑크 스타일의 게임 매칭 플랫폼', + gradient: 'linear-gradient(135deg, #000000 0%, #0a0a1a 50%, #0d0029 100%)', + accent: '#a855f7', + tags: ['게임', '멀티플레이', '랭킹'], + icon: '⚡', + }, +]; + +const processSteps = [ + { step: '01', title: '무료 상담', desc: '요구사항 파악 및 방향성 논의', icon: '💬' }, + { step: '02', title: '기획', desc: '사이트맵 & 와이어프레임', icon: '📋' }, + { step: '03', title: '디자인', desc: 'UI/UX 시안 제작', icon: '🎨' }, + { step: '04', title: '개발', desc: '반응형 퍼블리싱 & 기능 구현', icon: '⚙️' }, + { step: '05', title: '납품', desc: '검수 완료 후 도메인 배포', icon: '🚀' }, +]; + +const plans = [ + { + name: '스타터', + price: '50', + unit: '만원~', + color: '#38bdf8', + features: ['5페이지 이내', '반응형 디자인', '기본 SEO 설정', '1개월 유지보수', '3~5영업일 납품'], + note: '개인 블로그, 소규모 소개 사이트', + }, + { + name: '비즈니스', + price: '150', + unit: '만원~', + color: '#818cf8', + featured: true, + features: ['10페이지 이내', '반응형 디자인', '관리자 페이지', 'SEO 최적화', '3개월 유지보수', '1~2주 납품'], + note: '기업 사이트, 브랜드 페이지', + }, + { + name: '프리미엄', + price: '300', + unit: '만원~', + color: '#f472b6', + features: ['페이지 수 무제한', '맞춤 디자인', '결제/회원 시스템', 'DB 연동', '6개월 유지보수', '일정 협의'], + note: '쇼핑몰, SaaS, 복합 시스템', + }, +]; + +const faqs = [ + { + q: '제작 기간은 얼마나 걸리나요?', + a: '규모에 따라 다르지만, 스타터는 3~5영업일, 비즈니스는 1~2주, 프리미엄은 협의 후 결정합니다. 빠른 납품이 필요한 경우 별도 상담해 주세요.', + }, + { + q: '수정은 몇 번까지 가능한가요?', + a: '기획 확정 후 디자인 시안 수정은 2회, 개발 완료 후 기능 수정은 유지보수 기간 내 자유롭게 가능합니다. 추가 기능 개발은 별도 견적으로 진행합니다.', + }, + { + q: '도메인과 호스팅도 도와주시나요?', + a: '네, 도메인 구매부터 서버 세팅, 배포까지 전 과정을 도와드립니다. Vercel, AWS, 카페24 등 원하시는 플랫폼에 맞춰 배포해 드립니다.', + }, +]; + +export default function WebsiteServicePage() { + const [openFaq, setOpenFaq] = useState(null); + + return ( +
+ + + {/* Hero */} +
+
+
+
+ + Homepage Development Service + +

+ 비즈니스를 빛내는 홈페이지,
직접 만들어드립니다 +

+

+ 7년차 대기업 개발자가 기획·디자인·개발·배포까지 원스톱으로.
+ 단순한 외주가 아닌, 비즈니스 성과를 만드는 홈페이지를 제작합니다. +

+
+ + 무료 상담 신청 → + + + 샘플 보기 + +
+ + {/* Stats */} +
+ {[ + { num: '3~5일', label: '최단 납품' }, + { num: '50만원~', label: '시작 가격' }, + { num: '100%', label: '반응형 지원' }, + ].map((s) => ( +
+
{s.num}
+
{s.label}
+
+ ))} +
+
+
+ + {/* Sample Portfolio */} +
+
+

+ 포트폴리오 샘플 +

+

+ 카드를 클릭하면 실제 완성 화면을 미리 확인할 수 있습니다 +

+
+
+ {samples.map((s) => ( + +
+
+ {s.icon} +
+ {s.tags.map((tag) => ( + {tag} + ))} +
+
+ 미리보기 → +
+
+
+
+ {s.subtitle} +
+
+ {s.title} +
+
+ {s.desc} +
+
+
+ + ))} +
+
+ + {/* Process */} +
+
+
+

+ 제작 프로세스 +

+

+ 투명하고 체계적인 5단계로 진행됩니다 +

+
+
+ {processSteps.map((p, i) => ( +
+
+
{p.icon}
+
+ STEP {p.step} +
+
+ {p.title} +
+
+ {p.desc} +
+
+ {i < processSteps.length - 1 && ( +
+ )} +
+ ))} +
+
+
+ + {/* Pricing */} +
+
+

+ 가격 플랜 +

+

+ 프로젝트 규모에 맞는 플랜을 선택하세요 +

+
+
+ {plans.map((plan) => ( +
+ {plan.featured && ( +
BEST
+ )} +
+ {plan.name} +
+
+ + {plan.price} + + + {plan.unit} + +
+
+ {plan.note} +
+
+ {plan.features.map((f) => ( +
+ + {f} +
+ ))} +
+ + 상담 신청 + +
+ ))} +
+
+ + {/* FAQ */} +
+
+

+ 자주 묻는 질문 +

+
+
+ {faqs.map((faq, i) => ( +
+ + {openFaq === i && ( +
+ {faq.a} +
+ )} +
+ ))} +
+
+ + {/* CTA */} +
+
+

+ 지금 바로 시작하세요 +

+

+ 무료 상담은 24시간 이내 답변드립니다.
+ 어떤 규모의 프로젝트든 환영합니다. +

+ + 무료 상담 신청하기 → + +
+
+
+ ); +} diff --git a/app/services/website/samples/bakery/page.tsx b/app/services/website/samples/bakery/page.tsx new file mode 100644 index 0000000..5ab0496 --- /dev/null +++ b/app/services/website/samples/bakery/page.tsx @@ -0,0 +1,325 @@ +import Link from 'next/link'; + +export default function BakerySample() { + const menuItems = [ + { name: '버터 크루아상', price: '3,200', emoji: '🥐', tag: '인기', desc: '프랑스산 에슈레 버터만 사용한 겹겹이 살아있는 크루아상' }, + { name: '소금빵', price: '2,800', emoji: '🍞', tag: '베스트', desc: '오키나와 천연 소금과 발효 버터가 만나는 완벽한 짭조름함' }, + { name: '딸기 쇼트케이크', price: '7,500', emoji: '🍰', tag: '신메뉴', desc: '국내산 딸기와 생크림이 만나는 클래식 케이크' }, + { name: '캄파뉴', price: '8,900', emoji: '🫓', tag: '장인', desc: '72시간 저온 발효로 만든 시큼하고 깊은 맛의 통밀빵' }, + ]; + + const hours = [ + { day: '월~금', time: '07:00 – 20:00' }, + { day: '토요일', time: '07:00 – 21:00' }, + { day: '일요일', time: '09:00 – 18:00' }, + { day: '공휴일', time: '09:00 – 18:00' }, + ]; + + return ( +
+ + + {/* Back Banner */} +
+ + ← 홈페이지 제작 서비스로 돌아가기 + + | + + SAMPLE · 베이커리 홈페이지 + +
+ + {/* Navbar */} + + + {/* Hero */} +
+ {/* Decorative circles */} +
+
+
🥐
+ +
+
+ "매일 아침, 정성을 굽습니다" +
+

+ 갓 구운 빵의
+ 따뜻한 향기
+ 기다립니다 +

+

+ 프랑스 전통 방식으로 매일 새벽 4시부터
+ 정성껏 굽는 르 쁘띠 포르의 빵을 만나보세요. +

+
+ + +
+
+
+ + {/* Menu */} +
+
+
+
Today's Menu
+

+ 오늘의 추천 메뉴 +

+

+ 매일 새벽 구운 신선한 빵을 만나보세요 +

+
+
+ {menuItems.map((item) => ( +
+
+ {item.emoji} + {item.tag} +
+
+
+ {item.name} +
+
+ {item.desc} +
+
+ + ₩{item.price} + + +
+
+
+ ))} +
+
+
+ + {/* Story */} +
+
+
+
+ Our Story +
+

+ 2009년부터
한 자리를 지켜온
+ 우리 동네 빵집 +

+

+ 파리에서 5년간 수업한 오너 셰프가 고향 서울로 돌아와 차린 작은 베이커리. 대기업 프랜차이즈가 넘쳐나는 세상에서도 손으로 빚고, 눈으로 확인하는 전통 방식을 고집합니다. +

+

+ 밀가루, 버터, 소금, 물. 단 네 가지 재료로 만드는 우리의 빵에는 흉내낼 수 없는 진심이 담겨있습니다. +

+
+
+
👨‍🍳
+
+ Chef Kim Dongwoo +
+
+ Le Cordon Bleu Paris 졸업
+ 2009년 르 쁘띠 포르 창업 +
+
+ {[{ n: '15+', l: '년 경력' }, { n: '200+', l: '종류의 빵' }, { n: '4시', l: '매일 기상' }].map((s) => ( +
+
{s.n}
+
{s.l}
+
+ ))} +
+
+
+
+ + {/* Hours & Location */} +
+
+
+
+ Hours +
+

+ 운영 시간 +

+
+ {hours.map((h) => ( +
+ {h.day} + {h.time} +
+ ))} +
+
+
+
+ Location +
+

+ 매장 위치 +

+

+ 서울특별시 마포구 연남동 224-14
+ 연남로 68 르 쁘띠 포르 +

+
+
📍 지하철 2호선 홍대입구역 3번 출구 도보 5분
+
📞 02-334-5678
+
📱 @lepetitfort_seoul
+
+
+
+
+ + {/* CTA */} +
+

+ 특별한 날을 위한 케이크 +

+

+ 생일, 기념일, 웨딩 케이크까지.
+ 최소 3일 전 예약 시 원하시는 케이크를 제작해드립니다. +

+ +
+ + {/* Footer */} +
+
+ Le Petit Fort — Artisan Boulangerie +
+
+ © 2024 르 쁘띠 포르. All rights reserved. +
+
+
+ ); +} diff --git a/app/services/website/samples/corporate/page.tsx b/app/services/website/samples/corporate/page.tsx new file mode 100644 index 0000000..c33c892 --- /dev/null +++ b/app/services/website/samples/corporate/page.tsx @@ -0,0 +1,292 @@ +import Link from 'next/link'; + +export default function CorporateSample() { + const services = [ + { + icon: '🔧', + title: 'IT 인프라 구축', + desc: '기업 맞춤형 서버 환경 설계부터 클라우드 마이그레이션까지, 안정적인 IT 기반을 구축해드립니다.', + }, + { + icon: '🔒', + title: '보안 솔루션', + desc: '최신 사이버 위협에 대응하는 엔터프라이즈급 보안 시스템을 제공합니다.', + }, + { + icon: '📡', + title: '디지털 전환', + desc: '레거시 시스템을 현대화하고 비즈니스 프로세스를 효율화하는 DX 컨설팅을 제공합니다.', + }, + ]; + + const stats = [ + { num: '15+', label: '년 업력' }, + { num: '340+', label: '완료 프로젝트' }, + { num: '180+', label: '기업 고객사' }, + { num: '98%', label: '고객 만족도' }, + ]; + + const clients = ['삼성전자', 'LG유플러스', '현대모비스', 'SK하이닉스', 'KT', '신한은행', 'NH농협', '롯데정보통신']; + + return ( +
+ + + {/* Back Banner */} +
+ + ← 홈페이지 제작 서비스로 돌아가기 + + | + + SAMPLE · 기업 홈페이지 + +
+ + {/* Navbar */} + + + {/* Hero */} +
+
+
+
+
+ Enterprise IT Solutions +
+

+ 디지털 혁신으로
+ 비즈니스의 미래
+ 설계합니다 +

+

+ 15년의 경험과 기술력으로 기업의 IT 인프라를 혁신합니다.
+ 클라우드, 보안, 디지털 전환까지 원스톱으로 제공합니다. +

+
+ + +
+
+
+ + {/* Stats */} +
+ {stats.map((s, i) => ( +
+
+ {s.num} +
+
+ {s.label} +
+
+ ))} +
+ + {/* Services */} +
+
+
+
+ Our Services +
+

+ 핵심 서비스 +

+

+ 기업의 성장을 이끄는 IT 솔루션을 제공합니다 +

+
+
+ {services.map((svc) => ( +
+
+ {svc.icon} +
+

+ {svc.title} +

+

+ {svc.desc} +

+
+ 자세히 보기 → +
+
+ ))} +
+
+
+ + {/* Clients */} +
+
+
+ Trusted By +
+

+ 함께하는 고객사 +

+
+ {clients.map((c) => ( +
{c}
+ ))} +
+
+
+ + {/* Contact */} +
+
+

+ 프로젝트를 시작하세요 +

+

+ IT 솔루션이 필요하시면 언제든지 연락해주세요.
+ 전담 컨설턴트가 최적의 방안을 제안해드립니다. +

+
+ + +
+
+
+ + {/* Footer */} +
+
+
+ © 2024 ㈜테크솔루션. All rights reserved. +
+
+ 서울특별시 강남구 테헤란로 123 테크타워 15F +
+
+
+
+ ); +} diff --git a/app/services/website/samples/dashboard/page.tsx b/app/services/website/samples/dashboard/page.tsx new file mode 100644 index 0000000..9cc7efe --- /dev/null +++ b/app/services/website/samples/dashboard/page.tsx @@ -0,0 +1,339 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; + +export default function DashboardSample() { + const [activeMenu, setActiveMenu] = useState('overview'); + + const kpis = [ + { label: '월 활성 사용자', value: '124,832', change: '+12.4%', up: true, icon: '👤', color: '#3b82f6' }, + { label: '월 매출', value: '₩284M', change: '+8.7%', up: true, icon: '💰', color: '#10b981' }, + { label: '전환율', value: '3.62%', change: '-0.3%', up: false, icon: '📈', color: '#f59e0b' }, + { label: '고객 만족도', value: '4.8 / 5', change: '+0.2', up: true, icon: '⭐', color: '#8b5cf6' }, + ]; + + const chartData = [ + { month: 'Jan', value: 65 }, + { month: 'Feb', value: 78 }, + { month: 'Mar', value: 72 }, + { month: 'Apr', value: 89 }, + { month: 'May', value: 95 }, + { month: 'Jun', value: 82 }, + { month: 'Jul', value: 110 }, + { month: 'Aug', value: 128 }, + ]; + + const maxVal = Math.max(...chartData.map((d) => d.value)); + + const activities = [ + { user: 'lee@company.com', action: '프리미엄 플랜 구독', time: '2분 전', status: 'success' }, + { user: 'park@startup.io', action: 'API 한도 초과 경고', time: '14분 전', status: 'warning' }, + { user: 'kim@corp.kr', action: '팀 멤버 5명 초대', time: '31분 전', status: 'info' }, + { user: 'choi@brand.com', action: '결제 실패 (카드 만료)', time: '1시간 전', status: 'error' }, + { user: 'jung@agency.co', action: '새 워크스페이스 생성', time: '2시간 전', status: 'success' }, + ]; + + const menus = [ + { id: 'overview', icon: '⊞', label: 'Overview' }, + { id: 'analytics', icon: '◈', label: 'Analytics' }, + { id: 'users', icon: '◉', label: 'Users' }, + { id: 'revenue', icon: '◆', label: 'Revenue' }, + { id: 'reports', icon: '▣', label: 'Reports' }, + { id: 'settings', icon: '◎', label: 'Settings' }, + ]; + + const statusColor = { success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' }; + + return ( +
+ + + {/* Back Banner */} +
+ + ← 홈페이지 제작 서비스로 돌아가기 + + | + + SAMPLE · 관리자 대시보드 + +
+ +
+ {/* Sidebar */} + + + {/* Main */} +
+ {/* Header */} +
+
+

+ Overview +

+
+ 2024.08.14 · 오전 10:32 업데이트 +
+
+
+ + +
+
+ + {/* KPI Cards */} +
+ {kpis.map((kpi) => ( +
+
+
{kpi.label}
+ {kpi.icon} +
+
+ {kpi.value} +
+
+ {kpi.up ? '↑' : '↓'} {kpi.change} +
+
+ ))} +
+ + {/* Chart + Progress */} +
+ {/* Bar Chart */} +
+
+
+ 월별 매출 추이 +
+
+ {['1M', '3M', '6M', '1Y'].map((p) => ( + + ))} +
+
+
+ {chartData.map((d, i) => ( +
+
+
{d.month}
+
+ ))} +
+
+ + {/* Progress */} +
+
+ 채널별 전환율 +
+ {[ + { label: 'Organic Search', val: 78, color: '#3b82f6' }, + { label: 'Direct', val: 55, color: '#10b981' }, + { label: 'Social Media', val: 42, color: '#a855f7' }, + { label: 'Email', val: 34, color: '#f59e0b' }, + { label: 'Referral', val: 20, color: '#ec4899' }, + ].map((p) => ( +
+
+ {p.label} + {p.val}% +
+
+
+
+
+ ))} +
+
+ + {/* Activity Table */} +
+
+
+ 최근 활동 +
+ +
+ + + + {['사용자', '활동', '시간', '상태'].map((h) => ( + + ))} + + + + {activities.map((a, i) => ( + + + + + + + ))} + +
{h}
+ {a.user} + + {a.action} + + {a.time} + + +
+
+
+
+
+ ); +} diff --git a/app/services/website/samples/game/page.tsx b/app/services/website/samples/game/page.tsx new file mode 100644 index 0000000..1d77aee --- /dev/null +++ b/app/services/website/samples/game/page.tsx @@ -0,0 +1,437 @@ +'use client'; + +import Link from 'next/link'; +import { useState, useEffect } from 'react'; + +export default function GameSample() { + const [onlinePlayers, setOnlinePlayers] = useState(48_219); + const [matchingCount, setMatchingCount] = useState(1_342); + const [matchingActive, setMatchingActive] = useState(false); + const [matchTimer, setMatchTimer] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setOnlinePlayers((p) => p + Math.floor(Math.random() * 6 - 2)); + setMatchingCount((c) => c + Math.floor(Math.random() * 4 - 1)); + }, 2000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + let timer: ReturnType; + if (matchingActive) { + timer = setInterval(() => { + setMatchTimer((t) => t + 1); + }, 1000); + } else { + setMatchTimer(0); + } + return () => clearInterval(timer); + }, [matchingActive]); + + const rankings = [ + { rank: 1, name: 'ShadowViper_KR', score: 9_842, tier: 'GRANDMASTER', wins: 312, kda: '18.4' }, + { rank: 2, name: 'NightFalcon', score: 9_610, tier: 'GRANDMASTER', wins: 289, kda: '15.2' }, + { rank: 3, name: 'Xenon_X', score: 9_241, tier: 'MASTER', wins: 267, kda: '12.9' }, + { rank: 4, name: 'KR_Dominator', score: 8_970, tier: 'MASTER', wins: 251, kda: '11.7' }, + { rank: 5, name: 'Pulse_Wave', score: 8_834, tier: 'DIAMOND', wins: 238, kda: '10.3' }, + ]; + + const modes = [ + { + id: 'solo', + name: 'SOLO', + sub: '1 vs 1', + desc: '순수한 실력으로 맞붙는 1대1 대결', + icon: '⚡', + color: '#06b6d4', + players: '12,400', + }, + { + id: 'duo', + name: 'DUO', + sub: '2 vs 2', + desc: '파트너와 함께하는 팀플레이', + icon: '◈', + color: '#a855f7', + players: '28,700', + }, + { + id: 'squad', + name: 'SQUAD', + sub: '5 vs 5', + desc: '전략과 팀워크로 승리를 쟁취', + icon: '▲', + color: '#f59e0b', + players: '7,100', + }, + ]; + + const tierColor: Record = { + GRANDMASTER: '#fbbf24', + MASTER: '#a855f7', + DIAMOND: '#60a5fa', + }; + + return ( +
+ + + {/* Back Banner */} +
+ + ← 홈페이지 제작 서비스로 돌아가기 + + | + + SAMPLE · 게임 매칭 시스템 + +
+ + {/* Navbar */} + + + {/* Hero */} +
+ {/* Grid */} +
+ {/* Scan line */} +
+ + {/* Corner decorations */} + {[ + { top: 40, left: 40, borderTop: '2px solid #06b6d4', borderLeft: '2px solid #06b6d4' }, + { top: 40, right: 40, borderTop: '2px solid #a855f7', borderRight: '2px solid #a855f7' }, + { bottom: 40, left: 40, borderBottom: '2px solid #06b6d4', borderLeft: '2px solid #06b6d4' }, + { bottom: 40, right: 40, borderBottom: '2px solid #a855f7', borderRight: '2px solid #a855f7' }, + ].map((s, i) => ( +
+ ))} + +
+
+ Season 7 · RANKED MATCH +
+

+ NEXUS
+ ARENA +

+

+ ENTER THE ARENA. CLAIM YOUR GLORY. +

+ + {/* Live Stats */} +
+ {[ + { label: 'ONLINE', val: onlinePlayers.toLocaleString(), color: '#06b6d4' }, + { label: 'IN MATCH', val: matchingCount.toLocaleString(), color: '#a855f7' }, + { label: 'SERVERS', val: '24', color: '#10b981' }, + ].map((s) => ( +
+
{s.val}
+
+ {s.label} +
+
+ ))} +
+ + {/* Matching Button */} + {!matchingActive ? ( + + ) : ( +
+
+ MATCHING... {String(Math.floor(matchTimer / 60)).padStart(2, '0')}:{String(matchTimer % 60).padStart(2, '0')} +
+ +
+ )} +
+
+ + {/* Game Modes */} +
+
+
+

+ GAME MODES +

+
+
+
+ {modes.map((mode) => ( +
+
+
{mode.icon}
+
+
{mode.name}
+
{mode.sub}
+
+
{mode.desc}
+
+
+ {mode.players} IN QUEUE +
+ +
+
+ ))} +
+
+
+ + {/* Rankings */} +
+
+
+

+ GLOBAL RANKING. +

+
+ Season 7 · Top 100 +
+
+
+ {/* Header */} +
+ {['RANK', 'PLAYER', 'SCORE', 'WINS', 'K/D/A'].map((h) => ( +
{h}
+ ))} +
+ {rankings.map((r, i) => ( +
+
+ {r.rank < 10 ? `0${r.rank}` : r.rank} +
+
+
+ {r.name} +
+
{r.tier}
+
+
{r.score.toLocaleString()}
+
+ {r.wins} +
+
+ {r.kda} +
+
+ ))} +
+
+
+ + {/* Footer */} +
+
NEXUS ARENA
+
+ © 2024 NEXUS ARENA STUDIOS. ALL RIGHTS RESERVED. +
+
+ {['Twitter', 'Discord', 'YouTube'].map((s) => ( + {s} + ))} +
+
+
+ ); +} diff --git a/app/services/website/samples/portfolio/page.tsx b/app/services/website/samples/portfolio/page.tsx new file mode 100644 index 0000000..1c4ebf9 --- /dev/null +++ b/app/services/website/samples/portfolio/page.tsx @@ -0,0 +1,345 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; + +export default function PortfolioSample() { + const [hoveredProject, setHoveredProject] = useState(null); + + const skills = [ + { name: 'Figma', level: 98 }, + { name: 'React', level: 90 }, + { name: 'TypeScript', level: 85 }, + { name: 'After Effects', level: 88 }, + { name: 'Three.js', level: 75 }, + { name: 'Framer', level: 92 }, + ]; + + const projects = [ + { title: 'NEON CITY UI', desc: '사이버펑크 게임 인터페이스', gradient: 'linear-gradient(135deg, #00ff88, #00d4ff)', tag: 'UI/UX' }, + { title: 'FLOW BRAND', desc: '핀테크 스타트업 브랜딩', gradient: 'linear-gradient(135deg, #a855f7, #ec4899)', tag: 'Branding' }, + { title: 'ORBITAL APP', desc: '위성 추적 대시보드', gradient: 'linear-gradient(135deg, #3b82f6, #06b6d4)', tag: 'Web App' }, + { title: 'TERRA SHOP', desc: '친환경 D2C 쇼핑몰', gradient: 'linear-gradient(135deg, #22c55e, #84cc16)', tag: 'E-commerce' }, + { title: 'PULSE MOTION', desc: '모션 그래픽 패키지', gradient: 'linear-gradient(135deg, #f59e0b, #ef4444)', tag: 'Motion' }, + { title: 'AXIS SYSTEM', desc: '물류 관리 SaaS', gradient: 'linear-gradient(135deg, #64748b, #475569)', tag: 'Dashboard' }, + ]; + + const timeline = [ + { year: '2023', event: 'Google UX Design Certificate 취득', type: 'award' }, + { year: '2022', event: 'Awwwards SOTD 2회 수상', type: 'award' }, + { year: '2021', event: 'LINE Corp. UI 디자이너 입사', type: 'career' }, + { year: '2020', event: 'Hongik University 시각디자인과 졸업', type: 'edu' }, + { year: '2019', event: 'Adobe Design Award Korea 은상', type: 'award' }, + ]; + + return ( +
+ + + {/* Back Banner */} +
+ + ← 홈페이지 제작 서비스로 돌아가기 + + | + + SAMPLE · 개인 포트폴리오 + +
+ + {/* Navbar */} + + + {/* Hero */} +
+ {/* Scanline effect */} +
+ {/* Grid */} +
+ +
+
+ {'> Hello, World. I am'} +
+

+ Kim
+ Jisu + +

+
+ Product Designer & Creative Developer +
+

+ 픽셀 하나하나에 의미를 담는 디자이너. 아름다움과 기능의 교차점에서 디지털 경험을 설계합니다. +

+
+ + +
+ {['6+', '30+', '2x'].map((s, i) => ( +
+ {s} + {['YEARS', 'PROJECTS', 'AWWWARDS'][i]} +
+ ))} +
+
+
+
+ + {/* Projects */} +
+
+
+

+ Selected Work. +

+ + 2019 — 2024 + +
+
+ {projects.map((proj, i) => ( +
setHoveredProject(i)} + onMouseLeave={() => setHoveredProject(null)} + style={{ + border: '1px solid #111827', borderRadius: 12, overflow: 'hidden', + cursor: 'pointer', background: '#0a0a0a', + }} + > +
+
+ {proj.title} +
+
{proj.tag}
+
+
+
+ {proj.title} +
+
+ {proj.desc} +
+
+
+ ))} +
+
+
+ + {/* Skills */} +
+
+
+

+ Skills. +

+
+ {skills.map((s) => ( +
+
+ + {s.name} + + + {s.level}% + +
+
+
+
+
+ ))} +
+
+
+

+ Timeline. +

+
+
+ {timeline.map((t, i) => ( +
+
+
+ {t.year} +
+
+ {t.event} +
+
+ ))} +
+
+
+
+ + {/* Contact */} +
+
+ {'> LET\'S COLLABORATE'} +
+

+ Have a project? +

+

+ jisu.kim@design.studio · @jisu_creates +

+ +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 049fde6..53efcf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "jaengseung-made", "version": "0.1.0", "dependencies": { + "@anthropic-ai/sdk": "^0.79.0", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.99.0", "@tosspayments/tosspayments-sdk": "^2.6.0", @@ -44,6 +45,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.79.0.tgz", + "integrity": "sha512-ietmtM6glcnnrWq26H+BZm8J07iay9Cob6hRzDTr/A9QWF1m2T//TQhFO4MTKcZht2/7LS8bG9wUYEhcizKRnA==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -236,6 +257,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -5048,6 +5078,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7970,6 +8013,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", diff --git a/package.json b/package.json index 1e4a675..60e4735 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@anthropic-ai/sdk": "^0.79.0", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.99.0", "@tosspayments/tosspayments-sdk": "^2.6.0",