Phase 2 검증 중 발견. SUPER_ADMIN 이 admin 도메인에서 회사 필터를 "시연용회사2"
선택해도 TEST01 의 권한 그룹이 그대로 보이던 UI 버그.
원인: roleAPI.getList 의 cross-tenant 분기는 fan-out 으로 모든 회사 그룹을
모아 응답에 company_code 박아 돌려줌. 화면이 그 결과를 그대로 렌더하면서
selectedCompany dropdown 으로 client-side 필터링을 안 했음.
수정: rolesList 의 filteredRoleGroups 가 isSuperAdmin && selectedCompany!="all"
조건일 때 company_code 일치 행만 통과시키도록 한 줄 추가.
알려진 이슈 (별개, Phase 2 후속):
- GET /api/admin/cross-tenant/roles/{id}/workspace 가 500 떨어짐.
단일 모드 GET /api/roles/{id}/workspace 도 group=null 빈 결과 — 사전
존재 시드/스키마 mismatch 추정 (RoleService.getRoleWorkspace 의 5개 mapper
중 하나가 깨짐). cross-tenant 분기 자체 (runInCompany) 는 정상.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1(사용자관리) 패턴을 권한관리에 동일 적용. 권한 그룹 CRUD,
멤버 토글, 메뉴 권한 토글 모두 회사 컨텍스트 임시 전환 후 처리.
신규 백엔드
- crosstenant/CrossTenantRoleController.java
/api/admin/cross-tenant/roles/** — 8개 endpoint
· POST — 권한 그룹 생성 (body.company_code 필수)
· PUT /{id} — 권한 그룹 수정 (body.company_code 필수)
· DELETE /{id}?company_code= — 삭제
· GET /{id}/workspace?company_code= — 그룹 + 멤버 + 메뉴 통합 로드
· GET /menus/all?company_code= — 회사 메뉴 트리 (권한 설정용)
· POST /{id}/members/{userId}?company_code= — 멤버 1명 추가
· DELETE /{id}/members/{userId}?company_code= — 멤버 1명 제거
· PATCH /{id}/menu-permissions/{menuObjid} — 토글
CrossTenantExecutor 재사용. 기존 RoleController 무수정 (회귀 0).
중요: @RequestAttribute("user_id") 가 토큰 없을 때 missing 에러로 500
떨어지는 문제 — required=false 로 가드까지 안전하게 도달하도록.
프론트
- lib/api/role.ts — 7개 메서드(create/update/delete/getWorkspace/
getAllMenus/addSingleMember/removeSingleMember/toggleMenuPermission)에
isCrossTenantMode() 분기 + companyCode 인자 추가
- RoleFormModal — update 시 editingRole.company_code 같이 전달
- RoleDeleteModal — delete 시 role.company_code 같이 전달
- rolesList/page.tsx — loadWorkspace / addSingleMember / removeSingleMember /
toggleMenuPermission 호출 시 selectedRole.company_code 전달
검증 (curl, SUPER_ADMIN 토큰):
- 토큰 없음 → 403 super_admin_required
- POST 권한 그룹 (TEST02) → 201, /roles fan-out 에 by={TEST01:1, TEST02:1}
- DELETE → 200, fan-out by={TEST01:1} 로 복귀
미구현 (Phase 2 후속, 별도 작업):
- 일괄 멤버 추가/제거/diff (PUT/POST /members)
- 메뉴 권한 일괄 설정 (PUT /menu-permissions)
- 사용자별 권한 그룹 조회
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
직전 Phase 1 의 후속 폴리시.
신규 백엔드
- crosstenant/CrossTenantDeptController.java
GET /api/admin/cross-tenant/departments?company_code=TEST02
단일 모드 GET /admin/departments 와 응답 형태 동일. company_code query param
으로 명시된 회사 DB 컨텍스트로 임시 전환해서 부서 트리 반환.
버그 수정: 메타 DB DEPT_INFO 시드 (qnc/COMPANY_7 등 다른 회사 부서) 가
TEST02 선택 시에도 dropdown 에 섞여 보이던 문제 해결.
프론트
- lib/api/user.ts — getDepartmentList(companyCode) 가 isCrossTenantMode() 면
/admin/cross-tenant/departments?company_code= 호출.
cross-tenant 모드 + companyCode 미지정 → 빈 배열 반환 (회사 안 골랐는데
메타 부서 보여주는 것 방지).
UserFormModal
- 회사 dropdown 을 폼 가장 위로 이동 — 사용자 ID 중복확인·부서 선택이
모두 회사에 의존하므로 자연스러운 입력 순서
- SUPER_ADMIN 인데 회사 미선택 상태에선 사용자 ID input + 중복확인 버튼
disable + placeholder "회사 먼저 선택"
- checkUserIdDuplicate 가드: 회사 미선택이면 "회사를 먼저 선택해주세요"
(백엔드의 400 "company_code 가 비어있음" 보다 친절)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
지금까지 cross-tenant 는 READ 전용. admin 도메인에서 사용자 등록하면
JWT.company_code='*' 가 그대로 박혀 메타 DB 에 INSERT 되던 버그 해결.
이제 SUPER_ADMIN 이 폼의 "회사" 드롭다운에서 TEST01/TEST02 등 선택하면
그 회사 DB 에 정확히 INSERT.
신규 백엔드
- crosstenant/CrossTenantExecutor.java — 회사 컨텍스트 임시 전환 헬퍼
(company_code → db_name → ensureTenantPool → set → run → restore)
- crosstenant/CrossTenantUserController.java — /api/admin/cross-tenant/users
9개 endpoint (POST/PUT/DELETE/PATCH/with-dept/check-duplicate/단건/이력)
- mapper/provisioning.xml — resolveDbNameByCompanyCode (active 회사만)
기존 단일 회사 모드 (POST /admin/users 등) 무수정 — 회사 도메인
컨텍스트에서 회귀 0.
프론트
- lib/api/user.ts — createUser/updateUser/updateUserStatus/checkDuplicateUserId/
saveUserWithDept 가 isCrossTenantMode() 면 새 endpoint + body.company_code 로 분기
- UserFormModal — checkDuplicateId 호출 시 formData.company_code 같이 전달
- useUserManagement — status toggle 시 row 의 company_code 같이 전달
검증 (curl, SUPER_ADMIN 토큰):
- 토큰 없음 → 403 super_admin_required
- company_code 없음 → 400 "company_code 가 비어있음"
- 잘못된 company_code → 400 "등록되지 않았거나 비활성 회사"
- check-duplicate: TEST01.test02_admin → not_dup, TEST02.test02_admin → dup ✓
- POST 사용자 → TEST02 USER_INFO +1, TEST01·메타 격리 ✓
- /users fan-out: by={'*':8, 'TEST01':1, 'TEST02':2}, hjtest_ct_001 in TEST02만 ✓
- DELETE → status=inactive (soft) ✓
미구현 (Phase 1 후속):
- 부서 dropdown (cross-tenant department endpoint 별도 필요)
- 비밀번호 초기화 모달의 cross-tenant 분기 (UserPasswordResetModal)
- Phase 2 권한관리 (별도 커밋)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SUPER_ADMIN cross-tenant 모드에서 회사당 cap 200 에 걸리거나 한 회사
조회 실패 시 화면 상단에 안내 배너 노출. 아무 메타 없으면 자리 안 잡음.
신규
- components/common/CrossTenantBanner.tsx — amber(truncated) + red(failed)
v5 토큰 (surface-solid + glow-sm) 기반 솔리드 배너. blur 안 씀
API 클라이언트 4개에 cross_tenant_meta 노출
- lib/api/user.ts — userAPI.getList 응답에 cross_tenant_meta 추가
- lib/api/role.ts — roleAPI.getList 동일
- lib/api/batch.ts — BatchAPI.getBatchConfigs 동일
- lib/api/multilang.ts — getLangKeys 동일 (i18nList 페이지는 아직 직접
호출 패턴이라 자동 적용 X — 후속에서 페이지를 getLangKeys 로 통일하면 동작)
페이지 마운트 (3개)
- userMng/userMngList — useUserManagement hook 에 crossTenantMeta state 추가
- userMng/rolesList — loadRoleGroups 에서 메타 set
- automaticMng/batchmngList — loadBatchConfigs 에서 메타 set
- systemMng/i18nList — 스킵 (cross-tenant aggregation 미적용 상태, 별도 작업)
설계서 §11 검증 (직전 §11.2 부분 실패 시뮬) 결과: failed 배너가
header X-CrossTenant-Failed 와 동일 정보로 화면에 노출됨.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
테넌트 DB 만 만져도 됨 — 메타 DB 무수정.
TEST02 의 USER_INFO 를 임시 RENAME 해서 SELECT 실패 유도 → fan-out
호출 → 즉시 롤백. 메타 DB·다른 테이블 일체 영향 없음.
결과:
- RENAME 후 /users → HTTP 200, header X-CrossTenant-Failed: TEST02
total=9 q=2 failed=1 by={'*':8, 'TEST01':1} (TEST02 0)
- 롤백 후 /users → total=10 failed=0 by 원복
검증된 항목:
- fail-open: 한 회사 실패해도 전체 응답 200
- 회사 격리: TEST01·메타 행 영향 없음
- companies_failed: 1 + failed_company_codes: ["TEST02"]
- 응답 헤더 X-CrossTenant-Failed: TEST02
문서 갱신:
- 설계 27.md §11.2: ⏳ → ✅ + 실행로그 §9.4 링크
- 실행 28.md §9.4 신설 — 시뮬레이션 SQL + 결과 + 검증 항목 5개
- 실행 28.md §9.5 — 남은 시나리오 (§11.4 락 비획득 / §11.5 캐시 N/A)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
설계서 §11.1 (행복 경로) 의 첫 다회사 실증.
SUPER_ADMIN 토큰으로 /_active-companies + 4개 fan-out 엔드포인트 직접 curl.
검증 결과 (TEST01 + TEST02 둘 다 active):
- /users total=10 q=2 failed=0 by={'*':8, 'TEST01':1, 'TEST02':1}
- /roles total=1 q=2 failed=0 by={'TEST01':1}
- /batches total=14 q=2 failed=0 by={'TEST01':7, 'TEST02':7} ← 균등 머지
- /lang-keys total=0 q=2 failed=0 by={} ← 회사 DB 시드 없음
발견:
- /users 의 '*' 8행은 버그 아님 — listUsers 만 includeMeta=true 호출,
메타 DB SUPER_ADMIN 들을 company_code='*' 로 prepend (의도된 설계).
- runFanOut 의 마지막 boolean 은 wrapSearchWithPercent 일 뿐 includeMeta 아님
(시그니처 분리). roles/batches/lang-keys 는 모두 includeMeta=false.
- /lang-keys 0건은 회사 DB MULTI_LANG_KEY_MASTER 가 비어있는 것 (failed=0).
이전 §3 의 "TEST01: 646건" 은 시점/컨텍스트 다른 측정으로 추정.
문서 갱신:
- 설계 27.md §11: 11.1 ✅, 11.3 fan-out 검증 추가 인용
- 실행 28.md §6 #3 → ✅, §8 활성 회사 TEST02 추가
- 실행 28.md §9 신설 — 결과 표 + 해석 + 검증된 항목 + 미검증 + 재현용 명령
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
설계 27.md
- 상단 최종 갱신 + 실행 로그 링크 + Phase A/B/C 완료 배너
- §10 단계별 체크리스트: A 4/5, B 3/4, C 3/14, 페이지네이션 cap ✓, D 미진행, E 일부
- §11 검증 시나리오: 5개 시나리오 상태 표 + TEST01 실 검증 결과 인용
- §14 다음 세션 진입 시: 즉시 가능한 4개 작업 우선순위 (TEST02 fan-out 검증 1순위)
- gbpark/ 의 2026-04-24 두 문서 참조를 ../gbpark/ 로 보정
실행 로그 28.md
- 상단 최종 갱신 + 핸드오프 문서 참조
- TL;DR: 푸시·커밋 안 함 → 3 커밋 푸시 완료, TEST02 활성 명기
- §4.4 신규 — *.localhost 라우팅이 같은 커밋 배치에 묶인 사실 + 2026-04-29 검증 결과
- §5: 워킹트리 → 3개 커밋 분배 의도 표
- §6: 권장 단계 8개 → 9개 (Gitea PR 추가), 각 항목 ✅/🟡/⏳/❌ 표기
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md "notes/{git-user}/{date}-{slug}.md" 규약대로 정리.
git config user.name=hjjeong 인데 gbpark/ 에 쌓이고 있던 3개 분리.
- 2026-04-27-cross-tenant-admin-aggregation.md (설계, 내용 갱신: Phase A/B/C 체크리스트, §11 검증 시나리오 실 결과 병기, §14 다음 단계)
- 2026-04-28-cross-tenant-execution-log.md (실행 로그, 내용 갱신: 커밋·푸시 상태, §4.4 *.localhost 같은 배치 묶음 기재, §5 커밋 분배 의도, §6 권장 단계 ✅/🟡/⏳/❌ 표시)
- 2026-04-28-localhost-tenant-routing-handoff.md (순수 이동)
cross-tenant 27.md 의 같은 폴더 참조였던 2026-04-24 두 문서는
gbpark/ 에 그대로 남아 ../gbpark/ 로 경로 보정. 외부 참조(../../docs,
../../backend-spring 등) 는 깊이 동일해 무수정.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scrollContainer 모드의 flex 기반 height 계산이 shadcn Table 의 내부 wrapper
(data-slot="table-container", overflow-x-auto) 와 충돌해 잘림 발생.
flex-1/min-h-0 대신 max-h-[calc(100vh-280px)] + overflow-y-auto 로 강제.
hack 성이지만 안정적으로 동작.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubdomainResolverFilter.extractSubdomain() 가 2파트 {sub}.localhost 호스트도
첫 파트로 파싱 (RFC 6761). 운영 3파트 경로 무변경. 단위 테스트 8건 추가.
frontend/lib/api/client.ts 에 *.localhost (bare 제외) 직접 호출 분기 1-b 추가.
8081 로 직결해 Host 헤더 보존. subdomain.ts 도 동일 규칙 적용.
application.yml CORS 디폴트에 http://*.localhost:[*] 패턴 추가.
docs/MULTI_TENANCY_ARCHITECTURE.md §4.2 (실행 모드 A/B) + §6 (1-b 분기) 갱신.
.gitignore 에 .envrc/.direnv 추가, .java-version=21 명시 (jenv 호환).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SUPER_ADMIN 토큰(company_code=*)이면 등록 회사들 DB 를 순회해 결과를
집계해 돌려주는 CrossTenantAggregator/Controller 추가. 사용자/권한그룹/
배치/다국어 키 4개 도메인의 list API 가 cross-tenant 모드 지원.
UserTable + ResponsiveDataView 에 compact/scrollContainer prop 추가.
페이지 헤더/툴바/페이지네이션은 고정, 테이블만 자체 스크롤.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
화면 폭:
- 4개 페이지(사용자/권한 그룹/권한/부서) max-w 제거 → 화면 전체 사용
- 헤더 텍스트 컴팩트화 (text-xl + 작은 설명문)
부서관리 dept_info 스키마 1:1 매핑:
- 누락 필드 6개 추가: master_sabun, master_user_id, location, location_name, data_type, sales_yn
- frontend types/department.ts: Department / DepartmentFormData 확장
- frontend deptMngList: 폼 추가 + handleSelectDepartment / payload 매핑
- backend mapper: INSERT/UPDATE에 6개 컬럼 추가
- backend DepartmentService: insertParams/updateParams에 6개 필드 추가
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 사용자/권한 그룹/권한/부서 관리 4개 페이지 풀HD 기준 max-w 제한, 헤더 컴팩트화
- 권한 그룹 관리: 그리드 4~5열로 카드 컴팩트화, 검색 추가, 호버/액션 디자인 개선
- 부서관리: '일괄등록' 모달(CSV 라인 입력) / '변경이력' 모달 onClick 핸들러 연결
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 백엔드 TenantController: GET /api/tenant/check?subdomain=xxx
(메타 DB 강제 라우팅 + CompanyResolver 로 존재 여부 반환)
- frontend/lib/tenant/subdomain.ts: 호스트 파싱 + 예약어(solution/www/admin 등) 제외
- TenantGuard 클라이언트 컴포넌트: layout.tsx 에서 wrap,
sessionStorage 로 같은 서브도메인 재체크 방지
- /tenant-not-found 페이지: v5 solid+glow 스타일
등록되지 않은 서브도메인 접속 시 즉시 /tenant-not-found 로 리다이렉트.
- application.yml, k8s configmap, docker-compose 4종: SPRING_DATASOURCE_URL path
- provisioning 코드: {prefix}_vexplor → {prefix}_invyone 테넌트 DB 네이밍 규칙
- 프론트 마법사: Step1Basic, Step4Run 미리보기 라벨
- CompanyAccordionRow: 기본 DB 이름 포맷
- 마이그레이션/멀티테넌시 문서 동기화
- Traefik 와일드카드 설정 산출물 보관 (notes/)
비밀번호(vexplor0909!!) 및 역사 기록 문서(INVYONE_CONCEPT,
DDD1542, test-output, dashboard-runtime-fixes) 는 의도적으로 미변경.
- /admin 홈을 단순 링크 카드 → Welcome/Stats/SystemStatus/회사활동/최근변경/빠른진입 구성의 관리자 대시보드로 교체 (mock 데이터, 실데이터 배선은 후속)
- PopupTemplateRenderer 신규 — 팝업은 canvas 1:1 유지가 필요하므로 반응형 TemplateRenderer 와 경로 분리, absolute 좌표 + transform scale 로 창을 꽉 채움. form-popup 이 templateId 로 자체 fetch 해서 stale views(screenResolutions 누락) 문제 해소
- DashboardCard: 팝업 outer 크기를 canvas + 브라우저 크롬 보정치로 계산, 부모가 template 객체 직접 전달하던 것 제거
- TemplateRenderer: BlockRenderer export, formRow 기반 columnName/value/onChange/onRowSelect 등 바인딩 props 전달 강화
- TableComponent: 행 클릭/체크박스 선택 시 onRowSelect / onSelectedRowsChange emit, 헤더 전체 선택 동작 구현, 싱글/멀티 선택 분기 정리
- AppLayout: 모드 전환 연출 강화(burst ring + particles / 헤더 sweep / breadcrumb swap / glow flash), horizontal nav + 데스크톱에서 햄버거 자동 접힘, 헤더 도구군 래핑 구조 정리
- DashboardEmpty 삭제 → 공통 EmptyDashboard 로 통합
- dashboard.css: ud-htools stagger(오른쪽 → 왼쪽 슬라이드) 애니메이션 적용
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
- 타이포 스케일: body 12→14px, caption 9.6→12px, display 25.6→32px, 위계 강화
- 헤더 우측 3그룹화 (대시보드액션 | 테마/알림/설정 | 모드+프로필), v5-hdr-sep 구분자 추가
- 사이드바 SUPER_ADMIN 회사 카드 borderless slim 라벨로 압축
- 메뉴명 빈 텍스트 방어 + title 속성 추가
- 빈 대시보드(EmptyDashboard) 리디자인: 탭없음/위젯없음 2상태 분리, 2-CTA 카드
- 로그인 코스믹 공연 축소: 별 150→30, 파티클 20→0, 카피 한글화 (로그인 버튼/서브타이틀)
- 모드 전환 burst/sweep/badge-zoom 제거, sidebar stagger morph만 유지 (handleModeSwitch 100→25줄)
- View transitions duration 1800ms → 500ms
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>