31 KiB
웍스모바일 Directory 연동 기술 검토
개요
- 대상 Epic:
orgfront와 웍스모바일 Directory API 간 한맥가족 사용자/조직 연동 - 관련 이슈: #668 한맥가족 이메일 local-part unique 정책
- 대상 마일스톤:
한맥가족사 조직도 반영 및 웍스모바일 연동(id=42) - 기준 SoT:
hanmac-family테넌트 subtree 하위 Kratos identity - 작성일: 2026-05-04
현재 Baron SSO 구조 요약
Baron SSO는 Ory Stack을 SSOT로 둡니다. docs/SoT_Architecture_Policy.md와 docs/tenant-usergroup-policy.md 기준으로 Identity는 Kratos, 권한/멤버십은 Keto가 원장입니다. PostgreSQL은 Ory에 저장되지 않거나 조회가 불가능한 Worksmobile/조직 표시/검색 데이터의 read model과 처리 상태 저장소로만 사용합니다.
현재 사용자 생성 흐름은 다음과 같습니다.
backend/internal/handler/user_handler.goCreateUser:companyCode로 tenant slug를 찾아traits["tenant_id"]에 tenant UUID를 저장합니다.BulkCreateUsers: CSV/import row별로 tenant slug를 tenant UUID로 변환하고 Kratos identity를 생성합니다.- Kratos identity id는
users.id로 그대로 저장됩니다. 이 값이 웍스모바일userExternalKey후보입니다. mapToLocalUser는traits["tenant_id"]를users.tenant_id로 저장합니다.
backend/internal/handler/tenant_handler.goCreateTenant:TenantService.RegisterTenant호출 후 domains/config를 별도로 저장합니다.UpdateTenant: tenant 필드와 parent relation을 갱신합니다.
backend/internal/service/keto_relay_worker.goketo_outbox를 polling하여 Keto relation을 비동기로 반영합니다.
한맥가족 이메일 정책은 이미 #668에서 다음 방향으로 구현되어 있습니다.
hanmac-familyroot tenant와 descendant subtree에서 email local-part를 unique로 강제합니다.- local-part unique 검사는
archived를 포함한 모든 사용자 상태를 대상으로 합니다. - 단건 생성은 중복 시
409 Conflict로 차단합니다. - bulk import는
@domain입력 시 이름 기반 local-part를 제안하고, 생성 직전 재검증합니다. preboarding,baron_guest,extended_leave,archived사용자는 Worksmobile 구성원 생성/갱신/backfill 대상에서 제외합니다.baron_guest,extended_leave,archived상태로 전환된 사용자는 기존 Worksmobile 계정 delete/deprovision 대상입니다.
웍스모바일 Directory API 확인 사항
공식 문서 기준 Directory API는 구성원, 조직, 그룹, 직급, 직책, 사용자 유형 등을 관리합니다.
확인한 주요 엔드포인트와 제약은 다음과 같습니다.
- 인증
- API 호출에는 OAuth 2.0 Access Token이 필요합니다.
- 시스템 연동에는 서비스 계정 인증(JWT) 방식이 적합합니다.
- 필요한 scope는 최소
directory이며, 구성원만 다룰 경우user, 조직만 다룰 경우orgunit도 사용 가능합니다.
- 구성원
POST https://www.worksapis.com/v1.0/users- 필수 주요 필드:
domainId,email,userName - SSO 사용 시
userExternalKey가 필요합니다. userExternalKey는 테넌트 내 unique이며%,\,#,/,?를 사용할 수 없습니다.organizations[].orgUnits[].orgUnitId는 resource id 또는externalKey:{orgUnitExternalKey}형태를 사용할 수 있습니다.
- 조직
POST https://www.worksapis.com/v1.0/orgunits- 필수 주요 필드:
domainId,orgUnitName,displayOrder orgUnitExternalKey는 테넌트 내 unique이며%,\,#,/,?를 사용할 수 없습니다.parentOrgUnitId는 resource id 또는externalKey:{orgUnitExternalKey}형태를 사용할 수 있습니다.
- External Key Mapping
POST /users/external-keysPOST /orgunits/external-keys- 기존 웍스모바일 리소스에 External Key가 없는 경우 초기 bulk mapping에 사용합니다.
- 호출 제한/운영 주의
- 조직 추가/수정/부분 수정/이동 API는 도메인당 단일 스레드로 1초에 1회, 순서대로 호출해야 합니다.
- 동일 구성원에 대한 추가/수정/부분수정/전배 API는 동시에 호출하지 않아야 합니다.
- Directory API 조직 연동 배치는 직급/직책/사용자 유형 -> 조직 -> 구성원 -> 그룹 순서를 권장합니다.
- API 동시 호출은 5회 이상 하지 않도록 관리해야 하며, 특히 조직 API는 단일 스레드가 필요합니다.
AdminFront bulk 생성과 NAVERWORKS bulk 생성 비교
구현 전에 adminfront의 기존 조직/사용자 bulk 생성 방식과 adminfront/NAVERWORKS_member_add_sample_English.csv의 구성원 bulk 필드를 비교했습니다.
Baron/AdminFront 기존 방식
- 조직 bulk
adminfront/src/features/tenants/utils/tenantCsvImport.ts- 주요 컬럼:
tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain - Baron tenant tree를 직접 생성/갱신합니다.
- parent는 Baron tenant UUID 또는 slug 기준으로 해석합니다.
- 사용자 bulk
adminfront/src/features/users/components/UserBulkUploadModal.tsxparseUserCSV가BulkUserItem으로 변환한 뒤/api/v1/admin/users/bulk로 전송합니다.- 주요 컬럼:
email,name,phone,role,tenant_slug,department,position,jobTitle,employee_id - 사용자 import 중 없는 tenant를 미리 생성할 수 있고, #668 한맥가족 email local-part unique preview를 거칩니다.
NAVERWORKS sample 방식
- 구성원 bulk sample
- 파일:
adminfront/NAVERWORKS_member_add_sample_English.csv - 주요 컬럼:
LastName,FirstName,ID,Personal email,Sub email,User type,Level,Organization,Position,Mobile/Country code,Mobile/Numbers,Responsibilities,Workplace,Entry Date,Employee number,Account activation time ID는 Baron loginId 및 Worksmobile userExternalKey와는 다른 계정 local-part 성격입니다.Organization은org.1|org.2|org.3|myteam처럼 path 문자열로 제공됩니다.Employee number는 Baron metadata의employee_id로 보존합니다.
- 파일:
구현 반영
parseUserCSV를 quoted CSV/BOM에 대응하도록 보강했습니다.- NAVERWORKS 구성원 sample 필드를 Baron bulk user field로 흡수합니다.
Sub email의 첫 이메일 -> BaronemailID-> BaronloginId, metadatanaverworks_idFirstName+LastName-> BaronnameMobile/Country code+Mobile/Numbers-> BaronphoneOrganizationpath leaf -> Barondepartment, tenant import namePosition-> BaronpositionResponsibilities-> BaronjobTitleEmployee number-> metadataemployee_id
- Worksmobile API payload 생성 시에는 request body의 external key/domainId를 사용하지 않고 Baron UUID와
tenant.config.worksmobile.domainMappings및.env의 domainId 값을 server-side 계산합니다.
매핑 설계
External Key
Baron 내부 UUID는 웍스모바일 External Key 제한 문자와 충돌하지 않으므로 그대로 사용할 수 있습니다.
- 구성원
userExternalKey: Kratos identity UUID, 즉users.id - 조직
orgUnitExternalKey: Barontenants.id - 조직 지정:
externalKey:{tenant.ID} - 구성원 지정:
externalKey:{user.ID}또는 email/resource id
이 선택은 "Kratos account가 사용자 SoT"라는 정책과 맞습니다. 사용자 생성 후 Worksmobile resource id가 생기더라도 Baron의 primary mapping은 Kratos UUID를 유지하고, Worksmobile resource id는 캐시/응답 추적용으로만 보관하는 것이 좋습니다.
조직
Baron tenant를 Worksmobile orgunit으로 보냅니다.
- 대상 tenant:
hanmac-familyroot 하위 subtree 중COMPANY,USER_GROUP - 제외 후보:
PERSONAL, system/global 성격 tenant orgUnitName:tenant.nameorgUnitExternalKey:tenant.idparentOrgUnitId: parent가 Worksmobile 동기화 대상이면externalKey:{parentTenant.ID}domainId: tenant domain 또는 root integration config에서 email domain별로 해석displayOrder: 동일 parent 내 deterministic order 필요. 1차 구현은name asc,created_at asc, 또는 별도config.worksmobile.displayOrder정책 중 하나를 선택해야 합니다.
주의할 점은 Worksmobile orgunits가 domainId를 필수로 요구한다는 점입니다. Baron은 한맥가족 root 아래에 여러 이메일 도메인과 법인/조직 subtree를 둘 수 있으므로, 우선 tenant.config.worksmobile.domainMappings를 지원하되 운영 domainId는 .env의 다음 값을 fallback으로 사용합니다.
SAMAN_DOMAIN_ID: 삼안 계열HANMAC_DOMAIN_ID: 한맥 계열GPDTDC_DOMAIN_ID: 총괄기획&기술개발센터BARONGROUP_DOMAIN_ID: 위 세 가지에 속하지 않는 모든 한맥가족사
분류 순서는 config mapping -> 삼안 -> 한맥 -> GPDTDC -> BARONGROUP fallback입니다.
개발/스테이징에서 테스트한 값은 프로덕션에도 동일하게 반영해야 합니다. 네이버웍스 쪽 backend가 동일 환경이므로 이 mapping은 env-only 값으로 숨기지 않고 seed/config migration 등 코드 레벨에서 추적 가능한 값으로 남깁니다.
{
"worksmobile": {
"enabled": true,
"tenantId": "hanmac-family",
"domainMappings": {
"hanmaceng.co.kr": 10000001,
"samaneng.com": 10000002
}
}
}
구성원
Baron Kratos identity를 Worksmobile user로 보냅니다.
userExternalKey: Kratos UUID (users.id)email: #668 정책으로 확정된 emailuserName.lastName:traits["name"]또는users.name전체를 우선 입력합니다. 성/이름 분리가 확정되기 전까지는 API 필수 조건 충족을 위해 전체 표시명을lastName에 둡니다.cellPhone: normalized phoneemployeeNumber: metadata의employee_id또는 schema login ID 값이 있으면 사용privateEmail: 기본 매핑하지 않습니다. NAVERWORKS sample의Personal email은 Baron metadata에는 보존하지만 Worksmobile payload에는 기본 전송하지 않습니다.aliasEmails: 한맥 tenant에 속하고employee_id가 있으면employee_id@hanmaceng.co.kr을 추가합니다.locale: 별도 지정이 없으면ko_KRpasswordConfig.passwordCreationType:ADMIN값을 구성원 생성 시에만 사용합니다.passwordConfig.password: 구성원 생성 시 숫자, 영문, 기호를 모두 포함한 16자리 난수 초기 비밀번호를 생성합니다.task: BaronjobTitle을 우선 사용organizations- 원직: Worksmobile 연동 제외 테넌트를 제외한 뒤
additionalAppointments에 가장 먼저 등록된 tenant - 겸직:
metadata.additionalAppointments또는 KetojoinedTenants중 Worksmobile 연동 제외가 아닌 tenant orgUnits[].orgUnitId:externalKey:{tenant.ID}levelId,positionId,userTypeId: 이번 scope에서는 External Key mapping을 사용하지 않고 사용자 정보 업데이트 필드로 최대한 커버isManager:additionalAppointments[].isOwner == true또는 Keto owners/admins relation을 기준으로 변환
- 원직: Worksmobile 연동 제외 테넌트를 제외한 뒤
구성원 소속 변경 정책
Worksmobile 연동 화면의 변경 범위는 다음과 같이 분리합니다.
- 조직/그룹 탭은 Worksmobile
orgunits/groups리소스 자체의 생성, 수정, 삭제, 이동을 관리합니다. - 구성원 탭은 Worksmobile
users리소스의 조직 소속(organizations[].orgUnits[]) 변경을 관리합니다. - 구성원 소속 변경 중 Worksmobile에 대상 org unit이 없거나 부모 구조가 최신이 아니면, 먼저 조직/그룹 탭의 조직 sync로 구조 변경을 반영한 뒤 구성원 sync를 수행합니다.
worksmobileExcluded=true인 테넌트와 그 하위 조직은 조직 sync 및 구성원 소속 sync의 대상에서 제외합니다.
개인 사용자 상세 변경은 대표 조직 1개만 수정하는 동작으로 제한하지 않습니다. Baron의 metadata.additionalAppointments에 들어 있는 모든 소속 조직을 기준으로 Worksmobile payload의 organizations[].orgUnits[]를 구성합니다. 다만 Worksmobile 연동 제외 테넌트, Worksmobile domain ID를 해석할 수 없는 테넌트, 조직 연동 설정이 켜져 있지 않은 겸직 도메인은 payload에서 제외하고 비교 결과에는 skipped reason 또는 warning을 남깁니다.
WORKS Developers 문서 확인 결과, 구성원 추가/수정 payload는 organizations[]와 하위 orgUnits[] 배열을 제공하고 orgUnits는 최대 30개까지 허용합니다. 또한 대표 도메인과 대표 조직은 각각 하나가 필요합니다. Directory API의 조직 연동 설명에는 구성원 회사 겸직을 설정하려면 겸직 도메인도 조직 연동 사용 설정을 켜야 한다는 제약이 있습니다.
따라서 구현 원칙은 다음과 같습니다.
- 동일 Worksmobile domain 안에서는 Worksmobile 연동 제외가 아닌 조직 소속을 모두 동기화합니다.
- 여러 Worksmobile domain을 넘는 겸직은 해당 domain의 조직 연동 설정이 켜져 있고 Baron에서 domain ID를 해석할 수 있을 때만 동기화합니다.
- Worksmobile primary는 Baron 대표 테넌트 플래그나
additionalAppointments[].isPrimary=true를 기준으로 삼지 않습니다. Worksmobile 연동 제외 테넌트를 제거한 뒤additionalAppointments에 가장 먼저 등록된 소속을 최초 반영 소속이자 primary로 둡니다. representative,isPrimary,primary같은 Baron 대표 소속 플래그는 Baron 내부 대표 소속 정책에만 사용하고 Worksmobile 동기화 판단 필드로 사용하지 않습니다.- 조직장 여부(
isManager)는 Worksmobile 동기화 대상입니다. 비교 로직은 remote와 Baron의 조직장 여부 차이를needs_update로 판단해야 합니다. - 비교 로직도 대표 조직 1개가 아니라
organizations[].orgUnits[]전체 set을 기준으로org_unit_added,org_unit_removed,org_unit_moved,org_unit_primary_changed같은 diff reason을 산출합니다. - Worksmobile 공식 문서 근거: https://developers.worksmobile.com/kr/docs/user-create, https://developers.worksmobile.com/kr/docs/directory
초기 비밀번호는 Worksmobile user upsert outbox payload에 loginEmail, initialPassword 형태로 함께 보관하고, adminfront의 한맥가족 Worksmobile 관리 화면에서 email,initialPassword,status,lastError CSV로 다운로드할 수 있게 합니다. 생성 성공/실패 판정은 outbox 작업 상태(processed, failed)와 함께 확인할 수 있으며, 운영상 평문 초기 비밀번호가 포함되므로 다운로드 권한은 hanmac-family tenant manage 권한으로 제한하고 보존 기간 정책을 별도 확정해야 합니다.
현재 backend CreateUser와 UpdateUser는 adminfront가 보내는 top-level additionalAppointments 및 metadata.additionalAppointments를 수용합니다. 한맥가족 단건 생성에서 대표 tenantSlug 없이 appointment만 오는 경우에는 first/primary appointment tenant를 대표 tenant로 해석해 Ory/Keto 관계, 허용된 Backend read model, Worksmobile enqueue가 누락되지 않게 합니다.
구성원 수정과 비밀번호 정책
Worksmobile 구성원 수정 API에는 PUT(user-update-put)과 PATCH(user-update-patch)가 있지만, 비밀번호 변경 경로로 사용하지 않습니다.
- Worksmobile Directory API에서 관리자 지정 초기 비밀번호 값은 별도 top-level
password가 아니라passwordConfig.password입니다. passwordConfig.passwordCreationType은 생성 방식이고,passwordConfig.password가 실제 초기 비밀번호 값입니다.- 공식 문서와 개발자 포럼 확인 결과,
passwordConfig.passwordCreationType과passwordConfig.password는 모두 구성원 등록 시에만 실제 반영됩니다. - PUT/PATCH request body에
passwordConfig.password를 포함해도 기존 구성원의 비밀번호 변경에는 반영되지 않습니다. - 따라서 Baron SSO는 WORKS Mobile 구성원 생성 시에만 초기 비밀번호를 설정하고, 생성 이후 Baron 사용자 비밀번호 변경을 WORKS Mobile PUT/PATCH로 전파하지 않습니다.
- 생성 이후 WORKS Mobile 비밀번호 변경은 WORKS Mobile 관리자 페이지 또는 WORKS Mobile이 제공하는 별도 운영 절차에서 직접 처리합니다.
- PATCH/PUT payload에는
passwordConfig를 포함하지 않습니다. privateEmail도 기존 정책대로 기본 전송하지 않습니다.- 기존 WORKS Mobile 구성원에 대한 일반 속성/조직/겸직 동기화는 생성 효율을 위해 먼저
POST /v1.0/users를 시도하고,409 Conflict일 때PATCH /v1.0/users/{email}로 전환합니다. - PUT은 전체 교체 성격이 강하고 누락 필드 초기화 위험이 있으므로 현 scope에서는 사용하지 않습니다. 모든 Baron -> WORKS 변경 반영은 부분 수정 PATCH를 우선합니다.
구성원 비밀번호 관리 링크
Baron SSO는 생성 이후 WORKS Mobile 비밀번호 값을 직접 수정하지 않습니다. 운영자가 비밀번호 수정을 요청할 때는 해당 WORKS 계정의 식별자를 이용해 WORKS Mobile 관리자 비밀번호 관리 화면을 새 창으로 엽니다.
사용 URL:
https://auth.worksmobile.com/integrate/password/manage?usage=admin&targetUserTenantId={회사테넌트}&targetUserDomainId={회사도메인}&targetUserIdNo={변경대상works_USER_ID}&accessUrl=https://admin.worksmobile.com/assets/self-close.html
전제와 기준:
- 브라우저 사용자는
auth.worksmobile.com에 관리자 권한으로 로그인되어 있어야 합니다. targetUserTenantId는 Baron tenant UUID가 아니라 WORKS Mobile 회사 tenant 식별자입니다. Baron SSO backend는WORKS_ADMIN_TENANT_ID환경 변수로 이 값을 adminfront overview에 노출합니다.targetUserDomainId는 WORKS Mobile 비교 결과의worksmobileDomainId를 사용합니다.targetUserIdNo는 WORKS Mobile 비교 결과의worksmobileId를 사용합니다.- adminfront는 세 값이 모두 있을 때만 비밀번호 관리 버튼을 활성화합니다.
- 이 링크는 WORKS Mobile 관리자 화면을 여는 기능이며, Baron SSO backend에서 password 또는
passwordConfig변경 API를 호출하지 않습니다.
비동기 아키텍처 권장안
Worksmobile API를 handler에서 직접 호출하지 않고, 별도 outbox와 relay worker를 둡니다.
권장 신규 테이블:
worksmobile_outboxidresource_type:ORGUNIT,USERresource_id: Baron tenant/user UUIDaction:UPSERT,DELETE,EXTERNAL_KEY_MAPpayload: JSONBdedupe_keystatus:pending,processing,processed,failedretry_count,last_errornext_attempt_at,processed_at,created_at,updated_at
worksmobile_resource_mappingsbaron_resource_typebaron_resource_idexternal_keyworksmobile_resource_iddomain_idlast_synced_at
권장 신규 service/client:
backend/internal/service/worksmobile_client.gobackend/internal/service/worksmobile_sync_service.gobackend/internal/service/worksmobile_relay_worker.gobackend/internal/repository/worksmobile_outbox_repository.go
현재 구현은 WORKS_ADMIN_* OAuth 또는 directory token을 사용하는 WorksmobileHTTPClient와 WorksmobileRelayWorker를 통해 worksmobile_outbox의 pending 사용자 작업을 Directory API로 전달합니다. 사용자 생성은 POST https://www.worksapis.com/v1.0/users를 먼저 호출하고, 이미 존재하는 구성원으로 409 Conflict가 발생하면 PATCH /v1.0/users/{email}로 전환합니다.
SCIM은 주경로로 사용하지 않습니다. 기존 검토에서 SCIM은 다음 이유로 보류했습니다.
- Directory API의
passwordConfig.passwordCreationType = ADMIN생성 정책을 그대로 표현하기 어렵습니다. - SCIM 경로는 검증 조건 때문에 private mail 성격의 email 값을 강제해야 하는 문제가 있었습니다.
- Baron 정책은
privateEmail기본 미전송이므로 SCIM을 주경로로 삼지 않습니다.
WORKS_ADMIN_OAUTH_CLIENT_ID, WORKS_ADMIN_OAUTH_CLIENT_SECRET, service account private key는 Directory API 호출용 토큰 발급에 사용합니다. OAuth redirect URI 등록이 필요한 경우 다음 경로를 사용합니다.
http://localhost:5000/api/v1/admin/worksmobile/oauth/callback
로컬 Playwright 검증에서는 위 callback 경로가 브라우저에서 도달 가능함을 확인했습니다.
Worker 정책:
- orgunit 작업은
domainId별 단일 worker lane, 최소 1초 간격 - user 작업은 같은
userExternalKey단위로 순차 처리 - 전체 동시성은 5 미만
- 409는 idempotent conflict로 보고 user PATCH 전환
- 404 parent orgunit은 parent job 선처리 후 retry
- 429/5xx는 exponential backoff
AdminFront 운영 화면 배치와 권한 정책
Worksmobile 운영 화면은 orgfront가 아니라 adminfront의 tenant detail 하위에 둡니다. 데이터 성격상 tenant 관리자 도구이며, 실제 사용자 동선은 hanmac-family tenant detail에서 바로 확인할 수 있게 만드는 쪽이 맞습니다.
화면 노출 정책:
- 전역 메뉴에는 Worksmobile 메뉴를 추가하지 않습니다.
adminfronttenant list에서 한맥가족 root tenant detail에 들어갔을 때만 Worksmobile 탭 또는 버튼을 표시합니다.- 노출 조건은 tenant detail API 응답의
slug == "hanmac-family"또는 동일한 canonical 식별자로 판단합니다. - 별도 운영 URL을 새 탭으로 여는 방식은 허용합니다. 단, 새 탭 화면도 tenant-scoped route여야 하며 URL을 직접 입력해도 같은 권한 검사를 통과해야 합니다.
orgfront는 필요 시 read-only sync badge나 adminfront deep link 정도만 담당하고, 생성/재시도/삭제 같은 운영 액션은 제공하지 않습니다.
권한 강제 정책:
- frontend의 탭 숨김은 UX 보조일 뿐이며 보안 경계로 보지 않습니다.
- backend endpoint는
/api/v1/admin/tenants/:tenantId/worksmobile/*처럼 tenant-scoped 형태로 둡니다. - handler 또는 middleware에서
tenantId가 존재하는지, 해당 tenant가 정확히hanmac-familyroot인지 먼저 확인합니다. - 요청자는
super_admin이거나Tenant:{tenantId}에 대한 관리 권한을 Keto로 통과해야 합니다. 기존RequireKetoPermission(..., "Tenant", "manage")또는 이에 준하는 relation을 사용합니다. - user/orgunit 개별 동작은 대상 resource가
hanmac-familysubtree 안에 있는지 추가로 확인합니다. - Worksmobile
domainId,userExternalKey,orgUnitExternalKey는 request body의 임의 입력을 신뢰하지 않고 server-side tenant config와 Baron UUID에서 계산합니다. - tenant가
hanmac-family가 아니거나, 사용자가 해당 tenant를 관리할 수 없거나, 대상 resource가 subtree 밖이면 작업을 생성하지 않고403 Forbidden또는 존재 노출을 줄이는404 Not Found로 차단합니다.
관리 화면에서 필요한 최소 기능:
- Worksmobile config/domain mapping 조회
- 조직/구성원별 최근 sync 상태와 마지막 오류 조회
- 단건 조직/구성원 sync enqueue
- 실패 작업 retry
- backfill dry-run 결과 조회 및 제한된 실행 버튼
삽입 지점
기존 계정 bulk/backfill
hanmac-familysubtree tenant 전체를 읽습니다.- Worksmobile에 이미 존재하는 조직/구성원의 External Key Mapping을 먼저 수집하거나 Developer Console CSV mapping으로 보정합니다.
- Baron tenant를 depth asc로 정렬해
ORGUNIT UPSERToutbox를 생성합니다. - Baron user를
users.id기준으로 읽고USER UPSERToutbox를 생성합니다. USER UPSERT는 해당 사용자의 orgunit mapping이 processed인 뒤 실행합니다.
신규 tenant 생성
- 후보 위치:
TenantHandler.CreateTenant에서replaceTenantDomains성공 후 - 더 좋은 위치:
TenantService.RegisterTenant가 tenant/domain/config/outbox를 하나의 transaction으로 저장하도록 정리한 뒤 같은 transaction 안에서worksmobile_outbox를 생성 - 조건: 생성된 tenant가
hanmac-familysubtree 하위이고 type이COMPANY또는USER_GROUP
tenant 수정
- 후보 위치:
TenantHandler.UpdateTenant에서h.DB.Save(&tenant)및 domain update 성공 후 - parent 변경 시 Worksmobile orgunit move 또는 patch가 필요합니다.
- 현재
UpdateTenant는 Keto parent outbox를 tenant 저장 전에 생성하므로, Worksmobile 이전에 outbox transaction 정합성 개선을 권장합니다.
신규 사용자 단건 생성
- 후보 위치:
UserHandler.CreateUser에서 Kratos 생성, 허용된 Backend read model sync, login ID sync, Keto outbox enqueue 후 - payload에는
identityID, email, name, phone, tenantID, metadata/additionalAppointments를 포함합니다. hanmac-familyscope가 아니면 enqueue하지 않습니다.
신규 사용자 bulk 생성
- 후보 위치:
UserHandler.BulkCreateUsers에서 row별 허용된 Backend read model sync와 Keto outbox enqueue 후 - row별 partial success를 유지하고, Worksmobile enqueue 실패는 사용자 생성 실패와 분리하는 것이 좋습니다.
- 단, enqueue 실패는 audit/error로 남기고 운영자가 재시도할 수 있어야 합니다.
사용자 수정/소속 변경
- 후보 위치:
UserHandler.UpdateUser에서 중앙IdentityWriteService기반 Kratos update와 허용된 Backend read model sync 후 email,name,phone,companyCode,tenant_id,metadata.additionalAppointments변경 시USER UPSERTenqueuesuspended는 Worksmobile suspend로 동기화합니다.temporary_leave는 Worksmobile 계정을 유지합니다.preboarding은 Worksmobile 계정을 생성하지 않습니다.baron_guest,extended_leave,archived는 Worksmobile delete/deprovision으로 동기화합니다.- Baron user delete는 Worksmobile delete로 동기화합니다.
- 기존
inactive입력은preboarding,leave_of_absence입력은temporary_leave,baron_only입력은baron_guest로 호환 처리합니다. - backend bootstrap은 위 legacy
users.status값이 남아 있으면 canonical 상태값으로 자동 정규화합니다.
테스트 전략
기능 추가이므로 테스트를 먼저 작성합니다.
Backend unit
- Worksmobile request mapper
- tenant UUID ->
orgUnitExternalKey - parent tenant ->
parentOrgUnitId = externalKey:{parentID} - Kratos UUID ->
userExternalKey additionalAppointments->organizations[].orgUnits[]- #668 email final value 사용
- tenant UUID ->
- External Key validator
%,\,#,/,?포함 시 reject
- Domain mapping resolver
- email domain ->
domainId - missing mapping -> blocking error 또는 skipped job
- email domain ->
Backend repository/service
worksmobile_outboxenqueue idempotency- orgunit depth order enqueue
- user job이 orgunit processed 전에는 보류되는지 확인
- retry/backoff/status transition
- 409 conflict 시 get/patch 전환
Handler
CreateTenant가 한맥가족 subtree tenant 생성 시ORGUNIT UPSERTjob을 생성CreateUser가 한맥가족 사용자 생성 시USER UPSERTjob을 생성- 한맥가족 외부 tenant는 job을 만들지 않음
BulkCreateUsers에서 row별 성공 결과와 Worksmobile enqueue 결과가 분리됨
Frontend unit
- CSV alias가 Worksmobile/Baron 컬럼을 정상 매핑하는지 확인
additionalAppointments를 유지한 채 사용자 생성/수정 payload가 만들어지는지 확인- tenant detail이
hanmac-familyroot일 때만 Worksmobile 탭/버튼을 표시하는지 확인 - non-hanmac tenant detail이나 직접 URL 접근에서 Worksmobile 운영 화면이 차단되는지 확인
E2E/manual
- adminfront에서
hanmac-family하위 조직 생성 -> outbox 생성 -> mock Worksmobile orgunit create 확인 - bulk import로 기존 계정 생성 -> #668 email preview -> Worksmobile user create mock 확인
- 신규 구성원 단건 생성 -> Worksmobile user create mock 확인
- orgfront 조직도 표시가 기존
joinedTenants/additionalAppointments기준과 충돌하지 않는지 확인 - adminfront tenant list -> 한맥가족 detail -> Worksmobile 운영 화면 새 탭 -> 단건 sync 결과 확인
- non-hanmac tenant detail에서는 Worksmobile UI가 보이지 않고 직접 URL 접근도 backend에서 차단되는지 확인
주요 리스크와 선행 결정
-
domainIdmapping 관리- Worksmobile API는
domainId가 필수입니다. - mapping 위치는
tenant.config.worksmobile.domainMappings로 결정했습니다. - 개발/스테이징에서 검증한 값이 프로덕션에도 적용되어야 하므로 코드 레벨 seed/config로 추적 가능해야 합니다.
- Worksmobile API는
-
additionalAppointmentsbackend 정규화- 현재 frontend는 값을 보내지만 backend 단건 생성 path는 구조적으로 처리하지 않습니다.
- Worksmobile 연동 전 Baron 내부 membership과 metadata 정합성을 먼저 맞춰야 합니다.
-
transaction 정합성
- 현재 tenant create/update와 Keto outbox는 완전한 transactional outbox가 아닙니다.
- Worksmobile outbox는 신규 구현 시 DB 변경과 같은 transaction으로 enqueue되도록 설계하는 것이 좋습니다.
-
Worksmobile orgunit rate limit
- 조직 API는 도메인당 단일 스레드/1초 1회 제약이 있으므로 worker lane 설계가 필수입니다.
-
기존 Worksmobile 데이터 backfill
- Developer Console External Key Mapping이 비어 있는 기존 조직/구성원은 CSV mapping 또는 API external-key update가 선행되어야 합니다.
-
상태 정책
- Baron
active,temporary_leave,suspended는 Worksmobile 구성원 비교 및 backfill scope에 포함합니다. - Baron
suspended는 Worksmobile suspend로 동기화합니다. - Baron
preboarding은 Worksmobile 계정을 생성하지 않습니다. - Baron
baron_guest,extended_leave,archived는 Worksmobile delete/deprovision으로 동기화합니다. - Baron delete는 Worksmobile delete로 동기화합니다.
- 직급/직책/사용자 유형 External Key sync는 이번 scope에서 제외합니다.
- Baron
-
adminfront 권한 경계
- Worksmobile 운영 화면은
hanmac-familyroot tenant detail에서만 보이게 합니다. - 별도 URL/새 탭은 허용하지만 backend는 tenant slug, Keto 관리 권한, subtree membership을 모두 확인해야 합니다.
- UI hidden state만으로 접근 제어를 대신하지 않습니다.
- Worksmobile 운영 화면은
권장 구현 순서
- Worksmobile integration config와
tenant.config.worksmobile.domainMappingsseed/config 정책 확정 additionalAppointmentsbackend DTO/parser/Keto membership sync 정리- Worksmobile mapper unit test 작성 후 RED 확인
worksmobile_outboxrepository/service/worker 구현- tenant orgunit enqueue 및 mock client GREEN
- user enqueue 및 mock client GREEN
- backfill command 또는 admin API dry-run 구현
- adminfront 상태/재시도 UI 또는 최소 운영 조회 API 추가
- E2E/mock integration 검증
문서 업데이트 후보
README.md: 관리 데이터 import 정책에 Worksmobile sync 상태와 External Key 원칙 추가docs/organization-chart-policy.md: Worksmobile orgunit mapping 정책 추가docs/tenant-usergroup-policy.md: 외부 Directory sync outbox 정책 추가docs/worksmobile-directory-sync-technical-review.md: 본 문서를 위키 반영 전 검토본으로 유지