refactor(WEB): V2 파일 통합, store 구조 정리 및 대시보드 개선

- V2 컴포넌트를 원본에 통합 후 V2 파일 삭제 (InspectionModal, BillDetail, ContractDocumentModal, LaborDetailClient, PricingDetailClient, QuoteRegistration)
- store → stores 디렉토리 이동 및 favoritesStore 추가
- dashboard_type3~5 추가 및 기존 대시보드 차트/훅 분리
- Sidebar 리팩토링 및 HeaderFavoritesBar 추가
- DashboardSwitcher 컴포넌트 추가
- 백업 파일(.v1-backup) 및 불필요 코드 정리
- InspectionPreviewModal 레이아웃 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-11 15:09:51 +09:00
parent e14335b635
commit a38996b751
96 changed files with 4930 additions and 6550 deletions

39
src/stores/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',
}
)
);

View File

@@ -0,0 +1,92 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { safeJsonParse } from '@/lib/utils';
export interface FavoriteItem {
id: string;
label: string;
iconName: string;
path: string;
addedAt: number;
}
const MAX_FAVORITES = 8;
function getUserId(): string {
if (typeof window === 'undefined') return 'default';
const userStr = localStorage.getItem('user');
if (!userStr) return 'default';
const user = safeJsonParse<Record<string, unknown> | null>(userStr, null);
return user?.id ? String(user.id) : 'default';
}
function getStorageKey(): string {
return `sam-favorites-${getUserId()}`;
}
interface FavoritesState {
favorites: FavoriteItem[];
toggleFavorite: (item: FavoriteItem) => void;
isFavorite: (id: string) => boolean;
setFavorites: (items: FavoriteItem[]) => void;
initializeIfEmpty: (defaults: FavoriteItem[]) => void;
}
export const useFavoritesStore = create<FavoritesState>()(
persist(
(set, get) => ({
favorites: [],
toggleFavorite: (item: FavoriteItem) => {
const { favorites } = get();
const exists = favorites.some((f) => f.id === item.id);
if (exists) {
set({ favorites: favorites.filter((f) => f.id !== item.id) });
} else {
if (favorites.length >= MAX_FAVORITES) return;
set({ favorites: [...favorites, { ...item, addedAt: Date.now() }] });
}
},
isFavorite: (id: string) => {
return get().favorites.some((f) => f.id === id);
},
setFavorites: (items: FavoriteItem[]) => {
set({ favorites: items.slice(0, MAX_FAVORITES) });
},
initializeIfEmpty: (defaults: FavoriteItem[]) => {
const { favorites } = get();
if (favorites.length === 0) {
set({ favorites: defaults.slice(0, MAX_FAVORITES) });
}
},
}),
{
name: 'sam-favorites',
// 사용자별 키를 위해 storage 커스텀
storage: {
getItem: (name) => {
const key = getStorageKey();
const str = localStorage.getItem(key);
if (!str) {
// fallback: 기본 키에서도 확인
const fallback = localStorage.getItem(name);
return fallback ? JSON.parse(fallback) : null;
}
return JSON.parse(str);
},
setItem: (name, value) => {
const key = getStorageKey();
localStorage.setItem(key, JSON.stringify(value));
},
removeItem: (name) => {
const key = getStorageKey();
localStorage.removeItem(key);
},
},
}
)
);

66
src/stores/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/stores/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: 'theme', // ThemeContext와 동일한 키 사용 (마이그레이션 호환성)
// Zustand persist 재수화 시 HTML 클래스 복원
onRehydrateStorage: () => (state) => {
if (state?.theme) {
document.documentElement.className = state.theme === 'light' ? '' : state.theme;
}
},
}
)
);