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:
@@ -1,277 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useRef, ReactNode } from 'react';
|
||||
import { performFullLogout } from '@/lib/auth/logout';
|
||||
import { useMasterDataStore } from '@/stores/masterDataStore';
|
||||
/**
|
||||
* AuthContext - 하위호환 re-export 심
|
||||
*
|
||||
* 실제 구현은 src/stores/authStore.ts (Zustand)로 이동됨.
|
||||
* 기존 import { useAuth } from '@/contexts/AuthContext' 코드가
|
||||
* 깨지지 않도록 타입과 훅을 re-export.
|
||||
*/
|
||||
|
||||
// ===== 타입 정의 =====
|
||||
import { type ReactNode } from 'react';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
|
||||
// ✅ 추가: 테넌트 타입 (실제 서버 응답 구조)
|
||||
export interface Tenant {
|
||||
id: number; // 테넌트 고유 ID (number)
|
||||
company_name: string; // 회사명
|
||||
business_num: string; // 사업자번호
|
||||
tenant_st_code: string; // 테넌트 상태 코드 (trial, active 등)
|
||||
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;
|
||||
}
|
||||
|
||||
// ✅ 수정: User 타입을 실제 서버 응답에 맞게 변경
|
||||
export interface User {
|
||||
userId: string; // 사용자 ID (username 아님)
|
||||
name: string; // 사용자 이름
|
||||
position: string; // 직책
|
||||
roles: Role[]; // 권한 목록 (배열)
|
||||
tenant: Tenant; // ✅ 테넌트 정보 (필수!)
|
||||
menu: MenuItem[]; // 메뉴 목록
|
||||
}
|
||||
|
||||
// ❌ 삭제 예정: 기존 UserRole (더 이상 사용하지 않음)
|
||||
export type UserRole = 'CEO' | 'ProductionManager' | 'Worker' | 'SystemAdmin' | 'Sales';
|
||||
|
||||
// ===== Context 타입 =====
|
||||
|
||||
interface AuthContextType {
|
||||
users: User[];
|
||||
currentUser: User | null;
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ===== Context 생성 =====
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// ===== Provider 컴포넌트 =====
|
||||
// 타입 re-export
|
||||
export type { Tenant, Role, MenuItem, User, UserRole } from '@/stores/authStore';
|
||||
|
||||
// AuthProvider: 빈 passthrough (미발견 import 안전망)
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// 상태 관리 (SSR-safe: 항상 초기값으로 시작)
|
||||
const [users, setUsers] = useState<User[]>(initialUsers);
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
|
||||
// ✅ 추가: 이전 tenant.id 추적 (테넌트 전환 감지용)
|
||||
const previousTenantIdRef = useRef<number | null>(null);
|
||||
|
||||
// localStorage에서 초기 데이터 로드 (클라이언트에서만 실행)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedUsers = localStorage.getItem('mes-users');
|
||||
if (savedUsers) {
|
||||
setUsers(JSON.parse(savedUsers));
|
||||
}
|
||||
|
||||
const savedCurrentUser = localStorage.getItem('mes-currentUser');
|
||||
if (savedCurrentUser) {
|
||||
setCurrentUser(JSON.parse(savedCurrentUser));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load auth data from localStorage:', error);
|
||||
// 손상된 데이터 제거
|
||||
localStorage.removeItem('mes-users');
|
||||
localStorage.removeItem('mes-currentUser');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// localStorage 동기화 (상태 변경 시 자동 저장)
|
||||
useEffect(() => {
|
||||
localStorage.setItem('mes-users', JSON.stringify(users));
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
localStorage.setItem('mes-currentUser', JSON.stringify(currentUser));
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
// ✅ 추가: 테넌트 전환 감지
|
||||
useEffect(() => {
|
||||
const prevTenantId = previousTenantIdRef.current;
|
||||
const currentTenantId = currentUser?.tenant?.id;
|
||||
|
||||
if (prevTenantId && currentTenantId && prevTenantId !== currentTenantId) {
|
||||
clearTenantCache(prevTenantId);
|
||||
}
|
||||
|
||||
previousTenantIdRef.current = currentTenantId || null;
|
||||
}, [currentUser?.tenant?.id]);
|
||||
|
||||
// ✅ 추가: masterDataStore에 현재 테넌트 ID 동기화
|
||||
useEffect(() => {
|
||||
const tenantId = currentUser?.tenant?.id ?? null;
|
||||
useMasterDataStore.getState().setCurrentTenantId(tenantId);
|
||||
}, [currentUser?.tenant?.id]);
|
||||
|
||||
// ✅ 추가: 테넌트별 캐시 삭제 함수 (SSR-safe)
|
||||
const clearTenantCache = (tenantId: number) => {
|
||||
// 서버 환경에서는 실행 안함
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const tenantAwarePrefix = `mes-${tenantId}-`;
|
||||
const pageConfigPrefix = `page_config_${tenantId}_`;
|
||||
|
||||
// localStorage 캐시 삭제
|
||||
Object.keys(localStorage).forEach(key => {
|
||||
if (key.startsWith(tenantAwarePrefix)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
// sessionStorage 캐시 삭제 (TenantAwareCache + masterDataStore)
|
||||
Object.keys(sessionStorage).forEach(key => {
|
||||
if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ✅ 추가: 로그아웃 함수 (완전한 캐시 정리)
|
||||
const logout = async () => {
|
||||
|
||||
// 1. React 상태 초기화 (UI 즉시 반영)
|
||||
setCurrentUser(null);
|
||||
|
||||
// 2. 완전한 로그아웃 수행 (Zustand, sessionStorage, localStorage, 서버 API)
|
||||
await performFullLogout({
|
||||
skipServerLogout: false, // 서버 API 호출 (HttpOnly 쿠키 삭제)
|
||||
redirectTo: null, // 리다이렉트는 호출하는 곳에서 처리
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Context value
|
||||
const value: AuthContextType = {
|
||||
users,
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
addUser: (user) => setUsers(prev => [...prev, user]),
|
||||
updateUser: (userId, updates) => setUsers(prev =>
|
||||
prev.map(user => user.userId === userId ? { ...user, ...updates } : user)
|
||||
),
|
||||
deleteUser: (userId) => setUsers(prev => prev.filter(user => user.userId !== userId)),
|
||||
getUserByUserId: (userId) => users.find(user => user.userId === userId),
|
||||
logout,
|
||||
clearTenantCache,
|
||||
resetAllData: () => {
|
||||
setUsers(initialUsers);
|
||||
setCurrentUser(null);
|
||||
}
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// ===== Custom Hook =====
|
||||
|
||||
// useAuth: authStore 전체 상태를 반환 (기존 Context 인터페이스 유지)
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
return useAuthStore();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useMemo, ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { TenantAwareCache } from '@/lib/cache';
|
||||
import { itemMasterApi } from '@/lib/api/item-master';
|
||||
import { getErrorMessage, ApiError } from '@/lib/api/error-handler';
|
||||
@@ -224,7 +224,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
||||
const initialItemPages: ItemPage[] = [];
|
||||
|
||||
// ===== Auth & Cache Setup =====
|
||||
const { currentUser } = useAuth();
|
||||
const currentUser = useAuthStore((state) => state.currentUser);
|
||||
const tenantId = currentUser?.tenant?.id;
|
||||
|
||||
// ✅ TenantAwareCache 인스턴스 생성 (tenant.id 기반, SSR-safe)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { AuthProvider } from './AuthContext';
|
||||
import { PermissionProvider } from './PermissionContext';
|
||||
import { ItemMasterProvider } from './ItemMasterContext';
|
||||
|
||||
/**
|
||||
* RootProvider - 모든 Context Provider를 통합하는 최상위 Provider
|
||||
*
|
||||
* 현재 사용 중인 Context:
|
||||
* 1. AuthContext - 사용자/인증 (2개 상태)
|
||||
* 현재 사용 중인 Context/Store:
|
||||
* 1. authStore (Zustand) - 사용자/인증 (Provider 불필요)
|
||||
* 2. PermissionContext - 권한 관리 (URL 자동매칭)
|
||||
* 3. ItemMasterContext - 품목관리 (13개 상태)
|
||||
*
|
||||
@@ -19,13 +18,11 @@ import { ItemMasterProvider } from './ItemMasterContext';
|
||||
*/
|
||||
export function RootProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<PermissionProvider>
|
||||
<ItemMasterProvider>
|
||||
{children}
|
||||
</ItemMasterProvider>
|
||||
</PermissionProvider>
|
||||
</AuthProvider>
|
||||
<PermissionProvider>
|
||||
<ItemMasterProvider>
|
||||
{children}
|
||||
</ItemMasterProvider>
|
||||
</PermissionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user