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:
유병철
2026-02-23 17:17:13 +09:00
parent 6c3572e568
commit 07374c826c
75 changed files with 1704 additions and 1376 deletions

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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>
);
}