[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:
267
src/contexts/AuthContext.tsx
Normal file
267
src/contexts/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
1921
src/contexts/ItemMasterContext.tsx
Normal file
1921
src/contexts/ItemMasterContext.tsx
Normal file
File diff suppressed because it is too large
Load Diff
51
src/contexts/RootProvider.tsx
Normal file
51
src/contexts/RootProvider.tsx
Normal 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';
|
||||
* // ... 등등
|
||||
*/
|
||||
Reference in New Issue
Block a user