- 미사용 import/변수/console.log 대량 정리 (100+개 파일) - ItemMasterContext 간소화 (미사용 로직 제거) - IntegratedListTemplateV2 / UniversalListPage 개선 - 결재 컴포넌트(ApprovalBox, DraftBox, ReferenceBox) 정리 - HR 컴포넌트(급여/휴가/부서) 코드 간소화 - globals.css 스타일 정리 및 개선 - AuthenticatedLayout 개선 - middleware CSP 정리 - proxy route 불필요 로깅 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
278 lines
7.9 KiB
TypeScript
278 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useState, useEffect, useRef, ReactNode } from 'react';
|
|
import { performFullLogout } from '@/lib/auth/logout';
|
|
import { useMasterDataStore } from '@/stores/masterDataStore';
|
|
|
|
// ===== 타입 정의 =====
|
|
|
|
// ✅ 추가: 테넌트 타입 (실제 서버 응답 구조)
|
|
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 컴포넌트 =====
|
|
|
|
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>;
|
|
}
|
|
|
|
// ===== Custom Hook =====
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
}
|