Files
jaengseung-made/app/components/TopNav.tsx
gahusb 601bc38a12 feat(nav): TopNav supabase auth 구독 + 로그인 상태 토글
- 로그아웃 시: "로그인" link + "Try now" 버튼 (기존)
- 로그인 시: "마이페이지" link + "로그아웃" 버튼 (신규)
- 데스크톱 + 모바일 오버레이 둘 다 동일 패턴
- Sidebar.tsx:93-103 의 auth 구독 패턴 차용

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:46:16 +09:00

248 lines
8.6 KiB
TypeScript

'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { User } from '@supabase/supabase-js';
const LINKS = [
{ href: '/', label: '홈' },
{ href: '/services/music/samples', label: '샘플' },
{ href: '/services/music', label: '팩 상세' },
{ href: '/studio', label: '스튜디오' },
{ href: '/freelance', label: '외주' },
];
export default function TopNav() {
const pathname = usePathname();
const router = useRouter();
const supabase = createClient();
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Supabase auth state subscription (Sidebar.tsx:93-103 패턴)
useEffect(() => {
let mounted = true;
supabase.auth.getUser().then(({ data }) => {
if (mounted) setUser(data.user ?? null);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
if (mounted) setUser(session?.user ?? null);
});
return () => {
mounted = false;
subscription.unsubscribe();
};
}, [supabase]);
const handleLogout = async () => {
await supabase.auth.signOut();
setOpen(false);
router.push('/');
router.refresh();
};
useEffect(() => { setOpen(false); }, [pathname]);
useEffect(() => {
if (open) {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}
}, [open]);
const isActive = (href: string) => {
if (href === '/') return pathname === '/';
if (href === '/services/music') return pathname === '/services/music';
return pathname.startsWith(href);
};
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(' ')}
style={{
background: scrolled ? 'rgba(10,10,12,0.6)' : 'transparent',
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : '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>
<button
onClick={handleLogout}
className="hidden sm:inline-flex items-center px-5 py-2 rounded-full text-sm font-medium transition-colors"
style={{
color: 'var(--kx-on-surface)',
border: '1px solid rgba(255,255,255,0.15)',
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="/services/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)' }}
>
<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)' }}
>
<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">
{user ? (
<>
<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>
<button
onClick={handleLogout}
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)', background: 'transparent' }}
>
</button>
</>
) : (
<>
<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' }}
>
Try now
</Link>
</>
)}
</div>
</div>
</div>
)}
</>
);
}