# 관리자 RBAC · stg 동기화 기록 (2026-10-20)

운용 정책 「1-1」 의 세부 기록이다. 판정 근거의 정본은 이 파일이다.

## 1. 결과 요약

| 저장소 | 동기화 전 | 동기화 후 | 방식 |
|---|---|---|---|
| insuniverse-server (feat/admin-rbac) | `53a3e865` (M1~M6) | 병합 `ea3c0359` → 재생성 `11a87fdc` | `--no-ff origin/stg` 1회 + 레지스트리 재생성 1회 |
| insuniverse-site (feat/admin-rbac) | `b4200ff2` | `09181654` | fast-forward (전용 커밋 0) |
| insuniverse-admin (feat/admin-rbac) | `146ed4d6` | `146ed4d6` (변경 없음) | 이미 origin/stg 와 같다 (전용 커밋 0) |

- origin/stg 는 작업 중에 `530bf62a` → `34a0464d` 로 움직였다. 지시문의 기준값 `530bf62a` 뒤에 #127 (카드 삭제 가드) 과 #128 (본인인증 COOP) 두 병합이 더 들어왔다. 둘 다 이 동기화에 들어갔다. #128 은 `main.ts` 의 helmet 옵션 (`crossOriginOpenerPolicy: false`) 을 바꾼다.
- 부모 저장소 커밋: 서브모듈 포인터 · 재동결 산출물 · 이 기록 (커밋 목록은 세션 보고 참조).
- 푸시 · PR · stg 병합 · 운영 DB · stg DB 쓰기는 없었다. 이 서버 기준 Redis 는 `sync-iso-redis` (6380) · `sync-iso-redis-stg` (6381) 일회용 컨테이너였고 끝나면 내린다.

## 2. 충돌 해소

- 자동 병합 충돌은 `apps/api/src/main.ts` 한 곳이었다 (`git merge-tree` 로 사전 확인).
  - import: stg 는 `isSwaggerEnabled, setupSwagger` 를, 이 브랜치는 `setupSwagger` 와 `assertAccessContracts` 를 가져왔다. 둘 다 살렸다.
  - 본문은 자동 병합으로 양쪽이 모두 남았다: swagger 는 `isSwaggerEnabled()` 스위치 뒤에서만 열린다, 접근 계약 부팅 단언 (`assertAccessContracts`) 은 `listen` 앞에 있다.
- 자동 병합된 파일 중 의미가 겹치는 자리를 직접 읽었다.
  - `admin/admin.controller.ts` · `admin.service.ts` (#123 createAdmin 최고관리자 가드): stg 쪽 판정 (`readDesignation` 이 `designated` 이고 요청자가 최고관리자가 아니면 거부) 은 이 브랜치의 수정 · 삭제 판정 (`assertMayTouch`) 과 같은 조건이 아니다 (검토에서 정정). `assertMayTouch` 는 대상 목록에 지정 행이 있을 때만 막고, createAdmin 은 지정 환경이면 대상과 무관하게 막는다 (`admin.service.ts:43-46`, `:111-113`). 두 쪽이 같은 `SuperAdminService` 를 쓴다. 판정을 한 함수로 모으지 않았다. 모으면 한쪽 보호가 바뀌므로 그 정리는 의미를 먼저 정한 뒤 한다 (남은 위험 4).
  - `auth/user/auth.service.ts`: stg 쪽은 포맷과 비밀번호 재설정 티켓 파생키 변경이다. RBAC 와 겹치지 않는다.
  - `own-analysis.guard.ts` (#123 분석 가드 asbId/acbId): 가드 이름은 그대로라 범주 규칙을 건드리지 않는다.
  - 옵트인 CSRF 미들웨어 (`app.use`, 기본 OFF) 는 Nest 가드보다 앞선 Express 층이다. 전역 `AccessControlGuard` 와 순서 충돌이 없다.
  - `allow-ip-admin.controller.ts` (#123 가드 복구): 이 브랜치의 판정표 주석 (「인증 없는 관리자 쓰기 후보」) 이 틀렸다. 재동결에서 후보 목록과 주석 행을 지웠다 (아래 3).
  - 새 라우트 (`fcm-token` JwtAuthGuard · `mok-auth/decrypt` 빈도 가드 · `alimtalk` 빈도 가드 · `board` · `invoice`) 는 접근 계약 레지스트리에서 모두 선언됐다 (gate-compare 대조 통과).
- `package.json` 에 `helmet ^7.1.0` 이 추가됐다. 워킹트리 `node_modules` 에 없어 `npm ci` (lockfile 기준) 로 설치했다. 이 설치 없이는 `main.ts` 가 TS2307 로 깨진다. 신규 의존은 stg 가 들인 것이고 이 브랜치가 새로 들이지 않았다 (INV1 해당 없음).

## 3. 재동결 (M1 판정표 · M2 레지스트리)

- 새 동결본: `.claude/meta/2026-10-19-admin-rbac-m1/freeze-ea3c0359.list.txt` (704 행). 이전 동결본 `freeze-03ce7146` · `freeze-aff7ffcc` 는 이력으로 남긴다.
- 키 차이 (줄 번호 제외): 옛 동결본에서 빠진 행 10 · 새로 생긴 행 10. 대부분은 가드 변경 (stg #123 · #124 · #127 · #128).
- 판정표 · 주석:
  - `route-contract-table.md` 재생성 (704 행, 미분류 0).
  - `annotations.json`: 82 → 76 행. 키를 새 줄 번호로 옮겼다. 가드가 바뀐 행은 비고를 고쳤다 (알림톡 2 · 본인인증 복호화 1: 요청 빈도 가드). 가드가 붙은 행 (allow-ip 5 · FCM 토큰 1) 의 「가드 없음」 주석은 지웠다. 줄 번호가 이동해 실패하던 `auth/user/auth.controller.ts` 의 refresh 행은 옮겼다 (FCM 행을 지웠으니 정렬이 한 칸 밀린다).
  - `treat-provider-admin.controller.ts:121` 의 가드 보정 (`guardsAdd: SuperAdminGuard`) 은 `tree` 가 동결본 파일 이름에 있을 때만 적용된다. `tree` 를 `ea3c0359` 로 옮겼다. 적용하지 않으면 대조기 (c) 가 그 행에서 붉다.
- 생성기 보정 (M1 · M2 도구, 로컬 사본):
  - `build-table.mjs` · `route-runtime-dump.cjs`: `ADMIN_ROLES` 정본이 `auth/admin/jwt.guard.ts` 에서 `auth/principal/principal.ts` 로 옮겨졌다 (`3919bd24`, M-branch). 정본을 principal 로 먼저 보고 옛 위치를 폴백으로 둔다. 옛 동결본 (`03ce7146` 트리를 `git archive` 로 풀어 대조) 에서 통계 `noauth=80 admin51=38` 이 그대로 재현된다.
  - `build-table.mjs`: 「공개 후보」 목록에서 allow-ip 를 뺐다 (`UNGUARDED_CANDIDATES = {}`). 가드가 붙었다.
  - `unregistered.expected.txt` (신규): 「미등록 컨트롤러」 예외. `POST /admin/permission/role/:athId` (`permission-admin.controller.ts:30`, 최고관리자 전용 쓰기). M4 정책 §5-8 「등록하지 않는다」 를 사용자가 확정했다. 판정표에는 남긴다.
    - `route-compare.mjs` (M1 검사 #10): 예외 행은 표 쪽 키 · 가드 대조 · 중복 대조에서 뺀다. 런타임에 나타나면 실패한다. 표에 없는 예외 항목도 실패한다.
    - `build-registry.mjs` (M2): 예외 행은 레지스트리에 넣지 않는다. 넣으면 부팅 단언 fail 모드가 「런타임 부재」 위반을 낸다 (실측: `access-contract.spec.ts` 의 실제 AppModule 대조가 빨갛게 떨어졌다가 이 보정으로 녹색). 등록되면 미선언 핸들러로 잡힌다.
    - `gate-compare.mjs` (M2 검사 #2): 행 수 · 키 대조에서 예외를 뺀다. 런타임에 있으면 실패한다. 레지스트리에 들어와도 실패한다 (검토 정정).
- 레지스트리 재생성: `access-contract.registry.ts` 704 → 703 행 (예외 1행 제외) (`gate-compare`: OK rows=703 handlers=703 express(no *)=703). `--check-guards` rc 0 (미지 가드 없음).
- 범주 분포 (레지스트리 703행, 생성기 `--stats`): 무조건 공개 32 · 필수 신원 570 · 외부 신원 표지 37 · 자원 한정 토큰 35 · 서비스 검증 29. 개발 우회 있음 576 · 없음 127. 범주 규칙은 바꾸지 않았다.
  - 참고: 권한 가드 (`SuperAdminGuard` 등) 가 붙은 행도 `AdminJwtAuthGuard` 가 있으면 규칙 5 (필수 신원) 가 먼저 걸린다. 기존 규칙 순서 그대로다.
- 부팅 단언 (warn 모드): `gate-compare` 대조가 0 위반이므로 미선언 핸들러는 0이다 (실제 AppModule 덤프 703 핸들러).

## 4. 기준선 비교

### tsc

- `npx tsc --noEmit -p apps/api/tsconfig.app.json`: 오류 0.
- `npx tsc --noEmit -p tsconfig.json` (루트): 오류 23. 오류 집합 (파일 · 코드, 줄 번호 제외) 이 M4 기준선 `tsc-baseline.txt` 와 같다. 새 오류 0. 전부 spec 파일의 기존 타입 오류다.

### 호출 명세 (CI 검증 목록, `.github/workflows/api.yml` SPECS 33 파일)

- 전부 통과: 33/33 suite, 433/433 tests. 접근 계약 · 접근 감시 · 범위 강제 (scope-*) spec 포함.
- 첫 실행은 `access-contract.spec.ts` 의 실제 AppModule fail-모드 1건이 붉었다 (위 3 의 예외 보정 전). 보정 뒤 녹색.

### 서버 전체 jest (Redis 6380 일회용, `REDIS_URL` 지정)

| 구분 | 실패 suite | 비고 |
|---|---|---|
| 동기화 후 (병합 트리) | 6 suite · 3 test (`2562` 중 `2536` 통과 · 23 skip) | |
| origin/stg 단독 (`git archive`, Redis 6381) | 8 suite · 7 test | 6 suite 는 위와 같다. 나머지 2는 아래 |
| M4 기준선 `jest-baseline.txt` | 6 suite | 병합 트리 실패 집합과 같다 |

- 병합 트리의 실패 6 suite (모두 `analyze-insight-route` · `ddx/ddx-verdict.service` · `ddx/insight-payload` · `ddx/insight-report-v2` · `ddx/insight-view` · `libs/drizzle/drizzle.utill`) 는 M4 기준선과 이름까지 같다. 새 실패가 RBAC 탓인지 stg 탓인지 가른다 → 둘 다 아니다. 기준선부터 있던 실패다. 원인은 suite 마다 다르다. `libs/drizzle/drizzle.utill.spec.ts` 는 DB · Redis 와 무관한 단위 시험이다. 합성 mysql 오류 (`ER_LOCK_WAIT_TIMEOUT`) 가 500 응답 본문 `{"code":"ER_LOCK_WAIT_TIMEOUT"}` 에 실려 `:30` 이 붉다 (검토에서 이 파일만 단독 실행해 확인. 응답 원인은 코드 쪽 결함으로 보이며 이 동기화에서는 고치지 않았다). 나머지 suite 의 원인은 이 기록에서 가르지 않았다 (미검증).
- stg 단독에만 붉은 2 suite (`analyze/synth-view`, `ddx/insight-report`) 는 `git archive` 로 푼 트리에 없는 gitignored 픽스처 (`.claude/plans/*.json`) 를 cwd 기준으로 읽어서 ENOENT 가 난다. 환경 차이이고 코드 결함이 아니다 (병합 트리 실행에서는 통과).

## 5. 남은 위험 (사용자 판단 또는 후속)

1. 권한 쓰기 컨트롤러 (`POST /admin/permission/role/:athId`) 는 여전히 모듈 미등록이다. 정책 §5-8 대로 활성화 전에 R2 (선행 버전 조건 · CSRF) 를 닫아야 한다. 등록 시 `unregistered.expected.txt` 에서 행을 지우면 대조기가 등록 여부를 잡는다.
2. CSRF 는 옵트인 (`CSRF_ENFORCE=true`) 이고 기본 OFF 다. 관리자 쿠키 `_aaT` 의 SameSite=None 때문에 전역 CSRF 는 아직 보류다 (backlog `abp-m1p-impl-sec-admin-csrf-global`). R16 block 유지.
3. swagger 는 `SWAGGER_ENABLED=true` 일 때만 열린다. 운영·stg 의 실제 env 값은 확인하지 않았다 (미검증).
4. createAdmin 의 최고관리자 판정 (stg 쪽) 과 `assertMayTouch` (이 브랜치 쪽) 는 서로 다른 조건이다 (정정: 지정 행이 대상에 있을 때만 vs 지정 환경 전체). 한 함수로 모으면 한쪽 보호가 바뀌므로 의미를 먼저 정한다. 단일 공용 관리자 모델 재검토는 #75 범위다.
5. `helmet` 의 교차 출처 헤더 세 개를 껐다 (#122 · #128). 운영 쪽 iframe · PDF 흐름을 실측하지 않았다 (미검증).
6. 실제 stg 병합 sha (이 로컬 병합 `ea3c0359` 의 후손이 아닌 stg 병합 커밋) 대조와 그 뒤 재동결은 사용자 몫이다 (미검증).
7. admin 서브모듈에는 RBAC 전용 커밋이 아직 없다 (M7 이후 작업이 없다). 관리자 프런트 쪽은 이번 동기화에서 검증하지 않았다.
8. R19 · OQ4 (IP 5라우트) 의 정책 판정 「삭제 · 가드 복원 없음」 은 stg #123 이후 낡았다. 현행 코드는 관리자 인증과 `@Roles('admin')` 을 요구한다. M9 에서 다시 정한다 (정책 상단 착수 조건 상태 변화).

## 검토

stg 동기화 병합 뒤 리뷰 지적 판정. 근거는 각 행의 파일 · 줄 · 실측이다.

| 리뷰어 | 심각도 | 지적 | 판정 | 증거 |
|---|---|---|---|---|
| fable | HIGH | M4 카탈로그 · 경로 맵이 재동결 뒤 재생성되지 않아 `build-catalog.mjs --check` 가 rc 1 | 흡수 | 재생성 뒤 rc 0 (실측). 생성 규칙에 `permission-admin` 을 rbac.manage 묶음으로 넣었다 (정책 R3 「권한 표 편집」). 그래서 미등록 행 `POST /admin/permission/role/:athId` 는 새 코드 없이 `rbac.manage` 로 맵에 든다. 적재 코드에는 `allow-ip-admin.read` · `allow-ip-admin.write` 가 든다. 독립 계수 (M4 검사 #3 · M5 검사 #4 · M5 적재 행 계수) 에도 같은 스템을 넣었고 계수 13 = golden 13 이다. 수정 전 `--check` rc 1 · drift 4 파일, 수정 뒤 rc 0 |
| fable | MEDIUM | M5 `loaded-diff.txt` 기대 집합이 판정표와 어긋남 (allow-ip 2행 없음) | 흡수 | `loaded-diff.txt` 에 두 행을 더했다. 주석 제외 집합과 `loaded-codes.txt` 의 차이 0 (로컬 대조). 격리 적재 (`isolated-check.sh` 검사 #12) 는 Docker 를 돌리지 않아 (미검증) |
| codex | MEDIUM | `route-contract-table.md` 의 IP 5경로가 권한 맵에 없어 `codeForRoute` 가 null (unassigned) | 흡수 (위 HIGH 수정으로 함께 해소) | 맵에 `GET/POST /admin/ip*` 5행이 들어갔다 (`route-permission-map.ts`) |
| codex | MEDIUM | `gate-compare.mjs` 가 미등록 예외를 레지스트리에서도 빼서 통과시킴 | 흡수 | 반사실: 예외 행을 주입한 레지스트리 사본에서 수정 전 `OK rows=703` rc 0 재현 → 수정 뒤 `미등록 예외가 레지스트리에 있다` rc 1. 깨끗한 레지스트리는 수정 뒤에도 rc 0 |
| fable | MEDIUM | 정책 R19 · OQ4 「삭제 · 가드 복원 없음」 이 HEAD 코드와 모순 | 흡수 (정정) | `allow-ip-admin.controller.ts:26-27` 가드 확인. 정책 상단 착수 조건 상태 변화에 낡음 표지, R19 · OQ4 행에 표지를 달았다. 결정 자체 (M9 재결정) 는 미결로 둔다 |
| fable | LOW | createAdmin 과 `assertMayTouch` 가 「같은 의미」 라는 기록 | 흡수 (정정) | `admin.service.ts:43-46` (지정 행 포함 시만) 과 `:111-113` (지정 환경 전체) 은 다른 조건이다. 본문 §2 · 남은 위험 4 를 고쳤다 |
| codex | LOW | `drizzle.utill.spec` 실패를 DB · Redis 의존으로 적은 기록 | 흡수 (정정) | 단독 실행 (`libs/drizzle/src/drizzle.utill.spec.ts`): `Expected substring: not "ER_LOCK_WAIT_TIMEOUT"`, 받은 본문 `{"code":"ER_LOCK_WAIT_TIMEOUT"}`. DB · Redis 를 띄우지 않은 실행에서도 같은 결과다 |

미해소 HIGH · CRITICAL: 없음. 위 HIGH 는 흡수로 닫았다.

