대시보드 중간 커밋

This commit is contained in:
2026-02-10 11:16:39 +09:00
parent 851a2acd69
commit ca01f33d71
60 changed files with 6947 additions and 1292 deletions

View File

@@ -1,11 +1,6 @@
/**
/**
* @file features/layout/components/header.tsx
* @description 애플리케이션 상단 헤더 컴포넌트 (네비게이션, 테마, 유저 메뉴)
* @remarks
* - [레이어] Components/UI/Layout
* - [사용자 행동] 홈 이동, 테마 변경, 로그인/회원가입 이동, 대시보드 이동
* - [데이터 흐름] User Prop -> UI Conditional Rendering
* - [연관 파일] layout.tsx, session-timer.tsx, user-menu.tsx
* @description 애플리케이션 상단 헤더 컴포넌트
*/
import Link from "next/link";
@@ -14,74 +9,139 @@ import { AUTH_ROUTES } from "@/features/auth/constants";
import { UserMenu } from "@/features/layout/components/user-menu";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { SessionTimer } from "@/features/auth/components/session-timer";
import { cn } from "@/lib/utils";
interface HeaderProps {
/** 현재 로그인 사용자 정보 (없으면 null) */
/** 현재 로그인 사용자 정보(null 가능) */
user: User | null;
/** 대시보드 링크 표시 여부 */
/** 대시보드 링크 버튼 노출 여부 */
showDashboardLink?: boolean;
/** 홈 랜딩에서 배경과 자연스럽게 섞이는 헤더 모드 */
blendWithBackground?: boolean;
}
/**
* 글로벌 헤더 컴포넌트
* @param user Supabase User 객체
* @param showDashboardLink 대시보드 바로가기 버튼 노출 여부
* @param showDashboardLink 대시보드 버튼 노출 여부
* @param blendWithBackground 홈 랜딩 전용 반투명 모드
* @returns Header JSX
* @see layout.tsx - RootLayout에서 데이터 주입하여 호출
* @see app/(home)/page.tsx 홈 랜딩에서 blendWithBackground=true로 호출
*/
export function Header({ user, showDashboardLink = false }: HeaderProps) {
export function Header({
user,
showDashboardLink = false,
blendWithBackground = false,
}: HeaderProps) {
return (
<header className="fixed top-0 z-40 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl supports-backdrop-filter:bg-background/60">
<div className="flex h-16 w-full items-center justify-between px-4 md:px-6">
{/* ========== 좌측: 로고 영역 ========== */}
<Link href={AUTH_ROUTES.HOME} className="flex items-center gap-2 group">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-primary/10 transition-transform duration-200 group-hover:scale-110">
<header
className={cn(
"fixed inset-x-0 top-0 z-50 w-full",
blendWithBackground
? "text-white"
: "border-b border-border/40 bg-background/80 backdrop-blur-xl supports-backdrop-filter:bg-background/60",
)}
>
{blendWithBackground && (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-linear-to-b from-black/70 via-black/35 to-transparent"
/>
)}
<div
className={cn(
"relative z-10 flex h-16 w-full items-center justify-between px-4 md:px-6",
blendWithBackground
? "bg-black/30 backdrop-blur-xl supports-backdrop-filter:bg-black/20"
: "",
)}
>
{/* ========== LEFT: LOGO SECTION ========== */}
<Link
href={AUTH_ROUTES.HOME}
className={cn("group flex items-center gap-2", blendWithBackground ? "text-white" : "")}
>
<div
className={cn(
"flex h-9 w-9 items-center justify-center rounded-xl transition-transform duration-200 group-hover:scale-110",
blendWithBackground ? "bg-brand-500/45 ring-1 ring-white/55" : "bg-primary/10",
)}
>
<div className="h-5 w-5 rounded-lg bg-linear-to-br from-brand-500 to-brand-700" />
</div>
<span className="text-xl font-bold tracking-tight text-foreground transition-colors group-hover:text-primary">
<span
className={cn(
"text-xl font-bold tracking-tight transition-colors",
blendWithBackground
? "!text-white [text-shadow:0_2px_18px_rgba(0,0,0,0.75)] group-hover:text-brand-100"
: "text-foreground group-hover:text-primary",
)}
>
AutoTrade
</span>
</Link>
{/* ========== 우측: 액션 버튼 영역 ========== */}
<div className="flex items-center gap-4">
{/* 테마 토글 */}
<ThemeToggle />
{/* ========== RIGHT: ACTION SECTION ========== */}
<div className={cn("flex items-center gap-2 sm:gap-3", blendWithBackground ? "text-white" : "")}
>
<ThemeToggle
className={cn(
blendWithBackground
? "rounded-full border border-white/40 bg-black/50 !text-white backdrop-blur-md hover:bg-black/65 focus-visible:ring-white/80"
: "",
)}
iconClassName={blendWithBackground ? "!text-white" : undefined}
/>
{user ? (
// [Case 1] 로그인 상태
<>
{/* 세션 타임아웃 타이머 */}
<SessionTimer />
<SessionTimer blendWithBackground={blendWithBackground} />
{showDashboardLink && (
<Button
asChild
variant="ghost"
size="sm"
className="hidden sm:inline-flex"
className={cn(
"hidden font-medium sm:inline-flex",
blendWithBackground
? "rounded-full border border-white/40 bg-black/50 !text-white backdrop-blur-md [text-shadow:0_1px_8px_rgba(0,0,0,0.45)] hover:bg-black/65 hover:!text-white"
: "",
)}
>
<Link href={AUTH_ROUTES.DASHBOARD}></Link>
</Button>
)}
{/* 사용자 드롭다운 메뉴 */}
<UserMenu user={user} />
<UserMenu user={user} blendWithBackground={blendWithBackground} />
</>
) : (
// [Case 2] 비로그인 상태
<div className="flex items-center gap-2">
<Button
asChild
variant="ghost"
size="sm"
className="hidden sm:inline-flex"
className={cn(
"hidden sm:inline-flex",
blendWithBackground
? "rounded-full border border-white/40 bg-black/50 !text-white backdrop-blur-md hover:bg-black/65 hover:!text-white"
: "",
)}
>
<Link href={AUTH_ROUTES.LOGIN}></Link>
</Button>
<Button asChild size="sm" className="rounded-full px-6">
<Button
asChild
size="sm"
className={cn(
"rounded-full px-6",
blendWithBackground
? "bg-brand-500/90 text-white shadow-lg shadow-brand-700/40 hover:bg-brand-400"
: "",
)}
>
<Link href={AUTH_ROUTES.SIGNUP}></Link>
</Button>
</div>

View File

@@ -1,14 +1,13 @@
/**
/**
* @file features/layout/components/user-menu.tsx
* @description 사용자 프로필 드롭다운 메뉴 컴포넌트
* @remarks
* - [레이어] Components/UI
* - [사용자 행동] Avatar 클릭 -> 드롭다운 오픈 -> 프로필/설정 이동 또는 로그아웃
* - [연관 파일] header.tsx, features/auth/actions.ts (로그아웃)
*/
"use client";
import { User } from "@supabase/supabase-js";
import { LogOut, Settings, User as UserIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { signout } from "@/features/auth/actions";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
@@ -19,21 +18,23 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { User } from "@supabase/supabase-js";
import { LogOut, Settings, User as UserIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
interface UserMenuProps {
/** Supabase User 객체 */
user: User | null;
/** 홈 랜딩의 shader 배경 위에서 대비를 높이는 모드 */
blendWithBackground?: boolean;
}
/**
* 사용자 메뉴/프로필 컴포넌트 (로그인 시 헤더 노출)
* 사용자 메뉴/프로필 컴포넌트
* @param user 로그인한 사용자 정보
* @returns Avatar 버튼 및 드롭다운 메뉴
* @param blendWithBackground shader 배경 위 가독성 모드
* @returns Avatar 버튼 + 드롭다운 메뉴
* @see features/layout/components/header.tsx 헤더 우측 액션 영역에서 호출
*/
export function UserMenu({ user }: UserMenuProps) {
export function UserMenu({ user, blendWithBackground = false }: UserMenuProps) {
const router = useRouter();
if (!user) return null;
@@ -41,38 +42,55 @@ export function UserMenu({ user }: UserMenuProps) {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 outline-none">
<Avatar className="h-8 w-8 transition-opacity hover:opacity-80">
<button
className={cn(
"flex items-center gap-2 rounded-full outline-none transition-colors",
blendWithBackground
? "ring-1 ring-white/30 hover:bg-black/30 focus-visible:ring-2 focus-visible:ring-white/70"
: "",
)}
aria-label="사용자 메뉴 열기"
>
<Avatar className="h-8 w-8 transition-opacity hover:opacity-90">
<AvatarImage src={user.user_metadata?.avatar_url} />
<AvatarFallback className="bg-linear-to-br from-brand-500 to-brand-700 text-white text-xs font-bold">
<AvatarFallback
className={cn(
"text-xs font-bold text-white",
blendWithBackground
? "bg-brand-500/90 [text-shadow:0_1px_8px_rgba(0,0,0,0.45)]"
: "bg-linear-to-br from-brand-500 to-brand-700",
)}
>
{user.email?.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.user_metadata?.full_name ||
user.user_metadata?.name ||
"사용자"}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
{user.user_metadata?.full_name || user.user_metadata?.name || "사용자"}
</p>
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/profile")}>
<UserIcon className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/settings")}>
<Settings className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<form action={signout}>
<DropdownMenuItem asChild>
<button className="w-full text-red-600 dark:text-red-400">