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 */} + +
+ AJOU Logo +
+ + AIM AJOU + +
+ + {/* Navigation Menu */} + + + {/* Right Section: Auth & User Actions */} +
+ {/* Admin Mode Toggle */} + {user?.isAdmin && onAdminToggle && ( +
+ + {isAdminMode ? "관리 모드" : "일반 모드"} + + +
+ )} + + {user ? ( +
+ + + + + {/* Profile Dropdown */} + {showProfile && ( +
+
+
+

+ {user.name} +

+

+ {user.email} +

+
+ {user.userType} +
+
+ +
+ {isAdminMode && onAdminDashboardClick && ( + + )} + + +
+
+
+ )} +
+ ) : ( +
+ + +
+ )} +
+
+ + ); +}; + +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}! - - - - ) : ( - <> - - - - )} -
-
-
-);