- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
멀티 테넌시 검증 및 테스트 가이드
작성일: 2025-11-19 목적: Phase 1-4 구현 후 테넌트 격리 기능 검증
📋 목차
테스트 환경 준비
1. 개발 서버 실행
npm run dev
2. 브라우저 개발자 도구 열기
- Chrome:
F12또는Cmd+Option+I(Mac) - Console 탭과 Application 탭을 주로 사용
3. 테스트 사용자 확인
현재 등록된 테스트 사용자 (모두 tenant.id: 282):
| userId | name | tenant.id | 역할 |
|---|---|---|---|
| TestUser1 | 이재욱 | 282 | 일반 사용자 |
| TestUser2 | 박관리 | 282 | 생산관리자 |
| TestUser3 | 드미트리 | 282 | 시스템 관리자 |
⚠️ 테넌트 전환 테스트를 위해 다른 tenant.id를 가진 사용자가 필요합니다.
테스트 시나리오
시나리오 1: 기본 캐시 동작 확인 ✅
목적: TenantAwareCache가 제대로 동작하는지 확인
단계:
- 로그인: TestUser3 (tenant.id: 282)
/master-data/item-master-data-management페이지 이동- 데이터 입력:
- 규격 마스터 1개 추가
- 품목 분류 1개 추가
- 개발자 도구 → Application → Session Storage 확인
기대 결과:
✅ sessionStorage에 다음 키가 생성되어야 함:
- mes-282-itemMasters
- mes-282-specificationMasters
- mes-282-itemCategories
- (기타 입력한 데이터)
✅ 각 키의 값에 tenantId: 282 포함
✅ timestamp 포함
확인 방법:
// Console에서 실행
Object.keys(sessionStorage).filter(k => k.startsWith('mes-'))
// 결과: ["mes-282-itemMasters", "mes-282-specificationMasters", ...]
시나리오 2: 페이지 새로고침 시 캐시 로드 ✅
목적: 캐시에서 데이터를 제대로 불러오는지 확인
단계:
- 시나리오 1 완료 후
F5또는Cmd+R로 새로고침- Console에서 로그 확인
기대 결과:
✅ Console 로그:
[Cache] Loaded from cache: itemMasters
[Cache] Loaded from cache: specificationMasters
...
✅ 입력했던 데이터가 그대로 표시됨
✅ 서버 API 호출 없이 캐시에서 로드
시나리오 3: TTL (1시간) 만료 확인 ⏱️
목적: 캐시가 1시간 후 자동 삭제되는지 확인
⚠️ 주의: 실제 1시간을 기다릴 수 없으므로 수동 테스트
단계:
-
sessionStorage에서 캐시 데이터 조회:
const cached = sessionStorage.getItem('mes-282-itemMasters'); const parsed = JSON.parse(cached); console.log('Timestamp:', new Date(parsed.timestamp)); console.log('Age (minutes):', (Date.now() - parsed.timestamp) / 60000); -
수동으로 timestamp 수정 (과거 시간으로):
const cached = sessionStorage.getItem('mes-282-itemMasters'); const parsed = JSON.parse(cached); // 2시간 전으로 설정 (TTL 1시간 초과) parsed.timestamp = Date.now() - (7200 * 1000); sessionStorage.setItem('mes-282-itemMasters', JSON.stringify(parsed)); -
페이지 새로고침
기대 결과:
✅ Console 로그:
[Cache] Expired cache for key: itemMasters
✅ 만료된 캐시 자동 삭제
✅ 초기 데이터로 리셋
시나리오 4: 다중 탭 격리 확인 🔗
목적: 탭마다 독립적인 sessionStorage 사용 확인
단계:
- 탭 1: TestUser3 로그인 → 데이터 입력 (규격 마스터 A)
- 탭 2: 동일 URL을 새 탭으로 열기 (
Cmd+T→ URL 복사) - 탭 2에서 sessionStorage 확인
기대 결과:
✅ 탭 2의 sessionStorage는 비어있음
✅ 탭 1의 데이터가 탭 2에 공유되지 않음
✅ 각 탭이 독립적으로 동작
sessionStorage는 탭마다 격리됨!
확인 방법:
// 탭 1
sessionStorage.setItem('test', 'tab1');
// 탭 2 (새로 열린 탭)
sessionStorage.getItem('test'); // null (공유 안 됨)
시나리오 5: 탭 닫기 시 자동 삭제 🗑️
목적: 탭을 닫으면 sessionStorage가 자동으로 삭제되는지 확인
단계:
- 탭에서 데이터 입력
- Application → Session Storage에서 데이터 확인
- 탭 닫기
- 동일 URL을 새 탭으로 다시 열기
- Session Storage 확인
기대 결과:
✅ sessionStorage가 완전히 비어있음
✅ 이전 탭의 데이터가 남아있지 않음
✅ 새로운 세션으로 시작
시나리오 6: 로그아웃 시 캐시 삭제 🚪
목적: 로그아웃하면 테넌트 캐시가 완전히 삭제되는지 확인
단계:
- TestUser3 로그인 → 데이터 입력
- sessionStorage 확인 (캐시 있음)
- 로그아웃 버튼 클릭
- Console 로그 확인
- sessionStorage 다시 확인
기대 결과:
✅ Console 로그:
[Cache] Cleared sessionStorage: mes-282-itemMasters
[Cache] Cleared sessionStorage: mes-282-specificationMasters
...
[Auth] Logged out and cleared tenant cache
✅ sessionStorage에서 mes-282-* 키가 모두 삭제됨
✅ localStorage에서 mes-currentUser도 삭제됨
확인 방법:
// 로그아웃 후
Object.keys(sessionStorage).filter(k => k.startsWith('mes-282-'))
// 결과: [] (빈 배열)
시나리오 7: 테넌트 전환 시 캐시 삭제 🔄
⚠️ 현재 제약: 모든 테스트 사용자가 tenant.id: 282를 사용 중
필요 작업: 다른 tenant.id를 가진 사용자 추가
7-1. 테스트 사용자 추가 (tenant.id: 283)
src/contexts/AuthContext.tsx 수정:
const initialUsers: User[] = [
// ... 기존 사용자 ...
{
userId: "TestUser4",
name: "김테넌트",
position: "다른 회사 관리자",
roles: [
{
id: 1,
name: "admin",
description: "관리자"
}
],
tenant: {
id: 283, // ✅ 다른 테넌트!
company_name: "(주)다른회사",
business_num: "987-65-43210",
tenant_st_code: "active",
other_tenants: []
},
menu: [
{
id: "13664",
label: "시스템 대시보드",
iconName: "layout-dashboard",
path: "/dashboard"
}
]
}
];
7-2. 테넌트 전환 테스트
단계:
-
TestUser3 로그인 (tenant.id: 282)
- 데이터 입력 (규격 마스터 A, B)
- sessionStorage 확인:
mes-282-specificationMasters
-
로그아웃
-
TestUser4 로그인 (tenant.id: 283)
- Console 로그 확인
기대 결과:
✅ Console 로그:
[Auth] Tenant changed: 282 → 283
[Cache] Cleared sessionStorage: mes-282-itemMasters
[Cache] Cleared sessionStorage: mes-282-specificationMasters
...
✅ 이전 테넌트(282)의 캐시가 모두 삭제됨
✅ TestUser4의 데이터는 mes-283-* 키로 저장됨
✅ 테넌트 간 데이터 격리 확인
확인 방법:
// 테넌트 전환 후
Object.keys(sessionStorage).forEach(key => {
console.log(key);
});
// 결과:
// mes-283-itemMasters (새 테넌트)
// mes-283-specificationMasters
// (mes-282-* 키는 없어야 함!)
시나리오 8: PHP 백엔드 tenant.id 검증 🛡️
⚠️ 주의: PHP 백엔드가 실행 중이어야 함
목적: 다른 테넌트의 데이터 접근 시 403 반환 확인
단계:
- TestUser3 로그인 (tenant.id: 282)
- 브라우저 Console에서 다른 테넌트 API 직접 호출:
// 자신의 테넌트 (282) - 성공해야 함
fetch('/api/tenants/282/item-master-config')
.then(r => r.json())
.then(d => console.log('Own tenant:', d));
// 다른 테넌트 (283) - 403 에러여야 함
fetch('/api/tenants/283/item-master-config')
.then(r => r.json())
.then(d => console.log('Other tenant:', d));
기대 결과:
✅ 자신의 테넌트 (282):
{
success: true,
data: { ... }
}
✅ 다른 테넌트 (283):
{
success: false,
error: {
code: "FORBIDDEN",
message: "접근 권한이 없습니다."
}
}
Status: 403 Forbidden
✅ Next.js는 단순히 PHP 응답을 전달만 함
✅ PHP가 tenant.id 불일치를 감지하고 403 반환
체크리스트
캐시 동작 ✅
- sessionStorage에
mes-{tenantId}-{key}형식으로 저장 - 캐시 데이터에
tenantId,timestamp,version포함 - 페이지 새로고침 시 캐시에서 로드
- TTL (1시간) 만료 시 자동 삭제
탭 격리 🔗
- 탭마다 독립적인 sessionStorage
- 다른 탭과 데이터 공유 안 됨
- 탭 닫으면 sessionStorage 자동 삭제
로그아웃 🚪
- 로그아웃 시
mes-{tenantId}-*캐시 모두 삭제 - Console에 삭제 로그 출력
- localStorage의
mes-currentUser삭제
테넌트 전환 🔄
- 테넌트 변경 감지 (useEffect)
- 이전 테넌트 캐시 자동 삭제
- 새 테넌트 데이터는 새 키로 저장
- Console에 전환 로그 출력
API 보안 🛡️
- 자신의 테넌트 API 호출 성공
- 다른 테넌트 API 호출 시 403 Forbidden
- PHP 백엔드가 tenant.id 검증 수행
- Next.js는 PHP 응답 그대로 전달
문제 해결
문제 1: 캐시가 저장되지 않음
증상: sessionStorage가 비어있음
원인:
- ItemMasterContext가 제대로 마운트되지 않음
- tenantId가 null
해결:
-
Console에서 확인:
// AuthContext의 currentUser 확인 console.log(JSON.parse(localStorage.getItem('mes-currentUser'))); // tenant.id 확인 console.log(user?.tenant?.id); -
ItemMasterContext가 AuthContext 하위에 있는지 확인
문제 2: 테넌트 전환 시 캐시가 삭제되지 않음
증상: 이전 테넌트 캐시가 남아있음
원인:
useEffect의존성 배열 문제previousTenantIdRef초기화 안 됨
해결:
// AuthContext.tsx 확인
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]);
문제 3: TTL 만료 후에도 캐시가 남아있음
증상: 1시간 이상 지난 캐시가 그대로 사용됨
원인:
TenantAwareCache.get()메서드에서 TTL 체크 안 함
해결:
// TenantAwareCache.ts 확인
get<T>(key: string): T | null {
// ...
// TTL 검증
if (Date.now() - parsed.timestamp > this.ttl) {
console.warn(`[Cache] Expired cache for key: ${key}`);
this.remove(key);
return null;
}
return parsed.data;
}
문제 4: PHP 403 에러가 반환되지 않음
증상: 다른 테넌트 API 호출이 성공함
원인:
- PHP 백엔드에 tenant.id 검증 로직이 없음
- JWT에 tenant.id가 포함되지 않음
해결:
- PHP 백엔드 확인 (프론트엔드 작업 범위 밖)
- JWT payload에
tenant_id포함 여부 확인 - PHP middleware에서 tenant.id 검증 로직 확인
테스트 완료 기준
✅ 모든 시나리오 통과
- 시나리오 1-8 모두 기대 결과와 일치
✅ 모든 체크리스트 완료
- 캐시, 탭, 로그아웃, 테넌트 전환, API 보안
✅ Console 에러 없음
- 개발자 도구 Console에 빨간색 에러 없음
✅ 성능 확인
- 페이지 로드 시간 < 1초
- 캐시 히트 시 API 호출 없음
다음 단계
Phase 5 완료 후:
- Phase 6: 품목기준관리 페이지 작업 진행
- API 연동 및 실제 CRUD 구현
- UI/UX 개선
작성자: Claude 버전: 1.0 최종 업데이트: 2025-11-19