Files
sam-react-prod/claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md
byeongcheolryu 65a8510c0b fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:19:50 +09:00

12 KiB

멀티 테넌시 검증 및 테스트 가이드

작성일: 2025-11-19 목적: Phase 1-4 구현 후 테넌트 격리 기능 검증


📋 목차

  1. 테스트 환경 준비
  2. 테스트 시나리오
  3. 체크리스트
  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가 제대로 동작하는지 확인

단계:

  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 포함

확인 방법:

// 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에서 캐시 데이터 조회:

    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 수정 (과거 시간으로):

    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는 탭마다 격리됨!

확인 방법:

// 탭 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도 삭제됨

확인 방법:

// 로그아웃 후
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. 테넌트 전환 테스트

단계:

  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-* 키로 저장됨
✅ 테넌트 간 데이터 격리 확인

확인 방법:

// 테넌트 전환 후
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 직접 호출:
// 자신의 테넌트 (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에서 확인:

    // AuthContext의 currentUser 확인
    console.log(JSON.parse(localStorage.getItem('mes-currentUser')));
    
    // tenant.id 확인
    console.log(user?.tenant?.id);
    
  2. 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가 포함되지 않음

해결:

  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