Merge pull request #1 from gahusb/feature/renewal-phase1
리뉴얼 Phase 1: 외주+소프트웨어 2축 풀 리디자인 + 레거시 서비스 숨김
This commit is contained in:
@@ -51,10 +51,9 @@ export async function PATCH(request: Request) {
|
||||
}
|
||||
|
||||
const DEFAULT_SERVICES = [
|
||||
{ id: 'saju', name: 'AI 사주 분석', description: '사주 입력 및 AI 해석 서비스', is_active: true, order_index: 1 },
|
||||
{ id: 'lotto', name: '로또 번호 추천', description: '빅데이터 기반 로또 번호 분석', is_active: true, order_index: 2 },
|
||||
{ id: 'stock', name: '주식 자동매매', description: '텔레그램 연동 자동매매 프로그램', is_active: true, order_index: 3 },
|
||||
{ id: 'automation', name: '업무 자동화 RPA', description: '반복 업무 자동화 개발', is_active: true, order_index: 4 },
|
||||
{ id: 'prompt', name: '프롬프트 엔지니어링', description: 'AI 프롬프트 설계 서비스', is_active: true, order_index: 5 },
|
||||
{ id: 'freelance', name: '외주 개발', description: '맞춤형 소프트웨어 개발', is_active: true, order_index: 6 },
|
||||
{ id: 'saju', name: 'AI 사주 분석', description: '사주 입력 및 AI 해석 (레거시)', is_active: false, order_index: 101 },
|
||||
{ id: 'music', name: 'AI 음악 팩', description: '음악 가이드 패키지·샘플·스튜디오', is_active: false, order_index: 102 },
|
||||
{ id: 'gyeol', name: 'CONTOUR 설문', description: '/gyeol PMF 설문', is_active: false, order_index: 103 },
|
||||
{ id: 'packages', name: 'SaaS 제품 허브(구)', description: '구 /packages 페이지', is_active: false, order_index: 104 },
|
||||
{ id: 'lotto', name: '로또 추천', description: '로또 번호 추천 노출', is_active: false, order_index: 105 },
|
||||
];
|
||||
|
||||
@@ -55,11 +55,14 @@ function ContactFormInner() {
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
const fieldClass =
|
||||
'w-full px-3.5 py-2.5 text-sm border rounded-xl outline-none bg-white disabled:bg-slate-50 transition-colors focus:ring-2 focus:ring-[var(--jsm-accent)] focus:border-[var(--jsm-accent)]';
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||
<label className="block text-xs font-semibold mb-1.5" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
이름 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -70,11 +73,12 @@ function ContactFormInner() {
|
||||
required
|
||||
disabled={status === 'loading'}
|
||||
placeholder="홍길동"
|
||||
className="w-full px-3.5 py-2.5 text-sm border border-[#dbe8ff] rounded-xl focus:ring-2 focus:ring-[#1a56db] focus:border-[#1a56db] outline-none bg-white disabled:bg-slate-50"
|
||||
className={fieldClass}
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">연락처</label>
|
||||
<label className="block text-xs font-semibold mb-1.5" style={{ color: 'var(--jsm-ink-soft)' }}>연락처</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
@@ -82,13 +86,14 @@ function ContactFormInner() {
|
||||
onChange={handleChange}
|
||||
disabled={status === 'loading'}
|
||||
placeholder="010-0000-0000"
|
||||
className="w-full px-3.5 py-2.5 text-sm border border-[#dbe8ff] rounded-xl focus:ring-2 focus:ring-[#1a56db] focus:border-[#1a56db] outline-none bg-white disabled:bg-slate-50"
|
||||
className={fieldClass}
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||
<label className="block text-xs font-semibold mb-1.5" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
이메일 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -99,18 +104,20 @@ function ContactFormInner() {
|
||||
required
|
||||
disabled={status === 'loading'}
|
||||
placeholder="example@email.com"
|
||||
className="w-full px-3.5 py-2.5 text-sm border border-[#dbe8ff] rounded-xl focus:ring-2 focus:ring-[#1a56db] focus:border-[#1a56db] outline-none bg-white disabled:bg-slate-50"
|
||||
className={fieldClass}
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">문의 서비스</label>
|
||||
<label className="block text-xs font-semibold mb-1.5" style={{ color: 'var(--jsm-ink-soft)' }}>문의 서비스</label>
|
||||
<select
|
||||
name="service"
|
||||
value={formData.service}
|
||||
onChange={handleChange}
|
||||
disabled={status === 'loading'}
|
||||
className="w-full px-3.5 py-2.5 text-sm border border-[#dbe8ff] rounded-xl focus:ring-2 focus:ring-[#1a56db] focus:border-[#1a56db] outline-none bg-white disabled:bg-slate-50"
|
||||
className={fieldClass}
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<option>외주 개발 문의</option>
|
||||
<option>AI 자동화 키트 - 월 구독</option>
|
||||
@@ -125,7 +132,7 @@ function ContactFormInner() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||
<label className="block text-xs font-semibold mb-1.5" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
문의 내용 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
@@ -136,31 +143,33 @@ function ContactFormInner() {
|
||||
rows={5}
|
||||
disabled={status === 'loading'}
|
||||
placeholder="문의하실 내용을 자유롭게 작성해주세요. 프로젝트 목적, 원하시는 기능, 예산 등을 적어주시면 더 정확한 답변이 가능합니다."
|
||||
className="w-full px-3.5 py-2.5 text-sm border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none bg-white disabled:bg-slate-50"
|
||||
className={`${fieldClass} resize-none`}
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="bg-emerald-50 border border-emerald-200 text-emerald-800 text-sm px-4 py-3 rounded-xl">
|
||||
✅ 문의가 전송되었습니다! 24시간 이내 답변드리겠습니다.
|
||||
문의가 전송되었습니다. 영업일 2일 내에 회신드리겠습니다.
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 text-sm px-4 py-3 rounded-xl">
|
||||
❌ {errorMessage}
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className="w-full bg-[#1a56db] hover:bg-[#1e4fc2] text-white py-3 rounded-xl text-sm font-bold transition shadow-lg shadow-blue-900/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full text-white py-3 rounded-xl text-sm font-bold transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
>
|
||||
{status === 'loading' ? '전송 중...' : '문의 보내기'}
|
||||
</button>
|
||||
|
||||
<p className="text-slate-400 text-xs text-center">
|
||||
문의 후 24시간 이내 답변 보장 · 무료 상담 가능
|
||||
<p className="text-xs text-center" style={{ color: 'var(--jsm-ink-faint)' }}>
|
||||
영업일 2일 내 회신 · 무료 상담 가능
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -7,111 +7,147 @@ export default function PublicShell({ children }: { children: React.ReactNode })
|
||||
<>
|
||||
<TopNav />
|
||||
<main
|
||||
className="min-h-screen pt-20"
|
||||
className="min-h-screen pt-16"
|
||||
style={{
|
||||
background: 'var(--kx-surface)',
|
||||
color: 'var(--kx-on-surface)',
|
||||
background: 'var(--jsm-bg)',
|
||||
color: 'var(--jsm-ink)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<footer className="bg-black text-white/70 px-6 lg:px-12 py-14 text-sm border-t border-white/10">
|
||||
<footer
|
||||
className="text-white/70 px-6 lg:px-12 py-14 text-sm"
|
||||
style={{ background: 'var(--jsm-navy)' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-12 md:gap-8">
|
||||
{/* 좌 — JSM + social */}
|
||||
{/* 좌 — JSM + 연락처 */}
|
||||
<div>
|
||||
<p
|
||||
className="kx-display font-bold text-2xl mb-5 text-white tracking-tight"
|
||||
style={{ letterSpacing: '0.02em' }}
|
||||
>
|
||||
JSM
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="https://www.youtube.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="YouTube"
|
||||
className="w-9 h-9 rounded-full border border-white/20 hover:border-white hover:bg-white hover:text-black text-white flex items-center justify-center transition"
|
||||
<div className="flex items-baseline gap-2 mb-4">
|
||||
<span
|
||||
className="font-black text-2xl text-white"
|
||||
style={{ letterSpacing: '-0.02em' }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M23.5 6.2a3 3 0 0 0-2.1-2.1C19.5 3.6 12 3.6 12 3.6s-7.5 0-9.4.5A3 3 0 0 0 .5 6.2C0 8.1 0 12 0 12s0 3.9.5 5.8a3 3 0 0 0 2.1 2.1c1.9.5 9.4.5 9.4.5s7.5 0 9.4-.5a3 3 0 0 0 2.1-2.1c.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8zM9.6 15.6V8.4l6.2 3.6-6.2 3.6z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://x.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="X (Twitter)"
|
||||
className="w-9 h-9 rounded-full border border-white/20 hover:border-white hover:bg-white hover:text-black text-white flex items-center justify-center transition"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Instagram"
|
||||
className="w-9 h-9 rounded-full border border-white/20 hover:border-white hover:bg-white hover:text-black text-white flex items-center justify-center transition"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="mailto:bgg8988@gmail.com"
|
||||
aria-label="Email"
|
||||
className="w-9 h-9 rounded-full border border-white/20 hover:border-white hover:bg-white hover:text-black text-white flex items-center justify-center transition"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m3 7 9 6 9-6" />
|
||||
</svg>
|
||||
</a>
|
||||
JSM
|
||||
</span>
|
||||
<span className="text-sm text-white/50" style={{ letterSpacing: '-0.01em' }}>
|
||||
쟁승메이드
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
href="mailto:bgg8988@gmail.com"
|
||||
className="flex items-center gap-2 text-white/50 hover:text-white transition-colors duration-150 text-sm"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m3 7 9 6 9-6" />
|
||||
</svg>
|
||||
bgg8988@gmail.com
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* 우 — Link groups */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-10">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-10">
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">SaaS 제품</p>
|
||||
<p
|
||||
className="text-[11px] tracking-widest uppercase text-white/40 mb-4 font-medium"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
서비스
|
||||
</p>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/packages" className="hover:text-white transition">제품 카탈로그</Link></li>
|
||||
<li><Link href="/packages" className="hover:text-white transition">출시 알림 신청</Link></li>
|
||||
<li>
|
||||
<Link
|
||||
href="/outsourcing"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
외주 개발
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/products"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
소프트웨어
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">AI 음악</p>
|
||||
<p
|
||||
className="text-[11px] tracking-widest uppercase text-white/40 mb-4 font-medium"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
회사
|
||||
</p>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/music/packs" className="hover:text-white transition">음악 가이드 패키지</Link></li>
|
||||
<li><Link href="/music/samples" className="hover:text-white transition">샘플 갤러리</Link></li>
|
||||
<li><Link href="/music/packs#pricing" className="hover:text-white transition">가격</Link></li>
|
||||
<li>
|
||||
<a
|
||||
href="mailto:bgg8988@gmail.com"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
문의하기
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/outsourcing#process"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
진행 프로세스
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">커스텀 외주</p>
|
||||
<p
|
||||
className="text-[11px] tracking-widest uppercase text-white/40 mb-4 font-medium"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
Legal
|
||||
</p>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/work/freelance" className="hover:text-white transition">외주 개발</Link></li>
|
||||
<li><Link href="/work/website" className="hover:text-white transition">웹사이트 제작</Link></li>
|
||||
<li><Link href="/work/saju" className="hover:text-white transition">AI 사주</Link></li>
|
||||
<li><a href="mailto:bgg8988@gmail.com" className="hover:text-white transition">문의하기</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">Legal</p>
|
||||
<ul className="space-y-2.5">
|
||||
<li><Link href="/legal/terms" className="hover:text-white transition">이용약관</Link></li>
|
||||
<li><Link href="/legal/privacy" className="hover:text-white transition">개인정보처리방침</Link></li>
|
||||
<li><Link href="/legal/refund" className="hover:text-white transition">환불 정책</Link></li>
|
||||
<li>
|
||||
<Link
|
||||
href="/legal/terms"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
이용약관
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/legal/privacy"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
개인정보처리방침
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/legal/refund"
|
||||
className="hover:text-white transition-colors duration-150"
|
||||
style={{ letterSpacing: '-0.01em' }}
|
||||
>
|
||||
환불 정책
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-6 border-t border-white/10 flex flex-wrap gap-x-4 gap-y-1 text-xs text-white/40 leading-relaxed">
|
||||
<div
|
||||
className="mt-12 pt-6 border-t flex flex-wrap gap-x-4 gap-y-1 text-xs text-white/40 leading-relaxed"
|
||||
style={{ borderColor: 'rgba(255,255,255,0.08)' }}
|
||||
>
|
||||
<span>대표자: 박재오</span>
|
||||
<span>사업자등록번호: 267-53-00822</span>
|
||||
<span>서울시 동작구 여의대방로22아길 22, 1동 109호</span>
|
||||
|
||||
@@ -7,9 +7,8 @@ import { createClient } from '@/lib/supabase/client';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
const LINKS = [
|
||||
{ href: '/packages', label: 'SaaS 제품' },
|
||||
{ href: '/music', label: 'AI 음악' },
|
||||
{ href: '/work', label: '커스텀 외주' },
|
||||
{ href: '/outsourcing', label: '외주 개발' },
|
||||
{ href: '/products', label: '소프트웨어' },
|
||||
];
|
||||
|
||||
export default function TopNav() {
|
||||
@@ -59,6 +58,13 @@ export default function TopNav() {
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [open]);
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') return pathname === '/';
|
||||
return pathname === href || pathname.startsWith(href + '/');
|
||||
@@ -67,187 +73,222 @@ export default function TopNav() {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={[
|
||||
'fixed left-1/2 -translate-x-1/2 z-50 w-full border-b border-transparent',
|
||||
'md:rounded-full md:border transition-all duration-300 ease-out',
|
||||
scrolled
|
||||
? 'top-4 max-w-3xl md:shadow-[0_10px_40px_rgba(0,0,0,0.35)] md:border-white/10'
|
||||
: 'top-0 max-w-none',
|
||||
].join(' ')}
|
||||
className="fixed top-0 left-0 right-0 z-50 w-full transition-all duration-300"
|
||||
style={{
|
||||
background: scrolled ? 'rgba(10,10,12,0.6)' : 'transparent',
|
||||
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||
background: scrolled ? 'var(--jsm-surface)' : 'transparent',
|
||||
borderBottom: scrolled ? '1px solid var(--jsm-line)' : '1px solid transparent',
|
||||
boxShadow: scrolled ? '0 1px 8px rgba(15,23,42,0.06)' : 'none',
|
||||
}}
|
||||
>
|
||||
<nav
|
||||
className={[
|
||||
'flex w-full items-center justify-between transition-all duration-300 ease-out',
|
||||
scrolled ? 'h-14 px-4 md:px-3' : 'h-20 px-6 lg:px-12',
|
||||
].join(' ')}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="kx-display text-2xl font-black tracking-tight kx-gradient-text"
|
||||
style={{ textDecoration: 'none', letterSpacing: '0.02em' }}
|
||||
>
|
||||
JSM
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{LINKS.map((l) => (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
className="text-sm font-medium transition-colors"
|
||||
style={{
|
||||
color: isActive(l.href) ? '#fff' : 'var(--kx-on-variant)',
|
||||
borderBottom: isActive(l.href) ? '2px solid var(--kx-primary)' : '2px solid transparent',
|
||||
paddingBottom: 4,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{user ? (
|
||||
<>
|
||||
<Link
|
||||
href="/mypage"
|
||||
className="hidden sm:inline-block text-sm font-medium px-4 py-2 transition-colors"
|
||||
style={{ color: 'var(--kx-on-variant)', textDecoration: 'none' }}
|
||||
>
|
||||
마이페이지
|
||||
</Link>
|
||||
<Link
|
||||
href="/music"
|
||||
className="kx-btn-primary hidden sm:inline-flex items-center px-5 py-2 rounded-full text-sm"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
Try now
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="hidden sm:inline-flex items-center px-3 py-2 text-sm font-medium transition-colors"
|
||||
style={{ color: 'var(--kx-on-variant)', background: 'transparent' }}
|
||||
>
|
||||
로그아웃
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
className="hidden sm:inline-block text-sm font-medium px-4 py-2 transition-colors"
|
||||
style={{ color: 'var(--kx-on-variant)', textDecoration: 'none' }}
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
<Link
|
||||
href="/music"
|
||||
className="kx-btn-primary hidden sm:inline-flex items-center px-5 py-2 rounded-full text-sm"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
Try now
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="메뉴 열기"
|
||||
className="md:hidden p-2 rounded-lg"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
<nav className="max-w-7xl mx-auto flex w-full items-center justify-between h-16 px-6 lg:px-8">
|
||||
{/* 로고 */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-baseline gap-2"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* 모바일 오버레이 */}
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] md:hidden flex flex-col"
|
||||
style={{ background: 'rgba(6,14,32,0.98)', backdropFilter: 'blur(16px)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 h-20">
|
||||
<span className="kx-display text-2xl font-black kx-gradient-text" style={{ letterSpacing: '0.02em' }}>JSM</span>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="메뉴 닫기"
|
||||
className="p-2"
|
||||
style={{ color: 'var(--kx-on-surface)' }}
|
||||
<span
|
||||
className="text-xl font-black tracking-tight"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2 px-6 pt-6">
|
||||
JSM
|
||||
</span>
|
||||
<span
|
||||
className="hidden sm:inline text-sm font-medium"
|
||||
style={{ color: 'var(--jsm-ink-soft)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
쟁승메이드
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* 데스크탑 링크 */}
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{LINKS.map((l) => (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
className="kx-display text-2xl font-bold py-3"
|
||||
className="text-sm font-medium px-4 py-2 rounded-md transition-colors duration-150"
|
||||
style={{
|
||||
color: isActive(l.href) ? 'var(--kx-primary)' : 'var(--kx-on-surface)',
|
||||
color: isActive(l.href) ? 'var(--jsm-accent)' : 'var(--jsm-ink-soft)',
|
||||
background: isActive(l.href) ? 'var(--jsm-accent-soft)' : 'transparent',
|
||||
textDecoration: 'none',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="mt-6 flex flex-col gap-2">
|
||||
</div>
|
||||
|
||||
{/* 데스크탑 CTA + auth */}
|
||||
<div className="flex items-center gap-2">
|
||||
{user ? (
|
||||
<>
|
||||
<Link
|
||||
href="/mypage"
|
||||
className="hidden sm:inline-block text-sm font-medium px-3 py-2 rounded-md transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', textDecoration: 'none', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
마이페이지
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="hidden sm:inline-flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', background: 'transparent', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
로그아웃
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="hidden sm:inline-block text-sm font-medium px-3 py-2 rounded-md transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', textDecoration: 'none', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="hidden sm:inline-flex items-center px-4 py-2 rounded-lg text-sm font-semibold transition-colors duration-150 hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{
|
||||
background: 'var(--jsm-accent)',
|
||||
color: '#ffffff',
|
||||
textDecoration: 'none',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
프로젝트 문의
|
||||
</Link>
|
||||
|
||||
{/* 모바일 햄버거 */}
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="메뉴 열기"
|
||||
aria-expanded={open}
|
||||
className="md:hidden p-2 rounded-lg transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink)' }}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* 모바일 드로어 */}
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] md:hidden"
|
||||
style={{ background: 'rgba(15,23,42,0.4)' }}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 right-0 h-full w-72 flex flex-col shadow-xl"
|
||||
style={{ background: 'var(--jsm-surface)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="메뉴"
|
||||
>
|
||||
{/* 드로어 헤더 */}
|
||||
<div
|
||||
className="flex items-center justify-between px-6 h-16 border-b"
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span
|
||||
className="text-lg font-black tracking-tight"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
JSM
|
||||
</span>
|
||||
<span
|
||||
className="text-xs font-medium"
|
||||
style={{ color: 'var(--jsm-ink-soft)' }}
|
||||
>
|
||||
쟁승메이드
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="메뉴 닫기"
|
||||
className="p-2 rounded-lg transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)' }}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 드로어 링크 */}
|
||||
<div className="flex-1 flex flex-col px-4 pt-4 gap-1">
|
||||
{LINKS.map((l) => (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
className="text-base font-semibold px-3 py-3 rounded-lg transition-colors duration-150"
|
||||
style={{
|
||||
color: isActive(l.href) ? 'var(--jsm-accent)' : 'var(--jsm-ink)',
|
||||
background: isActive(l.href) ? 'var(--jsm-accent-soft)' : 'transparent',
|
||||
textDecoration: 'none',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div
|
||||
className="my-4 border-t"
|
||||
style={{ borderColor: 'var(--jsm-line)' }}
|
||||
/>
|
||||
|
||||
{user ? (
|
||||
<>
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href="/mypage"
|
||||
className="flex-1 py-3 text-center rounded-full text-sm font-bold"
|
||||
style={{ border: '1px solid rgba(255,255,255,0.15)', color: 'var(--kx-on-surface)', textDecoration: 'none' }}
|
||||
>
|
||||
마이페이지
|
||||
</Link>
|
||||
<Link
|
||||
href="/music"
|
||||
className="kx-btn-primary flex-1 py-3 text-center rounded-full text-sm"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
Try now
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href="/mypage"
|
||||
className="text-sm font-medium px-3 py-3 rounded-lg transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', textDecoration: 'none', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
마이페이지
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full py-3 text-center text-sm font-medium transition-colors"
|
||||
style={{ color: 'var(--kx-on-variant)', background: 'transparent' }}
|
||||
className="text-left text-sm font-medium px-3 py-3 rounded-lg transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', background: 'transparent', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
로그아웃
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex-1 py-3 text-center rounded-full text-sm font-bold"
|
||||
style={{ border: '1px solid rgba(255,255,255,0.15)', color: 'var(--kx-on-surface)', textDecoration: 'none' }}
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
<Link
|
||||
href="/music"
|
||||
className="kx-btn-primary flex-1 py-3 text-center rounded-full text-sm"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
Try now
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-sm font-medium px-3 py-3 rounded-lg transition-colors duration-150"
|
||||
style={{ color: 'var(--jsm-ink-soft)', textDecoration: 'none', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 드로어 하단 CTA */}
|
||||
<div className="px-4 pb-6">
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="flex items-center justify-center w-full py-3 rounded-lg text-sm font-semibold transition-colors duration-150"
|
||||
style={{
|
||||
background: 'var(--jsm-accent)',
|
||||
color: '#ffffff',
|
||||
textDecoration: 'none',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
프로젝트 문의
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -36,19 +36,32 @@
|
||||
--card-bg: #ffffff;
|
||||
--border: #dbe8ff;
|
||||
|
||||
/* ─── Kinetic Ether Tokens (다크 테마 섹션 전용) ─── */
|
||||
--kx-surface: #060e20;
|
||||
--kx-surface-low: #091328;
|
||||
--kx-surface-mid: #0f1930;
|
||||
--kx-surface-high: #141f38;
|
||||
--kx-surface-bright: #1f2b49;
|
||||
--kx-on-surface: #dee5ff;
|
||||
--kx-on-variant: #a3aac4;
|
||||
--kx-primary: #cc97ff;
|
||||
--kx-primary-dim: #9c48ea;
|
||||
--kx-secondary: #53ddfc;
|
||||
--kx-secondary-dim: #40ceed;
|
||||
--kx-outline: rgba(64, 72, 93, 0.15);
|
||||
/* === JSM Professional tokens (2026-06 renewal) === */
|
||||
--jsm-bg: #f8fafc; /* slate-50 본문 배경 */
|
||||
--jsm-surface: #ffffff; /* 카드 */
|
||||
--jsm-surface-alt: #f1f5f9; /* slate-100 섹션 교차 배경 */
|
||||
--jsm-ink: #0f172a; /* slate-900 본문 텍스트 */
|
||||
--jsm-ink-soft: #475569; /* slate-600 보조 텍스트 */
|
||||
--jsm-ink-faint: #94a3b8; /* slate-400 캡션 */
|
||||
--jsm-line: #e2e8f0; /* slate-200 보더 */
|
||||
--jsm-navy: #0b1f3a; /* 딥네이비 — 푸터/다크 섹션 */
|
||||
--jsm-accent: #1d4ed8; /* blue-700 포인트 (단일 포인트 컬러) */
|
||||
--jsm-accent-hover: #1e40af; /* blue-800 */
|
||||
--jsm-accent-soft: #dbeafe; /* blue-100 뱃지 배경 */
|
||||
|
||||
/* 기존 kx 변수 재매핑 (잔여 참조 호환용) */
|
||||
--kx-surface: var(--jsm-bg);
|
||||
--kx-surface-low: var(--jsm-surface-alt);
|
||||
--kx-surface-mid: var(--jsm-surface);
|
||||
--kx-surface-high: var(--jsm-surface);
|
||||
--kx-surface-bright: var(--jsm-surface-alt);
|
||||
--kx-on-surface: var(--jsm-ink);
|
||||
--kx-on-variant: var(--jsm-ink-soft);
|
||||
--kx-primary: var(--jsm-accent);
|
||||
--kx-primary-dim: var(--jsm-accent-hover);
|
||||
--kx-secondary: var(--jsm-accent);
|
||||
--kx-secondary-dim: var(--jsm-accent-hover);
|
||||
--kx-outline: var(--jsm-line);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -56,8 +69,8 @@
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-secondary: var(--secondary);
|
||||
--font-sans: var(--font-jua), 'Jua', -apple-system, system-ui, sans-serif;
|
||||
--font-mono: var(--font-jua), 'Jua', ui-monospace, monospace;
|
||||
--font-sans: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
--font-mono: 'Pretendard Variable', Pretendard, ui-monospace, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -69,16 +82,16 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-jua), 'Jua', -apple-system, system-ui, sans-serif;
|
||||
background: var(--jsm-bg);
|
||||
color: var(--jsm-ink);
|
||||
font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* font-mono utility → Jua 통일 */
|
||||
/* font-mono utility → Pretendard 통일 */
|
||||
.font-mono, code, pre, kbd, samp {
|
||||
font-family: var(--font-jua), 'Jua', ui-monospace, monospace;
|
||||
font-family: 'Pretendard Variable', Pretendard, ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Dashboard layout */
|
||||
@@ -86,7 +99,7 @@ body {
|
||||
display: flex;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--background);
|
||||
background: var(--jsm-bg);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@@ -107,18 +120,19 @@ body {
|
||||
.kx-section {
|
||||
background: var(--kx-surface);
|
||||
color: var(--kx-on-surface);
|
||||
font-family: var(--font-jua), 'Jua', system-ui, sans-serif;
|
||||
font-family: 'Pretendard Variable', Pretendard, system-ui, sans-serif;
|
||||
}
|
||||
.kx-section p, .kx-section li, .kx-section span:not(.kx-label) {
|
||||
color: var(--kx-on-variant);
|
||||
}
|
||||
.kx-display {
|
||||
font-family: var(--font-jua), 'Jua', system-ui, sans-serif;
|
||||
letter-spacing: -0.01em;
|
||||
font-family: 'Pretendard Variable', Pretendard, system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: inherit;
|
||||
}
|
||||
.kx-label {
|
||||
font-family: var(--font-jua), 'Jua', system-ui, sans-serif;
|
||||
font-family: 'Pretendard Variable', Pretendard, system-ui, sans-serif;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -147,15 +161,14 @@ body {
|
||||
0 0 80px 0 rgba(83, 221, 252, 0.08);
|
||||
}
|
||||
.kx-btn-primary {
|
||||
background: linear-gradient(135deg, #cc97ff 0%, #c284ff 100%);
|
||||
color: #0b0113;
|
||||
background: var(--jsm-accent);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 20px 0 rgba(168, 85, 247, 0.4);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.kx-btn-primary:hover {
|
||||
background: var(--jsm-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 28px 0 rgba(168, 85, 247, 0.55);
|
||||
}
|
||||
.kx-btn-ghost {
|
||||
color: var(--kx-secondary);
|
||||
@@ -166,10 +179,8 @@ body {
|
||||
background: var(--kx-surface-bright);
|
||||
}
|
||||
.kx-gradient-text {
|
||||
background: linear-gradient(135deg, #cc97ff 0%, #53ddfc 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: var(--jsm-ink);
|
||||
-webkit-text-fill-color: var(--jsm-ink);
|
||||
}
|
||||
.kx-orb {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { isServiceVisible } from '@/lib/service-visibility';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'CONTOUR — 나를 더 선명하게 이해하는 3분',
|
||||
@@ -22,7 +24,8 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function GyeolLayout({ children }: { children: React.ReactNode }) {
|
||||
export default async function GyeolLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!(await isServiceVisible('gyeol'))) notFound();
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { Jua } from "next/font/google";
|
||||
import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
|
||||
import "./globals.css";
|
||||
import DashboardShell from "./components/DashboardShell";
|
||||
import { GlassFilter } from "./components/LiquidGlass";
|
||||
|
||||
const jua = Jua({
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jua",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "AI 음악·뮤비 팩 ₩39,000~ | 쟁승메이드",
|
||||
default: "외주 개발 · 완성 소프트웨어 | 쟁승메이드",
|
||||
template: "%s | 쟁승메이드",
|
||||
},
|
||||
description:
|
||||
"Suno 프롬프트 + 뮤직비디오 워크플로우 + 유튜브 SEO 템플릿 팩. AI로 음악과 뮤비를 1시간 만에 완성하는 4단계 크리에이터 툴킷. ₩39,000부터.",
|
||||
"7년차 대기업 백엔드 개발자가 직접 설계하고 만듭니다. 맞춤 소프트웨어 외주 개발과 검증된 완성 소프트웨어를 제공하는 쟁승메이드.",
|
||||
keywords: [
|
||||
"AI 음악",
|
||||
"AI 작곡",
|
||||
"Suno 프롬프트",
|
||||
"AI 뮤직비디오",
|
||||
"유튜브 쇼츠 음악",
|
||||
"AI 뮤비",
|
||||
"음악 프롬프트",
|
||||
"AI 사주",
|
||||
"외주 개발",
|
||||
"소프트웨어 개발",
|
||||
"웹사이트 제작",
|
||||
"업무 자동화",
|
||||
"백엔드 개발자",
|
||||
"프리랜서 개발자",
|
||||
],
|
||||
authors: [{ name: "박재오", url: "https://jaengseung-made.com" }],
|
||||
creator: "박재오",
|
||||
@@ -36,22 +26,23 @@ export const metadata: Metadata = {
|
||||
locale: "ko_KR",
|
||||
url: "https://jaengseung-made.com",
|
||||
siteName: "쟁승메이드",
|
||||
title: "AI 음악·뮤비 팩 ₩39,000~ | 쟁승메이드",
|
||||
title: "외주 개발 · 완성 소프트웨어 | 쟁승메이드",
|
||||
description:
|
||||
"Suno 프롬프트 + 뮤비 워크플로우 + 유튜브 SEO 템플릿 팩. AI로 음악·뮤비를 1시간에 완성하는 4단계 크리에이터 툴킷.",
|
||||
"7년차 대기업 백엔드 개발자가 직접 설계·개발·운영합니다. 맞춤 외주 개발과 검증된 완성 소프트웨어를 제공하는 쟁승메이드.",
|
||||
images: [
|
||||
{
|
||||
url: "https://jaengseung-made.com/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "쟁승메이드 — AI 프롬프트 · 자동화 스토어",
|
||||
alt: "쟁승메이드 — 외주 개발 · 완성 소프트웨어",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "AI 음악·뮤비 팩 ₩39,000~ | 쟁승메이드",
|
||||
description: "AI로 음악과 뮤비를 1시간 만에. Suno 프롬프트 + 뮤비 워크플로우 + 유튜브 SEO 템플릿.",
|
||||
title: "외주 개발 · 완성 소프트웨어 | 쟁승메이드",
|
||||
description:
|
||||
"7년차 대기업 백엔드 개발자가 직접 만듭니다. 맞춤 외주 개발과 검증된 완성 소프트웨어를 제공합니다.",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -68,39 +59,35 @@ const jsonLd = {
|
||||
'@id': 'https://jaengseung-made.com/#person',
|
||||
name: '박재오',
|
||||
url: 'https://jaengseung-made.com',
|
||||
jobTitle: '백엔드 개발자 · AI 자동화 전문가',
|
||||
jobTitle: '백엔드 개발자 · 외주 개발 전문가',
|
||||
worksFor: { '@type': 'Organization', name: '대기업 재직 중' },
|
||||
email: 'bgg8988@gmail.com',
|
||||
telephone: '010-3907-1392',
|
||||
knowsAbout: ['Python', 'Java', 'Spring Boot', 'Next.js', 'AI 프롬프트', 'AI 자동화', '업무 자동화', 'ChatGPT', 'Claude'],
|
||||
description: '현직 엔지니어. AI 음악 생성 개발 가이드 패키지, AI 사주 분석 등 AI 크리에이티브 도구를 직접 개발·운영합니다.',
|
||||
knowsAbout: ['Python', 'Java', 'Spring Boot', 'Next.js', '외주 개발', '웹사이트 제작', '업무 자동화', 'API 설계'],
|
||||
description: '7년차 대기업 백엔드 개발자. 맞춤 소프트웨어 외주 개발과 검증된 완성 소프트웨어를 직접 설계·개발·운영합니다.',
|
||||
},
|
||||
{
|
||||
'@type': 'LocalBusiness',
|
||||
'@id': 'https://jaengseung-made.com/#business',
|
||||
name: '쟁승메이드',
|
||||
url: 'https://jaengseung-made.com',
|
||||
description: 'AI 음악 생성 개발 가이드 패키지, AI 사주 분석. 현직 엔지니어가 직접 설계·운영하는 AI 크리에이티브 스토어.',
|
||||
description: '7년차 대기업 백엔드 개발자가 직접 설계·개발·운영하는 외주 개발 · 완성 소프트웨어 스토어.',
|
||||
email: 'bgg8988@gmail.com',
|
||||
telephone: '010-3907-1392',
|
||||
priceRange: '₩',
|
||||
areaServed: '대한민국',
|
||||
hasOfferCatalog: {
|
||||
'@type': 'OfferCatalog',
|
||||
name: '쟁승메이드 AI 도구 · 서비스',
|
||||
name: '쟁승메이드 개발 서비스',
|
||||
itemListElement: [
|
||||
{ '@type': 'Offer', price: '39000', priceCurrency: 'KRW', availability: 'https://schema.org/InStock', url: 'https://jaengseung-made.com/music/packs', itemOffered: { '@type': 'Product', name: 'AI 음악 생성 개발 가이드 패키지 (입문)', url: 'https://jaengseung-made.com/music/packs', description: 'Suno 프롬프트 조합법 + MV 워크플로우 + 저작권 가이드 + 템플릿 PDF + 샘플 프로젝트. AI 음악 생성 개발 가이드 (1회 결제).' } },
|
||||
{ '@type': 'Offer', price: '99000', priceCurrency: 'KRW', availability: 'https://schema.org/InStock', url: 'https://jaengseung-made.com/music/packs', itemOffered: { '@type': 'Product', name: 'AI 음악 생성 개발 가이드 패키지 (프로)', url: 'https://jaengseung-made.com/music/packs', description: '입문 전체 + 샘플 프로젝트 1개(.prj · 영상 포함). 1회 결제.' } },
|
||||
{ '@type': 'Offer', price: '149000', priceCurrency: 'KRW', availability: 'https://schema.org/InStock', url: 'https://jaengseung-made.com/music/packs', itemOffered: { '@type': 'Product', name: 'AI 음악 생성 개발 가이드 패키지 (마스터)', url: 'https://jaengseung-made.com/music/packs', description: '프로 전체 + 샘플 다수 + 우선 업데이트·베타 선공개. 1회 결제.' } },
|
||||
{ '@type': 'Offer', price: '0', priceCurrency: 'KRW', url: 'https://jaengseung-made.com/work/saju', itemOffered: { '@type': 'Service', name: 'AI 사주 분석', url: 'https://jaengseung-made.com/work/saju', description: '생년월일 기반 AI 사주팔자 분석. 무료 체험 가능.' } },
|
||||
{
|
||||
'@type': 'Offer',
|
||||
url: 'https://jaengseung-made.com/work/freelance',
|
||||
url: 'https://jaengseung-made.com/outsourcing',
|
||||
availability: 'https://schema.org/InStock',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: '외주 개발',
|
||||
url: 'https://jaengseung-made.com/work/freelance',
|
||||
url: 'https://jaengseung-made.com/outsourcing',
|
||||
description: '7년차 백엔드 개발자의 1:1 맞춤 소프트웨어 개발 외주. 자동화·API·웹/모바일 등 사이트 한정가로 제공.',
|
||||
serviceType: 'Custom Software Development',
|
||||
provider: { '@id': 'https://jaengseung-made.com/#business' },
|
||||
@@ -109,12 +96,12 @@ const jsonLd = {
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
url: 'https://jaengseung-made.com/work/website',
|
||||
url: 'https://jaengseung-made.com/outsourcing',
|
||||
availability: 'https://schema.org/InStock',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: '웹사이트 제작',
|
||||
url: 'https://jaengseung-made.com/work/website',
|
||||
url: 'https://jaengseung-made.com/outsourcing',
|
||||
description: 'Next.js 기반 기업·브랜드 웹사이트 맞춤 제작. 반응형 + SEO + 배포 포함.',
|
||||
serviceType: 'Web Development',
|
||||
provider: { '@id': 'https://jaengseung-made.com/#business' },
|
||||
@@ -133,7 +120,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ko" data-scroll-behavior="smooth" className={jua.variable}>
|
||||
<html lang="ko" data-scroll-behavior="smooth">
|
||||
<head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
@@ -156,7 +143,6 @@ export default function RootLayout({
|
||||
</Script>
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<GlassFilter />
|
||||
<DashboardShell>{children}</DashboardShell>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const metadata: Metadata = {
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">개인정보처리방침</h1>
|
||||
<h1 className="text-2xl font-extrabold mb-8" style={{ color: 'var(--jsm-ink)' }}>개인정보처리방침</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<p>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const metadata: Metadata = {
|
||||
export default function RefundPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">환불 정책</h1>
|
||||
<h1 className="text-2xl font-extrabold mb-8" style={{ color: 'var(--jsm-ink)' }}>환불 정책</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<p>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const metadata: Metadata = {
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">이용약관</h1>
|
||||
<h1 className="text-2xl font-extrabold mb-8" style={{ color: 'var(--jsm-ink)' }}>이용약관</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<section>
|
||||
|
||||
@@ -73,117 +73,77 @@ function LoginForm() {
|
||||
if (error) setMessage('Google 로그인 오류: ' + error.message);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#04102b] flex items-center justify-center p-4">
|
||||
{/* 배경 장식 */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}
|
||||
/>
|
||||
const isSuccess =
|
||||
message.includes('완료') || message.includes('확인해주세요');
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* 로고 */}
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center px-4 py-12"
|
||||
style={{ background: 'var(--jsm-bg)' }}
|
||||
>
|
||||
<div className="w-full max-w-sm">
|
||||
{/* 워드마크 */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center gap-3 group">
|
||||
<div className="w-12 h-12 rounded-xl bg-[#1a56db] flex items-center justify-center text-white font-bold text-xl">
|
||||
쟁
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-white font-bold text-xl leading-tight">쟁승메이드</div>
|
||||
<div className="text-blue-400 text-xs font-medium">박재오의 개발 공방</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-block"
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
fontSize: '1.375rem',
|
||||
letterSpacing: '-0.03em',
|
||||
color: 'var(--jsm-ink)',
|
||||
transition: 'color 0.15s',
|
||||
}}
|
||||
>
|
||||
쟁승메이드
|
||||
</Link>
|
||||
<p
|
||||
className="mt-2 text-sm break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
{isSignUp
|
||||
? '가입 후 의뢰 현황과 구매 내역을 관리하세요'
|
||||
: '로그인하고 의뢰 현황과 구매 내역을 확인하세요'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 카드 */}
|
||||
<div className="bg-white/5 border border-white/10 backdrop-blur rounded-2xl p-8 shadow-2xl">
|
||||
<div className="text-center mb-7">
|
||||
<h1 className="text-2xl font-extrabold text-white mb-1">
|
||||
{isSignUp ? '회원가입' : '로그인'}
|
||||
</h1>
|
||||
<p className="text-blue-300/60 text-sm">
|
||||
{isSignUp
|
||||
? '가입 후 사주 기록, 결제 내역을 관리하세요'
|
||||
: '사주 기록·결제·의뢰 내역을 확인하세요'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 오류/성공 메시지 */}
|
||||
{message && (
|
||||
<div className={`mb-4 px-4 py-3 rounded-xl text-sm font-medium ${
|
||||
message.includes('완료') || message.includes('확인해주세요')
|
||||
? 'bg-emerald-500/10 border border-emerald-500/30 text-emerald-300'
|
||||
: 'bg-red-500/10 border border-red-500/30 text-red-300'
|
||||
}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이메일/비밀번호 폼 */}
|
||||
<form onSubmit={handleAuth} className="space-y-4 mb-5">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-slate-300 mb-1.5">
|
||||
이메일
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:bg-white/8 transition text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-slate-300 mb-1.5">
|
||||
비밀번호
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="6자 이상"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:bg-white/8 transition text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#1a56db] hover:bg-[#1e4fc2] text-white font-bold py-3 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? '처리 중...' : (isSignUp ? '회원가입' : '로그인')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* 전환 링크 */}
|
||||
<div className="text-center mb-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setIsSignUp(!isSignUp); setMessage(''); }}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition"
|
||||
>
|
||||
{isSignUp ? '이미 계정이 있으신가요? 로그인 →' : '아직 계정이 없으신가요? 회원가입 →'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="relative mb-5">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-white/10" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-3 bg-transparent text-slate-500 text-xs">또는 소셜 로그인</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구글 로그인 */}
|
||||
<button
|
||||
onClick={handleGoogleLogin}
|
||||
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition text-white font-medium text-sm"
|
||||
<div
|
||||
className="rounded-xl p-8"
|
||||
style={{
|
||||
background: 'var(--jsm-surface)',
|
||||
border: '1px solid var(--jsm-line)',
|
||||
boxShadow: '0 1px 4px 0 rgba(15,23,42,0.06), 0 4px 16px 0 rgba(15,23,42,0.04)',
|
||||
}}
|
||||
>
|
||||
{/* 카드 헤더 */}
|
||||
<h1
|
||||
className="text-xl font-bold mb-6 text-center"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
{isSignUp ? '회원가입' : '로그인'}
|
||||
</h1>
|
||||
|
||||
{/* Google 로그인 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
className="w-full flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-lg text-sm font-medium mb-5"
|
||||
style={{
|
||||
background: 'var(--jsm-surface)',
|
||||
border: '1px solid var(--jsm-line)',
|
||||
color: 'var(--jsm-ink)',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--jsm-surface-alt)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--jsm-surface)';
|
||||
}}
|
||||
>
|
||||
{/* Google G 로고 */}
|
||||
<svg className="w-4 h-4 shrink-0" viewBox="0 0 24 24" aria-hidden>
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
@@ -191,12 +151,169 @@ function LoginForm() {
|
||||
</svg>
|
||||
Google로 계속하기
|
||||
</button>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="relative mb-5">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden
|
||||
>
|
||||
<div className="w-full" style={{ borderTop: '1px solid var(--jsm-line)' }} />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span
|
||||
className="px-3 text-xs"
|
||||
style={{ background: 'var(--jsm-surface)', color: 'var(--jsm-ink-faint)' }}
|
||||
>
|
||||
또는
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오류/성공 메시지 */}
|
||||
{message && (
|
||||
<div
|
||||
className="mb-4 px-3.5 py-3 rounded-lg text-sm"
|
||||
style={{
|
||||
background: isSuccess ? '#f0fdf4' : '#fef2f2',
|
||||
border: `1px solid ${isSuccess ? '#bbf7d0' : '#fecaca'}`,
|
||||
color: isSuccess ? '#15803d' : '#dc2626',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이메일/비밀번호 폼 */}
|
||||
<form onSubmit={handleAuth} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="login-email"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
이메일
|
||||
</label>
|
||||
<input
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-3.5 py-2.5 rounded-lg text-sm outline-none focus-visible:ring-2 focus-visible:ring-[var(--jsm-accent)]"
|
||||
style={{
|
||||
background: 'var(--jsm-surface)',
|
||||
border: '1px solid var(--jsm-line)',
|
||||
color: 'var(--jsm-ink)',
|
||||
transition: 'border-color 0.15s',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--jsm-accent)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--jsm-line)';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="login-password"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--jsm-ink)', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
비밀번호
|
||||
</label>
|
||||
<input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="6자 이상 입력해주세요"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="w-full px-3.5 py-2.5 rounded-lg text-sm outline-none focus-visible:ring-2 focus-visible:ring-[var(--jsm-accent)]"
|
||||
style={{
|
||||
background: 'var(--jsm-surface)',
|
||||
border: '1px solid var(--jsm-line)',
|
||||
color: 'var(--jsm-ink)',
|
||||
transition: 'border-color 0.15s',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--jsm-accent)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--jsm-line)';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 rounded-lg text-sm font-semibold mt-1"
|
||||
style={{
|
||||
background: loading ? 'var(--jsm-ink-faint)' : 'var(--jsm-accent)',
|
||||
color: loading ? '#ffffff' : '#ffffff',
|
||||
border: 'none',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
transition: 'background 0.15s, transform 0.15s',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading) (e.currentTarget as HTMLButtonElement).style.background = 'var(--jsm-accent-hover)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading) (e.currentTarget as HTMLButtonElement).style.background = 'var(--jsm-accent)';
|
||||
}}
|
||||
>
|
||||
{loading ? '처리 중...' : isSignUp ? '가입하기' : '로그인'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* 가입/로그인 전환 */}
|
||||
<div className="mt-5 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setIsSignUp(!isSignUp); setMessage(''); }}
|
||||
className="text-sm"
|
||||
style={{
|
||||
color: 'var(--jsm-accent)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
letterSpacing: '-0.01em',
|
||||
transition: 'color 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--jsm-accent-hover)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--jsm-accent)';
|
||||
}}
|
||||
>
|
||||
{isSignUp
|
||||
? '이미 계정이 있으신가요? 로그인'
|
||||
: '계정이 없으신가요? 회원가입'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 홈으로 */}
|
||||
<div className="text-center mt-6">
|
||||
<Link href="/" className="text-slate-500 hover:text-slate-300 text-sm transition">
|
||||
← 홈으로 돌아가기
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--jsm-ink-faint)', transition: 'color 0.15s', letterSpacing: '-0.01em' }}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = 'var(--jsm-ink-soft)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = 'var(--jsm-ink-faint)';
|
||||
}}
|
||||
>
|
||||
홈으로 돌아가기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,8 +324,14 @@ function LoginForm() {
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-[#04102b] flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{ background: 'var(--jsm-bg)' }}
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border-2 border-t-transparent animate-spin"
|
||||
style={{ borderColor: 'var(--jsm-line)', borderTopColor: 'var(--jsm-accent)' }}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<LoginForm />
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { isServiceVisible } from '@/lib/service-visibility';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI 음악 제품',
|
||||
description: 'Suno 프롬프트 + 뮤직비디오 워크플로우 + 유튜브 SEO 템플릿 한 팩에. 1시간 만에 음악·뮤비 완성.',
|
||||
};
|
||||
|
||||
export default function MusicLayout({ children }: { children: React.ReactNode }) {
|
||||
export default async function MusicLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!(await isServiceVisible('music'))) notFound();
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
1458
app/mypage/page.tsx
1458
app/mypage/page.tsx
File diff suppressed because it is too large
Load Diff
539
app/outsourcing/page.tsx
Normal file
539
app/outsourcing/page.tsx
Normal file
@@ -0,0 +1,539 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import ContactForm from '@/app/components/ContactForm';
|
||||
|
||||
// 외주 개발 의뢰 페이지 (서버 컴포넌트)
|
||||
// PublicShell이 TopNav(h-16)·푸터·main 배경을 제공하므로 여기서는 콘텐츠 섹션만 렌더한다.
|
||||
// 메인(/)의 토큰·타이포 패턴(KOR_TIGHT/KOR_BODY)·섹션 리듬과 일관되게 구성한다.
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '외주 개발',
|
||||
description:
|
||||
'7년차 대기업 백엔드 개발자가 직접 진행하는 맞춤 소프트웨어 외주 개발. 웹 서비스, 업무 자동화, API·백엔드, 봇, AI 연동까지 기획부터 납품·하자보수까지 단독으로 책임집니다.',
|
||||
};
|
||||
|
||||
const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const FIELDS = [
|
||||
{
|
||||
t: '웹 서비스 개발',
|
||||
d: '회원·결제·관리자까지, 실제로 굴러가는 서비스를 기획부터 배포까지 만들어 드립니다.',
|
||||
},
|
||||
{
|
||||
t: '웹사이트 제작',
|
||||
d: '기업 소개·포트폴리오·랜딩 페이지를 반응형·SEO까지 갖춰 제작합니다.',
|
||||
},
|
||||
{
|
||||
t: '업무 자동화',
|
||||
d: 'RPA·엑셀 집계·웹 크롤링으로 반복 업무를 사람 손에서 떼어냅니다.',
|
||||
},
|
||||
{
|
||||
t: 'API·백엔드',
|
||||
d: '데이터 모델 설계부터 인증·외부 연동까지 안정적인 서버를 구축합니다.',
|
||||
},
|
||||
{
|
||||
t: '텔레그램·디스코드 봇',
|
||||
d: '알림·명령·자동 응답 봇으로 운영과 커뮤니티 관리를 자동화합니다.',
|
||||
},
|
||||
{
|
||||
t: 'AI 연동 개발',
|
||||
d: 'LLM·생성형 AI를 업무 흐름에 붙여 초안 작성·분류·요약을 자동화합니다.',
|
||||
},
|
||||
];
|
||||
|
||||
const PROCESS = [
|
||||
{ n: '01', t: '무료 상담', d: '요구사항을 함께 정리하고 실현 가능성을 점검합니다. 기획이 안 잡혔어도 괜찮습니다.' },
|
||||
{ n: '02', t: '견적·범위 확정', d: '기능 범위와 일정을 정리해 영업일 2일 내 견적으로 회신드립니다.' },
|
||||
{ n: '03', t: '계약·착수', d: '계약서 체결 후 착수금 30%를 받고 개발을 시작합니다.' },
|
||||
{ n: '04', t: '개발·중간 공유', d: '주 1회 이상 진행 상황을 공유하며 방향을 맞춰 갑니다.' },
|
||||
{ n: '05', t: '납품·검수', d: '완성본을 인도하고 함께 검수합니다. 전체 소스와 배포 문서를 전달합니다.' },
|
||||
{ n: '06', t: '무상 하자보수 30일', d: '납품 후 30일간 결함·수정을 무상으로 대응해 안정화까지 책임집니다.' },
|
||||
];
|
||||
|
||||
// 기존 work/freelance(lib/freelance-portfolio) 실사례를 새 토큰 기준으로 재구성.
|
||||
const CASES = [
|
||||
{
|
||||
t: '주식 자동매매 시스템',
|
||||
cat: '실시간 트레이딩 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '텔레그램과 연동해 실시간으로 주문을 집행하고 체결·손익 리포트를 자동 전송합니다.',
|
||||
tags: ['Python', 'Telegram Bot', '실시간 주문'],
|
||||
},
|
||||
{
|
||||
t: '부동산 청약 자동 수집·매칭',
|
||||
cat: '크롤링 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '공고를 주기적으로 크롤링해 조건에 맞는 매물만 골라내고, 신규 매칭을 즉시 푸시합니다.',
|
||||
tags: ['Python', '크롤링', '조건 매칭'],
|
||||
},
|
||||
{
|
||||
t: 'AI 콘텐츠 자동화 파이프라인',
|
||||
cat: 'AI 연동 · 직접 운영 중',
|
||||
live: true,
|
||||
d: '생성부터 검수, 발행까지 사람이 개입할 지점만 남기고 전 과정을 자동으로 연결합니다.',
|
||||
tags: ['AI 연동', '검수 워크플로우', '자동 발행'],
|
||||
},
|
||||
{
|
||||
t: 'Gmail 자동화 RPA',
|
||||
cat: 'RPA · 납품 완료',
|
||||
live: false,
|
||||
d: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림을 전송합니다.',
|
||||
tags: ['Python', 'Gmail API'],
|
||||
},
|
||||
{
|
||||
t: '쇼핑몰 가격 모니터링 봇',
|
||||
cat: '웹 스크래핑 · 납품 완료',
|
||||
live: false,
|
||||
d: '경쟁사 상품 가격을 매일 모니터링해 변동 시 텔레그램으로 즉시 알립니다.',
|
||||
tags: ['Python', 'Selenium', 'Telegram Bot'],
|
||||
},
|
||||
{
|
||||
t: '영업 일보 자동화 시스템',
|
||||
cat: '엑셀 자동화 · 납품 완료',
|
||||
live: false,
|
||||
d: '엑셀 데이터를 자동 집계해 일·주·월별 보고서 PDF를 생성하고 매일 09시 발송합니다.',
|
||||
tags: ['Python', 'OpenPyXL', 'ReportLab'],
|
||||
},
|
||||
];
|
||||
|
||||
// /work/website/samples/* 중 대표 샘플 — 이 라우트는 숨김이 아니라 포트폴리오용으로 잔존.
|
||||
const SAMPLES = [
|
||||
{ slug: 'corporate', t: '기업 홈페이지', sub: '테크솔루션㈜', tag: 'B2B · 신뢰' },
|
||||
{ slug: 'shopping', t: '개인 쇼핑몰', sub: 'MELLOW STUDIO', tag: '쇼핑몰 · 브랜드' },
|
||||
{ slug: 'dashboard', t: '관리자 대시보드', sub: 'DataFlow SaaS', tag: 'SaaS · 자동화' },
|
||||
{ slug: 'portfolio', t: '개인 포트폴리오', sub: 'Kim Jisu', tag: '크리에이터 · 수주' },
|
||||
];
|
||||
|
||||
const FAQ = [
|
||||
{
|
||||
q: '견적은 어떻게 산정되나요?',
|
||||
a: '기능 범위와 구현 난이도를 기준으로 산정합니다. 상담에서 필요한 기능을 함께 정리한 뒤, 영업일 2일 내에 범위·일정·금액을 명시한 견적으로 회신드립니다. 추측으로 부풀리지 않고 실제 작업량 기준으로 잡습니다.',
|
||||
},
|
||||
{
|
||||
q: '수정 요청은 몇 번까지 가능한가요?',
|
||||
a: '합의한 범위 안에서는 2회까지 무상으로 수정해 드립니다. 범위를 벗어나는 기능 추가나 방향 전환은 별도로 협의해 진행합니다. 무엇이 범위 안/밖인지는 착수 전 견적에 미리 명시합니다.',
|
||||
},
|
||||
{
|
||||
q: '소스코드도 제공되나요?',
|
||||
a: '제공됩니다. 잔금 완납 시 전체 소스코드와 배포·실행 문서를 함께 전달합니다. 직접 운영하시거나 다른 개발자에게 이어 맡기셔도 문제없도록 인도합니다.',
|
||||
},
|
||||
{
|
||||
q: '납품 후 유지보수는요?',
|
||||
a: '납품일로부터 30일간 결함·오류를 무상으로 하자보수합니다. 이후 기능 추가나 지속 운영이 필요하면 월 단위 유지보수 계약으로 이어갈 수 있습니다.',
|
||||
},
|
||||
];
|
||||
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m13 5 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OutsourcingPage() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── 1. Hero ─── */}
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-24 lg:py-32">
|
||||
<div className="max-w-3xl">
|
||||
<span
|
||||
className="inline-block text-xs font-semibold mb-6 px-2.5 py-1 rounded"
|
||||
style={{ color: 'var(--jsm-accent)', background: 'var(--jsm-accent-soft)', ...KOR_BODY }}
|
||||
>
|
||||
외주 개발
|
||||
</span>
|
||||
<h1
|
||||
className="text-4xl sm:text-5xl lg:text-[3.5rem] font-bold leading-[1.2] break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
맞춤 소프트웨어{' '}
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>외주 개발</span>
|
||||
</h1>
|
||||
<p
|
||||
className="mt-7 text-lg lg:text-xl leading-relaxed break-keep max-w-2xl"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
기획 정리가 안 됐어도 괜찮습니다. 상담에서 함께 정리합니다. 7년차 대기업 백엔드
|
||||
개발자가 기획부터 배포·하자보수까지 단독으로 책임집니다.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-3">
|
||||
<Link
|
||||
href="#contact"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg font-semibold text-white transition-colors duration-150 hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
의뢰 내용 보내기
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
<Link
|
||||
href="#portfolio"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg font-semibold border transition-colors duration-150 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{
|
||||
color: 'var(--jsm-ink)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
background: 'var(--jsm-surface)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
포트폴리오 보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 2. 제공 분야 ─── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Scope
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
이런 것들을 만들어 드립니다
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-12 grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{FIELDS.map((f) => (
|
||||
<div
|
||||
key={f.t}
|
||||
className="rounded-2xl p-7 border"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<h3
|
||||
className="text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{f.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{f.d}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 3. 진행 프로세스 ─── */}
|
||||
<section id="process" className="scroll-mt-20" style={{ background: 'var(--jsm-bg)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Process
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
상담부터 하자보수까지, 흐름이 분명합니다
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="mt-12 grid sm:grid-cols-2 lg:grid-cols-3 gap-px rounded-2xl overflow-hidden border"
|
||||
style={{ borderColor: 'var(--jsm-line)', background: 'var(--jsm-line)' }}
|
||||
>
|
||||
{PROCESS.map((s) => (
|
||||
<div key={s.n} className="p-7 lg:p-8" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<span
|
||||
className="text-sm font-bold"
|
||||
style={{ color: 'var(--jsm-accent)', fontFamily: 'monospace' }}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-4 text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{s.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{s.d}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 4. 포트폴리오 ─── */}
|
||||
<section id="portfolio" className="scroll-mt-20" style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Portfolio
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
직접 개발하고, 실제로 굴러가는 결과물
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
운영 중인 서비스와 납품 완료 프로젝트입니다. 의뢰하신 프로젝트도 같은 깊이로 만듭니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 실사례 카드 */}
|
||||
<div className="mt-12 grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{CASES.map((c) => (
|
||||
<div
|
||||
key={c.t}
|
||||
className="flex flex-col rounded-2xl p-7 border"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="self-start inline-flex items-center gap-1.5 text-[11px] font-semibold px-2.5 py-1 rounded-full mb-5"
|
||||
style={
|
||||
c.live
|
||||
? { color: 'var(--jsm-accent)', background: 'var(--jsm-accent-soft)' }
|
||||
: { color: 'var(--jsm-ink-soft)', background: 'var(--jsm-surface-alt)' }
|
||||
}
|
||||
>
|
||||
{c.live && (
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
/>
|
||||
)}
|
||||
{c.cat}
|
||||
</span>
|
||||
<h3
|
||||
className="text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{c.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 text-sm leading-relaxed break-keep flex-1"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{c.d}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-1.5">
|
||||
{c.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs px-2.5 py-1 rounded"
|
||||
style={{
|
||||
color: 'var(--jsm-ink-soft)',
|
||||
background: 'var(--jsm-surface-alt)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 웹사이트 샘플 링크 */}
|
||||
<div className="mt-14">
|
||||
<h3
|
||||
className="text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
웹사이트 제작 샘플
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
직접 둘러볼 수 있는 데모 사이트입니다. 카드를 눌러 화면을 확인하세요.
|
||||
</p>
|
||||
<div className="mt-6 grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{SAMPLES.map((s) => (
|
||||
<Link
|
||||
key={s.slug}
|
||||
href={`/work/website/samples/${s.slug}`}
|
||||
className="group flex flex-col rounded-2xl p-6 border transition-colors duration-200 hover:border-[var(--jsm-accent)]"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="text-[11px] font-semibold uppercase tracking-wider"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
{s.tag}
|
||||
</span>
|
||||
<h4
|
||||
className="mt-3 text-base font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{s.t}
|
||||
</h4>
|
||||
<p
|
||||
className="mt-1 text-sm break-keep"
|
||||
style={{ color: 'var(--jsm-ink-faint)', ...KOR_BODY }}
|
||||
>
|
||||
{s.sub}
|
||||
</p>
|
||||
<span
|
||||
className="mt-5 inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 group-hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
데모 보기
|
||||
<ArrowRight />
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 5. FAQ ─── */}
|
||||
<section style={{ background: 'var(--jsm-bg)' }}>
|
||||
<div className="max-w-3xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="mb-12">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
FAQ
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
자주 묻는 질문
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{FAQ.map((item) => (
|
||||
<details
|
||||
key={item.q}
|
||||
className="group rounded-2xl border overflow-hidden"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<summary
|
||||
className="flex items-center justify-between gap-4 cursor-pointer list-none px-6 py-5 font-semibold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{item.q}
|
||||
<svg
|
||||
className="shrink-0 transition-transform duration-200 group-open:rotate-45"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
aria-hidden
|
||||
style={{ color: 'var(--jsm-ink-faint)' }}
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p
|
||||
className="px-6 pb-5 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{item.a}
|
||||
</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 6. 의뢰 폼 ─── */}
|
||||
<section id="contact" className="scroll-mt-20" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="grid lg:grid-cols-5 gap-10 lg:gap-12">
|
||||
{/* 안내 */}
|
||||
<div className="lg:col-span-2">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: '#7aa7ff' }}
|
||||
>
|
||||
Contact
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-[2.5rem] font-bold leading-tight text-white break-keep"
|
||||
style={KOR_TIGHT}
|
||||
>
|
||||
프로젝트 문의
|
||||
</h2>
|
||||
<p
|
||||
className="mt-5 text-lg leading-relaxed text-white/70 break-keep"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
영업일 2일 내에 회신드립니다. 아이디어 단계여도 괜찮습니다 — 상담에서 방향을
|
||||
함께 잡아드립니다.
|
||||
</p>
|
||||
<div
|
||||
className="mt-8 pt-8 border-t space-y-3"
|
||||
style={{ borderColor: 'rgba(255,255,255,0.12)' }}
|
||||
>
|
||||
<a
|
||||
href="mailto:bgg8988@gmail.com"
|
||||
className="flex items-center gap-3 text-sm text-white/80 hover:text-white transition-colors"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
<span className="text-white/40 text-xs uppercase tracking-wider w-12">Mail</span>
|
||||
bgg8988@gmail.com
|
||||
</a>
|
||||
<a
|
||||
href="tel:010-3907-1392"
|
||||
className="flex items-center gap-3 text-sm text-white/80 hover:text-white transition-colors"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
<span className="text-white/40 text-xs uppercase tracking-wider w-12">Tel</span>
|
||||
010-3907-1392
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 폼 */}
|
||||
<div className="lg:col-span-3">
|
||||
<div
|
||||
className="rounded-2xl p-6 lg:p-8"
|
||||
style={{ background: 'var(--jsm-surface)' }}
|
||||
>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { isServiceVisible } from '@/lib/service-visibility';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'SaaS 제품 · 월 구독 패키지',
|
||||
@@ -13,6 +15,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function PackagesLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
export default async function PackagesLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!(await isServiceVisible('packages'))) notFound();
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
872
app/page.tsx
872
app/page.tsx
@@ -1,461 +1,433 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import ContactModal from './components/ContactModal';
|
||||
import { GlassButton } from './components/LiquidGlass';
|
||||
import { trackCTAClick } from '@/lib/gtag';
|
||||
import { PORTFOLIO } from '@/lib/freelance-portfolio';
|
||||
|
||||
const BEFORE = [
|
||||
'작곡 공부에만 최소 6개월 소요',
|
||||
'영상 편집 프로그램 학습의 높은 장벽',
|
||||
'항상 불안한 저작권 위반 위험',
|
||||
'곡 하나 완성에 드는 수백만 원의 외주비',
|
||||
// 쟁승메이드 메인 — 외주 개발 + 완성 소프트웨어 2축 랜딩 (서버 컴포넌트)
|
||||
// PublicShell이 TopNav(h-16)·푸터·main 배경을 제공하므로 여기서는 콘텐츠 섹션만 렌더한다.
|
||||
|
||||
const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const PROCESS = [
|
||||
{ n: '01', t: '무료 상담', d: '요구사항을 함께 정리하고 실현 가능성을 점검합니다.' },
|
||||
{ n: '02', t: '견적·범위 확정', d: '영업일 2일 내 범위와 견적을 정리해 회신드립니다.' },
|
||||
{ n: '03', t: '개발·중간 공유', d: '주 1회 이상 진행 상황을 공유하며 방향을 맞춥니다.' },
|
||||
{ n: '04', t: '납품·배포 지원', d: '검수 후 30일 무상 하자보수로 안정화까지 책임집니다.' },
|
||||
];
|
||||
|
||||
const AFTER = [
|
||||
'단 1시간 만에 프로급 음원 & 영상 완성',
|
||||
'드래그 앤 드롭 수준의 직관적인 워크플로우',
|
||||
'가이드대로 따라하면 완벽한 저작권 해결',
|
||||
'커피 한 잔 가격으로 무한대 콘텐츠 생산',
|
||||
const STATS = [
|
||||
{ v: '7년차', l: '대기업 백엔드 개발 경력' },
|
||||
{ v: '15+', l: '직접 운영 중인 서비스' },
|
||||
{ v: '기획→배포', l: '원스톱 단독 진행' },
|
||||
];
|
||||
|
||||
const TWEETS_ROW_A = [
|
||||
{ name: '김민재', handle: 'minjae_shorts', time: '2h', body: '작곡 하나 못 하던 내가 3일 만에 쇼츠 채널 열었다. 프롬프트북 반칙 수준 ㄹㅇ' },
|
||||
{ name: '이소영', handle: 'cafe_sohyang', time: '5h', body: '매장 BGM 직접 만들어요. 저작권 고민 없이 매달 플레이리스트 갈아끼우는 게 신기함.' },
|
||||
{ name: '박도현', handle: 'dohyun_side', time: '1d', body: '퇴근 후 1시간 = 쇼츠 한 편. 애드센스 첫 수익이 3주 만에 꽂혔습니다. 팩값 회수 완료.' },
|
||||
{ name: '정유진', handle: 'yujin_indie', time: '2d', body: '데모 작업 시간이 1/5로. 레퍼런스 탐색 → MV까지 한 번에. 인디 뮤지션들 다 써야 함.' },
|
||||
{ name: '최현우', handle: 'hyunwoo_tube', time: '3d', body: '구독자 정체기였는데 AI 뮤비 시리즈로 알고리즘 탑승. 조회수 월 +320%.' },
|
||||
{ name: '한지원', handle: 'jiwon_studio', time: '4d', body: '팩 안에 든 저작권 체크리스트가 실질적. Suno 약관 읽는 시간 아꼈다.' },
|
||||
{ name: '오세린', handle: 'serin_mv', time: '5d', body: 'Runway 프리셋 그대로 써도 퀄 나옴. 프롬프트 설계가 반이네요.' },
|
||||
{ name: '강태윤', handle: 'taeyun_ads', time: '6d', body: '광고 BGM 10개 찍어서 외주 드렸더니 클라이언트 반응이 달라졌습니다.' },
|
||||
const STACK = ['Python', 'Java', 'Spring', 'Next.js', 'AI 연동'];
|
||||
|
||||
const PORTFOLIO = [
|
||||
{
|
||||
t: '주식 자동매매 시스템',
|
||||
d: '텔레그램과 연동해 실시간으로 주문을 집행하고 체결·손익 리포트를 자동 전송합니다.',
|
||||
tags: ['실시간 주문', '텔레그램 연동', '리포트 자동화'],
|
||||
},
|
||||
{
|
||||
t: '부동산 청약 자동 수집·매칭',
|
||||
d: '공고를 주기적으로 크롤링해 조건에 맞는 매물만 골라내고, 신규 매칭을 즉시 알립니다.',
|
||||
tags: ['크롤링', '조건 매칭', '푸시 알림'],
|
||||
},
|
||||
{
|
||||
t: 'AI 콘텐츠 자동화 파이프라인',
|
||||
d: '생성부터 검수, 발행까지 사람이 개입할 지점만 남기고 전 과정을 자동으로 연결합니다.',
|
||||
tags: ['AI 연동', '검수 워크플로우', '자동 발행'],
|
||||
},
|
||||
];
|
||||
|
||||
const TWEETS_ROW_B = [
|
||||
{ name: '문가은', handle: 'gaeun_beats', time: '3h', body: '가사 생성 템플릿이 진짜 핵심. 한글 랩 가사 붙일 때 막히던 거 뚫렸어요.' },
|
||||
{ name: '류현석', handle: 'hyun_creator', time: '7h', body: '쇼츠 업로드 루틴이 1시간 안에 끝남. 주말마다 10편씩 쌓고 있습니다.' },
|
||||
{ name: '배수진', handle: 'sujin_pop', time: '1d', body: 'K-POP 스타일 프롬프트 조합 충격. 레퍼런스 없이도 그 느낌이 나옴.' },
|
||||
{ name: '송재훈', handle: 'jaehun_lab', time: '2d', body: '1:1 Q&A 답변 속도 미쳤어요. 당일 회신 + 실무 디테일까지.' },
|
||||
{ name: '조은비', handle: 'eunbi_vlog', time: '3d', body: '브이로그 BGM 자작하니까 조회수 + 체류시간 둘 다 올라감. 데이터가 말함.' },
|
||||
{ name: '신도윤', handle: 'doyoon_snd', time: '4d', body: '스템 분리본이 포함된 게 진짜 크다. 믹싱 작업 훨씬 편해짐.' },
|
||||
{ name: '윤채원', handle: 'chaewon_art', time: '5d', body: 'Midjourney 프롬프트 풀 가치가 팩값 넘음. 그냥 사세요.' },
|
||||
{ name: '임준혁', handle: 'junhyuk_tune', time: '6d', body: '업데이트 진짜로 오네요. 2주 만에 V4.5 프롬프트 가이드 추가됨.' },
|
||||
];
|
||||
|
||||
const CB_CARDS = [
|
||||
{ href: '/work/freelance', label: '외주 개발', desc: '맞춤 솔루션 · RPA·API 자동화 포함', key: 'freelance' },
|
||||
{ href: '/work/website', label: '웹사이트', desc: '기업·브랜드 사이트', key: 'website' },
|
||||
{ href: '/work/saju', label: 'AI 사주', desc: '12개 항목 무료 해석', key: 'saju' },
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalService, setModalService] = useState('일반 문의');
|
||||
|
||||
const openContact = (service: string) => {
|
||||
setModalService(service);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<div className="relative overflow-x-hidden bg-black text-white">
|
||||
<ContactModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
setModalService('일반 문의');
|
||||
}}
|
||||
service={modalService}
|
||||
checklist={['연락처/이메일', '원하는 작업 범위', '희망 일정']}
|
||||
/>
|
||||
|
||||
{/* 1. Brand Hero — kx-surface 검정, 60vh, 텍스트 중심 */}
|
||||
<section
|
||||
className="relative w-full min-h-[60vh] flex items-center justify-center px-6 border-b border-white/10 overflow-hidden"
|
||||
style={{ background: 'var(--kx-surface)' }}
|
||||
>
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
src="/hero-bg.mp4"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
style={{ filter: 'blur(8px)', opacity: 0.35 }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 pointer-events-none" aria-hidden />
|
||||
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||
<h1
|
||||
className="kx-display text-4xl md:text-6xl lg:text-7xl font-bold mb-5 leading-[1.1]"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
현직 엔지니어가
|
||||
<br />직접 만듭니다.
|
||||
</h1>
|
||||
<p className="text-base md:text-xl text-white/70 leading-relaxed">
|
||||
검증된 자동화는 SaaS로. AI 음악 가이드와 커스텀 외주까지.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. Two-up Cards */}
|
||||
<section className="py-20 px-6 bg-black border-b border-white/10">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Music 카드 */}
|
||||
<Link
|
||||
href="/music"
|
||||
onClick={() => trackCTAClick('home_v7_card_music')}
|
||||
className="group relative rounded-2xl border border-white/15 overflow-hidden min-h-[280px] flex flex-col justify-end p-8 hover:border-white/40 transition"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
src="/hero-bg.mp4"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
style={{ opacity: 0.5 }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent pointer-events-none" />
|
||||
<div className="relative z-10">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/60 mb-3">
|
||||
Music
|
||||
</p>
|
||||
<h2 className="kx-display text-2xl md:text-3xl font-bold text-white mb-2">
|
||||
AI 음악 제품
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-white/70 mb-4">
|
||||
Suno 프롬프트 + 뮤비 워크플로우 + 유튜브 SEO 한 팩에.
|
||||
</p>
|
||||
<p className="font-mono text-xs text-white mb-5">₩39,000~</p>
|
||||
<span className="inline-flex items-center gap-2 text-sm font-bold text-white">
|
||||
Try now <span aria-hidden>→</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 커스텀 외주 카드 */}
|
||||
<Link
|
||||
href="/work"
|
||||
onClick={() => trackCTAClick('home_v7_card_work')}
|
||||
className="group relative rounded-2xl border border-white/15 overflow-hidden min-h-[280px] flex flex-col justify-end p-8 hover:border-white/40 transition"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
background: 'linear-gradient(135deg, var(--kx-surface) 0%, rgba(204,151,255,0.15) 100%)',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 40px)',
|
||||
}}
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/60 mb-3">
|
||||
Custom Work
|
||||
</p>
|
||||
<h2 className="kx-display text-2xl md:text-3xl font-bold text-white mb-2">
|
||||
커스텀 외주
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-white/70 mb-4">
|
||||
외주 · 웹사이트 · AI 사주
|
||||
</p>
|
||||
<p className="text-xs text-white/50 mb-5">납품 5건 · 견적 24h 내 답변</p>
|
||||
<span className="inline-flex items-center gap-2 text-sm font-bold text-white">
|
||||
견적 문의 <span aria-hidden>→</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Music 섹션 — 기존 Features + Before/After + Tweet 마퀴 보존 */}
|
||||
|
||||
{/* 3-1. Features */}
|
||||
<section className="py-24 px-6 bg-white text-black border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-20">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-black/50 mb-4">
|
||||
Features
|
||||
</p>
|
||||
<h2
|
||||
className="kx-display text-3xl md:text-5xl font-bold text-black"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
한 팩에 담긴 3가지 무기.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-20 lg:space-y-28">
|
||||
{[
|
||||
{
|
||||
label: '01 · Prompt',
|
||||
title: '프롬프트 한 줄이 곡이 됩니다.',
|
||||
desc: '장르·무드·보컬 톤을 조합한 20+종 프롬프트 북. 복붙 한 번으로 Suno가 이해하는 구조로 변환됩니다.',
|
||||
video: '/feature-prompt.mp4',
|
||||
},
|
||||
{
|
||||
label: '02 · Visual',
|
||||
title: '비트에 맞춰 영상이 붙습니다.',
|
||||
desc: 'Midjourney·Runway·Luma 워크플로우. 가사와 비트를 싱크하는 9:16 쇼츠·16:9 풀버전을 바로 뽑아낼 수 있습니다.',
|
||||
video: '/feature-visual.mp4',
|
||||
},
|
||||
{
|
||||
label: '03 · Publish',
|
||||
title: '업로드 직전까지 마무리됩니다.',
|
||||
desc: '유튜브 제목·해시태그·설명란 SEO 템플릿. 복사-붙여넣기만으로 첫 쇼츠가 당일 채널에 올라갑니다.',
|
||||
video: null,
|
||||
},
|
||||
].map((f, i) => {
|
||||
const reverse = i % 2 === 1;
|
||||
return (
|
||||
<div
|
||||
key={f.label}
|
||||
className={`grid lg:grid-cols-2 gap-10 lg:gap-16 items-center ${reverse ? 'lg:[&>*:first-child]:order-2' : ''}`}
|
||||
>
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-black/50 mb-4">
|
||||
{f.label}
|
||||
</p>
|
||||
<h3
|
||||
className="kx-display text-2xl md:text-4xl font-bold mb-5 text-black"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
{f.title}
|
||||
</h3>
|
||||
<p className="text-base md:text-lg text-black/70 leading-relaxed max-w-lg">
|
||||
{f.desc}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative aspect-video rounded-2xl border border-black/15 bg-black/5 overflow-hidden">
|
||||
{f.video ? (
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
src={f.video}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="font-mono text-[11px] tracking-widest uppercase text-black/40">
|
||||
Video Placeholder
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3-2. Before / After */}
|
||||
<section className="py-24 px-6 bg-black text-white border-b border-white/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-14">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-4">
|
||||
Efficiency
|
||||
</p>
|
||||
<h2
|
||||
className="kx-display text-3xl md:text-5xl font-bold"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
압도적인 제작 효율의 차이.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="p-8 rounded-2xl border border-white/15 bg-white/[0.02]">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-3">
|
||||
Manual Process
|
||||
</p>
|
||||
<h3 className="text-2xl font-bold mb-6 text-white/60">Before</h3>
|
||||
<ul className="space-y-3">
|
||||
{BEFORE.map((t) => (
|
||||
<li key={t} className="flex items-start gap-3 text-sm text-white/60">
|
||||
<span className="text-white/40">·</span>
|
||||
<span>{t}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-8 rounded-2xl border border-white bg-white text-black">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-black/60 mb-3">
|
||||
AI Powered
|
||||
</p>
|
||||
<h3 className="text-2xl font-bold mb-6">After</h3>
|
||||
<ul className="space-y-3">
|
||||
{AFTER.map((t) => (
|
||||
<li key={t} className="flex items-start gap-3 text-sm text-black/80">
|
||||
<span className="text-black/50">·</span>
|
||||
<span>{t}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3-3. Use Cases — Tweet Marquee */}
|
||||
<section className="py-24 bg-white text-black border-b border-black/10 overflow-hidden">
|
||||
<div className="px-6 max-w-6xl mx-auto">
|
||||
<div className="text-center mb-14">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-black/50 mb-4">
|
||||
Use Cases
|
||||
</p>
|
||||
<h2
|
||||
className="kx-display text-3xl md:text-5xl font-bold"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
실제로 쓰고 있는 사람들.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 marquee-mask">
|
||||
{[TWEETS_ROW_A, TWEETS_ROW_B].map((row, rowIdx) => (
|
||||
<div key={rowIdx} className="marquee-viewport overflow-hidden">
|
||||
<div className={`marquee-track ${rowIdx === 1 ? 'marquee-track-reverse' : ''}`}>
|
||||
{[...row, ...row].map((t, i) => (
|
||||
<article
|
||||
key={`${rowIdx}-${i}`}
|
||||
className="shrink-0 w-[320px] sm:w-[360px] p-5 rounded-2xl border border-black/15 bg-black/[0.02]"
|
||||
>
|
||||
<header className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-black/5 border border-black/15 flex items-center justify-center font-bold text-black">
|
||||
{t.name.charAt(0)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-bold text-black text-sm truncate">{t.name}</p>
|
||||
<p className="font-mono text-[11px] text-black/50 truncate">@{t.handle}</p>
|
||||
</div>
|
||||
<span className="font-mono text-[10px] text-black/40 shrink-0">{t.time}</span>
|
||||
</header>
|
||||
<p
|
||||
className="text-sm leading-relaxed text-black/80"
|
||||
style={{ wordBreak: 'keep-all' }}
|
||||
>
|
||||
{t.body}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. 커스텀 외주 섹션 — 카드 + 5건 사례 + 견적 CTA */}
|
||||
<section className="py-24 px-6 bg-black text-white border-b border-white/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-14">
|
||||
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-4">
|
||||
Custom Work
|
||||
</p>
|
||||
<h2
|
||||
className="kx-display text-3xl md:text-5xl font-bold mb-5"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
맞춤 개발이 필요하신가요?
|
||||
</h2>
|
||||
<p className="text-base md:text-lg text-white/70 max-w-2xl mx-auto leading-relaxed">
|
||||
7년차 백엔드 개발자가 직접 설계·개발·납품. 외주, 웹사이트, AI 사주까지.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-12">
|
||||
{CB_CARDS.map((card) => (
|
||||
<Link
|
||||
key={card.key}
|
||||
href={card.href}
|
||||
onClick={() => trackCTAClick(`home_v7_cb_card_${card.key}`)}
|
||||
className="group rounded-2xl border border-white/15 bg-white/[0.02] p-5 hover:border-white/40 hover:bg-white/[0.05] transition flex flex-col"
|
||||
>
|
||||
<p className="font-bold text-white text-sm mb-1.5">{card.label}</p>
|
||||
<p className="text-xs text-white/60 leading-relaxed flex-1">{card.desc}</p>
|
||||
<span aria-hidden="true" className="mt-3 text-white/40 text-xs">→</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 납품 5건 사례 미리보기 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-12">
|
||||
{PORTFOLIO.map((p) => (
|
||||
<div
|
||||
key={p.title}
|
||||
className={`p-4 rounded-2xl border ${p.borderAccent} ${p.accentBg} flex flex-col`}
|
||||
>
|
||||
<p className={`font-mono text-[9px] uppercase tracking-widest ${p.accentColor} mb-2`}>
|
||||
{p.category}
|
||||
</p>
|
||||
<h3 className="font-bold text-white text-xs leading-tight mb-1.5">{p.title}</h3>
|
||||
<p className="text-[10px] text-white/50 line-clamp-2 flex-1">{p.result}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
trackCTAClick('home_v7_cb_cta');
|
||||
openContact('외주 개발 문의');
|
||||
}}
|
||||
className="kx-btn-primary inline-flex items-center px-7 py-3 rounded-full text-sm"
|
||||
>
|
||||
견적 문의하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Final CTA — 어느 쪽이든 시작하세요 */}
|
||||
<section className="relative w-full min-h-[400px] flex items-center justify-center px-6 py-24 bg-black overflow-hidden">
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
src="/hero-bg.mp4"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
style={{ filter: 'blur(8px)', opacity: 0.35 }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/50 pointer-events-none" />
|
||||
<div className="relative z-10 max-w-2xl mx-auto text-center">
|
||||
<h2
|
||||
className="kx-display text-3xl md:text-5xl font-bold mb-8"
|
||||
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
|
||||
>
|
||||
어느 쪽이든 시작하세요.
|
||||
</h2>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<GlassButton
|
||||
href="/music"
|
||||
onClick={() => trackCTAClick('home_v7_final_music')}
|
||||
tint="rgba(255,255,255,0.18)"
|
||||
className="text-base"
|
||||
>
|
||||
<span className="text-white">Music 팩 보기</span>
|
||||
</GlassButton>
|
||||
<button
|
||||
onClick={() => {
|
||||
trackCTAClick('home_v7_final_work');
|
||||
openContact('외주 개발 문의');
|
||||
}}
|
||||
className="kx-btn-primary inline-flex items-center justify-center px-7 py-3 rounded-full text-base"
|
||||
>
|
||||
견적 문의
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m13 5 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── 1. Hero ─── */}
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-24 lg:py-32">
|
||||
<div className="max-w-3xl">
|
||||
<span
|
||||
className="inline-block text-xs font-semibold mb-6 px-2.5 py-1 rounded"
|
||||
style={{
|
||||
color: 'var(--jsm-accent)',
|
||||
background: 'var(--jsm-accent-soft)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
외주 개발 · 완성 소프트웨어
|
||||
</span>
|
||||
<h1
|
||||
className="text-4xl sm:text-5xl lg:text-[3.5rem] font-bold leading-[1.2] break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
필요한 소프트웨어,
|
||||
<br className="hidden sm:block" /> 만들어 드리거나{' '}
|
||||
<span style={{ color: 'var(--jsm-accent)' }}>이미 만들어 두었습니다.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="mt-7 text-lg lg:text-xl leading-relaxed break-keep max-w-2xl"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
7년차 대기업 백엔드 개발자가 직접 설계·개발·운영합니다. 맞춤 외주 개발과
|
||||
검증된 완성 소프트웨어 중 필요한 쪽을 선택하세요.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-3">
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg font-semibold text-white transition-colors duration-150"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
프로젝트 문의하기
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg font-semibold border transition-colors duration-150 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{
|
||||
color: 'var(--jsm-ink)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
background: 'var(--jsm-surface)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
소프트웨어 보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 2. 2축 서비스 ─── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 외주 개발 */}
|
||||
<Link
|
||||
href="/outsourcing"
|
||||
className="group block rounded-2xl p-9 lg:p-11 border transition-colors duration-200 hover:border-[var(--jsm-accent)]"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Custom
|
||||
</span>
|
||||
<h2
|
||||
className="mt-3 text-2xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
외주 개발
|
||||
</h2>
|
||||
<p
|
||||
className="mt-3 leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
기획부터 배포·운영까지 한 사람이 책임집니다. 웹 서비스, API, 업무 자동화,
|
||||
봇 개발까지 필요한 형태로 만들어 드립니다.
|
||||
</p>
|
||||
<span
|
||||
className="mt-6 inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 group-hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
외주 개발 알아보기
|
||||
<ArrowRight />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* 완성 소프트웨어 */}
|
||||
<Link
|
||||
href="/products"
|
||||
className="group block rounded-2xl p-9 lg:p-11 border transition-colors duration-200 hover:border-[var(--jsm-accent)]"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Ready-made
|
||||
</span>
|
||||
<h2
|
||||
className="mt-3 text-2xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
완성 소프트웨어
|
||||
</h2>
|
||||
<p
|
||||
className="mt-3 leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
입금 확인 후 바로 다운로드해 사용합니다. 제가 직접 운영하며 검증한 도구만
|
||||
정리해 제공합니다.
|
||||
</p>
|
||||
<span
|
||||
className="mt-6 inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 group-hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
소프트웨어 둘러보기
|
||||
<ArrowRight />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 3. 개발 프로세스 ─── */}
|
||||
<section id="process" style={{ background: 'var(--jsm-bg)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Process
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
상담부터 납품까지, 흐름이 분명합니다
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-12 grid sm:grid-cols-2 lg:grid-cols-4 gap-px rounded-2xl overflow-hidden border" style={{ borderColor: 'var(--jsm-line)', background: 'var(--jsm-line)' }}>
|
||||
{PROCESS.map((s) => (
|
||||
<div key={s.n} className="p-7 lg:p-8" style={{ background: 'var(--jsm-surface)' }}>
|
||||
<span
|
||||
className="text-sm font-bold"
|
||||
style={{ color: 'var(--jsm-accent)', fontFamily: 'monospace' }}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<h3
|
||||
className="mt-4 text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{s.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2 text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{s.d}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 4. 신뢰 요소 ─── */}
|
||||
<section style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-24">
|
||||
<div className="grid sm:grid-cols-3 gap-10 sm:gap-8">
|
||||
{STATS.map((s) => (
|
||||
<div key={s.l}>
|
||||
<p
|
||||
className="text-3xl lg:text-4xl font-bold text-white"
|
||||
style={KOR_TIGHT}
|
||||
>
|
||||
{s.v}
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 text-sm leading-relaxed break-keep text-white/60"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
{s.l}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="mt-12 pt-8 border-t flex flex-wrap items-center gap-x-3 gap-y-2"
|
||||
style={{ borderColor: 'rgba(255,255,255,0.1)' }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-wider text-white/40 mr-1">Stack</span>
|
||||
{STACK.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="text-sm text-white/80 px-3 py-1 rounded-full"
|
||||
style={{ background: 'rgba(255,255,255,0.06)', ...KOR_BODY }}
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 5. 포트폴리오 하이라이트 ─── */}
|
||||
<section id="portfolio" style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Portfolio
|
||||
</p>
|
||||
<h2
|
||||
className="text-3xl lg:text-4xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
실제로 운영 중인 시스템들
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
데모가 아니라 매일 돌아가는 서비스입니다. 같은 깊이로 의뢰하신 프로젝트를 만듭니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-12 grid md:grid-cols-3 gap-6">
|
||||
{PORTFOLIO.map((p) => (
|
||||
<div
|
||||
key={p.t}
|
||||
className="flex flex-col rounded-2xl p-7 border"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="self-start inline-flex items-center gap-1.5 text-[11px] font-semibold px-2.5 py-1 rounded-full mb-5"
|
||||
style={{ color: 'var(--jsm-accent)', background: 'var(--jsm-accent-soft)' }}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
/>
|
||||
직접 개발·운영 중
|
||||
</span>
|
||||
<h3
|
||||
className="text-lg font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{p.t}
|
||||
</h3>
|
||||
<p
|
||||
className="mt-2.5 text-sm leading-relaxed break-keep flex-1"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{p.d}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-1.5">
|
||||
{p.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs px-2.5 py-1 rounded"
|
||||
style={{
|
||||
color: 'var(--jsm-ink-soft)',
|
||||
background: 'var(--jsm-surface-alt)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<Link
|
||||
href="/outsourcing#portfolio"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold transition-colors duration-150 hover:text-[var(--jsm-accent-hover)]"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
포트폴리오 자세히 보기
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 6. 소프트웨어 진열(예고) ─── */}
|
||||
{/* Phase 2: 이 섹션은 products 테이블 기반 동적 진열로 교체 예정.
|
||||
현재는 출시 전 정적 안내만 노출한다. */}
|
||||
<section style={{ background: 'var(--jsm-bg)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div
|
||||
className="rounded-2xl border px-8 py-14 lg:px-14 lg:py-16 text-center"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-3"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
Coming soon
|
||||
</p>
|
||||
<h2
|
||||
className="text-2xl lg:text-3xl font-bold break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
검증된 완성 소프트웨어를 준비하고 있습니다
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 max-w-xl mx-auto leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
직접 운영하며 다듬은 도구를 하나씩 다운로드 상품으로 공개할 예정입니다.
|
||||
출시 소식을 가장 먼저 받아보세요.
|
||||
</p>
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="mt-8 inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg font-semibold border transition-colors duration-150 hover:bg-[var(--jsm-surface-alt)]"
|
||||
style={{
|
||||
color: 'var(--jsm-ink)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
출시 소식 받기
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 7. 최종 CTA ─── */}
|
||||
<section style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8 py-24 lg:py-28">
|
||||
<div className="max-w-3xl">
|
||||
<h2
|
||||
className="text-3xl lg:text-[2.5rem] font-bold leading-tight text-white break-keep"
|
||||
style={KOR_TIGHT}
|
||||
>
|
||||
프로젝트, 이야기부터 시작하세요
|
||||
</h2>
|
||||
<p
|
||||
className="mt-5 text-lg leading-relaxed text-white/70 break-keep max-w-2xl"
|
||||
style={KOR_BODY}
|
||||
>
|
||||
아이디어 단계여도 괜찮습니다. 무료 상담에서 방향을 함께 잡아드립니다.
|
||||
</p>
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="mt-9 inline-flex items-center justify-center gap-2 px-7 py-4 rounded-lg font-semibold text-white transition-colors duration-150"
|
||||
style={{ background: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
무료 상담 신청
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function FailContent() {
|
||||
<div className="inline-block bg-slate-100 border border-slate-200 text-slate-600 text-xs font-bold px-3 py-1 rounded-full mb-4">
|
||||
{code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-[#04102b] mb-2">
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: 'var(--jsm-ink)' }}>
|
||||
{code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제를 취소하셨습니다' : '결제에 실패했습니다'}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-sm mb-8 max-w-xs mx-auto leading-relaxed">{message}</p>
|
||||
@@ -45,9 +45,9 @@ export default function PaymentFailPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center px-6 py-16">
|
||||
<div className="w-full max-w-md bg-white rounded-2xl border border-[#dbe8ff] shadow-lg overflow-hidden">
|
||||
<div className="bg-[#04102b] px-6 py-4" style={{ 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="px-6 py-4 border-b" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg bg-[#1a56db] flex items-center justify-center text-white font-bold text-xs">
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-white font-bold text-xs" style={{ background: 'var(--jsm-accent)' }}>
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">쟁승메이드 결제</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ function SuccessContent() {
|
||||
<div className="inline-block bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-bold px-3 py-1 rounded-full mb-4">
|
||||
결제 완료
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold text-[#04102b] mb-2">결제가 완료되었습니다!</h2>
|
||||
<h2 className="text-2xl font-extrabold mb-2" style={{ color: 'var(--jsm-ink)' }}>결제가 완료되었습니다!</h2>
|
||||
{paymentId && (
|
||||
<p className="text-slate-400 text-xs mb-1">주문번호: {paymentId}</p>
|
||||
)}
|
||||
@@ -47,9 +47,9 @@ export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center px-6 py-16">
|
||||
<div className="w-full max-w-md bg-white rounded-2xl border border-[#dbe8ff] shadow-lg overflow-hidden">
|
||||
<div className="bg-[#04102b] px-6 py-4" style={{ 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="px-6 py-4 border-b" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg bg-[#1a56db] flex items-center justify-center text-white font-bold text-xs">
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-white font-bold text-xs" style={{ background: 'var(--jsm-accent)' }}>
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">쟁승메이드 결제</span>
|
||||
|
||||
@@ -23,69 +23,78 @@ export default async function PortfolioGateway({ params }: Props) {
|
||||
const expires = new Date(payload.exp).toLocaleDateString('ko-KR');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white">
|
||||
<section
|
||||
className="relative overflow-hidden px-6 py-20 lg:px-14 lg:py-28"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(circle at 30% 20%, #1e293b 0%, #020617 55%)',
|
||||
}}
|
||||
>
|
||||
<div className="min-h-screen" style={{ background: 'var(--jsm-bg)' }}>
|
||||
{/* 헤더 배너 — jsm-navy 사용 (푸터/다크 섹션 전용 토큰) */}
|
||||
<div className="px-6 py-4" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="max-w-4xl mx-auto flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm" style={{ background: 'var(--jsm-accent)' }}>
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">쟁승메이드</span>
|
||||
<span className="ml-auto font-mono text-xs tracking-[0.2em] uppercase px-3 py-1 rounded-full border" style={{ color: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,255,255,0.2)', background: 'rgba(255,255,255,0.08)' }}>
|
||||
Private · {payload.memo || 'Confidential'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="px-6 py-16 lg:px-14 lg:py-24">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span className="font-mono text-xs text-emerald-300/80 tracking-[0.25em] uppercase">
|
||||
Private Portfolio · {payload.memo || 'Confidential'}
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="text-xs font-semibold uppercase tracking-widest" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
개인 공유 포트폴리오
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-extrabold leading-[1.05] mb-6" style={{ wordBreak: 'keep-all' }}>
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold leading-tight mb-4" style={{ color: 'var(--jsm-ink)', wordBreak: 'keep-all' }}>
|
||||
박재오
|
||||
<br />
|
||||
<span className="bg-gradient-to-r from-sky-300 via-blue-200 to-cyan-300 bg-clip-text text-transparent">
|
||||
외주 개발 포트폴리오
|
||||
</span>
|
||||
<span className="gradient-text">외주 개발 포트폴리오</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-slate-300 text-lg leading-relaxed max-w-2xl mb-10" style={{ wordBreak: 'keep-all' }}>
|
||||
<p className="text-lg leading-relaxed max-w-2xl mb-4" style={{ color: 'var(--jsm-ink-soft)', wordBreak: 'keep-all' }}>
|
||||
현직 실무 엔지니어 · 계약서 우선 · 납기 패널티 보장 · 소스코드 100% 인도.
|
||||
본 페이지는 {expires}까지 유효한 개별 공유 링크입니다.
|
||||
</p>
|
||||
<p className="text-sm mb-10 font-mono px-3 py-2 rounded-lg inline-block" style={{ color: 'var(--jsm-ink-faint)', background: 'var(--jsm-surface-alt)', border: '1px solid var(--jsm-line)' }}>
|
||||
이 링크는 {expires}까지 유효합니다
|
||||
</p>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/freelance"
|
||||
className="group border border-white/10 hover:border-sky-400/50 rounded-2xl p-6 bg-white/[0.02] hover:bg-white/[0.05] transition-all"
|
||||
className="group rounded-2xl p-6 transition-all hover:-translate-y-1"
|
||||
style={{ background: 'var(--jsm-surface)', border: '1px solid var(--jsm-line)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
||||
>
|
||||
<p className="font-mono text-xs text-sky-300/70 uppercase tracking-widest mb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest mb-2" style={{ color: 'var(--jsm-accent)' }}>
|
||||
Freelance
|
||||
</p>
|
||||
<h3 className="text-xl font-extrabold mb-2">외주 개발 · 전체 소개</h3>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
<h3 className="text-xl font-extrabold mb-2" style={{ color: 'var(--jsm-ink)' }}>외주 개발 · 전체 소개</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
계약 프로세스, 납기 패널티, 포트폴리오 사례, 견적 문의.
|
||||
</p>
|
||||
<span className="inline-block mt-4 text-sm font-bold text-sky-300 group-hover:underline">
|
||||
<span className="inline-block mt-4 text-sm font-bold group-hover:underline" style={{ color: 'var(--jsm-accent)' }}>
|
||||
자세히 보기 →
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/services/website"
|
||||
className="group border border-white/10 hover:border-violet-400/50 rounded-2xl p-6 bg-white/[0.02] hover:bg-white/[0.05] transition-all"
|
||||
className="group rounded-2xl p-6 transition-all hover:-translate-y-1"
|
||||
style={{ background: 'var(--jsm-surface)', border: '1px solid var(--jsm-line)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
|
||||
>
|
||||
<p className="font-mono text-xs text-violet-300/70 uppercase tracking-widest mb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest mb-2" style={{ color: '#7c3aed' }}>
|
||||
Website
|
||||
</p>
|
||||
<h3 className="text-xl font-extrabold mb-2">홈페이지·쇼핑몰 제작</h3>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
<h3 className="text-xl font-extrabold mb-2" style={{ color: 'var(--jsm-ink)' }}>홈페이지·쇼핑몰 제작</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'var(--jsm-ink-soft)' }}>
|
||||
Next.js 기반 반응형 웹, SEO 기본, 3개월 유지보수 포함.
|
||||
</p>
|
||||
<span className="inline-block mt-4 text-sm font-bold text-violet-300 group-hover:underline">
|
||||
<span className="inline-block mt-4 text-sm font-bold group-hover:underline" style={{ color: '#7c3aed' }}>
|
||||
자세히 보기 →
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 text-xs text-slate-500 font-mono">
|
||||
<div className="mt-10 text-xs font-mono" style={{ color: 'var(--jsm-ink-faint)' }}>
|
||||
© 쟁승메이드 · 010-3907-1392 · bgg8988@gmail.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
163
app/products/page.tsx
Normal file
163
app/products/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
// TODO(Phase 2): products 테이블 연동 후 동적 카탈로그로 교체 예정.
|
||||
// 현재는 404 방지용 정적 스텁 페이지입니다.
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '완성 소프트웨어',
|
||||
description:
|
||||
'쟁승메이드가 직접 운영하며 검증한 완성 소프트웨어 목록. 계좌이체 결제 후 입금 확인 즉시 마이페이지에서 다운로드할 수 있습니다.',
|
||||
};
|
||||
|
||||
const KOR_TIGHT = { letterSpacing: '-0.02em' } as const;
|
||||
const KOR_BODY = { letterSpacing: '-0.01em' } as const;
|
||||
|
||||
const HOW = [
|
||||
{ n: '01', t: '계좌이체 결제', d: '안내된 계좌로 입금합니다. 이체 확인 후 수동으로 승인합니다.' },
|
||||
{ n: '02', t: '입금 확인', d: '입금이 확인되면 메일로 안내해 드립니다. 보통 당일 처리됩니다.' },
|
||||
{ n: '03', t: '마이페이지 다운로드', d: '마이페이지에서 구매 내역을 확인하고 파일을 내려받습니다.' },
|
||||
];
|
||||
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── Hero ─── */}
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-20 lg:py-28">
|
||||
<div className="max-w-2xl">
|
||||
<span
|
||||
className="inline-block text-xs font-semibold mb-6 px-2.5 py-1 rounded"
|
||||
style={{
|
||||
color: 'var(--jsm-accent)',
|
||||
background: 'var(--jsm-accent-soft)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
완성 소프트웨어
|
||||
</span>
|
||||
<h1
|
||||
className="text-3xl sm:text-4xl lg:text-5xl font-bold leading-[1.2] break-keep mb-5"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
직접 운영하며 검증한
|
||||
<br />
|
||||
도구들을 준비하고 있습니다.
|
||||
</h1>
|
||||
<p
|
||||
className="text-base sm:text-lg leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
입금 확인 후 바로 다운로드할 수 있는 형태로 제공됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 출시 준비 중 안내 ─── */}
|
||||
<section className="border-b" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<div
|
||||
className="rounded-lg border p-8 text-center"
|
||||
style={{
|
||||
background: 'var(--jsm-surface-alt)',
|
||||
borderColor: 'var(--jsm-line)',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold mb-3"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
출시 준비 중
|
||||
</p>
|
||||
<p
|
||||
className="text-xl sm:text-2xl font-bold mb-4 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
현재 상품을 정비하고 있습니다.
|
||||
</p>
|
||||
<p
|
||||
className="text-sm sm:text-base leading-relaxed break-keep max-w-md mx-auto"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
로또 분석 도구, 주식 자동매매 유틸리티 등 실제로 운영 중인 도구들을
|
||||
구매 가능한 형태로 순차 공개할 예정입니다.
|
||||
출시 소식을 먼저 받고 싶다면 아래 링크로 문의해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 구매 방식 안내 ─── */}
|
||||
<section style={{ background: 'var(--jsm-surface-alt)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<h2
|
||||
className="text-xl sm:text-2xl font-bold mb-10 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
구매 방식
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{HOW.map((step) => (
|
||||
<div
|
||||
key={step.n}
|
||||
className="rounded-lg border p-6"
|
||||
style={{ background: 'var(--jsm-surface)', borderColor: 'var(--jsm-line)' }}
|
||||
>
|
||||
<span
|
||||
className="text-xs font-semibold mb-3 block"
|
||||
style={{ color: 'var(--jsm-accent)', ...KOR_BODY }}
|
||||
>
|
||||
{step.n}
|
||||
</span>
|
||||
<p
|
||||
className="font-bold mb-2 break-keep"
|
||||
style={{ color: 'var(--jsm-ink)', ...KOR_TIGHT }}
|
||||
>
|
||||
{step.t}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm leading-relaxed break-keep"
|
||||
style={{ color: 'var(--jsm-ink-soft)', ...KOR_BODY }}
|
||||
>
|
||||
{step.d}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── CTA ─── */}
|
||||
<section className="border-t" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
<div className="max-w-5xl mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link
|
||||
href="/outsourcing#contact"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg text-sm font-semibold transition-colors"
|
||||
style={{
|
||||
background: 'var(--jsm-accent)',
|
||||
color: '#ffffff',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
출시 소식 받기
|
||||
</Link>
|
||||
<Link
|
||||
href="/outsourcing"
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg border text-sm font-semibold transition-colors"
|
||||
style={{
|
||||
borderColor: 'var(--jsm-line)',
|
||||
color: 'var(--jsm-ink-soft)',
|
||||
background: 'var(--jsm-surface)',
|
||||
...KOR_BODY,
|
||||
}}
|
||||
>
|
||||
외주 개발 알아보기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,8 +20,8 @@ interface Quote {
|
||||
}
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
기획: '#60a5fa', 디자인: '#f472b6', 개발: '#34d399', 인프라: '#fb923c',
|
||||
유지보수: '#a78bfa', 기타: '#94a3b8',
|
||||
기획: '#2563eb', 디자인: '#db2777', 개발: '#059669', 인프라: '#ea580c',
|
||||
유지보수: '#7c3aed', 기타: '#64748b',
|
||||
};
|
||||
|
||||
export default function QuotePage() {
|
||||
@@ -100,10 +100,10 @@ export default function QuotePage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ minHeight: '100vh', background: 'var(--jsm-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ width: 40, height: 40, border: '3px solid rgba(99,102,241,0.3)', borderTopColor: '#6366f1', borderRadius: '50%', animation: 'spin 0.8s linear infinite', margin: '0 auto 16px' }} />
|
||||
<p style={{ color: '#475569', fontFamily: 'sans-serif' }}>견적서를 불러오는 중...</p>
|
||||
<div style={{ width: 40, height: 40, border: '3px solid var(--jsm-accent-soft)', borderTopColor: 'var(--jsm-accent)', borderRadius: '50%', animation: 'spin 0.8s linear infinite', margin: '0 auto 16px' }} />
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontFamily: 'sans-serif' }}>견적서를 불러오는 중...</p>
|
||||
</div>
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||||
</div>
|
||||
@@ -112,29 +112,29 @@ export default function QuotePage() {
|
||||
|
||||
if (notFound || !quote) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ minHeight: '100vh', background: 'var(--jsm-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ fontSize: 64 }}>🔍</div>
|
||||
<h1 style={{ color: 'white', fontSize: 24, fontWeight: 700, fontFamily: 'sans-serif' }}>견적서를 찾을 수 없습니다</h1>
|
||||
<p style={{ color: '#475569', fontFamily: 'sans-serif' }}>링크가 만료되었거나 잘못된 주소입니다</p>
|
||||
<h1 style={{ color: 'var(--jsm-ink)', fontSize: 24, fontWeight: 700, fontFamily: 'sans-serif' }}>견적서를 찾을 수 없습니다</h1>
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontFamily: 'sans-serif' }}>링크가 만료되었거나 잘못된 주소입니다</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#0a0f1e', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 20, padding: 24 }}>
|
||||
<div style={{ minHeight: '100vh', background: 'var(--jsm-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 20, padding: 24 }}>
|
||||
<style>{`@keyframes pop { 0% { transform: scale(0.5); opacity: 0; } 70% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }`}</style>
|
||||
<div style={{ fontSize: 80, animation: 'pop 0.5s ease forwards' }}>🎉</div>
|
||||
<h1 style={{ color: 'white', fontSize: 28, fontWeight: 800, fontFamily: 'sans-serif', textAlign: 'center' }}>견적을 수락해 주셨습니다!</h1>
|
||||
<p style={{ color: '#94a3b8', fontFamily: 'sans-serif', textAlign: 'center', lineHeight: 1.7 }}>
|
||||
<h1 style={{ color: 'var(--jsm-ink)', fontSize: 28, fontWeight: 800, fontFamily: 'sans-serif', textAlign: 'center' }}>견적을 수락해 주셨습니다!</h1>
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontFamily: 'sans-serif', textAlign: 'center', lineHeight: 1.7 }}>
|
||||
담당자가 확인 후 빠른 시일 내에 연락드리겠습니다.<br />
|
||||
선택하신 내용을 기반으로 계약을 진행합니다.
|
||||
</p>
|
||||
<div style={{ background: '#0f172a', border: '1px solid rgba(99,102,241,0.3)', borderRadius: 16, padding: '24px 32px', textAlign: 'center' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14, fontFamily: 'sans-serif', marginBottom: 8 }}>최종 견적 금액</div>
|
||||
<div style={{ color: 'white', fontSize: 36, fontWeight: 800, fontFamily: 'sans-serif' }}>{grandTotal.toLocaleString()}원</div>
|
||||
<div style={{ background: 'var(--jsm-surface)', border: '1px solid var(--jsm-accent-soft)', borderRadius: 16, padding: '24px 32px', textAlign: 'center', boxShadow: '0 4px 20px rgba(29,78,216,0.08)' }}>
|
||||
<div style={{ color: 'var(--jsm-ink-soft)', fontSize: 14, fontFamily: 'sans-serif', marginBottom: 8 }}>최종 견적 금액</div>
|
||||
<div style={{ color: 'var(--jsm-ink)', fontSize: 36, fontWeight: 800, fontFamily: 'sans-serif' }}>{grandTotal.toLocaleString()}원</div>
|
||||
{maintenanceTotal > 0 && (
|
||||
<div style={{ color: '#6366f1', fontSize: 14, fontFamily: 'sans-serif', marginTop: 6 }}>+ 유지보수 {maintenanceTotal.toLocaleString()}원/월</div>
|
||||
<div style={{ color: 'var(--jsm-accent)', fontSize: 14, fontFamily: 'sans-serif', marginTop: 6 }}>+ 유지보수 {maintenanceTotal.toLocaleString()}원/월</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,13 +149,12 @@ export default function QuotePage() {
|
||||
].filter((t) => t.show !== false);
|
||||
|
||||
return (
|
||||
<div style={{ background: '#0a0f1e', minHeight: '100vh', color: 'white', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif" }}>
|
||||
<div style={{ background: 'var(--jsm-bg)', minHeight: '100vh', color: 'var(--jsm-ink)', fontFamily: "'Pretendard Variable', Pretendard, 'Noto Sans KR', sans-serif" }}>
|
||||
<style>{`
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } }
|
||||
* { box-sizing: border-box; }
|
||||
input[type=checkbox] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
|
||||
input[type=radio] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
|
||||
input[type=checkbox] { accent-color: var(--jsm-accent); width: 18px; height: 18px; cursor: pointer; }
|
||||
input[type=radio] { accent-color: var(--jsm-accent); width: 18px; height: 18px; cursor: pointer; }
|
||||
@media print {
|
||||
html, body { height: auto !important; min-height: 0 !important; overflow: visible !important; background: white !important; color: #1e293b !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
* { overflow: visible !important; }
|
||||
@@ -173,17 +172,17 @@ export default function QuotePage() {
|
||||
`}</style>
|
||||
|
||||
{/* 헤더 */}
|
||||
<div style={{ background: 'linear-gradient(180deg, #0f172a 0%, #0a0f1e 100%)', borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '32px 24px 0' }}>
|
||||
<div style={{ background: 'var(--jsm-navy)', borderBottom: '1px solid rgba(255,255,255,0.08)', padding: '32px 24px 0' }}>
|
||||
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
||||
{/* 브랜드 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 32 }}>
|
||||
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700 }}>쟁</div>
|
||||
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'var(--jsm-accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700, color: 'white' }}>쟁</div>
|
||||
<div>
|
||||
<div style={{ color: 'white', fontWeight: 700, fontSize: 14 }}>쟁승메이드</div>
|
||||
<div style={{ color: '#475569', fontSize: 11 }}>jaengseung-made.com</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.45)', fontSize: 11 }}>jaengseung-made.com</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid rgba(99,102,241,0.3)', color: '#818cf8', fontSize: 11, fontWeight: 600, padding: '4px 12px', borderRadius: 100 }}>
|
||||
<span style={{ background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.2)', color: 'rgba(255,255,255,0.85)', fontSize: 11, fontWeight: 600, padding: '4px 12px', borderRadius: 100 }}>
|
||||
공식 견적서
|
||||
</span>
|
||||
<button
|
||||
@@ -197,13 +196,13 @@ export default function QuotePage() {
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)',
|
||||
color: '#cbd5e1', fontSize: 13, fontWeight: 600,
|
||||
background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.18)',
|
||||
color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: 600,
|
||||
padding: '6px 14px', borderRadius: 8, cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.14)'; e.currentTarget.style.color = 'white'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; e.currentTarget.style.color = '#cbd5e1'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.18)'; e.currentTarget.style.color = 'white'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; e.currentTarget.style.color = 'rgba(255,255,255,0.8)'; }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||||
@@ -220,29 +219,29 @@ export default function QuotePage() {
|
||||
</h1>
|
||||
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
|
||||
{quote.client_name && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>
|
||||
<span>👤</span> {quote.client_name} 고객님
|
||||
</div>
|
||||
)}
|
||||
{quote.valid_until && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>
|
||||
<span>📅</span> 유효기간: {quote.valid_until.slice(0, 10)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#94a3b8', fontSize: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>
|
||||
<span>📄</span> 발행일: {new Date(quote.created_at).toLocaleDateString('ko-KR')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="no-print" style={{ display: isPrinting ? 'none' : 'flex', gap: 0, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div className="no-print" style={{ display: isPrinting ? 'none' : 'flex', gap: 0, borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{tabs.map((t) => (
|
||||
<button key={t.key} onClick={() => setActiveTab(t.key as typeof activeTab)}
|
||||
style={{
|
||||
padding: '12px 20px', fontSize: 14, fontWeight: 500, border: 'none', cursor: 'pointer',
|
||||
background: 'none', color: activeTab === t.key ? '#818cf8' : '#64748b',
|
||||
borderBottom: `2px solid ${activeTab === t.key ? '#6366f1' : 'transparent'}`,
|
||||
background: 'none', color: activeTab === t.key ? 'white' : 'rgba(255,255,255,0.5)',
|
||||
borderBottom: `2px solid ${activeTab === t.key ? 'var(--jsm-accent)' : 'transparent'}`,
|
||||
transition: 'all 0.2s', marginBottom: -1,
|
||||
}}>
|
||||
{t.label}
|
||||
@@ -255,10 +254,10 @@ export default function QuotePage() {
|
||||
{/* 만료 배너 */}
|
||||
{isExpired && (
|
||||
<div style={{ maxWidth: 900, margin: '0 auto', padding: '16px 24px 0' }}>
|
||||
<div style={{ background: 'rgba(245,158,11,0.1)', border: '1px solid rgba(245,158,11,0.3)', borderRadius: 12, padding: '14px 20px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.3)', borderRadius: 12, padding: '14px 20px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 18 }}>⚠</span>
|
||||
<div>
|
||||
<div style={{ color: '#f59e0b', fontWeight: 700, fontSize: 14 }}>이 견적서는 만료되었습니다</div>
|
||||
<div style={{ color: '#b45309', fontWeight: 700, fontSize: 14 }}>이 견적서는 만료되었습니다</div>
|
||||
<div style={{ color: '#92400e', fontSize: 13 }}>유효기간({quote.valid_until?.slice(0, 10)})이 지났습니다. 새 견적이 필요하시면 문의해 주세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,16 +270,16 @@ export default function QuotePage() {
|
||||
{/* ── 개요 ── */}
|
||||
{(isPrinting || activeTab === 'overview') && (
|
||||
<div style={{ animation: 'fadeUp 0.4s ease' }}>
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}>개요</h2>}
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: 'var(--jsm-accent)', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid var(--jsm-accent-soft)' }}>개요</h2>}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
|
||||
<StatCard label="총 필수 항목" value={requiredItems.length + '개'} sub="반드시 포함" color="#60a5fa" />
|
||||
<StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#a78bfa" />
|
||||
<StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub={'정가 ' + requiredOriginal.toLocaleString() + '원 → 40% 할인'} color="#34d399" />
|
||||
<StatCard label="총 필수 항목" value={requiredItems.length + '개'} sub="반드시 포함" color="#2563eb" />
|
||||
<StatCard label="총 선택 항목" value={optionalItems.length + '개'} sub="고객 선택 가능" color="#7c3aed" />
|
||||
<StatCard label="필수 견적 합계" value={requiredTotal.toLocaleString() + '원'} sub={'정가 ' + requiredOriginal.toLocaleString() + '원 → 40% 할인'} color="#059669" />
|
||||
<StatCard
|
||||
label="선택 포함 합계"
|
||||
value={grandTotal.toLocaleString() + '원'}
|
||||
sub={optionalItems.filter(i => checkedOptional[i.id]).length + '개 선택됨'}
|
||||
color="#f59e0b"
|
||||
color="#d97706"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,16 +288,16 @@ export default function QuotePage() {
|
||||
{/* ── WBS ── */}
|
||||
{(isPrinting || activeTab === 'wbs') && quote.wbs.length > 0 && (
|
||||
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}>WBS (작업 분류 체계)</h2>}
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: 'var(--jsm-accent)', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid var(--jsm-accent-soft)' }}>WBS (작업 분류 체계)</h2>}
|
||||
{quote.wbs.map((phase, pi) => (
|
||||
<div key={phase.id} style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, flexShrink: 0 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'var(--jsm-accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, flexShrink: 0, color: 'white' }}>
|
||||
{pi + 1}
|
||||
</div>
|
||||
<h3 style={{ fontSize: 18, fontWeight: 700, color: 'white' }}>{phase.phase}</h3>
|
||||
<h3 style={{ fontSize: 18, fontWeight: 700, color: 'var(--jsm-ink)' }}>{phase.phase}</h3>
|
||||
</div>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ background: 'var(--jsm-surface)', borderRadius: 12, border: '1px solid var(--jsm-line)', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '28%' }} />
|
||||
@@ -306,7 +305,7 @@ export default function QuotePage() {
|
||||
<col style={{ width: '60%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<tr style={{ borderBottom: '1px solid var(--jsm-line)', background: 'var(--jsm-surface-alt)' }}>
|
||||
<th style={thStyle}>작업명</th>
|
||||
<th style={thStyle}>기간</th>
|
||||
<th style={thStyle}>설명</th>
|
||||
@@ -314,10 +313,10 @@ export default function QuotePage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{phase.tasks.map((task) => (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid var(--jsm-line)' }}>
|
||||
<td style={{ ...tdStyle, wordBreak: 'keep-all' }}>{task.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#818cf8', fontWeight: 600, whiteSpace: 'nowrap' }}>{task.duration}</td>
|
||||
<td style={{ ...tdStyle, color: '#64748b' }}>{task.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--jsm-accent)', fontWeight: 600, whiteSpace: 'nowrap' }}>{task.duration}</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--jsm-ink-soft)' }}>{task.description || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -331,16 +330,16 @@ export default function QuotePage() {
|
||||
{/* ── 견적 항목 ── */}
|
||||
{(isPrinting || activeTab === 'quote') && (
|
||||
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}>견적 항목</h2>}
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: 'var(--jsm-accent)', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid var(--jsm-accent-soft)' }}>견적 항목</h2>}
|
||||
{/* 필수 항목 */}
|
||||
{requiredItems.length > 0 && (
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#60a5fa', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#60a5fa', display: 'inline-block' }} />
|
||||
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#2563eb', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#2563eb', display: 'inline-block' }} />
|
||||
필수 항목
|
||||
<span style={{ background: 'linear-gradient(135deg, #ef4444, #f97316)', color: 'white', fontSize: 11, fontWeight: 700, padding: '2px 10px', borderRadius: 100, marginLeft: 4 }}>40% 할인 적용</span>
|
||||
</h3>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflowX: 'auto' }}>
|
||||
<div style={{ background: 'var(--jsm-surface)', borderRadius: 12, border: '1px solid var(--jsm-line)', overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '10%' }} />
|
||||
@@ -351,7 +350,7 @@ export default function QuotePage() {
|
||||
<col style={{ width: '12%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<tr style={{ borderBottom: '1px solid var(--jsm-line)', background: 'var(--jsm-surface-alt)' }}>
|
||||
<th style={thStyle}>카테고리</th>
|
||||
<th style={thStyle}>항목명</th>
|
||||
<th style={thStyle}>설명</th>
|
||||
@@ -362,17 +361,17 @@ export default function QuotePage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{requiredItems.map((item) => (
|
||||
<tr key={item.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<tr key={item.id} style={{ borderBottom: '1px solid var(--jsm-line)' }}>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#64748b') + '18', color: CATEGORY_COLORS[item.category] || '#64748b', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
{item.category}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: 'white' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#64748b' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{item.unitPrice.toLocaleString()}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'white', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{(item.unitPrice * item.quantity).toLocaleString()}원</td>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: 'var(--jsm-ink)' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--jsm-ink-soft)' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: 'var(--jsm-ink-soft)', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: 'var(--jsm-ink-soft)', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{item.unitPrice.toLocaleString()}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'var(--jsm-ink)', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{(item.unitPrice * item.quantity).toLocaleString()}원</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -380,9 +379,9 @@ export default function QuotePage() {
|
||||
</div>
|
||||
{/* 필수 항목 할인 소계 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12, gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<span style={{ color: '#64748b', fontSize: 13, textDecoration: 'line-through', fontFamily: 'monospace' }}>정가 {requiredOriginal.toLocaleString()}원</span>
|
||||
<span style={{ color: 'var(--jsm-ink-faint)', fontSize: 13, textDecoration: 'line-through', fontFamily: 'monospace' }}>정가 {requiredOriginal.toLocaleString()}원</span>
|
||||
<span style={{ color: '#ef4444', fontSize: 13, fontWeight: 600 }}>−{(requiredOriginal - requiredTotal).toLocaleString()}원 할인</span>
|
||||
<span style={{ color: '#34d399', fontSize: 15, fontWeight: 700, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}원</span>
|
||||
<span style={{ color: '#059669', fontSize: 15, fontWeight: 700, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -390,12 +389,12 @@ export default function QuotePage() {
|
||||
{/* 선택 항목 */}
|
||||
{optionalItems.length > 0 && (
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#a78bfa', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#a78bfa', display: 'inline-block' }} />
|
||||
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#7c3aed', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#7c3aed', display: 'inline-block' }} />
|
||||
선택 항목
|
||||
</h3>
|
||||
<p style={{ color: '#475569', fontSize: 13, marginBottom: 12 }}>아래 항목 중 원하시는 것을 선택하세요 — 총 금액에 실시간으로 반영됩니다</p>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(167,139,250,0.2)', overflowX: 'auto' }}>
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontSize: 13, marginBottom: 12 }}>아래 항목 중 원하시는 것을 선택하세요 — 총 금액에 실시간으로 반영됩니다</p>
|
||||
<div style={{ background: 'var(--jsm-surface)', borderRadius: 12, border: '1px solid rgba(29,78,216,0.2)', overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '6%' }} />
|
||||
@@ -406,7 +405,7 @@ export default function QuotePage() {
|
||||
<col style={{ width: '12%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<tr style={{ borderBottom: '1px solid var(--jsm-line)', background: 'var(--jsm-surface-alt)' }}>
|
||||
<th style={thStyle}>선택</th>
|
||||
<th style={thStyle}>카테고리</th>
|
||||
<th style={thStyle}>항목명</th>
|
||||
@@ -419,19 +418,19 @@ export default function QuotePage() {
|
||||
{optionalItems.map((item) => (
|
||||
<tr key={item.id}
|
||||
onClick={() => setCheckedOptional((prev) => ({ ...prev, [item.id]: !prev[item.id] }))}
|
||||
style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer', background: checkedOptional[item.id] ? 'rgba(167,139,250,0.05)' : 'transparent', transition: 'background 0.2s' }}>
|
||||
style={{ borderBottom: '1px solid var(--jsm-line)', cursor: 'pointer', background: checkedOptional[item.id] ? 'rgba(29,78,216,0.06)' : 'transparent', transition: 'background 0.2s' }}>
|
||||
<td style={{ ...tdStyle, textAlign: 'center' }}>
|
||||
<input type="checkbox" checked={!!checkedOptional[item.id]} onChange={() => {}} />
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#64748b') + '18', color: CATEGORY_COLORS[item.category] || '#64748b', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
{item.category}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: checkedOptional[item.id] ? 'white' : '#64748b' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#475569' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#64748b', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#a78bfa' : '#475569', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: checkedOptional[item.id] ? 'var(--jsm-ink)' : 'var(--jsm-ink-soft)' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--jsm-ink-soft)' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: 'var(--jsm-ink-faint)', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#7c3aed' : 'var(--jsm-ink-faint)', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
|
||||
{(item.unitPrice * item.quantity).toLocaleString()}원
|
||||
</td>
|
||||
</tr>
|
||||
@@ -444,24 +443,24 @@ export default function QuotePage() {
|
||||
|
||||
{/* 합계 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div style={{ background: '#0f172a', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, padding: '24px 28px', width: 360 }}>
|
||||
<div style={{ background: 'var(--jsm-surface)', border: '1px solid var(--jsm-line)', borderRadius: 16, padding: '24px 28px', width: 360, boxShadow: '0 4px 16px rgba(0,0,0,0.04)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ color: '#64748b', fontSize: 14 }}>필수 항목 정가</span>
|
||||
<span style={{ color: '#64748b', fontSize: 13, fontFamily: 'monospace', textDecoration: 'line-through' }}>{requiredOriginal.toLocaleString()}원</span>
|
||||
<span style={{ color: 'var(--jsm-ink-soft)', fontSize: 14 }}>필수 항목 정가</span>
|
||||
<span style={{ color: 'var(--jsm-ink-faint)', fontSize: 13, fontFamily: 'monospace', textDecoration: 'line-through' }}>{requiredOriginal.toLocaleString()}원</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
|
||||
<span style={{ color: '#ef4444', fontSize: 13, fontWeight: 600 }}>40% 할인 적용</span>
|
||||
<span style={{ color: '#34d399', fontSize: 14, fontWeight: 700, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}원</span>
|
||||
<span style={{ color: '#059669', fontSize: 14, fontWeight: 700, fontFamily: 'monospace' }}>{requiredTotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
{optionalTotal > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
|
||||
<span style={{ color: '#64748b', fontSize: 14 }}>선택 항목</span>
|
||||
<span style={{ color: '#a78bfa', fontSize: 14, fontFamily: 'monospace' }}>+{optionalTotal.toLocaleString()}원</span>
|
||||
<span style={{ color: 'var(--jsm-ink-soft)', fontSize: 14 }}>선택 항목</span>
|
||||
<span style={{ color: '#7c3aed', fontSize: 14, fontFamily: 'monospace' }}>+{optionalTotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||||
<span style={{ color: 'white', fontWeight: 700, fontSize: 16 }}>합계 (VAT 별도)</span>
|
||||
<span style={{ color: 'white', fontWeight: 800, fontSize: 24, fontFamily: 'monospace' }}>{grandTotal.toLocaleString()}원</span>
|
||||
<div style={{ borderTop: '1px solid var(--jsm-line)', paddingTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||||
<span style={{ color: 'var(--jsm-ink)', fontWeight: 700, fontSize: 16 }}>합계 (VAT 별도)</span>
|
||||
<span style={{ color: 'var(--jsm-ink)', fontWeight: 800, fontSize: 24, fontFamily: 'monospace' }}>{grandTotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,35 +470,36 @@ export default function QuotePage() {
|
||||
{/* ── 향후 관리 ── */}
|
||||
{(isPrinting || activeTab === 'maintenance') && quote.maintenance.length > 0 && (
|
||||
<div style={{ animation: 'fadeUp 0.4s ease', marginTop: isPrinting ? 40 : 0 }}>
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: '#818cf8', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid rgba(99,102,241,0.3)' }}>향후 관리</h2>}
|
||||
<p style={{ color: '#64748b', fontSize: 14, marginBottom: 20 }}>납품 후 유지보수 플랜을 선택해주세요 (선택 사항)</p>
|
||||
{isPrinting && <h2 className="print-section-title" style={{ fontSize: 20, fontWeight: 800, color: 'var(--jsm-accent)', marginBottom: 16, paddingBottom: 8, borderBottom: '2px solid var(--jsm-accent-soft)' }}>향후 관리</h2>}
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontSize: 14, marginBottom: 20 }}>납품 후 유지보수 플랜을 선택해주세요 (선택 사항)</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 16 }}>
|
||||
{quote.maintenance.map((plan) => {
|
||||
const isSelected = selectedMaintenance === plan.id;
|
||||
return (
|
||||
<div key={plan.id} onClick={() => setSelectedMaintenance(isSelected ? null : plan.id)}
|
||||
style={{
|
||||
background: isSelected ? 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.1))' : '#0f172a',
|
||||
border: `1px solid ${isSelected ? '#6366f1' : 'rgba(255,255,255,0.06)'}`,
|
||||
background: isSelected ? 'rgba(29,78,216,0.06)' : 'var(--jsm-surface)',
|
||||
border: `1px solid ${isSelected ? 'var(--jsm-accent)' : 'var(--jsm-line)'}`,
|
||||
borderRadius: 16, padding: 24, cursor: 'pointer', transition: 'all 0.25s', position: 'relative',
|
||||
boxShadow: isSelected ? '0 4px 16px rgba(29,78,216,0.1)' : '0 2px 8px rgba(0,0,0,0.04)',
|
||||
}}>
|
||||
{plan.recommended && (
|
||||
<div style={{ position: 'absolute', top: 16, right: 16, background: '#6366f1', color: 'white', fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 100 }}>추천</div>
|
||||
<div style={{ position: 'absolute', top: 16, right: 16, background: 'var(--jsm-accent)', color: 'white', fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 100 }}>추천</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
<input type="radio" checked={isSelected} onChange={() => {}} />
|
||||
<div>
|
||||
<div style={{ color: 'white', fontWeight: 700, fontSize: 16 }}>{plan.name}</div>
|
||||
<div style={{ color: '#475569', fontSize: 13 }}>{plan.period}</div>
|
||||
<div style={{ color: 'var(--jsm-ink)', fontWeight: 700, fontSize: 16 }}>{plan.name}</div>
|
||||
<div style={{ color: 'var(--jsm-ink-soft)', fontSize: 13 }}>{plan.period}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: isSelected ? '#818cf8' : 'white', marginBottom: 16, fontFamily: 'monospace' }}>
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: isSelected ? 'var(--jsm-accent)' : 'var(--jsm-ink)', marginBottom: 16, fontFamily: 'monospace' }}>
|
||||
{plan.monthlyFee === 0 ? '무료' : plan.monthlyFee.toLocaleString() + '원/월'}
|
||||
</div>
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ borderTop: '1px solid var(--jsm-line)', paddingTop: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{plan.includes.map((inc, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, fontSize: 13, color: '#94a3b8' }}>
|
||||
<span style={{ color: '#6366f1', flexShrink: 0, marginTop: 1 }}>✓</span>
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, fontSize: 13, color: 'var(--jsm-ink-soft)' }}>
|
||||
<span style={{ color: 'var(--jsm-accent)', flexShrink: 0, marginTop: 1 }}>✓</span>
|
||||
{inc}
|
||||
</div>
|
||||
))}
|
||||
@@ -513,32 +513,32 @@ export default function QuotePage() {
|
||||
|
||||
{/* 특이사항 */}
|
||||
{quote.notes && (
|
||||
<div style={{ marginTop: 40, background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', padding: 24 }}>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, color: '#475569', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.1em' }}>특이사항 및 참고사항</h3>
|
||||
<p style={{ color: '#64748b', fontSize: 14, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>{quote.notes}</p>
|
||||
<div style={{ marginTop: 40, background: 'var(--jsm-surface)', borderRadius: 12, border: '1px solid var(--jsm-line)', padding: 24 }}>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--jsm-ink-soft)', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.1em' }}>특이사항 및 참고사항</h3>
|
||||
<p style={{ color: 'var(--jsm-ink-soft)', fontSize: 14, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>{quote.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 고정 바 — 견적 수락 */}
|
||||
{quote.status !== 'accepted' && quote.status !== 'rejected' && !isExpired && (
|
||||
<div className="no-print" style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'rgba(10,15,30,0.95)', backdropFilter: 'blur(12px)', borderTop: '1px solid rgba(255,255,255,0.08)', padding: '16px 24px' }}>
|
||||
<div className="no-print" style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'var(--jsm-navy)', borderTop: '1px solid rgba(255,255,255,0.1)', padding: '16px 24px' }}>
|
||||
<div style={{ maxWidth: 900, margin: '0 auto', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<div style={{ color: '#64748b', fontSize: 13 }}>현재 선택된 견적 합계</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.55)', fontSize: 13 }}>현재 선택된 견적 합계</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||||
<span style={{ color: 'white', fontSize: 24, fontWeight: 800, fontFamily: 'monospace' }}>{grandTotal.toLocaleString()}원</span>
|
||||
{maintenanceTotal > 0 && selectedPlan && (
|
||||
<span style={{ color: '#6366f1', fontSize: 13 }}>+ {maintenanceTotal.toLocaleString()}원/월 ({selectedPlan.name})</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.65)', fontSize: 13 }}>+ {maintenanceTotal.toLocaleString()}원/월 ({selectedPlan.name})</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleAccept} disabled={submitting}
|
||||
style={{
|
||||
padding: '14px 36px', borderRadius: 12, border: 'none', cursor: 'pointer',
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
background: 'var(--jsm-accent)',
|
||||
color: 'white', fontSize: 16, fontWeight: 700, transition: 'all 0.2s',
|
||||
boxShadow: '0 8px 32px rgba(99,102,241,0.4)',
|
||||
boxShadow: '0 8px 32px rgba(29,78,216,0.4)',
|
||||
opacity: submitting ? 0.7 : 1,
|
||||
}}>
|
||||
{submitting ? '처리 중...' : '이 견적으로 진행하겠습니다 →'}
|
||||
@@ -549,8 +549,8 @@ export default function QuotePage() {
|
||||
|
||||
{/* 수락된 경우 */}
|
||||
{quote.status === 'accepted' && (
|
||||
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'rgba(16,185,129,0.1)', backdropFilter: 'blur(12px)', borderTop: '1px solid rgba(16,185,129,0.3)', padding: '16px 24px', textAlign: 'center' }}>
|
||||
<p style={{ color: '#34d399', fontWeight: 600, fontSize: 16 }}>✓ 이미 수락된 견적서입니다</p>
|
||||
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'rgba(5,150,105,0.08)', borderTop: '1px solid rgba(5,150,105,0.3)', padding: '16px 24px', textAlign: 'center' }}>
|
||||
<p style={{ color: '#059669', fontWeight: 600, fontSize: 16 }}>✓ 이미 수락된 견적서입니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -562,13 +562,13 @@ export default function QuotePage() {
|
||||
|
||||
function StatCard({ label, value, sub, color }: { label: string; value: string; sub: string; color: string }) {
|
||||
return (
|
||||
<div style={{ background: '#0f172a', border: `1px solid ${color}20`, borderRadius: 16, padding: 24 }}>
|
||||
<div style={{ color: '#475569', fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 10 }}>{label}</div>
|
||||
<div style={{ background: 'var(--jsm-surface)', border: `1px solid ${color}28`, borderRadius: 16, padding: 24, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
||||
<div style={{ color: 'var(--jsm-ink-soft)', fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 10 }}>{label}</div>
|
||||
<div style={{ color, fontSize: 28, fontWeight: 800, fontFamily: 'monospace', marginBottom: 4 }}>{value}</div>
|
||||
<div style={{ color: '#374151', fontSize: 12 }}>{sub}</div>
|
||||
<div style={{ color: 'var(--jsm-ink-faint)', fontSize: 12 }}>{sub}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: '#475569', textTransform: 'uppercase', letterSpacing: '0.08em' };
|
||||
const tdStyle: React.CSSProperties = { padding: '14px 16px', fontSize: 14, color: '#94a3b8' };
|
||||
const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: 'var(--jsm-ink-soft)', textTransform: 'uppercase', letterSpacing: '0.08em' };
|
||||
const tdStyle: React.CSSProperties = { padding: '14px 16px', fontSize: 14, color: 'var(--jsm-ink-soft)' };
|
||||
|
||||
@@ -6,9 +6,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
|
||||
return [
|
||||
{ url: base, lastModified: now, changeFrequency: 'weekly', priority: 1.0 },
|
||||
{ url: `${base}/packages`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${base}/services/music`, lastModified: now, changeFrequency: 'weekly', priority: 0.95 },
|
||||
{ url: `${base}/saju`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
|
||||
{ url: `${base}/outsourcing`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${base}/products`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 },
|
||||
{ url: `${base}/legal/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
|
||||
{ url: `${base}/legal/refund`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
|
||||
{ url: `${base}/legal/privacy`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { isServiceVisible } from '@/lib/service-visibility';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI 사주 분석',
|
||||
@@ -22,6 +24,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function SajuLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
export default async function SajuLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!(await isServiceVisible('saju'))) notFound();
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
446
docs/superpowers/plans/2026-06-11-renewal-phase1-design-ia.md
Normal file
446
docs/superpowers/plans/2026-06-11-renewal-phase1-design-ia.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# 사이트 리뉴얼 Phase 1 — 디자인 시스템 + IA 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **UI 페이지 태스크(5~8)는 구현 시 반드시 `designer` + `soft-skill` 스킬을 먼저 로드**하여 generic AI 패턴(과한 그래디언트·글래스모피즘·보라색 남용)을 차단할 것.
|
||||
|
||||
**Goal:** 쟁승메이드를 "외주 개발 + 완성 소프트웨어 판매" 2축의 전문 B2B 에이전시 사이트로 풀 리디자인하고, 기존 서비스(사주·음악·로또·설문)를 admin 전용 숨김 처리한다.
|
||||
|
||||
**Architecture:** 기존 다크 글래스 `--kx-*` 토큰 체계를 라이트 slate+딥블루 전문 토큰으로 전면 교체. Jua → Pretendard. 숨김은 `service_settings` 토글 + 서버 레이아웃 가드(admin_token 쿠키 예외). 페이지는 기존 라우트 구조 위에서 교체하며 web-backend·결제 로직은 건드리지 않는다.
|
||||
|
||||
**Tech Stack:** Next.js 16 App Router, TypeScript, Tailwind CSS v4, Supabase (Auth/DB), Pretendard(npm)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-11-site-renewal-outsourcing-products-design.md`
|
||||
|
||||
**검증 방식 노트:** 이 저장소에 테스트 인프라가 없고 Phase 1은 UI·라우팅 중심이므로 TDD 대신 `npm run build` + dev 서버 수동 E2E 체크리스트(Task 10)로 검증한다. 로직 단위 테스트는 Phase 2(주문/다운로드 검증 로직)에서 vitest 도입과 함께 시작.
|
||||
|
||||
---
|
||||
|
||||
## 사전 확인 사항 (현재 코드 상태)
|
||||
|
||||
- 레이아웃: `app/layout.tsx` (Jua 폰트, 음악 팩 중심 metadata/jsonLd) → 교체 대상
|
||||
- 셸: `app/components/DashboardShell.tsx` (standalone 분기) → 유지, `app/components/PublicShell.tsx`(TopNav+푸터) → 리뉴얼
|
||||
- 네비: `app/components/TopNav.tsx` — 링크가 SaaS제품/AI음악/커스텀외주 → 교체
|
||||
- 토큰: `app/globals.css` `--kx-*` 다크 테마 (#060e20 배경, 보라 #cc97ff 프라이머리) → 교체
|
||||
- 숨김 토글 인프라: `service_settings` 테이블 + `app/api/admin/services/route.ts`(GET/PATCH, DEFAULT_SERVICES) + `app/admin/services/page.tsx` 존재 → 재사용
|
||||
- admin 인증: `lib/admin-auth.ts`의 `verifyAdminTokenNode(token)` + `admin_token` 쿠키
|
||||
- 리다이렉트: `next.config.ts` redirects() 존재 → 추가만
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 디자인 토큰 + Pretendard 폰트 교체
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json` (pretendard 추가)
|
||||
- Modify: `app/globals.css` (토큰 교체)
|
||||
- Modify: `app/layout.tsx` (Jua 제거, Pretendard 적용)
|
||||
|
||||
- [ ] **Step 1: pretendard 설치**
|
||||
|
||||
```bash
|
||||
npm install pretendard
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `app/globals.css` 토큰 교체**
|
||||
|
||||
기존 `--kx-*` 변수 블록(40~51행 부근)을 **유지한 채 아래 신규 토큰을 추가**하고, 기존 kx 변수의 값만 새 라이트 테마로 재매핑한다 (kx 변수를 참조하는 기존 페이지가 많아 즉시 삭제하면 깨짐 — Phase 1 완료 후 잔여 참조 제거):
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* === JSM Professional tokens (2026-06 renewal) === */
|
||||
--jsm-bg: #f8fafc; /* slate-50 본문 배경 */
|
||||
--jsm-surface: #ffffff; /* 카드 */
|
||||
--jsm-surface-alt: #f1f5f9; /* slate-100 섹션 교차 배경 */
|
||||
--jsm-ink: #0f172a; /* slate-900 본문 텍스트 */
|
||||
--jsm-ink-soft: #475569; /* slate-600 보조 텍스트 */
|
||||
--jsm-ink-faint: #94a3b8; /* slate-400 캡션 */
|
||||
--jsm-line: #e2e8f0; /* slate-200 보더 */
|
||||
--jsm-navy: #0b1f3a; /* 딥네이비 — 푸터/다크 섹션 */
|
||||
--jsm-accent: #1d4ed8; /* blue-700 포인트 (단일 포인트 컬러) */
|
||||
--jsm-accent-hover: #1e40af; /* blue-800 */
|
||||
--jsm-accent-soft: #dbeafe; /* blue-100 뱃지 배경 */
|
||||
|
||||
/* 기존 kx 변수 재매핑 (잔여 참조 호환용) */
|
||||
--kx-surface: var(--jsm-bg);
|
||||
--kx-surface-low: var(--jsm-surface-alt);
|
||||
--kx-surface-mid: var(--jsm-surface);
|
||||
--kx-surface-high: var(--jsm-surface);
|
||||
--kx-surface-bright: var(--jsm-surface-alt);
|
||||
--kx-on-surface: var(--jsm-ink);
|
||||
--kx-on-variant: var(--jsm-ink-soft);
|
||||
--kx-primary: var(--jsm-accent);
|
||||
--kx-primary-dim: var(--jsm-accent-hover);
|
||||
--kx-secondary: var(--jsm-accent);
|
||||
--kx-secondary-dim: var(--jsm-accent-hover);
|
||||
--kx-outline: var(--jsm-line);
|
||||
}
|
||||
```
|
||||
|
||||
`body` 폰트 스택을 `var(--font-pretendard), Pretendard Variable, Pretendard, -apple-system, sans-serif`로 변경. `.kx-display`, `.kx-gradient-text`, `.kx-btn-primary` 등 기존 유틸 클래스는 새 토큰 기반으로 값만 정제 (gradient-text는 단색 `--jsm-ink`로, btn-primary는 `--jsm-accent` 솔리드로).
|
||||
|
||||
- [ ] **Step 3: `app/layout.tsx`에서 Jua 제거 + Pretendard import**
|
||||
|
||||
```tsx
|
||||
// 제거: import { Jua } from "next/font/google"; 및 jua 정의/사용
|
||||
import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
|
||||
// <html className={jua.variable}> → <html> (또는 className 제거)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 빌드 확인**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 성공 (스타일 회귀는 Task 4~8에서 페이지별 정리)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json app/globals.css app/layout.tsx
|
||||
git commit -m "feat(design): JSM 전문 토큰 체계 + Pretendard 도입, kx 토큰 재매핑"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 서비스 숨김 가드 라이브러리
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/service-visibility.ts`
|
||||
- Create: `supabase/migrations/2026-06-11-hide-legacy-services.sql`
|
||||
- Modify: `app/api/admin/services/route.ts` (DEFAULT_SERVICES 갱신)
|
||||
|
||||
- [ ] **Step 1: 마이그레이션 SQL 작성** (클라우드 + self-host 양쪽 적용 — 멱등)
|
||||
|
||||
```sql
|
||||
-- 2026-06-11 리뉴얼: 레거시 서비스 숨김 토글 시드
|
||||
-- service_settings: 신규 id 추가 (이미 있으면 무시)
|
||||
INSERT INTO service_settings (id, name, description, is_active, order_index)
|
||||
VALUES
|
||||
('saju', 'AI 사주 분석', '사주 입력 및 AI 해석 (레거시)', false, 101),
|
||||
('music', 'AI 음악 팩', '음악 가이드 패키지·샘플·스튜디오', false, 102),
|
||||
('gyeol', 'CONTOUR 설문', '/gyeol PMF 설문', false, 103),
|
||||
('packages', 'SaaS 제품 허브(구)', '구 /packages 페이지', false, 104),
|
||||
('lotto', '로또 추천', '로또 번호 추천 노출', false, 105)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `lib/service-visibility.ts` 작성**
|
||||
|
||||
```typescript
|
||||
import { cookies } from 'next/headers';
|
||||
import { createAdminClient } from '@/lib/supabase/admin';
|
||||
import { verifyAdminTokenNode } from '@/lib/admin-auth';
|
||||
|
||||
/** 숨김 가능 서비스 id (service_settings.id와 일치) */
|
||||
export type HideableService = 'saju' | 'music' | 'gyeol' | 'packages' | 'lotto';
|
||||
|
||||
/**
|
||||
* 서비스 노출 여부. admin_token 세션이면 항상 true.
|
||||
* service_settings 조회 실패(테이블 미생성 등) 시 안전하게 숨김(false).
|
||||
*/
|
||||
export async function isServiceVisible(id: HideableService): Promise<boolean> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('admin_token')?.value;
|
||||
if (token && verifyAdminTokenNode(token)) return true;
|
||||
|
||||
try {
|
||||
const supabase = createAdminClient();
|
||||
const { data, error } = await supabase
|
||||
.from('service_settings')
|
||||
.select('is_active')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (error || !data) return false;
|
||||
return data.is_active === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `app/api/admin/services/route.ts`의 `DEFAULT_SERVICES`를 신규 id 체계로 갱신**
|
||||
|
||||
```typescript
|
||||
const DEFAULT_SERVICES = [
|
||||
{ id: 'saju', name: 'AI 사주 분석', description: '사주 입력 및 AI 해석 (레거시)', is_active: false, order_index: 101 },
|
||||
{ id: 'music', name: 'AI 음악 팩', description: '음악 가이드 패키지·샘플·스튜디오', is_active: false, order_index: 102 },
|
||||
{ id: 'gyeol', name: 'CONTOUR 설문', description: '/gyeol PMF 설문', is_active: false, order_index: 103 },
|
||||
{ id: 'packages', name: 'SaaS 제품 허브(구)', description: '구 /packages 페이지', is_active: false, order_index: 104 },
|
||||
{ id: 'lotto', name: '로또 추천', description: '로또 번호 추천 노출', is_active: false, order_index: 105 },
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 마이그레이션 적용 (운영 DB 2곳)**
|
||||
|
||||
Run (CEO 또는 세션에서): 클라우드 Supabase SQL Editor + NAS self-host(`supa.jaengseung-made.com`) SQL Editor 양쪽에 Step 1 SQL 실행.
|
||||
Expected: 5 rows inserted (또는 0 — 재실행 시).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/service-visibility.ts supabase/migrations/2026-06-11-hide-legacy-services.sql app/api/admin/services/route.ts
|
||||
git commit -m "feat(visibility): service_settings 기반 서비스 숨김 가드 + 레거시 서비스 시드"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 레거시 라우트에 숨김 가드 적용
|
||||
|
||||
**Files:**
|
||||
- Create: `app/work/saju/layout.tsx`
|
||||
- Create: `app/music/layout.tsx`
|
||||
- Create: `app/gyeol/layout.tsx` (기존에 있으면 Modify — 가드만 추가)
|
||||
- Create: `app/packages/layout.tsx`
|
||||
|
||||
- [ ] **Step 1: 서버 레이아웃 가드 4개 작성** (모두 동일 패턴, id만 다름)
|
||||
|
||||
`app/work/saju/layout.tsx`:
|
||||
|
||||
```tsx
|
||||
import { notFound } from 'next/navigation';
|
||||
import { isServiceVisible } from '@/lib/service-visibility';
|
||||
|
||||
export default async function SajuLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!(await isServiceVisible('saju'))) notFound();
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
|
||||
`app/music/layout.tsx` → `isServiceVisible('music')`,
|
||||
`app/gyeol/layout.tsx` → `isServiceVisible('gyeol')` (기존 layout 있으면 함수 본문에 가드 추가),
|
||||
`app/packages/layout.tsx` → `isServiceVisible('packages')`.
|
||||
|
||||
주의: 해당 디렉토리에 기존 `layout.tsx`가 이미 있으면 새로 만들지 말고 기존 파일 상단에 가드 삽입. 클라이언트 레이아웃이면 서버 래퍼 레이아웃을 한 단계 위에 둘 수 없으므로 page 단위로 가드 서버 컴포넌트 래핑.
|
||||
|
||||
- [ ] **Step 2: dev 서버에서 동작 확인**
|
||||
|
||||
Run: `npm run dev` 후
|
||||
- 비로그인 브라우저(시크릿): `/work/saju`, `/music/packs`, `/gyeol`, `/packages` → 404
|
||||
- `/admin/login` 로그인 후 동일 URL → 정상 렌더
|
||||
Expected: 위와 같음
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/work/saju/layout.tsx app/music/layout.tsx app/gyeol/layout.tsx app/packages/layout.tsx
|
||||
git commit -m "feat(visibility): 사주·음악·설문·패키지 라우트 숨김 가드 적용"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: TopNav + 푸터(PublicShell) 리뉴얼
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/TopNav.tsx`
|
||||
- Modify: `app/components/PublicShell.tsx`
|
||||
|
||||
- [ ] **Step 1: TopNav 링크·CTA 교체**
|
||||
|
||||
```typescript
|
||||
const LINKS = [
|
||||
{ href: '/outsourcing', label: '외주 개발' },
|
||||
{ href: '/products', label: '소프트웨어' },
|
||||
];
|
||||
// CTA 버튼: "Try now"(→/music) 제거 → "프로젝트 문의" (→/outsourcing#contact)
|
||||
```
|
||||
|
||||
스타일: 라이트 배경 기준으로 재작성 — 스크롤 시 흰 배경 + `--jsm-line` 하단 보더 + 약한 그림자(현재의 다크 글래스 blur 제거). 로고 "JSM"은 gradient-text 제거, `--jsm-ink` 단색 + "쟁승메이드" 병기. 로그인/마이페이지/로그아웃 분기 로직은 그대로 유지(Supabase auth 구독 코드 무수정).
|
||||
|
||||
- [ ] **Step 2: PublicShell 푸터 링크 그룹 교체**
|
||||
|
||||
```
|
||||
서비스: 외주 개발(/outsourcing) · 소프트웨어(/products)
|
||||
회사: 문의하기(mailto) · 진행 프로세스(/outsourcing#process)
|
||||
Legal: 이용약관 · 개인정보처리방침 · 환불 정책 (유지)
|
||||
```
|
||||
|
||||
숨김 서비스 링크(SaaS 제품/AI 음악/AI 사주) 전부 제거. 푸터 배경은 `--jsm-navy`, 사업자 정보 라인 유지. `KakaoFloatButton` 유지.
|
||||
|
||||
- [ ] **Step 3: dev 서버에서 네비·푸터 렌더 확인** (모바일 드로어 포함)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/TopNav.tsx app/components/PublicShell.tsx
|
||||
git commit -m "feat(nav): 외주·소프트웨어 2축 네비게이션 + 푸터 리뉴얼"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 메인 페이지 리디자인 (`app/page.tsx`) + 메타데이터
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/page.tsx` (전면 교체)
|
||||
- Modify: `app/layout.tsx` (metadata + jsonLd 교체)
|
||||
|
||||
> **구현 전 designer + soft-skill 스킬 로드 필수.**
|
||||
|
||||
- [ ] **Step 1: `app/layout.tsx` metadata/jsonLd 교체**
|
||||
|
||||
- title default: `"외주 개발 · 완성 소프트웨어 | 쟁승메이드"`
|
||||
- description: `"7년차 대기업 백엔드 개발자가 직접 설계하고 만듭니다. 맞춤 소프트웨어 외주 개발과 검증된 완성 소프트웨어를 제공하는 쟁승메이드."`
|
||||
- keywords: 외주 개발, 소프트웨어 개발, 웹사이트 제작, 업무 자동화, 백엔드 개발자, 프리랜서 개발자
|
||||
- jsonLd: OfferCatalog에서 음악 팩·사주 Offer 제거, 외주 개발·웹사이트 제작 Service만 유지 (소프트웨어 제품 Offer는 Phase 2에서 동적 생성)
|
||||
|
||||
- [ ] **Step 2: 메인 페이지 섹션 구성으로 전면 재작성** (서버 컴포넌트, 이미지 없이 타이포·여백·SVG)
|
||||
|
||||
| 섹션 | 내용 (실제 카피) |
|
||||
|------|------|
|
||||
| Hero | 헤드라인: "필요한 소프트웨어, 만들어 드리거나 — 이미 만들어 두었습니다." 서브: "7년차 대기업 백엔드 개발자가 직접 설계·개발·운영합니다. 맞춤 외주 개발과 검증된 완성 소프트웨어 중 선택하세요." CTA 2개: [프로젝트 문의하기 → /outsourcing#contact] [소프트웨어 보기 → /products] |
|
||||
| 2축 서비스 | 카드 2장 — ① 외주 개발: "기획부터 배포·운영까지. 웹/API/자동화/봇" → /outsourcing ② 완성 소프트웨어: "결제 후 바로 다운로드. 직접 운영하며 검증한 도구" → /products |
|
||||
| 개발 프로세스 | 4단계 타임라인: 01 무료 상담·요구 정리 → 02 견적·범위 확정 → 03 개발·중간 공유 → 04 납품·배포 지원 (각 1줄 설명) |
|
||||
| 신뢰 요소 | 숫자 스탯: "7년차 대기업 백엔드" · "직접 운영 중인 서비스 15+" · "기획→배포 원스톱" + 기술 스택 라벨(Python·Java·Spring·Next.js·AI 연동) |
|
||||
| 포트폴리오 하이라이트 | 실제 운영 사례 3장: 주식 자동매매 시스템(텔레그램 연동) / 부동산 청약 자동 수집·매칭 / AI 콘텐츠 자동화 파이프라인 — 각 "직접 개발·운영 중" 뱃지, [더 보기 → /outsourcing#portfolio] |
|
||||
| 소프트웨어 진열 | Phase 1은 정적: "출시 준비 중인 제품" 카드 + [입고 알림 → /outsourcing#contact]. Phase 2에서 products 테이블 동적 진열로 교체 예정 주석 명시 |
|
||||
| 최종 CTA | 네이비 풀폭 밴드: "프로젝트, 이야기부터 시작하세요" + [무료 상담 신청] |
|
||||
|
||||
- [ ] **Step 3: 빌드 + 렌더 확인**
|
||||
|
||||
Run: `npm run build && npm run dev` → `/` 데스크톱·모바일 확인
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/page.tsx app/layout.tsx
|
||||
git commit -m "feat(home): 외주+소프트웨어 2축 메인 페이지 풀 리디자인"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: `/outsourcing` 페이지 신설 + 리다이렉트
|
||||
|
||||
**Files:**
|
||||
- Create: `app/outsourcing/page.tsx`
|
||||
- Modify: `next.config.ts` (redirects 추가)
|
||||
- 참고(복사 소스): `app/work/freelance/page.tsx`, `app/components/ContactForm.tsx`
|
||||
|
||||
> **구현 전 designer + soft-skill 스킬 로드 필수.** 단계형 폼은 Phase 3 — 이번엔 기존 `ContactForm` 재사용.
|
||||
|
||||
- [ ] **Step 1: `app/outsourcing/page.tsx` 작성** — 섹션 구성:
|
||||
|
||||
1. Hero: "맞춤 소프트웨어 외주 개발" + "기획 정리가 안 됐어도 괜찮습니다. 상담에서 함께 정리합니다."
|
||||
2. 제공 분야 카드 6: 웹 서비스 / 웹사이트 제작 / 업무 자동화(RPA·엑셀·크롤링) / API·백엔드 / 텔레그램·디스코드 봇 / AI 연동 개발
|
||||
3. `#process` 진행 프로세스: 상담 → 견적(영업일 2일 내) → 계약·착수 → 중간 공유(주 1회 이상) → 납품·검수 → 무상 하자보수 30일
|
||||
4. `#portfolio` 포트폴리오: 기존 `/work/freelance`의 실사례 + `/work/website/samples/*` 샘플 링크 카드 재사용
|
||||
5. 자주 묻는 질문 4개: 견적 기준 / 수정 횟수 / 소스코드 제공 / 유지보수
|
||||
6. `#contact` 의뢰 폼: 기존 `ContactForm` 컴포넌트 그대로 임베드 (새 토큰으로 스타일만 정제)
|
||||
|
||||
- [ ] **Step 2: `next.config.ts` redirects 추가**
|
||||
|
||||
```typescript
|
||||
{ source: '/work/freelance', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/work', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/work/website', destination: '/outsourcing', permanent: true },
|
||||
// 주의: /work/saju*, /work/website/samples/* 는 redirect 하지 않음 (admin 접근용 잔존)
|
||||
```
|
||||
|
||||
기존 redirects 중 destination이 `/work`·`/work/freelance`·`/work/website`인 항목은 새 destination(`/outsourcing`)으로 갱신해 redirect 체인 방지.
|
||||
|
||||
- [ ] **Step 3: dev 확인** — `/outsourcing` 렌더 + `/work/freelance` → `/outsourcing` 301 + 폼 제출 1회 테스트(접수 메일 수신)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/outsourcing/page.tsx next.config.ts
|
||||
git commit -m "feat(outsourcing): 외주 의뢰 페이지 신설 + work 라우트 리다이렉트"
|
||||
```
|
||||
|
||||
---### Task 7: `/login` 리디자인
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/login/page.tsx`
|
||||
|
||||
- [ ] **Step 1: 비주얼 리디자인** — Supabase 로직(signUp/signInWithPassword/signInWithOAuth, 에러 처리, redirect) **무수정**, 마크업·스타일만 교체:
|
||||
- 중앙 카드형 (max-w-sm, 흰 카드, `--jsm-line` 보더), 좌측 또는 상단에 "쟁승메이드" 워드마크
|
||||
- Google 버튼 → 이메일 폼 순서, 가입/로그인 토글 유지
|
||||
- 다크 배경·글래스 효과 제거, `--jsm-bg` 배경
|
||||
|
||||
- [ ] **Step 2: dev 확인** — 이메일 로그인 + Google OAuth 각 1회
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/login/page.tsx
|
||||
git commit -m "feat(login): 로그인 페이지 전문 톤 리디자인 (로직 무수정)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: `/mypage` 리디자인 + 탭 재구성
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/mypage/page.tsx` (1,048줄 — 탭 단위로 작업)
|
||||
|
||||
- [ ] **Step 1: 탭 구조 변경** — 기존 7탭 → 4탭:
|
||||
|
||||
| 새 탭 | 기존 소스 | 처리 |
|
||||
|------|----------|------|
|
||||
| 프로필 | 프로필 탭 | 유지·리스타일 |
|
||||
| 내 의뢰 | 의뢰 탭 | 유지·리스타일 (Phase 3에서 타임라인 고도화) |
|
||||
| 내 제품 | 팩 탭 | 명칭 변경·리스타일 — **다운로드 로직(sign-link 호출) 무수정** |
|
||||
| 주문 내역 | 결제+주문 탭 통합 | 병합·리스타일 |
|
||||
|
||||
사주·구독 탭: 렌더 분기에서 제외(코드는 주석이 아닌 조건 `false` 처리 또는 제거 — 데이터 로직은 남겨도 무방). 탭 상태/데이터 페칭 구조는 유지.
|
||||
|
||||
- [ ] **Step 2: 비주얼 리디자인** — 새 토큰 적용, 카드·테이블 정제. 다운로드 버튼/상태 뱃지 명확화("입금 확인 후 활성화됩니다" 안내 문구 포함).
|
||||
|
||||
- [ ] **Step 3: dev 확인** — 로그인 → 4탭 전환 → 기존 구매 계정으로 다운로드 버튼 동작(또는 테스트 데이터) 확인
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/mypage/page.tsx
|
||||
git commit -m "feat(mypage): 4탭 재구성 + 전문 톤 리디자인 (다운로드 로직 무수정)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 잔여 kx 참조·레거시 노출 정리
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/payment/success/page.tsx`, `app/payment/fail/page.tsx`, `app/quote/[token]/page.tsx`, `app/legal/*/page.tsx` — 새 토큰 기준 톤 정제 (구조 변경 없음)
|
||||
- Modify: `app/components/KakaoFloatButton.tsx` 등 공용 컴포넌트의 다크 전제 스타일
|
||||
|
||||
- [ ] **Step 1: 잔여 다크 전제 스타일 검색·정리**
|
||||
|
||||
Run: `grep -rn "kx-gradient-text\|GlassFilter\|LiquidGlass" app/ --include="*.tsx"`
|
||||
→ 노출 페이지(숨김 라우트 제외)에서 발견된 사용처를 새 토큰으로 정리. `app/layout.tsx`의 `<GlassFilter />` 제거.
|
||||
|
||||
- [ ] **Step 2: 빌드 확인** — `npm run build` 성공
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "refactor(design): 잔여 글래스·그래디언트 스타일 정리"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Phase 1 E2E 검증 (스펙 §8 시나리오 C + 회귀)
|
||||
|
||||
- [ ] **Step 1: 숨김 검증 (시나리오 C)**
|
||||
- 시크릿 창: `/work/saju`, `/music/packs`, `/music/studio`, `/gyeol`, `/packages` → 전부 404
|
||||
- admin 로그인 창: 동일 URL 전부 정상 렌더
|
||||
- `/admin/services`에서 'music' 토글 ON → 시크릿 창 `/music/packs` 정상 렌더 → 다시 OFF → 404
|
||||
|
||||
- [ ] **Step 2: 핵심 동선 회귀**
|
||||
- `/` → 네비·Hero CTA → `/outsourcing` → 폼 제출 → 접수 메일 수신 + `/admin/contacts`에 기록
|
||||
- 회원 로그인 → `/mypage` 4탭 정상 + (구매 데이터 있으면) 다운로드 링크 발급
|
||||
- `/work/freelance` → `/outsourcing` 301 확인
|
||||
- 모바일 뷰(375px): 네비 드로어·메인·외주 페이지 레이아웃 확인
|
||||
|
||||
- [ ] **Step 3: 빌드·배포 준비**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 에러 0. (배포는 CEO 확인 후 git push → Vercel)
|
||||
|
||||
- [ ] **Step 4: 최종 Commit + Phase 1 완료 보고**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "chore: Phase 1 디자인+IA 리뉴얼 완료"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2·3 예고 (별도 플랜으로 작성)
|
||||
|
||||
- **Phase 2 — 제품 판매 시스템**: DB 마이그레이션(products 확장·pack_files.product_id·orders 정비), `/products` 카탈로그·상세, 계좌이체 구매 모달 일반화, `/admin/orders`·`/admin/products`, sign-link 검증 orders 기반 교체, 기존 구매자 이관. vitest 도입.
|
||||
- **Phase 3 — 외주 고객 포털**: 단계형 의뢰 폼, contact↔quote FK, 상태 머신, `/track/[token]`, 자동 이메일(Resend), admin 의뢰·견적 통합 뷰.
|
||||
@@ -0,0 +1,157 @@
|
||||
# 쟁승메이드 사이트 리뉴얼 — 외주 의뢰 + 완성 소프트웨어 판매 중심 재구성
|
||||
|
||||
- **작성일**: 2026-06-11
|
||||
- **상태**: CEO 승인 완료
|
||||
- **목표**: 사이트 정체성을 "외주 개발 의뢰 수주 + 완성 소프트웨어 판매" 2축으로 재정립.
|
||||
전문 B2B 에이전시 수준의 UI/UX 풀 리디자인, 외주 의뢰 플로우 고도화(고객 포털 포함),
|
||||
NAS 기반 결제 후 다운로드 구조의 오류 제거.
|
||||
|
||||
---
|
||||
|
||||
## 0. 확정된 핵심 결정 (CEO)
|
||||
|
||||
| 항목 | 결정 |
|
||||
|------|------|
|
||||
| 결제 | **계좌이체 중심**으로 정리. PG(PortOne)는 코드만 보존, 유료라 추후 연결 |
|
||||
| 기존 서비스 (사주·음악 팩·로또·설문/gyeol·웹사이트 샘플 등) | **숨김** — 관리자만 토글·접근 가능 |
|
||||
| 외주 플로우 | **고객 포털까지** — 상태 추적 + 견적 열람/수락/거절 |
|
||||
| 디자인 | **풀 리디자인** — B2B 개발 에이전시 톤 (Pretendard, slate+딥블루) |
|
||||
| 제품 판매 | **범용 제품 시스템** — 음악 팩 전용 pack 인프라를 일반 제품으로 확장 |
|
||||
|
||||
## 1. 아키텍처 결정 (대안 비교 결과)
|
||||
|
||||
### 1-1. 외주 의뢰 데이터 모델 — `contact_requests` 확장 (채택)
|
||||
- 기존 테이블에 상태 머신 + `public_token` 컬럼 추가, `quotes.contact_request_id` FK로 연결.
|
||||
- 대안(projects 테이블 신설)은 이중 마이그레이션·전면 재작성 부담으로 기각.
|
||||
- **이유**: NAS self-host 전환 진행 중(클라우드/셀프호스트 양쪽 마이그레이션 필요) → 변경 폭 최소가 안전.
|
||||
|
||||
### 1-2. 제품 파일 시스템 — `products` 확장 + `pack_files.product_id` 연결 (채택)
|
||||
- `products`에 설명·`pay_method`·`is_hidden`(또는 is_active 재활용)·상세 콘텐츠 컬럼 추가.
|
||||
- `pack_files`에 `product_id` FK 추가, 기존 음악 팩 파일은 음악 제품에 연결.
|
||||
- 대안(신규 product_files 테이블)은 web-backend(packs-lab) 수정이 필요해 기각.
|
||||
- **이유**: DSM 링크 발급·HMAC·admin 업로드 인프라(web-backend) **무수정** 재사용.
|
||||
|
||||
### 1-3. 구매 식별 — `orders` 테이블 단일 소스 (채택)
|
||||
- 현재 `contact_requests.service` 문자열 파싱("구매 신청: AI 음악 마스터 팩 · 프로") 의존 → 제거.
|
||||
- 모든 구매는 `orders(product_id, status, method)` 기준. 기존 구매자는 이관 스크립트로 orders 생성.
|
||||
|
||||
## 2. 정보 구조 (IA)
|
||||
|
||||
```
|
||||
/ 메인 — Hero(외주+소프트웨어 2축), 서비스 소개, 개발 프로세스,
|
||||
포트폴리오 하이라이트, 제품 진열, 신뢰 요소(7년차 경력·실적), CTA
|
||||
/outsourcing 외주 의뢰 — 포트폴리오, 진행 프로세스 안내, 단계형 의뢰 폼
|
||||
/products 완성 소프트웨어 카탈로그 (is_active 제품만)
|
||||
/products/[id] 제품 상세 + 계좌이체 구매
|
||||
/track/[token] 비회원 의뢰 상태 추적 (이메일 링크로 접근)
|
||||
/login 리디자인 (Supabase Auth 유지: 이메일+Google OAuth)
|
||||
/mypage 리디자인 — 탭: 프로필 · 내 의뢰 · 내 제품/다운로드 · 주문 내역
|
||||
/quote/[token] 공개 견적서 (유지, 수락/거절 동기화 보강)
|
||||
/legal/* 유지 (톤만 리디자인)
|
||||
/admin/* 주문 관리(신설) + 제품 관리(packs 일반화) + 의뢰·견적 통합 뷰 + 숨김 서비스 토글
|
||||
```
|
||||
|
||||
### 숨김 처리 대상
|
||||
`/work/saju*`, `/music/*`, `/gyeol`, `/packages`, 로또 관련 노출 전부.
|
||||
- `service_settings` 토글 기반: 비활성 시 일반 사용자에게 404(notFound), **admin 세션(admin_token 쿠키)이면 접근 허용**.
|
||||
- 데이터(saju_records, 구독 등)는 보존 — 토글 재활성 시 복귀.
|
||||
- 기존 `/work/freelance` → `/outsourcing` redirect 추가 (next.config redirects).
|
||||
|
||||
### 외주로 통합
|
||||
- 웹사이트 제작(`/work/website`)은 외주 개발의 한 유형으로 `/outsourcing`에 통합.
|
||||
샘플 포트폴리오(`/work/website/samples/*`)는 포트폴리오 자료로 재사용 (숨김 아님).
|
||||
|
||||
## 3. 디자인 시스템 (풀 리디자인)
|
||||
|
||||
- **레이아웃**: 사이드바 대시보드형 → **상단 네비 + 풋터의 기업 사이트형**. 모바일은 햄버거 드로어.
|
||||
- **타이포**: Jua 제거 → **Pretendard** (next/font local 또는 CDN).
|
||||
- **컬러**: slate 뉴트럴 베이스 + 딥블루 포인트 1색. 카드/섹션/버튼/뱃지 공통 컴포넌트 정비.
|
||||
- **품질 가드**: 구현 시 designer·soft-skill 스킬 적용 — generic AI 패턴(과한 그래디언트, 이모지 남발, 보라색 남용) 차단. 이미지 없이 타이포·여백·SVG로 완성도.
|
||||
- admin 영역도 동일 토큰 적용하되 기능 우선의 밀도 높은 레이아웃.
|
||||
|
||||
## 4. 제품 구매 → 다운로드 흐름
|
||||
|
||||
```
|
||||
제품 상세(/products/[id]) → [구매하기] → 로그인 확인(미로그인 → /login?next=)
|
||||
→ 계좌이체 신청 모달 (케이뱅크 100-116-337157 박재오 안내, 약관 동의)
|
||||
→ orders 생성 (product_id, amount, status='pending', metadata.method='bank_transfer')
|
||||
→ 고객: 접수 확인 메일(Resend) / 관리자: 신규 주문 알림 메일
|
||||
→ /admin/orders: 입금 확인 [완료] 원클릭 → status='paid'
|
||||
→ 고객: "다운로드가 활성화되었습니다" 메일
|
||||
→ /mypage '내 제품': orders(status='paid') 기준 제품별 파일 목록
|
||||
→ 다운로드 클릭 → /api/packs/sign-link (검증을 orders 기반으로 교체)
|
||||
→ web-backend HMAC → DSM 공유 링크 (4시간 만료, 만료 시 재클릭으로 재발급)
|
||||
```
|
||||
|
||||
- `PurchaseAgreementModal`을 범용 제품용으로 일반화.
|
||||
- `PaymentButton`(PortOne)은 보존하되 `products.pay_method='bank_transfer'`일 때 미노출.
|
||||
추후 PG 계약 시 제품별 플래그만 변경하면 카드 결제 활성.
|
||||
- admin 팩 업로드 UI(/admin/packs)는 제품 관리(/admin/products) 안으로 통합 — 제품 선택 → 파일 업로드.
|
||||
- tier(starter/pro/master) 개념은 음악 팩 하위 호환용으로 유지하되, 신규 제품은 product_id 직접 매칭.
|
||||
|
||||
## 5. 외주 의뢰 플로우 (고객 포털 포함)
|
||||
|
||||
### 상태 머신 (contact_requests.status)
|
||||
`pending(접수) → reviewing(검토중) → quoted(견적발송) → accepted(수주)/on_hold(보류) → in_progress(진행중) → completed(완료)`
|
||||
(+ `cancelled`)
|
||||
|
||||
### 흐름
|
||||
```
|
||||
[고객] /outsourcing 단계형 의뢰 폼
|
||||
단계: ① 프로젝트 유형 → ② 예산/희망 일정 → ③ 상세 요구사항 → ④ 연락처(로그인 시 자동 채움)
|
||||
→ contact_requests 생성 (public_token 발급) + 접수 확인 메일(추적 링크 /track/[token] 포함)
|
||||
+ 관리자 알림 메일 (기존 Resend 흐름 유지)
|
||||
|
||||
[관리자] /admin 의뢰 상세 (contacts + quotes 통합 뷰)
|
||||
→ 상태 변경 드롭다운 (상태 머신 순서 강제)
|
||||
→ 견적서 작성 시 contact_request_id 자동 연결
|
||||
→ [견적 발송] 버튼 → quotes.status='sent' + 고객 메일 자동 발송(견적 링크 포함)
|
||||
+ contact_requests.status='quoted' 동기화
|
||||
|
||||
[고객] /track/[token] (비회원) 또는 /mypage '내 의뢰' (회원)
|
||||
→ 상태 타임라인 표시 + 연결된 견적서 열람
|
||||
→ 견적 수락/거절 버튼 → quotes.status='accepted'/'rejected'
|
||||
+ contact_requests.status 동기화('accepted'/'on_hold') + 관리자 알림 메일
|
||||
```
|
||||
|
||||
- rate limit·XSS 방지 등 기존 `/api/contact` 보안 로직 유지.
|
||||
- 견적서 작성 시 DB 자동 등록 원칙 유지 ([[feedback_quotes_admin]]).
|
||||
|
||||
## 6. DB 마이그레이션 (클라우드 + self-host 양쪽 적용)
|
||||
|
||||
신규 마이그레이션 SQL 파일 1~2개로 작성 (`supabase/migrations/2026-06-11-*.sql`):
|
||||
|
||||
1. `products`: `description_long TEXT`, `pay_method TEXT DEFAULT 'bank_transfer'`, `features JSONB`, `sort_order INT` 추가
|
||||
2. `pack_files`: `product_id TEXT REFERENCES products(id)` 추가 + 기존 음악 팩 파일 product_id 백필
|
||||
3. `contact_requests`: `public_token TEXT UNIQUE`, `budget TEXT`, `timeline TEXT`, `project_type TEXT` 추가
|
||||
+ status CHECK 갱신 (새 상태 머신)
|
||||
4. `quotes`: `contact_request_id UUID REFERENCES contact_requests(id)` 추가
|
||||
5. 기존 음악 팩 구매자(contact_requests 'completed' + "구매 신청:" 패턴) → orders 이관 스크립트
|
||||
6. RLS: `/track/[token]`용 조회는 서버(admin client)에서만 — 테이블 직접 노출 없음
|
||||
|
||||
**운영 주의**: 현 운영=Vercel+클라우드 Supabase, NAS self-host로 전환 직전(Phase 6 ③ DNS 전환 대기).
|
||||
마이그레이션은 **양쪽 DB에 동일 순서로 적용**하고, 이관 스크립트는 멱등하게 작성.
|
||||
|
||||
## 7. 구현 단계
|
||||
|
||||
| Phase | 내용 | 검증 |
|
||||
|-------|------|------|
|
||||
| **1. 디자인 시스템 + IA** | 디자인 토큰·공통 컴포넌트, 상단 네비 레이아웃 전환, 메인·/outsourcing 리디자인, 숨김 서비스 처리, /login·/mypage 리디자인 | 전 페이지 렌더·반응형·숨김 토글 확인 |
|
||||
| **2. 제품 판매 시스템** | DB 마이그레이션, /products 카탈로그·상세, 계좌이체 구매 플로우, /admin/orders·/admin/products, sign-link 검증 교체, 기존 구매자 이관 | 회원 구매→입금확인→다운로드 E2E |
|
||||
| **3. 외주 고객 포털** | 단계형 폼, contact↔quote 연결, 상태 머신, /track/[token], 자동 이메일, admin 통합 뷰 | 비회원 의뢰→견적→수락 E2E |
|
||||
|
||||
## 8. E2E 검증 시나리오 (각 Phase 종료 시 수동)
|
||||
|
||||
- **A. 외주 (비회원)**: 의뢰 폼 제출 → 접수 메일 수신 → /track 접속 → admin 견적 발송 → 메일 링크로 견적 열람 → 수락 → admin 알림 확인
|
||||
- **B. 제품 구매 (회원)**: 회원가입/로그인 → 제품 구매 신청 → 접수 메일 → admin 입금 확인 → 다운로드 활성 메일 → mypage 다운로드 → DSM 링크 파일 수신
|
||||
- **C. 숨김 서비스**: 일반 사용자 404 확인 / admin 세션 접근 확인 / 토글 재활성 복귀 확인
|
||||
- **D. 회귀**: 기존 음악 팩 구매자 mypage 다운로드 정상 동작 (이관 후)
|
||||
|
||||
## 9. 의도적 제외 (이번 범위 아님)
|
||||
|
||||
- PG(카드) 결제 활성화 — 플래그 구조만 준비
|
||||
- 관리자 RBAC(권한 분화)
|
||||
- 구독 정기 결제
|
||||
- ZIP 일괄 다운로드, 다운로드 횟수 제한
|
||||
- 사주 SaaS 도메인 분리 (별도 트랙)
|
||||
- web-backend(packs-lab) 코드 수정
|
||||
31
lib/service-visibility.ts
Normal file
31
lib/service-visibility.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { createAdminClient } from '@/lib/supabase/admin';
|
||||
import { verifyAdminTokenNode } from '@/lib/admin-auth';
|
||||
|
||||
/** 숨김 가능 서비스 id (service_settings.id와 일치) */
|
||||
export type HideableService = 'saju' | 'music' | 'gyeol' | 'packages' | 'lotto';
|
||||
|
||||
/**
|
||||
* 서비스 노출 여부. admin_token 세션이면 항상 true.
|
||||
* service_settings 조회 실패(테이블 미생성 등) 시 안전하게 숨김(false).
|
||||
* @warning 레거시 숨김 전용 — 일반 공개 서비스(products 등) 가드에 재사용 금지.
|
||||
* fail-closed 정책이라 DB 일시 장애 시 404가 됨. 캐싱 없음(매 렌더 DB 조회).
|
||||
*/
|
||||
export async function isServiceVisible(id: HideableService): Promise<boolean> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('admin_token')?.value;
|
||||
if (token && verifyAdminTokenNode(token)) return true;
|
||||
|
||||
try {
|
||||
const supabase = createAdminClient();
|
||||
const { data, error } = await supabase
|
||||
.from('service_settings')
|
||||
.select('is_active')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (error || !data) return false;
|
||||
return data.is_active === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,18 @@ const nextConfig: NextConfig = {
|
||||
{ source: '/services/music', destination: '/music/packs', permanent: true },
|
||||
{ source: '/services/music/samples', destination: '/music/samples', permanent: true },
|
||||
{ source: '/studio', destination: '/music/studio', permanent: true },
|
||||
// 커스텀 외주 마이그
|
||||
{ source: '/freelance', destination: '/work/freelance', permanent: true },
|
||||
{ source: '/services/website', destination: '/work/website', permanent: true },
|
||||
// 커스텀 외주 마이그 (2026-06-11 리뉴얼: work 라우트 → /outsourcing 통합)
|
||||
{ source: '/work/freelance', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/work', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/work/website', destination: '/outsourcing', permanent: true },
|
||||
// 구 URL은 체인 없이 한 번에 /outsourcing 으로
|
||||
{ source: '/freelance', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/services/website', destination: '/outsourcing', permanent: true },
|
||||
// 샘플 데모는 포트폴리오용으로 잔존 → samples 라우트 유지
|
||||
{ source: '/services/website/samples/:slug', destination: '/work/website/samples/:slug', permanent: true },
|
||||
// 블로그 자동화 폐기(2026-05-29 재정의): 기존 URL은 제품 라인 허브로 안내
|
||||
{ source: '/services/blog', destination: '/work', permanent: true },
|
||||
{ source: '/work/blog', destination: '/work', permanent: true },
|
||||
// 블로그 자동화 폐기(2026-05-29 재정의): 기존 URL은 외주 허브로 안내
|
||||
{ source: '/services/blog', destination: '/outsourcing', permanent: true },
|
||||
{ source: '/work/blog', destination: '/outsourcing', permanent: true },
|
||||
// 사주 마이그 (단순 URL, 카탈로그 spec은 보류)
|
||||
{ source: '/saju', destination: '/work/saju', permanent: true },
|
||||
{ source: '/saju/input', destination: '/work/saju/input', permanent: true },
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"lunar-javascript": "^1.7.7",
|
||||
"next": "^16.2.6",
|
||||
"openai": "^6.21.0",
|
||||
"pretendard": "^1.3.9",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -7958,6 +7959,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretendard": {
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/pretendard/-/pretendard-1.3.9.tgz",
|
||||
"integrity": "sha512-PaQAADyLY5v4kYFwkpSJHbSSYIkiriY/1xXw75TKoZ9UQQqeU+tvP05yTdZAWibiIYoo8ZKtRv8PM7w0IaywSw==",
|
||||
"license": "OFL-1.1"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"lunar-javascript": "^1.7.7",
|
||||
"next": "^16.2.6",
|
||||
"openai": "^6.21.0",
|
||||
"pretendard": "^1.3.9",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-markdown": "^9.0.1",
|
||||
|
||||
15
supabase/migrations/2026-06-11-hide-legacy-services.sql
Normal file
15
supabase/migrations/2026-06-11-hide-legacy-services.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- 2026-06-11 리뉴얼: 레거시 서비스 숨김 토글 시드
|
||||
-- service_settings: 이미 있으면 숨김 상태로 갱신 (2026-06 리뉴얼 의도 강제) — 멱등
|
||||
INSERT INTO service_settings (id, name, description, is_active, order_index)
|
||||
VALUES
|
||||
('saju', 'AI 사주 분석', '사주 입력 및 AI 해석 (레거시)', false, 101),
|
||||
('music', 'AI 음악 팩', '음악 가이드 패키지·샘플·스튜디오', false, 102),
|
||||
('gyeol', 'CONTOUR 설문', '/gyeol PMF 설문', false, 103),
|
||||
('packages', 'SaaS 제품 허브(구)', '구 /packages 페이지', false, 104),
|
||||
('lotto', '로또 추천', '로또 번호 추천 노출', false, 105)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
is_active = EXCLUDED.is_active,
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
order_index = EXCLUDED.order_index,
|
||||
updated_at = now();
|
||||
Reference in New Issue
Block a user