[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

265
src/lib/cache/TenantAwareCache.ts vendored Normal file
View File

@@ -0,0 +1,265 @@
/**
* TenantAwareCache - 테넌트별 데이터 격리 캐시 유틸리티
*
* 기능:
* - tenant.id 기반 캐시 키 생성 (예: 'mes-282-itemMasters')
* - TTL (Time To Live) 만료 처리
* - tenant.id 자동 검증
* - 손상된 캐시 자동 제거
* - localStorage 및 sessionStorage 지원
*/
interface CachedData<T> {
tenantId: number; // 테넌트 ID (number)
data: T; // 실제 데이터
timestamp: number; // 저장 시간 (ms)
version?: string; // 버전 정보 (선택)
}
export class TenantAwareCache {
private tenantId: number; // 테넌트 ID
private storage: Storage; // localStorage | sessionStorage
private ttl: number; // Time to Live (ms)
/**
* TenantAwareCache 생성자
*
* @param tenantId - 테넌트 ID (user.tenant.id)
* @param storage - 사용할 스토리지 (기본: sessionStorage)
* @param ttl - 캐시 만료 시간 (기본: 1시간)
*
* @example
* const cache = new TenantAwareCache(282, sessionStorage, 3600000);
* cache.set('itemMasters', data);
*/
constructor(
tenantId: number,
storage: Storage = sessionStorage,
ttl: number = 3600000 // 1시간 기본값
) {
this.tenantId = tenantId;
this.storage = storage;
this.ttl = ttl;
}
/**
* 테넌트별 고유 키 생성
*
* @param key - 기본 키 이름
* @returns tenant.id가 포함된 고유 키
*
* @example
* getKey('itemMasters') → 'mes-282-itemMasters'
*/
private getKey(key: string): string {
return `mes-${this.tenantId}-${key}`;
}
/**
* 캐시에 데이터 저장
*
* @param key - 캐시 키
* @param data - 저장할 데이터
* @param version - 버전 정보 (선택)
*
* @example
* cache.set('itemMasters', [item1, item2], '1.0');
*/
set<T>(key: string, data: T, version?: string): void {
const cacheData: CachedData<T> = {
tenantId: this.tenantId,
data,
timestamp: Date.now(),
version
};
this.storage.setItem(this.getKey(key), JSON.stringify(cacheData));
}
/**
* 캐시에서 데이터 조회 (tenantId 및 TTL 검증 포함)
*
* @param key - 캐시 키
* @returns 캐시된 데이터 또는 null
*
* @example
* const data = cache.get<ItemMaster[]>('itemMasters');
* if (data) {
* console.log('캐시 히트:', data);
* } else {
* console.log('캐시 미스 - API 호출 필요');
* }
*/
get<T>(key: string): T | null {
const cached = this.storage.getItem(this.getKey(key));
if (!cached) return null;
try {
const parsed: CachedData<T> = JSON.parse(cached);
// 🛡️ 1. tenantId 검증
if (parsed.tenantId !== this.tenantId) {
console.warn(
`[Cache] tenantId mismatch for key "${key}": ` +
`${parsed.tenantId} !== ${this.tenantId}`
);
this.remove(key);
return null;
}
// 🛡️ 2. TTL 검증 (만료 시간)
if (Date.now() - parsed.timestamp > this.ttl) {
console.warn(`[Cache] Expired cache for key: ${key}`);
this.remove(key);
return null;
}
return parsed.data;
} catch (error) {
console.error(`[Cache] Parse error for key: ${key}`, error);
this.remove(key);
return null;
}
}
/**
* 캐시에서 특정 키 삭제
*
* @param key - 삭제할 캐시 키
*
* @example
* cache.remove('itemMasters');
*/
remove(key: string): void {
this.storage.removeItem(this.getKey(key));
}
/**
* 현재 테넌트의 모든 캐시 삭제
*
* @example
* cache.clear(); // 'mes-282-*' 모두 삭제
*/
clear(): void {
const prefix = `mes-${this.tenantId}-`;
Object.keys(this.storage).forEach(key => {
if (key.startsWith(prefix)) {
this.storage.removeItem(key);
}
});
}
/**
* 버전 일치 여부 확인
*
* @param key - 캐시 키
* @param expectedVersion - 기대하는 버전
* @returns 버전 일치 여부
*
* @example
* if (!cache.isVersionMatch('itemMasters', '1.0')) {
* // 버전 불일치 - 재조회 필요
* const newData = await fetchFromAPI();
* cache.set('itemMasters', newData, '1.0');
* }
*/
isVersionMatch(key: string, expectedVersion: string): boolean {
const cached = this.storage.getItem(this.getKey(key));
if (!cached) return false;
try {
const parsed: CachedData<any> = JSON.parse(cached);
return parsed.version === expectedVersion;
} catch {
return false;
}
}
/**
* 캐시 메타데이터 조회
*
* @param key - 캐시 키
* @returns 메타데이터 또는 null
*
* @example
* const meta = cache.getMetadata('itemMasters');
* if (meta) {
* console.log('저장 시간:', new Date(meta.timestamp));
* console.log('버전:', meta.version);
* }
*/
getMetadata(key: string): { tenantId: number; timestamp: number; version?: string } | null {
const cached = this.storage.getItem(this.getKey(key));
if (!cached) return null;
try {
const parsed: CachedData<any> = JSON.parse(cached);
return {
tenantId: parsed.tenantId,
timestamp: parsed.timestamp,
version: parsed.version
};
} catch {
return null;
}
}
/**
* 캐시 존재 여부 확인
*
* @param key - 캐시 키
* @returns 캐시 존재 여부
*
* @example
* if (cache.has('itemMasters')) {
* const data = cache.get('itemMasters');
* }
*/
has(key: string): boolean {
return this.storage.getItem(this.getKey(key)) !== null;
}
/**
* 현재 테넌트 ID 반환
*
* @returns 테넌트 ID
*
* @example
* console.log('현재 테넌트:', cache.getTenantId()); // 282
*/
getTenantId(): number {
return this.tenantId;
}
/**
* 캐시 통계 정보 조회
*
* @returns 캐시 통계
*
* @example
* const stats = cache.getStats();
* console.log(`캐시 ${stats.count}개, 총 ${stats.totalSize} bytes`);
*/
getStats(): { count: number; totalSize: number; keys: string[] } {
const prefix = `mes-${this.tenantId}-`;
const keys: string[] = [];
let totalSize = 0;
Object.keys(this.storage).forEach(key => {
if (key.startsWith(prefix)) {
keys.push(key);
const value = this.storage.getItem(key);
if (value) {
totalSize += value.length;
}
}
});
return {
count: keys.length,
totalSize,
keys
};
}
}

8
src/lib/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/**
* 캐시 유틸리티 모듈
*
* @example
* import { TenantAwareCache } from '@/lib/cache';
*/
export { TenantAwareCache } from './TenantAwareCache';