refactor(routes): 원본 25 파일 삭제 — Phase B에서 컨텐츠 이동 완료
Phase D 마무리: - app/services/music/* (page, layout, samples — 3 파일) - app/studio/page.tsx (1) - app/freelance/* (page, layout — 2) - app/services/website/* (page, layout, 8 samples — 10) - app/services/blog/* (page, layout — 2) - app/saju/* (page, layout, input, result, 2 sections, SajuForm — 7) 총 25 파일. next.config.ts redirects()로 외부 링크 보존 (영구 301). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '외주 개발 의뢰',
|
||||
description:
|
||||
'계약서 먼저, 납기 지키고, 소스코드 100% 인도. 47건 납품 완료. 현직 실무 엔지니어에게 외주 개발을 맡겨보세요. 납기 지연 시 하루 10만 원 패널티.',
|
||||
keywords: [
|
||||
'외주 개발',
|
||||
'프리랜서 개발자',
|
||||
'웹 개발 외주',
|
||||
'앱 개발 외주',
|
||||
'RPA 개발',
|
||||
'업무 자동화 외주',
|
||||
'소프트웨어 개발',
|
||||
],
|
||||
openGraph: {
|
||||
title: '외주 개발 의뢰 | 쟁승메이드',
|
||||
description:
|
||||
'47건 납품 완료. 계약서 먼저, 납기 패널티, 소스코드 100% 인도. 연락 두절 없는 개발자.',
|
||||
url: 'https://jaengseung-made.com/freelance',
|
||||
},
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function FreelanceLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,711 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ContactForm from '../components/ContactForm';
|
||||
|
||||
/* ─── Data ─── */
|
||||
const portfolio = [
|
||||
{
|
||||
title: '기업 브랜드 홈페이지',
|
||||
category: '웹사이트 제작 · Next.js',
|
||||
desc: '제조업체의 영업용 기업 소개 사이트. 서비스·연혁·팀 소개·문의 폼 포함. 모바일 반응형 및 SEO 최적화까지 포함하여 납품.',
|
||||
result: '납품 후 B2B 영업 미팅 시 "홈페이지 보고 연락했다" 비율 증가',
|
||||
tags: ['Next.js', 'Tailwind CSS', 'Vercel', 'SEO'],
|
||||
status: '납품 완료',
|
||||
statusType: 'done',
|
||||
priceRange: '50~200만원',
|
||||
accentColor: 'text-indigo-400',
|
||||
accentBg: 'bg-[#0d0a2e]',
|
||||
borderAccent: 'border-indigo-400/20',
|
||||
},
|
||||
{
|
||||
title: 'Gmail 자동화 RPA',
|
||||
category: 'RPA · 업무 자동화',
|
||||
desc: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림 전송하는 Gmail 자동화 시스템.',
|
||||
result: '이메일 처리 시간 일 2시간 → 10분 (의뢰인 직접 확인)',
|
||||
tags: ['Python', 'Gmail API', 'Google Apps Script'],
|
||||
status: '납품 완료',
|
||||
statusType: 'done',
|
||||
priceRange: '30~150만원',
|
||||
accentColor: 'text-red-400',
|
||||
accentBg: 'bg-[#200a0a]',
|
||||
borderAccent: 'border-red-400/20',
|
||||
},
|
||||
{
|
||||
title: '쇼핑몰 가격 모니터링 봇',
|
||||
category: '웹 스크래핑 · 알림 자동화',
|
||||
desc: '경쟁사 쇼핑몰의 특정 상품 가격을 매일 모니터링하여 변동 시 텔레그램으로 즉시 알림.',
|
||||
result: '경쟁사 10곳 · 상품 50개 매일 자동 추적, 수동 확인 0분',
|
||||
tags: ['Python', 'Selenium', 'Telegram Bot'],
|
||||
status: '납품 완료',
|
||||
statusType: 'done',
|
||||
priceRange: '30~150만원',
|
||||
accentColor: 'text-violet-400',
|
||||
accentBg: 'bg-[#0d0a2e]',
|
||||
borderAccent: 'border-violet-400/20',
|
||||
},
|
||||
{
|
||||
title: '영업 일보 자동화 시스템',
|
||||
category: '엑셀 자동화 · 보고서 생성',
|
||||
desc: '영업 데이터 엑셀 파일을 자동으로 집계하여 일별/주별/월별 영업 일보 PDF를 생성하고 이메일 발송.',
|
||||
result: '보고서 작성 3시간 → 5분, 매일 09:00 자동 발송',
|
||||
tags: ['Python', 'OpenPyXL', 'ReportLab'],
|
||||
status: '납품 완료',
|
||||
statusType: 'done',
|
||||
priceRange: '30~150만원',
|
||||
accentColor: 'text-cyan-400',
|
||||
accentBg: 'bg-[#012030]',
|
||||
borderAccent: 'border-cyan-400/20',
|
||||
},
|
||||
{
|
||||
title: '부동산 공시지가 수집 시스템',
|
||||
category: '공공 데이터 · API 연동',
|
||||
desc: '국토교통부 공공 API를 통해 특정 지역 공시지가를 주기적으로 수집·저장하고 변동 알림 제공.',
|
||||
result: '전국 3개 지역 공시지가 주 1회 자동 수집·변동 알림',
|
||||
tags: ['Python', '공공데이터 API', 'PostgreSQL', 'Telegram'],
|
||||
status: '납품 완료',
|
||||
statusType: 'done',
|
||||
priceRange: '30~150만원',
|
||||
accentColor: 'text-blue-400',
|
||||
accentBg: 'bg-[#04102b]',
|
||||
borderAccent: 'border-blue-400/20',
|
||||
},
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: '이서준',
|
||||
role: '온라인 쇼핑몰 운영자',
|
||||
project: '경쟁사 가격 모니터링 봇',
|
||||
content: '경쟁사 10곳 가격을 매일 수동으로 확인했는데 이제 텔레그램으로 자동 알림 받습니다. 납기도 정확히 지켜주셨고, 완료 후에도 작은 수정 요청에 빠르게 응답해주셔서 믿음이 갔습니다.',
|
||||
result: '가격 모니터링 시간 → 0분/일',
|
||||
accentColor: 'bg-emerald-500',
|
||||
borderColor: 'border-emerald-200',
|
||||
tagColor: 'text-emerald-700 bg-emerald-50 border-emerald-200',
|
||||
},
|
||||
{
|
||||
name: '박하은',
|
||||
role: '스타트업 운영팀장',
|
||||
project: 'Excel 보고서 자동화 시스템',
|
||||
content: '매주 월요일 아침 2시간씩 쓰던 Excel 집계 작업을 자동화했습니다. 처음엔 반신반의했는데 계약서부터 작성해주셔서 진짜 전문가구나 싶었고, 결과물도 기대 이상이었습니다.',
|
||||
result: '주간 보고 작업 2시간 → 5분',
|
||||
accentColor: 'bg-blue-500',
|
||||
borderColor: 'border-blue-200',
|
||||
tagColor: 'text-blue-700 bg-blue-50 border-blue-200',
|
||||
},
|
||||
{
|
||||
name: '김도윤',
|
||||
role: '프리랜서 디자이너',
|
||||
project: '포트폴리오 웹사이트 제작',
|
||||
content: '이전에 다른 개발자한테 맡겼다가 중간에 연락이 끊겼던 경험이 있어서 많이 걱정했는데, 주 1회 진행 보고를 꼬박꼬박 해주시고 최종 소스코드까지 전달해주셔서 정말 만족했습니다.',
|
||||
result: '2주 납품 약속 정확히 이행',
|
||||
accentColor: 'bg-violet-500',
|
||||
borderColor: 'border-violet-200',
|
||||
tagColor: 'text-violet-700 bg-violet-50 border-violet-200',
|
||||
},
|
||||
];
|
||||
|
||||
const process = [
|
||||
{
|
||||
num: '01',
|
||||
title: '무료 상담',
|
||||
desc: '전화 또는 이메일로 요구사항 파악 (30분 이내)',
|
||||
sub: '비용 없음 · 부담 없음',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
title: '견적 제안',
|
||||
desc: '개발 범위, 일정, 비용 상세 견적서 제공',
|
||||
sub: '1~3일 이내 발송',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
title: '계약 체결',
|
||||
desc: '계약서 작성 및 계약금(30%) 입금 후 개발 시작',
|
||||
sub: '계약서 포함 · 안전 거래',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: '04',
|
||||
title: '개발 진행',
|
||||
desc: '주 1회 이상 진행 상황 공유 및 중간 검수',
|
||||
sub: '투명한 진행 보고',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
num: '05',
|
||||
title: '최종 납품',
|
||||
desc: '완성본 인도 + 사용 교육 + 소스코드 전달',
|
||||
sub: '소스코드 전체 제공',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: '06',
|
||||
title: 'AS 지원',
|
||||
desc: '1개월 무상 기술 지원 및 평생 유지보수 가능',
|
||||
sub: '1개월 무상 + 평생 AS',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const guarantees = [
|
||||
{
|
||||
label: '계약서 필수',
|
||||
detail: '구두 약속 없음 — 착수 전 계약서 발송',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
accentText: 'text-sky-400',
|
||||
accentBorder: 'border-sky-400/20',
|
||||
},
|
||||
{
|
||||
label: '납기 지연 패널티',
|
||||
detail: '하루 지연 = 10만원 감면 — 그래서 안 늦습니다',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
accentText: 'text-amber-400',
|
||||
accentBorder: 'border-amber-400/20',
|
||||
},
|
||||
{
|
||||
label: '소스코드 100% 인도',
|
||||
detail: '납품 후 전체 소스코드 + 배포 가이드 제공',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
accentText: 'text-emerald-400',
|
||||
accentBorder: 'border-emerald-400/20',
|
||||
},
|
||||
{
|
||||
label: '1개월 무상 AS',
|
||||
detail: '납품 후 한 달 — 버그·수정 무상 대응',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
),
|
||||
accentText: 'text-violet-400',
|
||||
accentBorder: 'border-violet-400/20',
|
||||
},
|
||||
{
|
||||
label: '실시간 진행 현황',
|
||||
detail: '마이페이지에서 7단계 진행 상황 직접 확인',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
),
|
||||
accentText: 'text-cyan-400',
|
||||
accentBorder: 'border-cyan-400/20',
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Scroll Reveal ─── */
|
||||
function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
|
||||
/* ─── Main Page ─── */
|
||||
export default function FreelancePage() {
|
||||
const [_contactPreset] = useState('');
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="min-h-full bg-[#f0f5ff]">
|
||||
<style>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(1.5rem);
|
||||
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.reveal-d1 { transition-delay: 80ms; }
|
||||
.reveal-d2 { transition-delay: 160ms; }
|
||||
.reveal-d3 { transition-delay: 240ms; }
|
||||
.reveal-d4 { transition-delay: 320ms; }
|
||||
`}</style>
|
||||
|
||||
{/* ─── Hero ─── */}
|
||||
<div
|
||||
className="relative overflow-hidden bg-[#04102b] px-6 py-14 lg:px-12"
|
||||
style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px), repeating-linear-gradient(45deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}
|
||||
>
|
||||
<div className="relative max-w-5xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="inline-flex items-center gap-2 bg-emerald-400/10 border border-emerald-400/20 text-emerald-300 text-xs font-semibold px-4 py-2 rounded-full mb-5">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
현재 프로젝트 접수 가능
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-white tracking-tight leading-tight mb-4">
|
||||
연락 두절? 그런 거 없습니다.<br />
|
||||
<span className="text-[#5ba4ff]">납기 지키고, 끝까지 책임집니다</span>
|
||||
</h1>
|
||||
<p className="text-blue-200/60 text-base md:text-lg max-w-xl leading-relaxed mb-2">
|
||||
개발자에게 맡겼다가 연락 두절된 경험 있으신가요?<br />
|
||||
계약서 작성, 중간 보고, 소스코드 인도까지 — 단계마다 증거를 남깁니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Developer tag */}
|
||||
<div className="flex items-center gap-4 bg-white/5 border border-white/10 rounded-2xl px-6 py-3 mb-8 w-fit">
|
||||
<div className="w-10 h-10 rounded-full bg-[#1a56db] flex items-center justify-center text-white font-extrabold text-sm flex-shrink-0">
|
||||
박
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white font-bold text-sm">쟁토리 (박재오)</div>
|
||||
<div className="text-blue-300/50 text-xs">실무 엔지니어 · Python / Java / Next.js</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Python', 'Java', 'Next.js', 'Docker'].map(t => (
|
||||
<span key={t} className="bg-[#1a56db]/20 border border-[#1a56db]/30 text-[#5ba4ff] text-xs px-2 py-0.5 rounded-md font-mono">{t}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 보증 카드 4개 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
|
||||
{guarantees.map((g) => (
|
||||
<div key={g.label} className={`bg-[#04102b]/60 border ${g.accentBorder} rounded-xl p-4`}>
|
||||
<div className={`${g.accentText} mb-2`}>{g.icon}</div>
|
||||
<div className="text-white font-bold text-sm mb-1">{g.label}</div>
|
||||
<div className="text-blue-300/40 text-xs leading-relaxed">{g.detail}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 포트폴리오 ─── */}
|
||||
<div className="px-6 py-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PORTFOLIO</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">직접 개발한 프로젝트</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">실제 운영 중인 서비스와 납품 완료 프로젝트입니다</p>
|
||||
</div>
|
||||
|
||||
<div className="reveal grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{portfolio.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden hover:shadow-xl hover:shadow-blue-100 hover:-translate-y-1 transition-all duration-200 group"
|
||||
>
|
||||
{/* card header */}
|
||||
<div className={`px-5 pt-5 pb-8 ${item.accentBg}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className={`text-xs font-bold mb-2 uppercase tracking-wider ${item.accentColor}`}>{item.category}</div>
|
||||
<h3 className="text-white font-extrabold text-sm leading-snug">{item.title}</h3>
|
||||
</div>
|
||||
{item.statusType === 'live' ? (
|
||||
<div className="flex items-center gap-1.5 bg-emerald-400/20 border border-emerald-400/30 text-emerald-300 text-xs font-bold px-2.5 py-1 rounded-full flex-shrink-0 ml-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
운영 중
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 bg-blue-400/20 border border-blue-400/30 text-blue-300 text-xs font-bold px-2.5 py-1 rounded-full flex-shrink-0 ml-2">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
납품 완료
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* card body */}
|
||||
<div className="px-5 py-4 -mt-3 relative">
|
||||
<p className="text-slate-600 text-xs leading-relaxed mb-3">{item.desc}</p>
|
||||
{item.result && (
|
||||
<div className="flex items-start gap-1.5 bg-emerald-50 border border-emerald-200 rounded-lg px-3 py-2 mb-3">
|
||||
<svg className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-emerald-700 text-xs font-semibold leading-snug">{item.result}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{item.tags.map((tag) => (
|
||||
<span key={tag} className="bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] text-xs font-mono px-2 py-0.5 rounded-md">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100">
|
||||
<span className="text-xs text-blue-600 font-semibold bg-blue-50 px-2 py-0.5 rounded-full">{item.priceRange}</span>
|
||||
<a href="#contact-form" className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 font-medium transition">
|
||||
비슷한 서비스 의뢰하기 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 추가 문구 */}
|
||||
<div className="reveal mt-6 text-center">
|
||||
<p className="text-slate-400 text-sm">
|
||||
위 프로젝트 외에도 다양한 프로젝트 경험이 있습니다 ·{' '}
|
||||
<a href="mailto:bgg8988@gmail.com" className="text-[#1a56db] hover:underline font-medium">포트폴리오 전체 요청</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 고객 후기 ─── */}
|
||||
<div className="px-6 pb-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">REVIEWS</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">실제 의뢰인 후기</h2>
|
||||
<p className="text-slate-500 text-sm mt-2" style={{ wordBreak: 'keep-all' }}>숫자보다 실제 말이 더 정직합니다</p>
|
||||
</div>
|
||||
|
||||
<div className="reveal grid sm:grid-cols-2 md:grid-cols-3 gap-5">
|
||||
{testimonials.map((t) => (
|
||||
<div
|
||||
key={t.name}
|
||||
className={`bg-white rounded-2xl border-2 ${t.borderColor} p-6 flex flex-col hover:shadow-lg hover:-translate-y-0.5`}
|
||||
style={{ transition: 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
||||
>
|
||||
{/* 별점 */}
|
||||
<div className="flex items-center gap-0.5 mb-4">
|
||||
{[1,2,3,4,5].map((n) => (
|
||||
<svg key={n} className="w-4 h-4 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 후기 내용 */}
|
||||
<p className="text-slate-600 text-sm leading-relaxed flex-1 mb-5" style={{ wordBreak: 'keep-all' }}>
|
||||
“{t.content}”
|
||||
</p>
|
||||
|
||||
{/* 결과 뱃지 */}
|
||||
<div className={`text-xs font-bold px-3 py-1.5 rounded-lg border mb-4 ${t.tagColor}`} style={{ wordBreak: 'keep-all' }}>
|
||||
✓ {t.result}
|
||||
</div>
|
||||
|
||||
{/* 의뢰인 */}
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-slate-100">
|
||||
<div className={`w-9 h-9 rounded-full ${t.accentColor} flex items-center justify-center text-white font-extrabold text-sm flex-shrink-0`}>
|
||||
{t.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-[#04102b] text-sm">{t.name}</div>
|
||||
<div className="text-slate-400 text-xs">{t.role} · {t.project}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-slate-400 text-xs mt-5">
|
||||
* 의뢰인 동의 하에 게시된 후기입니다. 전체 대화 내역 공개 요청 시 제공 가능합니다.
|
||||
</p>
|
||||
|
||||
<div className="reveal text-center py-6">
|
||||
<a href="#contact-form" className="inline-flex items-center gap-2 px-6 py-3 bg-[#1a56db] text-white font-semibold rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||
무료 상담 시작하기
|
||||
</a>
|
||||
<p className="text-sm text-slate-400 mt-2">24시간 내 답변 · 상담은 무료입니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 진행 프로세스 ─── */}
|
||||
<div className="px-6 pb-12 lg:px-12">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="reveal text-center mb-10">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PROCESS</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">진행 프로세스</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">투명하고 체계적인 6단계로 진행됩니다</p>
|
||||
</div>
|
||||
|
||||
{/* Vertical timeline */}
|
||||
<div className="reveal relative">
|
||||
{/* connecting line */}
|
||||
<div className="absolute left-6 top-6 bottom-6 w-px bg-[#dbe8ff]" />
|
||||
|
||||
<div className="space-y-4">
|
||||
{process.map((p) => (
|
||||
<div key={p.num} className="relative flex gap-5">
|
||||
{/* step circle */}
|
||||
<div className={`relative z-10 w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg ${
|
||||
p.highlight
|
||||
? 'bg-[#1a56db] shadow-blue-500/30 border border-[#1a56db]/50'
|
||||
: 'bg-white border-2 border-[#dbe8ff]'
|
||||
}`}>
|
||||
<span className={p.highlight ? 'text-white' : 'text-[#1a56db]'}>{p.icon}</span>
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div
|
||||
className={`flex-1 rounded-2xl border p-5 mb-0 ${
|
||||
p.highlight
|
||||
? 'border-[#1a56db]/40'
|
||||
: 'bg-white border-[#dbe8ff]'
|
||||
}`}
|
||||
style={p.highlight ? {
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||
} : {}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`text-xs font-bold font-mono ${p.highlight ? 'text-[#5ba4ff]' : 'text-slate-400'}`}>STEP {p.num}</span>
|
||||
{p.highlight && (
|
||||
<span className="bg-[#1a56db]/30 border border-[#1a56db]/40 text-[#5ba4ff] text-xs font-bold px-2 py-0.5 rounded-md">현재 진행</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className={`font-extrabold text-sm mb-1 ${p.highlight ? 'text-white' : 'text-[#04102b]'}`}>{p.title}</h3>
|
||||
<p className={`text-xs leading-relaxed ${p.highlight ? 'text-blue-200/60' : 'text-slate-500'}`}>{p.desc}</p>
|
||||
</div>
|
||||
<div className={`text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap flex-shrink-0 ${
|
||||
p.highlight
|
||||
? 'bg-[#1a56db]/30 text-[#5ba4ff]'
|
||||
: 'bg-[#f0f5ff] text-[#1a56db] border border-[#dbe8ff]'
|
||||
}`}>
|
||||
{p.sub}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 기술 스택 & 신뢰 ─── */}
|
||||
<div className="px-6 pb-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-5">
|
||||
|
||||
{/* Tech Stack */}
|
||||
<div className="reveal reveal-d1 bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-1 h-5 bg-[#1a56db] rounded-full" />
|
||||
<h3 className="font-bold text-[#04102b] text-sm">개발 가능 기술 스택</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: 'Backend', techs: ['Python', 'Java', 'Spring Boot', 'FastAPI', 'Node.js'] },
|
||||
{ label: 'Frontend', techs: ['Next.js', 'React', 'TypeScript', 'Tailwind CSS'] },
|
||||
{ label: 'Database', techs: ['PostgreSQL', 'MySQL', 'Redis', 'SQLite'] },
|
||||
{ label: 'Infra / API', techs: ['Docker', 'AWS', 'Telegram API', '공공 API'] },
|
||||
].map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="text-xs font-bold text-slate-400 mb-1.5 uppercase tracking-wider">{group.label}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{group.techs.map((t) => (
|
||||
<span key={t} className="bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] text-xs font-mono px-2.5 py-1 rounded-lg">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 신뢰 포인트 */}
|
||||
<div
|
||||
className="reveal reveal-d2 rounded-2xl border border-[#1a3a7a] p-6"
|
||||
style={{
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-1 h-5 bg-[#5ba4ff] rounded-full" />
|
||||
<h3 className="font-bold text-white text-sm">신뢰할 수 있는 이유</h3>
|
||||
</div>
|
||||
<ul className="space-y-3.5">
|
||||
{[
|
||||
{
|
||||
title: '지금 URL로 직접 확인',
|
||||
desc: 'jaengseung-made.com — 로또 분석, 주식 자동매매 지금도 운영 중',
|
||||
icon: (
|
||||
<svg className="w-4 h-4 text-[#5ba4ff] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '계약서 먼저, 개발 나중',
|
||||
desc: '구두 약속 없음 — 견적서·계약서 발송 후 착수',
|
||||
icon: (
|
||||
<svg className="w-4 h-4 text-[#5ba4ff] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '납품 전 전액 환불 보장',
|
||||
desc: '마음에 안 드시면 이유 불문 전액 환불',
|
||||
icon: (
|
||||
<svg className="w-4 h-4 text-[#5ba4ff] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '소스코드 100% 인도',
|
||||
desc: '완성 후 전체 소스코드 + 배포 가이드 제공',
|
||||
icon: (
|
||||
<svg className="w-4 h-4 text-[#5ba4ff] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '납기 지연 시 패널티',
|
||||
desc: '하루 늦을 때마다 10만원 감면 — 그래서 안 늦습니다',
|
||||
icon: (
|
||||
<svg className="w-4 h-4 text-[#5ba4ff] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
].map((item) => (
|
||||
<li key={item.title} className="flex items-start gap-3">
|
||||
{item.icon}
|
||||
<div>
|
||||
<div className="text-white text-sm font-bold">{item.title}</div>
|
||||
<div className="text-blue-300/50 text-xs">{item.desc}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 문의 폼 ─── */}
|
||||
<div id="contact-form" className="px-6 pb-14 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">CONTACT</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">프로젝트 문의</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.</p>
|
||||
</div>
|
||||
|
||||
<div className="reveal grid md:grid-cols-5 gap-6">
|
||||
{/* 왼쪽: 간단 안내 */}
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-5">
|
||||
<h3 className="font-bold text-[#04102b] text-sm mb-4">문의 전 체크리스트</h3>
|
||||
<ul className="space-y-2.5">
|
||||
{[
|
||||
'어떤 업무를 자동화/개발하고 싶은지',
|
||||
'현재 사용 중인 시스템 (엑셀, ERP 등)',
|
||||
'희망하는 완성 일정',
|
||||
'예산 범위 (대략적으로도 OK)',
|
||||
].map((item, i) => (
|
||||
<li key={item} className="flex items-start gap-2.5 text-xs text-slate-600">
|
||||
<span className="w-5 h-5 rounded-full bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] font-bold text-xs flex items-center justify-center flex-shrink-0">{i + 1}</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-5">
|
||||
<h3 className="font-bold text-[#04102b] text-sm mb-3">직접 연락</h3>
|
||||
<div className="space-y-2.5">
|
||||
<a href="mailto:bgg8988@gmail.com" className="flex items-center gap-2.5 text-sm text-slate-600 hover:text-[#1a56db] transition group">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center group-hover:bg-[#1a56db] group-hover:border-[#1a56db] transition">
|
||||
<svg className="w-4 h-4 text-[#1a56db] group-hover:text-white transition" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
bgg8988@gmail.com
|
||||
</a>
|
||||
<a href="tel:010-3907-1392" className="flex items-center gap-2.5 text-sm text-slate-600 hover:text-[#1a56db] transition group">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center group-hover:bg-[#1a56db] group-hover:border-[#1a56db] transition">
|
||||
<svg className="w-4 h-4 text-[#1a56db] group-hover:text-white transition" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
010-3907-1392
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-2xl border border-[#1a3a7a] p-5 text-center"
|
||||
style={{ background: '#04102b' }}
|
||||
>
|
||||
<div className="text-2xl font-extrabold text-white mb-0.5">24h</div>
|
||||
<div className="text-[#5ba4ff] text-xs font-bold mb-1">이내 답변 보장</div>
|
||||
<div className="text-blue-300/40 text-xs">영업일 기준 · 주말 포함</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 폼 */}
|
||||
<div className="md:col-span-3 bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { lunarToSolar } from '@/lib/lunar-utils';
|
||||
|
||||
export default function SajuForm() {
|
||||
const router = useRouter();
|
||||
const [year, setYear] = useState('');
|
||||
const [month, setMonth] = useState('');
|
||||
const [day, setDay] = useState('');
|
||||
const [hour, setHour] = useState('');
|
||||
const [calendarType, setCalendarType] = useState<'solar' | 'lunar'>('solar');
|
||||
const [gender, setGender] = useState<'male' | 'female'>('male');
|
||||
const [isLeapMonth, setIsLeapMonth] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!year || !month || !day) {
|
||||
alert('생년월일을 모두 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
let finalYear = year;
|
||||
let finalMonth = month;
|
||||
let finalDay = day;
|
||||
|
||||
// 음력인 경우 양력으로 변환
|
||||
if (calendarType === 'lunar') {
|
||||
const solar = lunarToSolar(
|
||||
parseInt(year),
|
||||
parseInt(month),
|
||||
parseInt(day),
|
||||
isLeapMonth
|
||||
);
|
||||
finalYear = solar.year.toString();
|
||||
finalMonth = solar.month.toString();
|
||||
finalDay = solar.day.toString();
|
||||
}
|
||||
|
||||
// URL 파라미터로 전달
|
||||
const params = new URLSearchParams({
|
||||
year: finalYear,
|
||||
month: finalMonth,
|
||||
day: finalDay,
|
||||
gender,
|
||||
calendarType,
|
||||
originalYear: year,
|
||||
originalMonth: month,
|
||||
originalDay: day,
|
||||
});
|
||||
|
||||
if (hour) {
|
||||
params.append('hour', hour);
|
||||
}
|
||||
|
||||
if (calendarType === 'lunar') {
|
||||
params.append('isLeapMonth', isLeapMonth.toString());
|
||||
}
|
||||
|
||||
router.push(`/saju/result?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* 생년월일 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||
생년월일
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="년 (예: 1990)"
|
||||
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||
min="1900"
|
||||
max="2100"
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="월 (1-12)"
|
||||
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||
min="1"
|
||||
max="12"
|
||||
value={month}
|
||||
onChange={(e) => setMonth(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="일 (1-31)"
|
||||
className="px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||
min="1"
|
||||
max="31"
|
||||
value={day}
|
||||
onChange={(e) => setDay(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 태어난 시간 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||
태어난 시간 (선택)
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-4 py-3 border-2 border-[#dbe8ff] rounded-xl focus:border-[#1a56db] focus:outline-none transition bg-white text-[#04102b]"
|
||||
value={hour}
|
||||
onChange={(e) => setHour(e.target.value)}
|
||||
>
|
||||
<option value="">모름 / 시간 선택 안함</option>
|
||||
<option value="0">자시 (子時) 23:00 - 01:00</option>
|
||||
<option value="1">축시 (丑時) 01:00 - 03:00</option>
|
||||
<option value="3">인시 (寅時) 03:00 - 05:00</option>
|
||||
<option value="5">묘시 (卯時) 05:00 - 07:00</option>
|
||||
<option value="7">진시 (辰時) 07:00 - 09:00</option>
|
||||
<option value="9">사시 (巳時) 09:00 - 11:00</option>
|
||||
<option value="11">오시 (午時) 11:00 - 13:00</option>
|
||||
<option value="13">미시 (未時) 13:00 - 15:00</option>
|
||||
<option value="15">신시 (申時) 15:00 - 17:00</option>
|
||||
<option value="17">유시 (酉時) 17:00 - 19:00</option>
|
||||
<option value="19">술시 (戌時) 19:00 - 21:00</option>
|
||||
<option value="21">해시 (亥時) 21:00 - 23:00</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 양력/음력 선택 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||
생일 구분
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCalendarType('solar')}
|
||||
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||
calendarType === 'solar'
|
||||
? 'bg-[#1a56db] text-white shadow-lg'
|
||||
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||
}`}
|
||||
>
|
||||
양력
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCalendarType('lunar')}
|
||||
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||
calendarType === 'lunar'
|
||||
? 'bg-[#1a56db] text-white shadow-lg'
|
||||
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||
}`}
|
||||
>
|
||||
음력
|
||||
</button>
|
||||
</div>
|
||||
{calendarType === 'lunar' && (
|
||||
<div className="mt-3">
|
||||
<label className="flex items-center justify-center gap-2 text-sm text-slate-500 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isLeapMonth}
|
||||
onChange={(e) => setIsLeapMonth(e.target.checked)}
|
||||
className="w-4 h-4 text-[#1a56db] border-gray-300 rounded focus:ring-[#1a56db]"
|
||||
/>
|
||||
<span>윤달</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 성별 선택 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-bold text-[#04102b] mb-3">
|
||||
성별
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender('male')}
|
||||
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||
gender === 'male'
|
||||
? 'bg-[#1a56db] text-white shadow-lg'
|
||||
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||
}`}
|
||||
>
|
||||
남성
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender('female')}
|
||||
className={`px-6 py-3 rounded-xl font-bold transition ${
|
||||
gender === 'female'
|
||||
? 'bg-[#1a56db] text-white shadow-lg'
|
||||
: 'bg-white border-2 border-[#dbe8ff] text-[#04102b] hover:border-[#1a56db]'
|
||||
}`}
|
||||
>
|
||||
여성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 제출 버튼 */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-[#1a56db] to-[#7c3aed] hover:from-[#1e4fc2] hover:to-[#6d28d9] text-white py-4 rounded-xl text-lg font-bold transition shadow-lg hover:shadow-xl hover:scale-[1.02]"
|
||||
>
|
||||
내 사주 보기 →
|
||||
</button>
|
||||
|
||||
<p className="text-sm text-slate-500 text-center">
|
||||
* 태어난 시간을 정확히 아시면 더 정확한 사주를 확인할 수 있습니다.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import SajuForm from '../components/SajuForm';
|
||||
|
||||
export default function SajuInputPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
{/* Hero */}
|
||||
<div className="relative overflow-hidden px-6 py-12"
|
||||
style={{ background: '#04102b', backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}>
|
||||
|
||||
<div className="relative max-w-xl mx-auto text-center">
|
||||
<div className="inline-flex items-center gap-2 bg-violet-400/10 border border-violet-400/25 text-violet-300 text-xs font-semibold px-4 py-1.5 rounded-full mb-4 tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||
AI 사주 분석 · 생년월일 입력
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-extrabold text-white leading-tight mb-3 tracking-tight">
|
||||
생년월일을 입력해주세요
|
||||
</h1>
|
||||
<p className="text-blue-200/60 text-sm leading-relaxed">
|
||||
정확한 생년월일과 태어난 시간을 입력하면<br />
|
||||
더 정밀한 사주팔자를 계산할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form 영역 */}
|
||||
<div className="px-6 py-10 max-w-2xl mx-auto">
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-8 shadow-lg">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div className="w-1 h-5 bg-gradient-to-b from-[#1a56db] to-[#7c3aed] rounded-full" />
|
||||
<h2 className="font-bold text-[#04102b] text-base">기본 정보 입력</h2>
|
||||
</div>
|
||||
<SajuForm />
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-slate-400 mt-6">
|
||||
입력하신 정보는 사주 계산에만 사용되며 별도로 저장되지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI 사주 분석',
|
||||
description:
|
||||
'생년월일시를 입력하면 Gemini AI가 사주팔자를 분석합니다. 일간·오행·대운·세운 기반 12개 항목 상세 해석. 재물운·애정운·직업·건강 포함.',
|
||||
keywords: [
|
||||
'AI 사주',
|
||||
'사주풀이',
|
||||
'사주팔자',
|
||||
'사주 분석',
|
||||
'오행 분석',
|
||||
'대운',
|
||||
'세운',
|
||||
'사주 운세',
|
||||
],
|
||||
openGraph: {
|
||||
title: 'AI 사주 분석 | 쟁승메이드',
|
||||
description:
|
||||
'Gemini AI 기반 사주팔자 분석. 일간·오행·대운·세운·재물운·애정운 12개 항목 해석.',
|
||||
url: 'https://jaengseung-made.com/saju',
|
||||
},
|
||||
};
|
||||
|
||||
export default function SajuLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PaymentButton from '@/app/components/PaymentButton';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
q: '사주팔자란 무엇인가요?',
|
||||
a: '사주팔자(四柱八字)는 태어난 년·월·일·시의 네 기둥(四柱)에 각각 천간과 지지 두 글자씩 총 여덟 글자(八字)로 이루어진 동양의 전통 운명 분석 체계입니다.',
|
||||
},
|
||||
{
|
||||
q: 'AI 해석은 어떻게 동작하나요?',
|
||||
a: '전통 명리학 계산 로직(오행, 신강/신약, 용신/희신 등)으로 산출된 데이터를 Gemini AI에 전달하여 12개 항목의 상세 해석을 생성합니다. 현재 기본 원국 분석과 AI 상세 해석 모두 무료로 제공됩니다.',
|
||||
},
|
||||
{
|
||||
q: '태어난 시간을 모르면 어떻게 하나요?',
|
||||
a: '시간을 모르더라도 년·월·일 세 기둥(三柱)만으로 사주를 계산할 수 있습니다. 다만 시주가 빠지면 세부 분석 정확도가 다소 낮아집니다.',
|
||||
},
|
||||
{
|
||||
q: '음력으로 입력할 수 있나요?',
|
||||
a: '네, 양력과 음력 모두 지원합니다. 음력을 선택하면 내부적으로 양력으로 변환하여 정확한 사주를 계산합니다. 윤달도 별도 선택이 가능합니다.',
|
||||
},
|
||||
];
|
||||
|
||||
interface SajuRecord {
|
||||
id: number;
|
||||
created_at: string;
|
||||
saju_data: {
|
||||
birth_year: number;
|
||||
birth_month: number;
|
||||
birth_day: number;
|
||||
birth_hour?: number;
|
||||
gender: string;
|
||||
};
|
||||
interpretation: string | null;
|
||||
is_paid: boolean;
|
||||
}
|
||||
|
||||
function buildResultUrl(rec: SajuRecord) {
|
||||
const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data;
|
||||
if (!birth_year || !birth_month || !birth_day) return '/saju/input';
|
||||
let url = `/saju/result?year=${birth_year}&month=${birth_month}&day=${birth_day}&gender=${gender}&calendarType=solar`;
|
||||
if (birth_hour != null) url += `&hour=${birth_hour}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
export default function SajuPage() {
|
||||
const supabase = createClient();
|
||||
const [paidRecords, setPaidRecords] = useState<SajuRecord[]>([]);
|
||||
const [hasPaid, setHasPaid] = useState(false);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRecords() {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) { setAuthChecked(true); return; }
|
||||
|
||||
const { data: records } = await supabase
|
||||
.from('saju_records')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.eq('is_paid', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(2);
|
||||
|
||||
if (records && records.length > 0) {
|
||||
setPaidRecords(records);
|
||||
setHasPaid(true);
|
||||
}
|
||||
setAuthChecked(true);
|
||||
}
|
||||
fetchRecords();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
{/* ─── Hero ─── */}
|
||||
<div className="relative overflow-hidden bg-[#04102b] px-6 py-14 lg:px-12" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
|
||||
|
||||
<div className="relative max-w-3xl mx-auto">
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||
<span className="text-violet-300/70 text-xs font-mono tracking-widest uppercase">전통 명리학 × AI 해석 · 무료</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-white leading-tight mb-5 tracking-tight">
|
||||
AI가 분석하는<br />
|
||||
<span className="text-amber-400">사주팔자</span>
|
||||
</h1>
|
||||
<p className="text-blue-200/70 text-base md:text-lg leading-relaxed mb-8 max-w-xl mx-auto">
|
||||
수천 년의 동양 명리학과 최신 AI 기술의 만남.<br />
|
||||
태어난 순간의 우주적 에너지를 12가지 항목으로 해석해드립니다.
|
||||
</p>
|
||||
|
||||
{/* 이전 기록 있으면 분기 버튼, 없으면 단일 CTA */}
|
||||
{authChecked && hasPaid ? (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-7 py-3.5 rounded-xl font-semibold text-base transition-all"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
새로 보기
|
||||
</Link>
|
||||
<a
|
||||
href="#past-records"
|
||||
className="inline-flex items-center gap-2 bg-white/10 border border-white/20 text-white px-7 py-3.5 rounded-xl font-semibold text-base transition-all hover:bg-white/20"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
이전 내역 다시 보기
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#1a56db] to-[#7c3aed] hover:from-[#1e4fc2] hover:to-[#6d28d9] text-white px-8 py-3.5 rounded-xl font-semibold text-base transition-all shadow-lg shadow-violet-900/40"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
지금 바로 시작하기
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-12 lg:px-12">
|
||||
<div className="max-w-4xl mx-auto space-y-10">
|
||||
|
||||
{/* ─── 이전 기록 섹션 (구매한 유저만) ─── */}
|
||||
{hasPaid && paidRecords.length > 0 && (
|
||||
<div id="past-records">
|
||||
<div className="text-center mb-6">
|
||||
<p className="text-violet-600 text-xs font-bold uppercase tracking-widest mb-2">MY RECORDS</p>
|
||||
<h2 className="text-2xl font-extrabold text-[#04102b]">이전 AI 사주 기록</h2>
|
||||
<p className="text-slate-500 text-sm mt-1">결제한 사주 기록을 다시 확인하세요</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{paidRecords.map((rec) => (
|
||||
<div key={rec.id} className="bg-white rounded-2xl border border-[#dbe8ff] p-5 hover:border-violet-300 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 mb-1">
|
||||
{new Date(rec.created_at).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
</div>
|
||||
<div className="font-bold text-[#04102b] text-base">
|
||||
{rec.saju_data.birth_year ?? '?'}년{' '}
|
||||
{rec.saju_data.birth_month ?? '?'}월{' '}
|
||||
{rec.saju_data.birth_day ?? '?'}일생
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 mt-0.5">
|
||||
{rec.saju_data.gender === 'male' ? '남성' : '여성'}
|
||||
{rec.saju_data.birth_hour != null ? ` · ${rec.saju_data.birth_hour}시생` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-bold px-2 py-1 rounded-lg bg-amber-50 text-amber-600 border border-amber-200 flex-shrink-0">
|
||||
AI 해석 완료
|
||||
</span>
|
||||
</div>
|
||||
{rec.interpretation && (
|
||||
<p className="text-xs text-slate-500 bg-slate-50 rounded-lg px-3 py-2 mb-3 line-clamp-2">
|
||||
{rec.interpretation.replace(/[#*]/g, '').substring(0, 80)}...
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
href={buildResultUrl(rec)}
|
||||
className="block w-full text-center py-2 rounded-xl text-sm font-bold bg-[#04102b] hover:bg-[#0a1f5c] text-white border border-[#1a3a7a] transition"
|
||||
>
|
||||
다시 보기 →
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── 바로 시작하기 CTA ─── */}
|
||||
<div
|
||||
className="rounded-2xl border border-[#1a3a7a] p-8 text-center"
|
||||
style={{
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-2xl font-extrabold text-white mb-2">지금 무료로 시작하세요</h3>
|
||||
<p className="text-blue-200/60 text-sm mb-6">회원가입 없이, 생년월일만 입력하면 바로 확인 가능합니다</p>
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="inline-flex items-center gap-2 bg-amber-400 hover:bg-amber-300 text-[#04102b] px-8 py-3.5 rounded-xl font-bold text-base transition-all"
|
||||
>
|
||||
사주 입력하러 가기 →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* ─── 무료 vs 유료 비교표 ─── */}
|
||||
<div>
|
||||
<div className="text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PRICING</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b] tracking-tight">무엇을 분석해드리나요</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">기본 원국은 무료, AI 상세 해석은 1,000원</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 무료 */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="w-10 h-10 rounded-xl bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-[#1a56db]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-slate-500 uppercase tracking-wide">FREE</div>
|
||||
<div className="text-lg font-extrabold text-[#04102b]">무료 기본 분석</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
'사주팔자 원국 (년·월·일·시주)',
|
||||
'천간·지지·지장간 표',
|
||||
'십성 및 십이운성',
|
||||
'오행 분포 차트',
|
||||
'지지 상호작용 (합·충·형)',
|
||||
'일간 분석 요약',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2.5 text-sm text-slate-700">
|
||||
<div className="w-4 h-4 rounded-full bg-blue-100 border border-blue-200 flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#1a56db]" />
|
||||
</div>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-6 pt-5 border-t border-slate-100">
|
||||
<div className="text-2xl font-extrabold text-[#04102b]">무료</div>
|
||||
<div className="text-xs text-slate-500 mt-1">회원가입 불필요</div>
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="mt-4 block w-full text-center py-2.5 rounded-xl text-sm font-bold bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] hover:bg-blue-50 transition"
|
||||
>
|
||||
무료로 시작하기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 해석 (현재 무료) */}
|
||||
<div
|
||||
className="rounded-2xl border border-[#1a3a7a] p-6 shadow-lg relative overflow-hidden"
|
||||
style={{
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-4 right-4 bg-amber-400 text-[#04102b] text-xs font-bold px-2 py-0.5 rounded-lg">
|
||||
1,000원
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-5 relative">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/20 border border-violet-400/30 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-violet-300 uppercase tracking-wide">AI PREMIUM</div>
|
||||
<div className="text-lg font-extrabold text-white">AI 상세 해석</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-3 relative">
|
||||
{[
|
||||
'무료 기본 분석 전체 포함',
|
||||
'신강/신약 정밀 판단',
|
||||
'용신·희신·기신 추정',
|
||||
'대운 (10년 주기) 분석',
|
||||
'올해 세운 흐름',
|
||||
'Gemini 2.5 Pro AI 12가지 상세 해석',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2.5 text-sm text-blue-200">
|
||||
<div className="w-4 h-4 rounded-full bg-amber-400/20 border border-amber-400/40 flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
||||
</div>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-6 pt-5 border-t border-white/10 relative">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className="text-2xl font-extrabold text-white">1,000원</span>
|
||||
<span className="text-xs text-blue-300/50">/ 1회</span>
|
||||
</div>
|
||||
<div className="text-xs text-blue-300/70 mt-1 mb-4">로그인 후 결제 · 12가지 항목 AI 해석</div>
|
||||
<Link
|
||||
href="/saju/input"
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold transition bg-amber-400 text-[#04102b] hover:bg-amber-300"
|
||||
>
|
||||
사주 분석 시작하기 →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── FAQ ─── */}
|
||||
<div>
|
||||
<div className="text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">FAQ</p>
|
||||
<h2 className="text-2xl font-extrabold text-[#04102b]">자주 묻는 질문</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{faqItems.map((item, i) => (
|
||||
<div key={i} className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-[#f0f5ff] border border-[#dbe8ff] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-[#1a56db] text-xs font-bold">Q</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-[#04102b] text-sm mb-2">{item.q}</p>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{item.a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import PaymentButton from '@/app/components/PaymentButton';
|
||||
|
||||
interface BirthKey {
|
||||
birth_year: number;
|
||||
birth_month: number;
|
||||
birth_day: number;
|
||||
birth_hour?: number;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
interface SajuAISectionProps {
|
||||
hasPaid: boolean;
|
||||
savedInterpretation: string | null;
|
||||
sajuData: object;
|
||||
daeun: object | null;
|
||||
daeunList: object[];
|
||||
gender: string;
|
||||
birthKey: BirthKey;
|
||||
currentUrl: string;
|
||||
engineData?: {
|
||||
interactions?: any[];
|
||||
shinsal?: any[];
|
||||
gongmang?: any;
|
||||
hiddenStems?: any[];
|
||||
};
|
||||
}
|
||||
|
||||
// ── 섹션별 메타 (아이콘·색상) ──────────────────────────────────────────
|
||||
const SECTION_META: {
|
||||
icon: string;
|
||||
gradient: string;
|
||||
border: string;
|
||||
badge: string;
|
||||
badgeText: string;
|
||||
}[] = [
|
||||
{ icon: '🌟', gradient: 'from-violet-500 to-purple-600', border: 'border-violet-100', badge: 'bg-violet-50 border-violet-200 text-violet-700', badgeText: '기질' },
|
||||
{ icon: '⚖️', gradient: 'from-emerald-500 to-teal-600', border: 'border-emerald-100', badge: 'bg-emerald-50 border-emerald-200 text-emerald-700', badgeText: '오행' },
|
||||
{ icon: '🔗', gradient: 'from-blue-500 to-indigo-600', border: 'border-blue-100', badge: 'bg-blue-50 border-blue-200 text-blue-700', badgeText: '지지' },
|
||||
{ icon: '✨', gradient: 'from-amber-500 to-orange-500', border: 'border-amber-100', badge: 'bg-amber-50 border-amber-200 text-amber-700', badgeText: '신살' },
|
||||
{ icon: '💰', gradient: 'from-yellow-500 to-amber-600', border: 'border-yellow-100', badge: 'bg-yellow-50 border-yellow-200 text-yellow-700', badgeText: '재물' },
|
||||
{ icon: '🎯', gradient: 'from-rose-500 to-pink-600', border: 'border-rose-100', badge: 'bg-rose-50 border-rose-200 text-rose-700', badgeText: '직업' },
|
||||
{ icon: '💕', gradient: 'from-pink-500 to-rose-500', border: 'border-pink-100', badge: 'bg-pink-50 border-pink-200 text-pink-700', badgeText: '애정' },
|
||||
{ icon: '🌿', gradient: 'from-green-500 to-emerald-600', border: 'border-green-100', badge: 'bg-green-50 border-green-200 text-green-700', badgeText: '건강' },
|
||||
{ icon: '🗺️', gradient: 'from-cyan-500 to-blue-600', border: 'border-cyan-100', badge: 'bg-cyan-50 border-cyan-200 text-cyan-700', badgeText: '대운' },
|
||||
{ icon: '📅', gradient: 'from-indigo-500 to-violet-600', border: 'border-indigo-100', badge: 'bg-indigo-50 border-indigo-200 text-indigo-700', badgeText: '세운' },
|
||||
{ icon: '🏆', gradient: 'from-amber-400 to-yellow-500', border: 'border-amber-100', badge: 'bg-amber-50 border-amber-200 text-amber-700', badgeText: '황금기' },
|
||||
{ icon: '💌', gradient: 'from-slate-600 to-slate-800', border: 'border-slate-100', badge: 'bg-slate-50 border-slate-200 text-slate-700', badgeText: '종합' },
|
||||
];
|
||||
|
||||
// ── 마크다운 → 섹션 파싱 ──────────────────────────────────────────────
|
||||
interface ParsedSection {
|
||||
number: number;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function parseInterpretation(text: string): ParsedSection[] {
|
||||
// "## 숫자. 제목" 패턴으로 분리
|
||||
const parts = text.split(/\n(?=##\s+\d+[\.\s])/).filter(Boolean);
|
||||
const sections: ParsedSection[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const lines = part.trim().split('\n');
|
||||
const headerLine = lines[0] ?? '';
|
||||
const match = headerLine.match(/^##\s+(\d+)[.\s]\s*(.+)$/);
|
||||
if (match) {
|
||||
sections.push({
|
||||
number: parseInt(match[1], 10),
|
||||
title: match[2].trim(),
|
||||
content: lines.slice(1).join('\n').trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 파싱 실패 시 전체를 하나의 섹션으로
|
||||
if (sections.length === 0 && text.trim()) {
|
||||
sections.push({ number: 0, title: 'AI 해석', content: text.trim() });
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
// ── 섹션 카드 컴포넌트 ────────────────────────────────────────────────
|
||||
function SectionCard({ section, meta, isOpen, onToggle }: {
|
||||
section: ParsedSection;
|
||||
meta: typeof SECTION_META[0];
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded-2xl border-2 ${meta.border} bg-white overflow-hidden shadow-sm transition-all`}>
|
||||
{/* 헤더 */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-4 text-left hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
{/* 번호 아이콘 */}
|
||||
<div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${meta.gradient} flex items-center justify-center text-white font-extrabold text-sm flex-shrink-0 shadow-sm`}>
|
||||
{section.number > 0 ? section.number : meta.icon}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`text-[11px] font-bold px-2 py-0.5 rounded-full border ${meta.badge}`}>
|
||||
{meta.badgeText}
|
||||
</span>
|
||||
<h3 className="font-extrabold text-[#04102b] text-sm leading-snug">
|
||||
{section.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 토글 화살표 */}
|
||||
<svg
|
||||
className={`w-4 h-4 text-slate-400 flex-shrink-0 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 내용 (아코디언) */}
|
||||
{isOpen && (
|
||||
<div className="px-5 pb-5 pt-1 border-t border-slate-100">
|
||||
<div className={`text-[11px] font-semibold mb-3 flex items-center gap-1.5 ${meta.badge.includes('violet') ? 'text-violet-400' : 'text-slate-400'}`}>
|
||||
<span className="text-base">{meta.icon}</span>
|
||||
</div>
|
||||
<div className="prose prose-sm max-w-none text-slate-700 leading-relaxed">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => <h1 className="text-base font-extrabold text-[#04102b] mt-4 mb-2">{children}</h1>,
|
||||
h2: ({ children }) => <h2 className="text-sm font-extrabold text-[#04102b] mt-3 mb-1.5">{children}</h2>,
|
||||
h3: ({ children }) => <h3 className="text-sm font-bold text-[#04102b] mt-2 mb-1">{children}</h3>,
|
||||
p: ({ children }) => <p className="mb-3 text-sm leading-relaxed text-slate-700">{children}</p>,
|
||||
strong: ({ children }) => <strong className="font-bold text-[#04102b]">{children}</strong>,
|
||||
em: ({ children }) => <em className="italic text-slate-600">{children}</em>,
|
||||
ul: ({ children }) => <ul className="list-disc list-inside space-y-1.5 mb-3 text-sm text-slate-700 pl-1">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-decimal list-inside space-y-1.5 mb-3 text-sm text-slate-700 pl-1">{children}</ol>,
|
||||
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-violet-300 pl-4 py-1 my-3 text-slate-600 bg-violet-50 rounded-r-lg text-sm italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
hr: () => <hr className="border-slate-200 my-4" />,
|
||||
code: ({ children }) => (
|
||||
<code className="bg-slate-100 text-violet-700 px-1.5 py-0.5 rounded text-xs font-mono">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{section.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// mock 데이터 여부 감지 (저장된 해석이 예시 데이터인 경우 재생성 필요)
|
||||
function isMockInterpretation(text: string | null): boolean {
|
||||
if (!text) return false;
|
||||
return (
|
||||
text.includes('API 키 문제 또는 할당량 초과') ||
|
||||
text.includes('GEMINI_API_KEY 환경변수를 설정') ||
|
||||
text.includes('예시 데이터를 보여드립니다') ||
|
||||
text.includes('API 설정이 필요합니다')
|
||||
);
|
||||
}
|
||||
|
||||
// ── 메인 컴포넌트 ──────────────────────────────────────────────────────
|
||||
export default function SajuAISection({
|
||||
hasPaid,
|
||||
savedInterpretation,
|
||||
sajuData,
|
||||
daeun,
|
||||
daeunList,
|
||||
gender,
|
||||
birthKey,
|
||||
currentUrl,
|
||||
engineData,
|
||||
}: SajuAISectionProps) {
|
||||
// 저장된 해석이 mock 데이터면 재생성 필요
|
||||
const isMock = isMockInterpretation(savedInterpretation);
|
||||
const validSaved = savedInterpretation && !isMock ? savedInterpretation : null;
|
||||
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>(
|
||||
validSaved ? 'done' : 'idle'
|
||||
);
|
||||
const [interpretation, setInterpretation] = useState(validSaved ?? '');
|
||||
const [openSections, setOpenSections] = useState<Set<number>>(new Set([0]));
|
||||
const called = useRef(false);
|
||||
|
||||
const sections = parseInterpretation(interpretation);
|
||||
|
||||
const toggleSection = (idx: number) => {
|
||||
setOpenSections(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(idx)) next.delete(idx);
|
||||
else next.add(idx);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const expandAll = () => setOpenSections(new Set(sections.map((_, i) => i)));
|
||||
const collapseAll = () => setOpenSections(new Set());
|
||||
|
||||
// 재생성: called ref 초기화 후 다시 API 호출
|
||||
const handleRegenerate = () => {
|
||||
called.current = false;
|
||||
setStatus('idle');
|
||||
setInterpretation('');
|
||||
// idle → useEffect가 다시 실행되도록 상태 전환 트리거
|
||||
setTimeout(() => {
|
||||
called.current = false;
|
||||
setStatus('loading');
|
||||
fetch('/api/saju/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.interpretation && !isMockInterpretation(data.interpretation)) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
setOpenSections(new Set([0]));
|
||||
// DB에 실제 해석으로 덮어쓰기
|
||||
const { birth_year, birth_month, birth_day } = birthKey;
|
||||
if (typeof birth_year === 'number' && typeof birth_month === 'number' && typeof birth_day === 'number') {
|
||||
fetch('/api/saju/save-interpretation', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interpretation: data.interpretation, birthKey }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
})
|
||||
.catch(() => setStatus('error'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPaid || validSaved || called.current) return;
|
||||
called.current = true;
|
||||
setStatus('loading');
|
||||
|
||||
fetch('/api/saju/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.interpretation) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
// 첫 번째 섹션 자동 열기
|
||||
setOpenSections(new Set([0]));
|
||||
|
||||
const { birth_year, birth_month, birth_day } = birthKey;
|
||||
if (
|
||||
typeof birth_year === 'number' && !isNaN(birth_year) &&
|
||||
typeof birth_month === 'number' && !isNaN(birth_month) &&
|
||||
typeof birth_day === 'number' && !isNaN(birth_day)
|
||||
) {
|
||||
fetch('/api/saju/save-interpretation', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interpretation: data.interpretation, birthKey }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
})
|
||||
.catch(() => setStatus('error'));
|
||||
}, [hasPaid]);
|
||||
|
||||
// ── 미결제 ──────────────────────────────────────────────────────────
|
||||
if (!hasPaid) {
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] rounded-2xl border border-[#1a3a7a] p-7 text-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-[0.05]"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '22px 22px' }} />
|
||||
<div className="relative">
|
||||
<div className="inline-flex items-center gap-2 bg-amber-400/10 border border-amber-400/25 text-amber-300 text-xs font-semibold px-3 py-1 rounded-full mb-3">
|
||||
AI PREMIUM
|
||||
</div>
|
||||
<h3 className="text-xl font-extrabold text-white mb-2">AI 상세 해석 (12개 항목)</h3>
|
||||
<p className="text-blue-200/60 text-sm mb-6">
|
||||
성격, 재물운, 직업 적성, 애정운, 건강운, 대운 분석 등<br />
|
||||
Gemini 2.5 Pro가 생성하는 맞춤형 사주 해석을 받아보세요.
|
||||
</p>
|
||||
|
||||
{/* 미리보기 섹션 목록 */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-6 text-left">
|
||||
{SECTION_META.map((meta, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2 py-1.5">
|
||||
<span className="text-sm">{meta.icon}</span>
|
||||
<span className="text-xs text-blue-200/70 font-medium">{meta.badgeText}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PaymentButton
|
||||
productId="saju_detail"
|
||||
className="inline-flex items-center gap-2 bg-amber-400 hover:bg-amber-300 text-[#04102b] font-bold px-7 py-3 rounded-xl transition-all"
|
||||
>
|
||||
AI 상세 해석 받기 — 1,000원
|
||||
</PaymentButton>
|
||||
<p className="text-blue-200/40 text-xs mt-3">결제 후 즉시 AI 분석 시작 · 로그인 필요</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 로딩 ──────────────────────────────────────────────────────────
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-8 text-center">
|
||||
<div className="w-10 h-10 border-2 border-violet-600 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-slate-500 text-sm font-medium">AI가 사주를 분석하는 중입니다...</p>
|
||||
<p className="text-slate-400 text-xs mt-1">약 20~30초 소요될 수 있습니다</p>
|
||||
<div className="mt-5 flex flex-wrap justify-center gap-2">
|
||||
{SECTION_META.map((meta, i) => (
|
||||
<span key={i} className="flex items-center gap-1 text-xs text-slate-400 animate-pulse">
|
||||
<span>{meta.icon}</span>{meta.badgeText}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 오류 ──────────────────────────────────────────────────────────
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-red-200 p-6 text-center">
|
||||
<p className="text-red-500 text-sm font-medium mb-3">AI 해석 생성에 실패했습니다.</p>
|
||||
<button
|
||||
onClick={() => { called.current = false; setStatus('idle'); }}
|
||||
className="text-xs text-blue-600 underline"
|
||||
>
|
||||
다시 시도하기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 해석 완료 ─────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-2 px-6 py-4 border-b border-slate-100 bg-gradient-to-r from-[#04102b] to-[#0a1f5c]">
|
||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-violet-400 to-amber-400 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-sm font-extrabold text-white">AI 상세 해석</h2>
|
||||
<p className="text-blue-300/60 text-[11px]">12개 항목 · 클릭해서 펼쳐보세요</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRegenerate}
|
||||
title="AI 해석 재생성"
|
||||
className="text-[11px] text-blue-300/60 hover:text-blue-200 px-2 py-1 rounded-lg hover:bg-white/10 transition-all flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
재생성
|
||||
</button>
|
||||
<span className="text-xs bg-emerald-400/20 border border-emerald-400/30 text-emerald-300 font-bold px-2.5 py-1 rounded-full">
|
||||
결제 완료
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 컨트롤 + 목록 */}
|
||||
<div className="p-5">
|
||||
{/* 전체 펼치기/접기 */}
|
||||
{sections.length > 1 && (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-xs text-slate-400 font-medium">
|
||||
총 {sections.length}개 항목
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={expandAll}
|
||||
className="text-xs text-violet-600 hover:text-violet-800 font-semibold px-3 py-1 rounded-lg border border-violet-200 hover:bg-violet-50 transition-colors"
|
||||
>
|
||||
전체 펼치기
|
||||
</button>
|
||||
<button
|
||||
onClick={collapseAll}
|
||||
className="text-xs text-slate-500 hover:text-slate-700 font-semibold px-3 py-1 rounded-lg border border-slate-200 hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
전체 접기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 섹션 카드 목록 */}
|
||||
<div className="space-y-3">
|
||||
{sections.map((section, idx) => {
|
||||
const metaIdx = section.number > 0 ? Math.min(section.number - 1, SECTION_META.length - 1) : idx % SECTION_META.length;
|
||||
const meta = SECTION_META[metaIdx];
|
||||
return (
|
||||
<SectionCard
|
||||
key={idx}
|
||||
section={section}
|
||||
meta={meta}
|
||||
isOpen={openSections.has(idx)}
|
||||
onToggle={() => toggleSection(idx)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 하단 안내 */}
|
||||
{sections.length > 0 && (
|
||||
<p className="text-center text-xs text-slate-400 mt-5">
|
||||
해석은 사주 데이터를 기반으로 AI가 생성한 내용입니다. 참고용으로 활용해주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
// ── 천간 / 지지 ───────────────────────────────────────────────────────
|
||||
const STEMS = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
|
||||
const STEMS_KR = ['갑','을','병','정','무','기','경','신','임','계'];
|
||||
const BRANCHES = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
|
||||
const BRANCHES_KR= ['자','축','인','묘','진','사','오','미','신','유','술','해'];
|
||||
|
||||
const STEM_ELEM: Record<string,string> = { '甲':'木','乙':'木','丙':'火','丁':'火','戊':'土','己':'土','庚':'金','辛':'金','壬':'水','癸':'水' };
|
||||
const BRANCH_ELEM: Record<string,string> = { '子':'水','亥':'水','寅':'木','卯':'木','巳':'火','午':'火','申':'金','酉':'金','丑':'土','辰':'土','未':'土','戌':'土' };
|
||||
|
||||
// 1900-01-01 = 甲戌 (stem=0, branch=10) — CLAUDE.md 검증 완료
|
||||
const BASE_MS = Date.UTC(1900, 0, 1);
|
||||
|
||||
function getTodayPillar() {
|
||||
const now = new Date();
|
||||
const todayMs = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const diff = Math.round((todayMs - BASE_MS) / 86400000);
|
||||
const si = ((0 + diff) % 10 + 10) % 10;
|
||||
const bi = ((10 + diff) % 12 + 12) % 12;
|
||||
return {
|
||||
stem: STEMS[si], stemKr: STEMS_KR[si],
|
||||
branch: BRANCHES[bi], branchKr: BRANCHES_KR[bi],
|
||||
stemElem: STEM_ELEM[STEMS[si]] ?? '木',
|
||||
branchElem: BRANCH_ELEM[BRANCHES[bi]] ?? '水',
|
||||
year: now.getFullYear(), month: now.getMonth() + 1, date: now.getDate(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 오행 상생·상극 ────────────────────────────────────────────────────
|
||||
const GENERATES: Record<string,string> = { '木':'火','火':'土','土':'金','金':'水','水':'木' };
|
||||
const OVERCOMES: Record<string,string> = { '木':'土','火':'金','土':'水','金':'木','水':'火' };
|
||||
|
||||
type Rel = 'same'|'generates'|'generated'|'overcomes'|'overcome'|'neutral';
|
||||
function getRelation(a: string, b: string): Rel {
|
||||
if (a === b) return 'same';
|
||||
if (GENERATES[a] === b) return 'generates';
|
||||
if (GENERATES[b] === a) return 'generated';
|
||||
if (OVERCOMES[a] === b) return 'overcomes';
|
||||
if (OVERCOMES[b] === a) return 'overcome';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
// ── 오늘 종합 점수 (0–100) ────────────────────────────────────────────
|
||||
function calcOverallScore(stemElem: string, branchElem: string, yongShin: string, heeShin: string) {
|
||||
let score = 50;
|
||||
const add = (rel: Rel, weight: number) => {
|
||||
if (rel === 'same') score += 25 * weight;
|
||||
else if (rel === 'generates' || rel === 'generated') score += 15 * weight;
|
||||
else if (rel === 'overcomes') score -= 20 * weight;
|
||||
else if (rel === 'overcome') score -= 8 * weight;
|
||||
};
|
||||
add(getRelation(stemElem, yongShin), 1);
|
||||
add(getRelation(branchElem, yongShin), 0.8);
|
||||
add(getRelation(stemElem, heeShin), 0.3);
|
||||
add(getRelation(branchElem, heeShin), 0.2);
|
||||
return Math.round(Math.max(10, Math.min(100, score)));
|
||||
}
|
||||
|
||||
type Level = 'great'|'good'|'neutral'|'caution';
|
||||
function toLevel(s: number): Level {
|
||||
if (s >= 78) return 'great';
|
||||
if (s >= 58) return 'good';
|
||||
if (s >= 38) return 'neutral';
|
||||
return 'caution';
|
||||
}
|
||||
|
||||
// ── 결정론적 랜덤 ────────────────────────────────────────────────────
|
||||
function seededRand(seed: number) {
|
||||
let s = seed;
|
||||
return () => { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 0xffffffff; };
|
||||
}
|
||||
|
||||
// ── 운세 항목 빌드 ────────────────────────────────────────────────────
|
||||
type Area = { icon: string; label: string; score: number; desc: string };
|
||||
|
||||
const DESCS: Record<string, Record<Level, string>> = {
|
||||
money: {
|
||||
great: '재물 흐름이 활발합니다. 작은 투자나 구매 결정에 긍정적인 시기입니다.',
|
||||
good: '재물 운이 순조롭습니다. 무리하지 않는 범위에서 움직이면 이익이 납니다.',
|
||||
neutral: '수입·지출이 균형을 이루는 날. 큰 결정은 잠시 미루세요.',
|
||||
caution: '충동 지출에 주의하세요. 중요한 금전 거래는 신중히 검토하세요.',
|
||||
},
|
||||
love: {
|
||||
great: '감정 교류가 잘 이루어지는 날. 마음을 전하기 좋은 타이밍입니다.',
|
||||
good: '관계에 따뜻한 기운이 감돕니다. 오래 연락 못 했던 사람에게 먼저 다가가 보세요.',
|
||||
neutral: '평온한 관계를 유지하는 날입니다. 억지로 변화를 만들 필요 없습니다.',
|
||||
caution: '오해가 생기기 쉬운 날입니다. 중요한 대화는 감정이 차분해진 후에 하세요.',
|
||||
},
|
||||
career: {
|
||||
great: '능력이 잘 발휘되는 날. 중요한 프레젠테이션이나 면담에 최적입니다.',
|
||||
good: '업무 효율이 올라가는 날입니다. 오늘 마무리한 과제는 좋은 결과로 이어집니다.',
|
||||
neutral: '꾸준히 하던 일을 이어가는 날. 새 프로젝트보다 마무리에 집중하세요.',
|
||||
caution: '실수가 생기기 쉬운 날입니다. 중요한 결재·계약은 하루 늦춰보세요.',
|
||||
},
|
||||
health: {
|
||||
great: '체력·집중력 모두 좋은 날. 평소보다 활동량을 늘려도 괜찮습니다.',
|
||||
good: '컨디션이 안정적입니다. 가벼운 운동으로 기운을 더 끌어올리세요.',
|
||||
neutral: '무리하지 않는 것이 최선. 충분한 수분과 수면을 챙겨주세요.',
|
||||
caution: '피로가 쌓이기 쉬운 날입니다. 무리한 약속은 피하고 충분히 쉬세요.',
|
||||
},
|
||||
social: {
|
||||
great: '대인관계 운이 열린 날. 중요한 만남·협상에 유리한 시기입니다.',
|
||||
good: '사교적 기운이 넘칩니다. 새 인맥을 만들거나 협업을 제안해보세요.',
|
||||
neutral: '조용히 자신의 일에 집중하는 날. 복잡한 인간관계는 잠시 내려놓으세요.',
|
||||
caution: '갈등이 생기기 쉬운 날입니다. 중요한 협상은 다음 기회로 미루는 것이 현명합니다.',
|
||||
},
|
||||
};
|
||||
|
||||
function buildAreas(
|
||||
overall: number,
|
||||
yongShin: string, heeShin: string,
|
||||
yearNum: number, monthNum: number, dayNum: number,
|
||||
): Area[] {
|
||||
const now = new Date();
|
||||
const seed = yearNum * 1_000_000 + monthNum * 10_000 + dayNum * 100 + now.getFullYear() % 100 * 10 + now.getMonth();
|
||||
const rand = seededRand(seed);
|
||||
const roll = () => Math.round(Math.max(15, Math.min(98, rand() * 40 + overall - 20)));
|
||||
const keys = ['money','love','career','health','social'] as const;
|
||||
const icons = ['💰','💕','🎯','🌿','🤝'];
|
||||
const labels = ['재물운','애정운','직업운','건강운','사회운'];
|
||||
return keys.map((k, i) => {
|
||||
const s = roll();
|
||||
return { icon: icons[i], label: labels[i], score: s, desc: DESCS[k][toLevel(s)] };
|
||||
});
|
||||
}
|
||||
|
||||
// ── 레벨별 색상/라벨 ─────────────────────────────────────────────────
|
||||
const LEVEL_META: Record<Level, { emoji: string; label: string; bar: string; bg: string; border: string; text: string; badge: string }> = {
|
||||
great: { emoji:'🌟', label:'아주 좋은 날', bar:'#f59e0b', bg:'bg-amber-50', border:'border-amber-300', text:'text-amber-800', badge:'bg-amber-100 text-amber-700 border-amber-300' },
|
||||
good: { emoji:'✨', label:'좋은 날', bar:'#22c55e', bg:'bg-emerald-50',border:'border-emerald-300',text:'text-emerald-800',badge:'bg-emerald-100 text-emerald-700 border-emerald-300' },
|
||||
neutral: { emoji:'🌤️', label:'평온한 날', bar:'#64748b', bg:'bg-slate-50', border:'border-slate-200', text:'text-slate-700', badge:'bg-slate-100 text-slate-600 border-slate-200' },
|
||||
caution: { emoji:'⚠️', label:'조심하는 날', bar:'#f97316', bg:'bg-orange-50', border:'border-orange-300',text:'text-orange-800', badge:'bg-orange-100 text-orange-700 border-orange-300' },
|
||||
};
|
||||
|
||||
const REL_DESC: (yongShin: string, yongShinKr: string) => Record<Rel, string> = (y, yk) => ({
|
||||
same: `오늘 기운이 당신의 용신 ${y}(${yk})과 같은 오행으로 강하게 공명합니다.`,
|
||||
generates: `오늘 기운이 용신 ${y}(${yk})을 생(生)해줍니다. 순조롭게 힘이 실리는 날.`,
|
||||
generated: `용신 ${y}(${yk})이 오늘 기운을 생(生)해주고 있어 에너지를 베풀기 좋은 날입니다.`,
|
||||
overcomes: `오늘 기운이 용신 ${y}(${yk})을 극(克)합니다. 신중하게 움직이는 것이 좋습니다.`,
|
||||
overcome: `용신 ${y}(${yk})이 오늘 기운을 극(克)합니다. 주도적으로 판단하기 좋은 날.`,
|
||||
neutral: `오늘 기운과 용신 ${y}(${yk})은 독립적으로 작용합니다. 차분하게 나아가세요.`,
|
||||
});
|
||||
|
||||
// ── 점수 바 ──────────────────────────────────────────────────────────
|
||||
function ScoreBar({ score, color }: { score: number; color: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex-1 h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div style={{ width: `${score}%`, background: color, transition: 'width 0.8s ease' }} className="h-full rounded-full" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold w-6 text-right" style={{ color }}>{score}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 메인 컴포넌트 ─────────────────────────────────────────────────────
|
||||
interface Props {
|
||||
yongShin: string;
|
||||
yongShinKr: string;
|
||||
heeShin: string;
|
||||
heeShinKr: string;
|
||||
yearNum: number;
|
||||
monthNum: number;
|
||||
dayNum: number;
|
||||
hasLottoSubscription: boolean;
|
||||
}
|
||||
|
||||
export default function SajuFortuneSection({
|
||||
yongShin, yongShinKr, heeShin, heeShinKr,
|
||||
yearNum, monthNum, dayNum,
|
||||
hasLottoSubscription,
|
||||
}: Props) {
|
||||
const today = useMemo(getTodayPillar, []);
|
||||
const overall = useMemo(() => calcOverallScore(today.stemElem, today.branchElem, yongShin, heeShin), [today, yongShin, heeShin]);
|
||||
const level = toLevel(overall);
|
||||
const meta = LEVEL_META[level];
|
||||
const areas = useMemo(() => buildAreas(overall, yongShin, heeShin, yearNum, monthNum, dayNum), [overall, yongShin, heeShin, yearNum, monthNum, dayNum]);
|
||||
const stemRel = getRelation(today.stemElem, yongShin);
|
||||
const relDesc = REL_DESC(yongShin, yongShinKr)[stemRel];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── 상단 연결 화살표 ── */}
|
||||
<div className="flex flex-col items-center gap-0 py-1">
|
||||
<div className="w-px h-5 bg-gradient-to-b from-blue-200 to-amber-300" />
|
||||
<div className="flex items-center gap-2 px-4 py-1.5 bg-amber-50 border border-amber-200 rounded-full text-[11px] font-bold text-amber-700">
|
||||
<span>✨</span> 사주 분석에서 이어지는 오늘의 운세
|
||||
</div>
|
||||
<div className="w-px h-5 bg-gradient-to-b from-amber-300 to-amber-100" />
|
||||
</div>
|
||||
|
||||
{/* ── 본문 카드 ── */}
|
||||
<div id="today-fortune" className="bg-white rounded-2xl border border-amber-200 overflow-hidden shadow-sm">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-gradient-to-r from-[#1a0a00] via-[#3d1a00] to-[#1a0a00] px-6 py-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center flex-shrink-0 shadow-md text-xl">
|
||||
☀️
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-extrabold text-white">오늘의 운세</h2>
|
||||
<p className="text-amber-300/70 text-[11px] mt-0.5">
|
||||
{today.year}년 {today.month}월 {today.date}일 · 일진 {today.stem}{today.branch} ({today.stemKr}{today.branchKr})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-[11px] font-extrabold px-3 py-1.5 rounded-full border ${meta.badge} flex-shrink-0`}>
|
||||
{meta.emoji} {meta.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-5">
|
||||
{/* 일진 × 용신 분석 */}
|
||||
<div className={`rounded-xl border p-4 ${meta.bg} ${meta.border}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center font-bold text-lg flex-shrink-0 ${meta.text}`}
|
||||
style={{ background: 'rgba(255,255,255,0.65)' }}>
|
||||
{today.stem}{today.branch}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className={`text-xs font-extrabold mb-1 ${meta.text}`}>
|
||||
오늘 일진과 당신의 용신 {yongShin}({yongShinKr}) 분석
|
||||
</div>
|
||||
<p className={`text-xs leading-relaxed ${meta.text}`} style={{ opacity: 0.88 }}>
|
||||
{relDesc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 종합 점수 바 */}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<span className="text-[11px] font-bold text-slate-500">오늘 종합 운세</span>
|
||||
<div className="flex-1 h-2.5 bg-white/70 rounded-full overflow-hidden border border-white/50">
|
||||
<div
|
||||
style={{ width: `${overall}%`, background: `linear-gradient(90deg, ${meta.bar}cc, ${meta.bar})` }}
|
||||
className="h-full rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-extrabold" style={{ color: meta.bar }}>{overall}점</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5대 운세 그리드 */}
|
||||
<div>
|
||||
<h3 className="text-xs font-extrabold text-[#04102b] mb-3">오늘의 분야별 운세</h3>
|
||||
<div className="space-y-3">
|
||||
{areas.map((area) => {
|
||||
const aLevel = toLevel(area.score);
|
||||
const aMeta = LEVEL_META[aLevel];
|
||||
return (
|
||||
<div key={area.label} className="flex gap-3 items-start">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-sm ${aMeta.bg} border ${aMeta.border}`}>
|
||||
{area.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-xs font-bold text-[#04102b]">{area.label}</span>
|
||||
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded-full border ${aMeta.badge}`}>{aMeta.emoji}</span>
|
||||
</div>
|
||||
<ScoreBar score={area.score} color={aMeta.bar} />
|
||||
<p className="text-[11px] text-slate-500 mt-1 leading-relaxed">{area.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 면책 */}
|
||||
<p className="text-center text-[11px] text-slate-400 leading-relaxed">
|
||||
오늘의 운세는 당신의 사주 용신({yongShinKr}·{yongShin})과 오늘 일진의 오행 상호작용을 기반으로 합니다.<br />
|
||||
명리학적 참고 자료이며 결과를 보장하지 않습니다.
|
||||
</p>
|
||||
|
||||
{/* 로또 CTA */}
|
||||
<div className="rounded-2xl bg-gradient-to-br from-[#04102b] via-[#0d1f5c] to-[#04102b] border border-[#1a3a7a] p-5 relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-[0.04]"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '20px 20px' }} />
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-base">🎱</span>
|
||||
<span className="text-xs font-extrabold text-amber-300">
|
||||
{level === 'great' ? '오늘 운이 아주 좋습니다! 로또도 한 번 도전해보세요.' : '사주 기반 행운 번호도 확인해보세요.'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-blue-200/70 leading-relaxed mb-4">
|
||||
용신 <strong className="text-amber-300">{yongShin}({yongShinKr})</strong> 오행이 담긴
|
||||
사주 기반 로또 번호가 아래에 준비되어 있습니다.
|
||||
{hasLottoSubscription
|
||||
? ' 구독 중이신 로또 서비스의 매주 최신 추천 번호도 함께 확인하세요.'
|
||||
: ' 로또 구독 시 대운 교차 분석으로 더 정밀한 번호를 매주 받을 수 있어요.'}
|
||||
</p>
|
||||
<a
|
||||
href="#saju-lotto-section"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
document.getElementById('saju-lotto-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}}
|
||||
className="block w-full text-center bg-gradient-to-r from-amber-500 to-amber-400 hover:from-amber-400 hover:to-amber-300 text-[#04102b] text-sm font-extrabold px-4 py-2.5 rounded-xl transition-all shadow-lg cursor-pointer"
|
||||
>
|
||||
오늘의 로또 번호 추천 보기 ↓
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 연결 */}
|
||||
<div className="flex flex-col items-center gap-0 py-1">
|
||||
<div className="w-px h-5 bg-gradient-to-b from-amber-200 to-blue-300" />
|
||||
<div className="flex items-center gap-2 px-4 py-1.5 bg-blue-50 border border-blue-200 rounded-full text-[11px] font-bold text-blue-700">
|
||||
<span>🎱</span> 오늘의 운세에서 이어지는 사주 로또 추천
|
||||
</div>
|
||||
<div className="w-px h-5 bg-gradient-to-b from-blue-200 to-transparent" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
import { calculateSaju } from '@/lib/saju-calculator';
|
||||
import Link from 'next/link';
|
||||
import { calculateDaeun, getCurrentDaeun, getDaeunDescription } from '@/lib/daeun-calculator';
|
||||
import { getCurrentSolarTerm, getSolarTermName, getSolarTermMonthBranch } from '@/lib/solar-terms';
|
||||
import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju-calculator';
|
||||
import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import SajuAISection from './SajuAISection';
|
||||
import SajuFortuneSection from './SajuFortuneSection';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
year: string;
|
||||
month: string;
|
||||
day: string;
|
||||
hour?: string;
|
||||
gender: 'male' | 'female';
|
||||
calendarType: 'solar' | 'lunar';
|
||||
originalYear?: string;
|
||||
originalMonth?: string;
|
||||
originalDay?: string;
|
||||
isLeapMonth?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function SajuResultPage({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const { year, month, day, hour, gender, calendarType, originalYear, originalMonth, originalDay, isLeapMonth } = params;
|
||||
|
||||
const yearNum = parseInt(year, 10);
|
||||
const monthNum = parseInt(month, 10);
|
||||
const dayNum = parseInt(day, 10);
|
||||
const hourNum = hour ? parseInt(hour, 10) : null;
|
||||
|
||||
if (isNaN(yearNum) || isNaN(monthNum) || isNaN(dayNum)) {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center">
|
||||
<div className="text-center py-20">
|
||||
<p className="text-slate-500 text-sm mb-4">잘못된 접근입니다. 생년월일을 다시 입력해주세요.</p>
|
||||
<a href="/saju/input" className="text-blue-600 underline text-sm">사주 입력하기</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const inputYear = originalYear ? parseInt(originalYear) : yearNum;
|
||||
const inputMonth = originalMonth ? parseInt(originalMonth) : monthNum;
|
||||
const inputDay = originalDay ? parseInt(originalDay) : dayNum;
|
||||
const isLunar = calendarType === 'lunar';
|
||||
const isLeap = isLeapMonth === 'true';
|
||||
|
||||
// ── 사주팔자 계산 (TypeScript — lunar-javascript 기반 정밀 절기 계산) ──
|
||||
const sajuData = calculateSaju(yearNum, monthNum, dayNum, hourNum, gender);
|
||||
|
||||
// 추가 분석 (신강신약, 용신, 오행균형, 세운)
|
||||
const analysis = performFullAnalysis(sajuData);
|
||||
const elementScores = analysis.elementScores;
|
||||
|
||||
// 대운
|
||||
const currentYear = new Date().getFullYear();
|
||||
const daeunList = calculateDaeun(
|
||||
yearNum, monthNum, dayNum, gender,
|
||||
sajuData.month.stem, sajuData.month.branch
|
||||
);
|
||||
const currentDaeun = getCurrentDaeun(daeunList, currentYear);
|
||||
|
||||
// 지지 상호작용 / 신살 / 공망 / 지장간
|
||||
const branchInteractions = analysis.branchInteractions;
|
||||
const shinsal = analysis.shinsal;
|
||||
const gongmang = analysis.gongmang;
|
||||
const hiddenStems = analysis.hiddenStems;
|
||||
|
||||
// ── 절기 정보 (표시용) ────────────────────────────────────────────────
|
||||
const solarTermIndex = getCurrentSolarTerm(yearNum, monthNum, dayNum);
|
||||
const solarTermName = getSolarTermName(solarTermIndex);
|
||||
|
||||
// ── 결제 여부 + 저장된 AI 해석 + 로또 구독 확인 ─────────────────────
|
||||
let hasPaid = false;
|
||||
let savedInterpretation: string | null = null;
|
||||
let hasLottoSubscription = false;
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
// 사주 결제 확인 (anon client — 본인 orders는 RLS 허용 가정)
|
||||
const { data: order } = await supabase
|
||||
.from('orders').select('id')
|
||||
.eq('user_id', user.id).eq('product_id', 'saju_detail').eq('status', 'paid')
|
||||
.maybeSingle();
|
||||
hasPaid = !!order;
|
||||
|
||||
if (hasPaid) {
|
||||
// 1차: birth_hour 포함 정확한 키로 조회
|
||||
const birthKey: Record<string, unknown> = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender };
|
||||
if (hourNum !== null) birthKey.birth_hour = hourNum;
|
||||
const { data: record } = await supabase
|
||||
.from('saju_records').select('interpretation')
|
||||
.eq('user_id', user.id).eq('is_paid', true)
|
||||
.contains('saju_data', birthKey).maybeSingle();
|
||||
savedInterpretation = record?.interpretation ?? null;
|
||||
|
||||
// 2차 폴백: birth_hour 없이 조회 (시간 입력 안 한 케이스 or 불일치 방지)
|
||||
if (!savedInterpretation) {
|
||||
const birthKeyNoHour: Record<string, unknown> = { birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender };
|
||||
const { data: record2 } = await supabase
|
||||
.from('saju_records').select('interpretation')
|
||||
.eq('user_id', user.id).eq('is_paid', true)
|
||||
.contains('saju_data', birthKeyNoHour).maybeSingle();
|
||||
savedInterpretation = record2?.interpretation ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// 로또 구독 확인 — subscriptions 테이블 (세션 클라이언트로 RLS select_own 통과)
|
||||
const { data: lottoSub } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'active')
|
||||
.in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual'])
|
||||
.maybeSingle();
|
||||
hasLottoSubscription = !!lottoSub;
|
||||
|
||||
// subscriptions에서 못 찾으면 orders 테이블로 폴백 (구독 마이그레이션 전 데이터)
|
||||
if (!hasLottoSubscription) {
|
||||
const now = new Date().toISOString();
|
||||
const thirtyOneDaysAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const { data: lottoOrder } = await supabase
|
||||
.from('orders')
|
||||
.select('id, created_at')
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'paid')
|
||||
.in('product_id', ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual'])
|
||||
.gte('created_at', thirtyOneDaysAgo)
|
||||
.maybeSingle();
|
||||
hasLottoSubscription = !!lottoOrder;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 미로그인 시 무시
|
||||
}
|
||||
|
||||
// ── 오행 색상 ──────────────────────────────────────────────────────────
|
||||
const elementColors: { [k: string]: string } = {
|
||||
'木': 'text-green-700', '火': 'text-red-600', '土': 'text-yellow-700',
|
||||
'金': 'text-amber-600', '水': 'text-blue-700',
|
||||
};
|
||||
const elementBgColors: { [k: string]: string } = {
|
||||
'木': 'bg-green-50 border-green-400', '火': 'bg-red-50 border-red-400',
|
||||
'土': 'bg-yellow-50 border-yellow-400', '金': 'bg-amber-50 border-amber-400',
|
||||
'水': 'bg-blue-50 border-blue-400',
|
||||
};
|
||||
|
||||
// ── 띠 계산 ────────────────────────────────────────────────────────────
|
||||
const zodiacAnimals = ['쥐', '소', '호랑이', '토끼', '용', '뱀', '말', '양', '원숭이', '닭', '개', '돼지'];
|
||||
const zodiacIdx = (yearNum - 4) % 12;
|
||||
const zodiacAnimal = zodiacAnimals[zodiacIdx >= 0 ? zodiacIdx : zodiacIdx + 12];
|
||||
|
||||
const engineBadge = <span className="text-[10px] bg-blue-50 border border-blue-200 text-blue-600 px-2 py-0.5 rounded-full font-semibold">TS 계산</span>;
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] px-6 py-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="inline-flex items-center gap-2 bg-violet-400/10 border border-violet-400/25 text-violet-300 text-xs font-semibold px-4 py-1.5 rounded-full mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
||||
사주팔자 감정서
|
||||
</div>
|
||||
<h1 className="text-3xl font-extrabold text-white mb-2">사주팔자 분석 결과</h1>
|
||||
<p className="text-blue-200/60 text-sm">전통 명리학과 AI 기술의 만남</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-8 max-w-4xl mx-auto">
|
||||
<div className="grid lg:grid-cols-[280px_1fr] gap-6">
|
||||
|
||||
{/* 사이드바 */}
|
||||
<aside className="lg:sticky lg:top-6 h-fit">
|
||||
<div className="bg-[#04102b] rounded-2xl p-6 text-white">
|
||||
<h2 className="text-base font-bold mb-5 text-center pb-4 border-b border-white/10">기본 정보</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">생년월일</div>
|
||||
<div className="font-bold">
|
||||
{isLunar ? (
|
||||
<div>
|
||||
<div>음력 {inputYear}.{inputMonth}.{inputDay}{isLeap ? ' (윤달)' : ''}</div>
|
||||
<div className="text-xs text-blue-300/50 mt-0.5">양력 {yearNum}.{monthNum}.{dayNum}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>{yearNum}.{monthNum}.{dayNum}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hourNum !== null && (
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">태어난 시간</div>
|
||||
<div className="font-bold">{hourNum}시</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">성별</div>
|
||||
<div className="font-bold">{gender === 'male' ? '남성' : '여성'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">띠</div>
|
||||
<div className="font-bold">{zodiacAnimal}띠</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">태어난 절기</div>
|
||||
<div className="font-bold text-amber-300">{solarTermName} 이후</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-300/60 mb-1">일간</div>
|
||||
<div className="font-bold text-2xl text-amber-400">
|
||||
{sajuData.day.stem} ({sajuData.day.stemKr})
|
||||
</div>
|
||||
<div className="text-xs text-blue-300/60 mt-1">
|
||||
{FIVE_ELEMENTS_KR[sajuData.day.element as keyof typeof FIVE_ELEMENTS_KR]}({sajuData.day.element})
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-blue-300/60 text-xs">계산 엔진</div>
|
||||
{engineBadge}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 pt-5 border-t border-white/10 space-y-2">
|
||||
<Link href="/saju/input"
|
||||
className="block w-full text-center bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||
다시 입력하기
|
||||
</Link>
|
||||
<Link href="/saju"
|
||||
className="block w-full text-center bg-violet-500/20 hover:bg-violet-500/30 text-violet-300 px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||
서비스 소개
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 메인 */}
|
||||
<main className="space-y-6">
|
||||
|
||||
{/* 사주팔자 표 */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<h2 className="text-xl font-extrabold text-[#04102b] mb-5 text-center">사주팔자 (四柱八字)</h2>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#04102b] text-white">
|
||||
<th className="py-2.5 px-3 text-center font-bold text-xs">구분</th>
|
||||
{sajuData.hour && <th className="py-2.5 px-3 text-center font-bold text-xs">시주</th>}
|
||||
<th className="py-2.5 px-3 text-center font-bold text-xs">일주</th>
|
||||
<th className="py-2.5 px-3 text-center font-bold text-xs">월주</th>
|
||||
<th className="py-2.5 px-3 text-center font-bold text-xs">년주</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* 천간 */}
|
||||
<tr className="border-b border-slate-100">
|
||||
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">천간</td>
|
||||
{sajuData.hour && (
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.hour.stem}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.hour.stemKr}</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.day.stem}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.day.stemKr}</div>
|
||||
<div className="text-xs text-amber-600 font-bold mt-0.5">일간</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.month.stem}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.month.stemKr}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.year.stem}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.year.stemKr}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 지지 */}
|
||||
<tr className="border-b border-slate-100">
|
||||
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">지지</td>
|
||||
{sajuData.hour && (
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.hour.branch}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.hour.branchKr}</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.day.branch}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.day.branchKr}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.month.branch}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.month.branchKr}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xl font-bold text-[#04102b]">{sajuData.year.branch}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{sajuData.year.branchKr}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 지장간 */}
|
||||
<tr className="border-b border-slate-100">
|
||||
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">
|
||||
<div>지장간</div>
|
||||
<div className="text-[10px] text-slate-400 font-normal">숨은 천간</div>
|
||||
</td>
|
||||
{(() => {
|
||||
const order = sajuData.hour
|
||||
? ['시주', '일주', '월주', '년주']
|
||||
: ['일주', '월주', '년주'];
|
||||
return order.map((pillarName, idx) => {
|
||||
const h = hiddenStems.find((hs: any) => hs.pillar === pillarName);
|
||||
return (
|
||||
<td key={idx} className={`py-2 px-2 text-center ${pillarName === '일주' ? 'bg-amber-50' : ''}`}>
|
||||
{h && (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
{h.stems.map((s: any, si: number) => (
|
||||
<span key={si}
|
||||
className={`inline-block px-1.5 py-0.5 rounded text-xs font-semibold border ${elementBgColors[s.element] || 'bg-gray-100'}`}
|
||||
title={s.role}>
|
||||
{s.stemKr}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</tr>
|
||||
|
||||
{/* 십성 */}
|
||||
<tr className="border-b border-slate-100">
|
||||
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">십성</td>
|
||||
{sajuData.hour && (
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.hour.tenGod}</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.day.tenGod}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.month.tenGod}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.year.tenGod}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 십이운성 */}
|
||||
<tr>
|
||||
<td className="py-2.5 px-3 text-center font-semibold text-[#04102b] bg-[#f0f5ff] text-xs">십이운성</td>
|
||||
{sajuData.hour && (
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.hour.fortune}</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="py-2.5 px-3 text-center bg-amber-50">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.day.fortune}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.month.fortune}</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-center">
|
||||
<div className="text-xs font-bold text-[#04102b]">{sajuData.year.fortune}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 지지 상호작용 */}
|
||||
{branchInteractions.length > 0 && (
|
||||
<div className="mt-5 pt-5 border-t border-slate-100">
|
||||
<h3 className="text-sm font-bold text-[#04102b] mb-3 text-center">지지 상호작용</h3>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{branchInteractions.map((inter: any, idx: number) => {
|
||||
const isPositive = inter.type.includes('합');
|
||||
const isNegative = inter.type.includes('충') || inter.type.includes('형');
|
||||
const colorClass = isPositive
|
||||
? 'bg-emerald-50 border-emerald-400 text-emerald-800'
|
||||
: isNegative
|
||||
? 'bg-red-50 border-red-400 text-red-800'
|
||||
: 'bg-amber-50 border-amber-400 text-amber-800';
|
||||
return (
|
||||
<span key={idx} className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-bold border ${colorClass}`}
|
||||
title={inter.description}>
|
||||
{inter.type} {inter.branchesKr.join('')}
|
||||
{inter.resultElement && ` → ${FIVE_ELEMENTS_KR[inter.resultElement as keyof typeof FIVE_ELEMENTS_KR]}`}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 오행 균형 */}
|
||||
<div className="mt-5 pt-5 border-t border-slate-100">
|
||||
<h3 className="text-sm font-bold text-[#04102b] mb-4 text-center">오행 균형</h3>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{Object.entries(elementScores).map(([element, score]) => (
|
||||
<div key={element} className="text-center">
|
||||
<div className={`text-lg font-bold mb-1 ${elementColors[element] || ''}`}>{element}</div>
|
||||
<div className="text-xs text-slate-500 mb-2">
|
||||
{FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR]}
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 rounded-full h-1.5 mb-1">
|
||||
<div
|
||||
className={`h-1.5 rounded-full transition-all ${element === sajuData.day.element
|
||||
? 'bg-gradient-to-r from-[#1a56db] to-[#7c3aed]'
|
||||
: 'bg-slate-400'
|
||||
}`}
|
||||
style={{ width: `${Math.max(score, 5)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs font-bold text-[#04102b]">{score}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 분석 카드 그리드 */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
|
||||
{/* 신강/신약 + 용신 */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<h3 className="text-base font-extrabold text-[#04102b] mb-4">일간 세력 분석</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className={`inline-block px-4 py-1.5 rounded-xl text-sm font-bold ${
|
||||
analysis.dayMasterStrength.result === '신강'
|
||||
? 'bg-red-100 text-red-700 border-2 border-red-400'
|
||||
: analysis.dayMasterStrength.result === '신약'
|
||||
? 'bg-blue-100 text-blue-700 border-2 border-blue-400'
|
||||
: 'bg-green-100 text-green-700 border-2 border-green-400'
|
||||
}`}>
|
||||
{analysis.dayMasterStrength.result}
|
||||
</span>
|
||||
<span className="text-slate-500 text-xs">점수: {analysis.dayMasterStrength.score}</span>
|
||||
</div>
|
||||
<ul className="space-y-1 text-xs text-slate-500 mb-5">
|
||||
{analysis.dayMasterStrength.reasons.map((r: string, i: number) => (
|
||||
<li key={i} className="flex items-start">
|
||||
<span className="text-amber-500 mr-1.5">-</span>
|
||||
<span>{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<h4 className="font-bold text-[#04102b] mb-2.5 text-sm">용신 / 희신 / 기신</h4>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.yongShin.yongShin] || 'bg-gray-100'}`}>
|
||||
용신: {analysis.yongShin.yongShinKr}
|
||||
</span>
|
||||
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.yongShin.heeShin] || 'bg-gray-100'}`}>
|
||||
희신: {analysis.yongShin.heeShinKr}
|
||||
</span>
|
||||
<span className="px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 border border-slate-300 text-slate-700">
|
||||
기신: {analysis.yongShin.giShinKr}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">{analysis.yongShin.explanation}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 신살 + 공망 */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<h3 className="text-base font-extrabold text-[#04102b] mb-4">신살 (神煞)</h3>
|
||||
{shinsal.length > 0 ? (
|
||||
<div className="space-y-2 mb-5">
|
||||
{shinsal.map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-2 p-3 rounded-xl bg-[#f0f5ff]">
|
||||
<span className="inline-block px-2 py-0.5 bg-[#04102b] text-white rounded-lg text-xs font-bold whitespace-nowrap">
|
||||
{s.name}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-[#04102b]">
|
||||
{s.pillar} {s.branchKr}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{s.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-xs mb-5">특별한 신살이 발견되지 않았습니다.</p>
|
||||
)}
|
||||
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<h4 className="font-bold text-[#04102b] mb-2 text-sm">공망 (空亡)</h4>
|
||||
<div className="flex gap-2 mb-2">
|
||||
{gongmang.branchesKr.map((bk: string, i: number) => (
|
||||
<span key={i} className="px-2.5 py-1 bg-[#04102b] text-white rounded-lg text-xs font-bold">
|
||||
{bk}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">{gongmang.description}</p>
|
||||
</div>
|
||||
|
||||
{/* 세운 정보 */}
|
||||
<div className="border-t border-slate-100 pt-4 mt-4">
|
||||
<h4 className="font-bold text-[#04102b] mb-2 text-sm">
|
||||
{analysis.seun.year}년 세운
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold border ${elementBgColors[analysis.seun.element] || 'bg-gray-100'}`}>
|
||||
{analysis.seun.stemKr}{analysis.seun.branchKr}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">{analysis.seun.elementKr} 기운</span>
|
||||
</div>
|
||||
{analysis.seun.interactions.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{analysis.seun.interactions.map((si: any, i: number) => (
|
||||
<span key={i} className={`text-xs px-2 py-0.5 rounded-full font-semibold ${
|
||||
si.type.includes('합') ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700'
|
||||
}`}>
|
||||
{si.type} {si.branchesKr.join('')}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 상세 해석 섹션 */}
|
||||
{(() => {
|
||||
const birthKey = {
|
||||
birth_year: yearNum, birth_month: monthNum, birth_day: dayNum, gender,
|
||||
...(hourNum !== null ? { birth_hour: hourNum } : {}),
|
||||
};
|
||||
const currentUrl = `/saju/result?year=${yearNum}&month=${monthNum}&day=${dayNum}${hourNum !== null ? `&hour=${hourNum}` : ''}&gender=${gender}&calendarType=${calendarType}${originalYear ? `&originalYear=${originalYear}&originalMonth=${originalMonth}&originalDay=${originalDay}` : ''}${isLeap ? '&isLeapMonth=true' : ''}`;
|
||||
return (
|
||||
<SajuAISection
|
||||
hasPaid={hasPaid}
|
||||
savedInterpretation={savedInterpretation}
|
||||
sajuData={sajuData}
|
||||
daeun={currentDaeun}
|
||||
daeunList={daeunList}
|
||||
gender={gender}
|
||||
birthKey={birthKey}
|
||||
currentUrl={currentUrl}
|
||||
engineData={undefined}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 오늘의 운세 (사주 결제 시 표시) */}
|
||||
{hasPaid && (
|
||||
<SajuFortuneSection
|
||||
yongShin={analysis.yongShin.yongShin}
|
||||
yongShinKr={analysis.yongShin.yongShinKr}
|
||||
heeShin={analysis.yongShin.heeShin}
|
||||
heeShinKr={analysis.yongShin.heeShinKr}
|
||||
yearNum={yearNum}
|
||||
monthNum={monthNum}
|
||||
dayNum={dayNum}
|
||||
hasLottoSubscription={hasLottoSubscription}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 대운 */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<h2 className="text-lg font-extrabold text-[#04102b] mb-5 text-center">
|
||||
대운 (大運) — 10년 주기 운세
|
||||
</h2>
|
||||
|
||||
{currentDaeun && (
|
||||
<div className="bg-gradient-to-r from-[#04102b] to-[#0a2060] rounded-2xl p-5 mb-5 text-white">
|
||||
<h3 className="text-sm font-bold mb-3 text-center text-blue-300">현재 대운</h3>
|
||||
<div className="text-center mb-3">
|
||||
<div className="text-3xl font-bold mb-1">
|
||||
{currentDaeun.stem}{currentDaeun.branch}
|
||||
</div>
|
||||
<div className="text-base text-blue-200">
|
||||
{currentDaeun.stemKr}{currentDaeun.branchKr}
|
||||
</div>
|
||||
<div className="text-xs text-blue-300/70 mt-1">
|
||||
{currentDaeun.age}세 ~ {currentDaeun.age + 9}세 ({currentDaeun.startYear} ~ {currentDaeun.endYear}년)
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center leading-relaxed text-xs text-blue-200/80">
|
||||
{getDaeunDescription(currentDaeun, sajuData.day.stem)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{daeunList.map((daeun: any, index: number) => {
|
||||
const isCurrent = currentDaeun &&
|
||||
daeun.startYear === currentDaeun.startYear &&
|
||||
daeun.endYear === currentDaeun.endYear;
|
||||
return (
|
||||
<div key={index}
|
||||
className={`rounded-xl p-3 border-2 transition ${isCurrent ? 'bg-amber-50 border-amber-400' : 'bg-white border-[#dbe8ff]'}`}>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-[#04102b] mb-0.5">{daeun.stem}{daeun.branch}</div>
|
||||
<div className="text-xs text-slate-500 mb-1.5">{daeun.stemKr}{daeun.branchKr}</div>
|
||||
<div className="text-xs text-slate-400">{daeun.age}세 ~ {daeun.age + 9}세</div>
|
||||
<div className="text-xs text-slate-400">{daeun.startYear} ~ {daeun.endYear}</div>
|
||||
{isCurrent && (
|
||||
<div className="mt-1.5">
|
||||
<span className="inline-block bg-[#04102b] text-white text-xs px-2.5 py-0.5 rounded-full font-semibold">현재</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '블로그 자동화 솔루션 팩 | 쟁승메이드',
|
||||
description:
|
||||
'쿠팡파트너스·네이버 애드포스트·브랜드커넥트 수익을 자동화하는 프롬프트 조합법 45종 + 구조 템플릿 PDF 80p + 샘플 글 10편. ₩29,000 한 번 결제, 평생 무료 업데이트.',
|
||||
keywords: [
|
||||
'블로그 자동화',
|
||||
'AI 블로그 글쓰기',
|
||||
'쿠팡파트너스 자동화',
|
||||
'애드포스트 수익화',
|
||||
'네이버 블로그 SEO',
|
||||
'ChatGPT 블로그',
|
||||
'블로그 프롬프트',
|
||||
],
|
||||
openGraph: {
|
||||
title: '블로그 자동화 솔루션 팩 | 쟁승메이드',
|
||||
description:
|
||||
'쿠팡파트너스·애드포스트 수익을 자동화하는 프롬프트 + 템플릿 + 샘플. ₩29,000.',
|
||||
url: 'https://jaengseung-made.com/services/blog',
|
||||
},
|
||||
};
|
||||
|
||||
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PurchaseAgreementModal from '../../components/PurchaseAgreementModal';
|
||||
|
||||
const PACK_ITEMS = [
|
||||
{
|
||||
icon: '📝',
|
||||
title: '프롬프트 조합법 45종',
|
||||
desc: '상품리뷰 / 정보글 / 후기 / 비교 / 하우투 글별 최적 프롬프트 조합',
|
||||
meta: 'PDF 80p',
|
||||
},
|
||||
{
|
||||
icon: '📐',
|
||||
title: '블로그 글 구조 템플릿 12종',
|
||||
desc: '쿠팡파트너스 · 애드포스트 클릭을 유도하는 검증된 글 구조',
|
||||
meta: 'Notion 템플릿',
|
||||
},
|
||||
{
|
||||
icon: '💰',
|
||||
title: '샘플 글 10편',
|
||||
desc: '실제로 수익이 발생한 블로그 글 전문 + 해설 주석',
|
||||
meta: '.docx · .md',
|
||||
},
|
||||
{
|
||||
icon: '🔍',
|
||||
title: '네이버 SEO 체크리스트',
|
||||
desc: 'C-Rank · D.I.A. 알고리즘 대응 14가지 체크 포인트',
|
||||
meta: 'PDF 20p',
|
||||
},
|
||||
];
|
||||
|
||||
const FAQS = [
|
||||
{
|
||||
q: '초보자도 쓸 수 있나요?',
|
||||
a: 'ChatGPT나 Claude 계정만 있으면 됩니다. 프롬프트를 복붙하는 것부터 시작해서 점차 응용하도록 설계했습니다.',
|
||||
},
|
||||
{
|
||||
q: '어떤 플랫폼에 맞나요?',
|
||||
a: '네이버 블로그·티스토리·브런치 모두 대응. 쿠팡파트너스·애드포스트·브랜드커넥트 3가지 수익화 흐름을 모두 다룹니다.',
|
||||
},
|
||||
{
|
||||
q: '업데이트는 얼마나 자주 되나요?',
|
||||
a: '월 1~2회 주요 업데이트. 구매자 전용 Notion 페이지에서 변경 이력과 최신 파일을 제공합니다. 구매 후 12개월간 무료.',
|
||||
},
|
||||
{
|
||||
q: '환불이 되나요?',
|
||||
a: '전자상거래법상 디지털 콘텐츠는 제공 시작 후 환불이 제한됩니다. 구매 전 샘플 미리보기를 충분히 확인해주세요. 파일 손상·전달 불량은 즉시 재전달 또는 환불됩니다.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function BlogServicePage() {
|
||||
const [agreeOpen, setAgreeOpen] = useState(false);
|
||||
const [openFaq, setOpenFaq] = useState<number | null>(0);
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-white">
|
||||
{/* HERO */}
|
||||
<section className="relative overflow-hidden px-6 py-20 lg:px-14 lg:py-28 bg-gradient-to-br from-blue-50 via-white to-sky-50">
|
||||
<div className="relative max-w-5xl mx-auto">
|
||||
<p className="font-mono text-xs text-blue-700/70 tracking-[0.25em] uppercase mb-6">
|
||||
Blog Automation Pack
|
||||
</p>
|
||||
<h1
|
||||
className="text-[2.4rem] md:text-[3.2rem] lg:text-[4rem] font-extrabold leading-[1.08] tracking-tight text-slate-900 mb-6"
|
||||
style={{ wordBreak: 'keep-all' }}
|
||||
>
|
||||
매일 글쓰기 고민,
|
||||
<br />
|
||||
<span className="text-blue-700">AI에게 맡기세요.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="text-slate-600 text-lg md:text-xl leading-relaxed mb-4 max-w-2xl"
|
||||
style={{ wordBreak: 'keep-all' }}
|
||||
>
|
||||
쿠팡파트너스 · 네이버 애드포스트 · 브랜드커넥트 수익을
|
||||
<br />
|
||||
<span className="text-slate-900 font-semibold">자동화하는 프롬프트 · 구조 · 샘플 세트</span>.
|
||||
</p>
|
||||
<div className="inline-flex items-center gap-3 bg-white border border-blue-200 rounded-xl px-5 py-3 mb-8 shadow-sm">
|
||||
<span className="text-3xl font-extrabold text-blue-700 font-mono">₩29,000</span>
|
||||
<span className="text-xs text-slate-500">한 번 결제 · 12개월 무료 업데이트</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => setAgreeOpen(true)}
|
||||
className="inline-flex items-center gap-2 bg-blue-700 hover:bg-blue-600 text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors shadow-lg shadow-blue-500/30"
|
||||
>
|
||||
구매하기 →
|
||||
</button>
|
||||
<a
|
||||
href="#sample"
|
||||
className="inline-flex items-center gap-2 border border-slate-300 hover:border-blue-500 text-slate-700 hover:text-blue-700 px-8 py-4 rounded-xl font-semibold text-sm transition-all"
|
||||
>
|
||||
샘플 미리보기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PAIN POINTS */}
|
||||
<section className="px-6 py-16 lg:px-14 bg-white">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2
|
||||
className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-10 text-center"
|
||||
style={{ wordBreak: 'keep-all' }}
|
||||
>
|
||||
이런 분들을 위한 팩입니다.
|
||||
</h2>
|
||||
<div className="grid sm:grid-cols-3 gap-5">
|
||||
{[
|
||||
{ icon: '🕐', title: '매일 1시간+', desc: '글 소재·구성에 시간 다 쓰고 수익은 제자리' },
|
||||
{ icon: '📉', title: '수익화 6개월+', desc: '블로그 키워놓고도 수익 구조가 안 잡힘' },
|
||||
{ icon: '🤖', title: 'AI 글은 어색', desc: 'ChatGPT 그대로 복붙하면 바로 들통' },
|
||||
].map((p) => (
|
||||
<div key={p.title} className="border border-slate-200 rounded-2xl p-6 bg-slate-50/50">
|
||||
<div className="text-3xl mb-3">{p.icon}</div>
|
||||
<h3 className="font-extrabold text-slate-900 mb-2">{p.title}</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
|
||||
{p.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PACK CONTENT */}
|
||||
<section id="sample" className="px-6 py-20 lg:px-14 bg-slate-50">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<p className="font-mono text-xs text-blue-700/70 tracking-widest uppercase mb-2">
|
||||
Pack Contents
|
||||
</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-10">
|
||||
구성품 4종
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
{PACK_ITEMS.map((it) => (
|
||||
<div
|
||||
key={it.title}
|
||||
className="flex gap-4 bg-white border border-slate-200 rounded-2xl p-6 hover:border-blue-400 transition-colors"
|
||||
>
|
||||
<div className="text-3xl flex-shrink-0">{it.icon}</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<h3 className="font-extrabold text-slate-900">{it.title}</h3>
|
||||
<span className="text-[10px] font-mono text-blue-700 bg-blue-100 px-1.5 py-0.5 rounded">
|
||||
{it.meta}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
|
||||
{it.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sample preview */}
|
||||
<div className="mt-12 bg-white border-2 border-dashed border-blue-300 rounded-2xl p-8 relative overflow-hidden">
|
||||
<span className="absolute top-4 right-4 text-[10px] font-bold text-blue-700 bg-blue-100 px-2 py-1 rounded">
|
||||
샘플 미리보기
|
||||
</span>
|
||||
<h4 className="font-extrabold text-slate-900 mb-3">프롬프트 예시 · 상품 리뷰 글 자동 생성</h4>
|
||||
<pre className="text-xs font-mono text-slate-700 bg-slate-50 rounded-lg p-4 overflow-x-auto leading-relaxed">{`당신은 [카테고리] 전문 블로거입니다.
|
||||
아래 상품의 [핵심 장점 3개]와 [주의점 1개]를 기반으로
|
||||
C-Rank 알고리즘에 최적화된 1,200자 리뷰 글을 작성하세요.
|
||||
|
||||
[구조]
|
||||
1. 후킹 도입 (공감형 질문)
|
||||
2. 상품 요약 (스펙 표)
|
||||
3. 실사용 관점 장점·단점
|
||||
4. 대안 비교 (쿠팡 링크 삽입 지점: {LINK})
|
||||
5. 결론 + 재질문 유도
|
||||
|
||||
[톤앤매너] 친근한 존댓말, 광고 느낌 최소화 ...`}</pre>
|
||||
<p className="text-xs text-slate-500 mt-4">
|
||||
실제 팩에는 카테고리별 45종의 프롬프트와 최적화 파라미터가 포함됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="px-6 py-20 lg:px-14 bg-white">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-8 text-center">
|
||||
자주 묻는 질문
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{FAQS.map((f, i) => (
|
||||
<div key={i} className="border border-slate-200 rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpenFaq(openFaq === i ? null : i)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<span className="font-bold text-slate-900 text-sm">{f.q}</span>
|
||||
<span className={`text-blue-700 text-xl transition-transform ${openFaq === i ? 'rotate-45' : ''}`}>
|
||||
+
|
||||
</span>
|
||||
</button>
|
||||
{openFaq === i && (
|
||||
<div className="px-5 pb-5 text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
|
||||
{f.a}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FINAL CTA */}
|
||||
<section className="px-6 py-20 lg:px-14 bg-gradient-to-br from-blue-700 to-blue-900">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4" style={{ wordBreak: 'keep-all' }}>
|
||||
오늘부터 블로그 수익 자동화.
|
||||
</h2>
|
||||
<p className="text-blue-100 text-lg mb-8">₩29,000 한 번 결제 · 평생 업데이트</p>
|
||||
<button
|
||||
onClick={() => setAgreeOpen(true)}
|
||||
className="inline-flex items-center gap-2 bg-white text-blue-700 hover:bg-blue-50 px-10 py-4 rounded-xl font-extrabold text-base transition-colors shadow-xl"
|
||||
>
|
||||
지금 구매하기 →
|
||||
</button>
|
||||
<p className="text-blue-200/80 text-xs mt-6">
|
||||
<Link href="/legal/refund" className="underline hover:text-white">환불 정책</Link>
|
||||
{' · '}
|
||||
<Link href="/legal/terms" className="underline hover:text-white">이용약관</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PurchaseAgreementModal
|
||||
isOpen={agreeOpen}
|
||||
onClose={() => setAgreeOpen(false)}
|
||||
productName="블로그 자동화 솔루션 팩"
|
||||
price="₩29,000"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI 음악 마스터 구조 팩 | Suno · MV · 유튜브 쇼츠',
|
||||
description:
|
||||
'엔지니어가 설계한 4단계 AI 음악 제작 공정. Suno 프롬프트 조합법 + MV 비디오 생성 워크플로우 + 저작권 가이드 + 템플릿 PDF + 샘플 프로젝트. 입문 ₩39k / 프로 ₩99k / 마스터 ₩149k.',
|
||||
keywords: [
|
||||
'AI 음악 만들기',
|
||||
'Suno 프롬프트',
|
||||
'AI 뮤직비디오',
|
||||
'AI 커버곡',
|
||||
'유튜브 쇼츠 음악',
|
||||
'AI 작곡',
|
||||
'크리에이터 이코노미',
|
||||
'Lyria 프롬프트',
|
||||
'Runway AI 비디오',
|
||||
],
|
||||
openGraph: {
|
||||
title: 'AI 음악 마스터 구조 팩 | 쟁승메이드',
|
||||
description:
|
||||
'네 사연을 노래로. 쇼츠까지 한 번에. 4단계 AI 음악 공정 · Suno Pro 검증 · 평생 업데이트.',
|
||||
url: 'https://jaengseung-made.com/services/music',
|
||||
},
|
||||
};
|
||||
|
||||
export default function MusicLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PurchaseAgreementModal from '../../components/PurchaseAgreementModal';
|
||||
import { SparklesOverlay } from '@/components/ui/sparkles-text';
|
||||
import { CardBody, CardContainer, CardItem } from '@/components/ui/3d-card-effect';
|
||||
|
||||
type Tier = 'starter' | 'pro' | 'master';
|
||||
|
||||
const TIERS: Record<Tier, { name: string; price: string; priceNum: string; desc: string; features: string[]; highlight?: boolean }> = {
|
||||
starter: {
|
||||
name: '입문',
|
||||
price: '₩39,000',
|
||||
priceNum: '39,000',
|
||||
desc: '첫 AI 음악을 위한 필수 구성',
|
||||
features: [
|
||||
'Suno 프롬프트 조합법 20종',
|
||||
'구조 템플릿 PDF 40p',
|
||||
'저작권 가이드 기본판',
|
||||
'12개월 무료 업데이트',
|
||||
],
|
||||
},
|
||||
pro: {
|
||||
name: '프로',
|
||||
price: '₩99,000',
|
||||
priceNum: '99,000',
|
||||
desc: '쇼츠 업로드까지 완성하는 풀세트',
|
||||
highlight: true,
|
||||
features: [
|
||||
'입문 전체 포함',
|
||||
'MV 워크플로우 (Runway · Luma · Pika)',
|
||||
'샘플 프로젝트 1개 (.prj · 영상)',
|
||||
'1:1 Q&A 1회 + 유튜브 SEO 템플릿',
|
||||
],
|
||||
},
|
||||
master: {
|
||||
name: '마스터',
|
||||
price: '₩149,000',
|
||||
priceNum: '149,000',
|
||||
desc: '여러 장르·포맷을 커버하는 마스터피스',
|
||||
features: [
|
||||
'프로 전체 포함',
|
||||
'샘플 프로젝트 장르별 3종',
|
||||
'저작권 심화판 + 상업 이용 체크리스트',
|
||||
'우선 업데이트 · 제작 레시피 영상',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const PROCESS = [
|
||||
{ num: '01', subtitle: 'Concept & Lyrics', title: '크리에이티브 디렉팅', result: 'AI 최적화 가사 · 메타데이터 시트' },
|
||||
{ num: '02', subtitle: 'Music Generation', title: '오디오 엔지니어링', result: '고품질 완곡 (Full Track, 스템 분리본)' },
|
||||
{ num: '03', subtitle: 'AI MV Generation', title: '비주얼 마스터링', result: '쇼츠(9:16) 또는 유튜브(16:9) 고화질 영상' },
|
||||
{ num: '04', subtitle: 'Viral Optimization', title: '퍼블리싱 가이드', result: '즉시 업로드 가능한 유튜브 배포 패키지' },
|
||||
];
|
||||
|
||||
const FAQS = [
|
||||
{
|
||||
q: 'Suno 유료 플랜 가입이 꼭 필요한가요?',
|
||||
a: 'Suno 무료 플랜은 상업적 이용이 제한됩니다. 본인 결과물을 유튜브·SNS에 업로드해 수익화하려면 Suno Pro 이상 권장. 팩 구매 후 가입 전 플랜 선택 가이드가 포함됩니다.',
|
||||
},
|
||||
{
|
||||
q: '제가 만든 결과물의 상업 이용·저작권은?',
|
||||
a: '결과물의 상업권은 고객이 가입한 AI 서비스의 이용약관을 따릅니다. 팩에는 Suno·Runway·Luma 각 서비스의 최신 약관 요약과 상업 이용 체크리스트가 포함되어 있습니다. (법률 자문이 아닌 참고용 가이드입니다.)',
|
||||
},
|
||||
{
|
||||
q: '결과물 품질을 보장하나요?',
|
||||
a: 'AI 생성물은 모델 버전·프롬프트 입력에 따라 달라지므로 결과물 자체를 보장하지 않습니다. 다만 팩은 동일 프롬프트로 반복 가능한 고품질 구간을 설계하는 방법을 제공합니다. 샘플 쇼츠·프로젝트로 품질 기대치를 사전 확인하세요.',
|
||||
},
|
||||
{
|
||||
q: '환불이 가능한가요?',
|
||||
a: '전자상거래법 제17조 제2항 제5호에 따라 디지털 콘텐츠는 제공 시작 후 청약철회가 제한됩니다. 무료 샘플로 사전 확인을 제공하므로 충분히 검토 후 구매해주세요. 파일 손상·전달 불량 등 회사 귀책은 즉시 재전달 또는 환불됩니다.',
|
||||
},
|
||||
{
|
||||
q: '업데이트는 어떻게 받나요?',
|
||||
a: '구매자 전용 Notion 페이지에서 변경 이력과 최신 파일을 제공. 12개월간 무료 업데이트가 기본, 마스터는 우선 업데이트·베타 선공개가 포함됩니다.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function MusicServicePage() {
|
||||
const [selectedTier, setSelectedTier] = useState<Tier | null>(null);
|
||||
const [openFaq, setOpenFaq] = useState<number | null>(0);
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-black text-white">
|
||||
{/* PRICING */}
|
||||
<section id="pricing" className="px-6 py-14 lg:px-14 bg-black">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-end justify-between flex-wrap gap-3 mb-8">
|
||||
<div>
|
||||
<p className="font-mono text-xs text-white/50 tracking-widest uppercase mb-1">Pricing · 1회 결제</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold">3개 티어, 목표에 맞게 선택</h2>
|
||||
</div>
|
||||
<Link href="/services/music/samples" className="text-sm text-white/80 hover:text-white underline underline-offset-4">
|
||||
샘플 먼저 보기
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-5 items-stretch">
|
||||
{(Object.keys(TIERS) as Tier[]).map((key) => {
|
||||
const t = TIERS[key];
|
||||
return (
|
||||
<CardContainer key={key} containerClassName="w-full py-0" className="w-full h-full">
|
||||
<CardBody
|
||||
className={`relative w-full h-full rounded-2xl p-8 flex flex-col border transition-all ${
|
||||
t.highlight
|
||||
? 'border-white bg-white text-black md:scale-[1.03] md:-translate-y-2'
|
||||
: 'border-white/15 bg-white/[0.02] hover:border-white/40 text-white'
|
||||
}`}
|
||||
>
|
||||
{t.highlight && (
|
||||
<SparklesOverlay
|
||||
sparklesCount={20}
|
||||
colors={{ first: '#9E7AFF', second: '#FE8BBB' }}
|
||||
className="rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
{t.highlight && (
|
||||
<CardItem translateZ={60} className="absolute -top-3 left-1/2 -translate-x-1/2 z-20">
|
||||
<span className="inline-flex items-center bg-black text-white text-[10px] font-extrabold px-3 py-1.5 rounded-full uppercase tracking-wider border border-white">
|
||||
가장 많이 팔림
|
||||
</span>
|
||||
</CardItem>
|
||||
)}
|
||||
<CardItem translateZ={40} as="h3" className="font-extrabold text-2xl mb-1 relative z-10">
|
||||
{t.name}
|
||||
</CardItem>
|
||||
<CardItem translateZ={20} as="p" className={`text-sm mb-6 ${t.highlight ? 'text-black/60' : 'text-white/60'}`}>
|
||||
{t.desc}
|
||||
</CardItem>
|
||||
<CardItem translateZ={50} className="mb-6">
|
||||
<span className="text-4xl font-extrabold font-mono">{t.price}</span>
|
||||
<span className={`text-xs ml-2 ${t.highlight ? 'text-black/50' : 'text-white/50'}`}>1회 결제</span>
|
||||
</CardItem>
|
||||
<CardItem
|
||||
translateZ={20}
|
||||
as="ul"
|
||||
className={`space-y-3 text-sm mb-8 flex-1 ${t.highlight ? 'text-black/80' : 'text-white/80'}`}
|
||||
>
|
||||
{t.features.map((f) => (
|
||||
<li key={f} className="flex gap-2.5">
|
||||
<span className="flex-shrink-0 mt-0.5">·</span>
|
||||
<span className="leading-relaxed">{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</CardItem>
|
||||
<CardItem
|
||||
translateZ={40}
|
||||
as="button"
|
||||
onClick={() => setSelectedTier(key)}
|
||||
className={`w-full py-4 rounded-xl font-extrabold text-sm transition-colors ${
|
||||
t.highlight
|
||||
? 'bg-black hover:bg-black/85 text-white'
|
||||
: 'bg-white/10 hover:bg-white/20 text-white border border-white/20'
|
||||
}`}
|
||||
>
|
||||
{t.name} 구매하기
|
||||
</CardItem>
|
||||
</CardBody>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-white/50 text-center mt-8">
|
||||
구매 전 <Link href="/legal/refund" className="underline hover:text-white">환불 정책</Link>을 반드시 확인해주세요.
|
||||
디지털 콘텐츠 특성상 제공 시작 후 청약철회가 제한됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 팩 구성품 */}
|
||||
<section className="px-6 py-16 lg:px-14 bg-black border-t border-white/10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<p className="font-mono text-xs text-white/50 tracking-widest uppercase mb-2">What's Included</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold mb-8">팩 구성품</h2>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{[
|
||||
{ title: 'Suno 프롬프트 북', desc: '장르·무드·보컬 톤 조합법 20+종. 복붙해서 바로 사용하는 PDF.' },
|
||||
{ title: 'MV 워크플로우', desc: 'Midjourney·Runway·Luma로 비트 싱크 영상 만드는 단계별 가이드.' },
|
||||
{ title: '저작권 & 상업 이용', desc: 'Suno·Runway 약관 요약 + 수익화 전 안전 체크리스트.' },
|
||||
{ title: '샘플 프로젝트 파일', desc: '완성된 가사·프롬프트·영상 세트. 그대로 수정해 재사용 가능.' },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="p-6 rounded-2xl border border-white/15 bg-white/[0.02]"
|
||||
>
|
||||
<h3 className="font-bold text-white mb-1">{item.title}</h3>
|
||||
<p className="text-sm text-white/60 leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PROCESS */}
|
||||
<section className="px-6 py-16 lg:px-14 bg-black border-t border-white/10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<p className="font-mono text-xs text-white/50 tracking-widest uppercase mb-2">Process</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold mb-10" style={{ wordBreak: 'keep-all' }}>
|
||||
컨셉 → 음악 → 비주얼 → 퍼블리싱
|
||||
</h2>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{PROCESS.map((step) => (
|
||||
<div
|
||||
key={step.num}
|
||||
className="rounded-2xl p-6 border border-white/15 bg-white/[0.02] hover:border-white/40 transition-colors"
|
||||
>
|
||||
<p className="font-mono text-xs text-white/50 mb-3">{step.num}</p>
|
||||
<p className="font-mono text-[10px] text-white/50 uppercase tracking-widest mb-1">
|
||||
{step.subtitle}
|
||||
</p>
|
||||
<h3 className="text-lg font-extrabold text-white mb-2">{step.title}</h3>
|
||||
<p className="text-sm text-white/60 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
|
||||
{step.result}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SAMPLES */}
|
||||
<section id="samples" className="px-6 py-12 lg:px-14 bg-black border-t border-white/10">
|
||||
<div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="font-mono text-xs text-white/50 tracking-widest uppercase mb-1">Samples</p>
|
||||
<h2 className="text-xl md:text-2xl font-extrabold">이 팩으로 만든 실제 쇼츠들</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/services/music/samples"
|
||||
className="inline-flex items-center px-6 py-3 rounded-xl border border-white/30 hover:bg-white hover:text-black text-sm font-semibold text-white transition whitespace-nowrap"
|
||||
>
|
||||
전체 샘플 갤러리
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="px-6 py-20 lg:px-14 bg-black border-t border-white/10">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-center mb-10">
|
||||
자주 묻는 질문
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{FAQS.map((f, i) => (
|
||||
<div key={i} className="border border-white/15 rounded-2xl overflow-hidden bg-white/[0.02]">
|
||||
<button
|
||||
onClick={() => setOpenFaq(openFaq === i ? null : i)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<span className="font-bold text-white text-sm">{f.q}</span>
|
||||
<span className={`text-white text-xl transition-transform ${openFaq === i ? 'rotate-45' : ''}`}>
|
||||
+
|
||||
</span>
|
||||
</button>
|
||||
{openFaq === i && (
|
||||
<div className="px-5 pb-5 text-sm text-white/70 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
|
||||
{f.a}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sticky CTA */}
|
||||
<div
|
||||
className="fixed bottom-0 inset-x-0 z-40 border-t border-white/15 backdrop-blur-md"
|
||||
style={{ background: 'rgba(0,0,0,0.85)' }}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-5 py-3 flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-mono text-white/50 tracking-widest uppercase">From</p>
|
||||
<p className="text-white font-extrabold text-lg leading-tight">
|
||||
₩39,000 <span className="text-xs text-white/50 font-medium">· 1회 결제</span>
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="#pricing"
|
||||
className="inline-flex items-center bg-white hover:bg-white/90 text-black px-6 py-3 rounded-xl font-extrabold text-sm transition-colors whitespace-nowrap"
|
||||
>
|
||||
팩 선택하기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-20" aria-hidden />
|
||||
|
||||
{selectedTier && (
|
||||
<PurchaseAgreementModal
|
||||
isOpen={!!selectedTier}
|
||||
onClose={() => setSelectedTier(null)}
|
||||
productName={`AI 음악 마스터 팩 · ${TIERS[selectedTier].name}`}
|
||||
price={TIERS[selectedTier].price}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI 음악·뮤비 샘플 갤러리',
|
||||
description: '쟁승메이드 AI 음악 팩으로 제작한 샘플 뮤직비디오 모음. 장르별 결과물을 직접 확인하세요.',
|
||||
};
|
||||
|
||||
type Sample = {
|
||||
id: string;
|
||||
title: string;
|
||||
genre: string;
|
||||
duration: string;
|
||||
views?: string;
|
||||
featured?: boolean;
|
||||
embedId?: string;
|
||||
};
|
||||
|
||||
const SAMPLES: Sample[] = [
|
||||
{ id: 's1', title: 'K-POP 스타일 TOP 샘플', genre: 'K-POP', duration: '0:45', featured: true },
|
||||
{ id: 's2', title: 'Lo-fi 감성 MV', genre: 'Lo-fi', duration: '1:02' },
|
||||
{ id: 's3', title: '시티팝 무드 영상', genre: 'City Pop', duration: '0:58' },
|
||||
{ id: 's4', title: 'EDM 쇼츠 훅', genre: 'EDM', duration: '0:30' },
|
||||
{ id: 's5', title: '발라드 감성 컷', genre: 'Ballad', duration: '1:10' },
|
||||
{ id: 's6', title: '트랩 비트 쇼츠', genre: 'Trap', duration: '0:35' },
|
||||
];
|
||||
|
||||
export default function MusicSamplesPage() {
|
||||
return (
|
||||
<div className="px-6 py-20 lg:px-14" style={{ background: 'var(--kx-surface)' }}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<span className="kx-label">SAMPLE GALLERY</span>
|
||||
<h1 className="kx-display text-3xl md:text-5xl font-bold mt-3 mb-4" style={{ color: 'var(--kx-on-surface)' }}>
|
||||
AI 음악·뮤비 샘플 모음
|
||||
</h1>
|
||||
<p className="max-w-2xl mx-auto text-sm md:text-base" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
팩 워크플로우로 제작된 결과물입니다. 장르별로 다양한 톤을 확인해보세요.
|
||||
<br className="hidden md:block" />
|
||||
<span className="text-xs" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
일부 샘플은 런칭 직후 순차 공개됩니다.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-6">
|
||||
{SAMPLES.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`group relative aspect-[9/16] rounded-2xl overflow-hidden border ${
|
||||
s.featured ? 'border-violet-400/50 shadow-2xl shadow-violet-900/40' : 'border-white/10'
|
||||
}`}
|
||||
style={{ background: 'linear-gradient(135deg, #1a0840 0%, #061228 100%)' }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/15 to-cyan-500/10 group-hover:from-violet-500/25 group-hover:to-cyan-500/20 transition-all" />
|
||||
|
||||
{s.featured && (
|
||||
<span
|
||||
className="absolute top-3 left-3 z-10 text-[10px] px-2 py-1 rounded-full font-semibold tracking-widest"
|
||||
style={{ background: 'rgba(204,151,255,0.2)', color: 'var(--kx-primary)', border: '1px solid rgba(204,151,255,0.5)' }}
|
||||
>
|
||||
TOP
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-5">
|
||||
<div className="text-5xl mb-3 opacity-80 group-hover:scale-110 transition-transform">🎬</div>
|
||||
<p className="text-[10px] md:text-xs font-mono tracking-widest text-violet-300/80 mb-1">{s.genre.toUpperCase()}</p>
|
||||
<p className="text-sm md:text-base font-semibold" style={{ color: 'var(--kx-on-surface)' }}>
|
||||
{s.title}
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--kx-on-variant)' }}>{s.duration}</p>
|
||||
<p className="text-[10px] mt-3 opacity-60" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
{s.embedId ? '영상 재생' : '영상 준비 중'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-16 text-center p-10 kx-glass"
|
||||
style={{ border: '1px solid rgba(204,151,255,0.12)', borderRadius: '0.75rem 0.75rem 0.125rem 0.125rem' }}
|
||||
>
|
||||
<span className="kx-label">NEXT</span>
|
||||
<h2 className="kx-display text-2xl md:text-3xl font-bold mt-2 mb-3" style={{ color: 'var(--kx-on-surface)' }}>
|
||||
내 채널에도 이런 쇼츠 올리고 싶다면
|
||||
</h2>
|
||||
<p className="text-sm mb-6" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
동일 워크플로우 팩 ₩39,000부터.
|
||||
</p>
|
||||
<Link
|
||||
href="/services/music#pricing"
|
||||
className="kx-btn-primary px-8 py-3.5 rounded-full text-sm inline-flex"
|
||||
>
|
||||
팩 가격 보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '홈페이지·쇼핑몰·랜딩페이지 제작 | 반응형 웹 개발 외주',
|
||||
description:
|
||||
'소상공인·스타트업·기업 홈페이지, 쇼핑몰, 랜딩페이지 제작. 템플릿 없이 직접 개발. Next.js 기반 반응형 웹, SEO 기본 적용. 스타터 20만원~, 계약서 포함, 3개월 유지보수.',
|
||||
keywords: [
|
||||
'홈페이지 제작 외주',
|
||||
'쇼핑몰 제작 외주',
|
||||
'랜딩페이지 제작',
|
||||
'소상공인 홈페이지',
|
||||
'스타트업 웹사이트',
|
||||
'반응형 홈페이지 제작',
|
||||
'Next.js 개발 외주',
|
||||
'기업 홈페이지 제작',
|
||||
'웹사이트 제작 비용',
|
||||
'SEO 최적화 홈페이지',
|
||||
'홈페이지 개발 프리랜서',
|
||||
],
|
||||
openGraph: {
|
||||
title: '홈페이지·쇼핑몰 제작 | 쟁승메이드',
|
||||
description:
|
||||
'소상공인·스타트업 홈페이지 제작. 템플릿 없이 직접 개발, SEO 포함, 20만원~.',
|
||||
url: 'https://jaengseung-made.com/services/website',
|
||||
},
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function WebsiteLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,962 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ContactModal from '../../components/ContactModal';
|
||||
import { trackCTAClick } from '../../../lib/gtag';
|
||||
|
||||
const samples = [
|
||||
{
|
||||
type: 'corporate',
|
||||
title: '기업 홈페이지',
|
||||
subtitle: '테크솔루션㈜',
|
||||
desc: '"홈페이지가 없어서 B2B 영업 때마다 명함만 건넸다"는 고민을 해결한 기업 브랜드 사이트',
|
||||
gradient: 'linear-gradient(135deg, #0a192f 0%, #112240 50%, #1a3a6c 100%)',
|
||||
accent: '#4fc3f7',
|
||||
tags: ['기업', 'B2B', '신뢰'],
|
||||
icon: '🏢',
|
||||
},
|
||||
{
|
||||
type: 'bakery',
|
||||
title: '베이커리 홈페이지',
|
||||
subtitle: '르 쁘띠 포르',
|
||||
desc: '"인스타 팔로워는 많은데 실제 방문 예약이 없다"는 F&B 매장의 전환율 문제를 해결한 사이트',
|
||||
gradient: 'linear-gradient(135deg, #78350f 0%, #92400e 50%, #d97706 100%)',
|
||||
accent: '#fbbf24',
|
||||
tags: ['F&B', '로컬', '예약'],
|
||||
icon: '🥐',
|
||||
},
|
||||
{
|
||||
type: 'portfolio',
|
||||
title: '개인 포트폴리오',
|
||||
subtitle: 'Kim Jisu',
|
||||
desc: '"링크드인에 PDF 올리면 아무도 안 보더라"는 문제를 해결한 채용·수주 전환형 포트폴리오',
|
||||
gradient: 'linear-gradient(135deg, #000000 0%, #0d0d0d 50%, #001a00 100%)',
|
||||
accent: '#00ff88',
|
||||
tags: ['크리에이터', '디자이너', '수주'],
|
||||
icon: '✦',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
title: '관리자 대시보드',
|
||||
subtitle: 'DataFlow SaaS',
|
||||
desc: '"엑셀로 수기 집계하다가 오류가 나서 야근"을 없애는 실시간 데이터 대시보드',
|
||||
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: '⚡',
|
||||
},
|
||||
{
|
||||
type: 'interior',
|
||||
title: '인테리어 업체 소개',
|
||||
subtitle: 'AURUM Interior',
|
||||
desc: '"포트폴리오 사진을 카톡으로만 보내다가 고급 고객을 놓쳤다"는 문제를 해결한 브랜드 사이트',
|
||||
gradient: 'linear-gradient(135deg, #2C1810 0%, #4A3728 50%, #6B4E37 100%)',
|
||||
accent: '#D4A853',
|
||||
tags: ['인테리어', '포트폴리오', '고급'],
|
||||
icon: '◈',
|
||||
},
|
||||
{
|
||||
type: 'reading',
|
||||
title: '독서 기록 노트',
|
||||
subtitle: '나의 독서 기록',
|
||||
desc: '읽은 책과 감상을 아름답게 기록하는 나만의 독서 저널 — 이런 것도 만들 수 있습니다',
|
||||
gradient: 'linear-gradient(135deg, #0C0B09 0%, #1A1710 50%, #2A2218 100%)',
|
||||
accent: '#D4A853',
|
||||
tags: ['라이프', '독서', '기록'],
|
||||
icon: '◻',
|
||||
},
|
||||
{
|
||||
type: 'shopping',
|
||||
title: '개인 쇼핑몰',
|
||||
subtitle: 'MELLOW STUDIO',
|
||||
desc: '"카페24 기본 테마가 너무 흔해서 브랜드가 안 살아난다"는 고민을 해결한 커스텀 쇼핑몰',
|
||||
gradient: 'linear-gradient(135deg, #2A2018 0%, #4A3C2C 50%, #7A6A52 100%)',
|
||||
accent: '#C4A882',
|
||||
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: '20',
|
||||
unit: '만원~',
|
||||
color: '#38bdf8',
|
||||
features: ['5페이지 이내', '반응형 디자인', '기본 SEO 설정', '1개월 유지보수', '3~5영업일 납품'],
|
||||
note: '개인 블로그, 소규모 소개 사이트',
|
||||
productId: 'website_starter',
|
||||
},
|
||||
{
|
||||
name: '비즈니스',
|
||||
price: '100',
|
||||
unit: '만원~',
|
||||
color: '#818cf8',
|
||||
featured: true,
|
||||
features: ['10페이지 이내', '반응형 디자인', '관리자 페이지', 'SEO 최적화', '3개월 유지보수', '1~2주 납품'],
|
||||
note: '기업 사이트, 브랜드 페이지',
|
||||
productId: 'website_business',
|
||||
},
|
||||
{
|
||||
name: '프리미엄',
|
||||
price: '200',
|
||||
unit: '만원~',
|
||||
color: '#f472b6',
|
||||
features: ['페이지 수 무제한', '맞춤 디자인', '결제/회원 시스템', 'DB 연동', '6개월 유지보수', '일정 협의'],
|
||||
note: '쇼핑몰, SaaS, 복합 시스템',
|
||||
productId: 'website_premium',
|
||||
},
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: '제작 기간은 얼마나 걸리나요?',
|
||||
a: '규모에 따라 다르지만, 스타터는 3~5영업일, 비즈니스는 1~2주, 프리미엄은 협의 후 결정합니다. 빠른 납품이 필요한 경우 별도 상담해 주세요.',
|
||||
},
|
||||
{
|
||||
q: '수정은 몇 번까지 가능한가요?',
|
||||
a: '기획 확정 후 디자인 시안 수정은 2회, 개발 완료 후 기능 수정은 유지보수 기간 내 자유롭게 가능합니다. 추가 기능 개발은 별도 견적으로 진행합니다.',
|
||||
},
|
||||
{
|
||||
q: '도메인과 호스팅도 도와주시나요?',
|
||||
a: '네, 도메인 구매부터 서버 세팅, 배포까지 전 과정을 도와드립니다. Vercel, AWS, 카페24 등 원하시는 플랫폼에 맞춰 배포해 드립니다.',
|
||||
},
|
||||
{
|
||||
q: '앱(모바일 앱)이나 쇼핑몰도 개발 가능한가요?',
|
||||
a: '네. iOS/Android 앱(React Native), 모바일 웹앱, 결제 연동 쇼핑몰, 회원/관리자 시스템 등 모두 개발 가능합니다. 프리미엄 플랜 이상이거나 규모에 따라 별도 견적으로 진행됩니다. 먼저 어떤 기능이 필요한지 상담해 주세요.',
|
||||
},
|
||||
{
|
||||
q: '계약금은 어떻게 되나요? 중간에 취소하면 어떻게 되나요?',
|
||||
a: '계약서 체결 후 착수금 30%를 먼저 입금받고 개발을 시작합니다. 납품 전 취소 시 완성 비율에 따라 정산하며, 착수 전 전액 환불됩니다. 모든 조건은 계약서에 명시됩니다.',
|
||||
},
|
||||
];
|
||||
|
||||
function useReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const scroller = (document.querySelector('.main-content') as HTMLElement | null) ?? document.documentElement;
|
||||
const obs = new IntersectionObserver(
|
||||
([entry]) => { if (entry.isIntersecting) { el.classList.add('ws-visible'); obs.disconnect(); } },
|
||||
{ threshold: 0.1, root: scroller === document.documentElement ? null : scroller }
|
||||
);
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
|
||||
function SampleMiniPreview({ type }: { type: string }) {
|
||||
const W = 700, H = 350;
|
||||
const inner = (content: React.ReactNode, bg: string) => (
|
||||
<div style={{ height: 175, overflow: 'hidden', position: 'relative', background: bg, borderRadius: '20px 20px 0 0' }}>
|
||||
<div style={{ width: W, height: H, transform: 'scale(0.5)', transformOrigin: 'top left', position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'corporate':
|
||||
return inner(
|
||||
<div style={{ background: '#fff', width: '100%', height: '100%', fontFamily: 'system-ui' }}>
|
||||
<div style={{ height: 50, background: '#0a192f', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 900, color: '#4fc3f7', letterSpacing: '0.1em' }}>TECHSOLUTION</div>
|
||||
<div style={{ display: 'flex', gap: 22 }}>
|
||||
{['서비스','솔루션','고객사','연락처'].map(t => <span key={t} style={{ fontSize: 11, color: '#475569' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, background: '#1d4ed8', color: '#fff', padding: '7px 18px', borderRadius: 4, fontWeight: 700 }}>상담 신청</div>
|
||||
</div>
|
||||
<div style={{ padding: '36px 32px', backgroundImage: 'linear-gradient(rgba(29,78,216,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(29,78,216,0.04) 1px, transparent 1px)', backgroundSize: '24px 24px' }}>
|
||||
<div style={{ fontSize: 10, color: '#1d4ed8', letterSpacing: '0.18em', marginBottom: 12, fontWeight: 700 }}>ENTERPRISE IT SOLUTIONS</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 900, color: '#0a192f', lineHeight: 1.1, marginBottom: 14 }}>기업 IT 인프라,<br/>처음부터 끝까지</div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 22, lineHeight: 1.6 }}>15년 경험의 엔터프라이즈 IT 전문 기업.<br/>클라우드·보안·DX 통합 솔루션을 제공합니다.</div>
|
||||
<div style={{ display: 'flex', gap: 10, marginBottom: 28 }}>
|
||||
<div style={{ background: '#1d4ed8', color: '#fff', fontSize: 12, padding: '10px 22px', borderRadius: 6, fontWeight: 700 }}>무료 상담 신청</div>
|
||||
<div style={{ border: '1px solid #cbd5e1', color: '#475569', fontSize: 12, padding: '10px 22px', borderRadius: 6 }}>서비스 소개서</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 36 }}>
|
||||
{[['15+','년 업력'],['340+','완료 프로젝트'],['180+','기업 고객'],['99.9%','가동률']].map(([n,l]) => (
|
||||
<div key={l}><div style={{ fontSize: 22, fontWeight: 800, color: '#0a192f', letterSpacing: '-0.02em' }}>{n}</div><div style={{ fontSize: 9, color: '#94a3b8', marginTop: 3 }}>{l}</div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#fff'
|
||||
);
|
||||
|
||||
case 'bakery':
|
||||
return inner(
|
||||
<div style={{ background: '#fffbf5', width: '100%', height: '100%', fontFamily: 'Georgia, serif' }}>
|
||||
<div style={{ height: 52, background: 'rgba(255,251,245,0.96)', borderBottom: '1px solid #fde8c8', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between' }}>
|
||||
<div><div style={{ fontSize: 18, fontStyle: 'italic', color: '#78350f', fontWeight: 700 }}>Le Petit Fort</div><div style={{ fontSize: 8, color: '#b45309', letterSpacing: '0.2em' }}>ARTISAN BOULANGERIE</div></div>
|
||||
<div style={{ display: 'flex', gap: 20 }}>
|
||||
{['메뉴','스토리','예약'].map(t => <span key={t} style={{ fontSize: 11, color: '#92400e', fontFamily: 'system-ui' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, background: '#b45309', color: '#fff', padding: '7px 18px', borderRadius: 100, fontFamily: 'system-ui', fontWeight: 700 }}>방문 예약</div>
|
||||
</div>
|
||||
<div style={{ padding: '28px 32px 0', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 28, alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#b45309', letterSpacing: '0.2em', marginBottom: 12, fontFamily: 'system-ui', fontWeight: 600 }}>Since 2018 · Paris Recipe</div>
|
||||
<div style={{ fontSize: 38, fontWeight: 700, color: '#1c1008', lineHeight: 1.05, marginBottom: 14 }}>매일 아침<br/><em>구워내는</em><br/>정직한 빵</div>
|
||||
<div style={{ fontSize: 11, color: '#92400e', marginBottom: 18, lineHeight: 1.7, fontFamily: 'system-ui' }}>프랑스산 에슈레 버터와 천연 발효종만으로<br/>만드는 정직한 아르티장 베이커리.</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div style={{ background: '#b45309', color: '#fff', fontSize: 11, padding: '9px 20px', borderRadius: 100, fontFamily: 'system-ui', fontWeight: 700 }}>오늘의 빵 보기</div>
|
||||
<div style={{ border: '1px solid #d97706', color: '#92400e', fontSize: 11, padding: '9px 20px', borderRadius: 100, fontFamily: 'system-ui' }}>매장 안내</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
{[{n:'버터 크루아상',p:'3,200',c:'#d97706'},{n:'소금빵',p:'2,800',c:'#b45309'},{n:'딸기 케이크',p:'7,500',c:'#be185d'},{n:'캄파뉴',p:'8,900',c:'#065f46'}].map(item => (
|
||||
<div key={item.n} style={{ background: '#fff8f0', borderRadius: 10, padding: 10, border: '1px solid #fde8c8' }}>
|
||||
<div style={{ height: 38, background: 'linear-gradient(135deg, #fde68a, #fbbf24)', borderRadius: 6, marginBottom: 6 }} />
|
||||
<div style={{ fontSize: 9, color: '#1c1008', fontFamily: 'system-ui', fontWeight: 600 }}>{item.n}</div>
|
||||
<div style={{ fontSize: 10, color: item.c, fontFamily: 'system-ui', fontWeight: 700 }}>₩{item.p}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#fffbf5'
|
||||
);
|
||||
|
||||
case 'portfolio':
|
||||
return inner(
|
||||
<div style={{ background: '#000', width: '100%', height: '100%' }}>
|
||||
<div style={{ position: 'absolute', top: -40, left: '25%', width: 320, height: 320, background: 'radial-gradient(circle, rgba(0,255,136,0.06) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||||
<div style={{ height: 50, background: 'rgba(0,0,0,0.95)', borderBottom: '1px solid rgba(0,255,136,0.1)', display: 'flex', alignItems: 'center', padding: '0 32px', justifyContent: 'space-between', position: 'relative', zIndex: 2 }}>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 16, fontWeight: 700, color: '#00ff88' }}>KJ<span style={{ color: 'rgba(0,255,136,0.3)' }}>_</span></div>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{['About','Work','Skills','Contact'].map(t => <span key={t} style={{ fontFamily: 'system-ui', fontSize: 10, color: '#374151', letterSpacing: '0.08em', textTransform: 'uppercase' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#00ff88' }} />
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 9, color: '#00ff88' }}>Available</span>
|
||||
<div style={{ marginLeft: 8, border: '1px solid #00ff88', color: '#00ff88', fontSize: 9, padding: '5px 12px', borderRadius: 3, fontFamily: 'monospace', fontWeight: 700 }}>HIRE ME</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', inset: 0, top: 50, backgroundImage: 'linear-gradient(rgba(0,255,136,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,255,136,0.04) 1px, transparent 1px)', backgroundSize: '32px 32px', pointerEvents: 'none' }} />
|
||||
<div style={{ padding: '38px 32px', position: 'relative', zIndex: 2, display: 'grid', gridTemplateColumns: '1fr auto', gap: 32, alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 10, color: '#00ff88', letterSpacing: '0.15em', marginBottom: 16, border: '1px solid rgba(0,255,136,0.2)', display: 'inline-block', padding: '3px 10px' }}>FULL-STACK DEVELOPER</div>
|
||||
<div style={{ fontSize: 56, fontWeight: 900, color: '#fff', lineHeight: 0.9, letterSpacing: '-0.03em', marginBottom: 18, fontFamily: 'system-ui' }}>Kim<br/><span style={{ color: '#00ff88' }}>Jisu</span></div>
|
||||
<div style={{ fontSize: 11, color: '#4b5563', lineHeight: 1.7, marginBottom: 22, fontFamily: 'system-ui' }}>React · Next.js · TypeScript · Node.js<br/>디자인과 코드의 경계를 탐험합니다.</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div style={{ background: '#00ff88', color: '#000', fontSize: 11, padding: '9px 22px', borderRadius: 4, fontWeight: 800, fontFamily: 'monospace' }}>VIEW WORK</div>
|
||||
<div style={{ border: '1px solid rgba(0,255,136,0.3)', color: '#00ff88', fontSize: 11, padding: '9px 22px', borderRadius: 4, fontFamily: 'monospace' }}>CONTACT</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 130, height: 160, background: 'linear-gradient(135deg, #001a0d, #003322)', border: '1px solid rgba(0,255,136,0.2)', borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<div style={{ width: 80, height: 80, borderRadius: '50%', border: '2px solid rgba(0,255,136,0.3)', background: 'rgba(0,255,136,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: 26, color: '#00ff88', fontFamily: 'monospace', fontWeight: 700 }}>KJ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#000'
|
||||
);
|
||||
|
||||
case 'dashboard':
|
||||
return inner(
|
||||
<div style={{ background: '#0f172a', width: '100%', height: '100%', display: 'flex', fontFamily: 'system-ui' }}>
|
||||
<div style={{ width: 140, background: '#020617', borderRight: '1px solid rgba(255,255,255,0.05)', padding: '20px 14px' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 800, color: '#38bdf8', marginBottom: 24, letterSpacing: '-0.02em' }}>DataFlow</div>
|
||||
{['대시보드','분석','리포트','사용자','설정'].map((item, i) => (
|
||||
<div key={item} style={{ fontSize: 10, color: i === 0 ? '#38bdf8' : '#475569', padding: '8px 10px', borderRadius: 6, marginBottom: 4, background: i === 0 ? 'rgba(56,189,248,0.1)' : 'transparent' }}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ flex: 1, padding: '20px 22px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: '#e2e8f0' }}>실시간 현황</div>
|
||||
<div style={{ fontSize: 10, color: '#475569', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)', padding: '4px 12px', borderRadius: 6 }}>이번 달</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 10, marginBottom: 14 }}>
|
||||
{[{l:'총 매출',v:'₩48.2M',c:'#38bdf8',u:true},{l:'신규 사용자',v:'1,247',c:'#34d399',u:true},{l:'전환율',v:'12.4%',c:'#a78bfa',u:false}].map(s => (
|
||||
<div key={s.l} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 8, padding: '12px 14px' }}>
|
||||
<div style={{ fontSize: 8, color: '#475569', marginBottom: 6 }}>{s.l}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 800, color: s.c, letterSpacing: '-0.02em' }}>{s.v}</div>
|
||||
<div style={{ fontSize: 8, color: s.u ? '#34d399' : '#f87171', marginTop: 4 }}>{s.u ? '↑ +8.3%' : '↓ -1.2%'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: 8, padding: 14, height: 110 }}>
|
||||
<div style={{ fontSize: 9, color: '#475569', marginBottom: 10 }}>월간 매출 추이</div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 72 }}>
|
||||
{[40,55,35,65,80,60,90,75,85,95,70,100].map((h, i) => (
|
||||
<div key={i} style={{ flex: 1, height: `${h}%`, background: i === 11 ? '#38bdf8' : 'rgba(56,189,248,0.22)', borderRadius: '2px 2px 0 0' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#0f172a'
|
||||
);
|
||||
|
||||
case 'game':
|
||||
return inner(
|
||||
<div style={{ background: '#000', width: '100%', height: '100%' }}>
|
||||
<div style={{ position: 'absolute', top: -60, left: '30%', width: 340, height: 340, background: 'radial-gradient(circle, rgba(168,85,247,0.14) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: -20, right: '10%', width: 200, height: 200, background: 'radial-gradient(circle, rgba(6,182,212,0.1) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||||
<div style={{ height: 50, background: 'rgba(0,0,0,0.9)', borderBottom: '1px solid rgba(6,182,212,0.2)', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between', position: 'relative', zIndex: 2 }}>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 15, fontWeight: 900, color: '#06b6d4', letterSpacing: '0.15em' }}>NEXUS<span style={{ color: '#a855f7' }}>ARENA</span></div>
|
||||
<div style={{ display: 'flex', gap: 20 }}>
|
||||
{['랭킹','매칭','챔피언','스토어'].map(t => <span key={t} style={{ fontFamily: 'system-ui', fontSize: 10, color: '#374151', letterSpacing: '0.08em' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(90deg, #06b6d4, #a855f7)', color: '#000', fontSize: 10, padding: '7px 18px', borderRadius: 3, fontWeight: 800, fontFamily: 'monospace' }}>PLAY NOW</div>
|
||||
</div>
|
||||
<div style={{ padding: '32px 32px', position: 'relative', zIndex: 2, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 9, color: '#06b6d4', letterSpacing: '0.2em', marginBottom: 14 }}>SEASON 7 · COMPETITIVE</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 900, color: '#fff', lineHeight: 0.88, letterSpacing: '-0.03em', marginBottom: 18, fontFamily: 'system-ui' }}>NEXUS<br/><span style={{ background: 'linear-gradient(90deg, #06b6d4, #a855f7)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>ARENA</span></div>
|
||||
<div style={{ fontSize: 10, color: '#4b5563', lineHeight: 1.65, marginBottom: 22, fontFamily: 'system-ui' }}>실시간 매칭 · 랭크 시스템 · 글로벌 토너먼트<br/>지금 바로 전장에 참전하세요.</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div style={{ background: 'linear-gradient(90deg, #06b6d4, #a855f7)', color: '#fff', fontSize: 11, padding: '10px 22px', borderRadius: 4, fontWeight: 800, fontFamily: 'monospace' }}>PLAY NOW</div>
|
||||
<div style={{ border: '1px solid rgba(6,182,212,0.4)', color: '#06b6d4', fontSize: 11, padding: '10px 22px', borderRadius: 4, fontFamily: 'monospace' }}>랭킹 보기</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
{[{name:'VIPER',role:'Assassin',c:'#06b6d4'},{name:'NOVA',role:'Mage',c:'#a855f7'},{name:'IRON',role:'Tank',c:'#94a3b8'},{name:'KIRA',role:'Support',c:'#ec4899'}].map(ch => (
|
||||
<div key={ch.name} style={{ background: 'rgba(255,255,255,0.03)', border: `1px solid ${ch.c}30`, borderRadius: 8, padding: 10 }}>
|
||||
<div style={{ height: 34, background: `linear-gradient(135deg, ${ch.c}20, ${ch.c}05)`, borderRadius: 4, marginBottom: 6, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ width: 24, height: 24, borderRadius: '50%', background: `${ch.c}30`, border: `1px solid ${ch.c}60` }} />
|
||||
</div>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 9, color: ch.c, fontWeight: 700 }}>{ch.name}</div>
|
||||
<div style={{ fontSize: 8, color: '#374151', fontFamily: 'system-ui' }}>{ch.role}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#000'
|
||||
);
|
||||
|
||||
case 'interior':
|
||||
return inner(
|
||||
<div style={{ background: '#faf8f4', width: '100%', height: '100%' }}>
|
||||
<div style={{ height: 50, background: '#2C1810', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between' }}>
|
||||
<div><div style={{ fontFamily: 'Georgia, serif', fontSize: 14, color: '#D4A853', fontWeight: 700, letterSpacing: '0.12em' }}>AURUM</div><div style={{ fontSize: 7, color: '#6B4E37', letterSpacing: '0.25em' }}>INTERIOR DESIGN</div></div>
|
||||
<div style={{ display: 'flex', gap: 18 }}>
|
||||
{['포트폴리오','서비스','견적 문의'].map(t => <span key={t} style={{ fontSize: 9, color: '#9a8070', fontFamily: 'system-ui' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ border: '1px solid #D4A853', color: '#D4A853', fontSize: 9, padding: '6px 14px', fontFamily: 'system-ui', letterSpacing: '0.08em' }}>CONTACT</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', height: 300 }}>
|
||||
<div style={{ padding: '32px 28px', display: 'flex', flexDirection: 'column', justifyContent: 'center', background: '#2C1810' }}>
|
||||
<div style={{ fontSize: 9, color: '#D4A853', letterSpacing: '0.25em', marginBottom: 14, fontFamily: 'system-ui', textTransform: 'uppercase' }}>Premium Interior Design</div>
|
||||
<div style={{ fontFamily: 'Georgia, serif', fontSize: 34, color: '#faf8f4', lineHeight: 1.1, marginBottom: 18 }}>공간이<br/><em>이야기가</em><br/>되는 순간</div>
|
||||
<div style={{ fontSize: 10, color: '#9a8070', lineHeight: 1.7, fontFamily: 'system-ui', marginBottom: 22 }}>20년 경험의 인테리어 전문가가<br/>당신만의 공간을 완성합니다.</div>
|
||||
<div style={{ display: 'inline-flex' }}><div style={{ background: '#D4A853', color: '#2C1810', fontSize: 10, padding: '10px 22px', fontFamily: 'system-ui', fontWeight: 700 }}>포트폴리오 보기</div></div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: 3 }}>
|
||||
{['linear-gradient(135deg, #c9b99a, #a8927a)','linear-gradient(135deg, #8B7355, #6B5A47)','linear-gradient(135deg, #D4C5A9, #B8A88A)','linear-gradient(135deg, #7C6555, #5C4A3A)'].map((bg, i) => (
|
||||
<div key={i} style={{ background: bg }}><div style={{ width: '100%', height: '100%', background: 'rgba(44,24,16,0.08)' }} /></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#faf8f4'
|
||||
);
|
||||
|
||||
case 'reading':
|
||||
return inner(
|
||||
<div style={{ background: '#0C0B09', width: '100%', height: '100%' }}>
|
||||
<div style={{ height: 46, background: '#0C0B09', borderBottom: '1px solid rgba(212,168,83,0.1)', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between' }}>
|
||||
<div style={{ fontFamily: 'Georgia, serif', fontSize: 14, fontStyle: 'italic', color: '#D4A853', fontWeight: 600 }}>나의 독서 기록</div>
|
||||
<div style={{ display: 'flex', gap: 18 }}>
|
||||
{['서재','월별 기록','통계'].map(t => <span key={t} style={{ fontSize: 9, color: '#5c5040', fontFamily: 'system-ui' }}>{t}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '32px 32px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 28, alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#D4A853', letterSpacing: '0.2em', marginBottom: 14, fontFamily: 'system-ui', textTransform: 'uppercase' }}>My Reading Journal</div>
|
||||
<div style={{ fontFamily: 'Georgia, serif', fontSize: 40, color: '#faf8f4', lineHeight: 1.05, marginBottom: 16 }}>읽은 책들이<br/><em style={{ color: '#D4A853' }}>별처럼</em><br/>빛나는 공간</div>
|
||||
<div style={{ fontSize: 10, color: '#5c5040', lineHeight: 1.7, fontFamily: 'system-ui', marginBottom: 22 }}>독서 기록을 아름답게.<br/>감상과 인용구를 나만의 서재에 담아보세요.</div>
|
||||
<div style={{ display: 'inline-flex', background: '#D4A853', color: '#0C0B09', fontSize: 10, padding: '9px 22px', fontFamily: 'system-ui', fontWeight: 700 }}>기록 시작하기</div>
|
||||
<div style={{ display: 'flex', gap: 24, marginTop: 22 }}>
|
||||
{[['47','완독'],['1,240','페이지'],['12','이번 달']].map(([n,l]) => (
|
||||
<div key={l}><div style={{ fontSize: 20, fontWeight: 800, color: '#D4A853', fontFamily: 'Georgia, serif' }}>{n}</div><div style={{ fontSize: 8, color: '#5c5040', fontFamily: 'system-ui', marginTop: 2 }}>{l}</div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||
{[{h:130,bg:'linear-gradient(180deg,#1e3a5f,#0a1628)',sp:'#2563eb'},{h:152,bg:'linear-gradient(180deg,#2C1810,#1a0e0a)',sp:'#D4A853'},{h:118,bg:'linear-gradient(180deg,#1a1a1a,#0d0d0d)',sp:'#6b7280'},{h:142,bg:'linear-gradient(180deg,#1e1b4b,#0f0d2e)',sp:'#7c3aed'},{h:120,bg:'linear-gradient(180deg,#064e3b,#022c22)',sp:'#10b981'}].map((b, i) => (
|
||||
<div key={i} style={{ width: 38, height: b.h, background: b.bg, borderRadius: '3px 3px 0 0', borderLeft: `3px solid ${b.sp}40`, boxShadow: '2px 0 8px rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ width: 1, height: '80%', background: `${b.sp}30` }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#0C0B09'
|
||||
);
|
||||
|
||||
case 'shopping':
|
||||
return inner(
|
||||
<div style={{ background: '#F4F2EF', width: '100%', height: '100%' }}>
|
||||
<div style={{ height: 52, background: '#F4F2EF', borderBottom: '1px solid #E0DDD8', display: 'flex', alignItems: 'center', padding: '0 28px', justifyContent: 'space-between' }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 900, color: '#0C0B09', letterSpacing: '0.2em', textTransform: 'uppercase', fontFamily: 'system-ui' }}>MELLOW<span style={{ fontWeight: 300 }}> STUDIO</span></div>
|
||||
<div style={{ display: 'flex', gap: 20 }}>
|
||||
{['NEW','OUTER','TOP','BOTTOM'].map(t => <span key={t} style={{ fontSize: 9, color: '#7C7870', fontFamily: 'system-ui', letterSpacing: '0.1em' }}>{t}</span>)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 14, fontSize: 12, color: '#0C0B09', fontFamily: 'system-ui' }}><span>🔍</span><span>🛍 2</span></div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', height: 298 }}>
|
||||
<div style={{ background: 'linear-gradient(135deg, #2A2018, #4A3C2C)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ width: 120, height: 200, background: 'linear-gradient(180deg, #c8b89a, #9a8a72)', borderRadius: 4, boxShadow: '16px 16px 40px rgba(0,0,0,0.35)' }} />
|
||||
<div style={{ position: 'absolute', bottom: 16, left: 16 }}>
|
||||
<div style={{ fontSize: 9, color: 'rgba(244,242,239,0.5)', letterSpacing: '0.2em', fontFamily: 'system-ui' }}>NEW ARRIVAL</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 900, color: '#F4F2EF', fontFamily: 'system-ui', letterSpacing: '-0.01em' }}>SS 2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 9, color: '#7C7870', letterSpacing: '0.2em', marginBottom: 10, fontFamily: 'system-ui' }}>COLLECTION</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 900, color: '#0C0B09', lineHeight: 1.05, fontFamily: 'system-ui', letterSpacing: '-0.02em', marginBottom: 12 }}>Quiet<br/>Luxury</div>
|
||||
<div style={{ fontSize: 10, color: '#7C7870', lineHeight: 1.65, fontFamily: 'system-ui', marginBottom: 18 }}>소음 없이 존재하는 옷.<br/>절제된 아름다움을 입으세요.</div>
|
||||
<div style={{ display: 'inline-flex', background: '#0C0B09', color: '#F4F2EF', fontSize: 9, padding: '9px 20px', letterSpacing: '0.15em', fontFamily: 'system-ui', fontWeight: 700 }}>SHOP NOW</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 }}>
|
||||
{[['#c8b89a','₩328K'],['#8a7860','₩498K'],['#d4c5a9','₩218K']].map(([bg, p], i) => (
|
||||
<div key={i} style={{ borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{ height: 52, background: `linear-gradient(160deg, ${bg}, rgba(0,0,0,0.08))` }} />
|
||||
<div style={{ fontSize: 8, color: '#7C7870', fontFamily: 'system-ui', paddingTop: 4 }}>{p}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, '#F4F2EF'
|
||||
);
|
||||
|
||||
default:
|
||||
return <div style={{ height: 175, background: '#0a0f1e', borderRadius: '20px 20px 0 0' }} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default function WebsiteServicePage() {
|
||||
const [openFaq, setOpenFaq] = useState<number | null>(null);
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalService, setModalService] = useState('홈페이지 제작');
|
||||
|
||||
const openModal = (service: string) => {
|
||||
trackCTAClick(service, '/services/website');
|
||||
setModalService(service);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const scroller = (document.querySelector('.main-content') as HTMLElement | null) ?? document.documentElement;
|
||||
const onScroll = () => setShowTop(scroller.scrollTop > 400);
|
||||
scroller.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => scroller.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const samplesRef = useReveal();
|
||||
const processRef = useReveal();
|
||||
const pricingRef = useReveal();
|
||||
const faqRef = useReveal();
|
||||
const ctaRef = useReveal();
|
||||
|
||||
return (
|
||||
<div style={{ background: '#030712', minHeight: '100vh', color: 'white', fontFamily: "'Pretendard', 'Apple SD Gothic Neo', system-ui, sans-serif" }}>
|
||||
<ContactModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
service={modalService}
|
||||
checklist={[
|
||||
'원하시는 홈페이지 종류 (소개/쇼핑몰/SaaS 등)',
|
||||
'참고하고 싶은 사이트 URL (있으면)',
|
||||
'필요한 주요 페이지 및 기능',
|
||||
'희망 납품 일정 및 예산 범위',
|
||||
'디자인 선호 스타일 (모던/감성/심플 등)',
|
||||
]}
|
||||
accentColor="text-indigo-400"
|
||||
headerFrom="#0a0a1a"
|
||||
headerTo="#1e1b4b"
|
||||
/>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.min.css');
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
word-break { word-break: keep-all; }
|
||||
|
||||
/* scroll reveal */
|
||||
.ws-reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(32px);
|
||||
filter: blur(3px);
|
||||
transition: opacity 0.7s cubic-bezier(0.16,1,0.3,1),
|
||||
transform 0.7s cubic-bezier(0.16,1,0.3,1),
|
||||
filter 0.7s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.ws-reveal.ws-visible { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
.ws-reveal > *:nth-child(1) { transition-delay: 0ms; }
|
||||
.ws-reveal > *:nth-child(2) { transition-delay: 80ms; }
|
||||
.ws-reveal > *:nth-child(3) { transition-delay: 160ms; }
|
||||
.ws-reveal > *:nth-child(4) { transition-delay: 240ms; }
|
||||
.ws-reveal > *:nth-child(5) { transition-delay: 320ms; }
|
||||
|
||||
@keyframes ws-fadeUp {
|
||||
from { opacity: 0; transform: translateY(28px); filter: blur(4px); }
|
||||
to { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
@keyframes ws-gridScroll {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 48px 48px; }
|
||||
}
|
||||
@keyframes ws-glow {
|
||||
0%,100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.ws-sample-card {
|
||||
border-radius: 20px; overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
background: #0a0f1e; cursor: pointer;
|
||||
transition: transform 0.45s cubic-bezier(0.16,1,0.3,1),
|
||||
box-shadow 0.45s cubic-bezier(0.16,1,0.3,1),
|
||||
border-color 0.3s;
|
||||
}
|
||||
.ws-sample-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
||||
border-color: rgba(255,255,255,0.14);
|
||||
}
|
||||
|
||||
.ws-plan-card {
|
||||
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), box-shadow 0.4s;
|
||||
}
|
||||
.ws-plan-card:hover { transform: translateY(-4px); }
|
||||
|
||||
.ws-faq-item {
|
||||
border-radius: 14px; overflow: hidden;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.ws-step-card {
|
||||
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), box-shadow 0.4s;
|
||||
}
|
||||
.ws-step-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 모바일 반응형 */
|
||||
@media (max-width: 640px) {
|
||||
.ws-portfolio-grid { grid-template-columns: 1fr !important; }
|
||||
.ws-process-steps { flex-direction: column !important; align-items: stretch !important; }
|
||||
.ws-process-divider { display: none !important; }
|
||||
.ws-pricing-grid { grid-template-columns: 1fr !important; }
|
||||
.ws-hero-stats { gap: 0 !important; flex-wrap: nowrap !important; }
|
||||
.ws-hero-stats > div { padding: 0 16px !important; }
|
||||
.ws-cta-box { padding: 36px 24px !important; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.ws-hero-buttons { flex-direction: column !important; align-items: stretch !important; }
|
||||
.ws-hero-buttons a, .ws-hero-buttons button { text-align: center !important; justify-content: center !important; }
|
||||
}
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: #030712; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.3); border-radius: 2px; }
|
||||
`}} />
|
||||
|
||||
{/* ── Hero ── */}
|
||||
<section style={{ padding: '80px 24px 60px', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Diagonal pattern */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, pointerEvents: 'none',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)',
|
||||
}} />
|
||||
|
||||
<div style={{ maxWidth: 820, margin: '0 auto', position: 'relative', animation: 'ws-fadeUp 0.9s cubic-bezier(0.16,1,0.3,1) both' }}>
|
||||
<p style={{
|
||||
fontSize: 11, fontWeight: 700, letterSpacing: '0.18em',
|
||||
color: '#6366f1', textTransform: 'uppercase',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
홈페이지 제작 서비스
|
||||
</p>
|
||||
<h1 style={{
|
||||
fontSize: 'clamp(28px, 4.5vw, 54px)', fontWeight: 800,
|
||||
lineHeight: 1.2, marginBottom: 20,
|
||||
letterSpacing: '-0.02em',
|
||||
color: '#ffffff',
|
||||
wordBreak: 'keep-all',
|
||||
}}>
|
||||
홈페이지·웹앱·앱 개발,<br/>연락 끊기는 일 없습니다
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: 16, color: '#64748b', lineHeight: 1.85, marginBottom: 36,
|
||||
wordBreak: 'keep-all',
|
||||
}}>
|
||||
소개 사이트부터 SaaS·쇼핑몰·모바일웹까지 — 계약서부터 소스코드 인도까지<br/>
|
||||
단계마다 증거를 남깁니다. 납기 지연 시 하루당 10만원 감면.
|
||||
</p>
|
||||
<div className="ws-hero-buttons" style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<Link href="/freelance?service=website" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '14px 28px',
|
||||
background: '#6366f1',
|
||||
borderRadius: 12, color: 'white', fontWeight: 700, fontSize: 15,
|
||||
textDecoration: 'none',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#4f46e5'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#6366f1'; }}>
|
||||
무료 상담 신청 →
|
||||
</Link>
|
||||
<a href="#samples" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '14px 28px',
|
||||
border: '1px solid rgba(255,255,255,0.1)', borderRadius: 12,
|
||||
color: '#94a3b8', fontWeight: 600, fontSize: 15,
|
||||
textDecoration: 'none',
|
||||
transition: 'border-color 0.3s, color 0.3s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.25)'; (e.currentTarget as HTMLElement).style.color = '#e2e8f0'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.1)'; (e.currentTarget as HTMLElement).style.color = '#94a3b8'; }}>
|
||||
샘플 보기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="ws-hero-stats" style={{ display: 'flex', gap: 0, justifyContent: 'center', marginTop: 56, flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ num: '3~5일', label: '최단 납품 (스타터)' },
|
||||
{ num: '20만원~', label: '시작 가격' },
|
||||
{ num: '전액환불', label: '납품 전 환불 보장' },
|
||||
].map((s, i) => (
|
||||
<div key={s.label} style={{
|
||||
textAlign: 'center', padding: '0 40px',
|
||||
borderRight: i < 2 ? '1px solid rgba(255,255,255,0.08)' : 'none',
|
||||
}}>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: 'white', letterSpacing: '-0.02em' }}>{s.num}</div>
|
||||
<div style={{ fontSize: 12, color: '#475569', marginTop: 4, letterSpacing: '0.02em' }}>{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Feature tags ── */}
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', borderBottom: '1px solid rgba(255,255,255,0.05)', padding: '14px 24px' }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto', display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
{['반응형 디자인', 'SEO 최적화', '웹앱·모바일웹', '계약서 작성', '소스코드 제공', '납기 패널티 보장', '도메인 배포'].map((t) => (
|
||||
<span key={t} style={{ padding: '4px 12px', fontSize: '11px', color: '#475569', letterSpacing: '0.06em', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 4, fontFamily: 'monospace' }}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Trust badges ── */}
|
||||
<section style={{ padding: '48px 24px', maxWidth: 1000, margin: '0 auto' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 1, border: '1px solid rgba(255,255,255,0.06)', borderRadius: 12, overflow: 'hidden' }}>
|
||||
{[
|
||||
{
|
||||
title: '계약서 필수 작성', desc: '모든 프로젝트 계약서 체결 후 진행',
|
||||
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 20, height: 20 }}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" /></svg>,
|
||||
},
|
||||
{
|
||||
title: '주간 진행 보고', desc: '매주 작업 현황 공유, 연락 두절 없음',
|
||||
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 20, height: 20 }}><path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /></svg>,
|
||||
},
|
||||
{
|
||||
title: '소스코드 전액 제공', desc: '완성 후 전체 소스코드 인도',
|
||||
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 20, height: 20 }}><path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" /></svg>,
|
||||
},
|
||||
{
|
||||
title: '납기 지연 패널티', desc: '지연 1일당 10만원 자동 감면',
|
||||
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 20, height: 20 }}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>,
|
||||
},
|
||||
{
|
||||
title: '실시간 진행 현황', desc: '마이페이지에서 7단계 진행 상황 직접 확인',
|
||||
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 20, height: 20 }}><path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>,
|
||||
},
|
||||
].map((b) => (
|
||||
<div key={b.title} style={{
|
||||
padding: '20px 22px',
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
display: 'flex', gap: 14, alignItems: 'flex-start',
|
||||
}}>
|
||||
<span style={{ color: '#6366f1', flexShrink: 0, marginTop: 2 }}>{b.icon}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#e2e8f0', marginBottom: 4, wordBreak: 'keep-all' }}>{b.title}</div>
|
||||
<div style={{ fontSize: 12, color: '#475569', lineHeight: 1.6, wordBreak: 'keep-all' }}>{b.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Sample Portfolio ── */}
|
||||
<section id="samples" style={{ padding: '56px 24px', maxWidth: 1160, margin: '0 auto' }}>
|
||||
<div ref={samplesRef} className="ws-reveal">
|
||||
<div style={{ textAlign: 'center', marginBottom: 44 }}>
|
||||
<p style={{ fontSize: 11, color: '#6366f1', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 12, fontWeight: 700 }}>Portfolio Samples</p>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, color: 'white', marginBottom: 10, letterSpacing: '-0.02em' }}>
|
||||
포트폴리오 샘플
|
||||
</h2>
|
||||
<p style={{ color: '#475569', fontSize: 14 }}>
|
||||
카드를 클릭하면 실제 완성 화면을 미리 확인할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
<div className="ws-portfolio-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 18 }}>
|
||||
{samples.map((s) => (
|
||||
<Link key={s.type} href={`/services/website/samples/${s.type}`} style={{ textDecoration: 'none' }}>
|
||||
<div className="ws-sample-card">
|
||||
<div style={{ position: 'relative' }}>
|
||||
<SampleMiniPreview type={s.type} />
|
||||
<div style={{ position: 'absolute', top: 12, left: 12, display: 'flex', gap: 5, zIndex: 10 }}>
|
||||
{s.tags.map((tag) => (
|
||||
<span key={tag} style={{
|
||||
fontSize: 10, fontWeight: 600, color: '#e2e8f0',
|
||||
background: 'rgba(0,0,0,0.52)', backdropFilter: 'blur(8px)',
|
||||
border: '1px solid rgba(255,255,255,0.13)',
|
||||
padding: '2px 8px', borderRadius: 100,
|
||||
}}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 12, right: 12, zIndex: 10,
|
||||
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(8px)',
|
||||
border: `1px solid ${s.accent}45`,
|
||||
borderRadius: 8, padding: '5px 12px',
|
||||
fontSize: 11, color: s.accent, fontWeight: 700,
|
||||
}}>
|
||||
미리보기 →
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '18px 22px 22px' }}>
|
||||
<div style={{ fontSize: 11, color: '#334155', marginBottom: 5, letterSpacing: '0.05em' }}>
|
||||
{s.subtitle}
|
||||
</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: 'white', marginBottom: 8, letterSpacing: '-0.01em' }}>
|
||||
{s.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#475569', lineHeight: 1.65, wordBreak: 'keep-all' }}>
|
||||
{s.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Process ── */}
|
||||
<section style={{ padding: '56px 24px', background: 'rgba(255,255,255,0.015)', borderTop: '1px solid rgba(255,255,255,0.04)', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<div ref={processRef} className="ws-reveal" style={{ maxWidth: 1060, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 44 }}>
|
||||
<p style={{ fontSize: 11, color: '#6366f1', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 12, fontWeight: 700 }}>Process</p>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, color: 'white', marginBottom: 10, letterSpacing: '-0.02em' }}>
|
||||
제작 프로세스
|
||||
</h2>
|
||||
<p style={{ color: '#475569', fontSize: 14 }}>투명하고 체계적인 5단계로 진행됩니다</p>
|
||||
</div>
|
||||
<div className="ws-process-steps" style={{ display: 'flex', alignItems: 'stretch', flexWrap: 'wrap', justifyContent: 'center', gap: 0 }}>
|
||||
{processSteps.map((p, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div className="ws-step-card" style={{
|
||||
textAlign: 'center', padding: '28px 22px', minWidth: 138,
|
||||
background: '#080d1a', borderRadius: 16,
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
}}>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#6366f1', fontFamily: 'monospace', marginBottom: 12, letterSpacing: '-0.02em' }}>
|
||||
{p.step}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'white', marginBottom: 6, wordBreak: 'keep-all' }}>
|
||||
{p.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#334155', lineHeight: 1.55, wordBreak: 'keep-all' }}>
|
||||
{p.desc}
|
||||
</div>
|
||||
</div>
|
||||
{i < processSteps.length - 1 && (
|
||||
<div className="ws-process-divider" style={{ color: '#1e293b', fontSize: 20, padding: '0 4px', flexShrink: 0 }}>›</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Pricing ── */}
|
||||
<section style={{ padding: '64px 24px', maxWidth: 1040, margin: '0 auto' }}>
|
||||
<div ref={pricingRef} className="ws-reveal">
|
||||
<div style={{ textAlign: 'center', marginBottom: 44 }}>
|
||||
<p style={{ fontSize: 11, color: '#6366f1', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 12, fontWeight: 700 }}>Pricing</p>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, color: 'white', marginBottom: 10, letterSpacing: '-0.02em' }}>
|
||||
가격 플랜
|
||||
</h2>
|
||||
<p style={{ color: '#475569', fontSize: 14 }}>프로젝트 규모에 맞는 플랜을 선택하세요</p>
|
||||
</div>
|
||||
<div className="ws-pricing-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(270px, 1fr))', gap: 20 }}>
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.name} className="ws-plan-card" style={{
|
||||
padding: 32, borderRadius: 20,
|
||||
background: plan.featured ? '#0d1240' : '#080d1a',
|
||||
border: `1px solid ${plan.featured ? plan.color + '40' : 'rgba(255,255,255,0.05)'}`,
|
||||
position: 'relative', overflow: 'hidden',
|
||||
boxShadow: plan.featured ? `0 24px 64px ${plan.color}12` : 'none',
|
||||
}}>
|
||||
{plan.featured && (
|
||||
<div style={{
|
||||
position: 'absolute', top: 20, right: 20,
|
||||
background: plan.color, color: '#1e1b4b',
|
||||
fontSize: 10, fontWeight: 800, padding: '3px 10px', borderRadius: 100,
|
||||
}}>BEST</div>
|
||||
)}
|
||||
<div style={{ fontSize: 12, color: plan.color, fontWeight: 700, marginBottom: 12, letterSpacing: '0.05em' }}>
|
||||
{plan.name}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 40, fontWeight: 800, color: 'white', letterSpacing: '-0.03em' }}>{plan.price}</span>
|
||||
<span style={{ fontSize: 15, color: '#64748b' }}>{plan.unit}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#334155', marginBottom: 24, wordBreak: 'keep-all' }}>{plan.note}</div>
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 20, marginBottom: 24 }}>
|
||||
{plan.features.map((f) => (
|
||||
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 12 }}>
|
||||
<span style={{ color: plan.color, fontSize: 13, flexShrink: 0, fontWeight: 700 }}>✓</span>
|
||||
<span style={{ fontSize: 13, color: '#94a3b8', wordBreak: 'keep-all' }}>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openModal(`홈페이지 제작 - ${plan.name}`)}
|
||||
style={{
|
||||
display: 'block', width: '100%', textAlign: 'center', padding: '13px',
|
||||
background: plan.featured ? plan.color : 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 10, color: plan.featured ? '#1e1b4b' : '#94a3b8',
|
||||
fontWeight: 700, fontSize: 14, border: plan.featured ? 'none' : '1px solid rgba(255,255,255,0.07)',
|
||||
transition: 'opacity 0.2s', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
견적 문의
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── FAQ ── */}
|
||||
<section style={{ padding: '0 24px 64px', maxWidth: 720, margin: '0 auto' }}>
|
||||
<div ref={faqRef} className="ws-reveal">
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
<p style={{ fontSize: 11, color: '#6366f1', letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 12, fontWeight: 700 }}>FAQ</p>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, color: 'white', letterSpacing: '-0.02em' }}>
|
||||
자주 묻는 질문
|
||||
</h2>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{faqs.map((faq, i) => (
|
||||
<div key={i} className="ws-faq-item" style={{
|
||||
background: '#080d1a',
|
||||
border: `1px solid ${openFaq === i ? 'rgba(99,102,241,0.4)' : 'rgba(255,255,255,0.05)'}`,
|
||||
}}>
|
||||
<button onClick={() => setOpenFaq(openFaq === i ? null : i)} style={{
|
||||
width: '100%', textAlign: 'left', padding: '18px 22px',
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12,
|
||||
}}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: 'white', wordBreak: 'keep-all' }}>
|
||||
{faq.q}
|
||||
</span>
|
||||
<span style={{
|
||||
color: '#6366f1', fontSize: 22, flexShrink: 0,
|
||||
transition: 'transform 0.25s',
|
||||
transform: openFaq === i ? 'rotate(45deg)' : 'none',
|
||||
display: 'inline-block',
|
||||
}}>+</span>
|
||||
</button>
|
||||
{openFaq === i && (
|
||||
<div style={{ padding: '0 22px 18px', fontSize: 14, color: '#475569', lineHeight: 1.8, wordBreak: 'keep-all' }}>
|
||||
{faq.a}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── CTA ── */}
|
||||
<section style={{ padding: '0 24px 80px', textAlign: 'center' }}>
|
||||
<div ref={ctaRef} className="ws-reveal">
|
||||
<div className="ws-cta-box" style={{
|
||||
maxWidth: 640, margin: '0 auto',
|
||||
padding: '56px 44px', borderRadius: 24,
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)',
|
||||
border: '1px solid rgba(99,102,241,0.3)',
|
||||
boxShadow: '0 24px 80px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, color: 'white', marginBottom: 14, letterSpacing: '-0.02em', wordBreak: 'keep-all' }}>
|
||||
내일도 고민만 하실 건가요?
|
||||
</h2>
|
||||
<p style={{ color: '#94a3b8', fontSize: 15, lineHeight: 1.75, marginBottom: 32, wordBreak: 'keep-all' }}>
|
||||
상담 신청 후 24시간 이내 답변드립니다.<br/>
|
||||
소개 사이트·웹앱·쇼핑몰·모바일앱, 규모 무관하게 검토해드립니다.
|
||||
</p>
|
||||
<Link href="/freelance?service=website" style={{
|
||||
display: 'inline-block', padding: '15px 40px',
|
||||
background: '#6366f1',
|
||||
borderRadius: 12, color: 'white', fontWeight: 700, fontSize: 15,
|
||||
textDecoration: 'none',
|
||||
boxShadow: '0 8px 24px rgba(99,102,241,0.4)',
|
||||
transition: 'transform 0.3s, box-shadow 0.3s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 16px 40px rgba(99,102,241,0.5)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = ''; (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 24px rgba(99,102,241,0.4)'; }}>
|
||||
무료 상담 신청하기 →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Scroll to Top ── */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const scroller = (document.querySelector('.main-content') as HTMLElement | null) ?? document.documentElement;
|
||||
scroller.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed', bottom: '5.5rem', right: '2rem', zIndex: 200,
|
||||
width: 48, height: 48, borderRadius: '50%',
|
||||
background: '#6366f1',
|
||||
border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(99,102,241,0.4)',
|
||||
opacity: showTop ? 1 : 0,
|
||||
transform: showTop ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)',
|
||||
transition: 'opacity 0.35s cubic-bezier(0.16,1,0.3,1), transform 0.35s cubic-bezier(0.16,1,0.3,1)',
|
||||
pointerEvents: showTop ? 'auto' : 'none',
|
||||
}}
|
||||
aria-label="맨 위로"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
const menuItems = [
|
||||
{ name: '버터 크루아상', price: '3,200', tag: '인기', tagColor: '#dc2626', desc: '프랑스산 에슈레 버터만 사용한 겹겹이 살아있는 크루아상', ingredients: ['에슈레 버터', '천연 발효', 'T65 밀가루'] },
|
||||
{ name: '소금빵', price: '2,800', tag: '베스트', tagColor: '#d97706', desc: '오키나와 천연 소금과 발효 버터가 만나는 완벽한 짭조름함', ingredients: ['오키나와 소금', '발효 버터', '홋카이도 밀'] },
|
||||
{ name: '딸기 쇼트케이크', price: '7,500', tag: '신메뉴', tagColor: '#7c3aed', desc: '국내산 딸기와 생크림이 만나는 클래식 케이크', ingredients: ['국내산 딸기', '생크림', '제노아즈'] },
|
||||
{ name: '캄파뉴', price: '8,900', tag: '장인', tagColor: '#065f46', desc: '72시간 저온 발효로 만든 시큼하고 깊은 맛의 통밀빵', ingredients: ['통밀', '사워도우', '72h 발효'] },
|
||||
];
|
||||
|
||||
const reviews = [
|
||||
{ name: '김민서', rating: 5, text: '매일 아침 출근 전 들르게 되는 마법 같은 빵집. 크루아상은 그냥 인생 최고입니다.', date: '2024.08', verified: true },
|
||||
{ name: '이준혁', rating: 5, text: '파리 유학시절 먹던 그 맛과 거의 흡사합니다. 국내에 이런 퀄리티가 있다니 놀랍습니다.', date: '2024.07', verified: true },
|
||||
{ name: '박지은', rating: 5, text: '웨딩 케이크 주문했는데 하객들이 케이크 맛있다고 난리났어요. 다음에도 꼭 맡기겠습니다!', date: '2024.06', verified: true },
|
||||
];
|
||||
|
||||
const specials = [
|
||||
{ season: '봄', item: '딸기 타르틀렛', desc: '4월 한정', color: '#fda4af' },
|
||||
{ season: '여름', item: '레몬 틸레', desc: '7–8월 한정', color: '#fde68a' },
|
||||
{ season: '가을', item: '밤 크루아상', desc: '10월 한정', color: '#d97706' },
|
||||
{ season: '겨울', item: '뱅쇼 브리오슈', desc: '12월 한정', color: '#c4b5fd' },
|
||||
];
|
||||
|
||||
const hours = [
|
||||
{ day: '월~금', time: '07:00 – 20:00' },
|
||||
{ day: '토요일', time: '07:00 – 21:00' },
|
||||
{ day: '일·공휴일', time: '09:00 – 18:00' },
|
||||
];
|
||||
|
||||
export default function BakerySample() {
|
||||
return (
|
||||
<div style={{ background: '#fffbf5', minHeight: '100vh', color: '#1c1008' }}>
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400;1,700&family=Noto+Sans+KR:wght@300;400;500;600;700&display=swap');
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0px) rotate(-1deg); } 50% { transform: translateY(-12px) rotate(1deg); } }
|
||||
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
||||
@keyframes sway { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(2deg); } }
|
||||
.menu-card:hover { transform: translateY(-8px); box-shadow: 0 32px 80px rgba(120,53,15,0.15) !important; }
|
||||
.menu-card { transition: transform 0.4s cubic-bezier(.23,1,.32,1), box-shadow 0.4s; }
|
||||
.review-card:hover { border-color: #fbbf24 !important; }
|
||||
.review-card { transition: border-color 0.3s; }
|
||||
.bk-btn-primary:hover { background: #92400e !important; transform: translateY(-1px); box-shadow: 0 12px 32px rgba(180,83,9,0.45) !important; }
|
||||
.bk-btn-primary { transition: background 0.2s, transform 0.2s, box-shadow 0.2s; }
|
||||
.bk-btn-ghost:hover { background: rgba(180,83,9,0.06) !important; }
|
||||
.bk-btn-ghost { transition: background 0.2s; }
|
||||
.special-card:hover { transform: scale(1.04); }
|
||||
.special-card { transition: transform 0.3s; }
|
||||
.ingredient-tag { background: rgba(180,83,9,0.07); color: #92400e; border: 1px solid rgba(180,83,9,0.15); border-radius: 100px; padding: 2px 10px; font-size: 11px; font-family: 'Noto Sans KR', sans-serif; white-space: nowrap; }
|
||||
`}</style>
|
||||
|
||||
{/* Back Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #1e1b4b, #312e81)', padding: '10px 24px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none', fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#fcd34d', fontSize: 12, fontFamily: 'Playfair Display, serif', fontStyle: 'italic', fontWeight: 700 }}>
|
||||
SAMPLE · 베이커리 홈페이지
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav style={{
|
||||
background: 'rgba(255,251,245,0.96)', backdropFilter: 'blur(16px)',
|
||||
borderBottom: '1px solid #fde8c8',
|
||||
padding: '0 48px', height: 68,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
position: 'sticky', top: 0, zIndex: 100,
|
||||
}}>
|
||||
{/* Logo with wheat SVG */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||
<ellipse cx="14" cy="8" rx="5" ry="7" fill="#d97706" opacity="0.9" />
|
||||
<ellipse cx="8" cy="14" rx="5" ry="4" fill="#b45309" opacity="0.7" transform="rotate(-30 8 14)" />
|
||||
<ellipse cx="20" cy="14" rx="5" ry="4" fill="#b45309" opacity="0.7" transform="rotate(30 20 14)" />
|
||||
<line x1="14" y1="28" x2="14" y2="8" stroke="#78350f" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<div style={{ fontSize: 19, fontFamily: 'Playfair Display, serif', fontStyle: 'italic', color: '#78350f', fontWeight: 700, lineHeight: 1 }}>
|
||||
Le Petit Fort
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#c9a87c', letterSpacing: '0.18em', textTransform: 'uppercase', fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
Artisan Boulangerie
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 28 }}>
|
||||
{['메뉴', '스토리', '시즌 메뉴', '매장안내', '예약'].map((item) => (
|
||||
<span key={item} style={{ fontSize: 14, color: '#78350f', cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 500 }}>{item}</span>
|
||||
))}
|
||||
</div>
|
||||
<button className="bk-btn-primary" style={{
|
||||
background: '#b45309', color: 'white', border: 'none',
|
||||
padding: '9px 22px', borderRadius: 24, fontSize: 13,
|
||||
fontWeight: 600, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif",
|
||||
}}>
|
||||
예약하기
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section style={{
|
||||
background: 'linear-gradient(160deg, #fef3c7 0%, #fde68a 40%, #fbbf24 75%, #d97706 100%)',
|
||||
padding: '80px 48px', position: 'relative', overflow: 'hidden', minHeight: 500,
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}>
|
||||
{/* Decorative circles */}
|
||||
<div style={{ position: 'absolute', right: -80, top: -80, width: 480, height: 480, borderRadius: '50%', background: 'rgba(180,83,9,0.07)' }} />
|
||||
<div style={{ position: 'absolute', right: 60, bottom: -60, width: 280, height: 280, borderRadius: '50%', background: 'rgba(180,83,9,0.1)' }} />
|
||||
|
||||
{/* SVG Croissant illustration */}
|
||||
<div style={{ position: 'absolute', right: '8%', top: '50%', transform: 'translateY(-50%)', animation: 'float 5s ease-in-out infinite' }}>
|
||||
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" style={{ filter: 'drop-shadow(0 20px 40px rgba(120,53,15,0.3))' }}>
|
||||
{/* Croissant shape */}
|
||||
<path d="M90 30 C50 30, 20 60, 20 90 C20 110, 30 130, 50 145 C65 155, 75 158, 90 155 C105 158, 115 155, 130 145 C150 130, 160 110, 160 90 C160 60, 130 30, 90 30Z" fill="#d97706" />
|
||||
<path d="M90 40 C55 40, 30 65, 30 90 C30 108, 38 124, 55 136 C68 145, 78 148, 90 145 C102 148, 112 145, 125 136 C142 124, 150 108, 150 90 C150 65, 125 40, 90 40Z" fill="#b45309" />
|
||||
{/* Layered flakes */}
|
||||
<path d="M60 80 Q90 70 120 80" stroke="#fde68a" strokeWidth="2.5" strokeLinecap="round" opacity="0.6" />
|
||||
<path d="M55 92 Q90 82 125 92" stroke="#fef3c7" strokeWidth="2" strokeLinecap="round" opacity="0.5" />
|
||||
<path d="M60 104 Q90 94 120 104" stroke="#fde68a" strokeWidth="2" strokeLinecap="round" opacity="0.4" />
|
||||
{/* Shine */}
|
||||
<ellipse cx="70" cy="65" rx="15" ry="8" fill="white" opacity="0.15" transform="rotate(-20 70 65)" />
|
||||
{/* Steam */}
|
||||
<path d="M75 28 Q78 20 75 12" stroke="#fbbf24" strokeWidth="2" strokeLinecap="round" opacity="0.4" />
|
||||
<path d="M90 24 Q93 14 90 5" stroke="#fbbf24" strokeWidth="2" strokeLinecap="round" opacity="0.5" />
|
||||
<path d="M105 28 Q108 20 105 12" stroke="#fbbf24" strokeWidth="2" strokeLinecap="round" opacity="0.4" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Wheat decoration top right */}
|
||||
<svg width="80" height="120" viewBox="0 0 80 120" fill="none" style={{ position: 'absolute', right: '28%', top: 20, opacity: 0.15, animation: 'sway 4s ease-in-out infinite' }}>
|
||||
<line x1="40" y1="120" x2="40" y2="0" stroke="#78350f" strokeWidth="2" />
|
||||
{[15, 35, 55, 75].map((y, i) => (
|
||||
<g key={y}>
|
||||
<ellipse cx="28" cy={y} rx="10" ry="5" fill="#78350f" transform={`rotate(-40 28 ${y})`} />
|
||||
<ellipse cx="52" cy={y} rx="10" ry="5" fill="#78350f" transform={`rotate(40 52 ${y})`} />
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
<div style={{ maxWidth: 560, animation: 'fadeUp 0.8s ease forwards', position: 'relative', zIndex: 1 }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'rgba(255,255,255,0.5)', backdropFilter: 'blur(8px)', borderRadius: 100, padding: '5px 14px', marginBottom: 20 }}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="#16a34a" /><circle cx="5" cy="5" r="2" fill="white" /></svg>
|
||||
<span style={{ fontSize: 12, color: '#78350f', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 600 }}>오늘도 새벽 4시부터 굽고 있습니다</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 13, color: '#78350f', fontFamily: 'Playfair Display, serif', fontStyle: 'italic', marginBottom: 14, letterSpacing: '0.05em' }}>
|
||||
“매일 아침, 정성을 굽습니다”
|
||||
</div>
|
||||
<h1 style={{ fontFamily: 'Playfair Display, serif', fontSize: 'clamp(40px, 5vw, 64px)', fontWeight: 700, color: '#451a03', lineHeight: 1.15, marginBottom: 18 }}>
|
||||
갓 구운 빵의<br />
|
||||
<span style={{ color: '#b45309' }}>따뜻한 향기</span>가<br />
|
||||
기다립니다
|
||||
</h1>
|
||||
<p style={{ color: '#78350f', fontSize: 16, lineHeight: 1.85, marginBottom: 32, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
파리 전통 방식으로 매일 새벽 4시부터<br />정성껏 굽는 르 쁘띠 포르의 빵을 만나보세요.
|
||||
</p>
|
||||
|
||||
{/* Social proof */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{['#d97706', '#b45309', '#92400e', '#78350f'].map((c, i) => (
|
||||
<div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid white', marginLeft: i > 0 ? -8 : 0 }} />
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: 2 }}>
|
||||
{[1,2,3,4,5].map(i => (
|
||||
<svg key={i} width="12" height="12" viewBox="0 0 12 12"><path d="M6 1l1.3 2.6L10 4.1 8 6.1l.5 2.9L6 7.6 3.5 9l.5-2.9-2-2 2.7-.5z" fill="#d97706" /></svg>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#78350f', fontFamily: "'Noto Sans KR', sans-serif" }}>4.9점 · 1,200+ 리뷰</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<button className="bk-btn-primary" style={{
|
||||
background: '#b45309', color: 'white', border: 'none',
|
||||
padding: '14px 32px', borderRadius: 28, fontSize: 15, fontWeight: 700,
|
||||
cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif",
|
||||
boxShadow: '0 8px 24px rgba(180,83,9,0.35)',
|
||||
}}>
|
||||
오늘의 메뉴 보기
|
||||
</button>
|
||||
<button className="bk-btn-ghost" style={{
|
||||
background: 'rgba(255,255,255,0.5)', color: '#78350f',
|
||||
border: '1px solid rgba(180,83,9,0.25)',
|
||||
padding: '14px 32px', borderRadius: 28, fontSize: 15, fontWeight: 600,
|
||||
cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif",
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}>
|
||||
매장 찾아오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Season Special Banner */}
|
||||
<div style={{ background: '#451a03', padding: '14px 48px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 1l1.8 3.6L14 5.6l-3 2.9.7 4.1L8 10.5 4.3 12.6l.7-4.1L2 5.6l4.2-.6z" fill="#fbbf24" /></svg>
|
||||
<span style={{ color: '#fde68a', fontSize: 13, fontFamily: 'Playfair Display, serif', fontStyle: 'italic', fontWeight: 600 }}>Spring Limited</span>
|
||||
</div>
|
||||
<span style={{ color: '#78350f' }}>|</span>
|
||||
<span style={{ color: 'white', fontSize: 14, fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 600 }}>
|
||||
🌸 딸기 타르틀렛 — 4월 한정 메뉴 출시
|
||||
</span>
|
||||
<span style={{ color: '#78350f' }}>|</span>
|
||||
<span style={{ color: '#fcd34d', fontSize: 13, fontFamily: "'Noto Sans KR', sans-serif", cursor: 'pointer', textDecoration: 'underline' }}>자세히 보기 →</span>
|
||||
</div>
|
||||
|
||||
{/* Menu */}
|
||||
<section style={{ padding: '80px 48px', background: '#fffbf5' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 52 }}>
|
||||
<div style={{ fontSize: 11, color: '#b45309', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 12 }}>
|
||||
Today's Menu
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Playfair Display, serif', fontSize: 40, color: '#451a03', marginBottom: 12 }}>
|
||||
오늘의 추천 메뉴
|
||||
</h2>
|
||||
<p style={{ color: '#a78060', fontSize: 15, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
매일 새벽 구운 신선한 빵을 만나보세요
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: 24 }}>
|
||||
{menuItems.map((item) => (
|
||||
<div key={item.name} className="menu-card" style={{
|
||||
background: 'white', borderRadius: 20, border: '1px solid #fde8c8', overflow: 'hidden',
|
||||
boxShadow: '0 4px 20px rgba(120,53,15,0.06)',
|
||||
}}>
|
||||
{/* Card visual — SVG pattern instead of emoji */}
|
||||
<div style={{ height: 150, background: 'linear-gradient(135deg, #fef3c7, #fde68a)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Subtle dotted pattern */}
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute', inset: 0 }}>
|
||||
<pattern id={`dots-${item.name}`} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<circle cx="10" cy="10" r="1.5" fill="rgba(180,83,9,0.1)" />
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill={`url(#dots-${item.name})`} />
|
||||
</svg>
|
||||
{/* Bread silhouette SVG */}
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" style={{ position: 'relative', zIndex: 1, filter: 'drop-shadow(0 4px 12px rgba(120,53,15,0.25))' }}>
|
||||
<ellipse cx="40" cy="45" rx="30" ry="18" fill="#d97706" />
|
||||
<ellipse cx="40" cy="38" rx="24" ry="16" fill="#b45309" />
|
||||
<path d="M20 42 Q40 30 60 42" stroke="#fde68a" strokeWidth="2" strokeLinecap="round" opacity="0.5" />
|
||||
<path d="M24 48 Q40 38 56 48" stroke="#fef3c7" strokeWidth="1.5" strokeLinecap="round" opacity="0.4" />
|
||||
<ellipse cx="30" cy="32" rx="8" ry="5" fill="white" opacity="0.1" transform="rotate(-20 30 32)" />
|
||||
</svg>
|
||||
<span style={{
|
||||
position: 'absolute', top: 12, right: 12,
|
||||
background: item.tagColor, color: 'white',
|
||||
fontSize: 10, fontWeight: 700, padding: '3px 9px', borderRadius: 100,
|
||||
fontFamily: "'Noto Sans KR', sans-serif",
|
||||
}}>{item.tag}</span>
|
||||
</div>
|
||||
<div style={{ padding: '18px 20px' }}>
|
||||
<div style={{ fontSize: 17, fontWeight: 700, color: '#451a03', fontFamily: 'Playfair Display, serif', marginBottom: 6 }}>
|
||||
{item.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#a78060', lineHeight: 1.65, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 12 }}>
|
||||
{item.desc}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14 }}>
|
||||
{item.ingredients.map(ig => (
|
||||
<span key={ig} className="ingredient-tag">{ig}</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 19, fontWeight: 700, color: '#b45309', fontFamily: 'Playfair Display, serif' }}>
|
||||
₩{item.price}
|
||||
</span>
|
||||
<button style={{
|
||||
background: '#b45309', color: 'white', border: 'none',
|
||||
padding: '7px 16px', borderRadius: 12, fontSize: 12,
|
||||
fontWeight: 700, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif",
|
||||
}}>담기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Seasonal Specials */}
|
||||
<section style={{ padding: '72px 48px', background: '#fef9f0', borderTop: '1px solid #fde8c8' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 44 }}>
|
||||
<div style={{ fontSize: 11, color: '#b45309', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 10 }}>
|
||||
Seasonal Specials
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Playfair Display, serif', fontSize: 36, color: '#451a03' }}>
|
||||
계절마다 새로운 감동
|
||||
</h2>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
|
||||
{specials.map((s) => (
|
||||
<div key={s.season} className="special-card" style={{
|
||||
borderRadius: 16, padding: '28px 20px', textAlign: 'center',
|
||||
background: 'white', border: '1px solid #fde8c8',
|
||||
cursor: 'pointer', overflow: 'hidden', position: 'relative',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 4, background: s.color, borderRadius: '16px 16px 0 0' }} />
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: '50%', margin: '0 auto 14px',
|
||||
background: s.color + '30', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: '50%', background: s.color }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#a78060', fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 8 }}>{s.season} 한정</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#451a03', fontFamily: 'Playfair Display, serif', marginBottom: 6 }}>{s.item}</div>
|
||||
<div style={{ fontSize: 11, color: '#b45309', fontWeight: 600, fontFamily: "'Noto Sans KR', sans-serif", background: s.color + '25', borderRadius: 100, padding: '2px 10px', display: 'inline-block' }}>{s.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Story */}
|
||||
<section style={{ padding: '80px 48px', background: '#fffbf5', borderTop: '1px solid #fde8c8' }}>
|
||||
<div style={{ maxWidth: 960, margin: '0 auto', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 64, alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#b45309', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 14 }}>
|
||||
Our Story
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Playfair Display, serif', fontSize: 36, color: '#451a03', lineHeight: 1.3, marginBottom: 22 }}>
|
||||
2009년부터<br />한 자리를 지켜온<br />
|
||||
<em>우리 동네 빵집</em>
|
||||
</h2>
|
||||
<p style={{ color: '#78350f', fontSize: 15, lineHeight: 1.9, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 16 }}>
|
||||
파리에서 5년간 수업한 오너 셰프가 고향 서울로 돌아와 차린 작은 베이커리. 대기업 프랜차이즈가 넘쳐나는 세상에서도 손으로 빚고, 눈으로 확인하는 전통 방식을 고집합니다.
|
||||
</p>
|
||||
<p style={{ color: '#78350f', fontSize: 15, lineHeight: 1.9, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 28 }}>
|
||||
밀가루, 버터, 소금, 물. 단 네 가지 재료로 만드는 우리의 빵에는 흉내낼 수 없는 진심이 담겨있습니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 32 }}>
|
||||
{[{ n: '15+', l: '년 경력' }, { n: '200+', l: '종류의 빵' }, { n: '4시', l: '매일 기상' }].map((s) => (
|
||||
<div key={s.l}>
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: '#b45309', fontFamily: 'Playfair Display, serif' }}>{s.n}</div>
|
||||
<div style={{ fontSize: 12, color: '#a78060', fontFamily: "'Noto Sans KR', sans-serif" }}>{s.l}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chef card with SVG illustration */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #fde68a, #fbbf24)', borderRadius: 24, padding: '44px 32px', textAlign: 'center', border: '2px solid #fcd34d', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Pattern overlay */}
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute', inset: 0, opacity: 0.07 }}>
|
||||
<pattern id="chef-dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
|
||||
<circle cx="12" cy="12" r="2" fill="#78350f" />
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill="url(#chef-dots)" />
|
||||
</svg>
|
||||
{/* Chef SVG illustration */}
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
<div style={{ width: 96, height: 96, borderRadius: '50%', background: 'rgba(255,255,255,0.4)', margin: '0 auto 20px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '3px solid rgba(255,255,255,0.6)' }}>
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none">
|
||||
{/* Chef hat */}
|
||||
<ellipse cx="28" cy="20" rx="16" ry="12" fill="white" />
|
||||
<rect x="12" y="18" width="32" height="8" rx="2" fill="white" />
|
||||
<ellipse cx="28" cy="18" rx="8" ry="5" fill="#f3f4f6" />
|
||||
{/* Face */}
|
||||
<circle cx="28" cy="34" r="12" fill="#fbbf24" />
|
||||
<circle cx="24" cy="32" r="1.5" fill="#78350f" />
|
||||
<circle cx="32" cy="32" r="1.5" fill="#78350f" />
|
||||
<path d="M23 38 Q28 42 33 38" stroke="#78350f" strokeWidth="1.5" strokeLinecap="round" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ fontSize: 20, fontFamily: 'Playfair Display, serif', color: '#451a03', fontWeight: 700, marginBottom: 6 }}>
|
||||
Chef Kim Dongwoo
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#78350f', fontFamily: "'Noto Sans KR', sans-serif", lineHeight: 1.7, marginBottom: 16 }}>
|
||||
Le Cordon Bleu Paris 졸업<br />
|
||||
2009년 르 쁘띠 포르 창업
|
||||
</div>
|
||||
{/* Awards */}
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
{['미쉐린 추천', '블루리본', '로컬 레전드'].map(award => (
|
||||
<span key={award} style={{
|
||||
background: 'rgba(255,255,255,0.5)', color: '#78350f',
|
||||
fontSize: 10, fontWeight: 700, padding: '3px 10px',
|
||||
borderRadius: 100, border: '1px solid rgba(255,255,255,0.7)',
|
||||
fontFamily: "'Noto Sans KR', sans-serif",
|
||||
}}>{award}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Customer Reviews */}
|
||||
<section style={{ padding: '80px 48px', background: '#fef9f0', borderTop: '1px solid #fde8c8' }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 48 }}>
|
||||
<div style={{ fontSize: 11, color: '#b45309', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 10 }}>
|
||||
Guest Reviews
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Playfair Display, serif', fontSize: 36, color: '#451a03', marginBottom: 12 }}>
|
||||
고객들의 이야기
|
||||
</h2>
|
||||
<div style={{ display: 'flex', gap: 4, justifyContent: 'center', alignItems: 'center', marginBottom: 8 }}>
|
||||
{[1,2,3,4,5].map(i => (
|
||||
<svg key={i} width="18" height="18" viewBox="0 0 18 18"><path d="M9 1.5l2 4L16 7l-3.5 3.4.8 4.8L9 13l-4.3 2.3.8-4.8L2 7l5-.5z" fill="#d97706" /></svg>
|
||||
))}
|
||||
<span style={{ marginLeft: 8, fontSize: 15, color: '#78350f', fontFamily: 'Playfair Display, serif', fontWeight: 700 }}>4.9</span>
|
||||
<span style={{ fontSize: 13, color: '#a78060', fontFamily: "'Noto Sans KR', sans-serif" }}> (1,243개 리뷰)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
{reviews.map((r) => (
|
||||
<div key={r.name} className="review-card" style={{
|
||||
background: 'white', borderRadius: 20, padding: '28px 24px',
|
||||
border: '1px solid #fde8c8', boxShadow: '0 2px 12px rgba(120,53,15,0.04)',
|
||||
}}>
|
||||
{/* Stars */}
|
||||
<div style={{ display: 'flex', gap: 3, marginBottom: 14 }}>
|
||||
{[1,2,3,4,5].map(i => (
|
||||
<svg key={i} width="13" height="13" viewBox="0 0 13 13"><path d="M6.5 1l1.5 3 3.3.5-2.4 2.3.6 3.3L6.5 9 3 10.1l.6-3.3L1.2 4.5 4.5 4z" fill="#d97706" /></svg>
|
||||
))}
|
||||
{r.verified && (
|
||||
<span style={{ marginLeft: 6, fontSize: 10, color: '#16a34a', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 600 }}>
|
||||
✓ 인증 방문
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Quote SVG */}
|
||||
<svg width="24" height="18" viewBox="0 0 24 18" fill="none" style={{ marginBottom: 10, opacity: 0.3 }}>
|
||||
<path d="M0 18V10.5C0 4.7 3.5 1.3 10.5 0l1.5 2.5C8.3 3.7 6.3 5.7 6 9h4.5V18H0zm12 0V10.5C12 4.7 15.5 1.3 22.5 0L24 2.5C20.3 3.7 18.3 5.7 18 9h4.5V18H12z" fill="#b45309" />
|
||||
</svg>
|
||||
<p style={{ fontSize: 14, color: '#78350f', lineHeight: 1.75, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 18 }}>
|
||||
{r.text}
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 36, height: 36, borderRadius: '50%', background: 'linear-gradient(135deg, #fde68a, #d97706)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, fontWeight: 700, color: '#451a03', fontFamily: 'Playfair Display, serif' }}>
|
||||
{r.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: '#451a03', fontFamily: "'Noto Sans KR', sans-serif" }}>{r.name}</div>
|
||||
<div style={{ fontSize: 11, color: '#c9a87c', fontFamily: "'Noto Sans KR', sans-serif" }}>{r.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Hours & Location */}
|
||||
<section style={{ padding: '72px 48px', background: '#451a03' }}>
|
||||
<div style={{ maxWidth: 960, margin: '0 auto', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 64 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#fbbf24', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 16 }}>Hours</div>
|
||||
<h3 style={{ fontFamily: 'Playfair Display, serif', fontSize: 28, color: 'white', marginBottom: 28 }}>운영 시간</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{hours.map((h) => (
|
||||
<div key={h.day} style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid rgba(255,255,255,0.08)', paddingBottom: 12 }}>
|
||||
<span style={{ fontSize: 14, color: '#fde68a', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 600 }}>{h.day}</span>
|
||||
<span style={{ fontSize: 14, color: '#fcd34d', fontFamily: 'Playfair Display, serif' }}>{h.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 20, padding: '12px 16px', background: 'rgba(251,191,36,0.1)', borderRadius: 10, border: '1px solid rgba(251,191,36,0.2)' }}>
|
||||
<p style={{ fontSize: 13, color: '#fde68a', fontFamily: "'Noto Sans KR', sans-serif", lineHeight: 1.6 }}>
|
||||
⚡ 품절 시 조기 마감될 수 있습니다.<br />
|
||||
인스타그램에서 당일 재고를 확인하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#fbbf24', fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', fontFamily: 'Playfair Display, serif', marginBottom: 16 }}>Location</div>
|
||||
<h3 style={{ fontFamily: 'Playfair Display, serif', fontSize: 28, color: 'white', marginBottom: 20 }}>매장 위치</h3>
|
||||
<p style={{ color: '#fde68a', fontSize: 15, lineHeight: 1.7, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 20 }}>
|
||||
서울특별시 마포구 연남동 224-14<br />연남로 68 르 쁘띠 포르
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{[
|
||||
{ icon: 'M', text: '2호선 홍대입구역 3번 출구 도보 5분' },
|
||||
{ icon: 'T', text: '02-334-5678' },
|
||||
{ icon: '@', text: '@lepetitfort_seoul' },
|
||||
].map((info) => (
|
||||
<div key={info.text} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 24, height: 24, borderRadius: '50%', background: 'rgba(251,191,36,0.15)', border: '1px solid rgba(251,191,36,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, color: '#fbbf24', fontFamily: 'Playfair Display, serif', flexShrink: 0 }}>
|
||||
{info.icon}
|
||||
</div>
|
||||
<span style={{ color: '#fcd34d', fontSize: 13, fontFamily: "'Noto Sans KR', sans-serif" }}>{info.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA — Custom Cake */}
|
||||
<section style={{ background: 'linear-gradient(135deg, #fef3c7, #fde68a)', padding: '72px 48px', textAlign: 'center' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'rgba(180,83,9,0.1)', borderRadius: 100, padding: '5px 14px', marginBottom: 20 }}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M6 1l1.3 2.6L10 4.1 8 6.1l.5 2.9L6 7.6 3.5 9l.5-2.9-2-2 2.7-.5z" fill="#b45309" /></svg>
|
||||
<span style={{ fontSize: 12, color: '#b45309', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 700 }}>Custom Order</span>
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Playfair Display, serif', fontSize: 38, color: '#451a03', marginBottom: 14 }}>
|
||||
특별한 날을 위한 케이크
|
||||
</h2>
|
||||
<p style={{ color: '#78350f', fontSize: 16, lineHeight: 1.75, marginBottom: 12, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
생일, 기념일, 웨딩 케이크까지.<br />
|
||||
최소 3일 전 예약 시 원하시는 케이크를 제작해드립니다.
|
||||
</p>
|
||||
<p style={{ color: '#a78060', fontSize: 13, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 32 }}>
|
||||
가격 상담 무료 · 사진 참고 가능 · 직접 수령 또는 배달 가능
|
||||
</p>
|
||||
<button className="bk-btn-primary" style={{
|
||||
background: '#b45309', color: 'white', border: 'none',
|
||||
padding: '15px 44px', borderRadius: 28, fontSize: 16, fontWeight: 700,
|
||||
cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif",
|
||||
boxShadow: '0 8px 28px rgba(180,83,9,0.4)',
|
||||
}}>
|
||||
케이크 주문 예약하기 →
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{ background: '#1c1008', padding: '28px 48px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ color: '#78350f', fontSize: 14, fontFamily: 'Playfair Display, serif', fontStyle: 'italic', marginBottom: 4 }}>
|
||||
Le Petit Fort — Artisan Boulangerie
|
||||
</div>
|
||||
<div style={{ color: '#3c1a08', fontSize: 12, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
© 2024 르 쁘띠 포르. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
{['Instagram', 'Naver Map', 'Kakao'].map((s) => (
|
||||
<span key={s} style={{ fontSize: 12, color: '#78350f', fontFamily: "'Noto Sans KR', sans-serif", cursor: 'pointer' }}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function CorporateSample() {
|
||||
const services = [
|
||||
{
|
||||
title: 'IT 인프라 구축',
|
||||
desc: '기업 맞춤형 서버 환경 설계부터 클라우드 마이그레이션까지, 안정적인 IT 기반을 구축합니다.',
|
||||
detail: '온프레미스 · 하이브리드 클라우드 · AWS · Azure',
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 28, height: 28 }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12H3a9 9 0 1018 0h-2M12 3v4m0 10v4M4.22 4.22l2.83 2.83m9.9 9.9l2.83 2.83M3 12h.01M20.99 12H21M4.22 19.78l2.83-2.83m9.9-9.9l2.83-2.83" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '사이버 보안 솔루션',
|
||||
desc: '최신 위협에 대응하는 엔터프라이즈급 보안 시스템. 침해사고 예방부터 대응까지 통합 관리합니다.',
|
||||
detail: 'ISMS · 취약점 분석 · 보안 모니터링 · 컴플라이언스',
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 28, height: 28 }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '디지털 전환 (DX)',
|
||||
desc: '레거시 시스템을 현대화하고 비즈니스 프로세스를 자동화하는 DX 컨설팅을 제공합니다.',
|
||||
detail: 'ERP 연동 · 프로세스 자동화 · 데이터 시각화 · AI 도입',
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} style={{ width: 28, height: 28 }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ num: '15+', label: '년 업력' },
|
||||
{ num: '340+', label: '완료 프로젝트' },
|
||||
{ num: '180+', label: '기업 고객사' },
|
||||
{ num: '99.9%', label: '서비스 가동률' },
|
||||
];
|
||||
|
||||
const certs = ['AWS Partner', 'ISO 27001', 'ISMS-P 인증', 'Microsoft Partner', 'Google Cloud'];
|
||||
|
||||
const clients = ['삼성전자', 'LG유플러스', '현대모비스', 'SK하이닉스', 'KT', '신한은행', 'NH농협', '롯데정보통신'];
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote: '마이그레이션 기간 동안 단 한 번의 서비스 중단도 없었습니다. 완벽한 이전이었습니다.',
|
||||
name: '이○○ CTO',
|
||||
company: '핀테크 스타트업',
|
||||
},
|
||||
{
|
||||
quote: '보안 감사 이후 취약점 제로. 컴플라이언스 대응이 이렇게 빠를 수 있다는 게 놀라웠습니다.',
|
||||
name: '박○○ IT팀장',
|
||||
company: '중견 제조사',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ background: '#ffffff', minHeight: '100vh', color: '#1e293b', fontFamily: 'system-ui, sans-serif' }}>
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700;800&family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes floatDot { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||
@keyframes gridPan { from { background-position: 0 0; } to { background-position: 60px 60px; } }
|
||||
.corp-card:hover { transform: translateY(-5px); box-shadow: 0 24px 64px rgba(29,78,216,0.1); }
|
||||
.corp-card { transition: transform 0.3s, box-shadow 0.3s; }
|
||||
.corp-client:hover { background: #eff6ff !important; border-color: #bfdbfe !important; color: #1d4ed8 !important; }
|
||||
.corp-client { transition: all 0.2s; }
|
||||
.corp-cert:hover { border-color: #93c5fd !important; }
|
||||
.corp-cert { transition: border-color 0.2s; }
|
||||
.corp-btn-primary:hover { background: #1d4ed8 !important; box-shadow: 0 8px 24px rgba(29,78,216,0.4) !important; }
|
||||
.corp-btn-primary { transition: all 0.2s; }
|
||||
`}</style>
|
||||
|
||||
{/* Back Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #1e1b4b, #312e81)', padding: '10px 24px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none', fontFamily: "'Noto Sans KR', sans-serif", display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#6366f1', fontSize: 12, fontFamily: 'Montserrat, sans-serif', fontWeight: 700 }}>SAMPLE · 기업 홈페이지</span>
|
||||
</div>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav style={{ background: '#fff', borderBottom: '1px solid #e2e8f0', padding: '0 48px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 68, position: 'sticky', top: 0, zIndex: 100, boxShadow: '0 2px 16px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 38, height: 38, borderRadius: 10, background: 'linear-gradient(135deg, #1d4ed8, #1e40af)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: 16, fontWeight: 800, fontFamily: 'Montserrat, sans-serif' }}>T</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: '#0f172a', fontFamily: 'Montserrat, sans-serif', lineHeight: 1 }}>TechSolution</div>
|
||||
<div style={{ fontSize: 10, color: '#94a3b8', letterSpacing: '0.1em', fontFamily: 'Montserrat, sans-serif' }}>ENTERPRISE IT</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 28, alignItems: 'center' }}>
|
||||
{['회사소개', '서비스', '포트폴리오', '고객사', '채용', '연락처'].map((item) => (
|
||||
<span key={item} style={{ fontSize: 13, color: '#475569', cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif", fontWeight: 500 }}>{item}</span>
|
||||
))}
|
||||
<button className="corp-btn-primary" style={{ background: '#2563eb', color: 'white', border: 'none', padding: '9px 22px', borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif", boxShadow: '0 4px 16px rgba(37,99,235,0.3)' }}>
|
||||
문의하기
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section style={{ background: 'linear-gradient(150deg, #0a192f 0%, #0d2757 50%, #0a2060 100%)', padding: '96px 48px 80px', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Animated grid */}
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(59,130,246,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(59,130,246,0.08) 1px, transparent 1px)', backgroundSize: '60px 60px', animation: 'gridPan 12s linear infinite' }} />
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 70% 60% at 75% 50%, rgba(59,130,246,0.12) 0%, transparent 70%)' }} />
|
||||
|
||||
{/* Floating tech nodes */}
|
||||
{[
|
||||
{ x: '68%', y: '20%', delay: '0s', size: 6, color: '#60a5fa' },
|
||||
{ x: '78%', y: '55%', delay: '1s', size: 4, color: '#a78bfa' },
|
||||
{ x: '85%', y: '35%', delay: '2s', size: 8, color: '#34d399' },
|
||||
{ x: '72%', y: '72%', delay: '0.5s', size: 5, color: '#60a5fa' },
|
||||
].map((dot, i) => (
|
||||
<div key={i} style={{ position: 'absolute', left: dot.x, top: dot.y, width: dot.size, height: dot.size, borderRadius: '50%', background: dot.color, boxShadow: `0 0 12px ${dot.color}`, animation: `floatDot 3s ease-in-out ${dot.delay} infinite` }} />
|
||||
))}
|
||||
|
||||
{/* SVG Tech Illustration */}
|
||||
<svg style={{ position: 'absolute', right: 60, top: '50%', transform: 'translateY(-50%)', width: 320, height: 280, opacity: 0.12 }} viewBox="0 0 320 280">
|
||||
<rect x="40" y="40" width="100" height="70" rx="8" stroke="#60a5fa" strokeWidth="1.5" fill="none"/>
|
||||
<rect x="180" y="40" width="100" height="70" rx="8" stroke="#a78bfa" strokeWidth="1.5" fill="none"/>
|
||||
<rect x="110" y="170" width="100" height="70" rx="8" stroke="#34d399" strokeWidth="1.5" fill="none"/>
|
||||
<line x1="140" y1="110" x2="160" y2="170" stroke="#60a5fa" strokeWidth="1" strokeDasharray="4 4"/>
|
||||
<line x1="180" y1="110" x2="160" y2="170" stroke="#a78bfa" strokeWidth="1" strokeDasharray="4 4"/>
|
||||
<circle cx="90" cy="75" r="16" stroke="#60a5fa" strokeWidth="1" fill="rgba(96,165,250,0.1)"/>
|
||||
<circle cx="230" cy="75" r="16" stroke="#a78bfa" strokeWidth="1" fill="rgba(167,139,250,0.1)"/>
|
||||
<circle cx="160" cy="205" r="20" stroke="#34d399" strokeWidth="1" fill="rgba(52,211,153,0.1)"/>
|
||||
</svg>
|
||||
|
||||
<div style={{ maxWidth: 680, position: 'relative', animation: 'fadeInUp 0.8s ease forwards' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: 'rgba(96,165,250,0.1)', border: '1px solid rgba(96,165,250,0.25)', padding: '6px 16px', borderRadius: 4, marginBottom: 28, fontFamily: 'Montserrat, sans-serif' }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#34d399', boxShadow: '0 0 8px rgba(52,211,153,0.8)' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', color: '#60a5fa', textTransform: 'uppercase' }}>Enterprise IT Solutions</span>
|
||||
</div>
|
||||
<h1 style={{ fontFamily: 'Montserrat, sans-serif', fontSize: 'clamp(34px, 4vw, 54px)', fontWeight: 800, color: 'white', lineHeight: 1.2, marginBottom: 20 }}>
|
||||
디지털 혁신으로<br />
|
||||
<span style={{ background: 'linear-gradient(90deg, #60a5fa, #a78bfa)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>비즈니스의 미래</span>를<br />
|
||||
설계합니다
|
||||
</h1>
|
||||
<p style={{ color: '#94a3b8', fontSize: 16, lineHeight: 1.85, marginBottom: 36, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
15년의 경험과 검증된 기술력으로 기업의 IT 인프라를 혁신합니다.<br />
|
||||
클라우드, 보안, 디지털 전환까지 원스톱으로 제공합니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 14, flexWrap: 'wrap' }}>
|
||||
<button className="corp-btn-primary" style={{ background: '#2563eb', color: 'white', border: 'none', padding: '14px 32px', borderRadius: 8, fontSize: 15, fontWeight: 700, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif", boxShadow: '0 8px 24px rgba(37,99,235,0.35)' }}>
|
||||
서비스 상담 신청 →
|
||||
</button>
|
||||
<button style={{ background: 'transparent', color: '#cbd5e1', border: '1px solid rgba(255,255,255,0.18)', padding: '14px 32px', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
회사 소개서 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<section style={{ background: '#1d4ed8', padding: '36px 48px' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} style={{ textAlign: 'center', padding: '12px 24px', borderRight: i < 3 ? '1px solid rgba(255,255,255,0.2)' : 'none' }}>
|
||||
<div style={{ fontSize: 38, fontWeight: 800, color: 'white', fontFamily: 'Montserrat, sans-serif', lineHeight: 1 }}>{s.num}</div>
|
||||
<div style={{ fontSize: 13, color: '#bfdbfe', fontFamily: "'Noto Sans KR', sans-serif", marginTop: 6 }}>{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Certifications */}
|
||||
<section style={{ background: '#f8fafc', padding: '28px 48px', borderBottom: '1px solid #e2e8f0' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto', display: 'flex', alignItems: 'center', gap: 24, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', letterSpacing: '0.15em', textTransform: 'uppercase', fontFamily: 'Montserrat, sans-serif', flexShrink: 0 }}>인증 · 파트너십</span>
|
||||
{certs.map((c) => (
|
||||
<div key={c} className="corp-cert" style={{ padding: '8px 18px', border: '1px solid #e2e8f0', borderRadius: 6, background: 'white', fontSize: 12, fontWeight: 700, color: '#475569', fontFamily: 'Montserrat, sans-serif', letterSpacing: '0.03em' }}>
|
||||
{c}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services */}
|
||||
<section style={{ padding: '88px 48px', background: 'white' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 56 }}>
|
||||
<div style={{ fontSize: 11, color: '#2563eb', fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', fontFamily: 'Montserrat, sans-serif', marginBottom: 12 }}>Our Services</div>
|
||||
<h2 style={{ fontFamily: 'Montserrat, sans-serif', fontSize: 36, fontWeight: 800, color: '#0f172a', marginBottom: 14 }}>핵심 서비스</h2>
|
||||
<p style={{ color: '#64748b', fontSize: 16, fontFamily: "'Noto Sans KR', sans-serif" }}>기업의 성장을 이끄는 검증된 IT 솔루션</p>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 28 }}>
|
||||
{services.map((svc, i) => (
|
||||
<div key={svc.title} className="corp-card" style={{ padding: 36, borderRadius: 18, background: 'white', border: '1px solid #e2e8f0', boxShadow: '0 4px 24px rgba(0,0,0,0.05)', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, background: ['linear-gradient(90deg, #2563eb, #60a5fa)', 'linear-gradient(90deg, #7c3aed, #a78bfa)', 'linear-gradient(90deg, #059669, #34d399)'][i] }} />
|
||||
<div style={{ width: 56, height: 56, borderRadius: 14, background: ['#eff6ff', '#f5f3ff', '#f0fdf4'][i], border: `1px solid ${['#bfdbfe', '#ddd6fe', '#bbf7d0'][i]}`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 22, color: ['#2563eb', '#7c3aed', '#059669'][i] }}>
|
||||
{svc.icon}
|
||||
</div>
|
||||
<h3 style={{ fontSize: 19, fontWeight: 700, color: '#0f172a', fontFamily: 'Montserrat, sans-serif', marginBottom: 12 }}>{svc.title}</h3>
|
||||
<p style={{ fontSize: 14, color: '#64748b', lineHeight: 1.8, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 16 }}>{svc.desc}</p>
|
||||
<div style={{ padding: '10px 14px', background: '#f8fafc', borderRadius: 8, fontSize: 12, color: '#94a3b8', fontFamily: 'Montserrat, sans-serif', letterSpacing: '0.02em' }}>{svc.detail}</div>
|
||||
<div style={{ marginTop: 20, color: '#2563eb', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'Montserrat, sans-serif', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
자세히 보기
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section style={{ padding: '80px 48px', background: 'linear-gradient(135deg, #f0f7ff, #f8f5ff)' }}>
|
||||
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 48 }}>
|
||||
<div style={{ fontSize: 11, color: '#2563eb', fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', fontFamily: 'Montserrat, sans-serif', marginBottom: 12 }}>Client Reviews</div>
|
||||
<h2 style={{ fontFamily: 'Montserrat, sans-serif', fontSize: 32, fontWeight: 800, color: '#0f172a' }}>고객 후기</h2>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
|
||||
{testimonials.map((t, i) => (
|
||||
<div key={i} style={{ background: 'white', borderRadius: 16, padding: '32px 28px', border: '1px solid #e2e8f0', boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ fontSize: 32, color: '#bfdbfe', fontFamily: 'Georgia, serif', lineHeight: 1, marginBottom: 16 }}>"</div>
|
||||
<p style={{ fontSize: 15, color: '#475569', lineHeight: 1.8, fontFamily: "'Noto Sans KR', sans-serif", marginBottom: 20 }}>{t.quote}</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'linear-gradient(135deg, #2563eb, #7c3aed)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: 15, fontWeight: 700 }}>
|
||||
{t.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#0f172a', fontFamily: "'Noto Sans KR', sans-serif" }}>{t.name}</div>
|
||||
<div style={{ fontSize: 12, color: '#94a3b8', fontFamily: "'Noto Sans KR', sans-serif" }}>{t.company}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Clients */}
|
||||
<section style={{ padding: '72px 48px', background: 'white' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 11, color: '#94a3b8', fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', fontFamily: 'Montserrat, sans-serif', marginBottom: 36 }}>Trusted By Leading Companies</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 12 }}>
|
||||
{clients.map((c) => (
|
||||
<div key={c} className="corp-client" style={{ padding: '14px 28px', borderRadius: 10, border: '1px solid #e2e8f0', background: '#f8fafc', fontSize: 14, fontWeight: 600, color: '#475569', fontFamily: "'Noto Sans KR', sans-serif", cursor: 'pointer' }}>{c}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact CTA */}
|
||||
<section style={{ background: 'linear-gradient(150deg, #0a192f, #0d2757)', padding: '88px 48px', textAlign: 'center' }}>
|
||||
<div style={{ maxWidth: 600, margin: '0 auto' }}>
|
||||
<div style={{ fontSize: 11, color: '#60a5fa', fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', fontFamily: 'Montserrat, sans-serif', marginBottom: 20 }}>Get Started</div>
|
||||
<h2 style={{ fontFamily: 'Montserrat, sans-serif', fontSize: 38, fontWeight: 800, color: 'white', marginBottom: 16, lineHeight: 1.2 }}>
|
||||
프로젝트를 시작하세요
|
||||
</h2>
|
||||
<p style={{ color: '#94a3b8', fontSize: 16, lineHeight: 1.8, marginBottom: 36, fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
IT 솔루션이 필요하시면 언제든지 연락해주세요.<br />
|
||||
전담 컨설턴트가 최적의 방안을 제안해드립니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<button className="corp-btn-primary" style={{ background: '#2563eb', color: 'white', border: 'none', padding: '15px 40px', borderRadius: 8, fontSize: 15, fontWeight: 700, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif", boxShadow: '0 8px 24px rgba(37,99,235,0.4)' }}>
|
||||
무료 상담 신청
|
||||
</button>
|
||||
<button style={{ background: 'transparent', color: '#cbd5e1', border: '1px solid rgba(255,255,255,0.18)', padding: '15px 40px', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: 'pointer', fontFamily: "'Noto Sans KR', sans-serif" }}>
|
||||
02-1234-5678
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{ background: '#020817', padding: '28px 48px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div style={{ color: '#334155', fontSize: 13, fontFamily: "'Noto Sans KR', sans-serif" }}>© 2024 ㈜테크솔루션. All rights reserved.</div>
|
||||
<div style={{ color: '#1e293b', fontSize: 12, fontFamily: 'Montserrat, sans-serif' }}>서울특별시 강남구 테헤란로 123 테크타워 15F</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
const kpis = [
|
||||
{ label: '월 활성 사용자', value: '124,832', change: '+12.4%', up: true, color: '#3b82f6', sparkline: [40, 55, 45, 70, 60, 85, 100] },
|
||||
{ label: '월 매출', value: '₩284M', change: '+8.7%', up: true, color: '#10b981', sparkline: [50, 60, 55, 75, 80, 78, 100] },
|
||||
{ label: '전환율', value: '3.62%', change: '-0.3%', up: false, color: '#f59e0b', sparkline: [90, 80, 85, 75, 70, 72, 68] },
|
||||
{ label: '고객 만족도', value: '4.8', change: '+0.2', up: true, color: '#8b5cf6', sparkline: [70, 72, 74, 76, 78, 80, 85] },
|
||||
];
|
||||
|
||||
const lineData = [
|
||||
{ month: 'Jan', revenue: 65, users: 48 },
|
||||
{ month: 'Feb', revenue: 78, users: 55 },
|
||||
{ month: 'Mar', revenue: 72, users: 52 },
|
||||
{ month: 'Apr', revenue: 89, users: 64 },
|
||||
{ month: 'May', revenue: 95, users: 71 },
|
||||
{ month: 'Jun', revenue: 82, users: 66 },
|
||||
{ month: 'Jul', revenue: 110, users: 88 },
|
||||
{ month: 'Aug', revenue: 128, users: 100 },
|
||||
];
|
||||
|
||||
const activities = [
|
||||
{ user: 'lee@company.com', action: '프리미엄 플랜 구독', time: '2분 전', status: 'success', avatar: 'L' },
|
||||
{ user: 'park@startup.io', action: 'API 한도 초과 경고', time: '14분 전', status: 'warning', avatar: 'P' },
|
||||
{ user: 'kim@corp.kr', action: '팀 멤버 5명 초대', time: '31분 전', status: 'info', avatar: 'K' },
|
||||
{ user: 'choi@brand.com', action: '결제 실패 (카드 만료)', time: '1시간 전', status: 'error', avatar: 'C' },
|
||||
{ user: 'jung@agency.co', action: '새 워크스페이스 생성', time: '2시간 전', status: 'success', avatar: 'J' },
|
||||
];
|
||||
|
||||
const menus = [
|
||||
{ id: 'overview', label: 'Overview', dot: '#3b82f6' },
|
||||
{ id: 'analytics', label: 'Analytics', dot: '#10b981' },
|
||||
{ id: 'users', label: 'Users', dot: null },
|
||||
{ id: 'revenue', label: 'Revenue', dot: null },
|
||||
{ id: 'reports', label: 'Reports', dot: null },
|
||||
{ id: 'settings', label: 'Settings', dot: null },
|
||||
];
|
||||
|
||||
const channels = [
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
const alerts = [
|
||||
{ type: 'error', msg: 'API 응답 지연 (p99 > 2s)', time: '5분 전' },
|
||||
{ type: 'warning', msg: '스토리지 사용량 85% 초과', time: '32분 전' },
|
||||
{ type: 'success', msg: '일일 백업 완료', time: '1시간 전' },
|
||||
];
|
||||
|
||||
const statusColor: Record<string, string> = { success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' };
|
||||
|
||||
function SparkLine({ data, color }: { data: number[]; color: string }) {
|
||||
const max = Math.max(...data);
|
||||
const min = Math.min(...data);
|
||||
const h = 28;
|
||||
const w = 72;
|
||||
const step = w / (data.length - 1);
|
||||
const pts = data.map((v, i) => {
|
||||
const x = i * step;
|
||||
const y = h - ((v - min) / (max - min || 1)) * h;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
return (
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.7" />
|
||||
<polyline points={`${pts} ${w},${h} 0,${h}`} fill={color} fillOpacity="0.08" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LineChart({ data }: { data: typeof lineData }) {
|
||||
const chartH = 130;
|
||||
const chartW = 440;
|
||||
const padL = 32;
|
||||
const padB = 24;
|
||||
const innerW = chartW - padL;
|
||||
const innerH = chartH - padB;
|
||||
const maxRev = Math.max(...data.map(d => d.revenue));
|
||||
|
||||
const revPts = data.map((d, i) => {
|
||||
const x = padL + (i / (data.length - 1)) * innerW;
|
||||
const y = (1 - d.revenue / maxRev) * innerH;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
|
||||
const usrPts = data.map((d, i) => {
|
||||
const x = padL + (i / (data.length - 1)) * innerW;
|
||||
const y = (1 - d.users / maxRev) * innerH;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<svg width="100%" viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="xMidYMid meet">
|
||||
{/* Grid lines */}
|
||||
{[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
|
||||
<g key={i}>
|
||||
<line x1={padL} y1={t * innerH} x2={chartW} y2={t * innerH} stroke="rgba(255,255,255,0.04)" strokeWidth="1" />
|
||||
<text x={padL - 4} y={t * innerH + 4} textAnchor="end" fontSize="8" fill="#1e3a5f">
|
||||
{Math.round(maxRev * (1 - t))}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
{/* Area fills */}
|
||||
<defs>
|
||||
<linearGradient id="grad-rev" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="grad-usr" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#10b981" stopOpacity="0.2" />
|
||||
<stop offset="100%" stopColor="#10b981" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polyline points={`${revPts} ${chartW},${innerH} ${padL},${innerH}`} fill="url(#grad-rev)" stroke="none" />
|
||||
<polyline points={`${usrPts} ${chartW},${innerH} ${padL},${innerH}`} fill="url(#grad-usr)" stroke="none" />
|
||||
{/* Lines */}
|
||||
<polyline points={revPts} fill="none" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<polyline points={usrPts} fill="none" stroke="#10b981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="4 2" />
|
||||
{/* Dots at last point */}
|
||||
{[{ pts: revPts, color: '#3b82f6' }, { pts: usrPts, color: '#10b981' }].map(({ pts: p, color }) => {
|
||||
const last = p.split(' ').pop()!;
|
||||
const [lx, ly] = last.split(',').map(Number);
|
||||
return <circle key={color} cx={lx} cy={ly} r="3.5" fill={color} stroke="#0f172a" strokeWidth="2" />;
|
||||
})}
|
||||
{/* X axis labels */}
|
||||
{data.map((d, i) => (
|
||||
<text key={i} x={padL + (i / (data.length - 1)) * innerW} y={innerH + 16} textAnchor="middle" fontSize="8" fill="#334155">{d.month}</text>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardSample() {
|
||||
const [activeMenu, setActiveMenu] = useState('overview');
|
||||
const [alertsVisible, setAlertsVisible] = useState(true);
|
||||
|
||||
return (
|
||||
<div style={{ background: '#0a0f1e', minHeight: '100vh', color: 'white' }}>
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
.dash-menu-item:hover { background: rgba(255,255,255,0.05) !important; }
|
||||
.dash-menu-item { transition: background 0.15s; }
|
||||
.dash-kpi:hover { border-color: rgba(255,255,255,0.1) !important; transform: translateY(-2px); }
|
||||
.dash-kpi { transition: border-color 0.2s, transform 0.2s; cursor: default; }
|
||||
.dash-row:hover { background: rgba(255,255,255,0.03) !important; }
|
||||
.dash-row { transition: background 0.15s; }
|
||||
.quick-action:hover { background: rgba(59,130,246,0.12) !important; border-color: rgba(59,130,246,0.3) !important; }
|
||||
.quick-action { transition: background 0.2s, border-color 0.2s; cursor: pointer; }
|
||||
`}</style>
|
||||
|
||||
{/* Back Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #1e1b4b, #312e81)', padding: '10px 24px', display: 'flex', alignItems: 'center', gap: 12, position: 'relative', zIndex: 200 }}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none', fontFamily: 'DM Sans, sans-serif' }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#38bdf8', fontSize: 12, fontFamily: 'DM Mono, monospace', fontWeight: 500 }}>
|
||||
SAMPLE · 관리자 대시보드
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', height: 'calc(100vh - 40px)' }}>
|
||||
{/* Sidebar */}
|
||||
<aside style={{ width: 220, background: '#060b18', borderRight: '1px solid rgba(255,255,255,0.05)', display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
|
||||
{/* Logo */}
|
||||
<div style={{ padding: '18px 18px 14px', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 34, height: 34, borderRadius: 9, background: 'linear-gradient(135deg, #3b82f6, #06b6d4)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{/* DF logo SVG */}
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none">
|
||||
<rect x="0" y="0" width="7" height="6" rx="1" fill="white" opacity="0.9" />
|
||||
<rect x="0" y="8" width="7" height="6" rx="1" fill="white" opacity="0.6" />
|
||||
<rect x="9" y="0" width="7" height="14" rx="1" fill="white" opacity="0.4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'white', fontFamily: 'DM Sans, sans-serif' }}>DataFlow</div>
|
||||
<div style={{ fontSize: 10, color: '#334155', fontFamily: 'DM Mono, monospace' }}>v2.4.1 · PRO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={{ padding: '12px 10px 8px' }}>
|
||||
<div style={{ fontSize: 9, color: '#1e3a5f', fontFamily: 'DM Mono, monospace', letterSpacing: '0.15em', padding: '4px 10px 6px', textTransform: 'uppercase' }}>QUICK ACTIONS</div>
|
||||
<div style={{ display: 'flex', gap: 6, padding: '0 2px' }}>
|
||||
{[
|
||||
{ label: 'Export', color: '#3b82f6' },
|
||||
{ label: 'Invite', color: '#10b981' },
|
||||
{ label: 'Alert', color: '#f59e0b' },
|
||||
].map(a => (
|
||||
<button key={a.label} className="quick-action" style={{ flex: 1, padding: '6px 4px', borderRadius: 6, border: '1px solid rgba(255,255,255,0.05)', background: 'transparent', color: a.color, fontSize: 10, fontFamily: 'DM Mono, monospace', fontWeight: 500 }}>
|
||||
{a.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav style={{ flex: 1, padding: '4px 10px', overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: 9, color: '#1e3a5f', fontFamily: 'DM Mono, monospace', letterSpacing: '0.15em', padding: '8px 10px 4px', textTransform: 'uppercase' }}>MAIN</div>
|
||||
{menus.map((m) => (
|
||||
<button key={m.id} className="dash-menu-item" onClick={() => setActiveMenu(m.id)} style={{
|
||||
width: '100%', textAlign: 'left', padding: '9px 12px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 10, marginBottom: 2,
|
||||
background: activeMenu === m.id ? 'rgba(59,130,246,0.15)' : 'transparent',
|
||||
color: activeMenu === m.id ? '#60a5fa' : '#475569',
|
||||
}}>
|
||||
{/* Menu icon SVG */}
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ flexShrink: 0 }}>
|
||||
<rect x="1" y="1" width="5" height="5" rx="1.5" fill={activeMenu === m.id ? '#60a5fa' : '#1e3a5f'} />
|
||||
<rect x="8" y="1" width="5" height="5" rx="1.5" fill={activeMenu === m.id ? '#60a5fa' : '#1e3a5f'} opacity="0.6" />
|
||||
<rect x="1" y="8" width="5" height="5" rx="1.5" fill={activeMenu === m.id ? '#60a5fa' : '#1e3a5f'} opacity="0.6" />
|
||||
<rect x="8" y="8" width="5" height="5" rx="1.5" fill={activeMenu === m.id ? '#60a5fa' : '#1e3a5f'} opacity="0.4" />
|
||||
</svg>
|
||||
<span style={{ fontSize: 13, fontWeight: activeMenu === m.id ? 600 : 400, fontFamily: 'DM Sans, sans-serif', flex: 1 }}>
|
||||
{m.label}
|
||||
</span>
|
||||
{m.dot && <div style={{ width: 4, height: 4, borderRadius: '50%', background: m.dot, animation: 'pulse-dot 2s infinite' }} />}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User */}
|
||||
<div style={{ padding: '12px 14px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700 }}>A</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#e2e8f0', fontFamily: 'DM Sans, sans-serif', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>Admin</div>
|
||||
<div style={{ fontSize: 10, color: '#334155', fontFamily: 'DM Mono, monospace' }}>Super Admin</div>
|
||||
</div>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#10b981', flexShrink: 0 }} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main style={{ flex: 1, overflowY: 'auto', animation: 'fadeIn 0.4s ease' }}>
|
||||
{/* Top bar */}
|
||||
<div style={{ padding: '14px 24px', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#080d1a', position: 'sticky', top: 0, zIndex: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)' }}>
|
||||
<circle cx="7" cy="7" r="5" stroke="#334155" strokeWidth="1.5" />
|
||||
<line x1="11" y1="11" x2="14" y2="14" stroke="#334155" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<input placeholder="검색..." style={{ background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)', color: '#94a3b8', padding: '7px 12px 7px 32px', borderRadius: 8, fontSize: 12, fontFamily: 'DM Sans, sans-serif', outline: 'none', width: 180 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<select style={{ background: '#0f172a', border: '1px solid rgba(255,255,255,0.07)', color: '#94a3b8', padding: '7px 12px', borderRadius: 8, fontSize: 12, fontFamily: 'DM Sans, sans-serif', cursor: 'pointer' }}>
|
||||
<option>최근 30일</option>
|
||||
</select>
|
||||
{/* Alert bell */}
|
||||
<button onClick={() => setAlertsVisible(!alertsVisible)} style={{ position: 'relative', background: '#0f172a', border: '1px solid rgba(255,255,255,0.07)', padding: '7px 10px', borderRadius: 8, cursor: 'pointer', display: 'flex' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M7 1.5C4.5 1.5 3 3.2 3 5.5v3L1.5 10.5h11L11 8.5v-3C11 3.2 9.5 1.5 7 1.5z" stroke="#94a3b8" strokeWidth="1.2" fill="none" />
|
||||
<path d="M5.5 10.5a1.5 1.5 0 003 0" stroke="#94a3b8" strokeWidth="1.2" />
|
||||
</svg>
|
||||
<div style={{ position: 'absolute', top: 5, right: 5, width: 7, height: 7, borderRadius: '50%', background: '#ef4444', border: '1.5px solid #080d1a' }} />
|
||||
</button>
|
||||
<button style={{ background: '#3b82f6', border: 'none', color: 'white', padding: '7px 16px', borderRadius: 8, fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'DM Sans, sans-serif' }}>
|
||||
리포트 내보내기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px 24px' }}>
|
||||
{/* Alerts panel */}
|
||||
{alertsVisible && (
|
||||
<div style={{ marginBottom: 20, padding: '14px 18px', borderRadius: 12, background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'white', fontFamily: 'DM Sans, sans-serif' }}>시스템 알림</div>
|
||||
<button onClick={() => setAlertsVisible(false)} style={{ background: 'none', border: 'none', color: '#334155', fontSize: 11, cursor: 'pointer', fontFamily: 'DM Sans, sans-serif' }}>닫기</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{alerts.map((a, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', borderRadius: 8, background: statusColor[a.type] + '10', border: `1px solid ${statusColor[a.type]}25`, flex: 1 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor[a.type], flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, color: '#e2e8f0', fontFamily: 'DM Sans, sans-serif', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.msg}</div>
|
||||
<div style={{ fontSize: 10, color: '#334155', fontFamily: 'DM Mono, monospace' }}>{a.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<div>
|
||||
<h1 style={{ fontFamily: 'DM Sans, sans-serif', fontSize: 22, fontWeight: 700, color: 'white', marginBottom: 2 }}>Overview</h1>
|
||||
<div style={{ fontSize: 12, color: '#334155', fontFamily: 'DM Mono, monospace' }}>2024.08.14 · 오전 10:32 업데이트</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginBottom: 18 }}>
|
||||
{kpis.map((kpi) => (
|
||||
<div key={kpi.label} className="dash-kpi" style={{ padding: '16px 18px', borderRadius: 14, background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, color: '#475569', fontFamily: 'DM Sans, sans-serif', fontWeight: 500, maxWidth: 80 }}>{kpi.label}</div>
|
||||
<SparkLine data={kpi.sparkline} color={kpi.color} />
|
||||
</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700, color: 'white', fontFamily: 'DM Sans, sans-serif', marginBottom: 6 }}>{kpi.value}</div>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 3, background: kpi.up ? 'rgba(16,185,129,0.12)' : 'rgba(239,68,68,0.12)', borderRadius: 6, padding: '2px 7px', fontSize: 11, fontWeight: 700, color: kpi.up ? '#10b981' : '#ef4444', fontFamily: 'DM Mono, monospace' }}>
|
||||
{kpi.up ? '↑' : '↓'} {kpi.change}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts row */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: 14, marginBottom: 18 }}>
|
||||
{/* Line Chart */}
|
||||
<div style={{ padding: '20px 22px', borderRadius: 14, background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'white', fontFamily: 'DM Sans, sans-serif' }}>월별 매출 & 사용자 추이</div>
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<div style={{ width: 20, height: 2, background: '#3b82f6', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 10, color: '#475569', fontFamily: 'DM Mono, monospace' }}>매출</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<div style={{ width: 20, height: 2, background: '#10b981', borderRadius: 2, backgroundImage: 'repeating-linear-gradient(90deg, #10b981 0, #10b981 4px, transparent 4px, transparent 6px)' }} />
|
||||
<span style={{ fontSize: 10, color: '#475569', fontFamily: 'DM Mono, monospace' }}>사용자</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{['1M', '3M', '6M', '1Y'].map((p) => (
|
||||
<button key={p} style={{ background: p === '6M' ? '#1e3a5f' : 'transparent', border: 'none', color: p === '6M' ? '#60a5fa' : '#334155', padding: '3px 7px', borderRadius: 5, fontSize: 10, cursor: 'pointer', fontFamily: 'DM Mono, monospace' }}>{p}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LineChart data={lineData} />
|
||||
</div>
|
||||
|
||||
{/* Channel Progress */}
|
||||
<div style={{ padding: '20px 22px', borderRadius: 14, background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'white', fontFamily: 'DM Sans, sans-serif', marginBottom: 18 }}>채널별 전환율</div>
|
||||
{channels.map((p) => (
|
||||
<div key={p.label} style={{ marginBottom: 13 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5 }}>
|
||||
<span style={{ fontSize: 12, color: '#94a3b8', fontFamily: 'DM Sans, sans-serif' }}>{p.label}</span>
|
||||
<span style={{ fontSize: 11, color: p.color, fontFamily: 'DM Mono, monospace', fontWeight: 500 }}>{p.val}%</span>
|
||||
</div>
|
||||
<div style={{ height: 4, background: '#1e293b', borderRadius: 2, overflow: 'hidden', position: 'relative' }}>
|
||||
<div style={{ height: '100%', borderRadius: 2, background: `linear-gradient(90deg, ${p.color}90, ${p.color})`, width: `${p.val}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Donut summary */}
|
||||
<div style={{ marginTop: 18, display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<svg width="52" height="52" viewBox="0 0 52 52">
|
||||
{(() => {
|
||||
const total = channels.reduce((s, c) => s + c.val, 0);
|
||||
let offset = 0;
|
||||
const r = 20;
|
||||
const circ = 2 * Math.PI * r;
|
||||
return channels.map((c) => {
|
||||
const dash = (c.val / total) * circ;
|
||||
const el = (
|
||||
<circle key={c.label} cx="26" cy="26" r={r} fill="none" stroke={c.color} strokeWidth="8"
|
||||
strokeDasharray={`${dash} ${circ - dash}`}
|
||||
strokeDashoffset={-offset}
|
||||
transform="rotate(-90 26 26)"
|
||||
style={{ transition: 'stroke-dasharray 0.5s' }} />
|
||||
);
|
||||
offset += dash;
|
||||
return el;
|
||||
});
|
||||
})()}
|
||||
<circle cx="26" cy="26" r="12" fill="#0f172a" />
|
||||
<text x="26" y="29" textAnchor="middle" fontSize="8" fill="#60a5fa" fontWeight="700">ALL</text>
|
||||
</svg>
|
||||
<div style={{ fontSize: 11, color: '#475569', fontFamily: 'DM Sans, sans-serif', lineHeight: 1.6 }}>
|
||||
Organic 채널이<br />
|
||||
<span style={{ color: '#3b82f6', fontWeight: 700 }}>최고 전환율 78%</span><br />
|
||||
달성 중
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity Table */}
|
||||
<div style={{ borderRadius: 14, background: '#0f172a', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '14px 18px', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'white', fontFamily: 'DM Sans, sans-serif' }}>최근 활동</div>
|
||||
<button style={{ background: 'none', border: '1px solid rgba(255,255,255,0.08)', color: '#475569', padding: '5px 12px', borderRadius: 6, fontSize: 11, cursor: 'pointer', fontFamily: 'DM Sans, sans-serif' }}>전체 보기</button>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
{['사용자', '활동', '시간', '상태'].map((h) => (
|
||||
<th key={h} style={{ padding: '9px 18px', textAlign: 'left', fontSize: 9, color: '#334155', fontFamily: 'DM Mono, monospace', letterSpacing: '0.12em', textTransform: 'uppercase', fontWeight: 500 }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activities.map((a, i) => (
|
||||
<tr key={i} className="dash-row" style={{ borderBottom: '1px solid rgba(255,255,255,0.025)' }}>
|
||||
<td style={{ padding: '11px 18px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 26, height: 26, borderRadius: '50%', background: `hsl(${i * 60}, 60%, 40%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, flexShrink: 0 }}>{a.avatar}</div>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 11, color: '#60a5fa' }}>{a.user}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '11px 18px', fontFamily: 'DM Sans, sans-serif', fontSize: 13, color: '#94a3b8' }}>{a.action}</td>
|
||||
<td style={{ padding: '11px 18px', fontFamily: 'DM Mono, monospace', fontSize: 11, color: '#334155' }}>{a.time}</td>
|
||||
<td style={{ padding: '11px 18px' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, background: statusColor[a.status] + '12', borderRadius: 6, padding: '3px 8px' }}>
|
||||
<div style={{ width: 5, height: 5, borderRadius: '50%', background: statusColor[a.status] }} />
|
||||
<span style={{ fontSize: 10, color: statusColor[a.status], fontFamily: 'DM Mono, monospace', fontWeight: 500 }}>
|
||||
{a.status === 'success' ? 'OK' : a.status === 'warning' ? 'WARN' : a.status === 'error' ? 'ERR' : 'INFO'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const rankings = [
|
||||
{ rank: 1, name: 'ShadowViper_KR', score: 9_842, tier: 'GRANDMASTER', wins: 312, kda: '18.4', change: '+2' },
|
||||
{ rank: 2, name: 'NightFalcon', score: 9_610, tier: 'GRANDMASTER', wins: 289, kda: '15.2', change: '0' },
|
||||
{ rank: 3, name: 'Xenon_X', score: 9_241, tier: 'MASTER', wins: 267, kda: '12.9', change: '+1' },
|
||||
{ rank: 4, name: 'KR_Dominator', score: 8_970, tier: 'MASTER', wins: 251, kda: '11.7', change: '-1' },
|
||||
{ rank: 5, name: 'Pulse_Wave', score: 8_834, tier: 'DIAMOND', wins: 238, kda: '10.3', change: '+3' },
|
||||
];
|
||||
|
||||
const modes = [
|
||||
{ id: 'solo', name: 'SOLO', sub: '1 vs 1', desc: '순수한 실력으로 맞붙는 1대1 대결', color: '#06b6d4', players: '12,400', season: 'S7' },
|
||||
{ id: 'duo', name: 'DUO', sub: '2 vs 2', desc: '파트너와 함께하는 전략적 팀플레이', color: '#a855f7', players: '28,700', season: 'S7' },
|
||||
{ id: 'squad', name: 'SQUAD', sub: '5 vs 5', desc: '전략과 팀워크로 승리를 쟁취', color: '#f59e0b', players: '7,100', season: 'S7' },
|
||||
];
|
||||
|
||||
const recentMatches = [
|
||||
{ result: 'WIN', mode: 'DUO', duration: '18:32', kills: 12, deaths: 2, assists: 8, rating: '+32' },
|
||||
{ result: 'WIN', mode: 'SOLO', duration: '22:11', kills: 8, deaths: 1, assists: 4, rating: '+24' },
|
||||
{ result: 'LOSS', mode: 'SQUAD', duration: '31:45', kills: 5, deaths: 5, assists: 12, rating: '-18' },
|
||||
{ result: 'WIN', mode: 'DUO', duration: '15:20', kills: 15, deaths: 3, assists: 6, rating: '+40' },
|
||||
];
|
||||
|
||||
const champions = [
|
||||
{ name: 'VIPER', role: 'Assassin', color: '#06b6d4', power: 92, winRate: '63%' },
|
||||
{ name: 'KIRA', role: 'Support', color: '#ec4899', power: 78, winRate: '58%' },
|
||||
{ name: 'IRON', role: 'Tank', color: '#94a3b8', power: 85, winRate: '55%' },
|
||||
{ name: 'NOVA', role: 'Mage', color: '#a855f7', power: 88, winRate: '61%' },
|
||||
];
|
||||
|
||||
const tierColor: Record<string, string> = {
|
||||
GRANDMASTER: '#fbbf24',
|
||||
MASTER: '#a855f7',
|
||||
DIAMOND: '#60a5fa',
|
||||
};
|
||||
|
||||
const tierIcon = (tier: string) => (
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none">
|
||||
<path d="M7 0L9 4L13 5L10 8.5L11 13L7 11L3 13L4 8.5L1 5L5 4Z" fill={tierColor[tier] || '#6b7280'} opacity="0.9" />
|
||||
<path d="M7 2L8.5 5L12 5.8L9.5 8.2L10.2 11.8L7 10L3.8 11.8L4.5 8.2L2 5.8L5.5 5Z" fill="white" opacity="0.2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
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);
|
||||
const [selectedChampion, setSelectedChampion] = useState(0);
|
||||
const [seasonProgress] = useState(67);
|
||||
|
||||
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<typeof setInterval>;
|
||||
if (matchingActive) {
|
||||
timer = setInterval(() => setMatchTimer((t) => t + 1), 1000);
|
||||
} else {
|
||||
setMatchTimer(0);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [matchingActive]);
|
||||
|
||||
return (
|
||||
<div style={{ background: '#000000', minHeight: '100vh', color: 'white', overflowX: 'hidden' }}>
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;500;600;700&display=swap');
|
||||
@keyframes pulse-ring { 0% { transform: scale(0.97); box-shadow: 0 0 0 0 rgba(6,182,212,0.6); } 70% { transform: scale(1); box-shadow: 0 0 0 14px rgba(6,182,212,0); } 100% { transform: scale(0.97); } }
|
||||
@keyframes scan { 0% { top: 0; } 100% { top: 100%; } }
|
||||
@keyframes glitch { 0%, 90%, 100% { transform: none; clip-path: none; } 92% { transform: translate(-2px, 1px); clip-path: inset(30% 0 50% 0); } 94% { transform: translate(2px, -1px); clip-path: inset(60% 0 10% 0); } 96% { transform: translate(-1px, 0); clip-path: inset(10% 0 70% 0); } }
|
||||
@keyframes neonFlicker { 0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { opacity: 1; } 20%, 24%, 55% { opacity: 0.4; } }
|
||||
@keyframes matchPulse { 0%, 100% { box-shadow: 0 0 20px rgba(6,182,212,0.4); } 50% { box-shadow: 0 0 50px rgba(6,182,212,0.9), 0 0 100px rgba(6,182,212,0.3); } }
|
||||
@keyframes float-particle { 0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.6; } 50% { transform: translateY(-20px) rotate(180deg); opacity: 1; } }
|
||||
@keyframes progress-fill { from { width: 0; } to { width: var(--w); } }
|
||||
.mode-card:hover { border-color: var(--card-color) !important; transform: translateY(-4px); box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
||||
.mode-card { transition: border-color 0.3s, transform 0.3s, box-shadow 0.3s; }
|
||||
.rank-row:hover { background: rgba(6,182,212,0.04) !important; }
|
||||
.rank-row { transition: background 0.15s; }
|
||||
.champ-card:hover { border-color: var(--champ-color) !important; transform: scale(1.04); }
|
||||
.champ-card { transition: border-color 0.3s, transform 0.3s; cursor: pointer; }
|
||||
.match-row:hover { background: rgba(255,255,255,0.03) !important; }
|
||||
.match-row { transition: background 0.15s; }
|
||||
`}</style>
|
||||
|
||||
{/* Back Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #1e1b4b, #312e81)', padding: '10px 24px', display: 'flex', alignItems: 'center', gap: 12, position: 'relative', zIndex: 200 }}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none', fontFamily: 'Rajdhani, sans-serif' }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#06b6d4', fontSize: 12, fontFamily: 'Orbitron, sans-serif', fontWeight: 700 }}>
|
||||
SAMPLE · 게임 매칭 시스템
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav style={{ position: 'sticky', top: 0, zIndex: 100, background: 'rgba(0,0,0,0.95)', backdropFilter: 'blur(20px)', borderBottom: '1px solid rgba(6,182,212,0.15)', padding: '0 48px', height: 60, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 18, fontWeight: 900, background: 'linear-gradient(90deg, #06b6d4, #a855f7)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', letterSpacing: '0.1em', animation: 'neonFlicker 8s infinite' }}>
|
||||
NEXUS ARENA
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 28, alignItems: 'center' }}>
|
||||
{['HOME', 'MATCH', 'RANK', 'SHOP', 'CLAN'].map((item) => (
|
||||
<span key={item} style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 11, fontWeight: 700, color: '#1e3a5f', cursor: 'pointer', letterSpacing: '0.1em' }}>{item}</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
{/* Season pass mini */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: '#f59e0b', letterSpacing: '0.1em' }}>S7 PASS</span>
|
||||
<div style={{ width: 60, height: 4, background: '#1a1a2e', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${seasonProgress}%`, background: 'linear-gradient(90deg, #f59e0b, #fbbf24)', borderRadius: 2 }} />
|
||||
</div>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: '#f59e0b' }}>{seasonProgress}%</span>
|
||||
</div>
|
||||
<div style={{ width: 1, height: 20, background: 'rgba(6,182,212,0.2)' }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#10b981', boxShadow: '0 0 8px rgba(16,185,129,0.8)' }} />
|
||||
<span style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 13, color: '#10b981', fontWeight: 600 }}>
|
||||
{onlinePlayers.toLocaleString()} ONLINE
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section style={{ minHeight: '88vh', position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 48px', background: 'radial-gradient(ellipse 80% 70% at 50% 40%, rgba(6,182,212,0.09) 0%, rgba(168,85,247,0.06) 50%, transparent 100%)' }}>
|
||||
{/* Grid */}
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(6,182,212,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(6,182,212,0.05) 1px, transparent 1px)', backgroundSize: '60px 60px' }} />
|
||||
{/* Scan */}
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, height: '1px', background: 'linear-gradient(90deg, transparent, rgba(6,182,212,0.5), transparent)', animation: 'scan 6s linear infinite', pointerEvents: 'none' }} />
|
||||
|
||||
{/* Floating particles */}
|
||||
{[
|
||||
{ left: '10%', top: '20%', size: 4, delay: '0s', color: '#06b6d4' },
|
||||
{ left: '85%', top: '30%', size: 6, delay: '1s', color: '#a855f7' },
|
||||
{ left: '20%', top: '70%', size: 3, delay: '2s', color: '#06b6d4' },
|
||||
{ left: '75%', top: '65%', size: 5, delay: '0.5s', color: '#f59e0b' },
|
||||
{ left: '50%', top: '15%', size: 3, delay: '1.5s', color: '#a855f7' },
|
||||
{ left: '60%', top: '80%', size: 4, delay: '2.5s', color: '#10b981' },
|
||||
].map((p, i) => (
|
||||
<div key={i} style={{ position: 'absolute', left: p.left, top: p.top, width: p.size, height: p.size, borderRadius: '50%', background: p.color, boxShadow: `0 0 ${p.size * 3}px ${p.color}`, animation: `float-particle 4s ${p.delay} ease-in-out infinite` }} />
|
||||
))}
|
||||
|
||||
{/* Corner brackets */}
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={i} style={{ position: 'absolute', width: 40, height: 40, ...s }} />
|
||||
))}
|
||||
|
||||
<div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 11, fontWeight: 700, letterSpacing: '0.3em', color: '#06b6d4', marginBottom: 20, textTransform: 'uppercase' }}>
|
||||
Season 7 · RANKED MATCH
|
||||
</div>
|
||||
<h1 style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 'clamp(48px, 8vw, 96px)', fontWeight: 900, lineHeight: 1.0, marginBottom: 16, background: 'linear-gradient(180deg, #ffffff 0%, rgba(255,255,255,0.55) 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'glitch 12s infinite' }}>
|
||||
NEXUS<br />
|
||||
<span style={{ background: 'linear-gradient(90deg, #06b6d4, #a855f7)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>ARENA</span>
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 18, color: '#475569', letterSpacing: '0.05em', marginBottom: 44 }}>
|
||||
ENTER THE ARENA. CLAIM YOUR GLORY.
|
||||
</p>
|
||||
|
||||
{/* Live Stats */}
|
||||
<div style={{ display: 'flex', gap: 24, justifyContent: 'center', marginBottom: 44 }}>
|
||||
{[
|
||||
{ label: 'ONLINE', val: onlinePlayers.toLocaleString(), color: '#06b6d4' },
|
||||
{ label: 'IN MATCH', val: matchingCount.toLocaleString(), color: '#a855f7' },
|
||||
{ label: 'SERVERS', val: '24', color: '#10b981' },
|
||||
{ label: 'AVG WAIT', val: '< 30s', color: '#f59e0b' },
|
||||
].map((s) => (
|
||||
<div key={s.label} style={{ padding: '14px 20px', background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(8px)', border: `1px solid ${s.color}30`, borderTop: `2px solid ${s.color}`, textAlign: 'center', minWidth: 90 }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 22, fontWeight: 900, color: s.color }}>{s.val}</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 10, color: '#334155', letterSpacing: '0.2em', marginTop: 4 }}>{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Match button */}
|
||||
{!matchingActive ? (
|
||||
<button onClick={() => setMatchingActive(true)} style={{ background: 'linear-gradient(135deg, #06b6d4, #0891b2)', border: 'none', color: 'white', padding: '18px 56px', fontSize: 16, fontWeight: 900, cursor: 'pointer', fontFamily: 'Orbitron, sans-serif', letterSpacing: '0.1em', clipPath: 'polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px))', animation: 'pulse-ring 2s infinite' }}>
|
||||
FIND MATCH
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ padding: '18px 56px', border: '2px solid #06b6d4', color: '#06b6d4', fontFamily: 'Orbitron, sans-serif', fontSize: 16, fontWeight: 900, letterSpacing: '0.1em', animation: 'matchPulse 1.5s infinite', clipPath: 'polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px))' }}>
|
||||
MATCHING... {String(Math.floor(matchTimer / 60)).padStart(2, '0')}:{String(matchTimer % 60).padStart(2, '0')}
|
||||
</div>
|
||||
<button onClick={() => setMatchingActive(false)} style={{ background: 'none', border: 'none', color: '#334155', fontFamily: 'Rajdhani, sans-serif', fontSize: 13, cursor: 'pointer', letterSpacing: '0.1em', textDecoration: 'underline' }}>
|
||||
CANCEL SEARCH
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Season Pass */}
|
||||
<section style={{ padding: '40px 48px', background: 'rgba(0,0,0,0.9)', borderTop: '1px solid rgba(251,191,36,0.15)', borderBottom: '1px solid rgba(251,191,36,0.15)' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 32 }}>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 11, fontWeight: 700, color: '#f59e0b', letterSpacing: '0.2em', marginBottom: 4 }}>SEASON 7 PASS</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 28, fontWeight: 700, color: 'white' }}>
|
||||
Lv. <span style={{ color: '#f59e0b' }}>42</span>
|
||||
<span style={{ fontSize: 14, color: '#475569', marginLeft: 8 }}>/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 12, color: '#475569' }}>{seasonProgress}% 완료</span>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 10, color: '#f59e0b' }}>다음 보상까지 1,840 XP</span>
|
||||
</div>
|
||||
<div style={{ height: 8, background: '#0d0d1a', borderRadius: 4, overflow: 'hidden', border: '1px solid rgba(251,191,36,0.15)' }}>
|
||||
<div style={{ height: '100%', width: `${seasonProgress}%`, background: 'linear-gradient(90deg, #d97706, #fbbf24, #f59e0b)', borderRadius: 4, boxShadow: '0 0 12px rgba(251,191,36,0.4)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, flexShrink: 0 }}>
|
||||
{['스킨 3개', '이모트 5개', '칭호 2개', '골드 5,000'].map((reward) => (
|
||||
<div key={reward} style={{ padding: '6px 12px', background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.2)', borderRadius: 4 }}>
|
||||
<span style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 11, color: '#fbbf24', fontWeight: 600 }}>{reward}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Champion Select */}
|
||||
<section style={{ padding: '60px 48px', background: '#000000' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 28 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: '#06b6d4', letterSpacing: '0.2em', marginBottom: 8 }}>// ROSTER</div>
|
||||
<h2 style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 22, fontWeight: 900, letterSpacing: '0.05em', color: 'white' }}>
|
||||
CHAMPIONS<span style={{ color: '#06b6d4' }}>.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<button style={{ background: 'none', border: '1px solid rgba(6,182,212,0.3)', color: '#06b6d4', padding: '7px 16px', borderRadius: 3, fontFamily: 'Orbitron, sans-serif', fontSize: 10, cursor: 'pointer', letterSpacing: '0.1em' }}>
|
||||
ALL CHAMPIONS →
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
|
||||
{champions.map((c, i) => (
|
||||
<div
|
||||
key={c.name}
|
||||
className="champ-card"
|
||||
onClick={() => setSelectedChampion(i)}
|
||||
// @ts-expect-error CSS variable
|
||||
style={{ '--champ-color': c.color, border: `1px solid ${selectedChampion === i ? c.color : c.color + '25'}`, borderRadius: 6, padding: '22px 18px', background: selectedChampion === i ? c.color + '10' : 'rgba(0,0,0,0.6)', position: 'relative', overflow: 'hidden' }}
|
||||
>
|
||||
{selectedChampion === i && (
|
||||
<div style={{ position: 'absolute', top: 8, right: 8 }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 8, color: c.color, border: `1px solid ${c.color}60`, padding: '1px 6px', borderRadius: 2, letterSpacing: '0.1em' }}>SELECTED</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: `linear-gradient(90deg, transparent, ${c.color}, transparent)` }} />
|
||||
{/* Champion SVG silhouette */}
|
||||
<div style={{ width: 56, height: 56, borderRadius: '50%', background: c.color + '20', border: `2px solid ${c.color}40`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 14 }}>
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||
<circle cx="14" cy="8" r="5" fill={c.color} opacity="0.8" />
|
||||
<path d="M5 28C5 21 9 18 14 18C19 18 23 21 23 28" fill={c.color} opacity="0.6" />
|
||||
<rect x="10" y="18" width="8" height="2" rx="1" fill={c.color} opacity="0.4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 16, fontWeight: 900, color: 'white', marginBottom: 4 }}>{c.name}</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 12, color: c.color, letterSpacing: '0.1em', fontWeight: 600, marginBottom: 14 }}>{c.role}</div>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 8, color: '#334155', letterSpacing: '0.1em' }}>POWER</span>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: c.color }}>{c.power}</span>
|
||||
</div>
|
||||
<div style={{ height: 3, background: '#0d0d1a', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', background: c.color, width: `${c.power}%`, boxShadow: `0 0 6px ${c.color}` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 12, color: '#475569' }}>
|
||||
Win Rate: <span style={{ color: '#10b981', fontWeight: 700 }}>{c.winRate}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Game Modes */}
|
||||
<section style={{ padding: '56px 48px', background: 'rgba(0,0,0,0.85)' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
<h2 style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 26, fontWeight: 900, color: 'white', letterSpacing: '0.05em', marginBottom: 8 }}>
|
||||
GAME MODES
|
||||
</h2>
|
||||
<div style={{ width: 60, height: 2, background: 'linear-gradient(90deg, #06b6d4, #a855f7)', margin: '0 auto' }} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
{modes.map((mode) => (
|
||||
<div key={mode.id} className="mode-card"
|
||||
// @ts-expect-error CSS variable
|
||||
style={{ '--card-color': mode.color, border: `1px solid ${mode.color}25`, borderRadius: 4, padding: '28px 24px', background: 'rgba(0,0,0,0.7)', cursor: 'pointer', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: `linear-gradient(90deg, transparent, ${mode.color}, transparent)` }} />
|
||||
{/* Mode icon */}
|
||||
<div style={{ width: 44, height: 44, borderRadius: '50%', background: mode.color + '15', border: `1px solid ${mode.color}35`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 16 }}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
{mode.id === 'solo' && <polygon points="10,2 18,18 2,18" fill={mode.color} opacity="0.9" />}
|
||||
{mode.id === 'duo' && <><circle cx="7" cy="10" r="5" fill={mode.color} opacity="0.9" /><circle cx="13" cy="10" r="5" fill={mode.color} opacity="0.5" /></>}
|
||||
{mode.id === 'squad' && <><rect x="2" y="2" width="7" height="7" rx="2" fill={mode.color} opacity="0.9" /><rect x="11" y="2" width="7" height="7" rx="2" fill={mode.color} opacity="0.6" /><rect x="2" y="11" width="7" height="7" rx="2" fill={mode.color} opacity="0.6" /><rect x="11" y="11" width="7" height="7" rx="2" fill={mode.color} opacity="0.4" /></>}
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 22, fontWeight: 900, color: 'white' }}>{mode.name}</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 13, color: mode.color, fontWeight: 700, letterSpacing: '0.1em' }}>{mode.sub}</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 15, color: '#475569', lineHeight: 1.5, marginBottom: 20 }}>{mode.desc}</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 11, color: mode.color, letterSpacing: '0.08em' }}>{mode.players} IN QUEUE</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 10, color: '#1e3a5f', letterSpacing: '0.1em' }}>AVG WAIT: < 30s</div>
|
||||
</div>
|
||||
<button style={{ background: `${mode.color}20`, border: `1px solid ${mode.color}50`, color: mode.color, padding: '7px 18px', borderRadius: 2, fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'Orbitron, sans-serif', letterSpacing: '0.08em' }}>
|
||||
PLAY →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Rankings + Recent Matches */}
|
||||
<section style={{ padding: '56px 48px', background: '#000000' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto', display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 24 }}>
|
||||
{/* Rankings */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h2 style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 20, fontWeight: 900, letterSpacing: '0.05em', color: 'white' }}>
|
||||
GLOBAL RANKING<span style={{ color: '#06b6d4' }}>.</span>
|
||||
</h2>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 11, color: '#1e3a5f', letterSpacing: '0.1em' }}>Season 7 · Top 100</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid rgba(6,182,212,0.12)', borderRadius: 4, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '52px 1fr 88px 60px 60px', padding: '9px 16px', borderBottom: '1px solid rgba(6,182,212,0.1)', background: 'rgba(6,182,212,0.05)' }}>
|
||||
{['RANK', 'PLAYER', 'SCORE', 'WINS', 'K/D/A'].map((h) => (
|
||||
<div key={h} style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 8, color: '#06b6d4', letterSpacing: '0.15em' }}>{h}</div>
|
||||
))}
|
||||
</div>
|
||||
{rankings.map((r, i) => (
|
||||
<div key={i} className="rank-row" style={{ display: 'grid', gridTemplateColumns: '52px 1fr 88px 60px 60px', padding: '13px 16px', borderBottom: i < rankings.length - 1 ? '1px solid rgba(255,255,255,0.03)' : 'none', alignItems: 'center' }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 18, fontWeight: 900, color: i === 0 ? '#fbbf24' : i === 1 ? '#9ca3af' : i === 2 ? '#cd7c2f' : '#1e3a5f' }}>
|
||||
{r.rank < 10 ? `0${r.rank}` : r.rank}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
|
||||
{tierIcon(r.tier)}
|
||||
<span style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 14, fontWeight: 700, color: 'white' }}>{r.name}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 7, color: tierColor[r.tier] || '#6b7280', border: `1px solid ${tierColor[r.tier] || '#6b7280'}40`, padding: '1px 5px', letterSpacing: '0.08em' }}>{r.tier}</span>
|
||||
<span style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: r.change.startsWith('+') ? '#10b981' : r.change === '0' ? '#334155' : '#ef4444' }}>
|
||||
{r.change === '0' ? '–' : r.change}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 13, fontWeight: 700, color: '#06b6d4' }}>{r.score.toLocaleString()}</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 13, color: '#475569', fontWeight: 600 }}>{r.wins}</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 12, color: '#10b981' }}>{r.kda}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Matches */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h2 style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 20, fontWeight: 900, letterSpacing: '0.05em', color: 'white' }}>
|
||||
RECENT MATCHES<span style={{ color: '#a855f7' }}>.</span>
|
||||
</h2>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 11, color: '#1e3a5f', letterSpacing: '0.1em' }}>Last 10 games</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{recentMatches.map((m, i) => (
|
||||
<div key={i} className="match-row" style={{ display: 'grid', gridTemplateColumns: '64px 1fr auto', gap: 12, alignItems: 'center', padding: '14px 16px', border: `1px solid ${m.result === 'WIN' ? 'rgba(16,185,129,0.15)' : 'rgba(239,68,68,0.15)'}`, borderLeft: `3px solid ${m.result === 'WIN' ? '#10b981' : '#ef4444'}`, borderRadius: 4, background: m.result === 'WIN' ? 'rgba(16,185,129,0.04)' : 'rgba(239,68,68,0.04)', cursor: 'default' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 13, fontWeight: 900, color: m.result === 'WIN' ? '#10b981' : '#ef4444' }}>{m.result}</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 8, color: '#334155', letterSpacing: '0.1em', marginTop: 2 }}>{m.mode}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 15, color: 'white', fontWeight: 700 }}>
|
||||
{m.kills} / {m.deaths} / {m.assists}
|
||||
<span style={{ fontSize: 11, color: '#475569', marginLeft: 6 }}>K/D/A</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 9, color: '#334155', marginTop: 2 }}>{m.duration}</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 14, fontWeight: 900, color: m.rating.startsWith('+') ? '#10b981' : '#ef4444', textAlign: 'right' }}>
|
||||
{m.rating}
|
||||
<div style={{ fontSize: 8, color: '#334155', letterSpacing: '0.1em' }}>RATING</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{ background: '#000000', borderTop: '1px solid rgba(6,182,212,0.08)', padding: '24px 48px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ fontFamily: 'Orbitron, sans-serif', fontSize: 14, fontWeight: 900, color: '#1e3a5f', letterSpacing: '0.1em' }}>NEXUS ARENA</div>
|
||||
<div style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 11, color: '#1e293b', letterSpacing: '0.1em' }}>
|
||||
© 2024 NEXUS ARENA STUDIOS. ALL RIGHTS RESERVED.
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
{['Twitter', 'Discord', 'YouTube', 'Twitch'].map((s) => (
|
||||
<span key={s} style={{ fontFamily: 'Rajdhani, sans-serif', fontSize: 12, color: '#1e3a5f', cursor: 'pointer', letterSpacing: '0.05em' }}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
DATA
|
||||
══════════════════════════════════════════════ */
|
||||
const portfolio = [
|
||||
{ title: '한남동 단독주택', cat: '주거 인테리어', area: '245㎡', img: 'https://picsum.photos/seed/interior-hannam/800/600' },
|
||||
{ title: '청담 파인다이닝', cat: '상업 공간', area: '190㎡', img: 'https://picsum.photos/seed/interior-dining/800/600' },
|
||||
{ title: '성수 브랜드 오피스', cat: '업무 공간', area: '380㎡', img: 'https://picsum.photos/seed/interior-office/800/600' },
|
||||
{ title: '용산 아파트 리모델링', cat: '리모델링', area: '95㎡', img: 'https://picsum.photos/seed/interior-remodel/800/600' },
|
||||
{ title: '강남 카페 에스프레소랩', cat: '상업 공간', area: '120㎡', img: 'https://picsum.photos/seed/interior-cafe/800/600' },
|
||||
];
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: '주거 인테리어', sub: 'Residential',
|
||||
desc: '생활의 리듬에 맞춘 공간을 설계합니다. 단독주택부터 아파트까지, 당신의 일상이 더 아름다워지도록 모든 디테일을 손수 고릅니다.',
|
||||
details: ['공간 기획 및 3D 시뮬레이션', '자재 선정 동행 서비스', '시공 전 과정 PM', '준공 후 AS 1년'],
|
||||
img: 'https://picsum.photos/seed/interior-residential/800/600',
|
||||
},
|
||||
{
|
||||
title: '상업 공간 디자인', sub: 'Commercial',
|
||||
desc: '브랜드의 철학이 공간 언어로 번역됩니다. 첫 방문객이 문을 열었을 때 느끼는 그 감정까지 설계의 범위입니다.',
|
||||
details: ['브랜드 아이덴티티 반영', '동선 및 고객 UX 설계', '조명·음향 플래닝', '설비 협력사 연계'],
|
||||
img: 'https://picsum.photos/seed/interior-commercial/800/600',
|
||||
},
|
||||
{
|
||||
title: '리모델링 & 재생', sub: 'Remodeling',
|
||||
desc: '기존 공간의 가능성을 새로운 시선으로 바라봅니다. 구조적 변경부터 마감재 교체까지, 완전한 변신을 지원합니다.',
|
||||
details: ['현장 실측 및 구조 분석', '철거~완공 원스톱', '예산 내 최적 시공', '친환경 자재 우선 적용'],
|
||||
img: 'https://picsum.photos/seed/interior-remodeling/800/600',
|
||||
},
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{ name: '하윤서', role: '한남동 단독주택 의뢰인', u: 'hayunseo', rating: 5, highlight: '계획보다 적은 예산',
|
||||
text: '처음엔 예산이 걱정됐는데, 아우라 팀이 범위를 명확히 정해줘서 오히려 계획보다 적게 들었습니다. 무엇보다 완공된 공간에서 매일 아침 커피 한 잔 하는 지금이 너무 행복해요.' },
|
||||
{ name: '박도현', role: '카페 에스프레소랩 대표', u: 'parkdohyun', rating: 5, highlight: '매출 340% 상승',
|
||||
text: '우리 브랜드 철학을 완벽하게 공간으로 옮겨줬습니다. 오픈 첫날부터 SNS 바이럴이 터졌고, 오픈 3개월 만에 매출이 전년 대비 340% 올랐어요.' },
|
||||
{ name: '이서진', role: '루미너스 COO', u: 'leeseojin', rating: 5, highlight: '직원 만족도 93점',
|
||||
text: '직원들이 출근하고 싶은 공간을 만드는 게 목표였습니다. 리모델링 후 직원 만족도 설문에서 93점, 퇴직률이 절반으로 줄었습니다.' },
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{ num: '01', title: '무료 상담', desc: '공간 사진, 예산, 취향을 공유해 주세요. 72시간 내 맞춤 제안서를 드립니다.' },
|
||||
{ num: '02', title: '콘셉트 기획', desc: '무드보드와 3D 시뮬레이션으로 완공 이후를 미리 경험합니다.' },
|
||||
{ num: '03', title: '시공', desc: '전담 PM이 공정마다 현장을 점검하고 일일 리포트를 공유합니다.' },
|
||||
{ num: '04', title: '준공 & AS', desc: '완공 후 1년간 무상 AS. 공간이 오래 아름답도록 함께합니다.' },
|
||||
];
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
SVG ICONS
|
||||
══════════════════════════════════════════════ */
|
||||
const StarIcon = ({ filled }: { filled: boolean }) => (
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill={filled ? '#8B6914' : 'none'} stroke="#8B6914" strokeWidth="1">
|
||||
<path d="M6.5 1l1.5 3 3.5.5-2.5 2.4.6 3.4L6.5 9 3 10.3l.6-3.4L1 4.9 4.5 4.4z" />
|
||||
</svg>
|
||||
);
|
||||
const CheckIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="6.5" stroke="#4E5C3E" strokeWidth="1" />
|
||||
<path d="M4.5 7l2 2 3-3" stroke="#4E5C3E" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
const ArrowRight = ({ color = '#8B6914' }: { color?: string }) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3 8h10M9 4l4 4-4 4" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
CONSTANTS
|
||||
══════════════════════════════════════════════ */
|
||||
const BANNER_H = 40;
|
||||
const NAV_H = 72;
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
PAGE COMPONENT
|
||||
══════════════════════════════════════════════ */
|
||||
export default function InteriorSample() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const scroller: HTMLElement =
|
||||
(document.querySelector('.main-content') as HTMLElement | null) ??
|
||||
document.documentElement;
|
||||
|
||||
const onNavScroll = () => {
|
||||
setScrolled(scroller.scrollTop > 60);
|
||||
setShowTop(scroller.scrollTop > 400);
|
||||
};
|
||||
scroller.addEventListener('scroll', onNavScroll, { passive: true });
|
||||
|
||||
const scrollToTop = () => scroller.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const topBtn = document.getElementById('au-top-btn');
|
||||
if (topBtn) topBtn.addEventListener('click', scrollToTop);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add('au-visible'); }),
|
||||
{ threshold: 0.08, root: scroller === document.documentElement ? null : scroller }
|
||||
);
|
||||
document.querySelectorAll('.au-reveal').forEach((el) => observer.observe(el));
|
||||
|
||||
return () => {
|
||||
scroller.removeEventListener('scroll', onNavScroll);
|
||||
if (topBtn) topBtn.removeEventListener('click', scrollToTop);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const GOLD = '#8B6914';
|
||||
const DARK = '#1C1A17';
|
||||
const SAGE = '#4E5C3E';
|
||||
const CREAM = '#FAF8F5';
|
||||
const SURFACE = '#F0ECE4';
|
||||
|
||||
return (
|
||||
<div className="au-page" style={{ background: CREAM, color: DARK, fontFamily: "'Pretendard','Apple SD Gothic Neo',system-ui,sans-serif", overflowX: 'hidden' }}>
|
||||
|
||||
{/* ══ 폰트 + 전역 CSS ══ */}
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400;1,600&display=swap');
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.min.css');
|
||||
|
||||
/* 전역 리셋을 .au-page 하위로 한정 — 사이드바 오염 방지 */
|
||||
.au-page, .au-page *, .au-page *::before, .au-page *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
@keyframes au-fadeUp {
|
||||
from { opacity: 0; transform: translateY(2rem); filter: blur(4px); }
|
||||
to { opacity: 1; transform: none; filter: blur(0); }
|
||||
}
|
||||
@keyframes au-float {
|
||||
0%,100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
@keyframes au-marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
@keyframes au-pulse-gold {
|
||||
0%,100% { box-shadow: 0 0 0 0 rgba(139,105,20,.3); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(139,105,20,0); }
|
||||
}
|
||||
|
||||
/* 스크롤 reveal */
|
||||
.au-reveal {
|
||||
opacity: 0; transform: translateY(1.5rem); filter: blur(2px);
|
||||
transition: opacity .7s cubic-bezier(.16,1,.3,1),
|
||||
transform .7s cubic-bezier(.16,1,.3,1),
|
||||
filter .7s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.au-reveal.au-visible { opacity: 1; transform: none; filter: none; }
|
||||
.au-reveal:nth-child(2) { transition-delay: 80ms; }
|
||||
.au-reveal:nth-child(3) { transition-delay: 160ms; }
|
||||
.au-reveal:nth-child(4) { transition-delay: 240ms; }
|
||||
|
||||
/* 네비 */
|
||||
.au-nav-link { color: #6B6456; text-decoration: none; font-size: 14px; font-weight: 500; transition: color .3s; }
|
||||
.au-nav-link:hover { color: #1C1A17; }
|
||||
.au-nav-link-light { color: rgba(245,237,223,.7); text-decoration: none; font-size: 14px; font-weight: 500; transition: color .3s; }
|
||||
.au-nav-link-light:hover { color: #F5EDDF; }
|
||||
|
||||
/* 버튼 */
|
||||
.au-btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 10px;
|
||||
background: #1C1A17; color: #FAF8F5; border: none; border-radius: 100px;
|
||||
padding: 14px 28px 14px 20px; font-size: 15px; font-weight: 600;
|
||||
font-family: inherit; cursor: pointer; text-decoration: none;
|
||||
transition: transform .4s cubic-bezier(.16,1,.3,1), box-shadow .4s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.au-btn-primary:hover { transform: scale(1.02); box-shadow: 0 12px 36px rgba(28,26,23,.25); }
|
||||
.au-btn-primary:active { transform: scale(.98); }
|
||||
|
||||
.au-btn-ghost {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: transparent; color: #1C1A17;
|
||||
border: 1px solid rgba(28,26,23,.2); border-radius: 100px;
|
||||
padding: 13px 24px; font-size: 14px; font-weight: 500;
|
||||
font-family: inherit; cursor: pointer; text-decoration: none;
|
||||
transition: border-color .3s, background .3s;
|
||||
}
|
||||
.au-btn-ghost:hover { border-color: rgba(28,26,23,.5); background: rgba(28,26,23,.04); }
|
||||
|
||||
/* 포트폴리오 */
|
||||
.au-portfolio-cell { overflow: hidden; border-radius: 16px; position: relative; cursor: pointer; }
|
||||
.au-portfolio-img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .7s cubic-bezier(.16,1,.3,1); }
|
||||
.au-portfolio-cell:hover .au-portfolio-img { transform: scale(1.04); }
|
||||
.au-portfolio-overlay {
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(to top, rgba(28,26,23,.7) 0%, transparent 55%);
|
||||
opacity: 0; transition: opacity .4s cubic-bezier(.16,1,.3,1);
|
||||
display: flex; flex-direction: column; justify-content: flex-end; padding: 22px;
|
||||
}
|
||||
.au-portfolio-cell:hover .au-portfolio-overlay { opacity: 1; }
|
||||
|
||||
/* 서비스 카드 */
|
||||
.au-service-card {
|
||||
background: white; border-radius: 20px; overflow: hidden;
|
||||
box-shadow: 0 4px 32px rgba(28,26,23,.06);
|
||||
transition: transform .4s cubic-bezier(.16,1,.3,1), box-shadow .4s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.au-service-card:hover { transform: translateY(-4px); box-shadow: 0 20px 60px rgba(28,26,23,.12); }
|
||||
|
||||
/* Double-Bezel 후기 */
|
||||
.au-testimony { background: rgba(240,236,228,.5); border-radius: 24px; padding: 6px; border: 1px solid rgba(139,105,20,.12); transition: border-color .3s; }
|
||||
.au-testimony:hover { border-color: rgba(139,105,20,.3); }
|
||||
.au-testimony-inner { background: white; border-radius: 18px; padding: 28px 26px; box-shadow: inset 0 1px 1px rgba(255,255,255,.8); height: 100%; }
|
||||
|
||||
/* 프로세스 번호 */
|
||||
.au-step-num { font-family:'Playfair Display',Georgia,serif; font-size:48px; font-weight:700; color:rgba(139,105,20,.15); line-height:1; margin-bottom:12px; }
|
||||
|
||||
/* 히어로 영상 프레임 */
|
||||
.au-video-frame {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 40px 100px rgba(10,8,6,.45), 0 0 0 1px rgba(139,105,20,.2);
|
||||
}
|
||||
.au-video-frame::after {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
border-radius: 20px;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
.au-video-frame video {
|
||||
display: block; width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.au-hero-grid { grid-template-columns: 1fr !important; }
|
||||
.au-hero-video-col { display: none !important; }
|
||||
.au-service-grid { grid-template-columns: 1fr !important; }
|
||||
.au-process-grid { grid-template-columns: 1fr 1fr !important; }
|
||||
.au-portfolio-grid { grid-template-columns: 1fr 1fr !important; }
|
||||
.au-portfolio-span2 { grid-column: span 1 !important; }
|
||||
.au-testimony-grid { grid-template-columns: 1fr 1fr !important; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.au-process-grid { grid-template-columns: 1fr !important; }
|
||||
.au-testimony-grid { grid-template-columns: 1fr !important; }
|
||||
.au-portfolio-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* ══ BACK BANNER ══ */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg,#1e1b4b,#312e81)',
|
||||
height: BANNER_H, display: 'flex', alignItems: 'center',
|
||||
padding: '0 24px', gap: 12, flexShrink: 0,
|
||||
}}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none' }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#fcd34d', fontSize: 12, fontFamily: "'Playfair Display',serif", fontStyle: 'italic', fontWeight: 700 }}>
|
||||
SAMPLE · 인테리어 업체 홈페이지
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ══ NAV ══ */}
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 50, height: NAV_H,
|
||||
display: 'flex', alignItems: 'center', padding: '0 48px',
|
||||
background: scrolled ? 'rgba(250,248,245,.94)' : 'rgba(10,8,6,.55)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
borderBottom: scrolled ? '1px solid rgba(139,105,20,.1)' : '1px solid rgba(255,255,255,.06)',
|
||||
transition: 'background .45s cubic-bezier(.16,1,.3,1), border-color .45s',
|
||||
}}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: "'Playfair Display',Georgia,serif", fontSize: 20, fontWeight: 700, color: scrolled ? DARK : '#F5EDDF', letterSpacing: '-0.01em', transition: 'color .45s' }}>
|
||||
Aura Interior
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: scrolled ? '#A0917C' : 'rgba(245,237,223,.5)', letterSpacing: '0.2em', textTransform: 'uppercase', marginTop: -2, transition: 'color .45s' }}>
|
||||
아우라 인테리어
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 32 }}>
|
||||
{['포트폴리오', '서비스', '프로세스', '고객 후기', '상담 신청'].map((l) => (
|
||||
<a key={l} href={`#${l}`} className={scrolled ? 'au-nav-link' : 'au-nav-link-light'}>{l}</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link href="#contact" className="au-btn-primary"
|
||||
style={{ background: scrolled ? DARK : 'rgba(139,105,20,.9)', fontSize: 13, padding: '10px 20px 10px 16px', transition: 'background .45s, transform .4s, box-shadow .4s' }}>
|
||||
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'rgba(250,248,245,.12)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<ArrowRight color="#FAF8F5" />
|
||||
</span>
|
||||
무료 상담 신청
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ══ HERO — 좌: 텍스트 / 우: 영상 ══ */}
|
||||
<section style={{
|
||||
background: DARK,
|
||||
minHeight: `calc(100dvh - ${BANNER_H}px - ${NAV_H}px)`,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
overflow: 'hidden',
|
||||
}} className="au-hero-grid">
|
||||
|
||||
{/* 좌: 텍스트 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '80px 64px 80px 80px', position: 'relative' }}>
|
||||
{/* 미세 노이즈 */}
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=\'0 0 200 200\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.8\' numOctaves=\'4\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E")', opacity: 0.035, pointerEvents: 'none' }} />
|
||||
|
||||
<div style={{ animation: 'au-fadeUp .9s cubic-bezier(.16,1,.3,1) both', position: 'relative' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: 'rgba(139,105,20,.18)', border: '1px solid rgba(139,105,20,.4)', borderRadius: 100, padding: '5px 14px', marginBottom: 24 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: GOLD }} />
|
||||
<span style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase' }}>서울 기반 인테리어 디자인</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontFamily: "'Playfair Display',Georgia,serif", fontSize: 'clamp(36px,4.5vw,64px)', fontWeight: 700, lineHeight: 1.14, color: '#F5EDDF', letterSpacing: '-0.02em', marginBottom: 20, wordBreak: 'keep-all' }}>
|
||||
공간이 당신의<br />이야기를<br />
|
||||
<em style={{ color: GOLD, fontStyle: 'italic' }}>담습니다.</em>
|
||||
</h1>
|
||||
|
||||
<p style={{ fontSize: 16, color: 'rgba(245,237,223,.58)', lineHeight: 1.85, maxWidth: 400, marginBottom: 36, wordBreak: 'keep-all' }}>
|
||||
아우라 인테리어는 12년간 247개의 공간을 완성했습니다.<br />
|
||||
주거부터 상업 공간까지, 당신의 이야기가 머무는 곳을 만듭니다.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center', marginBottom: 48 }}>
|
||||
<Link href="#contact" className="au-btn-primary" style={{ background: GOLD, color: DARK }}>
|
||||
<span style={{ width: 32, height: 32, borderRadius: '50%', background: 'rgba(28,26,23,.12)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<ArrowRight color={DARK} />
|
||||
</span>
|
||||
무료 공간 상담 시작
|
||||
</Link>
|
||||
<a href="#포트폴리오" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'rgba(245,237,223,.6)', fontSize: 14, textDecoration: 'none', transition: 'color .3s' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = '#F5EDDF')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(245,237,223,.6)')}>
|
||||
포트폴리오 보기 <ArrowRight color="rgba(245,237,223,.6)" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* 미니 통계 */}
|
||||
<div style={{ display: 'flex', gap: 36, paddingTop: 28, borderTop: '1px solid rgba(250,248,245,.08)' }}>
|
||||
{[['247+','완공 프로젝트'],['4.96','고객 만족도'],['12년','디자인 경력']].map(([n, l]) => (
|
||||
<div key={l}>
|
||||
<div style={{ fontFamily: "'Playfair Display',serif", fontSize: 26, fontWeight: 700, color: '#F5EDDF' }}>{n}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(245,237,223,.4)', marginTop: 3 }}>{l}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우: 영상 패널 */}
|
||||
<div className="au-hero-video-col" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 64px 48px 32px', background: 'linear-gradient(135deg,rgba(10,8,6,0) 0%,rgba(139,105,20,.04) 100%)' }}>
|
||||
{/* 배경 글로우 */}
|
||||
<div style={{ position: 'absolute', width: 360, height: 500, borderRadius: '50%', background: 'radial-gradient(ellipse,rgba(139,105,20,.12) 0%,transparent 70%)', pointerEvents: 'none' }} />
|
||||
|
||||
{/* 영상 프레임 — 세로형 영상을 자연스럽게 표시 */}
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* Award 뱃지 */}
|
||||
<div style={{ position: 'absolute', top: -16, right: -20, zIndex: 2, background: 'rgba(250,248,245,.97)', borderRadius: 100, padding: '7px 16px', boxShadow: '0 8px 32px rgba(10,8,6,.3)', border: '1px solid rgba(139,105,20,.15)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: GOLD }} />
|
||||
<span style={{ fontSize: 11, fontFamily: "'Playfair Display',serif", fontStyle: 'italic', fontWeight: 700, color: DARK, whiteSpace: 'nowrap' }}>Award Winner 2024</span>
|
||||
</div>
|
||||
|
||||
{/* 영상 컨테이너: 세로 비율, 적당한 크기 */}
|
||||
<div className="au-video-frame" style={{ width: 'clamp(240px,22vw,320px)', aspectRatio: '9/16' }}>
|
||||
<video autoPlay muted loop playsInline src="/interior-hero.mp4" />
|
||||
</div>
|
||||
|
||||
{/* 플로팅 리뷰 배지 */}
|
||||
<div style={{ position: 'absolute', bottom: -20, left: -28, background: 'white', borderRadius: 14, padding: '14px 18px', boxShadow: '0 20px 60px rgba(10,8,6,.3)', animation: 'au-float 5s ease-in-out infinite', border: '1px solid rgba(139,105,20,.1)', zIndex: 2 }}>
|
||||
<div style={{ display: 'flex', gap: 3, marginBottom: 5 }}>
|
||||
{[1,2,3,4,5].map(i => <StarIcon key={i} filled />)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: DARK }}>최근 완공 · 한남동</div>
|
||||
<div style={{ fontSize: 11, color: '#A0917C', marginTop: 2 }}>만족도 5.0 / 5.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ PORTFOLIO BENTO ══ */}
|
||||
<section id="포트폴리오" style={{ padding: '100px 80px', background: SURFACE }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<div className="au-reveal" style={{ marginBottom: 56, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', marginBottom: 12 }}>Portfolio</div>
|
||||
<h2 style={{ fontFamily: "'Playfair Display',serif", fontSize: 'clamp(32px,3.5vw,48px)', fontWeight: 700, color: DARK, letterSpacing: '-0.02em', lineHeight: 1.2, wordBreak: 'keep-all' }}>
|
||||
공간이 말하는<br />우리의 언어
|
||||
</h2>
|
||||
</div>
|
||||
<a href="#" className="au-btn-ghost" style={{ flexShrink: 0 }}>전체 보기 <ArrowRight /></a>
|
||||
</div>
|
||||
|
||||
<div className="au-reveal au-portfolio-grid" style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr 1fr', gridTemplateRows: 'auto auto', gap: 14 }}>
|
||||
<div className="au-portfolio-cell" style={{ gridRow: 'span 2', minHeight: 580 }}>
|
||||
<img src={portfolio[0].img} alt={portfolio[0].title} className="au-portfolio-img" />
|
||||
<div className="au-portfolio-overlay">
|
||||
<div style={{ fontSize: 11, color: 'rgba(250,248,245,.6)', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: 4 }}>{portfolio[0].cat}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'white', fontFamily: "'Playfair Display',serif" }}>{portfolio[0].title}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(250,248,245,.7)', marginTop: 4 }}>{portfolio[0].area}</div>
|
||||
</div>
|
||||
</div>
|
||||
{[1, 2].map((idx) => (
|
||||
<div key={idx} className="au-portfolio-cell" style={{ minHeight: 280 }}>
|
||||
<img src={portfolio[idx].img} alt={portfolio[idx].title} className="au-portfolio-img" />
|
||||
<div className="au-portfolio-overlay">
|
||||
<div style={{ fontSize: 11, color: 'rgba(250,248,245,.6)', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: 4 }}>{portfolio[idx].cat}</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: 'white', fontFamily: "'Playfair Display',serif" }}>{portfolio[idx].title}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="au-portfolio-cell au-portfolio-span2" style={{ gridColumn: 'span 2', minHeight: 280 }}>
|
||||
<img src={portfolio[4].img} alt={portfolio[4].title} className="au-portfolio-img" />
|
||||
<div className="au-portfolio-overlay">
|
||||
<div style={{ fontSize: 11, color: 'rgba(250,248,245,.6)', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: 4 }}>{portfolio[4].cat}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'white', fontFamily: "'Playfair Display',serif" }}>{portfolio[4].title}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(250,248,245,.7)', marginTop: 4 }}>{portfolio[4].area}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ SERVICES ZIG-ZAG ══ */}
|
||||
<section id="서비스" style={{ padding: '100px 80px', background: CREAM }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<div className="au-reveal" style={{ textAlign: 'center', marginBottom: 72 }}>
|
||||
<div style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', marginBottom: 12 }}>Services</div>
|
||||
<h2 style={{ fontFamily: "'Playfair Display',serif", fontSize: 'clamp(32px,3.5vw,48px)', fontWeight: 700, color: DARK, letterSpacing: '-0.02em', wordBreak: 'keep-all' }}>
|
||||
우리가 잘하는 세 가지
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 48 }}>
|
||||
{services.map((svc, i) => (
|
||||
<div key={svc.title} className="au-reveal au-service-card au-service-grid" style={{ display: 'grid', gridTemplateColumns: i % 2 === 0 ? '1fr 1.2fr' : '1.2fr 1fr', gap: 0 }}>
|
||||
<div style={{ order: i % 2 === 0 ? 2 : 1, position: 'relative', minHeight: 400, overflow: 'hidden', borderRadius: i % 2 === 0 ? '0 20px 20px 0' : '20px 0 0 20px' }}>
|
||||
<img src={svc.img} alt={svc.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform .7s cubic-bezier(.16,1,.3,1)' }} loading="lazy" />
|
||||
</div>
|
||||
<div style={{ order: i % 2 === 0 ? 1 : 2, padding: '52px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 11, color: SAGE, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: 14 }}>{svc.sub}</div>
|
||||
<h3 style={{ fontFamily: "'Playfair Display',serif", fontSize: 32, fontWeight: 700, color: DARK, letterSpacing: '-0.02em', marginBottom: 18, lineHeight: 1.2, wordBreak: 'keep-all' }}>{svc.title}</h3>
|
||||
<p style={{ fontSize: 15, color: '#6B6456', lineHeight: 1.85, marginBottom: 28, wordBreak: 'keep-all' }}>{svc.desc}</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 36 }}>
|
||||
{svc.details.map((d) => (
|
||||
<div key={d} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<CheckIcon /><span style={{ fontSize: 14, color: '#5A5148' }}>{d}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a href="#contact" className="au-btn-ghost" style={{ alignSelf: 'flex-start' }}>
|
||||
상담 신청하기 <ArrowRight />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ PROCESS (dark) ══ */}
|
||||
<section id="프로세스" style={{ padding: '100px 80px', background: DARK }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<div className="au-reveal" style={{ textAlign: 'center', marginBottom: 72 }}>
|
||||
<div style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', marginBottom: 12 }}>Process</div>
|
||||
<h2 style={{ fontFamily: "'Playfair Display',serif", fontSize: 'clamp(32px,3.5vw,48px)', fontWeight: 700, color: '#F5EDDF', letterSpacing: '-0.02em', wordBreak: 'keep-all' }}>
|
||||
상담부터 준공까지
|
||||
</h2>
|
||||
</div>
|
||||
<div className="au-process-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 2 }}>
|
||||
{steps.map((s, i) => (
|
||||
<div key={s.num} className="au-reveal" style={{ padding: '40px 32px', borderLeft: i > 0 ? '1px solid rgba(250,248,245,.06)' : 'none' }}>
|
||||
<div className="au-step-num">{s.num}</div>
|
||||
<div style={{ fontFamily: "'Playfair Display',serif", fontSize: 20, fontWeight: 600, color: '#F5EDDF', marginBottom: 12, lineHeight: 1.3 }}>{s.title}</div>
|
||||
<p style={{ fontSize: 14, color: '#8A7E70', lineHeight: 1.8, wordBreak: 'keep-all' }}>{s.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ TESTIMONIALS MASONRY ══ */}
|
||||
<section id="고객 후기" style={{ padding: '100px 80px', background: SURFACE }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div className="au-reveal" style={{ textAlign: 'center', marginBottom: 64 }}>
|
||||
<div style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', marginBottom: 12 }}>Reviews</div>
|
||||
<h2 style={{ fontFamily: "'Playfair Display',serif", fontSize: 'clamp(32px,3.5vw,48px)', fontWeight: 700, color: DARK, letterSpacing: '-0.02em', wordBreak: 'keep-all' }}>
|
||||
공간이 바꾼 이야기들
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="au-testimony-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, alignItems: 'start' }}>
|
||||
{testimonials.map((t, i) => (
|
||||
<div key={t.name} className="au-reveal au-testimony" style={{ marginTop: i === 1 ? 40 : 0 }}>
|
||||
<div className="au-testimony-inner">
|
||||
<div style={{ display: 'inline-block', background: 'rgba(139,105,20,.08)', borderRadius: 100, padding: '4px 12px', marginBottom: 18, border: '1px solid rgba(139,105,20,.15)' }}>
|
||||
<span style={{ fontSize: 11, color: GOLD, fontWeight: 700 }}>{t.highlight}</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: "'Playfair Display',serif", fontSize: 48, color: 'rgba(139,105,20,.15)', lineHeight: 0.8, marginBottom: 14 }}>"</div>
|
||||
<p style={{ fontSize: 15, color: '#4A4440', lineHeight: 1.85, marginBottom: 24, wordBreak: 'keep-all' }}>{t.text}</p>
|
||||
<div style={{ display: 'flex', gap: 3, marginBottom: 16 }}>
|
||||
{[1,2,3,4,5].map(j => <StarIcon key={j} filled={j <= t.rating} />)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingTop: 16, borderTop: '1px solid rgba(139,105,20,.08)' }}>
|
||||
<img src={`https://i.pravatar.cc/80?u=${t.u}`} alt={t.name} style={{ width: 40, height: 40, borderRadius: '50%', objectFit: 'cover' }} loading="lazy" />
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: DARK }}>{t.name}</div>
|
||||
<div style={{ fontSize: 12, color: '#A0917C' }}>{t.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="au-reveal" style={{ marginTop: 72, overflow: 'hidden', borderTop: '1px solid rgba(139,105,20,.1)', paddingTop: 40 }}>
|
||||
<div style={{ fontSize: 11, color: '#A0917C', textAlign: 'center', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: 24 }}>함께한 브랜드들</div>
|
||||
<div style={{ display: 'flex', animation: 'au-marquee 22s linear infinite', width: 'fit-content', gap: 64 }}>
|
||||
{['에스프레소랩','루미너스','플로우캔버스','스텔라랩스','넥스트비전','브릿지웍스',
|
||||
'에스프레소랩','루미너스','플로우캔버스','스텔라랩스','넥스트비전','브릿지웍스'].map((b, i) => (
|
||||
<span key={i} style={{ fontFamily: "'Playfair Display',serif", fontSize: 18, fontWeight: 600, color: 'rgba(28,26,23,.18)', whiteSpace: 'nowrap', fontStyle: 'italic' }}>{b}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ CTA ══ */}
|
||||
<section id="contact" style={{ padding: '120px 80px', background: DARK, position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', right: -100, top: '50%', transform: 'translateY(-50%)', width: 600, height: 600, borderRadius: '50%', background: 'radial-gradient(circle,rgba(139,105,20,.1),transparent 70%)', pointerEvents: 'none' }} />
|
||||
|
||||
<div style={{ maxWidth: 720, margin: '0 auto', textAlign: 'center', position: 'relative', zIndex: 1 }}>
|
||||
<div className="au-reveal" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: 'rgba(139,105,20,.12)', border: '1px solid rgba(139,105,20,.25)', borderRadius: 100, padding: '5px 14px', marginBottom: 32 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: GOLD }} />
|
||||
<span style={{ fontSize: 11, color: GOLD, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase' }}>72시간 내 제안서 발송</span>
|
||||
</div>
|
||||
|
||||
<h2 className="au-reveal" style={{ fontFamily: "'Playfair Display',serif", fontSize: 'clamp(36px,5vw,56px)', fontWeight: 700, color: '#F5EDDF', letterSpacing: '-0.02em', lineHeight: 1.2, marginBottom: 20, wordBreak: 'keep-all' }}>
|
||||
당신의 공간 이야기를<br />
|
||||
<em style={{ color: GOLD }}>지금 시작하세요.</em>
|
||||
</h2>
|
||||
|
||||
<p className="au-reveal" style={{ fontSize: 16, color: '#8A7E70', lineHeight: 1.85, marginBottom: 44, wordBreak: 'keep-all' }}>
|
||||
사진 한 장과 예산만 알려주세요.<br />
|
||||
72시간 내에 맞춤 제안서와 무드보드를 드립니다. 무료입니다.
|
||||
</p>
|
||||
|
||||
<div className="au-reveal" style={{ display: 'flex', gap: 14, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<Link href="/freelance?service=website" className="au-btn-primary" style={{ background: GOLD, color: DARK, fontSize: 16, padding: '16px 36px 16px 28px', animation: 'au-pulse-gold 3s infinite' }}>
|
||||
<span style={{ width: 34, height: 34, borderRadius: '50%', background: 'rgba(28,26,23,.15)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<ArrowRight color={DARK} />
|
||||
</span>
|
||||
무료 상담 신청하기
|
||||
</Link>
|
||||
<a href="tel:02-1234-5678" className="au-btn-ghost" style={{ color: '#F5EDDF', borderColor: 'rgba(245,237,223,.15)' }}>
|
||||
02-1234-5678
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="au-reveal" style={{ display: 'flex', gap: 24, justifyContent: 'center', marginTop: 40 }}>
|
||||
{['완공 보장','계약서 필수','AS 1년'].map((b) => (
|
||||
<div key={b} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<CheckIcon /><span style={{ fontSize: 13, color: '#8A7E70' }}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ FOOTER ══ */}
|
||||
<footer style={{ background: '#100E0B', padding: '40px 80px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: "'Playfair Display',serif", fontSize: 16, fontWeight: 700, color: '#F5EDDF', fontStyle: 'italic' }}>Aura Interior</div>
|
||||
<div style={{ fontSize: 12, color: '#4A4035', marginTop: 4 }}>© 2024 아우라 인테리어. All rights reserved.</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{['포트폴리오','서비스 안내','상담 신청','Instagram'].map((l) => (
|
||||
<a key={l} href="#" style={{ fontSize: 13, color: '#4A4035', textDecoration: 'none', transition: 'color .3s' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = '#A0917C')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = '#4A4035')}>{l}</a>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* ══ SCROLL TO TOP ══ */}
|
||||
<button
|
||||
id="au-top-btn"
|
||||
style={{
|
||||
position: 'fixed', bottom: '5.5rem', right: '2rem', zIndex: 200,
|
||||
width: 48, height: 48, borderRadius: '50%',
|
||||
background: GOLD, border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(139,105,20,0.35)',
|
||||
opacity: showTop ? 1 : 0,
|
||||
transform: showTop ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)',
|
||||
transition: 'opacity 0.35s cubic-bezier(0.16,1,0.3,1), transform 0.35s cubic-bezier(0.16,1,0.3,1)',
|
||||
pointerEvents: showTop ? 'auto' : 'none',
|
||||
}}
|
||||
aria-label="맨 위로"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FAF8F5" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
const awards = [
|
||||
{ name: 'Awwwards SOTD', count: '× 2', color: '#f59e0b' },
|
||||
{ name: 'CSS Design Awards', count: 'Winner', color: '#06b6d4' },
|
||||
{ name: 'Adobe Design Award', count: '은상', color: '#ec4899' },
|
||||
{ name: 'Google UX Cert.', count: '2023', color: '#34d399' },
|
||||
];
|
||||
|
||||
const services = [
|
||||
{ title: 'UI/UX Design', desc: '사용자 중심의 인터페이스 설계와 프로토타입', icon: 'M', color: '#00ff88' },
|
||||
{ title: 'Creative Development', desc: 'Three.js · WebGL · 몰입형 웹 경험 구현', icon: 'C', color: '#a855f7' },
|
||||
{ title: 'Brand Identity', desc: '로고부터 브랜드 시스템 전체 디자인', icon: 'B', color: '#06b6d4' },
|
||||
{ title: 'Motion Design', desc: 'After Effects · Lottie 모션 에셋 제작', icon: 'M', color: '#f59e0b' },
|
||||
];
|
||||
|
||||
const projects = [
|
||||
{ title: 'NEON CITY UI', desc: '사이버펑크 게임 인터페이스 디자인. 넥슨 신작 게임의 전체 UI 시스템 구축', gradient: 'linear-gradient(135deg, #00ff88, #00d4ff)', tag: 'UI/UX', year: '2024', featured: true },
|
||||
{ title: 'FLOW BRAND', desc: '핀테크 스타트업 브랜딩 — 시리즈 B 투자유치 직전 리브랜딩', gradient: 'linear-gradient(135deg, #a855f7, #ec4899)', tag: 'Branding', year: '2024', featured: false },
|
||||
{ title: 'ORBITAL APP', desc: '위성 추적 실시간 대시보드 — NASA 오픈 API 활용', gradient: 'linear-gradient(135deg, #3b82f6, #06b6d4)', tag: 'Web App', year: '2023', featured: false },
|
||||
{ title: 'TERRA SHOP', desc: '친환경 D2C 쇼핑몰 — 전환율 340% 개선 달성', gradient: 'linear-gradient(135deg, #22c55e, #84cc16)', tag: 'E-commerce', year: '2023', featured: false },
|
||||
{ title: 'PULSE MOTION', desc: '글로벌 광고 에이전시를 위한 모션 그래픽 패키지', gradient: 'linear-gradient(135deg, #f59e0b, #ef4444)', tag: 'Motion', year: '2023', featured: false },
|
||||
{ title: 'AXIS SYSTEM', desc: '스타트업 물류 관리 SaaS — 현재 MAU 12만', gradient: 'linear-gradient(135deg, #64748b, #475569)', tag: 'Dashboard', year: '2022', featured: false },
|
||||
];
|
||||
|
||||
const skills = [
|
||||
{ name: 'Figma', level: 98, category: 'Design' },
|
||||
{ name: 'Framer', level: 92, category: 'Design' },
|
||||
{ name: 'After Effects', level: 88, category: 'Motion' },
|
||||
{ name: 'React', level: 90, category: 'Code' },
|
||||
{ name: 'TypeScript', level: 85, category: 'Code' },
|
||||
{ name: 'Three.js', level: 75, category: 'Code' },
|
||||
];
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{ client: 'Joon Park', role: 'CEO, FlowTech', text: 'Jisu가 리디자인한 결과물로 투자 피칭에서 VC들의 반응이 완전히 달라졌습니다. 결과적으로 시리즈 B 클로징에 큰 역할을 했어요.' },
|
||||
{ client: 'Yuna Kim', role: 'Product Lead, Orbital', text: '기술적인 복잡함을 이렇게 아름다운 인터페이스로 풀어낼 수 있다는 게 놀라웠습니다. 유저 피드백 NPS가 34점 올랐습니다.' },
|
||||
];
|
||||
|
||||
export default function PortfolioSample() {
|
||||
const [hoveredProject, setHoveredProject] = useState<number | null>(null);
|
||||
const [activeCategory, setActiveCategory] = useState<string>('All');
|
||||
|
||||
const categories = ['All', 'UI/UX', 'Branding', 'Web App', 'Motion'];
|
||||
const filteredProjects = activeCategory === 'All' ? projects : projects.filter(p => p.tag === activeCategory);
|
||||
|
||||
return (
|
||||
<div style={{ background: '#000000', minHeight: '100vh', color: 'white' }}>
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap');
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||
@keyframes scanline { 0% { top: -10%; } 100% { top: 110%; } }
|
||||
@keyframes glow { 0%, 100% { text-shadow: 0 0 20px rgba(0,255,136,0.5); } 50% { text-shadow: 0 0 40px rgba(0,255,136,0.9), 0 0 80px rgba(0,255,136,0.3); } }
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideBar { from { width: 0; } to { width: 100%; } }
|
||||
@keyframes marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }
|
||||
@keyframes pulse-green { 0%, 100% { box-shadow: 0 0 0 0 rgba(0,255,136,0.4); } 50% { box-shadow: 0 0 0 6px rgba(0,255,136,0); } }
|
||||
.proj-card { transition: border-color 0.3s, transform 0.3s; }
|
||||
.proj-card:hover { border-color: rgba(0,255,136,0.3) !important; transform: translateY(-4px); }
|
||||
.service-card { transition: background 0.3s, border-color 0.3s; }
|
||||
.service-card:hover { background: rgba(255,255,255,0.04) !important; }
|
||||
.cat-btn { transition: background 0.2s, color 0.2s; cursor: pointer; }
|
||||
.cat-btn:hover { color: white !important; }
|
||||
.award-badge { transition: transform 0.2s; }
|
||||
.award-badge:hover { transform: translateY(-2px); }
|
||||
`}</style>
|
||||
|
||||
{/* Back Banner */}
|
||||
<div style={{ background: 'linear-gradient(135deg, #1e1b4b, #312e81)', padding: '10px 24px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Link href="/services/website" style={{ color: '#a5b4fc', fontSize: 13, textDecoration: 'none', fontFamily: 'Space Grotesk, sans-serif' }}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: '#4c1d95' }}>|</span>
|
||||
<span style={{ color: '#00ff88', fontSize: 12, fontFamily: 'Space Mono, monospace', fontWeight: 700 }}>
|
||||
SAMPLE · 개인 포트폴리오
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Awards Marquee */}
|
||||
<div style={{ background: '#050505', borderBottom: '1px solid rgba(0,255,136,0.12)', padding: '10px 0', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', gap: 48, animation: 'marquee 18s linear infinite', width: 'fit-content' }}>
|
||||
{[...awards, ...awards].map((a, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M6 1l1.3 2.6L10 4.1 8 6.1l.5 2.9L6 7.6 3.5 9l.5-2.9-2-2 2.7-.5z" fill={a.color} /></svg>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 11, color: a.color, fontWeight: 700, letterSpacing: '0.1em', whiteSpace: 'nowrap' }}>{a.name}</span>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: '#374151' }}>{a.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 100,
|
||||
background: 'rgba(0,0,0,0.92)', backdropFilter: 'blur(20px)',
|
||||
borderBottom: '1px solid rgba(0,255,136,0.1)',
|
||||
padding: '0 48px', height: 64,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 15, fontWeight: 700, color: '#00ff88', letterSpacing: '-0.02em', animation: 'glow 3s ease-in-out infinite' }}>
|
||||
KJ<span style={{ color: 'rgba(0,255,136,0.35)' }}>_</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 32 }}>
|
||||
{['About', 'Work', 'Skills', 'Contact'].map((item) => (
|
||||
<span key={item} style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 13, fontWeight: 600, color: '#374151', cursor: 'pointer', letterSpacing: '0.05em', textTransform: 'uppercase' }}>{item}</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginRight: 8 }}>
|
||||
<div style={{ width: 7, height: 7, borderRadius: '50%', background: '#00ff88', animation: 'pulse-green 2s infinite' }} />
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 11, color: '#00ff88' }}>Available for work</span>
|
||||
</div>
|
||||
<button style={{
|
||||
background: 'transparent', border: '1px solid #00ff88', color: '#00ff88',
|
||||
padding: '8px 20px', borderRadius: 4, fontSize: 12, fontWeight: 700,
|
||||
cursor: 'pointer', fontFamily: 'Space Mono, monospace', letterSpacing: '0.08em',
|
||||
}}>
|
||||
HIRE ME
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section style={{ padding: '100px 48px 80px', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, rgba(0,255,136,0.4), transparent)', animation: 'scanline 8s linear infinite', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(0,255,136,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,255,136,0.04) 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
|
||||
|
||||
<div style={{ maxWidth: 1000, display: 'grid', gridTemplateColumns: '1fr auto', gap: 48, alignItems: 'center', position: 'relative', animation: 'fadeUp 0.8s ease forwards' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 12, color: 'rgba(0,255,136,0.6)', letterSpacing: '0.2em', marginBottom: 20, textTransform: 'uppercase' }}>
|
||||
{'> Hello, World. I am'}
|
||||
</div>
|
||||
<h1 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 'clamp(52px, 7vw, 88px)', fontWeight: 700, lineHeight: 1.0, marginBottom: 16, letterSpacing: '-0.03em' }}>
|
||||
Kim<br />
|
||||
<span style={{ color: '#00ff88' }}>Jisu</span>
|
||||
<span style={{ display: 'inline-block', width: 6, height: 'clamp(52px, 7vw, 88px)', background: '#00ff88', marginLeft: 8, verticalAlign: 'middle', animation: 'blink 1.2s step-end infinite' }} />
|
||||
</h1>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 20, color: '#4b5563', fontWeight: 500, marginBottom: 20 }}>
|
||||
Product Designer & Creative Developer
|
||||
</div>
|
||||
<p style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 16, color: '#6b7280', lineHeight: 1.8, maxWidth: 520, marginBottom: 36 }}>
|
||||
픽셀 하나하나에 의미를 담는 디자이너. 아름다움과 기능의 교차점에서 디지털 경험을 설계합니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<button style={{
|
||||
background: '#00ff88', color: '#000000', border: 'none', padding: '14px 32px',
|
||||
fontSize: 14, fontWeight: 700, cursor: 'pointer', fontFamily: 'Space Mono, monospace',
|
||||
letterSpacing: '0.05em', clipPath: 'polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px))',
|
||||
}}>
|
||||
VIEW WORK →
|
||||
</button>
|
||||
<button style={{
|
||||
background: 'transparent', color: '#9ca3af', border: '1px solid #1f2937',
|
||||
padding: '14px 32px', fontSize: 14, fontWeight: 600, cursor: 'pointer',
|
||||
fontFamily: 'Space Grotesk, sans-serif', borderRadius: 4,
|
||||
}}>
|
||||
Download CV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats panel */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[
|
||||
{ val: '6+', label: 'YEARS EXP.', color: '#00ff88' },
|
||||
{ val: '30+', label: 'PROJECTS', color: '#a855f7' },
|
||||
{ val: '2×', label: 'AWWWARDS', color: '#f59e0b' },
|
||||
{ val: '340%', label: 'MAX CVR LIFT', color: '#06b6d4' },
|
||||
].map((s) => (
|
||||
<div key={s.label} style={{ padding: '14px 18px', border: '1px solid #111827', borderLeft: `3px solid ${s.color}`, borderRadius: 4, background: '#050505', minWidth: 140 }}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 24, fontWeight: 700, color: s.color }}>{s.val}</div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 9, color: '#374151', letterSpacing: '0.15em', marginTop: 2 }}>{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services */}
|
||||
<section style={{ padding: '72px 48px', background: '#050505', borderTop: '1px solid rgba(0,255,136,0.08)' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 10 }}>// WHAT I DO</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 32, fontWeight: 700, letterSpacing: '-0.02em' }}>
|
||||
Services<span style={{ color: '#00ff88' }}>.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
|
||||
{services.map((svc) => (
|
||||
<div key={svc.title} className="service-card" style={{
|
||||
padding: '24px 20px', border: '1px solid #111827', borderRadius: 8,
|
||||
background: 'transparent', cursor: 'pointer', position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: 3, height: '100%', background: svc.color, opacity: 0.6 }} />
|
||||
<div style={{ width: 36, height: 36, borderRadius: 8, background: svc.color + '15', border: `1px solid ${svc.color}30`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 16 }}>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 12, fontWeight: 700, color: svc.color }}>{svc.icon}</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 15, fontWeight: 700, color: 'white', marginBottom: 8 }}>{svc.title}</div>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 13, color: '#4b5563', lineHeight: 1.6 }}>{svc.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Awards Detail */}
|
||||
<section style={{ padding: '60px 48px', background: '#000000', borderTop: '1px solid rgba(0,255,136,0.08)' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
{awards.map((a) => (
|
||||
<div key={a.name} className="award-badge" style={{
|
||||
padding: '12px 20px', border: `1px solid ${a.color}30`, borderRadius: 8,
|
||||
background: a.color + '08', display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
|
||||
}}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M7 1l1.5 3 3.5.5-2.5 2.4.6 3.4L7 8.8 3.9 10.3l.6-3.4L2 4.5 5.5 4z" fill={a.color} /></svg>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 13, fontWeight: 700, color: 'white' }}>{a.name}</div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: a.color }}>{a.count}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Projects */}
|
||||
<section style={{ padding: '80px 48px', background: '#050505', borderTop: '1px solid rgba(0,255,136,0.08)' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 8 }}>// SELECTED WORK</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 36, fontWeight: 700, letterSpacing: '-0.02em' }}>
|
||||
Projects<span style={{ color: '#00ff88' }}>.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<span style={{ color: '#374151', fontSize: 12, fontFamily: 'Space Mono, monospace' }}>2019 — 2024</span>
|
||||
</div>
|
||||
{/* Category filter */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 32 }}>
|
||||
{categories.map((cat) => (
|
||||
<button key={cat} className="cat-btn" onClick={() => setActiveCategory(cat)} style={{
|
||||
background: activeCategory === cat ? '#00ff88' : 'transparent',
|
||||
color: activeCategory === cat ? '#000' : '#374151',
|
||||
border: '1px solid ' + (activeCategory === cat ? '#00ff88' : '#1f2937'),
|
||||
padding: '6px 16px', borderRadius: 4, fontSize: 11,
|
||||
fontWeight: 700, fontFamily: 'Space Mono, monospace', letterSpacing: '0.06em',
|
||||
}}>{cat}</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 16 }}>
|
||||
{filteredProjects.map((proj, i) => (
|
||||
<div
|
||||
key={proj.title}
|
||||
className="proj-card"
|
||||
onMouseEnter={() => setHoveredProject(i)}
|
||||
onMouseLeave={() => setHoveredProject(null)}
|
||||
style={{ border: '1px solid #111827', borderRadius: 12, overflow: 'hidden', cursor: 'pointer', background: '#0a0a0a', position: 'relative' }}
|
||||
>
|
||||
<div style={{ height: 200, background: proj.gradient, position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 22, fontWeight: 700, color: 'rgba(0,0,0,0.35)', letterSpacing: '-0.02em' }}>{proj.title}</div>
|
||||
</div>
|
||||
{/* Hover overlay */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
|
||||
transition: 'opacity 0.3s', opacity: hoveredProject === i ? 1 : 0,
|
||||
}}>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 14, color: 'white', fontWeight: 600, textAlign: 'center', padding: '0 20px', lineHeight: 1.5 }}>{proj.desc}</div>
|
||||
<button style={{ background: '#00ff88', color: '#000', border: 'none', padding: '8px 20px', borderRadius: 4, fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'Space Mono, monospace', letterSpacing: '0.06em' }}>
|
||||
VIEW PROJECT →
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 12, left: 12, background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(8px)', border: '1px solid rgba(255,255,255,0.15)', borderRadius: 4, padding: '3px 10px', fontSize: 10, fontWeight: 700, color: 'white', fontFamily: 'Space Mono, monospace', letterSpacing: '0.1em' }}>
|
||||
{proj.tag}
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 12, right: 12, fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(255,255,255,0.4)' }}>
|
||||
{proj.year}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 18px' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'white', fontFamily: 'Space Grotesk, sans-serif', marginBottom: 4 }}>{proj.title}</div>
|
||||
<div style={{ fontSize: 13, color: '#4b5563', fontFamily: 'Space Grotesk, sans-serif', lineHeight: 1.5 }}>{proj.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Skills + Timeline */}
|
||||
<section style={{ padding: '80px 48px', background: '#000000', borderTop: '1px solid rgba(0,255,136,0.08)' }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 64, alignItems: 'start' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 10 }}>// EXPERTISE</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 32, fontWeight: 700, marginBottom: 36, letterSpacing: '-0.02em' }}>
|
||||
Skills<span style={{ color: '#00ff88' }}>.</span>
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
||||
{skills.map((s) => (
|
||||
<div key={s.name}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 7 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 12, color: '#9ca3af', letterSpacing: '0.05em' }}>{s.name}</span>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 9, color: '#1f2937', letterSpacing: '0.1em', border: '1px solid #1f2937', padding: '1px 6px', borderRadius: 2 }}>{s.category}</span>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 11, color: '#00ff88' }}>{s.level}%</span>
|
||||
</div>
|
||||
<div style={{ height: 3, background: '#111827', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', borderRadius: 2, background: 'linear-gradient(90deg, #00ff88, #00d4ff)', width: `${s.level}%`, animation: 'slideBar 1.5s ease forwards' }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 10 }}>// JOURNEY</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 32, fontWeight: 700, marginBottom: 36, letterSpacing: '-0.02em' }}>
|
||||
Timeline<span style={{ color: '#00ff88' }}>.</span>
|
||||
</h2>
|
||||
<div style={{ position: 'relative', paddingLeft: 20 }}>
|
||||
<div style={{ position: 'absolute', left: 4, top: 8, bottom: 8, width: 1, background: '#1f2937' }} />
|
||||
{timeline.map((t, i) => (
|
||||
<div key={i} style={{ position: 'relative', paddingBottom: 24 }}>
|
||||
<div style={{
|
||||
position: 'absolute', left: -20, top: 4, width: 8, height: 8, borderRadius: '50%',
|
||||
background: t.type === 'award' ? '#00ff88' : t.type === 'career' ? '#3b82f6' : '#a855f7',
|
||||
boxShadow: `0 0 8px ${t.type === 'award' ? 'rgba(0,255,136,0.6)' : t.type === 'career' ? 'rgba(59,130,246,0.6)' : 'rgba(168,85,247,0.6)'}`,
|
||||
}} />
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 11, color: '#374151', marginBottom: 3 }}>{t.year}</div>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 14, color: '#d1d5db', lineHeight: 1.5 }}>{t.event}</div>
|
||||
{t.type === 'award' && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<span style={{ fontFamily: 'Space Mono, monospace', fontSize: 9, color: '#00ff88', border: '1px solid rgba(0,255,136,0.3)', padding: '1px 6px', borderRadius: 2 }}>AWARD</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section style={{ padding: '72px 48px', background: '#050505', borderTop: '1px solid rgba(0,255,136,0.08)' }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 10 }}>// CLIENT VOICES</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 32, fontWeight: 700, marginBottom: 36, letterSpacing: '-0.02em' }}>
|
||||
Testimonials<span style={{ color: '#00ff88' }}>.</span>
|
||||
</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||
{testimonials.map((t) => (
|
||||
<div key={t.client} style={{ padding: '28px 24px', border: '1px solid #111827', borderRadius: 8, background: '#0a0a0a', position: 'relative' }}>
|
||||
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" style={{ marginBottom: 16, opacity: 0.25 }}>
|
||||
<path d="M0 20V12C0 5.3 4 1.5 12 0l1.7 3C9.5 4.3 7.3 6.7 7 10h5.3V20H0zm14.7 0V12C14.7 5.3 18.7 1.5 26.7 0l1.3 3c-4.2 1.3-6.4 3.7-6.7 7H27V20H14.7z" fill="#00ff88" />
|
||||
</svg>
|
||||
<p style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 14, color: '#9ca3af', lineHeight: 1.8, marginBottom: 20 }}>{t.text}</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 36, height: 36, borderRadius: '50%', background: 'linear-gradient(135deg, #00ff88, #00d4ff)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, color: '#000', fontFamily: 'Space Mono, monospace' }}>
|
||||
{t.client[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 14, fontWeight: 600, color: 'white' }}>{t.client}</div>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 10, color: '#374151' }}>{t.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact */}
|
||||
<section style={{ padding: '96px 48px', textAlign: 'center', background: 'linear-gradient(180deg, #000000, #001a00)', borderTop: '1px solid rgba(0,255,136,0.1)' }}>
|
||||
<div style={{ fontFamily: 'Space Mono, monospace', fontSize: 12, color: 'rgba(0,255,136,0.5)', letterSpacing: '0.2em', marginBottom: 20 }}>
|
||||
{'> LET\'S COLLABORATE'}
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 'clamp(36px, 5vw, 60px)', fontWeight: 700, letterSpacing: '-0.03em', marginBottom: 16, animation: 'glow 3s ease-in-out infinite' }}>
|
||||
Have a project<span style={{ color: '#00ff88' }}>?</span>
|
||||
</h2>
|
||||
<p style={{ fontFamily: 'Space Grotesk, sans-serif', fontSize: 16, color: '#4b5563', marginBottom: 8 }}>
|
||||
jisu.kim@design.studio
|
||||
</p>
|
||||
<p style={{ fontFamily: 'Space Mono, monospace', fontSize: 12, color: '#1f2937', marginBottom: 36 }}>
|
||||
@jisu_creates · Response within 24h
|
||||
</p>
|
||||
<button style={{
|
||||
background: '#00ff88', color: '#000000', border: 'none', padding: '16px 48px',
|
||||
fontSize: 14, fontWeight: 700, cursor: 'pointer', fontFamily: 'Space Mono, monospace',
|
||||
letterSpacing: '0.08em', clipPath: 'polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px))',
|
||||
}}>
|
||||
START A PROJECT →
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,663 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
SVG ICONS
|
||||
═══════════════════════════════════════ */
|
||||
const BookOpenIcon = () => (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||
</svg>
|
||||
);
|
||||
const StarIcon = ({ filled = true }: { filled?: boolean }) => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={filled ? '#D4A853' : 'none'} stroke="#D4A853" strokeWidth="1.5">
|
||||
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/>
|
||||
</svg>
|
||||
);
|
||||
const QuoteIcon = () => (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M8 8C4 10.5 2 14 2 18c0 3.3 2 5 4 5s3.5-1.5 3.5-4c0-2.3-1.2-3.5-2.5-3.5C5.5 15.5 5 15 6 13c.8-1.5 2.5-3 4-3.5L8 8zm16 0c-4 2.5-6 6-6 10 0 3.3 2 5 4 5s3.5-1.5 3.5-4c0-2.3-1.2-3.5-2.5-3.5C21.5 15.5 21 15 22 13c.8-1.5 2.5-3 4-3.5L24 8z" fill="#D4A853" opacity="0.6"/>
|
||||
</svg>
|
||||
);
|
||||
const CheckIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#D4A853" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
);
|
||||
const ArrowUpRight = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/>
|
||||
</svg>
|
||||
);
|
||||
const ChevronDown = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
COUNTUP HOOK
|
||||
═══════════════════════════════════════ */
|
||||
function useCountUp(target: number, duration = 1600) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const obs = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
obs.disconnect();
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
setCount(Math.round(target * ease));
|
||||
if (progress < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
},
|
||||
{ threshold: 0.4 }
|
||||
);
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, [target, duration]);
|
||||
|
||||
return { count, ref };
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
STAT ITEM
|
||||
═══════════════════════════════════════ */
|
||||
function StatItem({ value, suffix, label }: { value: number; suffix?: string; label: string }) {
|
||||
const { count, ref } = useCountUp(value);
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '0 2rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: '2px' }}>
|
||||
<span ref={ref} style={{ fontSize: '3rem', fontFamily: "'Cormorant Garamond', serif", fontWeight: 700, color: '#D4A853', lineHeight: 1 }}>{count}</span>
|
||||
{suffix && <span style={{ fontSize: '1.5rem', fontFamily: "'Cormorant Garamond', serif", color: '#D4A853' }}>{suffix}</span>}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.78rem', color: '#8A8070', marginTop: '0.5rem', letterSpacing: '0.1em', textTransform: 'uppercase', fontFamily: "'Pretendard', sans-serif" }}>{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
BOOK CARD
|
||||
═══════════════════════════════════════ */
|
||||
interface Book {
|
||||
seed: string; title: string; author: string; rating: number; genre: string; year: number; note?: string;
|
||||
}
|
||||
|
||||
const books: Book[] = [
|
||||
{ seed: 'book1', title: '파친코', author: '이민진', rating: 5, genre: '소설', year: 2024, note: '역사 속에 묻힌 삶의 무게를 느꼈다' },
|
||||
{ seed: 'book2', title: '어린 왕자', author: '생텍쥐페리', rating: 5, genre: '고전', year: 2023 },
|
||||
{ seed: 'book3', title: '원씽', author: '게리 켈러', rating: 4, genre: '자기계발', year: 2024 },
|
||||
{ seed: 'book4', title: '채식주의자', author: '한강', rating: 5, genre: '소설', year: 2023, note: '불편함 속의 아름다움' },
|
||||
{ seed: 'book5', title: '지능의 본질', author: '제프 호킨스', rating: 4, genre: '과학', year: 2024 },
|
||||
{ seed: 'book6', title: '82년생 김지영', author: '조남주', rating: 4, genre: '소설', year: 2022 },
|
||||
{ seed: 'book7', title: '노인과 바다', author: '헤밍웨이', rating: 5, genre: '고전', year: 2022 },
|
||||
{ seed: 'book8', title: '생각에 관한 생각', author: '다니엘 카너먼', rating: 4, genre: '심리학', year: 2023 },
|
||||
];
|
||||
|
||||
const genreColors: Record<string, string> = {
|
||||
'소설': '#6B5B95', '고전': '#D4A853', '자기계발': '#4A7C59', '과학': '#2E6EA6', '심리학': '#8B4E62'
|
||||
};
|
||||
|
||||
function BookCard({ book, large = false }: { book: Book; large?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className="rd-reveal"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(212,168,83,0.15)',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
height: large ? '420px' : '340px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.5s cubic-bezier(0.16,1,0.3,1), box-shadow 0.5s cubic-bezier(0.16,1,0.3,1)',
|
||||
cursor: 'default',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'translateY(-6px)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 20px 60px rgba(212,168,83,0.12)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{/* cover */}
|
||||
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||||
<img
|
||||
src={`https://picsum.photos/seed/${book.seed}/300/450`}
|
||||
alt={book.title}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'brightness(0.75) saturate(0.7)' }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, transparent 40%, rgba(12,11,9,0.95))' }}/>
|
||||
{/* genre badge */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '0.75rem', left: '0.75rem',
|
||||
background: genreColors[book.genre] || '#6B5B95',
|
||||
color: 'white', fontSize: '0.65rem', fontFamily: "'Pretendard', sans-serif",
|
||||
padding: '3px 8px', borderRadius: '4px', fontWeight: 600, letterSpacing: '0.05em'
|
||||
}}>{book.genre}</div>
|
||||
{/* rating */}
|
||||
<div style={{ position: 'absolute', top: '0.75rem', right: '0.75rem', display: 'flex', gap: '2px' }}>
|
||||
{[1,2,3,4,5].map(i => <StarIcon key={i} filled={i <= book.rating}/>)}
|
||||
</div>
|
||||
</div>
|
||||
{/* info */}
|
||||
<div style={{ padding: '0.875rem', background: 'rgba(12,11,9,0.95)' }}>
|
||||
<p style={{ fontSize: '0.65rem', color: '#8A8070', fontFamily: "'Pretendard', sans-serif", marginBottom: '0.25rem' }}>{book.author} · {book.year}</p>
|
||||
<h3 style={{ fontSize: large ? '1.1rem' : '0.95rem', fontFamily: "'Cormorant Garamond', serif", fontWeight: 700, color: '#F0EAD8', margin: 0, wordBreak: 'keep-all', lineHeight: 1.3 }}>{book.title}</h3>
|
||||
{book.note && <p style={{ fontSize: '0.72rem', color: '#6A6050', fontFamily: "'Pretendard', sans-serif", marginTop: '0.4rem', wordBreak: 'keep-all', fontStyle: 'italic' }}>"{book.note}"</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
MAIN PAGE
|
||||
═══════════════════════════════════════ */
|
||||
export default function ReadingPage() {
|
||||
const [activeGenre, setActiveGenre] = useState('전체');
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const scroller: HTMLElement =
|
||||
(document.querySelector('.main-content') as HTMLElement | null) ??
|
||||
document.documentElement;
|
||||
|
||||
const onScroll = () => setShowTop(scroller.scrollTop > 400);
|
||||
scroller.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
const obs = new IntersectionObserver(
|
||||
entries => entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('rd-visible'); }),
|
||||
{ threshold: 0.1, root: scroller === document.documentElement ? null : scroller }
|
||||
);
|
||||
document.querySelectorAll('.rd-reveal').forEach(el => obs.observe(el));
|
||||
return () => {
|
||||
scroller.removeEventListener('scroll', onScroll);
|
||||
obs.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const genres = ['전체', '소설', '고전', '자기계발', '과학', '심리학'];
|
||||
const filteredBooks = activeGenre === '전체' ? books : books.filter(b => b.genre === activeGenre);
|
||||
|
||||
const quotes = [
|
||||
{ text: '당신이 무엇을 읽느냐가 당신이 누구인지를 말해준다. 책은 당신이 선택한 삶의 궤적이다.', author: '파친코 — 이민진', genre: '소설' },
|
||||
{ text: '어른들은 숫자를 좋아한다. 새 친구 이야기를 해줄 때, 중요한 것을 절대 묻지 않는다. 그의 목소리가 어떤지, 어떤 놀이를 좋아하는지.', author: '어린 왕자 — 생텍쥐페리', genre: '고전' },
|
||||
{ text: '성공한 사람들은 하나에 집중하고 나머지를 내려놓는 법을 배웠다. 한 가지를 잘하면 나머지가 따라온다.', author: '원씽 — 게리 켈러', genre: '자기계발' },
|
||||
{ text: '빠른 생각은 직관이고 느린 생각은 이성이다. 우리는 대부분 빠른 생각이 옳다고 착각하며 살아간다.', author: '생각에 관한 생각 — 다니엘 카너먼', genre: '심리학' },
|
||||
];
|
||||
|
||||
const currentlyReading = {
|
||||
seed: 'current1', title: '도둑맞은 집중력', author: '요한 하리', genre: '자기계발',
|
||||
progress: 67, totalPages: 380, currentPage: 255,
|
||||
startDate: '2024.03.10', note: '현대 사회가 어떻게 우리의 집중력을 앗아가는지 설득력 있게 풀어냄'
|
||||
};
|
||||
|
||||
const tbr = [
|
||||
{ seed: 'tbr1', title: '총, 균, 쇠', author: '재레드 다이아몬드', genre: '역사', priority: '높음' },
|
||||
{ seed: 'tbr2', title: '코스모스', author: '칼 세이건', genre: '과학', priority: '높음' },
|
||||
{ seed: 'tbr3', title: '사피엔스', author: '유발 하라리', genre: '역사', priority: '중간' },
|
||||
];
|
||||
|
||||
const monthlyData = [
|
||||
{ month: '10월', books: 2 }, { month: '11월', books: 3 }, { month: '12월', books: 1 },
|
||||
{ month: '1월', books: 4 }, { month: '2월', books: 3 }, { month: '3월', books: 2 },
|
||||
];
|
||||
const maxBooks = Math.max(...monthlyData.map(d => d.books));
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap');
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.min.css');
|
||||
|
||||
.rd-root { font-family: 'Pretendard', sans-serif; background: #0C0B09; color: #F0EAD8; min-height: 100dvh; }
|
||||
.rd-serif { font-family: 'Cormorant Garamond', serif; }
|
||||
|
||||
/* grain overlay */
|
||||
.rd-grain::before {
|
||||
content: '';
|
||||
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.07'/%3E%3C/svg%3E");
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* scroll reveal */
|
||||
.rd-reveal {
|
||||
opacity: 0; transform: translateY(2rem); filter: blur(2px);
|
||||
transition: opacity 0.7s cubic-bezier(0.16,1,0.3,1), transform 0.7s cubic-bezier(0.16,1,0.3,1), filter 0.7s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.rd-reveal.rd-visible { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
.rd-reveal:nth-child(2) { transition-delay: 0.08s; }
|
||||
.rd-reveal:nth-child(3) { transition-delay: 0.16s; }
|
||||
.rd-reveal:nth-child(4) { transition-delay: 0.24s; }
|
||||
.rd-reveal:nth-child(5) { transition-delay: 0.32s; }
|
||||
|
||||
/* nav */
|
||||
.rd-nav {
|
||||
position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); z-index: 100;
|
||||
background: rgba(12,11,9,0.7); backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(212,168,83,0.2); border-radius: 100px;
|
||||
padding: 0.75rem 2rem; display: flex; align-items: center; gap: 2.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* double-bezel cards */
|
||||
.rd-bezel {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(212,168,83,0.2);
|
||||
border-radius: 16px; padding: 1px;
|
||||
box-shadow: inset 0 1px 0 rgba(212,168,83,0.1), 0 0 0 1px rgba(0,0,0,0.5);
|
||||
}
|
||||
.rd-bezel-inner {
|
||||
background: rgba(20,18,14,0.9);
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
border-radius: 15px; padding: 1.75rem;
|
||||
}
|
||||
|
||||
/* genre filter */
|
||||
.rd-genre-btn {
|
||||
background: transparent; border: 1px solid rgba(212,168,83,0.2);
|
||||
color: #8A8070; font-family: 'Pretendard', sans-serif; font-size: 0.8rem;
|
||||
padding: 0.4rem 1rem; border-radius: 100px; cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.rd-genre-btn:hover { border-color: rgba(212,168,83,0.5); color: #D4A853; }
|
||||
.rd-genre-btn.active { background: rgba(212,168,83,0.15); border-color: #D4A853; color: #D4A853; }
|
||||
|
||||
/* progress bar */
|
||||
.rd-progress-track { background: rgba(255,255,255,0.06); border-radius: 100px; height: 4px; overflow: hidden; }
|
||||
.rd-progress-fill { height: 100%; border-radius: 100px; background: linear-gradient(90deg, #D4A853, #F5C842); transition: width 1s cubic-bezier(0.16,1,0.3,1); }
|
||||
|
||||
/* marquee */
|
||||
@keyframes rd-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
||||
.rd-marquee-track { animation: rd-marquee 25s linear infinite; display: flex; gap: 0; }
|
||||
|
||||
/* hero pulse */
|
||||
@keyframes rd-pulse { 0%,100% { opacity:0.3; transform: scale(1); } 50% { opacity:0.6; transform: scale(1.05); } }
|
||||
.rd-orb { animation: rd-pulse 4s ease-in-out infinite; }
|
||||
|
||||
/* CTA glow */
|
||||
.rd-cta-btn {
|
||||
background: #D4A853; color: #0C0B09; border: none;
|
||||
font-family: 'Pretendard', sans-serif; font-weight: 700; font-size: 0.9rem;
|
||||
padding: 0.875rem 2.5rem; border-radius: 100px; cursor: pointer;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.3s cubic-bezier(0.16,1,0.3,1);
|
||||
box-shadow: 0 0 40px rgba(212,168,83,0.3);
|
||||
}
|
||||
.rd-cta-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 40px rgba(212,168,83,0.5); background: #E8BC60; }
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: #0C0B09; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(212,168,83,0.3); border-radius: 2px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rd-nav { padding: 0.6rem 1.25rem; gap: 1.5rem; }
|
||||
.rd-bento { grid-template-columns: 1fr !important; }
|
||||
.rd-hero-title { font-size: clamp(3.5rem, 12vw, 8rem) !important; }
|
||||
.rd-stats-grid { grid-template-columns: 1fr 1fr !important; gap: 1.5rem !important; }
|
||||
.rd-quote-grid { grid-template-columns: 1fr !important; }
|
||||
.rd-currently { grid-template-columns: 1fr !important; }
|
||||
.rd-monthly-bar { height: 80px !important; }
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* ── BACK BANNER ── */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg,#0C0B09,#1A1710)',
|
||||
borderBottom: '1px solid rgba(212,168,83,0.12)',
|
||||
height: 40, display: 'flex', alignItems: 'center',
|
||||
padding: '0 24px', gap: 12, flexShrink: 0, position: 'relative', zIndex: 300,
|
||||
}}>
|
||||
<Link href="/services/website" style={{ color: 'rgba(212,168,83,0.7)', fontSize: 13, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, transition: 'color 0.2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = '#D4A853')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(212,168,83,0.7)')}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: 'rgba(212,168,83,0.2)' }}>|</span>
|
||||
<span style={{ color: 'rgba(212,168,83,0.4)', fontSize: 12, fontFamily: "'Cormorant Garamond', serif", fontStyle: 'italic' }}>
|
||||
SAMPLE · 독서 기록 노트
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rd-root rd-grain" style={{ position: 'relative' }}>
|
||||
|
||||
{/* ── NAV ── */}
|
||||
<nav className="rd-nav">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<BookOpenIcon/>
|
||||
<span className="rd-serif" style={{ fontSize: '1rem', fontWeight: 600, color: '#D4A853' }}>나의 독서 기록</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.8rem', color: '#8A8070' }}>
|
||||
{['컬렉션', '독서록', '통계'].map(item => (
|
||||
<a key={item} href={`#${item}`} style={{ color: 'inherit', textDecoration: 'none', transition: 'color 0.2s' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = '#D4A853')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = '#8A8070')}>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ── HERO ── */}
|
||||
<section style={{ minHeight: '100dvh', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 clamp(1.5rem, 6vw, 6rem)', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* ambient orbs */}
|
||||
<div className="rd-orb" style={{ position: 'absolute', top: '20%', right: '10%', width: '500px', height: '500px', background: 'radial-gradient(circle, rgba(212,168,83,0.06) 0%, transparent 70%)', borderRadius: '50%', pointerEvents: 'none' }}/>
|
||||
<div style={{ position: 'absolute', bottom: '15%', left: '5%', width: '300px', height: '300px', background: 'radial-gradient(circle, rgba(107,91,149,0.06) 0%, transparent 70%)', borderRadius: '50%', pointerEvents: 'none' }}/>
|
||||
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto', width: '100%', position: 'relative', zIndex: 1 }}>
|
||||
{/* eyebrow */}
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', border: '1px solid rgba(212,168,83,0.3)', borderRadius: '100px', padding: '0.35rem 0.875rem', marginBottom: '2.5rem' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#D4A853' }}/>
|
||||
<span style={{ fontSize: '0.72rem', color: '#D4A853', letterSpacing: '0.15em', textTransform: 'uppercase', fontFamily: "'Pretendard', sans-serif" }}>Personal Reading Journal</span>
|
||||
</div>
|
||||
|
||||
{/* hero title */}
|
||||
<h1 className="rd-hero-title" style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: 'clamp(4.5rem, 13vw, 10rem)', fontWeight: 700, lineHeight: 0.9, letterSpacing: '-0.02em', color: '#F0EAD8', margin: '0 0 2rem', wordBreak: 'keep-all' }}>
|
||||
책이 쌓이면<br/>
|
||||
<span style={{ color: '#D4A853', fontStyle: 'italic' }}>삶이 된다.</span>
|
||||
</h1>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '3rem', flexWrap: 'wrap' }}>
|
||||
<p style={{ fontSize: '1.1rem', color: '#8A8070', maxWidth: '360px', lineHeight: 1.8, wordBreak: 'keep-all', margin: 0 }}>
|
||||
읽은 책마다 남긴 생각들, 좋아하는 문장들, 그리고 아직 읽지 못한 설렘들을 기록하는 공간.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<button className="rd-cta-btn">기록 시작하기</button>
|
||||
<button style={{ background: 'transparent', border: '1px solid rgba(212,168,83,0.3)', color: '#D4A853', fontFamily: "'Pretendard', sans-serif", fontSize: '0.9rem', padding: '0.875rem 2rem', borderRadius: '100px', cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.16,1,0.3,1)' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(212,168,83,0.08)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}>
|
||||
둘러보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* scroll indicator */}
|
||||
<div style={{ marginTop: '5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', color: '#4A4035' }}>
|
||||
<div style={{ width: '40px', height: '1px', background: 'rgba(212,168,83,0.3)' }}/>
|
||||
<span style={{ fontSize: '0.72rem', letterSpacing: '0.15em', textTransform: 'uppercase' }}>Scroll to explore</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── STATS BAR ── */}
|
||||
<section style={{ padding: '3rem clamp(1.5rem, 6vw, 6rem)', borderTop: '1px solid rgba(212,168,83,0.1)', borderBottom: '1px solid rgba(212,168,83,0.1)' }}>
|
||||
<div className="rd-stats-grid rd-reveal" style={{ maxWidth: '1200px', margin: '0 auto', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '2rem', position: 'relative', zIndex: 1 }}>
|
||||
<StatItem value={47} label="완독한 책" />
|
||||
<div style={{ width: '1px', background: 'rgba(212,168,83,0.1)', margin: '-0.5rem 0' }}/>
|
||||
<StatItem value={2340} suffix="p" label="올해 읽은 페이지" />
|
||||
<div style={{ width: '1px', background: 'rgba(212,168,83,0.1)', margin: '-0.5rem 0' }}/>
|
||||
<StatItem value={128} label="저장한 문장" />
|
||||
<div style={{ width: '1px', background: 'rgba(212,168,83,0.1)', margin: '-0.5rem 0' }}/>
|
||||
<StatItem value={12} label="독서 중인 달" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── CURRENTLY READING ── */}
|
||||
<section style={{ padding: '5rem clamp(1.5rem, 6vw, 6rem)' }} id="독서록">
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div className="rd-reveal" style={{ display: 'flex', alignItems: 'baseline', gap: '1rem', marginBottom: '3rem' }}>
|
||||
<h2 className="rd-serif" style={{ fontSize: '2.5rem', fontWeight: 700, color: '#F0EAD8', margin: 0 }}>지금 읽는 중</h2>
|
||||
<span style={{ fontSize: '0.75rem', color: '#8A8070', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Currently reading</span>
|
||||
</div>
|
||||
|
||||
<div className="rd-currently rd-reveal" style={{ display: 'grid', gridTemplateColumns: '280px 1fr', gap: '2.5rem', alignItems: 'stretch' }}>
|
||||
{/* book cover */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<img src={`https://picsum.photos/seed/${currentlyReading.seed}/280/420`} alt={currentlyReading.title}
|
||||
style={{ width: '100%', aspectRatio: '2/3', objectFit: 'cover', borderRadius: '12px', filter: 'brightness(0.85) saturate(0.8)', display: 'block' }}/>
|
||||
<div style={{ position: 'absolute', inset: 0, borderRadius: '12px', boxShadow: 'inset 0 0 0 1px rgba(212,168,83,0.2), -12px 20px 60px rgba(0,0,0,0.6)' }}/>
|
||||
{/* reading badge */}
|
||||
<div style={{ position: 'absolute', top: '1rem', left: '-0.5rem', background: '#D4A853', color: '#0C0B09', fontSize: '0.65rem', fontWeight: 700, padding: '4px 10px', borderRadius: '4px', letterSpacing: '0.05em' }}>READING</div>
|
||||
</div>
|
||||
|
||||
{/* info */}
|
||||
<div className="rd-bezel" style={{ flex: 1 }}>
|
||||
<div className="rd-bezel-inner" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ marginBottom: 'auto' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: '0.7rem', color: '#D4A853', border: '1px solid rgba(212,168,83,0.3)', padding: '2px 8px', borderRadius: '4px' }}>{currentlyReading.genre}</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#8A8070', border: '1px solid rgba(255,255,255,0.08)', padding: '2px 8px', borderRadius: '4px' }}>시작 {currentlyReading.startDate}</span>
|
||||
</div>
|
||||
<h3 className="rd-serif" style={{ fontSize: '2rem', fontWeight: 700, color: '#F0EAD8', margin: '0 0 0.25rem', wordBreak: 'keep-all' }}>{currentlyReading.title}</h3>
|
||||
<p style={{ fontSize: '0.875rem', color: '#8A8070', margin: '0 0 1.5rem' }}>{currentlyReading.author}</p>
|
||||
|
||||
<div style={{ background: 'rgba(212,168,83,0.06)', border: '1px solid rgba(212,168,83,0.15)', borderRadius: '8px', padding: '1rem', marginBottom: '1.5rem' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#C4A060', fontStyle: 'italic', wordBreak: 'keep-all', margin: 0, lineHeight: 1.7 }}>"{currentlyReading.note}"</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* progress */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: '#8A8070' }}>읽은 진도</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#D4A853', fontFamily: "'Cormorant Garamond', serif", fontWeight: 600 }}>
|
||||
{currentlyReading.currentPage} / {currentlyReading.totalPages} p — {currentlyReading.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="rd-progress-track">
|
||||
<div className="rd-progress-fill" style={{ width: `${currentlyReading.progress}%` }}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TBR list */}
|
||||
<div style={{ marginTop: '2.5rem' }}>
|
||||
<h3 className="rd-serif rd-reveal" style={{ fontSize: '1.5rem', fontWeight: 600, color: '#6A5840', marginBottom: '1.25rem', margin: '0 0 1.25rem' }}>다음으로 읽을 책 <span style={{ fontSize: '1rem', fontStyle: 'italic' }}>(TBR)</span></h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{tbr.map((book, i) => (
|
||||
<div key={i} className="rd-reveal" style={{ display: 'flex', alignItems: 'center', gap: '1rem', padding: '1rem 1.25rem', background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '10px', transition: 'background 0.3s cubic-bezier(0.16,1,0.3,1)' }}
|
||||
onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = 'rgba(212,168,83,0.04)'}
|
||||
onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.02)'}>
|
||||
<img src={`https://picsum.photos/seed/${book.seed}/40/60`} alt={book.title} style={{ width: '36px', height: '54px', objectFit: 'cover', borderRadius: '4px', filter: 'brightness(0.8)' }}/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p className="rd-serif" style={{ fontSize: '1rem', fontWeight: 600, color: '#F0EAD8', margin: '0 0 0.2rem' }}>{book.title}</p>
|
||||
<p style={{ fontSize: '0.75rem', color: '#6A5840', margin: 0 }}>{book.author}</p>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.65rem', color: book.priority === '높음' ? '#D4A853' : '#8A8070', border: `1px solid ${book.priority === '높음' ? 'rgba(212,168,83,0.4)' : 'rgba(255,255,255,0.1)'}`, padding: '2px 8px', borderRadius: '4px' }}>{book.priority}</span>
|
||||
<ArrowUpRight/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── BOOK COLLECTION ── */}
|
||||
<section style={{ padding: '2rem clamp(1.5rem, 6vw, 6rem) 5rem' }} id="컬렉션">
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div className="rd-reveal" style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2.5rem', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<h2 className="rd-serif" style={{ fontSize: '2.5rem', fontWeight: 700, color: '#F0EAD8', margin: 0 }}>완독 컬렉션</h2>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{genres.map(g => (
|
||||
<button key={g} className={`rd-genre-btn ${activeGenre === g ? 'active' : ''}`} onClick={() => setActiveGenre(g)}>{g}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* bento grid */}
|
||||
<div className="rd-bento" style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr', gap: '1rem' }}>
|
||||
{filteredBooks.slice(0, 1).map((book, i) => (
|
||||
<div key={book.seed} style={{ gridRow: 'span 2' }}>
|
||||
<BookCard book={book} large />
|
||||
</div>
|
||||
))}
|
||||
{filteredBooks.slice(1, 3).map(book => (
|
||||
<BookCard key={book.seed} book={book} />
|
||||
))}
|
||||
{filteredBooks.slice(3, 5).map(book => (
|
||||
<BookCard key={book.seed} book={book} />
|
||||
))}
|
||||
{filteredBooks.slice(5, 7).map((book, i) => (
|
||||
<div key={book.seed} style={i === 1 ? { gridColumn: 'span 2' } : {}}>
|
||||
<BookCard book={book} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredBooks.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '4rem', color: '#4A4035' }}>
|
||||
<p className="rd-serif" style={{ fontSize: '1.5rem' }}>이 장르에 읽은 책이 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── QUOTES MASONRY ── */}
|
||||
<section style={{ padding: '5rem clamp(1.5rem, 6vw, 6rem)', background: 'rgba(212,168,83,0.03)', borderTop: '1px solid rgba(212,168,83,0.08)', borderBottom: '1px solid rgba(212,168,83,0.08)' }}>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div className="rd-reveal" style={{ marginBottom: '3rem' }}>
|
||||
<p style={{ fontSize: '0.72rem', color: '#D4A853', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '0.75rem' }}>Highlighted Quotes</p>
|
||||
<h2 className="rd-serif" style={{ fontSize: '2.5rem', fontWeight: 700, color: '#F0EAD8', margin: 0 }}>마음에 남은 문장들</h2>
|
||||
</div>
|
||||
|
||||
<div className="rd-quote-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1.25rem' }}>
|
||||
{quotes.map((quote, i) => (
|
||||
<div key={i} className={`rd-bezel rd-reveal ${i === 1 ? 'rd-reveal' : ''}`}
|
||||
style={{ marginTop: i % 2 === 1 ? '2rem' : '0' }}>
|
||||
<div className="rd-bezel-inner">
|
||||
<QuoteIcon/>
|
||||
<p className="rd-serif" style={{ fontSize: '1.2rem', color: '#C4B090', lineHeight: 1.8, margin: '1rem 0 1.25rem', fontStyle: 'italic', wordBreak: 'keep-all' }}>
|
||||
{quote.text}
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<p style={{ fontSize: '0.75rem', color: '#6A5840', margin: 0 }}>{quote.author}</p>
|
||||
<span style={{ fontSize: '0.65rem', color: '#D4A853', border: '1px solid rgba(212,168,83,0.3)', padding: '2px 8px', borderRadius: '4px' }}>{quote.genre}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── MONTHLY STATS ── */}
|
||||
<section style={{ padding: '5rem clamp(1.5rem, 6vw, 6rem)' }} id="통계">
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div className="rd-reveal" style={{ marginBottom: '3rem' }}>
|
||||
<p style={{ fontSize: '0.72rem', color: '#D4A853', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '0.75rem' }}>Reading Rhythm</p>
|
||||
<h2 className="rd-serif" style={{ fontSize: '2.5rem', fontWeight: 700, color: '#F0EAD8', margin: 0 }}>월별 독서 페이스</h2>
|
||||
</div>
|
||||
|
||||
<div className="rd-bezel rd-reveal">
|
||||
<div className="rd-bezel-inner">
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '1rem', height: '160px' }}>
|
||||
{monthlyData.map((d, i) => (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', height: '100%', justifyContent: 'flex-end' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: '#D4A853', fontFamily: "'Cormorant Garamond', serif", fontWeight: 600 }}>{d.books}</span>
|
||||
<div style={{ width: '100%', background: 'rgba(212,168,83,0.12)', borderRadius: '6px 6px 0 0', height: `${(d.books / maxBooks) * 100}%`, position: 'relative', transition: 'height 0.8s cubic-bezier(0.16,1,0.3,1)', minHeight: '8px' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, borderRadius: '6px 6px 0 0', background: `linear-gradient(to top, rgba(212,168,83,0.6), rgba(212,168,83,0.2))` }}/>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.72rem', color: '#6A5840' }}>{d.month}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '2rem', paddingTop: '1.5rem', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', gap: '2.5rem', flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ label: '총 완독 권수', value: '15권', sub: '최근 6개월' },
|
||||
{ label: '평균', value: '2.5권', sub: '월 평균' },
|
||||
{ label: '최다 독서 달', value: '1월', sub: '4권 완독' },
|
||||
].map((item, i) => (
|
||||
<div key={i}>
|
||||
<p className="rd-serif" style={{ fontSize: '1.75rem', fontWeight: 700, color: '#D4A853', margin: '0 0 0.2rem' }}>{item.value}</p>
|
||||
<p style={{ fontSize: '0.75rem', color: '#8A8070', margin: 0 }}>{item.label} · <span style={{ color: '#6A5840' }}>{item.sub}</span></p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── BRAND MARQUEE ── */}
|
||||
<div style={{ borderTop: '1px solid rgba(212,168,83,0.08)', borderBottom: '1px solid rgba(212,168,83,0.08)', overflow: 'hidden', padding: '1rem 0' }}>
|
||||
<div className="rd-marquee-track">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: '0', whiteSpace: 'nowrap' }}>
|
||||
{['파친코', '어린 왕자', '원씽', '채식주의자', '노인과 바다', '사피엔스', '총·균·쇠', '코스모스', '82년생 김지영'].map((title, j) => (
|
||||
<span key={j} style={{ padding: '0 2rem', fontSize: '0.8rem', color: '#4A4035', letterSpacing: '0.1em', textTransform: 'uppercase' }}>
|
||||
{title} <span style={{ color: 'rgba(212,168,83,0.3)', margin: '0 1rem' }}>·</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── CTA ── */}
|
||||
<section style={{ padding: '6rem clamp(1.5rem, 6vw, 6rem)', position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: '600px', height: '600px', background: 'radial-gradient(circle, rgba(212,168,83,0.08) 0%, transparent 70%)', pointerEvents: 'none' }}/>
|
||||
|
||||
<div className="rd-reveal" style={{ maxWidth: '700px', margin: '0 auto', textAlign: 'center', position: 'relative', zIndex: 1 }}>
|
||||
<p style={{ fontSize: '0.72rem', color: '#D4A853', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '1.5rem' }}>당신의 독서 기록</p>
|
||||
<h2 className="rd-serif" style={{ fontSize: 'clamp(2.5rem, 6vw, 4rem)', fontWeight: 700, color: '#F0EAD8', margin: '0 0 1.5rem', lineHeight: 1.1, wordBreak: 'keep-all' }}>
|
||||
오늘 읽은 한 페이지가<br/>
|
||||
<span style={{ color: '#D4A853', fontStyle: 'italic' }}>내일의 나를 만든다.</span>
|
||||
</h2>
|
||||
<p style={{ fontSize: '1rem', color: '#6A5840', maxWidth: '400px', margin: '0 auto 2.5rem', lineHeight: 1.8, wordBreak: 'keep-all' }}>
|
||||
읽은 책을 기록하고, 좋아하는 문장을 저장하고, 나만의 독서 여정을 쌓아가세요.
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<button className="rd-cta-btn">무료로 시작하기</button>
|
||||
<button style={{ background: 'transparent', border: '1px solid rgba(212,168,83,0.3)', color: '#D4A853', fontFamily: "'Pretendard', sans-serif", fontSize: '0.9rem', padding: '0.875rem 2rem', borderRadius: '100px', cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.16,1,0.3,1)' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(212,168,83,0.08)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}>
|
||||
샘플 둘러보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── FOOTER ── */}
|
||||
<footer style={{ borderTop: '1px solid rgba(212,168,83,0.08)', padding: '2rem clamp(1.5rem, 6vw, 6rem)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<BookOpenIcon/>
|
||||
<span className="rd-serif" style={{ color: '#D4A853', fontWeight: 600 }}>나의 독서 기록</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '0.75rem', color: '#4A4035', margin: 0 }}>책은 시간을 초월한 대화다.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{/* ── SCROLL TO TOP ── */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const scroller = (document.querySelector('.main-content') as HTMLElement | null) ?? document.documentElement;
|
||||
scroller.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed', bottom: '5.5rem', right: '2rem', zIndex: 400,
|
||||
width: 48, height: 48, borderRadius: '50%',
|
||||
background: '#D4A853', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(212,168,83,0.4)',
|
||||
opacity: showTop ? 1 : 0,
|
||||
transform: showTop ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)',
|
||||
transition: 'opacity 0.35s cubic-bezier(0.16,1,0.3,1), transform 0.35s cubic-bezier(0.16,1,0.3,1)',
|
||||
pointerEvents: showTop ? 'auto' : 'none',
|
||||
}}
|
||||
aria-label="맨 위로"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0C0B09" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,770 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
DESIGN TOKEN — 인테리어 페이지와 완전히 다른 팔레트
|
||||
No gold, no cream, no warm amber.
|
||||
Editorial B&W + cool stone.
|
||||
═══════════════════════════════════════════════════ */
|
||||
const T = {
|
||||
ink: '#0C0B09',
|
||||
paper: '#F4F2EF',
|
||||
sand: '#E9E5DF',
|
||||
stone: '#7C7870',
|
||||
chalk: '#B8B3AB',
|
||||
white: '#FAFAF8',
|
||||
} as const;
|
||||
|
||||
const BANNER_H = 40;
|
||||
const NAV_H = 56;
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
FASHION IMAGES — picsum.photos (안정적, 항상 로드됨)
|
||||
seed 값은 항상 동일한 이미지를 반환하므로 일관성 유지
|
||||
═══════════════════════════════════════════════════ */
|
||||
const IMG = {
|
||||
/* 히어로 — 세로형 풀스크린 */
|
||||
hero: 'https://picsum.photos/seed/mellow-hero/900/1200',
|
||||
/* 상품 — 세로 portrait (3:4) */
|
||||
p1: 'https://picsum.photos/seed/mellow-p1/480/640',
|
||||
p2: 'https://picsum.photos/seed/mellow-p2/480/640',
|
||||
p3: 'https://picsum.photos/seed/mellow-p3/480/640',
|
||||
p4: 'https://picsum.photos/seed/mellow-p4/480/640',
|
||||
p5: 'https://picsum.photos/seed/mellow-p5/480/640',
|
||||
p6: 'https://picsum.photos/seed/mellow-p6/480/640',
|
||||
p7: 'https://picsum.photos/seed/mellow-p7/480/640',
|
||||
p8: 'https://picsum.photos/seed/mellow-p8/480/640',
|
||||
/* 에디토리얼 배너 — 와이드 */
|
||||
edit1: 'https://picsum.photos/seed/mellow-edit/1400/700',
|
||||
/* 브랜드 스토리 이미지 */
|
||||
feat1: 'https://picsum.photos/seed/mellow-feat1/600/800',
|
||||
feat2: 'https://picsum.photos/seed/mellow-feat2/600/800',
|
||||
/* 룩북 — portrait strip */
|
||||
lb1: 'https://picsum.photos/seed/mellow-lb1/400/533',
|
||||
lb2: 'https://picsum.photos/seed/mellow-lb2/400/533',
|
||||
lb3: 'https://picsum.photos/seed/mellow-lb3/400/533',
|
||||
lb4: 'https://picsum.photos/seed/mellow-lb4/400/533',
|
||||
lb5: 'https://picsum.photos/seed/mellow-lb5/400/533',
|
||||
} as const;
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
DATA
|
||||
═══════════════════════════════════════════════════ */
|
||||
type Cat = '전체' | '아우터' | '상의' | '하의' | '액세서리';
|
||||
|
||||
const products = [
|
||||
{ id: 1, name: 'Structured Blazer', kr: '스트럭처드 블레이저', price: 328000, cat: '아우터' as Cat, img: IMG.p1, isNew: true },
|
||||
{ id: 2, name: 'Cocoon Coat', kr: '코쿤 코트', price: 498000, cat: '아우터' as Cat, img: IMG.p2, isBest: true },
|
||||
{ id: 3, name: 'Slip Midi Dress', kr: '슬립 미디 드레스', price: 218000, cat: '상의' as Cat, img: IMG.p3, isNew: true },
|
||||
{ id: 4, name: 'Relaxed Oxford', kr: '릴랙스드 옥스포드', price: 145000, cat: '상의' as Cat, img: IMG.p4 },
|
||||
{ id: 5, name: 'Wide Trousers', kr: '와이드 트라우저', price: 185000, cat: '하의' as Cat, img: IMG.p5 },
|
||||
{ id: 6, name: 'Barrel Denim', kr: '배럴 데님', price: 198000, cat: '하의' as Cat, img: IMG.p6, isBest: true },
|
||||
{ id: 7, name: 'Leather Mini Bag', kr: '레더 미니백', price: 278000, cat: '액세서리' as Cat, img: IMG.p7, isNew: true },
|
||||
{ id: 8, name: 'Square Sunglasses', kr: '스퀘어 선글라스', price: 128000, cat: '액세서리' as Cat, img: IMG.p8 },
|
||||
];
|
||||
|
||||
const reviews = [
|
||||
{ quote: '입고 나서 동료가 어디서 샀냐고 계속 물어봐요. 핏이 정말 완벽합니다.', name: '하윤서', city: '서울', item: 'Structured Blazer' },
|
||||
{ quote: '코쿤 코트, 사진보다 실물이 훨씬 좋아요. 소재감이 기대 이상입니다.', name: '이서진', city: '제주', item: 'Cocoon Coat' },
|
||||
{ quote: '슬립 드레스를 받았는데 바느질 하나하나에서 정성이 느껴져요.', name: '박도현', city: '부산', item: 'Slip Midi Dress' },
|
||||
];
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
MAIN PAGE
|
||||
═══════════════════════════════════════════════════ */
|
||||
export default function ShoppingPage() {
|
||||
const [activeCat, setActiveCat] = useState<Cat | '전체'>('전체');
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
const [cart, setCart] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const sc: HTMLElement =
|
||||
(document.querySelector('.main-content') as HTMLElement | null) ??
|
||||
document.documentElement;
|
||||
|
||||
const onScroll = () => {
|
||||
setScrolled(sc.scrollTop > 40);
|
||||
setShowTop(sc.scrollTop > 500);
|
||||
};
|
||||
sc.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
/* reveal */
|
||||
const obs = new IntersectionObserver(
|
||||
(entries) => entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('ml-in'); }),
|
||||
{ threshold: 0.07, root: sc === document.documentElement ? null : sc }
|
||||
);
|
||||
document.querySelectorAll('.ml-reveal').forEach(el => obs.observe(el));
|
||||
|
||||
return () => {
|
||||
sc.removeEventListener('scroll', onScroll);
|
||||
obs.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const cats: (Cat | '전체')[] = ['전체', '아우터', '상의', '하의', '액세서리'];
|
||||
const filtered = activeCat === '전체' ? products : products.filter(p => p.cat === activeCat);
|
||||
|
||||
const scrollTop = () => {
|
||||
const sc = (document.querySelector('.main-content') as HTMLElement | null) ?? document.documentElement;
|
||||
sc.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ml-page" style={{ fontFamily: "'Pretendard','Apple SD Gothic Neo',system-ui,sans-serif", background: T.paper, color: T.ink, overflowX: 'hidden' }}>
|
||||
|
||||
{/* ── FONTS + GLOBAL ── */}
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap');
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.min.css');
|
||||
|
||||
/* 전역 리셋을 .ml-page 하위로 한정 — 사이드바 오염 방지 */
|
||||
.ml-page, .ml-page *, .ml-page *::before, .ml-page *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
/* scroll reveal */
|
||||
.ml-reveal {
|
||||
opacity: 0; transform: translateY(28px);
|
||||
transition: opacity .75s cubic-bezier(.16,1,.3,1), transform .75s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.ml-reveal.ml-in { opacity: 1; transform: none; }
|
||||
.ml-reveal > *:nth-child(2) { transition-delay: 90ms; }
|
||||
.ml-reveal > *:nth-child(3) { transition-delay: 180ms; }
|
||||
.ml-reveal > *:nth-child(4) { transition-delay: 270ms; }
|
||||
.ml-reveal > *:nth-child(5) { transition-delay: 360ms; }
|
||||
.ml-reveal > *:nth-child(6) { transition-delay: 450ms; }
|
||||
.ml-reveal > *:nth-child(7) { transition-delay: 540ms; }
|
||||
.ml-reveal > *:nth-child(8) { transition-delay: 630ms; }
|
||||
|
||||
@keyframes ml-fadeUp {
|
||||
from { opacity: 0; transform: translateY(2rem); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@keyframes ml-ticker {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
@keyframes ml-float {
|
||||
0%,100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* product card hover */
|
||||
.ml-card-img {
|
||||
transition: transform .6s cubic-bezier(.16,1,.3,1), filter .4s;
|
||||
}
|
||||
.ml-card:hover .ml-card-img { transform: scale(1.04); filter: brightness(0.92); }
|
||||
|
||||
/* lookbook scroll */
|
||||
.ml-lb { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; }
|
||||
.ml-lb::-webkit-scrollbar { display: none; }
|
||||
|
||||
/* nav underline */
|
||||
.ml-nav-a {
|
||||
position: relative; font-size: 12px; font-weight: 500;
|
||||
letter-spacing: .06em; text-decoration: none; color: inherit;
|
||||
transition: opacity .25s;
|
||||
}
|
||||
.ml-nav-a::after {
|
||||
content: ''; position: absolute; bottom: -2px; left: 0; right: 0;
|
||||
height: 1px; background: currentColor; transform: scaleX(0);
|
||||
transform-origin: left; transition: transform .35s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.ml-nav-a:hover::after { transform: scaleX(1); }
|
||||
|
||||
/* cat pill */
|
||||
.ml-pill {
|
||||
border: 1px solid rgba(12,11,9,.18); border-radius: 0;
|
||||
background: transparent; font-family: inherit; font-size: 11px;
|
||||
font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
|
||||
padding: 8px 20px; cursor: pointer; color: ${T.stone};
|
||||
transition: all .3s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
.ml-pill:hover { border-color: ${T.ink}; color: ${T.ink}; }
|
||||
.ml-pill.on { background: ${T.ink}; color: ${T.white}; border-color: ${T.ink}; }
|
||||
|
||||
/* ticker */
|
||||
.ml-ticker-track { animation: ml-ticker 32s linear infinite; display: flex; white-space: nowrap; }
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar { width: 3px; }
|
||||
::-webkit-scrollbar-track { background: ${T.paper}; }
|
||||
::-webkit-scrollbar-thumb { background: ${T.chalk}; border-radius: 2px; }
|
||||
|
||||
@media (max-width:1024px) {
|
||||
.ml-hero-grid { grid-template-columns: 1fr !important; }
|
||||
.ml-hero-img { display: none !important; }
|
||||
.ml-feat-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
@media (max-width:768px) {
|
||||
.ml-prod-grid { grid-template-columns: 1fr 1fr !important; }
|
||||
}
|
||||
@media (max-width:480px) {
|
||||
.ml-prod-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
BACK BANNER
|
||||
════════════════════════════════════════════ */}
|
||||
<div style={{
|
||||
background: T.ink, height: BANNER_H,
|
||||
display: 'flex', alignItems: 'center', padding: '0 32px', gap: 14,
|
||||
}}>
|
||||
<Link href="/services/website"
|
||||
style={{ fontSize: 12, color: T.chalk, textDecoration: 'none', letterSpacing: '.04em', transition: 'color .2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = T.white)}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = T.chalk)}>
|
||||
← 홈페이지 제작 서비스로 돌아가기
|
||||
</Link>
|
||||
<span style={{ color: T.stone, fontSize: 11 }}>|</span>
|
||||
<span style={{ fontSize: 11, color: T.stone, fontStyle: 'italic', fontFamily: "'DM Serif Display', serif" }}>
|
||||
SAMPLE · 개인 쇼핑몰
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
NAV — minimal hairline
|
||||
════════════════════════════════════════════ */}
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 50, height: NAV_H,
|
||||
display: 'flex', alignItems: 'center', padding: '0 48px',
|
||||
background: scrolled ? `rgba(244,242,239,.96)` : T.ink,
|
||||
backdropFilter: 'blur(16px)',
|
||||
borderBottom: `1px solid ${scrolled ? 'rgba(12,11,9,.1)' : 'rgba(255,255,255,.06)'}`,
|
||||
transition: 'background .4s cubic-bezier(.16,1,.3,1), border-color .4s',
|
||||
}}>
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
|
||||
{/* wordmark */}
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 18, fontWeight: 400, letterSpacing: '.12em', color: scrolled ? T.ink : T.white, transition: 'color .4s' }}>
|
||||
MELLOW
|
||||
</div>
|
||||
|
||||
{/* links */}
|
||||
<div style={{ display: 'flex', gap: 36 }}>
|
||||
{['신상품', '아우터', '상의 / 하의', '액세서리', '세일'].map(l => (
|
||||
<a key={l} href="#collection" className="ml-nav-a"
|
||||
style={{ color: scrolled ? T.stone : 'rgba(244,242,239,.6)' }}>{l}</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* actions */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
|
||||
{/* search */}
|
||||
<button style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4,
|
||||
color: scrolled ? T.stone : 'rgba(244,242,239,.6)', transition: 'color .3s' }}
|
||||
aria-label="검색">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="15.65" y2="15.65"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/* cart */}
|
||||
<button onClick={() => setCart(c => c + 1)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, position: 'relative',
|
||||
color: scrolled ? T.ink : T.white, transition: 'color .3s' }}
|
||||
aria-label="장바구니">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<path d="M16 10a4 4 0 01-8 0"/>
|
||||
</svg>
|
||||
{cart > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute', top: -5, right: -5,
|
||||
width: 16, height: 16, borderRadius: '50%',
|
||||
background: scrolled ? T.ink : T.white,
|
||||
color: scrolled ? T.white : T.ink,
|
||||
fontSize: 9, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>{cart}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
HERO — editorial dark split
|
||||
════════════════════════════════════════════ */}
|
||||
<section style={{
|
||||
background: T.ink,
|
||||
minHeight: `calc(100dvh - ${BANNER_H}px - ${NAV_H}px)`,
|
||||
display: 'grid', gridTemplateColumns: '1fr 1fr',
|
||||
overflow: 'hidden',
|
||||
}} className="ml-hero-grid">
|
||||
|
||||
{/* Left */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
|
||||
padding: '80px 64px 72px 80px',
|
||||
animation: 'ml-fadeUp .9s cubic-bezier(.16,1,.3,1) both',
|
||||
}}>
|
||||
<p style={{ fontSize: 10, color: T.stone, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 40 }}>
|
||||
2025 — Spring / Summer Collection
|
||||
</p>
|
||||
<h1 style={{
|
||||
fontFamily: "'DM Serif Display', serif", fontStyle: 'italic',
|
||||
fontSize: 'clamp(3.5rem, 6vw, 6.5rem)',
|
||||
lineHeight: 1.0, color: T.white, marginBottom: 40,
|
||||
letterSpacing: '-.02em', wordBreak: 'keep-all',
|
||||
}}>
|
||||
Clothes<br/>that last<br/>
|
||||
<span style={{ color: T.sand, fontStyle: 'normal' }}>longer than</span><br/>
|
||||
trends.
|
||||
</h1>
|
||||
<p style={{ fontSize: 14, color: T.stone, lineHeight: 1.9, marginBottom: 44, maxWidth: 360, wordBreak: 'keep-all' }}>
|
||||
자연 소재, 정직한 제작, 긴 수명.<br/>
|
||||
유행 대신 오래가는 옷을 만듭니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 14 }}>
|
||||
<a href="#collection" style={{
|
||||
display: 'inline-block', padding: '14px 32px',
|
||||
background: T.white, color: T.ink,
|
||||
fontSize: 12, fontWeight: 700, letterSpacing: '.1em', textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
transition: 'opacity .25s',
|
||||
}}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '.82')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}>
|
||||
컬렉션 보기
|
||||
</a>
|
||||
<a href="#story" style={{
|
||||
display: 'inline-block', padding: '14px 28px',
|
||||
border: '1px solid rgba(244,242,239,.18)', color: 'rgba(244,242,239,.6)',
|
||||
fontSize: 12, fontWeight: 500, letterSpacing: '.08em', textTransform: 'uppercase',
|
||||
textDecoration: 'none', transition: 'border-color .3s, color .3s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(244,242,239,.4)'; (e.currentTarget as HTMLElement).style.color = T.white; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(244,242,239,.18)'; (e.currentTarget as HTMLElement).style.color = 'rgba(244,242,239,.6)'; }}>
|
||||
브랜드 스토리
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* inline stats */}
|
||||
<div style={{ display: 'flex', gap: 48, marginTop: 64, paddingTop: 40, borderTop: '1px solid rgba(244,242,239,.08)' }}>
|
||||
{[['1,247+','누적 고객'],['4.87','평점'],['98%','재구매율']].map(([n,l]) => (
|
||||
<div key={l}>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 22, color: T.white, letterSpacing: '-.01em' }}>{n}</div>
|
||||
<div style={{ fontSize: 10, color: T.stone, marginTop: 4, letterSpacing: '.08em' }}>{l}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right — image */}
|
||||
<div className="ml-hero-img" style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<img src={IMG.hero} alt="SS25 Collection"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: 'brightness(.75)' }}
|
||||
loading="eager"
|
||||
/>
|
||||
{/* Caption */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 32, left: 32, right: 32,
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end',
|
||||
}}>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 11, color: 'rgba(244,242,239,.5)', letterSpacing: '.18em', textTransform: 'uppercase' }}>
|
||||
Look 01 / Structured Blazer
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 13, color: T.white }}>
|
||||
₩328,000
|
||||
</div>
|
||||
</div>
|
||||
{/* Floating badge */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 32, right: 32,
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(12,11,9,.7)', backdropFilter: 'blur(12px)',
|
||||
border: '1px solid rgba(244,242,239,.1)',
|
||||
animation: 'ml-float 5s ease-in-out infinite',
|
||||
}}>
|
||||
<div style={{ fontSize: 9, color: T.stone, letterSpacing: '.18em', textTransform: 'uppercase', marginBottom: 4 }}>신규 입고</div>
|
||||
<div style={{ fontSize: 12, color: T.white, fontWeight: 600 }}>2025 S/S</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
TICKER
|
||||
════════════════════════════════════════════ */}
|
||||
<div style={{ background: T.sand, borderTop: '1px solid rgba(12,11,9,.08)', borderBottom: '1px solid rgba(12,11,9,.08)', overflow: 'hidden', padding: '11px 0' }}>
|
||||
<div className="ml-ticker-track">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<span key={i} style={{ flexShrink: 0 }}>
|
||||
{['Free Shipping Over 50,000', '무료 배송', '7일 무료 반품', '국내 장인 제작', '천연 소재만 사용', 'No Trend — Just Quality'].map((t, j) => (
|
||||
<span key={j} style={{ padding: '0 2rem', fontSize: 11, color: T.stone, letterSpacing: '.12em', textTransform: 'uppercase' }}>
|
||||
{t}<span style={{ color: T.chalk, margin: '0 .5rem' }}> · </span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
COLLECTION GRID
|
||||
════════════════════════════════════════════ */}
|
||||
<section id="collection" style={{ padding: '80px 48px', maxWidth: 1360, margin: '0 auto' }}>
|
||||
|
||||
{/* Header row */}
|
||||
<div className="ml-reveal" style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 40, flexWrap: 'wrap', gap: 20 }}>
|
||||
<div>
|
||||
<h2 style={{ fontFamily: "'DM Serif Display', serif", fontSize: 'clamp(1.8rem,3vw,2.8rem)', fontWeight: 400, color: T.ink, letterSpacing: '-.01em' }}>
|
||||
{activeCat === '전체' ? 'All Collection' : activeCat}
|
||||
</h2>
|
||||
<p style={{ fontSize: 12, color: T.stone, marginTop: 4, letterSpacing: '.04em' }}>
|
||||
{filtered.length}개 제품
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{cats.map(c => (
|
||||
<button key={c} className={`ml-pill${activeCat === c ? ' on' : ''}`}
|
||||
onClick={() => setActiveCat(c)}>{c}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid — asymmetric sizing */}
|
||||
<div className="ml-prod-grid ml-reveal" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '4px',
|
||||
}}>
|
||||
{filtered.map((p, i) => (
|
||||
<div key={p.id} className="ml-card" style={{
|
||||
cursor: 'pointer',
|
||||
gridRow: (i === 0 || i === 5) ? 'span 2' : 'span 1',
|
||||
}}>
|
||||
{/* Image */}
|
||||
<div style={{
|
||||
overflow: 'hidden',
|
||||
aspectRatio: (i === 0 || i === 5) ? '3/5' : '3/4',
|
||||
background: T.sand, position: 'relative',
|
||||
}}>
|
||||
<img src={p.img} alt={p.kr} className="ml-card-img"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
{/* Badges */}
|
||||
{(p.isNew || p.isBest) && (
|
||||
<div style={{
|
||||
position: 'absolute', top: 14, left: 14,
|
||||
background: p.isBest ? T.ink : 'transparent',
|
||||
border: `1px solid ${p.isBest ? T.ink : 'rgba(12,11,9,.35)'}`,
|
||||
color: p.isBest ? T.white : T.ink,
|
||||
fontSize: 9, fontWeight: 700, letterSpacing: '.12em', textTransform: 'uppercase',
|
||||
padding: '4px 9px',
|
||||
}}>
|
||||
{p.isBest ? 'BEST' : 'NEW'}
|
||||
</div>
|
||||
)}
|
||||
{/* Quick add */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0,
|
||||
background: 'rgba(12,11,9,.82)', backdropFilter: 'blur(4px)',
|
||||
padding: '13px 0', textAlign: 'center',
|
||||
opacity: 0, transition: 'opacity .3s',
|
||||
}} className="ml-quick">
|
||||
<button
|
||||
onClick={() => setCart(c => c + 1)}
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: T.white, fontSize: 11, fontWeight: 600, letterSpacing: '.1em', textTransform: 'uppercase',
|
||||
fontFamily: 'inherit',
|
||||
}}>
|
||||
장바구니 추가 +
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div style={{ padding: '14px 2px 24px' }}>
|
||||
<p style={{ fontSize: 10, color: T.stone, letterSpacing: '.1em', textTransform: 'uppercase', marginBottom: 5 }}>{p.cat}</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
|
||||
<div>
|
||||
<p style={{ fontSize: 14, fontWeight: 500, color: T.ink, marginBottom: 2, wordBreak: 'keep-all', letterSpacing: '-.01em' }}>{p.name}</p>
|
||||
<p style={{ fontSize: 11, color: T.chalk, wordBreak: 'keep-all' }}>{p.kr}</p>
|
||||
</div>
|
||||
<p style={{ fontSize: 14, fontWeight: 600, color: T.ink, flexShrink: 0, letterSpacing: '-.01em' }}>
|
||||
{p.price.toLocaleString()}<span style={{ fontSize: 11, fontWeight: 400 }}>원</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="ml-reveal" style={{ textAlign: 'center', marginTop: 56 }}>
|
||||
<button style={{
|
||||
border: '1px solid rgba(12,11,9,.2)', background: 'transparent',
|
||||
color: T.ink, fontFamily: 'inherit', fontSize: 11, fontWeight: 600,
|
||||
letterSpacing: '.12em', textTransform: 'uppercase', padding: '14px 48px',
|
||||
cursor: 'pointer', transition: 'background .3s, color .3s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = T.ink; (e.currentTarget as HTMLElement).style.color = T.white; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; (e.currentTarget as HTMLElement).style.color = T.ink; }}>
|
||||
더 보기 (24)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
EDITORIAL BANNER — full-bleed dark
|
||||
════════════════════════════════════════════ */}
|
||||
<section style={{ position: 'relative', height: 560, overflow: 'hidden', margin: '0 0 0 0' }}>
|
||||
<img src={IMG.edit1} alt="Editorial"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', filter: 'brightness(.45)' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="ml-reveal" style={{
|
||||
position: 'absolute', inset: 0, display: 'flex',
|
||||
flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
|
||||
padding: '40px',
|
||||
}}>
|
||||
<p style={{ fontSize: 10, color: T.chalk, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 24 }}>
|
||||
Look Book — 2025 S/S
|
||||
</p>
|
||||
<h2 style={{
|
||||
fontFamily: "'DM Serif Display', serif", fontStyle: 'italic',
|
||||
fontSize: 'clamp(2.5rem,5vw,5rem)', color: T.white,
|
||||
textAlign: 'center', lineHeight: 1.1, wordBreak: 'keep-all', maxWidth: 800,
|
||||
}}>
|
||||
Dressed for<br/>the life you live.
|
||||
</h2>
|
||||
<a href="#collection" style={{
|
||||
marginTop: 40, display: 'inline-block', padding: '13px 36px',
|
||||
border: '1px solid rgba(244,242,239,.4)', color: T.white,
|
||||
fontSize: 11, letterSpacing: '.14em', textTransform: 'uppercase', textDecoration: 'none',
|
||||
transition: 'background .3s, border-color .3s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(244,242,239,.12)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}>
|
||||
룩북 보기
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
BRAND STORY — 2-col asymmetric
|
||||
════════════════════════════════════════════ */}
|
||||
<section id="story" style={{ padding: '100px 80px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div className="ml-feat-grid ml-reveal" style={{ display: 'grid', gridTemplateColumns: '5fr 4fr', gap: '80px', alignItems: 'center' }}>
|
||||
|
||||
{/* Images side-by-side */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div style={{ overflow: 'hidden', aspectRatio: '3/4', background: T.sand }}>
|
||||
<img src={IMG.feat1} alt="Brand story 1"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(.7) brightness(.95)', transition: 'transform .7s cubic-bezier(.16,1,.3,1)' }}
|
||||
loading="lazy"
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLImageElement).style.transform = 'scale(1.03)')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLImageElement).style.transform = 'scale(1)')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ overflow: 'hidden', aspectRatio: '3/4', background: T.sand, marginTop: 40 }}>
|
||||
<img src={IMG.feat2} alt="Brand story 2"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(.7) brightness(.95)', transition: 'transform .7s cubic-bezier(.16,1,.3,1)' }}
|
||||
loading="lazy"
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLImageElement).style.transform = 'scale(1.03)')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLImageElement).style.transform = 'scale(1)')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div style={{ paddingLeft: 16 }}>
|
||||
<p style={{ fontSize: 10, color: T.stone, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 28 }}>
|
||||
About MELLOW
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'DM Serif Display', serif", fontSize: 'clamp(2rem,3.5vw,3.2rem)', fontWeight: 400, lineHeight: 1.15, color: T.ink, marginBottom: 28, wordBreak: 'keep-all', letterSpacing: '-.01em' }}>
|
||||
옷을 만드는<br/>철학이 있습니다.
|
||||
</h2>
|
||||
<p style={{ fontSize: 14, color: T.stone, lineHeight: 1.95, marginBottom: 18, wordBreak: 'keep-all' }}>
|
||||
멜로우는 2019년, 옷 한 벌의 수명을 늘리고 싶다는 생각에서 시작했습니다.
|
||||
계절이 바뀌어도 입을 수 있고, 5년 후에도 유행에서 벗어나지 않는 옷을 만드는 것이 목표입니다.
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: T.stone, lineHeight: 1.95, marginBottom: 40, wordBreak: 'keep-all' }}>
|
||||
모든 제품은 국내 협력 공방에서 소량 생산합니다. 린넨·코튼·울 등 천연 소재만 사용하며,
|
||||
염색에도 저자극 공정만 택합니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 40 }}>
|
||||
{[['6년', '브랜드 히스토리'],['100%','천연 소재'],['소량','국내 생산']].map(([n,l]) => (
|
||||
<div key={l}>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 24, color: T.ink }}>{n}</div>
|
||||
<div style={{ fontSize: 11, color: T.chalk, marginTop: 4, wordBreak: 'keep-all', letterSpacing: '.04em' }}>{l}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
LOOKBOOK — horizontal scroll strip
|
||||
════════════════════════════════════════════ */}
|
||||
<section style={{ padding: '0 48px 80px', maxWidth: 1360, margin: '0 auto' }}>
|
||||
<div className="ml-reveal" style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||
<h3 style={{ fontFamily: "'DM Serif Display', serif", fontSize: 20, fontWeight: 400, color: T.ink }}>Lookbook</h3>
|
||||
<a href="#" style={{ fontSize: 11, color: T.stone, letterSpacing: '.1em', textTransform: 'uppercase', textDecoration: 'none', transition: 'color .2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = T.ink)}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = T.stone)}>
|
||||
전체 보기 →
|
||||
</a>
|
||||
</div>
|
||||
<div className="ml-lb">
|
||||
{[IMG.lb1, IMG.lb2, IMG.lb3, IMG.lb4, IMG.lb5].map((src, i) => (
|
||||
<div key={i} style={{
|
||||
flexShrink: 0, width: 240, aspectRatio: '3/4', overflow: 'hidden',
|
||||
background: T.sand, cursor: 'pointer',
|
||||
}}>
|
||||
<img src={src} alt={`Lookbook ${i+1}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(.65)', transition: 'transform .6s cubic-bezier(.16,1,.3,1), filter .4s' }}
|
||||
loading="lazy"
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLImageElement).style.transform = 'scale(1.04)'; (e.currentTarget as HTMLImageElement).style.filter = 'saturate(.85)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLImageElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLImageElement).style.filter = 'saturate(.65)'; }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
REVIEWS — typographic, no card bg
|
||||
════════════════════════════════════════════ */}
|
||||
<section style={{ background: T.ink, padding: '96px 80px' }}>
|
||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
||||
<p className="ml-reveal" style={{ fontSize: 10, color: T.stone, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 64 }}>
|
||||
Customer Notes
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: '1px', background: 'rgba(244,242,239,.06)' }}>
|
||||
{reviews.map((r, i) => (
|
||||
<div key={i} className="ml-reveal" style={{ padding: '40px 36px', background: T.ink }}>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 11, color: T.stone, letterSpacing: '.14em', textTransform: 'uppercase', marginBottom: 24 }}>
|
||||
{r.item}
|
||||
</div>
|
||||
<p style={{ fontSize: 16, color: 'rgba(244,242,239,.85)', lineHeight: 1.75, marginBottom: 32, wordBreak: 'keep-all', fontStyle: 'italic', fontFamily: "'DM Serif Display', serif" }}>
|
||||
"{r.quote}"
|
||||
</p>
|
||||
<div style={{ borderTop: '1px solid rgba(244,242,239,.07)', paddingTop: 20 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: T.chalk }}>{r.name}</div>
|
||||
<div style={{ fontSize: 11, color: T.stone, marginTop: 2 }}>{r.city}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
CTA — minimal centered on paper
|
||||
════════════════════════════════════════════ */}
|
||||
<section style={{ padding: '100px 48px', textAlign: 'center', background: T.paper }}>
|
||||
<div className="ml-reveal" style={{ maxWidth: 540, margin: '0 auto' }}>
|
||||
<p style={{ fontSize: 10, color: T.stone, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 24 }}>
|
||||
Newsletter
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'DM Serif Display', serif", fontSize: 'clamp(2rem,4vw,3rem)', fontWeight: 400, color: T.ink, lineHeight: 1.2, marginBottom: 20, wordBreak: 'keep-all' }}>
|
||||
신상품 소식을<br/>가장 먼저 받아보세요
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: T.stone, lineHeight: 1.8, marginBottom: 36, wordBreak: 'keep-all' }}>
|
||||
구독자에게는 새 컬렉션 사전 공개와 10% 할인 쿠폰을 드립니다.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 0, maxWidth: 420, margin: '0 auto' }}>
|
||||
<input type="email" placeholder="이메일 주소"
|
||||
style={{
|
||||
flex: 1, padding: '13px 18px',
|
||||
border: '1px solid rgba(12,11,9,.18)', borderRight: 'none',
|
||||
background: 'transparent', fontFamily: 'inherit', fontSize: 13,
|
||||
color: T.ink, outline: 'none',
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = T.ink)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = 'rgba(12,11,9,.18)')}
|
||||
/>
|
||||
<button style={{
|
||||
padding: '13px 22px', background: T.ink, color: T.white,
|
||||
border: '1px solid transparent', fontFamily: 'inherit', fontSize: 11,
|
||||
fontWeight: 700, letterSpacing: '.1em', textTransform: 'uppercase',
|
||||
cursor: 'pointer', flexShrink: 0, transition: 'opacity .2s',
|
||||
}}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.opacity = '.8')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.opacity = '1')}>
|
||||
구독
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
FOOTER
|
||||
════════════════════════════════════════════ */}
|
||||
<footer style={{ background: T.ink, padding: '64px 80px 36px', color: T.stone }}>
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr', gap: 48, marginBottom: 56 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: "'DM Serif Display', serif", fontSize: 20, color: T.white, letterSpacing: '.1em', marginBottom: 6 }}>MELLOW</div>
|
||||
<div style={{ fontSize: 9, color: T.stone, letterSpacing: '.22em', textTransform: 'uppercase', marginBottom: 18 }}>studio</div>
|
||||
<p style={{ fontSize: 12, lineHeight: 1.85, wordBreak: 'keep-all', maxWidth: 220, color: 'rgba(244,242,239,.4)' }}>
|
||||
자연 소재와 정직한 제작으로<br/>오래가는 옷을 만듭니다.
|
||||
</p>
|
||||
</div>
|
||||
{[
|
||||
{ title: 'Shop', links: ['신상품', '아우터', '상의', '하의', '액세서리', '세일'] },
|
||||
{ title: 'Brand', links: ['브랜드 스토리', '지속가능성', '룩북', '장인 협업'] },
|
||||
{ title: 'Support', links: ['배송·반품', '사이즈 가이드', 'FAQ', '문의하기'] },
|
||||
].map(col => (
|
||||
<div key={col.title}>
|
||||
<div style={{ fontSize: 9, color: 'rgba(244,242,239,.4)', letterSpacing: '.2em', textTransform: 'uppercase', marginBottom: 20, fontWeight: 700 }}>{col.title}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{col.links.map(l => (
|
||||
<a key={l} href="#" style={{ fontSize: 12, color: T.stone, textDecoration: 'none', transition: 'color .2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = T.chalk)}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = T.stone)}>
|
||||
{l}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ borderTop: '1px solid rgba(244,242,239,.06)', paddingTop: 28, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 14 }}>
|
||||
<p style={{ fontSize: 11, color: 'rgba(244,242,239,.3)', letterSpacing: '.03em' }}>© 2025 MELLOW STUDIO. All rights reserved.</p>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{['개인정보처리방침', '이용약관', 'Instagram', 'Pinterest'].map(l => (
|
||||
<a key={l} href="#" style={{ fontSize: 11, color: T.stone, textDecoration: 'none', transition: 'color .2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = T.chalk)}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = T.stone)}>
|
||||
{l}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
QUICK-ADD HOVER — global CSS patch
|
||||
════════════════════════════════════════════ */}
|
||||
<style dangerouslySetInnerHTML={{ __html: `.ml-card:hover .ml-quick { opacity: 1 !important; }` }} />
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
SCROLL TO TOP
|
||||
════════════════════════════════════════════ */}
|
||||
<button onClick={scrollTop} aria-label="맨 위로"
|
||||
style={{
|
||||
position: 'fixed', bottom: '5.5rem', right: '2rem', zIndex: 200,
|
||||
width: 46, height: 46,
|
||||
background: T.ink, border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 28px rgba(12,11,9,.25)',
|
||||
opacity: showTop ? 1 : 0,
|
||||
transform: showTop ? 'translateY(0)' : 'translateY(12px)',
|
||||
transition: 'opacity .35s cubic-bezier(.16,1,.3,1), transform .35s cubic-bezier(.16,1,.3,1)',
|
||||
pointerEvents: showTop ? 'auto' : 'none',
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.white} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,543 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Mode = 'simple' | 'custom';
|
||||
|
||||
type SunoClip = {
|
||||
id: string;
|
||||
title?: string;
|
||||
audioUrl?: string;
|
||||
streamAudioUrl?: string;
|
||||
imageUrl?: string;
|
||||
tags?: string;
|
||||
duration?: number;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
type TaskState = {
|
||||
taskId: string;
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
clips: SunoClip[];
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
const MODELS = [
|
||||
{ id: 'V4', label: 'V4 (기본)', desc: '안정적 고품질' },
|
||||
{ id: 'V4_5', label: 'V4.5', desc: '최신 · 풍부한 디테일' },
|
||||
{ id: 'V3_5', label: 'V3.5', desc: '빠른 생성' },
|
||||
];
|
||||
|
||||
const TAG_PRESETS = [
|
||||
'k-pop', 'lo-fi', 'city pop', 'ballad', 'edm', 'trap',
|
||||
'rock', 'jazz', 'acoustic', 'cinematic', 'synthwave', 'ambient',
|
||||
];
|
||||
|
||||
const LS_KEY = 'jsm_studio_task_ids_v2';
|
||||
|
||||
const isDone = (s: string) => s === 'SUCCESS' || s === 'FIRST_SUCCESS';
|
||||
const isFailed = (s: string) => s.includes('FAILED') || s === 'SENSITIVE_WORD_ERROR';
|
||||
|
||||
export default function StudioPage() {
|
||||
const [mode, setMode] = useState<Mode>('simple');
|
||||
const [model, setModel] = useState('V4');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [lyrics, setLyrics] = useState('');
|
||||
const [tags, setTags] = useState('');
|
||||
const [instrumental, setInstrumental] = useState(false);
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tasks, setTasks] = useState<TaskState[]>([]);
|
||||
const pollRef = useRef<number | null>(null);
|
||||
|
||||
const saveToLS = useCallback((ids: string[]) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try { localStorage.setItem(LS_KEY, JSON.stringify(ids.slice(0, 20))); } catch { /* noop */ }
|
||||
}, []);
|
||||
|
||||
const fetchOne = useCallback(async (taskId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/studio/status?taskId=${encodeURIComponent(taskId)}`);
|
||||
const json = await res.json();
|
||||
if (!json.ok) return null;
|
||||
const d = json.data?.data ?? json.data;
|
||||
const status: string = d?.status ?? 'PENDING';
|
||||
const errMsg: string | undefined = d?.errorMessage;
|
||||
const sunoData: SunoClip[] = d?.response?.sunoData ?? [];
|
||||
return { taskId, status, errorMessage: errMsg, clips: sunoData, updatedAt: Date.now() } as TaskState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshAll = useCallback(async (ids: string[]) => {
|
||||
const results = await Promise.all(ids.map((id) => fetchOne(id)));
|
||||
setTasks((prev) => {
|
||||
const map = new Map(prev.map((t) => [t.taskId, t]));
|
||||
for (const r of results) if (r) map.set(r.taskId, r);
|
||||
return Array.from(map.values()).sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
});
|
||||
}, [fetchOne]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_KEY);
|
||||
const ids = raw ? (JSON.parse(raw) as string[]) : [];
|
||||
if (ids.length) {
|
||||
setTasks(ids.map((id) => ({ taskId: id, status: 'PENDING', clips: [], updatedAt: Date.now() })));
|
||||
refreshAll(ids);
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}, [refreshAll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pollRef.current) window.clearInterval(pollRef.current);
|
||||
const pending = tasks.filter((t) => !isDone(t.status) && !isFailed(t.status));
|
||||
if (pending.length) {
|
||||
pollRef.current = window.setInterval(() => {
|
||||
refreshAll(pending.map((t) => t.taskId));
|
||||
}, 8000);
|
||||
}
|
||||
return () => { if (pollRef.current) window.clearInterval(pollRef.current); };
|
||||
}, [tasks, refreshAll]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
setError(null);
|
||||
if (mode === 'simple' && !prompt.trim()) { setError('프롬프트를 입력해주세요.'); return; }
|
||||
if (mode === 'custom') {
|
||||
if (!title.trim()) { setError('트랙 제목을 입력해주세요.'); return; }
|
||||
if (!tags.trim()) { setError('스타일 태그를 입력해주세요.'); return; }
|
||||
if (!lyrics.trim() && !instrumental) { setError('가사를 입력하거나 Instrumental을 켜주세요.'); return; }
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/studio/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
mode, model,
|
||||
prompt: prompt.trim(),
|
||||
title: title.trim(),
|
||||
lyrics: lyrics.trim(),
|
||||
tags: tags.trim(),
|
||||
make_instrumental: instrumental,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok || !json.ok) {
|
||||
setError(typeof json.error === 'string' ? json.error : '생성 실패');
|
||||
return;
|
||||
}
|
||||
const taskId: string | undefined = json.data?.data?.taskId ?? json.data?.taskId;
|
||||
if (!taskId) {
|
||||
setError('응답에서 taskId를 찾지 못했습니다.');
|
||||
return;
|
||||
}
|
||||
setTasks((prev) => {
|
||||
const next: TaskState[] = [
|
||||
{ taskId, status: 'PENDING', clips: [], updatedAt: Date.now() },
|
||||
...prev.filter((t) => t.taskId !== taskId),
|
||||
];
|
||||
saveToLS(next.map((t) => t.taskId));
|
||||
return next;
|
||||
});
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addTag = (t: string) => {
|
||||
const cur = tags.split(',').map((x) => x.trim()).filter(Boolean);
|
||||
if (cur.includes(t)) return;
|
||||
setTags([...cur, t].join(', '));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen px-4 md:px-8 lg:px-12 py-10"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(1200px 600px at 20% -10%, rgba(156,72,234,0.18), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(83,221,252,0.12), transparent 55%), var(--kx-surface)',
|
||||
color: 'var(--kx-on-surface)',
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-end justify-between flex-wrap gap-4 mb-8">
|
||||
<div>
|
||||
<span className="kx-label">JAENGSEUNG STUDIO</span>
|
||||
<h1 className="kx-display text-3xl md:text-5xl font-extrabold mt-2" style={{ letterSpacing: '-0.02em' }}>
|
||||
프롬프트 한 줄로 트랙 만들기
|
||||
</h1>
|
||||
<p className="mt-2 text-sm" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
Suno 엔진 기반 · Custom 모드로 가사·태그·보컬까지 세밀 제어
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs px-3 py-1.5 rounded-full border"
|
||||
style={{
|
||||
borderColor: 'rgba(204,151,255,0.35)',
|
||||
background: 'rgba(204,151,255,0.1)',
|
||||
color: 'var(--kx-primary)',
|
||||
}}
|
||||
>
|
||||
⚡ v1 Studio · Live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)] gap-6">
|
||||
{/* 좌측: 제어판 */}
|
||||
<div
|
||||
className="rounded-2xl p-6 md:p-8"
|
||||
style={{
|
||||
background: 'rgba(12,22,45,0.7)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-1 p-1 rounded-full mb-6" style={{ background: 'rgba(255,255,255,0.04)' }}>
|
||||
{(['simple', 'custom'] as Mode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m)}
|
||||
className="flex-1 py-2.5 text-sm font-semibold rounded-full transition-all"
|
||||
style={
|
||||
mode === m
|
||||
? {
|
||||
background: 'linear-gradient(135deg, rgba(204,151,255,0.25), rgba(83,221,252,0.15))',
|
||||
color: '#fff',
|
||||
boxShadow: '0 0 24px rgba(204,151,255,0.25) inset',
|
||||
}
|
||||
: { color: 'var(--kx-on-variant)' }
|
||||
}
|
||||
>
|
||||
{m === 'simple' ? '간단 모드' : 'Custom 모드'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{mode === 'simple' ? (
|
||||
<div className="space-y-5">
|
||||
<Field label="프롬프트" hint="무드·장르·가사 아이디어를 한 줄로">
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="예: 비 오는 서울 새벽, 감성 시티팝 with 여성 보컬, 2010년대 무드"
|
||||
className="w-full bg-transparent outline-none resize-none text-base"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<Field label="트랙 제목">
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="예: 새벽 세 시의 도시"
|
||||
className="w-full bg-transparent outline-none text-base"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="가사" hint="Suno 포맷: [Verse] [Chorus] [Bridge] 등 태그 가능">
|
||||
<textarea
|
||||
value={lyrics}
|
||||
onChange={(e) => setLyrics(e.target.value)}
|
||||
rows={8}
|
||||
placeholder={'[Verse]\n차가운 조명 아래 걷는 나\n새벽 세 시의 도시는 낯설어\n\n[Chorus]\n...'}
|
||||
className="w-full bg-transparent outline-none resize-none font-mono text-sm leading-relaxed"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="스타일 태그" hint="쉼표로 구분 · 장르·무드·악기·보컬 톤">
|
||||
<input
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="city pop, female vocal, 120bpm, synth, nostalgic"
|
||||
className="w-full bg-transparent outline-none text-base"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1.5 mt-3">
|
||||
{TAG_PRESETS.map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => addTag(t)}
|
||||
className="text-xs px-2.5 py-1 rounded-full transition"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
color: 'var(--kx-on-variant)',
|
||||
}}
|
||||
>
|
||||
+ {t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-6">
|
||||
<Field label="모델">
|
||||
<select
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
className="w-full bg-transparent outline-none text-sm"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
>
|
||||
{MODELS.map((m) => (
|
||||
<option key={m.id} value={m.id} style={{ background: '#0b1428' }}>
|
||||
{m.label} — {m.desc}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Instrumental (가사 없음)">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<span
|
||||
className="relative inline-block w-11 h-6 rounded-full transition"
|
||||
style={{ background: instrumental ? 'rgba(204,151,255,0.6)' : 'rgba(255,255,255,0.1)' }}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all"
|
||||
style={{ left: instrumental ? '22px' : '2px' }}
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={instrumental}
|
||||
onChange={(e) => setInstrumental(e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-xs" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
{instrumental ? 'ON' : 'OFF'}
|
||||
</span>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={submitting}
|
||||
className="w-full py-4 rounded-xl font-extrabold text-base transition-all disabled:opacity-60"
|
||||
style={{
|
||||
background: submitting
|
||||
? 'rgba(204,151,255,0.2)'
|
||||
: 'linear-gradient(135deg, #cc97ff 0%, #7c3aed 50%, #53ddfc 100%)',
|
||||
color: '#0b1428',
|
||||
boxShadow: submitting ? 'none' : '0 12px 40px -12px rgba(204,151,255,0.6)',
|
||||
letterSpacing: '0.01em',
|
||||
}}
|
||||
>
|
||||
{submitting ? '생성 요청 중…' : '▶ Generate Track'}
|
||||
</button>
|
||||
{error && (
|
||||
<p className="mt-3 text-xs px-3 py-2 rounded-lg" style={{ background: 'rgba(215,51,87,0.12)', color: '#ff8ba7' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-3 text-[11px] leading-relaxed" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
생성된 결과는 Suno 서비스 약관을 따릅니다. 상업 이용 전 플랜·저작권을 반드시 확인하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결과 */}
|
||||
<div
|
||||
className="rounded-2xl p-6 md:p-7"
|
||||
style={{
|
||||
background: 'rgba(9,17,36,0.7)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<span className="kx-label">RECENT TRACKS</span>
|
||||
<h2 className="kx-display text-xl font-bold mt-1">최근 생성 결과</h2>
|
||||
</div>
|
||||
{tasks.length > 0 && (
|
||||
<button
|
||||
onClick={() => { setTasks([]); saveToLS([]); }}
|
||||
className="text-[11px] underline underline-offset-4"
|
||||
style={{ color: 'var(--kx-on-variant)' }}
|
||||
>
|
||||
기록 지우기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<div
|
||||
className="rounded-xl p-8 text-center text-sm"
|
||||
style={{ border: '1px dashed rgba(255,255,255,0.1)', color: 'var(--kx-on-variant)' }}
|
||||
>
|
||||
아직 생성된 트랙이 없습니다.
|
||||
<br />왼쪽에서 프롬프트를 입력하고 Generate를 눌러보세요.
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-4 max-h-[640px] overflow-y-auto pr-1">
|
||||
{tasks.map((task) => (
|
||||
<li
|
||||
key={task.taskId}
|
||||
className="rounded-xl p-4"
|
||||
style={{
|
||||
background: 'rgba(20,31,56,0.6)',
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<span className="text-[11px] font-mono opacity-60">task: {task.taskId.slice(0, 10)}…</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
|
||||
{task.clips.length === 0 ? (
|
||||
<div
|
||||
className="h-9 rounded-md flex items-center justify-center text-xs"
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, rgba(204,151,255,0.08) 0%, rgba(83,221,252,0.08) 100%)',
|
||||
color: 'var(--kx-on-variant)',
|
||||
}}
|
||||
>
|
||||
{isFailed(task.status)
|
||||
? (task.errorMessage ?? '생성 실패')
|
||||
: '오디오 생성 중… (보통 1~3분)'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{task.clips.map((c) => {
|
||||
const src = c.audioUrl || c.streamAudioUrl;
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
className="rounded-lg p-3"
|
||||
style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.04)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{c.imageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={c.imageUrl}
|
||||
alt=""
|
||||
className="w-12 h-12 rounded-md object-cover flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-sm truncate" style={{ color: 'var(--kx-on-surface)' }}>
|
||||
{c.title || '제목 없음'}
|
||||
</p>
|
||||
{c.tags && (
|
||||
<p className="text-[11px] truncate mt-0.5" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
{c.tags}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{c.duration && (
|
||||
<span className="text-[10px] font-mono opacity-60">
|
||||
{Math.round(c.duration)}s
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{src ? (
|
||||
<audio controls src={src} className="w-full mt-2" style={{ height: 36 }} />
|
||||
) : null}
|
||||
{c.audioUrl && (
|
||||
<div className="mt-1.5 text-[11px]" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
<a href={c.audioUrl} download className="underline underline-offset-4 hover:text-white">
|
||||
MP3 다운로드
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid md:grid-cols-3 gap-4 text-xs" style={{ color: 'var(--kx-on-variant)' }}>
|
||||
<Tip title="① 간단 모드" body="한 줄 프롬프트로 즉시 생성. 결과물 다양성 높음." />
|
||||
<Tip title="② Custom 모드" body="가사·태그·보컬·악기까지 정밀 제어. 반복 생성에 유리." />
|
||||
<Tip title="③ 상업 이용" body="Suno Pro 이상 플랜에서 생성한 결과만 수익화 가능. 플랜 확인 필수." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
hint,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
hint?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-4"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-baseline justify-between mb-2">
|
||||
<span className="text-[11px] font-semibold tracking-widest uppercase" style={{ color: 'var(--kx-primary)' }}>
|
||||
{label}
|
||||
</span>
|
||||
{hint && <span className="text-[10px]" style={{ color: 'var(--kx-on-variant)' }}>{hint}</span>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const map: Record<string, { bg: string; fg: string; label: string }> = {
|
||||
SUCCESS: { bg: 'rgba(64,206,172,0.18)', fg: '#6cf0c6', label: '완료' },
|
||||
FIRST_SUCCESS: { bg: 'rgba(83,221,252,0.18)', fg: '#53ddfc', label: '첫 트랙 준비' },
|
||||
TEXT_SUCCESS: { bg: 'rgba(83,221,252,0.18)', fg: '#53ddfc', label: '가사 완료' },
|
||||
PENDING: { bg: 'rgba(204,151,255,0.18)', fg: '#cc97ff', label: '대기' },
|
||||
};
|
||||
let entry = map[status];
|
||||
if (!entry) {
|
||||
entry = isFailed(status)
|
||||
? { bg: 'rgba(215,51,87,0.18)', fg: '#ff8ba7', label: '실패' }
|
||||
: { bg: 'rgba(255,255,255,0.06)', fg: 'rgba(255,255,255,0.6)', label: status };
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className="text-[10px] font-semibold px-2 py-0.5 rounded-full whitespace-nowrap"
|
||||
style={{ background: entry.bg, color: entry.fg }}
|
||||
>
|
||||
{entry.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Tip({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-4"
|
||||
style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}
|
||||
>
|
||||
<p className="font-semibold mb-1" style={{ color: 'var(--kx-on-surface)' }}>
|
||||
{title}
|
||||
</p>
|
||||
<p className="leading-relaxed">{body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user