fix(WEB): 마스터데이터 캐시 테넌트 격리 및 상세 템플릿 개선

- masterDataStore: 테넌트별 캐시 격리 로직 강화
- AuthContext: 인증 컨텍스트 안정성 개선
- IntegratedDetailTemplate: 상세 템플릿 동작 수정
- VendorDetail: 거래처 상세 수정
- AttendanceManagement: 타입 정의 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-29 16:57:49 +09:00
parent 4014b3fb84
commit 106ce09482
7 changed files with 211 additions and 29 deletions

View File

@@ -79,6 +79,7 @@ const createEmptyPageConfigs = (): Record<PageType, PageConfig | null> => ({
});
const initialState = {
currentTenantId: null as number | null,
pageConfigs: createEmptyPageConfigs(),
loading: {} as Record<PageType, boolean>,
errors: {} as Record<PageType, string | null>,
@@ -92,15 +93,27 @@ const STORAGE_PREFIX = 'page_config_';
const STORAGE_TIMESTAMP_SUFFIX = '_timestamp';
const CACHE_TTL = 10 * 60 * 1000; // 10분
/**
* 테넌트별 캐시 키 생성
* tenantId가 있으면: page_config_{tenantId}_{pageType}
* tenantId가 없으면: page_config_{pageType} (하위 호환)
*/
function getStorageKey(tenantId: number | null, pageType: PageType): string {
return tenantId != null
? `${STORAGE_PREFIX}${tenantId}_${pageType}`
: `${STORAGE_PREFIX}${pageType}`;
}
/**
* sessionStorage에서 페이지 구성 가져오기
*/
function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
function getConfigFromSessionStorage(tenantId: number | null, pageType: PageType): PageConfig | null {
if (typeof window === 'undefined') return null;
try {
const cachedData = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}`);
const timestamp = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
const key = getStorageKey(tenantId, pageType);
const cachedData = window.sessionStorage.getItem(key);
const timestamp = window.sessionStorage.getItem(`${key}${STORAGE_TIMESTAMP_SUFFIX}`);
if (!cachedData || !timestamp) return null;
@@ -108,8 +121,8 @@ function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
const cacheAge = Date.now() - parseInt(timestamp, 10);
if (cacheAge > CACHE_TTL) {
// 만료된 캐시 삭제
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}`);
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
window.sessionStorage.removeItem(key);
window.sessionStorage.removeItem(`${key}${STORAGE_TIMESTAMP_SUFFIX}`);
return null;
}
@@ -123,12 +136,13 @@ function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
/**
* sessionStorage에 페이지 구성 저장
*/
function setConfigToSessionStorage(pageType: PageType, config: PageConfig): void {
function setConfigToSessionStorage(tenantId: number | null, pageType: PageType, config: PageConfig): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.setItem(`${STORAGE_PREFIX}${pageType}`, JSON.stringify(config));
window.sessionStorage.setItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`, Date.now().toString());
const key = getStorageKey(tenantId, pageType);
window.sessionStorage.setItem(key, JSON.stringify(config));
window.sessionStorage.setItem(`${key}${STORAGE_TIMESTAMP_SUFFIX}`, Date.now().toString());
} catch (error) {
console.error(`[sessionStorage] Failed to set config for ${pageType}:`, error);
}
@@ -137,12 +151,13 @@ function setConfigToSessionStorage(pageType: PageType, config: PageConfig): void
/**
* sessionStorage에서 페이지 구성 삭제
*/
function removeConfigFromSessionStorage(pageType: PageType): void {
function removeConfigFromSessionStorage(tenantId: number | null, pageType: PageType): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}`);
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
const key = getStorageKey(tenantId, pageType);
window.sessionStorage.removeItem(key);
window.sessionStorage.removeItem(`${key}${STORAGE_TIMESTAMP_SUFFIX}`);
} catch (error) {
console.error(`[sessionStorage] Failed to remove config for ${pageType}:`, error);
}
@@ -156,10 +171,15 @@ export const useMasterDataStore = create<MasterDataStore>()(
// Initial state
...initialState,
// ===== 테넌트 관리 =====
setCurrentTenantId: (tenantId: number | null) =>
set({ currentTenantId: tenantId }, false, 'setCurrentTenantId'),
// ===== 하이브리드 로딩 전략 =====
fetchPageConfig: async (pageType: PageType) => {
const { pageConfigs, loading, errors } = get();
const { pageConfigs, loading, errors, currentTenantId } = get();
// 🔒 이미 로딩 중이면 중복 요청 방지
if (loading[pageType]) {
@@ -180,7 +200,7 @@ export const useMasterDataStore = create<MasterDataStore>()(
}
// 2⃣ sessionStorage 확인
const cachedConfig = getConfigFromSessionStorage(pageType);
const cachedConfig = getConfigFromSessionStorage(currentTenantId, pageType);
if (cachedConfig) {
console.log(`✅ [Cache Hit - Session] ${pageType}`);
@@ -239,7 +259,7 @@ export const useMasterDataStore = create<MasterDataStore>()(
'fetchPageConfig/success'
);
setConfigToSessionStorage(pageType, config);
setConfigToSessionStorage(currentTenantId, pageType, config);
console.log(`✅ [Config Loaded] ${pageType}`, config);
return config;
@@ -270,7 +290,7 @@ export const useMasterDataStore = create<MasterDataStore>()(
// ===== 캐시 무효화 =====
invalidateConfig: (pageType: PageType) => {
const { pageConfigs } = get();
const { pageConfigs, currentTenantId } = get();
const newConfigs = { ...pageConfigs };
delete newConfigs[pageType];
@@ -280,16 +300,17 @@ export const useMasterDataStore = create<MasterDataStore>()(
'invalidateConfig'
);
removeConfigFromSessionStorage(pageType);
removeConfigFromSessionStorage(currentTenantId, pageType);
console.log(`🗑️ [Cache Invalidated] ${pageType}`);
},
invalidateAllConfigs: () => {
const { currentTenantId } = get();
const pageTypes: PageType[] = ['item-master', 'quotation', 'sales-order', 'formula', 'pricing'];
pageTypes.forEach((pageType) => {
removeConfigFromSessionStorage(pageType);
removeConfigFromSessionStorage(currentTenantId, pageType);
});
set(
@@ -376,11 +397,12 @@ export const useMasterDataStore = create<MasterDataStore>()(
// 1. 메모리 캐시 초기화
set(initialState, false, 'reset');
// 2. sessionStorage 캐시도 정리
// 2. sessionStorage 캐시도 정리 (프리픽스 기반으로 모든 테넌트 캐시 제거)
if (typeof window !== 'undefined') {
const pageTypes: PageType[] = ['item-master', 'quotation', 'sales-order', 'formula', 'pricing'];
pageTypes.forEach((pageType) => {
removeConfigFromSessionStorage(pageType);
Object.keys(window.sessionStorage).forEach(key => {
if (key.startsWith(STORAGE_PREFIX)) {
window.sessionStorage.removeItem(key);
}
});
console.log('[masterDataStore] Reset: cleared memory and sessionStorage cache');
}