refactor(WEB): claudedocs 재정리 및 AuthContext/Zustand/유틸 코드 개선
- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류 - 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제) - AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화 - GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가 - PermissionDialog 삭제 → GenericCRUDDialog로 대체 - RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링 - toast-utils.ts 삭제 (미사용) - fileDownload.ts 개선, excel-download.ts 정리 - menuStore/themeStore Zustand 셀렉터 최적화 - useColumnSettings/useTableColumnStore 기능 보강 - 세금계산서/견적/작업자화면/결재 등 소규모 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
247
src/stores/authStore.ts
Normal file
247
src/stores/authStore.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Auth Zustand Store
|
||||
*
|
||||
* AuthContext(React Context + useState)에서 마이그레이션.
|
||||
* - persist: custom storage로 기존 localStorage 키(mes-users, mes-currentUser) 유지
|
||||
* - devtools: Redux DevTools 디버깅 지원
|
||||
* - subscribe: 테넌트 전환 감지 + masterDataStore 동기화
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { useMasterDataStore } from '@/stores/masterDataStore';
|
||||
|
||||
// ===== 타입 정의 =====
|
||||
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
company_name: string;
|
||||
business_num: string;
|
||||
tenant_st_code: string;
|
||||
options?: {
|
||||
company_scale?: string;
|
||||
industry?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
iconName: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
userId: string;
|
||||
name: string;
|
||||
position: string;
|
||||
roles: Role[];
|
||||
tenant: Tenant;
|
||||
menu: MenuItem[];
|
||||
}
|
||||
|
||||
export type UserRole = 'CEO' | 'ProductionManager' | 'Worker' | 'SystemAdmin' | 'Sales';
|
||||
|
||||
// ===== Store 타입 =====
|
||||
|
||||
interface AuthState {
|
||||
// State
|
||||
users: User[];
|
||||
currentUser: User | null;
|
||||
|
||||
// Actions
|
||||
setCurrentUser: (user: User | null) => void;
|
||||
addUser: (user: User) => void;
|
||||
updateUser: (userId: string, updates: Partial<User>) => void;
|
||||
deleteUser: (userId: string) => void;
|
||||
getUserByUserId: (userId: string) => User | undefined;
|
||||
logout: () => Promise<void>;
|
||||
clearTenantCache: (tenantId: number) => void;
|
||||
resetAllData: () => void;
|
||||
}
|
||||
|
||||
// ===== 초기 데이터 =====
|
||||
|
||||
const initialUsers: User[] = [
|
||||
{
|
||||
userId: "TestUser1",
|
||||
name: "김대표",
|
||||
position: "대표이사",
|
||||
roles: [{ id: 1, name: "ceo", description: "최고경영자" }],
|
||||
tenant: {
|
||||
id: 282,
|
||||
company_name: "(주)테크컴퍼니",
|
||||
business_num: "123-45-67890",
|
||||
tenant_st_code: "trial",
|
||||
},
|
||||
menu: [{ id: "13664", label: "시스템 대시보드", iconName: "layout-dashboard", path: "/dashboard" }],
|
||||
},
|
||||
{
|
||||
userId: "TestUser2",
|
||||
name: "박관리",
|
||||
position: "생산관리자",
|
||||
roles: [{ id: 2, name: "production_manager", description: "생산관리자" }],
|
||||
tenant: {
|
||||
id: 282,
|
||||
company_name: "(주)테크컴퍼니",
|
||||
business_num: "123-45-67890",
|
||||
tenant_st_code: "trial",
|
||||
},
|
||||
menu: [{ id: "13664", label: "시스템 대시보드", iconName: "layout-dashboard", path: "/dashboard" }],
|
||||
},
|
||||
{
|
||||
userId: "TestUser3",
|
||||
name: "드미트리",
|
||||
position: "시스템 관리자",
|
||||
roles: [{ id: 19, name: "system_manager", description: "시스템 관리자" }],
|
||||
tenant: {
|
||||
id: 282,
|
||||
company_name: "(주)테크컴퍼니",
|
||||
business_num: "123-45-67890",
|
||||
tenant_st_code: "trial",
|
||||
},
|
||||
menu: [{ id: "13664", label: "시스템 대시보드", iconName: "layout-dashboard", path: "/dashboard" }],
|
||||
},
|
||||
];
|
||||
|
||||
// ===== Custom Storage =====
|
||||
// 기존 코드가 mes-users / mes-currentUser 두 개 키를 사용하므로 호환성 유지
|
||||
|
||||
const authStorage = createJSONStorage<Pick<AuthState, 'users' | 'currentUser'>>(() => ({
|
||||
getItem: (_name: string): string | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const users = localStorage.getItem('mes-users');
|
||||
const currentUser = localStorage.getItem('mes-currentUser');
|
||||
return JSON.stringify({
|
||||
state: {
|
||||
users: users ? JSON.parse(users) : initialUsers,
|
||||
currentUser: currentUser ? JSON.parse(currentUser) : null,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
localStorage.removeItem('mes-users');
|
||||
localStorage.removeItem('mes-currentUser');
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setItem: (_name: string, value: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const { users, currentUser } = parsed.state;
|
||||
localStorage.setItem('mes-users', JSON.stringify(users));
|
||||
if (currentUser) {
|
||||
localStorage.setItem('mes-currentUser', JSON.stringify(currentUser));
|
||||
}
|
||||
} catch {
|
||||
// 저장 실패 무시
|
||||
}
|
||||
},
|
||||
removeItem: (_name: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem('mes-users');
|
||||
localStorage.removeItem('mes-currentUser');
|
||||
},
|
||||
}));
|
||||
|
||||
// ===== Store 생성 =====
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// State
|
||||
users: initialUsers,
|
||||
currentUser: null,
|
||||
|
||||
// Actions
|
||||
setCurrentUser: (user) => set({ currentUser: user }),
|
||||
|
||||
addUser: (user) => set((state) => ({ users: [...state.users, user] })),
|
||||
|
||||
updateUser: (userId, updates) =>
|
||||
set((state) => ({
|
||||
users: state.users.map((u) =>
|
||||
u.userId === userId ? { ...u, ...updates } : u
|
||||
),
|
||||
})),
|
||||
|
||||
deleteUser: (userId) =>
|
||||
set((state) => ({
|
||||
users: state.users.filter((u) => u.userId !== userId),
|
||||
})),
|
||||
|
||||
getUserByUserId: (userId) => get().users.find((u) => u.userId === userId),
|
||||
|
||||
logout: async () => {
|
||||
set({ currentUser: null });
|
||||
const { performFullLogout } = await import('@/lib/auth/logout');
|
||||
await performFullLogout({
|
||||
skipServerLogout: false,
|
||||
redirectTo: null,
|
||||
});
|
||||
},
|
||||
|
||||
clearTenantCache: (tenantId: number) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const tenantAwarePrefix = `mes-${tenantId}-`;
|
||||
const pageConfigPrefix = `page_config_${tenantId}_`;
|
||||
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
if (key.startsWith(tenantAwarePrefix)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(sessionStorage).forEach((key) => {
|
||||
if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetAllData: () => set({ users: initialUsers, currentUser: null }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-store',
|
||||
storage: authStorage,
|
||||
partialize: (state) => ({
|
||||
users: state.users,
|
||||
currentUser: state.currentUser,
|
||||
}),
|
||||
}
|
||||
),
|
||||
{ name: 'AuthStore' }
|
||||
)
|
||||
);
|
||||
|
||||
// ===== Subscribe: 테넌트 전환 감지 + masterDataStore 동기화 =====
|
||||
|
||||
let _prevTenantId: number | null = null;
|
||||
|
||||
useAuthStore.subscribe((state) => {
|
||||
const currentTenantId = state.currentUser?.tenant?.id ?? null;
|
||||
|
||||
// 테넌트 전환 감지 (이전값이 있고, 현재값과 다를 때만)
|
||||
if (_prevTenantId && currentTenantId && _prevTenantId !== currentTenantId) {
|
||||
state.clearTenantCache(_prevTenantId);
|
||||
}
|
||||
|
||||
_prevTenantId = currentTenantId;
|
||||
|
||||
// masterDataStore 동기화
|
||||
useMasterDataStore.getState().setCurrentTenantId(currentTenantId);
|
||||
});
|
||||
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
export const useCurrentUser = () => useAuthStore((state) => state.currentUser);
|
||||
export const useAuthLogout = () => useAuthStore((state) => state.logout);
|
||||
@@ -63,4 +63,22 @@ export const useMenuStore = create<MenuState>()(
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 사이드바 접힘 상태만 구독 */
|
||||
export const useSidebarCollapsed = () =>
|
||||
useMenuStore((state) => state.sidebarCollapsed);
|
||||
|
||||
/** 활성 메뉴 ID만 구독 */
|
||||
export const useActiveMenu = () =>
|
||||
useMenuStore((state) => state.activeMenu);
|
||||
|
||||
/** 메뉴 아이템 목록만 구독 */
|
||||
export const useMenuItems = () =>
|
||||
useMenuStore((state) => state.menuItems);
|
||||
|
||||
/** 하이드레이션 완료 여부만 구독 */
|
||||
export const useMenuHydrated = () =>
|
||||
useMenuStore((state) => state._hasHydrated);
|
||||
@@ -41,4 +41,14 @@ export const useThemeStore = create<ThemeState>()(
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 현재 테마만 구독 */
|
||||
export const useTheme = () =>
|
||||
useThemeStore((state) => state.theme);
|
||||
|
||||
/** setTheme 액션만 구독 */
|
||||
export const useSetTheme = () =>
|
||||
useThemeStore((state) => state.setTheme);
|
||||
@@ -99,3 +99,17 @@ export const useTableColumnStore = create<TableColumnState>()(
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 특정 페이지의 컬럼 설정만 구독 */
|
||||
export const usePageColumnSettings = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS);
|
||||
|
||||
/** 특정 페이지의 숨김 컬럼만 구독 */
|
||||
export const useHiddenColumns = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []);
|
||||
|
||||
/** 특정 페이지의 컬럼 너비만 구독 */
|
||||
export const useColumnWidths = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {});
|
||||
|
||||
Reference in New Issue
Block a user