[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;
}

View File

@@ -5219,41 +5219,49 @@ const DataContext = createContext<DataContextType | undefined>(undefined);
export function DataProvider({ children }: { children: ReactNode }) {
// 상태 관리
const [salesOrders, setSalesOrders] = useState<SalesOrder[]>(() => {
if (typeof window === 'undefined') return initialSalesOrders;
const saved = localStorage.getItem('mes-salesOrders');
return saved ? JSON.parse(saved) : initialSalesOrders;
});
const [quotes, setQuotes] = useState<Quote[]>(() => {
if (typeof window === 'undefined') return initialQuotes;
const saved = localStorage.getItem('mes-quotes');
return saved ? JSON.parse(saved) : initialQuotes;
});
const [productionOrders, setProductionOrders] = useState<ProductionOrder[]>(() => {
if (typeof window === 'undefined') return initialProductionOrders;
const saved = localStorage.getItem('mes-productionOrders');
return saved ? JSON.parse(saved) : initialProductionOrders;
});
const [qualityInspections, setQualityInspections] = useState<QualityInspection[]>(() => {
if (typeof window === 'undefined') return initialQualityInspections;
const saved = localStorage.getItem('mes-qualityInspections');
return saved ? JSON.parse(saved) : initialQualityInspections;
});
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>(() => {
if (typeof window === 'undefined') return initialInventoryItems;
const saved = localStorage.getItem('mes-inventoryItems');
return saved ? JSON.parse(saved) : initialInventoryItems;
});
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>(() => {
if (typeof window === 'undefined') return initialPurchaseOrders;
const saved = localStorage.getItem('mes-purchaseOrders');
return saved ? JSON.parse(saved) : initialPurchaseOrders;
});
const [employees, setEmployees] = useState<Employee[]>(() => {
if (typeof window === 'undefined') return initialEmployees;
const saved = localStorage.getItem('mes-employees');
return saved ? JSON.parse(saved) : initialEmployees;
});
const [attendances, setAttendances] = useState<Attendance[]>(() => {
if (typeof window === 'undefined') return initialAttendances;
const saved = localStorage.getItem('mes-attendances');
return saved ? JSON.parse(saved) : initialAttendances;
});

View File

@@ -1,113 +0,0 @@
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
export interface ComponentMetadata {
componentName: string;
pagePath: string;
description: string;
// API 정보
apis?: {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
description: string;
requestBody?: any;
responseBody?: any;
queryParams?: { name: string; type: string; required: boolean; description: string }[];
pathParams?: { name: string; type: string; description: string }[];
}[];
// 데이터 구조
dataStructures?: {
name: string;
type: string;
fields: { name: string; type: string; required: boolean; description: string }[];
example?: any;
}[];
// 컴포넌트 정보
components?: {
name: string;
path: string;
props?: { name: string; type: string; required: boolean; description: string }[];
children?: string[];
}[];
// 상태 관리
stateManagement?: {
type: 'Context' | 'Local' | 'Props';
name: string;
description: string;
methods?: string[];
}[];
// 의존성
dependencies?: {
package: string;
version?: string;
usage: string;
}[];
// DB 스키마 (백엔드)
dbSchema?: {
tableName: string;
columns: { name: string; type: string; nullable: boolean; key?: 'PK' | 'FK'; description: string }[];
indexes?: string[];
relations?: { table: string; type: '1:1' | '1:N' | 'N:M'; description: string }[];
}[];
// 비즈니스 로직
businessLogic?: {
name: string;
description: string;
steps: string[];
}[];
// 유효성 검사
validations?: {
field: string;
rules: string[];
errorMessages: string[];
}[];
}
interface DeveloperModeContextType {
isDeveloperMode: boolean;
setIsDeveloperMode: (value: boolean) => void;
currentMetadata: ComponentMetadata | null;
setCurrentMetadata: (metadata: ComponentMetadata | null) => void;
isConsoleExpanded: boolean;
setIsConsoleExpanded: (value: boolean) => void;
}
const DeveloperModeContext = createContext<DeveloperModeContextType | undefined>(undefined);
export function DeveloperModeProvider({ children }: { children: ReactNode }) {
const [isDeveloperMode, setIsDeveloperMode] = useState(false);
const [currentMetadata, setCurrentMetadata] = useState<ComponentMetadata | null>(null);
const [isConsoleExpanded, setIsConsoleExpanded] = useState(true);
return (
<DeveloperModeContext.Provider
value={{
isDeveloperMode,
setIsDeveloperMode,
currentMetadata,
setCurrentMetadata,
isConsoleExpanded,
setIsConsoleExpanded,
}}
>
{children}
</DeveloperModeContext.Provider>
);
}
export function useDeveloperMode() {
const context = useContext(DeveloperModeContext);
if (!context) {
throw new Error('useDeveloperMode must be used within DeveloperModeProvider');
}
return context;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
'use client';
import { ReactNode } from 'react';
import { AuthProvider } from './AuthContext';
import { ItemMasterProvider } from './ItemMasterContext';
/**
* RootProvider - 모든 Context Provider를 통합하는 최상위 Provider
*
* 현재 사용 중인 Context:
* 1. AuthContext - 사용자/인증 (2개 상태)
* 2. ItemMasterContext - 품목관리 (13개 상태)
*
* 미사용 Context (contexts/_unused/로 이동됨):
* - FacilitiesContext, AccountingContext, HRContext, ShippingContext
* - InventoryContext, ProductionContext, PricingContext, SalesContext
*/
export function RootProvider({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ItemMasterProvider>
{children}
</ItemMasterProvider>
</AuthProvider>
);
}
/**
* 사용법:
*
* // app/layout.tsx
* import { RootProvider } from '@/contexts/RootProvider';
*
* export default function RootLayout({ children }) {
* return (
* <html>
* <body>
* <RootProvider>
* {children}
* </RootProvider>
* </body>
* </html>
* );
* }
*
* // 각 페이지/컴포넌트에서 사용:
* import { useAuth } from '@/contexts/AuthContext';
* import { useItemMaster } from '@/contexts/ItemMasterContext';
* import { useSales } from '@/contexts/SalesContext';
* // ... 등등
*/