feat(ia): SaaS 제품 카탈로그(/packages) + 네비를 SaaS·음악·외주 3축으로 재편

- lib/saas-catalog.ts: 확장 가능한 SaaS 제품 데이터 모델(배열에 추가 시 자동 노출)
- app/packages: 카탈로그 페이지 — available 카드 그리드 / coming_soon / 빈 상태 예고+출시 알림 수집(ContactModal 재사용)
- TopNav·Footer: SaaS 제품(/packages)·AI 음악(/music)·커스텀 외주(/work) 3축
- 홈 Hero·라벨 카피를 새 정체성으로 정렬, 'Custom Build/사업부' 잔재 정리
- sitemap에 /packages 등록, STRATEGY.md에 크몽·숨고 미사용+인스타 유입 정책 명시
- 음악은 카탈로그에 넣지 않고 단품 라인(/music) 유지

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 16:08:28 +09:00
parent ec8c4345b8
commit 4eee1b5c31
10 changed files with 292 additions and 18 deletions

View File

@@ -75,17 +75,24 @@ export default function PublicShell({ children }: { children: React.ReactNode })
</div>
{/* 우 — Link groups */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-10">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-10">
<div>
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">Music</p>
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">SaaS </p>
<ul className="space-y-2.5">
<li><Link href="/music/packs" className="hover:text-white transition">AI </Link></li>
<li><Link href="/packages" className="hover:text-white transition"> </Link></li>
<li><Link href="/packages" className="hover:text-white transition"> </Link></li>
</ul>
</div>
<div>
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">AI </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>
</ul>
</div>
<div>
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4">Custom Build</p>
<p className="font-mono text-[11px] tracking-widest uppercase text-white/40 mb-4"> </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>

View File

@@ -7,8 +7,9 @@ import { createClient } from '@/lib/supabase/client';
import type { User } from '@supabase/supabase-js';
const LINKS = [
{ href: '/music', label: 'Music' },
{ href: '/work', label: 'Custom Build' },
{ href: '/packages', label: 'SaaS 제품' },
{ href: '/music', label: 'AI 음악' },
{ href: '/work', label: '커스텀 외주' },
];
export default function TopNav() {

18
app/packages/layout.tsx Normal file
View File

@@ -0,0 +1,18 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'SaaS 제품 · 월 구독 패키지',
description:
'현직 엔지니어가 실제 운영하며 검증한 자동화를 월 구독 SaaS 제품으로 제공합니다. 첫 제품 준비 중 — 출시 알림을 신청하세요.',
keywords: ['SaaS', '자동화 구독', '월 구독 자동화', 'AI 자동화 제품', '쟁승메이드'],
openGraph: {
title: 'SaaS 제품 · 월 구독 패키지 | 쟁승메이드',
description:
'검증된 자동화를 SaaS로. 현직 엔지니어가 직접 운영·검증한 자동화 제품 카탈로그.',
url: 'https://jaengseung-made.com/packages',
},
};
export default function PackagesLayout({ children }: { children: React.ReactNode }) {
return children;
}

173
app/packages/page.tsx Normal file
View File

@@ -0,0 +1,173 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import ContactModal from '@/app/components/ContactModal';
import { trackCTAClick } from '@/lib/gtag';
import {
getAvailablePackages,
getComingSoonPackages,
type SaasCatalogItem,
} from '@/lib/saas-catalog';
const WAITLIST_SERVICE = 'SaaS 출시 알림 신청';
function PackageCard({ pkg, dimmed }: { pkg: SaasCatalogItem; dimmed?: boolean }) {
const inner = (
<>
<div className="flex items-center justify-between mb-3">
<p className="font-mono text-[10px] uppercase tracking-widest text-white/50">
{pkg.category}
</p>
{pkg.badge && (
<span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full border border-white/30 text-white/80">
{pkg.badge}
</span>
)}
{dimmed && !pkg.badge && (
<span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full border border-white/20 text-white/50">
Coming Soon
</span>
)}
</div>
<h3 className="kx-display text-xl font-bold text-white mb-1.5">{pkg.name}</h3>
<p className="text-sm text-white/70 mb-3">{pkg.tagline}</p>
<p className="text-xs text-white/55 leading-relaxed mb-4 flex-1">{pkg.description}</p>
<ul className="space-y-2 mb-5">
{pkg.features.map((f) => (
<li key={f} className="flex gap-2 text-xs text-white/70">
<span className="text-white/40">·</span>
<span className="leading-relaxed">{f}</span>
</li>
))}
</ul>
<div className="mt-auto flex items-center justify-between">
{pkg.priceLabel ? (
<span className="font-mono text-sm text-white">{pkg.priceLabel}</span>
) : (
<span className="font-mono text-xs text-white/40"> </span>
)}
{!dimmed && <span aria-hidden className="text-white/50 text-sm"></span>}
</div>
</>
);
const base =
'group rounded-2xl border p-6 flex flex-col transition';
if (dimmed) {
return (
<div className={`${base} border-white/10 bg-white/[0.01] opacity-60`}>{inner}</div>
);
}
return (
<Link
href={pkg.href ?? '#'}
onClick={() => trackCTAClick(`packages_card_${pkg.slug}`)}
className={`${base} border-white/15 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05]`}
style={{ textDecoration: 'none' }}
>
{inner}
</Link>
);
}
export default function PackagesPage() {
const [modalOpen, setModalOpen] = useState(false);
const available = getAvailablePackages();
const comingSoon = getComingSoonPackages();
const isEmpty = available.length === 0;
return (
<div className="min-h-screen bg-black text-white">
<ContactModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
service={WAITLIST_SERVICE}
checklist={['관심 있는 업무·자동화 분야', '연락받을 이메일', '현재 겪는 반복 업무(선택)']}
/>
{/* Hero */}
<section className="relative w-full min-h-[60vh] flex items-center justify-center px-6 border-b border-white/10">
<div className="absolute inset-0 bg-gradient-to-b from-[#0a0618] to-black pointer-events-none" />
<div className="relative z-10 max-w-3xl mx-auto text-center">
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-4">
SaaS Products
</p>
<h1
className="kx-display text-4xl md:text-6xl font-bold mb-5"
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
>
<br />SaaS로 .
</h1>
<p className="text-base md:text-lg text-white/70 max-w-2xl mx-auto leading-relaxed">
.
{isEmpty ? ' 첫 제품을 준비하고 있습니다.' : ''}
</p>
{isEmpty && (
<button
onClick={() => {
trackCTAClick('packages_waitlist_hero');
setModalOpen(true);
}}
className="kx-btn-primary inline-flex items-center px-7 py-3 rounded-full text-sm mt-8"
>
</button>
)}
</div>
</section>
{/* Available 카탈로그 */}
{available.length > 0 && (
<section className="py-20 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{available.map((pkg) => (
<PackageCard key={pkg.slug} pkg={pkg} />
))}
</div>
</section>
)}
{/* Coming Soon 예고 */}
{comingSoon.length > 0 && (
<section className="py-20 px-6 bg-white/[0.02] border-t border-white/10">
<div className="max-w-6xl mx-auto">
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-4 text-center">
Coming Soon
</p>
<h2 className="kx-display text-2xl md:text-3xl font-bold text-center mb-10">
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{comingSoon.map((pkg) => (
<PackageCard key={pkg.slug} pkg={pkg} dimmed />
))}
</div>
</div>
</section>
)}
{/* 출시 알림 CTA — 항상 노출(빈 상태 아닐 때도 대기자 수집) */}
<section className="py-20 px-6 border-t border-white/10">
<div className="max-w-3xl mx-auto text-center">
<h2 className="kx-display text-2xl md:text-4xl font-bold mb-5">
?
</h2>
<p className="text-base text-white/70 mb-8">
. .
</p>
<button
onClick={() => {
trackCTAClick('packages_waitlist_cta');
setModalOpen(true);
}}
className="kx-btn-primary inline-flex items-center px-7 py-3 rounded-full text-sm"
>
</button>
</div>
</section>
</div>
);
}

View File

@@ -92,11 +92,11 @@ export default function Home() {
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 /> .
<br /> .
</h1>
<p className="text-base md:text-xl text-white/70 leading-relaxed">
AI , .
SaaS로. AI .
</p>
</div>
</section>
@@ -140,7 +140,7 @@ export default function Home() {
</div>
</Link>
{/* Custom Build 카드 */}
{/* 커스텀 외주 카드 */}
<Link
href="/work"
onClick={() => trackCTAClick('home_v7_card_work')}
@@ -153,10 +153,10 @@ export default function Home() {
>
<div className="relative z-10">
<p className="font-mono text-[11px] tracking-widest uppercase text-white/60 mb-3">
Custom Build
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
@@ -351,12 +351,12 @@ export default function Home() {
</div>
</section>
{/* 4. Custom Build 섹션 — 4 카드 + 5건 사례 + 견적 CTA */}
{/* 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 Build
Custom Work
</p>
<h2
className="kx-display text-3xl md:text-5xl font-bold mb-5"

View File

@@ -6,6 +6,7 @@ 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}/legal/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },

View File

@@ -52,13 +52,13 @@ export default function WorkHub() {
<div className="absolute inset-0 bg-gradient-to-b from-[#060e20] to-black pointer-events-none" />
<div className="relative z-10 max-w-3xl mx-auto text-center">
<p className="font-mono text-[11px] tracking-widest uppercase text-white/50 mb-4">
Custom Build
Custom Work
</p>
<h1
className="kx-display text-4xl md:text-6xl font-bold mb-5"
style={{ wordBreak: 'keep-all', letterSpacing: '-0.02em' }}
>
</h1>
<p className="text-base md:text-lg text-white/70 max-w-2xl mx-auto leading-relaxed">
7 ··. , , AI .