Files
sam-react-prod/claudedocs/architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md
유병철 106ce09482 fix(WEB): 마스터데이터 캐시 테넌트 격리 및 상세 템플릿 개선
- masterDataStore: 테넌트별 캐시 격리 로직 강화
- AuthContext: 인증 컨텍스트 안정성 개선
- IntegratedDetailTemplate: 상세 템플릿 동작 수정
- VendorDetail: 거래처 상세 수정
- AttendanceManagement: 타입 정의 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:57:49 +09:00

5.0 KiB

masterDataStore 캐시 테넌트 격리 수정

작성일: 2026-01-29 타입: 버그 수정 (캐시 격리 누락) 관련 문서: [REF-2025-11-19] multi-tenancy-implementation.md


배경

멀티테넌시 검토 결과, TenantAwareCache(mes-{tenantId}-{key})는 테넌트별로 캐시가 격리되어 있지만, masterDataStore의 sessionStorage 캐시는 테넌트 구분 없이 page_config_{pageType} 키를 사용하고 있었음.

추가로 setCurrentTenantId 액션이 인터페이스에만 선언되어 있고 구현도, 호출도 없는 dead code 상태였음.


문제

1. 캐시 키에 tenantId 미포함

TenantAwareCache:  mes-282-itemMasters       ← 테넌트 격리됨
masterDataStore:   page_config_item-master    ← 테넌트 격리 안됨

2. 발생 가능한 시나리오

1. 테넌트 282 사용자가 품목관리 접속
   → sessionStorage: page_config_item-master = {테넌트282 설정}

2. 세션 내 테넌트 500으로 전환 (로그아웃 없이)
   → clearTenantCache()는 mes-282-* 만 삭제
   → page_config_item-master 는 삭제되지 않음

3. 테넌트 500 사용자에게 테넌트 282의 페이지 설정이 노출

3. setCurrentTenantId 미구현

// 인터페이스에 선언만 있고 구현 없음
interface MasterDataStore {
  currentTenantId: number | null;            // ← initialState에도 누락
  setCurrentTenantId: (tenantId: number | null) => void;  // ← 구현 없음
}

수정 내역

masterDataStore.ts

영역 Before After
initialState currentTenantId 누락 currentTenantId: null 추가
캐시 키 포맷 page_config_{pageType} page_config_{tenantId}_{pageType}
setCurrentTenantId 인터페이스만 선언 구현 추가
fetchPageConfig tenantId 미사용 currentTenantId를 캐시 함수에 전달
invalidateConfig tenantId 미사용 currentTenantId 기반 삭제
invalidateAllConfigs tenantId 미사용 currentTenantId 기반 삭제
reset() pageType 목록 순회 삭제 page_config_ 프리픽스 기반 전체 삭제

핵심 변경: 캐시 키 생성 함수 추가

function getStorageKey(tenantId: number | null, pageType: PageType): string {
  return tenantId != null
    ? `${STORAGE_PREFIX}${tenantId}_${pageType}`   // page_config_282_item-master
    : `${STORAGE_PREFIX}${pageType}`;               // page_config_item-master (하위 호환)
}

핵심 변경: reset()을 프리픽스 기반으로 변경

// Before: 고정된 pageType 목록으로 삭제 (tenantId 포함 키를 찾지 못함)
pageTypes.forEach((pt) => removeConfigFromSessionStorage(pt));

// After: page_config_ 프리픽스로 모든 테넌트 캐시 일괄 삭제
Object.keys(window.sessionStorage).forEach(key => {
  if (key.startsWith(STORAGE_PREFIX)) {
    window.sessionStorage.removeItem(key);
  }
});

AuthContext.tsx

영역 Before After
import - useMasterDataStore 추가
tenantId 동기화 없음 currentUser.tenant.id 변경 시 setCurrentTenantId() 호출
clearTenantCache mes-{tenantId}-* 만 삭제 mes-{tenantId}-* + page_config_{tenantId}_* 삭제

핵심 변경: tenantId 동기화 useEffect

useEffect(() => {
  const tenantId = currentUser?.tenant?.id ?? null;
  useMasterDataStore.getState().setCurrentTenantId(tenantId);
}, [currentUser?.tenant?.id]);

핵심 변경: clearTenantCache 범위 확장

const tenantAwarePrefix = `mes-${tenantId}-`;
const pageConfigPrefix = `page_config_${tenantId}_`;

Object.keys(sessionStorage).forEach(key => {
  if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) {
    sessionStorage.removeItem(key);
  }
});

하위 호환

항목 영향
기존 캐시 키 page_config_item-master → 키 불일치로 miss → API 재요청 → 새 포맷으로 자동 전환
logout.ts page_config_ 프리픽스 매칭이 새 키 포맷(page_config_282_item-master)도 커버
sessionStorage TTL 10분 만료이므로 기존 키는 자연 소멸
tenantId가 null인 경우 기존 포맷(page_config_{pageType}) 유지하여 동작 보장

효과

  1. 세션 내 테넌트 전환 시 캐시 누수 차단: clearTenantCachepage_config_{tenantId}_*까지 삭제
  2. 캐시 패턴 일관성: TenantAwareCache(mes-{tenantId}-)와 masterDataStore(page_config_{tenantId}_) 모두 테넌트 격리
  3. dead code 해소: currentTenantId 필드와 setCurrentTenantId 액션이 실제로 동작

관련 파일

  • src/stores/masterDataStore.ts - 캐시 키 변경, setCurrentTenantId 구현
  • src/contexts/AuthContext.tsx - tenantId 동기화, clearTenantCache 범위 확장
  • src/lib/auth/logout.ts - 기존 page_config_ 프리픽스 매칭 (변경 없음, 호환 확인)
  • src/lib/cache/TenantAwareCache.ts - 참고 (기존 정상 동작)