From 45731ebd03e13579cbb4e8700b7acd8dc6214f77 Mon Sep 17 00:00:00 2001 From: kimsman Date: Tue, 10 Mar 2026 23:46:52 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[Feat]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/icons/index.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 }) => ( Date: Tue, 10 Mar 2026 23:47:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[Feat]=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EA=B8=B0=EB=B0=98=20GNB=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/navigation/navigation.stories.tsx | 105 +++++++++ src/shared/ui/navigation/navigation.tsx | 216 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 src/shared/ui/navigation/navigation.stories.tsx create mode 100644 src/shared/ui/navigation/navigation.tsx diff --git a/src/shared/ui/navigation/navigation.stories.tsx b/src/shared/ui/navigation/navigation.stories.tsx new file mode 100644 index 0000000..5c79849 --- /dev/null +++ b/src/shared/ui/navigation/navigation.stories.tsx @@ -0,0 +1,105 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Navigation } from "./navigation"; +import { fn } from "storybook/test"; + +// --- Mock Data --- +const mockUsers = { + // 1. 어드민 권한이 있는 학생 (토글 노출됨) + studentAdmin: { + name: "김철수", + email: "chulsoo.kim@ajou.ac.kr", + userType: "학생", + isAdmin: true, + }, + // 2. 일반 학생 (토글 노출 안됨) + student: { + name: "이영희", + email: "younghee.lee@ajou.ac.kr", + userType: "학생", + isAdmin: false, + }, + // 3. 어드민 권한이 있는 교수 (토글 노출됨) + professorAdmin: { + name: "박교수", + email: "park.prof@ajou.ac.kr", + userType: "교수", + isAdmin: true, + }, + // 4. 기업 사용자 (토글 노출 안됨) + company: { + name: "(주)에이아이엠", + email: "contact@aim.com", + userType: "기업", + isAdmin: false, + }, +} as const; + +const meta = { + title: "Shared/UI/Navigation", + component: Navigation, + parameters: { + layout: "fullscreen", + }, + tags: ["autodocs"], + args: { + items: [ + { label: "포트폴리오", href: "/portfolio", isActive: true }, + { label: "소개", href: "/about" }, + { label: "공지사항", href: "/notice" }, + ], + onLogin: fn(), + onSignup: fn(), + onLogout: fn(), + onAdminToggle: fn(), + onProfileClick: fn(), + onAdminDashboardClick: fn(), + }, +} satisfies Meta; + +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..7da18f4 --- /dev/null +++ b/src/shared/ui/navigation/navigation.tsx @@ -0,0 +1,216 @@ +import { useState, useRef, useEffect } from "react"; +import { Button } from "@/shared/ui/button/button"; +import { UserIcon, LogOutIcon } from "@/shared/ui/icons"; + +export interface NavItem { + label: string; + href: string; + isActive?: boolean; +} + +export interface User { + name: string; + email: string; + userType: "학생" | "기업" | "교수"; + isAdmin?: boolean; +} + +export interface NavigationProps { + items: NavItem[]; + user?: User; + isAdminMode?: boolean; + onAdminToggle?: () => void; + onLogin?: () => void; + onSignup?: () => void; + onLogout?: () => void; + onProfileClick?: () => void; + onAdminDashboardClick?: () => void; + logoHref?: string; + className?: string; +} + +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 = [ + "sticky top-0 z-50 h-[80px] bg-white/95 backdrop-blur-[6px] border-b border-[var(--color-gray-200,#e5e5ec)] w-full", + className, + ] + .filter(Boolean) + .join(" "); + + const navLinkClasses = (isActive?: boolean) => { + const base = + "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"; + const activeState = isActive + ? "border-[var(--color-primary-800,#004a9c)] font-medium" + : "border-transparent hover:border-[var(--color-primary-800,#004a9c)]/50 font-medium"; + return `${base} ${activeState}`; + }; + + return ( +
+
+ {/* Logo Section */} + +
+ AJOU Logo +
+ + AIM AJOU + +
+ + {/* Navigation Menu */} + + + {/* Right Section: Auth & User Actions */} +
+ {/* Admin Mode Toggle */} + {user?.isAdmin && onAdminToggle && ( +
+ + {isAdminMode ? "관리 모드" : "일반 모드"} + + +
+ )} + + {user ? ( +
+ + + {/* Logout Button: 유저 아이콘과 동일한 호버 스타일 적용 */} + + + {/* Profile Dropdown */} + {showProfile && ( +
+
+
+

+ {user.name} +

+

+ {user.email} +

+
+ {user.userType} +
+
+ +
+ {isAdminMode && onAdminDashboardClick && ( + + )} + + + +
+
+
+ )} +
+ ) : ( +
+ + +
+ )} +
+
+ + ); +}; + +Navigation.displayName = "Navigation"; From a2a0e7e9e95991f8e7d7913efd9ccff6a34e96e5 Mon Sep 17 00:00:00 2001 From: kimsman Date: Tue, 10 Mar 2026 23:48:00 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[Refactor]=20shared/ui=20=EB=82=B4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=82=B4=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index dd81212..d8a4202 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,2 +1,4 @@ export { Button } from "./button"; export { Input } from "./input"; +export { Navigation } from "./navigation/navigation"; +export type { NavigationProps, NavItem, User as NavUser } from "./navigation/navigation"; From c56be730ca436ed07a0c33c4978dab717d2ae1d2 Mon Sep 17 00:00:00 2001 From: kimsman Date: Tue, 10 Mar 2026 23:59:05 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[Refactor]=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- public/assets/ajou-logo.svg | 54 +++++++++++++ src/shared/ui/index.ts | 2 +- src/shared/ui/navigation/navigation.tsx | 102 ++++++++++++++++-------- 4 files changed, 126 insertions(+), 36 deletions(-) create mode 100644 public/assets/ajou-logo.svg diff --git a/.gitignore b/.gitignore index 85210e5..c6cae4f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ sb-*.log # Testing (Storybook Addon - Vitest & Playwright) /coverage /playwright-report -/test-results \ No newline at end of file +/test-results + +GEMINI.md \ No newline at end of file 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/index.ts b/src/shared/ui/index.ts index d8a4202..a66b052 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,4 +1,4 @@ export { Button } from "./button"; export { Input } from "./input"; export { Navigation } from "./navigation/navigation"; -export type { NavigationProps, NavItem, User as NavUser } from "./navigation/navigation"; +export type { NavigationProps, NavItem, NavUser } from "./navigation/navigation"; diff --git a/src/shared/ui/navigation/navigation.tsx b/src/shared/ui/navigation/navigation.tsx index 7da18f4..3854ceb 100644 --- a/src/shared/ui/navigation/navigation.tsx +++ b/src/shared/ui/navigation/navigation.tsx @@ -2,22 +2,23 @@ import { useState, useRef, useEffect } from "react"; import { Button } from "@/shared/ui/button/button"; import { UserIcon, LogOutIcon } from "@/shared/ui/icons"; -export interface NavItem { +// --- Types --- +export type NavItem = { label: string; href: string; isActive?: boolean; -} +}; -export interface User { +export type NavUser = { name: string; email: string; userType: "학생" | "기업" | "교수"; isAdmin?: boolean; -} +}; -export interface NavigationProps { +export type NavigationProps = { items: NavItem[]; - user?: User; + user?: NavUser; isAdminMode?: boolean; onAdminToggle?: () => void; onLogin?: () => void; @@ -27,8 +28,63 @@ export interface NavigationProps { 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, @@ -56,21 +112,8 @@ export const Navigation = ({ return () => document.removeEventListener("mousedown", handleClickOutside); }, []); - const headerClasses = [ - "sticky top-0 z-50 h-[80px] bg-white/95 backdrop-blur-[6px] border-b border-[var(--color-gray-200,#e5e5ec)] w-full", - className, - ] - .filter(Boolean) - .join(" "); - - const navLinkClasses = (isActive?: boolean) => { - const base = - "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"; - const activeState = isActive - ? "border-[var(--color-primary-800,#004a9c)] font-medium" - : "border-transparent hover:border-[var(--color-primary-800,#004a9c)]/50 font-medium"; - return `${base} ${activeState}`; - }; + // 최종 클래스 변수 (명사형) + const headerClasses = getHeaderClasses(className); return (
@@ -92,7 +135,7 @@ export const Navigation = ({ {/* Navigation Menu */}