hjjeong #3

Merged
hjjeong merged 20 commits from hjjeong into main 2026-05-08 08:29:47 +00:00

20 Commits

Author SHA1 Message Date
hjjeong 74ddc4936f Merge branch 'main' into hjjeong 2026-05-08 14:59:12 +09:00
DDD1542 59f5cf22f0 Merge remote-tracking branch 'origin/gbpark-node' into gbpark-node
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m4s
2026-05-07 17:09:57 +09:00
DDD1542 c4631efbd2 중간저장 2026-05-07 17:06:26 +09:00
hjjeong 84b9060e4e Merge branch 'hjjeong' into gbpark-node
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m36s
2026-05-07 17:04:57 +09:00
hjjeong 3280be8bd4 fix(rolesList): cross-tenant row 식별 + 메뉴 트리 스크롤 보강
cross-tenant fan-out 결과에서 회사 A·B 의 동일 objid 가 합본에 들어와
React key 중복 경고 발생 + isSelected 가 회사 구분 못 하던 문제.

- li key: role.objid → \`\${company_code}-\${objid}\` 조합으로 unique
- isSelected 비교: objid + company_code 둘 다 매칭
- selectedRole 유효성 체크(useEffect)에도 company_code 매칭 추가

추가:
- 메뉴 전체 트리구조에 자체 스크롤 (maxHeight: calc(100vh - 32rem))
- thead sticky top-0 + bg-muted (투명도 제거) → 스크롤 시 헤더 가려짐 해소

SUPER_ADMIN cross-tenant 정책 변경 없음 (모든 회사 합본 표시 유지),
React 식별만 정확해지는 fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:51:16 +09:00
hjjeong b782bb298f merge: origin/gbpark-node → hjjeong (60 commits, 5 conflicts resolved)
충돌 해결 5개 파일:
- .gitignore: .envrc/.direnv (hjjeong direnv 셋업) + .omc/ (gbpark) 양쪽 보존
- docs/MULTI_TENANCY_ARCHITECTURE.md: *.localhost dev 분기 + *.invyone.com/solution.invyone.com 통합
- frontend/lib/api/client.ts: 1-b *.localhost:8081 dev + 1-c DEV_TENANT_HOST(nip.io):8083 + invyone.com 신 도메인
- frontend/lib/tenant/subdomain.ts: IPv4 차단 + *.invyone.com + DEV_TENANT_HOST + *.localhost 모두 처리
- frontend/app/(auth)/login/page.tsx: B안 채택 — buttons 항상 렌더, className 만 mounted 가드 (next-themes 표준 패턴)

검증:
- backend: ./gradlew compileJava 성공 (Java 21)
- frontend: 머지된 4개 파일 관련 타입 에러 0개

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:51:06 +09:00
hjjeong 205428533d fix(rolesList): 직원 체크박스 클릭 시 이중 토글로 해제 안 되던 버그
증상: 권한있는/없는 직원 영역의 체크박스를 클릭하면 즉시 다시 해제됨
처럼 보이고 (사실은 두 번 토글됨), 체크 상태 자체가 유지 안 됨.
전체선택 버튼 (별개 핸들러) 만 정상 동작.

원인: <li onClick={토글}><Checkbox onCheckedChange={토글} /></li> 구조에서
체크박스 클릭 → onCheckedChange 트리거 (토글 1회) → 이벤트가 li 로 버블 →
li onClick 트리거 (토글 1회 더) → 합쳐서 2회 토글 = 원상복귀.

수정: Checkbox 에 onClick={(e) => e.stopPropagation()} 추가해서 li 로
버블 차단. li 빈 영역 클릭 시는 li onClick 만 동작, 체크박스 직접 클릭
시는 onCheckedChange 만 동작 — 둘 다 정확히 1회 토글.

권한있는 직원 + 권한없는 직원 두 곳 모두 수정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:11:39 +09:00
hjjeong b5a60d1c8b fix(role): getRoleNonMemberList OGNL test 의 inner-quote 컨벤션 위반 수정
증상: SUPER_ADMIN cross-tenant 모드에서 권한 그룹 클릭 시 workspace
500 — NumberFormatException: For input string: "TEST02"

원인: role.xml getRoleNonMemberList 의 if 가
    test="company_code != null and company_code != '' and company_code != '*'"
형태로 single-quote 안에 single-quote 가 박혀, OGNL 이 '*' 를 number 로
변환 시도하다 NumberFormatException. 단일 모드 (test01.localhost) 에서는
group.company_code 가 시드 잔여 '*' 라 if 가 false 로 fall-through 해
안 터졌고, cross-tenant 에서 명시적 "TEST02" 가 들어가니 평가 시도
→ 깨짐.

CLAUDE.md "OGNL test: 바깥 작은따옴표" 컨벤션대로 수정:
    test='company_code != null and company_code != "" and company_code != "*"'

검증: TEST02 의 '관리자' 그룹 workspace 호출 → HTTP 200, members: 0,
nonMembers: 2 (test02_admin, test02), menus: 0 정상 반환.

22, 334번 라인의 다른 if 문은 != '*' 가 없어 OGNL 평가가 numeric
coercion 까지 안 가므로 무수정 (회귀 위험 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:06:19 +09:00
hjjeong 42f7ae35db fix(cross-tenant): rolesList 회사 dropdown 이 cross-tenant 모드에서 무시되던 문제
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>
2026-04-29 21:48:48 +09:00
hjjeong 7d9ec39b5d feat(cross-tenant): SUPER_ADMIN 의 회사별 권한관리 WRITE (Phase 2)
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>
2026-04-29 18:45:55 +09:00
hjjeong 4d19c31440 feat(cross-tenant): 부서 endpoint + UserFormModal 회사-우선 reorder
직전 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>
2026-04-29 18:38:30 +09:00
hjjeong a41f99c579 feat(cross-tenant): SUPER_ADMIN 의 회사별 사용자 WRITE (Phase 1)
지금까지 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>
2026-04-29 17:22:52 +09:00
hjjeong cdc55dfd48 feat(cross-tenant): truncated/failed 안내 배너 (READ 트랙 마무리)
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>
2026-04-29 17:14:48 +09:00
hjjeong 280e25a4df notes(hjjeong): cross-tenant §11.2 부분 실패 시뮬레이션 검증 (2026-04-29)
테넌트 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>
2026-04-29 11:42:12 +09:00
hjjeong 568eb14503 notes(hjjeong): cross-tenant fan-out 2회사 머지 검증 결과 (2026-04-29)
설계서 §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>
2026-04-29 11:27:53 +09:00
hjjeong a3c74f926c notes(hjjeong): cross-tenant 27/28.md 현재 상태 반영 (2026-04-29)
설계 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>
2026-04-29 09:50:41 +09:00
hjjeong 36d6ad508d notes: hjjeong 작성 MD 3개를 notes/hjjeong/ 으로 이동 + 현재 상태 반영
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>
2026-04-29 09:50:08 +09:00
hjjeong a6be4f2efe 사용자관리 테이블 자체 스크롤 — viewport 기반 max-height 로 강제
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>
2026-04-28 17:52:30 +09:00
hjjeong 383b837a60 dev *.localhost 테넌트 라우팅 + direnv/Java 21 dev 환경 정비
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>
2026-04-28 17:52:30 +09:00
hjjeong e16fb16987 어드민 cross-tenant 집계 (SUPER_ADMIN) + 사용자관리 자체 스크롤
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>
2026-04-28 17:52:30 +09:00