[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:
byeongcheolryu
2025-11-11 18:55:16 +09:00
parent fa7f62383d
commit a68a25b737
79 changed files with 43173 additions and 118 deletions

39
src/store/demoStore.ts Normal file
View 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
View 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
View 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;
}
},
}
)
);