[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:
265
src/lib/cache/TenantAwareCache.ts
vendored
Normal file
265
src/lib/cache/TenantAwareCache.ts
vendored
Normal 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
8
src/lib/cache/index.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 캐시 유틸리티 모듈
|
||||
*
|
||||
* @example
|
||||
* import { TenantAwareCache } from '@/lib/cache';
|
||||
*/
|
||||
|
||||
export { TenantAwareCache } from './TenantAwareCache';
|
||||
Reference in New Issue
Block a user