# 멀티 테넌시 검증 및 테스트 가이드 **작성일**: 2025-11-19 **목적**: Phase 1-4 구현 후 테넌트 격리 기능 검증 --- ## 📋 목차 1. [테스트 환경 준비](#테스트-환경-준비) 2. [테스트 시나리오](#테스트-시나리오) 3. [체크리스트](#체크리스트) 4. [문제 해결](#문제-해결) --- ## 테스트 환경 준비 ### 1. 개발 서버 실행 ```bash 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가 제대로 동작하는지 확인 **단계**: 1. 로그인: TestUser3 (tenant.id: 282) 2. `/master-data/item-master-data-management` 페이지 이동 3. 데이터 입력: - 규격 마스터 1개 추가 - 품목 분류 1개 추가 4. **개발자 도구 → Application → Session Storage** 확인 **기대 결과**: ``` ✅ sessionStorage에 다음 키가 생성되어야 함: - mes-282-itemMasters - mes-282-specificationMasters - mes-282-itemCategories - (기타 입력한 데이터) ✅ 각 키의 값에 tenantId: 282 포함 ✅ timestamp 포함 ``` **확인 방법**: ```javascript // Console에서 실행 Object.keys(sessionStorage).filter(k => k.startsWith('mes-')) // 결과: ["mes-282-itemMasters", "mes-282-specificationMasters", ...] ``` --- ### 시나리오 2: 페이지 새로고침 시 캐시 로드 ✅ **목적**: 캐시에서 데이터를 제대로 불러오는지 확인 **단계**: 1. 시나리오 1 완료 후 2. `F5` 또는 `Cmd+R`로 새로고침 3. Console에서 로그 확인 **기대 결과**: ``` ✅ Console 로그: [Cache] Loaded from cache: itemMasters [Cache] Loaded from cache: specificationMasters ... ✅ 입력했던 데이터가 그대로 표시됨 ✅ 서버 API 호출 없이 캐시에서 로드 ``` --- ### 시나리오 3: TTL (1시간) 만료 확인 ⏱️ **목적**: 캐시가 1시간 후 자동 삭제되는지 확인 **⚠️ 주의**: 실제 1시간을 기다릴 수 없으므로 **수동 테스트** **단계**: 1. sessionStorage에서 캐시 데이터 조회: ```javascript 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); ``` 2. **수동으로 timestamp 수정** (과거 시간으로): ```javascript 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)); ``` 3. 페이지 새로고침 **기대 결과**: ``` ✅ Console 로그: [Cache] Expired cache for key: itemMasters ✅ 만료된 캐시 자동 삭제 ✅ 초기 데이터로 리셋 ``` --- ### 시나리오 4: 다중 탭 격리 확인 🔗 **목적**: 탭마다 독립적인 sessionStorage 사용 확인 **단계**: 1. **탭 1**: TestUser3 로그인 → 데이터 입력 (규격 마스터 A) 2. **탭 2**: 동일 URL을 새 탭으로 열기 (`Cmd+T` → URL 복사) 3. 탭 2에서 sessionStorage 확인 **기대 결과**: ``` ✅ 탭 2의 sessionStorage는 비어있음 ✅ 탭 1의 데이터가 탭 2에 공유되지 않음 ✅ 각 탭이 독립적으로 동작 sessionStorage는 탭마다 격리됨! ``` **확인 방법**: ```javascript // 탭 1 sessionStorage.setItem('test', 'tab1'); // 탭 2 (새로 열린 탭) sessionStorage.getItem('test'); // null (공유 안 됨) ``` --- ### 시나리오 5: 탭 닫기 시 자동 삭제 🗑️ **목적**: 탭을 닫으면 sessionStorage가 자동으로 삭제되는지 확인 **단계**: 1. 탭에서 데이터 입력 2. Application → Session Storage에서 데이터 확인 3. **탭 닫기** 4. **동일 URL을 새 탭으로 다시 열기** 5. Session Storage 확인 **기대 결과**: ``` ✅ sessionStorage가 완전히 비어있음 ✅ 이전 탭의 데이터가 남아있지 않음 ✅ 새로운 세션으로 시작 ``` --- ### 시나리오 6: 로그아웃 시 캐시 삭제 🚪 **목적**: 로그아웃하면 테넌트 캐시가 완전히 삭제되는지 확인 **단계**: 1. TestUser3 로그인 → 데이터 입력 2. sessionStorage 확인 (캐시 있음) 3. **로그아웃 버튼 클릭** 4. Console 로그 확인 5. 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도 삭제됨 ``` **확인 방법**: ```javascript // 로그아웃 후 Object.keys(sessionStorage).filter(k => k.startsWith('mes-282-')) // 결과: [] (빈 배열) ``` --- ### 시나리오 7: 테넌트 전환 시 캐시 삭제 🔄 **⚠️ 현재 제약**: 모든 테스트 사용자가 tenant.id: 282를 사용 중 **필요 작업**: 다른 tenant.id를 가진 사용자 추가 #### 7-1. 테스트 사용자 추가 (tenant.id: 283) `src/contexts/AuthContext.tsx` 수정: ```typescript 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. 테넌트 전환 테스트 **단계**: 1. **TestUser3 로그인** (tenant.id: 282) - 데이터 입력 (규격 마스터 A, B) - sessionStorage 확인: `mes-282-specificationMasters` 2. **로그아웃** 3. **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-* 키로 저장됨 ✅ 테넌트 간 데이터 격리 확인 ``` **확인 방법**: ```javascript // 테넌트 전환 후 Object.keys(sessionStorage).forEach(key => { console.log(key); }); // 결과: // mes-283-itemMasters (새 테넌트) // mes-283-specificationMasters // (mes-282-* 키는 없어야 함!) ``` --- ### 시나리오 8: PHP 백엔드 tenant.id 검증 🛡️ **⚠️ 주의**: PHP 백엔드가 실행 중이어야 함 **목적**: 다른 테넌트의 데이터 접근 시 403 반환 확인 **단계**: 1. **TestUser3 로그인** (tenant.id: 282) 2. 브라우저 Console에서 다른 테넌트 API 직접 호출: ```javascript // 자신의 테넌트 (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 **해결**: 1. Console에서 확인: ```javascript // AuthContext의 currentUser 확인 console.log(JSON.parse(localStorage.getItem('mes-currentUser'))); // tenant.id 확인 console.log(user?.tenant?.id); ``` 2. ItemMasterContext가 AuthContext 하위에 있는지 확인 ### 문제 2: 테넌트 전환 시 캐시가 삭제되지 않음 **증상**: 이전 테넌트 캐시가 남아있음 **원인**: - `useEffect` 의존성 배열 문제 - `previousTenantIdRef` 초기화 안 됨 **해결**: ```typescript // 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 체크 안 함 **해결**: ```typescript // TenantAwareCache.ts 확인 get(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가 포함되지 않음 **해결**: 1. PHP 백엔드 확인 (프론트엔드 작업 범위 밖) 2. JWT payload에 `tenant_id` 포함 여부 확인 3. 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