---
template: flow-doc@1
doc_id: 01-admin-console
title: 통합 관리자 콘솔 절차군
config: ../_meta/template-config.yaml
last_verified: fb001e1d
flows:
  - id: admin-console-session-entry
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-manage-login
    evidence: [S-route, S-component, S-guard]
    limitations:
      - 절차의 단계 순서는 라우트 등록과 컴포넌트 실존과 서버 guard 정독에서 역산했다. 런타임 통과 기록은 대조하지 않았다
      - 사이드바에 실제로 어떤 메뉴가 뜨는지는 tn_menu_auth_map 행에 달려 있고 그 스냅샷은 이 창에 없다
    refs:
      - ./00-ia.md#admin-manage-login
      - ./00-ia.md#admin-login
      - ./00-ia.md#admin-root
      - ./00-ia.md#admin-403
    anchors:
      - insuniverse-admin/src/app/router/routes/page.routes.tsx:46
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:26
      - insuniverse-server/apps/api/src/modules/menu/menu.repository.ts:32
      - insuniverse-server/apps/api/src/modules/auth/admin/roles.guard.ts:22
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-readonly.guard.ts:21
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-analysis-view.guard.ts:34
  - id: admin-console-member-lifecycle
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-user-user
    evidence: [S-route, S-component, S-roles]
    limitations:
      - 계정 정지가 진행 중 분석·구독·결제에 각각 무엇을 하는지는 이 창의 코드 정독으로 전건 확정하지 못했다. 확정한 것만 절차에 적고 나머지는 아래 실패·이탈 표의 미상 행으로 남긴다
      - 해제 시 원상 복구 범위는 관측하지 못했다. 그 사실 자체를 적는다
    refs:
      - ./00-ia.md#admin-user-user
      - ./00-ia.md#admin-subscription-subscription
    anchors:
      - insuniverse-admin/src/app/router/routes/domains/b2c.routes.tsx:24
      - insuniverse-server/apps/api/src/modules/user/user-admin.controller.ts:21
      - insuniverse-server/apps/api/src/modules/auth/admin/roles.guard.ts:27
  - id: admin-console-settlement-adjust
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-client-settlement-client-settlement
    evidence: [S-route, S-component, S-roles]
    limitations:
      - 정산 확정 취소가 남기는 변경 이력의 소재를 이 창에서 찾지 못했다. 검색어는 audit · admin_log · tnAdminLog 셋이며 그 밖은 보지 못한다
    refs:
      - ./00-ia.md#admin-client-settlement-client-settlement
      - ./00-ia.md#admin-corp-subscription-corp-settlement
    anchors:
      - insuniverse-server/apps/api/src/modules/order/order/order-admin.controller.ts:1
      - insuniverse-admin/src/app/router/routes/domains/b2c.routes.tsx:36
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-readonly.guard.ts:33
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-readonly.guard.ts:24
  - id: admin-console-payment-reconcile
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-money-payment
    evidence: [S-route, S-component, S-roles]
    limitations:
      - 환불 컨트롤러가 통째로 주석 처리돼 있다. 화면은 등록돼 있고 서버 경로는 열려 있지 않다는 비대칭을 절차에 적었다
    refs:
      - ./00-ia.md#admin-money-payment
      - ./00-ia.md#admin-money-refund
      - ./00-ia.md#admin-subscription-order
    anchors:
      - insuniverse-server/apps/api/src/modules/refund/refund-admin.controller.ts:3
      - insuniverse-server/apps/api/src/modules/money/money.repository.ts:1859
  - id: admin-console-bulk-operation
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-subscription-analysis
    evidence: [S-route, S-component, S-constant]
    limitations:
      - 부분 실패의 표시와 재실행 수단은 정산 축 일괄 작업에서 확인했다. 조회·내보내기 축은 쓰기가 아니라 부분 실패 개념이 서지 않으며, 그 축의 행 단위 실패 표시는 이 창에서도 확정하지 못했다
      - 건너뛴 사유 문자열의 전수와 그 분류의 정확성은 이 문서가 판정하지 않았다
    refs:
      - ./00-ia.md#admin-subscription-analysis
      - ./00-ia.md#admin-statistics-analysis
    anchors:
      - insuniverse-server/apps/api/src/modules/treat/monitor/treat-monitor.controller.ts:52
      - insuniverse-admin/src/app/router/routes/domains/b2c.routes.tsx:24
      - insuniverse-admin/src/features/settlement/lib/formatBulkResult.ts:21
      - insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.service.ts:64
  - id: admin-console-data-correction
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-search-codes
    evidence: [S-route, S-component, S-roles]
    limitations:
      - 기준 데이터 수정이 정산·통계에 각각 어떻게 반영되는지는 코드 경로를 따라가지 못했다. 반영 시점의 미상을 실패·이탈 표에 적었다
    refs:
      - ./00-ia.md#admin-search-codes
      - ./00-ia.md#admin-site-criteria
    anchors:
      - insuniverse-server/apps/api/src/modules/search/search-admin.controller.ts:1
      - insuniverse-admin/src/app/router/routes/domains/data.routes.tsx:1
  - id: admin-console-concurrent-write
    actor: 관리자
    status: active-prod
    status_source: ./00-ia.md#admin-money-general
    evidence: [S-route, S-schema, S-constant]
    limitations:
      - 잠금 부재는 대상 표 넷의 열 정의로 세웠다. 그 넷 밖의 표에서 무슨 일이 일어나는지는 이 문서가 판정하지 않았다
      - 데이터베이스 밖의 잠금(애플리케이션 뮤텍스·분산 락 저장소)은 이 축이 보지 못한다. 스키마는 그 존재를 부정하지 못한다
      - 충돌이 일어났을 때 사용자에게 무엇이 표시되는지는 이 문서가 판정하지 않았다
    refs:
      - ./00-ia.md#admin-money-general
      - ./00-ia.md#admin-money-corp-money
    anchors:
      - insuniverse-server/apps/api/src/modules/money/money.repository.ts:1859
      - insuniverse-server/apps/api/src/modules/money/money.repository.ts:2073
      - insuniverse-server/drizzle/schema.ts:504
      - insuniverse-server/drizzle/schema.ts:1629
---

# 통합 관리자 콘솔 절차군

> **admin 은 자동화된 테스트 근거가 없다.** `insuniverse-admin` 동결 HEAD `fb001e1d` 에 테스트 파일이 한 건도 없다. 이 디렉터리의 모든 문서는 라우트 등록과 컴포넌트 실존과 소스 읽기만을 근거로 하며 화면 축에 `S-test` 등급을 단 항목이 하나도 없다. 절차의 단계 순서가 런타임에서 실제로 그 순서로 도는지는 이 문서가 판정하지 않았다.

이 문서는 운영사 관리자(`admin.role === 'admin'`)가 통합 콘솔에서 수행하는 절차군을 담는다. 화면 구조의 정본은 [화면 인벤토리](./00-ia.md)이며 이 문서는 그 화면들을 지나는 **순서**와 각 지점에서 걸리는 **권한 층**을 쓴다.

**총론은 이 문서가 재기술하지 않는다.** 권한 규칙의 정본은 `docs/02-access/` 소유이고, 계약·한도는 `docs/03-contracts/` 와 `docs/04-limits/` 소유이며, 이 창에는 그 디렉터리가 한 파일도 없다. **부재 문서로는 링크를 걸지 않는다** — 경로를 산문으로 적고 실재하게 되는 창에서 링크로 바꾼다. 스케줄 정책은 이미 서 있으므로 [보존 주기](../05-schedules/01-retention.md)와 [정산 주기](../05-schedules/02-billing-cycles.md)를 그대로 참조한다.

**권한 층 5종의 판정은 흐름마다 다르다.** 다섯 층을 전부 적는 것은 규약이고, 걸리지 않는 층에 `해당 없음` 과 이유를 적는 것이 정보다 — 층을 빠뜨린 것과 그 층이 이 흐름에서 무력하다고 판정한 것은 다르다.

**인증 층에는 이 문서가 검증하지 않은 우회 분기가 둘 있다.** `admin/jwt.guard.ts:33` 은 핸들러 메타데이터 `isPublic` 이 참이면 토큰을 읽기 전에 통과시키고, `admin/jwt.guard.ts:42` 는 토큰 검증이 실패해도 `isDevSwaggerRequest` 가 참이면 `DEV_ADMIN`(`admId 19`)을 주입하고 통과시킨다. **아래 모든 권한 표의 인증 층 `차단` 판정과 모든 이탈 표의 `HTTP 401` 은 그 두 분기가 거짓일 때의 값이다.** 이 창의 관측으로는 `isPublic` 을 세팅하는 데코레이터가 저장소 전체에 0건이고 `isDevSwaggerRequest` 는 `NODE_ENV === 'dev'` 와 `referer` 와 `x-forwarded-for` 셋을 함께 요구한다 — 그래서 지금 도달 가능한 분기는 없다고 본다. **부재 증명이 아니다** — 검색어는 `isPublic` 하나이고 배포 환경변수는 대조하지 않았다. 코드 수정은 이 창 범위 밖이며 backlog `50m3-admin-jwt-guard-bypass-branches` 로 이월했다.

## 흐름 목록

| flow id | 목적 | actor | status |
|---|---|---|---|
| admin-console-session-entry | 통합 콘솔에 들어가 자기 권한 범위의 메뉴를 받는다 | 관리자 | active-prod |
| admin-console-member-lifecycle | 회원 계정을 정지하고 해제한다 | 관리자 | active-prod |
| admin-console-settlement-adjust | 확정된 정산을 수정하거나 취소한다 | 관리자 | active-prod |
| admin-console-payment-reconcile | 결제는 됐는데 후속 처리가 어긋난 건을 바로잡는다 | 관리자 | active-prod |
| admin-console-bulk-operation | 대량 조회·엑셀 내보내기를 돌리고 결과를 확인한다 | 관리자 | active-prod |
| admin-console-data-correction | 운영 중 발견한 기준 데이터 오류를 화면에서 고친다 | 관리자 | active-prod |
| admin-console-concurrent-write | 두 관리자가 같은 건을 동시에 저장한다 | 관리자 | active-prod |

## 흐름

### admin-console-session-entry

**전제**

- 운영사 관리자 계정을 보유하고 있고 그 계정에 권한 그룹(`tn_auth`)이 배정되어 있다
- 로그인 화면은 둘이다 — `admin-manage-login` 이 통합 진입이고 `admin-login` 은 기업·조직 전용이다. 경로 분리는 `48e52b5d` 가 넣었다
- 사이드바 구성의 입력은 `tn_menu_auth_map` 배분이며 그 스냅샷은 이 창에 없다

**절차**

1. `admin-manage-login` 에서 아이디·비밀번호를 제출한다. 성공하면 서버가 `_aaT` 쿠키를 내린다 (`admin/jwt.guard.ts:26` 이 이후 모든 요청에서 그 쿠키를 읽는다).
2. 클라이언트가 `GET /admin/menu/my` 로 자기 메뉴 트리를 받는다. 서버는 `tnMenuAuthMap` 을 `tnAdmin.athId` 로 조인해 배분된 메뉴와 그 부모를 낸다 (`menu.repository.ts:32-39`).
3. 배분된 메뉴가 0건이면 `menu.repository.ts:41-43` 이 빈 배열을 낸다. 사이드바가 비고 진입 지점이 사라진다.
4. `admin-root` 로 들어가면 각 도메인 화면으로 이동할 수 있다. 권한이 맞지 않는 요청은 서버가 거부하고 클라이언트가 `admin-403` 으로 보낸다.

**사이드바는 인가가 아니라 표시다.** 2단계가 만드는 것은 메뉴 목록이고 API 차단이 아니다 — 그 귀결은 `authority-api-exposure` 가 다룬다 ([권한 관리](./04-authority-management.md)).

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — `_aaT` 쿠키를 검증한다 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:22` — 이 흐름의 `GET /admin/menu/my` 에는 `@Roles` 선언이 없어 통과한다 | 해당 없음 — 선언이 없으면 통과가 정상 경로다 |
| 쓰기 스코프 | 차단 | 해당 없음 — `CorpReadonlyGuard` 는 `admin.corpScope === 'ORG'` 에서만 걸리고 운영사 관리자는 그 값이 아니다 (`corp-readonly.guard.ts:21`) | 해당 없음 |
| 기업 노출 등급 | 차단 | 해당 없음 — `CorpAnalysisViewGuard` 는 `admin.role === 'corp'` 에서만 걸린다 (`corp-analysis-view.guard.ts:34`) | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 배분된 메뉴만 사이드바에 실린다 | 사이드바에서 사라짐. URL 을 직접 치면 위 네 층만 통과하면 열린다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 로그인 제출 | 자격 증명 불일치 | 막다름. 로그인 화면에 머문다 |
| 이후 모든 요청 | `_aaT` 쿠키 부재·만료 | HTTP 401 (`admin/jwt.guard.ts:47`) — 위 우회 분기 둘이 거짓일 때 |
| 메뉴 조회 | 배분된 메뉴 0건 | 계속 빈 사이드바로 진입한다. 절차는 성립하나 이동 지점이 없다 |
| 도메인 화면 진입 | 계정 종류가 맞지 않는 API 응답 403 | `./00-ia.md#admin-403` 으로 이탈 |

### admin-console-member-lifecycle

**전제**

- 통합 콘솔에 로그인되어 있다 (`admin-console-session-entry` 완료)
- 대상 회원이 `admin-user-user` 목록에 있다
- 그 회원의 진행 중 구독은 `admin-subscription-subscription` 에서 따로 본다

**절차**

1. `admin-user-user` 에서 대상 회원을 찾는다. 목록 조회는 `GET /admin/user` 이고 `@Roles('admin','sales','corp')` 아래 있다 (`user-admin.controller.ts:21-23`).
2. 상세에서 계정 상태를 정지로 바꾼다.
3. 진행 중이던 구독의 상태는 `admin-subscription-subscription` 에서 별도로 확인한다. **정지가 구독을 자동으로 무엇으로 바꾸는지는 이 창에서 확정하지 못했다.**
4. 해제는 같은 화면에서 상태를 되돌리는 것이다.

**모르는 것을 모른다고 적는다.** 3단계의 연쇄(분석·구독·결제 각각의 귀결)와 4단계의 원상 복구 범위는 코드 정독으로 확정하지 못했다. `막다름` 을 모름의 대체로 쓰지 않으므로 아래 표에 미상 행으로 남긴다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — 컨트롤러 전체에 `AdminJwtAuthGuard` (`user-admin.controller.ts:21`) | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — `@Roles('admin','sales','corp')` (`user-admin.controller.ts:23`) | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | 해당 없음 — 이 컨트롤러에 `CorpReadonlyGuard` 가 걸려 있지 않다. 운영사 관리자는 걸렸어도 통과한다 | 해당 없음 |
| 기업 노출 등급 | 차단 | 해당 없음 — `CorpAnalysisViewGuard` 미적용 컨트롤러다 | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 회원 관리 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. `@Roles` 셋 중 하나면 API 는 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 목록 조회 | 세 role 밖 계정 | HTTP 403. 콘솔 진입 자체가 다른 라우터다 |
| 정지 저장 | 진행 중 분석·결제의 귀결 | 계속 이 문서가 판정하지 않았다. 코드 정독으로 확정하지 못한 자리다 |
| 해제 저장 | 정지 전 상태로 돌아오는가 | 계속 원상 복구 범위를 관측하지 못했다 |
| 상태 변경 | 변경 이력이 남는가 | 계속 감사 로그 어휘 3종으로 훑어 파일 0건이다. 부재 증명은 아니다 |

### admin-console-settlement-adjust

**전제**

- 통합 콘솔에 로그인되어 있다
- 대상 정산이 확정 상태이며 `admin-client-settlement-client-settlement` 또는 `admin-corp-subscription-corp-settlement` 목록에 있다
- 정산 주기의 정본은 [정산 주기](../05-schedules/02-billing-cycles.md)이며 이 흐름은 그 주기가 낸 결과를 사후에 손대는 절차다

**절차**

1. `admin-client-settlement-client-settlement` 에서 대상 정산 건을 연다. 기업 축은 `admin-corp-subscription-corp-settlement` 다.
2. 확정 취소를 수행한다. 정산 상태 개편과 확정 취소는 `89bc923a` 가 넣었다.
3. 값을 고쳐 다시 확정한다.

**누가 언제 무엇을 바꿨는지는 이 문서가 답하지 못한다.** `audit` · `admin_log` · `tnAdminLog` 세 어휘로 서버 `modules/` 전체를 훑어 파일 0건이다. 세 어휘 밖의 구현은 이 검색이 보지 못하므로 **부재 증명이 아니다.** 그 사실을 그대로 적는 것이 이 문단의 목적이다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — `_aaT` 쿠키 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — 정산 쓰기는 운영사 관리자 축이다 | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:24` — 비-GET 이므로 조직관리자였다면 여기서 잘린다. 운영사 관리자는 21행에서 먼저 통과한다 | HTTP 403 조직관리자는 허용된 작업만 수행할 수 있습니다 |
| 기업 노출 등급 | 차단 | 해당 없음 — `role` 이 `corp` 가 아니면 통과한다 (`corp-analysis-view.guard.ts:34`) | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 정산 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. API 는 계정 종류만 맞으면 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 확정 취소 | 조직관리자 계정 | HTTP 403 (`corp-readonly.guard.ts:33`) |
| 확정 취소 | 후속 정산 주기가 이미 지난 건 | 계속 이 문서가 판정하지 않았다 |
| 재확정 | 변경 이력 조회 | 계속 감사 로그 어휘 3종 검색에서 파일 0건. 이력의 소재가 이 문서에 없다 |

### admin-console-payment-reconcile

**전제**

- 통합 콘솔에 로그인되어 있다
- 결제는 승인됐고 그 뒤 주문·구독 반영이 어긋난 건이 있다
- 결제 축 화면은 `admin-money-payment` 이고 환불 축은 `admin-money-refund` 이며 주문 축은 `admin-subscription-order` 다

**절차**

1. `admin-money-payment` 에서 결제 승인 기록을 확인한다.
2. `admin-subscription-order` 에서 대응 주문의 상태를 확인한다.
3. 어긋난 건을 화면에서 바로잡는다.
4. 환불이 필요하면 `admin-money-refund` 로 이동한다.

**4단계에는 서버 경로가 없다.** `refund-admin.controller.ts` 는 파일 전체가 주석이다 — 2행의 `@Controller('admin/refund')` 부터 `@Post('delete')` 까지 전부 주석 처리돼 있다. 화면 라우트는 등록돼 있고 서버 엔드포인트는 열려 있지 않다. **이 비대칭이 개발자 개입이 필요한 지점이며 연락 경로는 이 문서가 알지 못한다** — 그 사실을 아래 표에 적는다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — `_aaT` 쿠키 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — 결제·주문 쓰기 경로는 운영사 관리자 축이다 | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:24` — 비-GET 이라 조직관리자면 잘린다 | HTTP 403 조직관리자는 허용된 작업만 수행할 수 있습니다 |
| 기업 노출 등급 | 차단 | 해당 없음 — `role` 이 `corp` 가 아니면 통과한다 | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 결제 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. API 는 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 주문 상태 확인 | 결제는 있고 주문이 없다 | 계속 화면에서 대응 건을 찾지 못한다 |
| 환불 실행 | 서버 엔드포인트가 주석 처리돼 있다 | 막다름. 화면에서 끝나지 않고 개발자 개입이 필요하다 (`refund-admin.controller.ts:2-40`) |
| 정정 저장 | 머니 잔액이 함께 움직이는 건 | `admin-console-concurrent-write` 의 잠금 관측이 그대로 적용된다 |

### admin-console-bulk-operation

**전제**

- 통합 콘솔에 로그인되어 있다
- 대량 조회 대상이 `admin-subscription-analysis` 목록 또는 `admin-statistics-analysis` 집계에 있다

**절차**

1. `admin-subscription-analysis` 에서 조건을 걸어 목록을 조회한다.
2. 엑셀 내보내기를 실행한다. 진료집계 축의 대응 엔드포인트는 `treat-monitor.controller.ts:52` 의 `GET admin/treat-monitor/daily-excel` 이고 `ef4908eb` 가 넣었다.
3. 집계 결과는 `admin-statistics-analysis` 에서 확인한다.

**상태를 바꾸는 일괄 작업은 정산 축에 있고 부분 실패를 건별로 표시한다.** 조회·내보내기 축(위 절차)과 달리 정산 일괄 처리는 쓰기이며, 서버가 처리분과 건너뛴 분을 나눠 응답하고 화면이 그것을 건별 사유로 편다. 진료 조회 축의 재처리는 별개 절차이며 [감시](./05-treat-monitoring.md)가 진다.

**부분 실패 표시 지점**

- 응답 형태가 처리 수와 건너뛴 목록으로 갈린다 — `insuniverse-admin/src/api/agencySettlementsApi.ts:10` 이 정산 단위 형태이고 `insuniverse-admin/src/api/agencySettlementsApi.ts:17` 이 주문 단위 형태다
- 건너뛴 분을 건별 사유로 펴는 자리 — `insuniverse-admin/src/features/settlement/lib/formatBulkResult.ts:21`. 건너뛴 것이 없으면 처리 수만 한 줄로 낸다 `insuniverse-admin/src/features/settlement/lib/formatBulkResult.ts:16`
- 그 결과가 실제로 사용자에게 뜨는 자리 — `insuniverse-admin/src/features/settlement/hooks/useSettlementConfirmModal.ts:105` 의 확정·해제 모달이다. 라벨은 `insuniverse-admin/src/features/settlement/hooks/useSettlementConfirmModal.ts:109` 가 붙인다
- 같은 표시가 걸린 다른 일괄 작업 셋 — 반영변경 `insuniverse-admin/src/features/settlement/hooks/useSettlementOrderBulkActionModal.ts:83` · 단가변경 `insuniverse-admin/src/features/settlement/hooks/useSettlementOrderBulkPriceModal.ts:84` · 입금처리 `insuniverse-admin/src/features/settlement/hooks/useSettlementPaymentModal.ts:93`
- 요청 자체가 실패한 경우는 건별 표시로 가지 않는다 — `insuniverse-admin/src/features/settlement/hooks/useSettlementConfirmModal.ts:96` 이 한 줄 경고로 접는다

**실패분 재실행 수단**

- 건너뛴 건은 상태가 바뀌지 않는다 — 서버가 처리 대상과 건너뛴 분을 트랜잭션 **앞에서** 가른다 `insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.service.ts:64`. 갱신은 처리 대상만 받는다 `insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.service.ts:353`
- 그래서 재실행 수단은 같은 화면의 재선택이다 — 처리 직후 목록을 다시 읽고 `insuniverse-admin/src/features/settlement/hooks/useSettlementConfirmModal.ts:112`, 건너뛴 건은 원래 상태로 그 목록에 남는다. 같은 일괄 작업을 그 건들만 골라 다시 건다
- **실패분만 자동으로 다시 거는 전용 조작은 없다.** 건너뛴 목록은 조작할 수 있는 항목이 아니라 평문 한 줄씩으로 조립돼 경고창에 실린다 — `insuniverse-admin/src/features/settlement/lib/formatBulkResult.ts:19` 가 그 조립이고 그 반환값이 가는 곳은 위 경고창뿐이다 `insuniverse-admin/src/features/settlement/hooks/useSettlementConfirmModal.ts:105`

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — 진료집계 컨트롤러 전체에 `AdminJwtAuthGuard` (`treat-monitor.controller.ts:9`) | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — `RolesGuard` 가 컨트롤러에 걸려 있다 (`treat-monitor.controller.ts:9`) | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | 해당 없음 — 이 흐름은 전 구간 GET 이고 `corp-readonly.guard.ts:24` 는 GET 을 항상 허용한다 | 해당 없음 |
| 기업 노출 등급 | 차단 | 해당 없음 — `role` 이 `corp` 가 아니면 통과한다 | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 통계 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. API 는 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 엑셀 내보내기 | 조회 범위가 커 타임아웃 | 계속 이 문서가 상한을 판정하지 않았다 |
| 엑셀 내보내기 | 일부 행만 실패 | 계속 이 축은 쓰기가 아니라 행 단위 실패 표시를 이 창에서도 확정하지 못했다 |
| 정산 일괄 처리 | 일부 건이 조건에 안 맞는다 | 계속 처리분과 건너뛴 분을 나눠 응답하고 건별 사유가 경고창에 뜬다 (`formatBulkResult.ts:21`) |
| 정산 일괄 처리 | 요청 자체가 실패한다 | 막다름. 건별 표시 없이 한 줄 경고로 접힌다 (`useSettlementConfirmModal.ts:96`) |
| 집계 확인 | 집계 시점과 목록 시점이 다르다 | 계속 두 화면의 기준 시점 정합은 이 문서가 판정하지 않았다 |

### admin-console-data-correction

**전제**

- 통합 콘솔에 로그인되어 있다
- 고칠 대상이 기준 데이터다 — 코드 마스터는 `admin-search-codes` 이고 노출 기준은 `admin-site-criteria` 다

**절차**

1. `admin-search-codes` 에서 코드 값을 찾는다.
2. 값을 고쳐 저장한다.
3. 노출 기준을 함께 바꿔야 하면 `admin-site-criteria` 에서 이어서 고친다.

**수정이 사용자 노출값과 정산·통계에 각각 언제 반영되는지는 이 문서가 답하지 못한다.** 코드 경로를 끝까지 따라가지 못했고, 그것을 추측으로 메우지 않는다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — `_aaT` 쿠키 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — 기준 데이터 쓰기는 운영사 관리자 축이다 | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:24` — 비-GET 이라 조직관리자면 잘린다 | HTTP 403 조직관리자는 허용된 작업만 수행할 수 있습니다 |
| 기업 노출 등급 | 차단 | 해당 없음 — `role` 이 `corp` 가 아니면 통과한다 | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 기준 데이터 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. API 는 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 코드 저장 | 이미 참조 중인 코드 값 | 계속 참조 무결성 처리를 이 문서가 판정하지 않았다 |
| 저장 후 | 사용자 노출값 반영 시점 | 계속 코드 경로를 따라가지 못했다 |
| 저장 후 | 정산·통계 반영 시점 | 계속 같은 이유로 미상이다 |

### admin-console-concurrent-write

**전제**

- 두 관리자가 같은 건을 동시에 연다
- 대상은 머니 축이다 — 회원 머니는 `admin-money-general` 이고 기업 머니는 `admin-money-corp-money` 다

**절차**

1. 관리자 A 가 `admin-money-general` 에서 대상 건을 연다.
2. 관리자 B 가 같은 건을 연다.
3. A 가 저장한다.
4. B 가 저장한다.

**머니 잠금은 두 자리에만 있다.** `SELECT … FOR UPDATE` 는 서버 저장소 전체에서 2건이고 둘 다 `money.repository.ts` 다 — 회원 머니가 1859행, 대리점 머니가 2073행이다. 그 두 경로에서는 4단계가 A 의 트랜잭션을 기다린다.

**잠금의 유무를 관측이 아니라 스키마로 세운다.** 비관적 잠금은 그것을 거는 질의가 있어야 하고, 낙관적 잠금은 대상 표에 버전 열이 있어야 한다. 앞은 호출 자리로, 뒤는 열 정의로 확인할 수 있다.

**잠금 보유 경로**

- 회원 머니 잔액 — 잠금 질의가 `insuniverse-server/apps/api/src/modules/money/money.repository.ts:1859` 이고 그 질의가 잠그는 표의 정의는 `insuniverse-server/drizzle/schema.ts:1480` 이다. 호출 자리는 `insuniverse-server/apps/api/src/modules/money/money.service.ts:339` 이며 트랜잭션 핸들을 함께 넘긴다 — 잠금은 그 트랜잭션 안에서만 유효하다
- 대리점·기업 머니 잔액 — 잠금 질의가 `insuniverse-server/apps/api/src/modules/money/money.repository.ts:2073` 이고 표 정의는 `insuniverse-server/drizzle/schema.ts:393` 이다. 호출 자리 하나가 `insuniverse-server/apps/api/src/modules/money/money.service.ts:511` 이다

**잠금 미보유 경로**

- 정산 확정·해제 일괄 — 대상 표의 정의는 `insuniverse-server/drizzle/schema.ts:504` 이고 그 열 목록에 버전 열이 없다. 처리 대상을 고르는 자리 `insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.service.ts:64` 도 갱신하는 자리 `insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.service.ts:353` 도 잠금 질의를 지나지 않는다. 두 관리자가 같은 정산 건을 동시에 확정하면 나중 것이 먼저 것을 덮는다
- 주문상세 재분석 초기화 — 대상 표의 정의는 `insuniverse-server/drizzle/schema.ts:1629` 이고 여기에도 버전 열이 없다. 상태를 되돌리는 자리 `insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.service.ts:275` 앞에 잠금이 없다
- 스키마 전체 — `insuniverse-server/drizzle/schema.ts:1` 부터 파일 끝까지 버전·잠금 성격의 열이 **한 건도 없다**. 아래 어휘로 훑은 결과가 그 근거다

**부재 판정 탐색 어휘**

- `FOR UPDATE` — 스키마 파일 0건. 비관적 잠금은 질의에만 있고 열 정의에는 나타나지 않으므로 이 결과는 기대한 값이다
- `forUpdate` — 스키마 파일 0건. 같은 이유다
- `version` — 스키마 파일 1건이며 그것은 자동차 카나리 동의문의 버전 문자열이다. 낙관적 잠금과 무관하다
- `lock` — 스키마 파일 3건이며 전부 기관 사용중지 표의 이름과 열에서 온 부분 일치다. 잠금 열이 아니다
- `revision` — 스키마 파일 0건
- `etag` — 스키마 파일 0건

**이 여섯 어휘 밖의 구현은 이 검색이 보지 못한다.** 그러나 앞 창과 달리 이 절의 부재 주장은 검색 결과가 아니라 **대상 표의 열 정의**에 걸려 있다 — 위 네 표의 열 목록에 버전 열이 없다는 것은 그 파일을 열면 그대로 보인다. 어휘 목록은 그 결론을 뒤집을 후보를 어디까지 찾았는지를 밝히는 데 쓴다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — 두 관리자 모두 `_aaT` 쿠키를 갖는다 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — 머니 쓰기는 운영사 관리자 축이다 | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:24` — 비-GET 이라 조직관리자면 잘린다 | HTTP 403 조직관리자는 허용된 작업만 수행할 수 있습니다 |
| 기업 노출 등급 | 차단 | 해당 없음 — `role` 이 `corp` 가 아니면 통과한다 | 해당 없음 |
| 메뉴 노출 | 비차단 | `menu.repository.ts:32-39` — 머니 메뉴 미배분이면 사이드바에서 사라진다 | 사이드바에서 사라짐. API 는 응답한다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| B 저장 | 대상이 회원 머니 잔액 | 계속 `money.repository.ts:1859` 의 행 잠금이 A 의 트랜잭션을 기다린다 |
| B 저장 | 대상이 대리점 머니 잔액 | 계속 `money.repository.ts:2073` 의 행 잠금이 같은 일을 한다 |
| B 저장 | 그 밖의 관리자 쓰기 경로 | 계속 잠금이 없어 나중 저장이 먼저 저장을 덮는다. 근거는 위 §잠금 미보유 경로의 표 정의다. 충돌 통지 여부는 이 문서가 판정하지 않았다 |

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋 SHA 로 추적한다. 절차의 단계·권한 층·이탈 경로가 바뀐 시점만 적는다. 화면 문구 변경은 연혁이 아니다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2026-07-16 | `89bc923a` | 기업주문 정산상태 개편과 정산확정 취소 — 정산 사후 정정 절차가 여기서 생겼다 |
| — | 2026-07-23 | `ef4908eb` | 진료집계 엑셀 다운로드 엔드포인트 — 대량 내보내기 절차의 서버 자리 |
| — | 2026-07-30 | `48e52b5d` | 로그인 경로 분리 — 통합 진입이 `/manage/login`, 기업·조직이 `/login` 으로 갈렸다 |
| — | 2026-07-31 | `d05f5d91` | 로그아웃 시 본인 포털 로그인 화면 복귀 — 세션 종료 이탈 대상이 바뀌었다 |
