[feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정

- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리)
- HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가)
- API 클라이언트 구현 (item-master.ts, 13개 엔드포인트)
- ItemMasterContext 구현 (상태 관리 및 데이터 흐름)
- 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등)
- SSR 호환성 수정 (navigator API typeof window 체크)
- 미사용 변수 ESLint 에러 해결
- Context 리팩토링 (AuthContext, RootProvider 추가)
- API 유틸리티 추가 (error-handler, logger, transformers)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-23 16:10:27 +09:00
parent 63f5df7d7d
commit df3db155dd
69 changed files with 31467 additions and 4796 deletions

View File

@@ -0,0 +1,267 @@
'use client';
import { createContext, useContext, useState, useEffect, useRef, ReactNode } from 'react';
// ===== 타입 정의 =====
// ✅ 추가: 테넌트 타입 (실제 서버 응답 구조)
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: () => 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>(initialUsers[2]); // TestUser3 (드미트리)
// ✅ 추가: 이전 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) {
console.log(`[Auth] Tenant changed: ${prevTenantId}${currentTenantId}`);
clearTenantCache(prevTenantId);
}
previousTenantIdRef.current = currentTenantId || null;
}, [currentUser?.tenant?.id]);
// ✅ 추가: 테넌트별 캐시 삭제 함수 (SSR-safe)
const clearTenantCache = (tenantId: number) => {
// 서버 환경에서는 실행 안함
if (typeof window === 'undefined') return;
const prefix = `mes-${tenantId}-`;
// localStorage 캐시 삭제
Object.keys(localStorage).forEach(key => {
if (key.startsWith(prefix)) {
localStorage.removeItem(key);
console.log(`[Cache] Cleared localStorage: ${key}`);
}
});
// sessionStorage 캐시 삭제
Object.keys(sessionStorage).forEach(key => {
if (key.startsWith(prefix)) {
sessionStorage.removeItem(key);
console.log(`[Cache] Cleared sessionStorage: ${key}`);
}
});
};
// ✅ 추가: 로그아웃 함수
const logout = () => {
if (currentUser?.tenant?.id) {
clearTenantCache(currentUser.tenant.id);
}
setCurrentUser(null);
localStorage.removeItem('mes-currentUser');
console.log('[Auth] Logged out and cleared tenant cache');
};
// 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(initialUsers[2]); // TestUser3
}
};
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;
}