# WORKS only 사용자 복구 보고서

관련 이슈: #1037

## 요약

- 작업 시각: 2026-06-09 KST
- 대상: WORKS 비교 결과에서 `missing_in_baron`으로 보이던 사용자 row
- 최신 후보 수: 189명
- 조치: Baron `users.deleted_at` soft-delete 해제
- Kratos 조치: 직접 삽입/수정 없음. 189개 Kratos identity가 모두 현재 DB에 active 상태로 존재함을 확인함
- 최종 결과: WORKS 사용자 비교의 `missing_in_baron` 0건

## 원인

이전 사용자 projection 동기화 코드가 Kratos `ListIdentities()` 결과를 전체 identity 목록으로 간주했습니다. 해당 API 결과는 제한된 페이지 결과였고, 그 목록에 없던 기존 사용자가 Baron `users`에서 soft-delete 처리되었습니다.

이로 인해 WORKS에는 사용자가 남아 있고 `externalKey`도 Baron 사용자 UUID를 가리키지만, Baron 비교 로직에서는 soft-deleted 사용자가 visible 사용자로 조회되지 않아 `missing_in_baron`으로 표시되었습니다.

## 대조 결과

복구 전 후보 189건 기준:

| 구분 | 현재 Baron | 과거 Baron 백업 | 현재 Kratos | 과거 Kratos 백업 |
| --- | ---: | ---: | ---: | ---: |
| 후보 | 189 | 189 | 189 | 189 |
| visible 사용자 | 0 | 189 | - | - |
| soft-delete 사용자 | 189 | 0 | - | - |
| 누락 사용자 | 0 | 0 | 0 | 0 |
| active identity | - | - | 189 | 189 |
| credential 보유 identity | - | - | 189 | 189 |

확인한 백업:

- `backups/pre-saman-works-users-20260608-063605Z`
- `backups/pre-works-only-user-recovery-20260609-083105KST`

상세 리포트:

- `reports/works-only-user-recovery-20260609-0832/missing_in_baron_external_keys.csv`
- `reports/works-only-user-recovery-20260609-0832/current_baron_candidate_status.csv`
- `reports/works-only-user-recovery-20260609-0832/pre_saman_baron_candidate_status.csv`
- `reports/works-only-user-recovery-20260609-0832/current_kratos_candidate_status.csv`
- `reports/works-only-user-recovery-20260609-0832/pre_saman_kratos_candidate_status.csv`
- `reports/works-only-user-recovery-20260609-0832/post_recovery_baron_candidate_status.csv`

## 조치 내용

복구 전 DB 백업을 생성했습니다.

- `make dump DUMP_SERVICES=postgres,ory-postgres BACKUP=backups/pre-works-only-user-recovery-20260609-083105KST`

이후 후보 189건에 대해 현재 Baron DB에서 다음 조건으로만 soft-delete를 해제했습니다.

- WORKS `missing_in_baron` row의 `externalKey`가 UUID여야 함
- 해당 UUID가 현재 Baron `users.id`에 존재해야 함
- 해당 Baron row가 `deleted_at IS NOT NULL`이어야 함
- 해당 UUID가 현재 Kratos `identities.id`에 active 상태로 존재해야 함

복구 결과:

- 복구된 Baron 사용자: 189명
- 전체 Baron `users`: 2114명
- visible 사용자: 2106명
- soft-delete 사용자: 8명
- 후보 189명 중 Kratos active identity: 189명
- 후보 189명 중 credential 보유 identity: 189명

## 최종 검증

실제 Kratos admin 세션으로 로컬 게이트웨이를 경유해 검증했습니다.

- `GET /api/v1/admin/users?limit=5000&offset=0`
  - HTTP 200
  - `total=2106`
  - `items=2106`
- `GET /api/v1/admin/tenants/038326b6-954a-48a7-a85f-efd83f62b82a/worksmobile/comparison?includeMatched=true`
  - HTTP 200
  - 사용자 비교 총 2110건
  - `matched=2073`
  - `missing_in_baron=0`
  - `missing_in_worksmobile=30`
  - `needs_update=1`
  - `missing_external_key=6`
  - 조직/그룹 비교 총 187건
  - 조직/그룹 `missing_in_baron=1`

테스트:

- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/service -run 'TestWorksmobileSyncServiceSkipsSoftDeletedUsersInComparison' -count=1`
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler ./internal/repository -run 'Test.*User|Test.*Projection|Test.*SoftDeleted|Test.*ListUsers' -count=1`
- `BASE_URL=http://127.0.0.1:5173 npm --prefix adminfront test -- worksmobile.spec.ts --project=chromium`

결과:

- Go service 테스트 통과
- Go handler/repository 테스트 통과
- adminfront Worksmobile Playwright 4개 테스트 통과

## 재발 방지

이미 적용된 코드 변경으로 다음 조건을 방어합니다.

- admin 사용자 목록은 Kratos 250개 제한 결과가 아니라 로컬 projection repository를 기준으로 조회합니다.
- projection replace 동기화는 Kratos partial list에 없는 사용자를 삭제 처리하지 않습니다.
- WORKS 비교 로직은 soft-deleted Baron 사용자를 visible 사용자로 취급하지 않습니다.
- WORKS 비교 UI에는 필터링 후 표시 row와 전체 row 수를 함께 표시합니다.

남은 확인 항목:

- 조직/그룹 비교의 `missing_in_baron=1`은 사용자 복구와 별개 항목입니다. 별도 이슈로 원인 확인이 필요합니다.
- `missing_external_key=6` 사용자 row는 WORKS 측 externalKey가 없으므로 자동 복구 대상에서 제외했습니다.
