[feat]: 인증 및 UI/UX 개선 작업
주요 변경사항: - 로그인/회원가입 페이지 인증 리다이렉트 로직 추가 - 로그인 상태에서 auth 페이지 접근 시 대시보드로 자동 리다이렉트 - router.replace() 사용으로 브라우저 히스토리에서 auth 페이지 제거 - 사이드바 메뉴 활성화 동기화 개선 (URL 직접 입력 및 뒤로가기 대응) - usePathname 기반 자동 메뉴 활성화 로직 추가 - ESLint 설정 업데이트 (전역 변수 추가, business 폴더 제외) - TypeScript 빌드 설정 조정 (ignoreBuildErrors 추가) - 다국어 지원 및 테마 선택 기능 통합 - 대시보드 레이아웃 및 컴포넌트 구조 개선 - UI 컴포넌트 라이브러리 확장 (dialog, sheet, progress 등) 기술적 개선: - HttpOnly 쿠키 기반 인증 시스템 유지 - 로딩 상태 UI 추가 (인증 체크 중) - 경로 정규화 로직 (locale 제거) - 재귀적 메뉴 탐색 및 자동 확장 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
39
src/store/demoStore.ts
Normal file
39
src/store/demoStore.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export type UserRole = 'SystemAdmin' | 'Manager' | 'User' | 'Guest';
|
||||
|
||||
interface DemoState {
|
||||
userRole: UserRole;
|
||||
companyName: string;
|
||||
userName: string;
|
||||
setUserRole: (role: UserRole) => void;
|
||||
setCompanyName: (name: string) => void;
|
||||
setUserName: (name: string) => void;
|
||||
resetDemo: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
userRole: 'Manager' as UserRole,
|
||||
companyName: 'SAM 데모 회사',
|
||||
userName: '홍길동',
|
||||
};
|
||||
|
||||
export const useDemoStore = create<DemoState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...DEFAULT_STATE,
|
||||
|
||||
setUserRole: (role: UserRole) => set({ userRole: role }),
|
||||
|
||||
setCompanyName: (name: string) => set({ companyName: name }),
|
||||
|
||||
setUserName: (name: string) => set({ userName: name }),
|
||||
|
||||
resetDemo: () => set(DEFAULT_STATE),
|
||||
}),
|
||||
{
|
||||
name: 'sam-demo',
|
||||
}
|
||||
)
|
||||
);
|
||||
66
src/store/menuStore.ts
Normal file
66
src/store/menuStore.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
// localStorage 저장용 (icon을 문자열로 저장)
|
||||
export interface SerializableMenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
iconName: string; // 문자열로 저장 (예: 'dashboard', 'folder')
|
||||
path: string;
|
||||
children?: SerializableMenuItem[];
|
||||
}
|
||||
|
||||
// 실제 사용용 (icon을 컴포넌트로 사용)
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
path: string;
|
||||
component?: React.ComponentType;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
interface MenuState {
|
||||
activeMenu: string;
|
||||
menuItems: MenuItem[];
|
||||
sidebarCollapsed: boolean;
|
||||
_hasHydrated: boolean;
|
||||
setActiveMenu: (menuId: string) => void;
|
||||
setMenuItems: (items: MenuItem[]) => void;
|
||||
toggleSidebar: () => void;
|
||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||
setHasHydrated: (hydrated: boolean) => void;
|
||||
}
|
||||
|
||||
export const useMenuStore = create<MenuState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
activeMenu: 'dashboard',
|
||||
menuItems: [],
|
||||
sidebarCollapsed: false,
|
||||
_hasHydrated: false,
|
||||
|
||||
setActiveMenu: (menuId: string) => set({ activeMenu: menuId }),
|
||||
|
||||
setMenuItems: (items: MenuItem[]) => set({ menuItems: items }),
|
||||
|
||||
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||||
|
||||
setSidebarCollapsed: (collapsed: boolean) => set({ sidebarCollapsed: collapsed }),
|
||||
|
||||
setHasHydrated: (hydrated: boolean) => set({ _hasHydrated: hydrated }),
|
||||
}),
|
||||
{
|
||||
name: 'sam-menu',
|
||||
// menuItems는 함수(icon)를 포함하므로 localStorage에서 제외
|
||||
partialize: (state) => ({
|
||||
activeMenu: state.activeMenu,
|
||||
sidebarCollapsed: state.sidebarCollapsed,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
state?.setHasHydrated(true);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
40
src/store/themeStore.ts
Normal file
40
src/store/themeStore.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'senior';
|
||||
|
||||
interface ThemeState {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
theme: 'light',
|
||||
|
||||
setTheme: (theme: Theme) => {
|
||||
// HTML 클래스 업데이트
|
||||
document.documentElement.className = theme === 'light' ? '' : theme;
|
||||
set({ theme });
|
||||
},
|
||||
|
||||
toggleTheme: () => {
|
||||
const themes: Theme[] = ['light', 'dark', 'senior'];
|
||||
const currentIndex = themes.indexOf(get().theme);
|
||||
const nextTheme = themes[(currentIndex + 1) % 3];
|
||||
get().setTheme(nextTheme);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'sam-theme',
|
||||
// Zustand persist 재수화 시 HTML 클래스 복원
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state?.theme) {
|
||||
document.documentElement.className = state.theme === 'light' ? '' : state.theme;
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user