diff --git a/.gitignore b/.gitignore index 51a28db..e13d19f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ sb-*.log # Testing (Storybook Addon - Vitest & Playwright) /coverage /playwright-report +/test-results + +GEMINI.md /test-results.claude/ diff --git a/public/assets/ajou-logo.svg b/public/assets/ajou-logo.svg new file mode 100644 index 0000000..eccf1e0 --- /dev/null +++ b/public/assets/ajou-logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/ui/icons/index.tsx b/src/shared/ui/icons/index.tsx index 9dc836d..4e7a19a 100644 --- a/src/shared/ui/icons/index.tsx +++ b/src/shared/ui/icons/index.tsx @@ -191,6 +191,26 @@ export const EyeOffIcon: React.FC = ({ size = 20, ...props }) => ( ); +// LogOut 아이콘 +export const LogOutIcon: React.FC = ({ size = 24, ...props }) => ( + + + + + +); + // Chevron Down (Select) 아이콘 export const ChevronDownIcon: React.FC = ({ size = 20, ...props }) => ( ; + +export default meta; +type Story = StoryObj; + +// 1. 로그인 전 상태 +export const LoggedOut: Story = { + args: { + user: undefined, + }, +}; + +// 2. 학생 어드민 (토글 노출) +export const StudentAdmin: Story = { + args: { + user: mockUsers.studentAdmin, + isAdminMode: false, + }, +}; + +// 3. 학생 어드민 - 관리 모드 활성화 (토글 파란색, 대시보드 메뉴 노출) +export const StudentAdminActive: Story = { + args: { + user: mockUsers.studentAdmin, + isAdminMode: true, + }, +}; + +// 4. 일반 학생 (토글 노출 안됨) +export const GeneralStudent: Story = { + args: { + user: mockUsers.student, + }, +}; + +// 5. 교수 어드민 (토글 노출) +export const ProfessorAdmin: Story = { + args: { + user: mockUsers.professorAdmin, + isAdminMode: true, + }, +}; + +// 6. 기업 사용자 (토글 노출 안됨) +export const CompanyUser: Story = { + args: { + user: mockUsers.company, + }, +}; diff --git a/src/shared/ui/navigation/navigation.tsx b/src/shared/ui/navigation/navigation.tsx new file mode 100644 index 0000000..e5ba770 --- /dev/null +++ b/src/shared/ui/navigation/navigation.tsx @@ -0,0 +1,244 @@ +import { useState, useRef, useEffect } from "react"; +import { Button } from "@/shared/ui/button/button"; +import { UserIcon, LogOutIcon } from "@/shared/ui/icons"; + +// --- Types --- +export type NavItem = { + label: string; + href: string; + isActive?: boolean; +}; + +export type NavUser = { + name: string; + email: string; + userType: "학생" | "기업" | "교수"; + isAdmin?: boolean; +}; + +export type NavigationProps = { + items: NavItem[]; + user?: NavUser; + isAdminMode?: boolean; + onAdminToggle?: () => void; + onLogin?: () => void; + onSignup?: () => void; + onLogout?: () => void; + onProfileClick?: () => void; + onAdminDashboardClick?: () => void; + logoHref?: string; + className?: string; +}; + +// --- Styles --- + +// 1. Header Styles +const headerBaseClasses = + "sticky top-0 z-50 h-[80px] bg-white/95 backdrop-blur-[6px] border-b border-[var(--color-gray-200,#e5e5ec)] w-full"; + +const getHeaderClasses = (className?: string) => { + return [headerBaseClasses, className].filter(Boolean).join(" "); +}; + +// 2. NavLink Styles +const navLinkBaseClasses = + "flex items-center justify-center py-[10px] text-[16px] leading-[1.5] tracking-[-0.4px] text-[var(--color-gray-900,#1a1a1a)] transition-all duration-200 border-b-2 h-full font-medium"; + +const navLinkStatusClasses = { + active: "border-[var(--color-primary-800,#004a9c)]", + inactive: "border-transparent hover:border-[var(--color-primary-800,#004a9c)]/50", +}; + +const getNavLinkClasses = (isActive?: boolean) => { + return [ + navLinkBaseClasses, + isActive ? navLinkStatusClasses.active : navLinkStatusClasses.inactive, + ].join(" "); +}; + +// 3. Admin Toggle Styles +const toggleBaseClasses = + "relative inline-flex h-[24px] w-[44px] items-center rounded-full transition-colors duration-300 ease-in-out"; +const toggleBgClasses = { + active: "bg-[var(--color-primary-800,#004a9c)]", + inactive: "bg-[var(--color-gray-200,#e5e5ec)]", +}; + +const getToggleClasses = (isAdminMode: boolean) => { + return [toggleBaseClasses, isAdminMode ? toggleBgClasses.active : toggleBgClasses.inactive].join( + " ", + ); +}; + +const toggleHandleBaseClasses = + "inline-block h-[18px] w-[18px] transform rounded-full bg-white shadow-sm transition-all duration-300 ease-in-out"; +const toggleHandlePosClasses = { + active: "translate-x-[23px]", + inactive: "translate-x-[3px]", +}; + +const getToggleHandleClasses = (isAdminMode: boolean) => { + return [ + toggleHandleBaseClasses, + isAdminMode ? toggleHandlePosClasses.active : toggleHandlePosClasses.inactive, + ].join(" "); +}; + +// --- Component --- +export const Navigation = ({ + items, + user, + isAdminMode = false, + onAdminToggle, + onLogin, + onSignup, + onLogout, + onProfileClick, + onAdminDashboardClick, + logoHref = "/", + className = "", +}: NavigationProps) => { + const [showProfile, setShowProfile] = useState(false); + const profileRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (profileRef.current && !profileRef.current.contains(event.target as Node)) { + setShowProfile(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + // 최종 클래스 변수 (명사형) + const headerClasses = getHeaderClasses(className); + + return ( + + + {/* Logo Section */} + + + + + + AIM AJOU + + + + {/* Navigation Menu */} + + {items.map((item) => ( + + {item.label} + + ))} + + + {/* Right Section: Auth & User Actions */} + + {/* Admin Mode Toggle */} + {user?.isAdmin && onAdminToggle && ( + + + {isAdminMode ? "관리 모드" : "일반 모드"} + + + + + + )} + + {user ? ( + + setShowProfile(!showProfile)} + className="flex items-center justify-center h-10 w-10 text-[var(--color-primary-800,#004a9c)] hover:bg-[var(--color-primary-50)] rounded-full transition-all duration-200" + aria-label="사용자 프로필" + > + + + + + + + + {/* Profile Dropdown */} + {showProfile && ( + + + + + {user.name} + + + {user.email} + + + {user.userType} + + + + + {isAdminMode && onAdminDashboardClick && ( + { + onAdminDashboardClick(); + setShowProfile(false); + }} + className="text-left px-3 py-2.5 text-[14px] text-[var(--color-primary-800,#004a9c)] font-medium hover:bg-[var(--color-primary-50)] rounded-md transition-colors" + > + 관리자 대시보드 + + )} + { + onProfileClick?.(); + setShowProfile(false); + }} + className="text-left px-3 py-2.5 text-[14px] text-[var(--color-gray-900,#1a1a1a)] hover:bg-[var(--color-gray-100)] rounded-md transition-colors" + > + 내 프로필 + + setShowProfile(false)} + className="text-left px-3 py-2.5 text-[14px] text-[var(--color-gray-900,#1a1a1a)] hover:bg-[var(--color-gray-100)] rounded-md transition-colors" + > + 내 포트폴리오 + + + + + )} + + ) : ( + + + 회원가입 + + + 로그인 + + + )} + + + + ); +}; + +Navigation.displayName = "Navigation"; diff --git a/src/widgets/header/index.ts b/src/widgets/header/index.ts deleted file mode 100644 index ec87ee8..0000000 --- a/src/widgets/header/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Header } from "./ui/header"; -export type { HeaderProps } from "./ui/header"; diff --git a/src/widgets/header/ui/header.stories.tsx b/src/widgets/header/ui/header.stories.tsx deleted file mode 100644 index 9e6e03a..0000000 --- a/src/widgets/header/ui/header.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { fn } from "storybook/test"; - -import { Header } from "./header"; - -const meta = { - title: "Widgets/Header", - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ["autodocs"], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: "fullscreen", - }, - args: { - onLogin: fn(), - onLogout: fn(), - onCreateAccount: fn(), - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: "Jane Doe", - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/src/widgets/header/ui/header.tsx b/src/widgets/header/ui/header.tsx deleted file mode 100644 index 9c5fa90..0000000 --- a/src/widgets/header/ui/header.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Button } from "@/shared/ui/button/button"; - -type User = { - name: string; -}; - -export interface HeaderProps { - user?: User; - onLogin?: () => void; - onLogout?: () => void; - onCreateAccount?: () => void; -} - -export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( - - - - - - - - - - - - Acme - - - - {user ? ( - <> - - Welcome, {user.name}! - - - Log out - - > - ) : ( - <> - - Log in - - - Sign up - - > - )} - - - -);
+ {user.email} +