feat: PublicShell + TopNav + 홈 v6 (ai_music_creator 참조)

- 사이드바 대시보드는 /admin, /mypage 에서만 사용
- 공개 페이지는 상단 TopNav + 다크 footer(PublicShell)
- 홈 v6: Hero + Evidence + Before/After + Toolkit + 3-Step + Other Products

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 02:18:30 +09:00
parent 6c74b2cc93
commit a9b53a3327
4 changed files with 552 additions and 212 deletions

View File

@@ -3,19 +3,26 @@
import { useState } from 'react';
import { usePathname } from 'next/navigation';
import Sidebar from './Sidebar';
import PublicShell from './PublicShell';
const STANDALONE_PATHS = ['/login', '/signup', '/admin'];
const SIDEBAR_PATHS = ['/mypage'];
export default function DashboardShell({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const pathname = usePathname();
const isStandalone = STANDALONE_PATHS.some((p) => pathname.startsWith(p));
const useSidebar = SIDEBAR_PATHS.some((p) => pathname.startsWith(p));
if (isStandalone) {
return <>{children}</>;
}
if (!useSidebar) {
return <PublicShell>{children}</PublicShell>;
}
return (
<div className="dashboard-layout">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />

View File

@@ -0,0 +1,51 @@
import TopNav from './TopNav';
import Link from 'next/link';
export default function PublicShell({ children }: { children: React.ReactNode }) {
return (
<>
<TopNav />
<main
className="min-h-screen pt-20"
style={{
background: 'var(--kx-surface)',
color: 'var(--kx-on-surface)',
}}
>
{children}
<footer
className="mt-20 px-6 lg:px-12 py-10 text-xs"
style={{
background: 'var(--kx-surface-low)',
color: 'var(--kx-on-variant)',
borderTop: '1px solid rgba(255,255,255,0.05)',
}}
>
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row justify-between gap-6 mb-5">
<div>
<p className="kx-display font-extrabold text-lg mb-2" style={{ color: 'var(--kx-on-surface)' }}></p>
<p className="leading-relaxed max-w-md">
AI · · . · AI .
</p>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-2">
<Link href="/legal/terms" className="hover:text-white transition"></Link>
<Link href="/legal/privacy" className="hover:text-white transition"></Link>
<Link href="/legal/refund" className="hover:text-white transition"> </Link>
</div>
</div>
<div className="pt-5 border-t flex flex-wrap gap-x-4 gap-y-1 leading-relaxed" style={{ borderColor: 'rgba(255,255,255,0.05)' }}>
<span>대표자: 박재오</span>
<span>사업자등록번호: 267-53-00822</span>
<span>주소: 서울시 22 22, 1 109</span>
<span>전화: 010-3907-1392</span>
<span>이메일: bgg8988@gmail.com</span>
</div>
<p className="mt-3">© 2026 . All rights reserved.</p>
</div>
</footer>
</main>
</>
);
}

152
app/components/TopNav.tsx Normal file
View File

@@ -0,0 +1,152 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState, useEffect } from 'react';
const LINKS = [
{ href: '/', label: 'Home' },
{ href: '/services/music', label: 'AI 음악' },
{ href: '/services/blog', label: '블로그 팩' },
{ href: '/saju', label: 'AI 사주' },
{ href: '/legal/refund', label: '환불정책' },
];
export default function TopNav() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => { setOpen(false); }, [pathname]);
const isActive = (href: string) =>
href === '/' ? pathname === '/' : pathname.startsWith(href);
return (
<>
<nav
className={`fixed top-0 inset-x-0 z-50 h-20 px-6 lg:px-12 flex items-center justify-between transition-all ${
scrolled ? 'backdrop-blur-md' : ''
}`}
style={{
background: scrolled ? 'rgba(6,14,32,0.85)' : 'rgba(6,14,32,0.55)',
borderBottom: '1px solid rgba(204,151,255,0.08)',
boxShadow: scrolled ? '0 8px 40px 0 rgba(156,72,234,0.12)' : 'none',
}}
>
<Link
href="/"
className="kx-display text-2xl font-black tracking-tight kx-gradient-text"
style={{ textDecoration: 'none' }}
>
</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">
<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="/services/music"
className="kx-btn-primary hidden sm:inline-flex items-center px-5 py-2 rounded-full text-sm"
style={{ textDecoration: 'none' }}
>
</Link>
<button
onClick={() => setOpen(true)}
aria-label="메뉴 열기"
className="md:hidden p-2 rounded-lg"
style={{ color: 'var(--kx-on-surface)' }}
>
<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>
{/* 모바일 오버레이 */}
{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"></span>
<button
onClick={() => setOpen(false)}
aria-label="메뉴 닫기"
className="p-2"
style={{ color: 'var(--kx-on-surface)' }}
>
<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">
{LINKS.map((l) => (
<Link
key={l.href}
href={l.href}
className="kx-display text-2xl font-bold py-3"
style={{
color: isActive(l.href) ? 'var(--kx-primary)' : 'var(--kx-on-surface)',
textDecoration: 'none',
}}
>
{l.label}
</Link>
))}
<div className="mt-6 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="/services/music"
className="kx-btn-primary flex-1 py-3 text-center rounded-full text-sm"
style={{ textDecoration: 'none' }}
>
</Link>
</div>
</div>
</div>
)}
</>
);
}