# insuniverse-server 작업 규칙

## 관리자 API 추가 · 변경

새 `/admin/` 경로는 아래 다섯 가지를 같은 커밋에 넣는다.

1. **가드**: 컨트롤러에 `@UseGuards(AdminJwtAuthGuard, RolesGuard)` 와 `@Roles(...)` 를 건다.
   `@Roles` 를 빼면 관리자 역할 전원이 통과한다. 최고관리자 전용이면 `SuperAdminGuard` 를 셋째에 붙인다.
   순서는 인증, 역할, 최고관리자다.
2. **역할**: `@Roles` 에는 실제로 부를 역할만 적는다. 기업 최고관리자 · 조직관리자만 받으려면 신원 종류 낱말(`corp-super` · `org-manager`)을 쓴다 — 핸들러 본문에서 `corpScope` 로 거부하지 않는다. 영업·기업 역할을 넣으면 담당 범위 검사를 핸들러 안에 둔다.
   범위 상수가 꺼져 있다고 검사 호출을 빼지 않는다.
   분석 건을 읽는 경로는 예외다. 핸들러 안이 아니라 클래스 `AnalysisRowGuard`(판정 함수 `canViewAnalysis` 하나)가 본다.
   모든 가드를 통과한 관리자 읽기는 감사 줄 한 줄(`AuditService.recordAnalysisRead`)을 남긴다. 컨트롤러는 `@UseInterceptors(AnalysisReadInterceptor('analysis' | 'b2c-analysis' | 'client-analysis'))` 로, 보조 조회는 마지막 가드인 `AnalysisTargetGuard` 가 남긴다. 이 인터셉터를 걸지 않은 새 관리자 분석 읽기 경로는 기록되지 않는다.
3. **맵 행**: `apps/api/src/modules/auth/permission/route-permission-map.ts` 에 `['METHOD /admin/...', '코드']` 를 넣는다.
   이 파일 머리 주석의 「직접 고치지 않는다」는 낡았다. 생성기 입력이 동결돼 있어 행은 손으로 넣는다.
   코드는 `permission-catalog.ts` 에 있어야 한다.
4. **칸 표**: 새 권한 코드는 `apps/api/src/modules/menu/menu-permission-map.ts` 의 칸 표(`MENU_PERMISSION_TABLE`, 키는 tn_menu 메뉴 코드)에
   보기 · 변경 코드로 건다. 어느 칸에도 안 걸리는 코드는 `MENU_OUTSIDE_CODES` 에 사유와 함께 적는다 (사유 빈칸은 붉다).
   표시 이름은 두지 않는다 — 칸 이름은 실행 중 tn_menu 가 준다 (E5 · 14 결정 문서).
5. **예외**: 맵 행을 둘 수 없는 경로(공개 로그인, 본인 조회, 다른 역할 전용 등)는
   `apps/api/src/modules/auth/permission/route-permission-exceptions.ts` 에 `{ method, path, reason }` 로 적는다.
   관리자 쿠키의 신원(admId 있음)이 맵 밖 `/admin/` 요청을 보내면 이 원장에 없을 때 403 이다 (전역 가드, `ENFORCE_UNMAPPED_ADMIN_DENY`).
   영업 · 기업 신원과 판정 준비가 실패한 요청은 이 거부를 받지 않는다.

강제 스위치(`access-control.constants.ts` 의 `ENFORCE_*`)를 켜는 변경은 이 규칙이 아니라 운영 절차를 따른다.

## 관리자 화면 쪽 규칙

관리자 화면(`../insuniverse-admin`)은 이 맵을 그대로 읽는다. 새 `/admin/` 경로를 넣으면 화면 쪽도 같은 커밋 흐름에서 맞춘다.

- 새 관리자 라우트를 화면에 올리면 라우트 메타 `handle.api` 를 달거나 화면 쪽 베이스라인에 사유를 적는다 (`../insuniverse-admin/CLAUDE.md` 「권한 판정」 절).
- 화면의 쓰기 버튼은 공용 판정(`useWriteGate`)을 거친다. 권한 코드 목록을 화면에 복사하지 않는다 (판정 지도 `GET /admin/permission-map` 이 원천이다).

## CI 가 붉히는 것

`apps/api/src/modules/auth/access-contract/access-contract.spec.ts` 의 실제 AppModule 시험이 아래를 본다.
이 시험은 `.github/workflows/api.yml` 의 SPECS 목록에 있다.

- `/admin/` 런타임 경로가 맵에도 예외 원장에도 없음 (관리자 맵 누락)
- 맵 행이 런타임에 없고 예외 원장에도 없음 (맵 고아)
- 맵 코드가 권한 카탈로그에 없음 (맵 코드 카탈로그 밖)
- 인증 가드(`AdminJwtAuthGuard`)가 없는 `/admin/` 핸들러이고 예외 원장에도 없음
- `/admin/authority` 핸들러 · 관리자 계정 조회(`GET /admin/admin`, `GET /admin/admin/:admId`)에 `SuperAdminGuard` 가 없음
- 예외 원장 항목의 사유가 빈칸
- 레지스트리 `roles` 칸이 실제 `@Roles` 와 다름 (역할 칸 불일치)
- 인증 가드 핸들러에 `RolesGuard` + 비지 않은 `@Roles` 도 `SuperAdminGuard` 도 없음 (예외: `GET /admin/me` · `GET /admin/permission-map` · `GET /admin/menu/my`) · 공개 표지(`isPublic`)가 붙음
- `@Roles` 에 `corp` 또는 `org-manager` 가 있는데 `CorpReadonlyGuard` 가 없음
- 분석 수준 표지(`@CorpAnalysisView`)에 `CorpAnalysisViewGuard` 가 없음 · 표지가 있는 컨트롤러의 기업 핸들러가 수준을 선언하지 않음
- 인증 가드 핸들러에 비관리자 엔드포인트 표(`identity-endpoints.ts` 의 알려진 가드 표)가 모르는 가드가 붙음
- 영업 · 기업 최고 · 조직관리자가 부를 수 있는 엔드포인트 표가 골든(`identity-endpoints.golden.txt`)과 다름.
  역할 · 가드를 바꿔 표가 달라지면 `IDENTITY_GOLDEN_WRITE=1` 로 이 시험을 돌려 골든을 다시 쓰고 diff 를 커밋에 넣는다
  (관리자 화면은 이 표를 판정 지도 `GET /admin/permission-map` 로 받는다)
- 경로에 `:oddId` · `:asbId` · `:acbId` 가 있는 핸들러에 행 판정(`AnalysisRowGuard` · `OwnAnalysisGuard` · 내부 관리자 전용)이 없음 · 보조 조회 5개에 `AnalysisTargetGuard` 가 없음 · 분석 서비스 · 저장소를 받는 컨트롤러 목록이 고정 목록과 다름
- `/admin-view/analyze/*` 의 핸들러 · 가드 · 역할이 `/admin/analyze/*` 와 다름 · `/admin-view/*` 핸들러에 `AdminJwtAuthGuard` 나 맵 행이 없음
- `/admin-view-token` 경로가 다시 생김 · 비시험 소스가 옛 토큰 헤더(`x-admin-view-token`)를 읽음
- 카탈로그 코드가 칸 표 · 메뉴 밖 목록 어디에도 없음 · 칸 표 코드가 카탈로그 밖 · 메뉴 밖 사유 빈칸 · 메뉴 밖과 칸 표 겹침
  (`menu-permission-map.spec.ts` 가 음성을 고정한다. 접근 계약 시험도 같은 함수를 부른다)

## 시험이 보지 않는 것

- 역할 · 최고관리자 가드가 의도한 것인지 (리뷰가 본다)
- 맵 값이 맞는 권한 코드인지 (카탈로그에 있는지만 본다)
- 칸에 건 권한 묶음이 그 칸의 하위 기능과 맞는지 (리뷰가 본다)
- 분석을 `:oddId` · `:asbId` · `:acbId` 밖의 경로 칸이나 `AnalyzeService` · `AnalyzeRepository` 밖의 서비스 · 저장소(`DiseaseRepository` 등)로 읽는 핸들러 (리뷰가 본다)
