공통 사용자 그룹 동기화 런북
공통 사용자 그룹(그룹 + 멤버십)은 PortalConsole 이 원장이고, 소비 포털이 REST 로 push/pull 한다. 리소스 권한(resource/action)은 각 포털 소유로 남는다. 배경·소유 경계는 central-directory-integration.md 「개정 (2026-10-04)」.
계약
| 방향 | 엔드포인트 | scope | 비고 |
|---|---|---|---|
| pull | GET /api/directory/v1/user-groups?since=&includeDeleted= | directory:read | includeDeleted 기본 포함(true). tombstone이 있어야 삭제가 전파된다 |
| push(생성·수정) | POST /api/directory/v1/user-groups | directory:sync | 본문 { id?, name, description?, isActive? }. 멱등 |
| push(삭제) | DELETE /api/directory/v1/user-groups/:groupId | directory:sync | 소프트 삭제(tombstone). 멱등 |
| push(구성원 추가) | POST /api/directory/v1/user-groups/:groupId/members | directory:sync | 본문 { directoryUserId }. 멱등 |
| push(구성원 제거) | DELETE /api/directory/v1/user-groups/:groupId/members/:directoryUserId | directory:sync | 멱등 |
조인 키: PC 의 directory 사용자 id 는 users.openId = "directory:<usr_…>" → 접두사를 뗀 값이 원장의 directory_user_id 와 같다. 관리자 화면은 admin.listUserGroups 가 아니라 directory.listUserGroups(tRPC)로 조회한다.
Process Center 측 동작
server/directoryGroupSync.ts
- push outbox:
portalGroupRouter의 로컬 쓰기(생성·수정·삭제·구성원 추가/제거)가portal_user_group_sync_queue에 op 를 적재한다.flushUserGroupOutbox()가 원장으로 전송하고, 실패하면attempts를 올리고 지수 backoff(최대 1시간)로next_attempt_at을 민다. - pull:
pullDirectoryUserGroups()가 원장을 로컬로 미러한다.tombstone은 로컬에서 제거한다. - 순서:
syncUserGroups()=flush→pull. 원장이 정본이라 충돌 시 원장 값이 이긴다. - 임포트(이름 매칭): 로컬 그룹과 원장 그룹의 이름이 같은데 id 가 다르면, 로컬 구성원·리소스 권한을 원장 id 로 옮기고 로컬 행을 지운다. 옮긴 구성원은
ADD_MEMBER로 큐에 넣어 원장에도 반영한다. - 격리: 그룹 동기화 실패는 사용자·부서 동기화를 실패로 만들지 않는다(
syncDirectorySnapshot이 warn 만 남긴다). 원장이 아직user-groupsAPI 를 제공하지 않아도 사용자·부서는 정상 동기된다.
마이그레이션
- PG:
drizzle/postgres-migrations/0023_user_group_sync_queue.sql - MySQL:
drizzle/0032_user_group_sync_queue.sql - 콘솔 원장 테이블: PG
drizzle/postgres-migrations/0012_user_groups.sql, MySQLdrizzle/0039_user_groups.sql
점검
- 콘솔에서 그룹을 만들고 구성원을 넣는다 →
directory_user_groups/directory_user_group_members확인. - PC 스케줄러가 한 주기 돌면 PC 의
portal_user_groups/portal_user_group_members가 원장과 같아진다. - PC 에서 그룹을 만들면
portal_user_group_sync_queue에UPSERT가 쌓였다가 비워진다(원장 반영). attempts > 0행이 남아 있으면last_error를 보고 scope·키·central API 상태를 확인한다. 붙지 못한 op 는next_attempt_at이후 재시도된다.- 삭제는 양쪽 모두 소프트 삭제로 전파된다 —
tombstone을 건너뛰는 pull 은 삭제를 되살리므로 금지.