---
template: flow-doc@1
doc_id: 02-corp-console
title: 기업 콘솔 절차군
config: ../_meta/template-config.yaml
last_verified: fb001e1d
flows:
  - id: corp-console-session-entry
    actor: 기업담당자
    status: active-prod
    status_source: ./00-ia.md#corp-login
    evidence: [S-route, S-component, S-guard]
    limitations:
      - 진입 후 착지 화면이 기업역할에 따라 갈리는 것은 로그인 훅 코드로 확인했다. 실제 배분값(어느 계정이 어느 역할인지)은 DB 에만 있고 이 창에 없다
      - 절차의 단계 순서는 라우트 등록과 서버 guard 정독에서 역산했다. 런타임 통과 기록은 대조하지 않았다
    refs:
      - ./00-ia.md#corp-login
      - ./00-ia.md#corp-manage-login
      - ./00-ia.md#corp-corp-organization
      - ./00-ia.md#corp-user-corp-fc
      - ./00-ia.md#corp-403
    anchors:
      - insuniverse-admin/src/app/router/routes/page.routes.tsx:46
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-readonly.guard.ts:21
      - insuniverse-server/apps/api/src/modules/auth/admin/roles.guard.ts:27
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:42
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:33
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:26
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-analysis-view.guard.ts:40
  - id: corp-console-analysis-view
    actor: 기업담당자
    status: active-prod
    status_source: ./00-ia.md#corp-subscription-analysis
    evidence: [S-guard, S-schema, S-route]
    limitations:
      - tn_agency.agc_analysis_view 의 기업별 실제 배분값 미확보. 절차는 활성이나 실제로 통과하는 기업이 몇 곳인지는 알 수 없다
    refs:
      - ./00-ia.md#corp-subscription-analysis
      - ./00-ia.md#corp-client-analysis-detail-param
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-analysis-view.guard.ts:47
      - insuniverse-server/apps/api/src/modules/analyze/analyze-admin.controller.ts:45
      - insuniverse-server/apps/api/src/modules/auth/admin/corp-readonly.guard.ts:24
---

# 기업 콘솔 절차군

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

이 문서는 기업담당자(`admin.role === 'corp'`)가 기업 콘솔에서 수행하는 절차군을 담는다. 화면 구조의 정본은 [화면 인벤토리](./00-ia.md)다.

**기업 콘솔이 통합 콘솔과 다른 점은 셋이다.** 첫째, 라우터가 다르다 — 같은 저장소가 만드는 `corp` 라우터이며 통합 콘솔의 `admin` 라우터와 화면 집합이 겹치지 않는다. 둘째, **인가 층이 둘 더 걸린다** — `쓰기 스코프`(`corp-readonly.guard.ts`)와 `기업 노출 등급`(`corp-analysis-view.guard.ts`)은 `role === 'corp'` 에서만 발화하고 [통합 콘솔](./01-admin-console.md)의 절차에서는 전부 `해당 없음` 이다. 셋째, 기업역할이 하나 더 있다 — `corpScope` 가 `ORG` 인 조직관리자는 같은 `corp` role 안에서 쓰기가 기본 차단된다.

**근거의 강도도 다르다.** 통합 콘솔 절차의 서버 축 근거는 컨트롤러 데코레이터이고, 기업 콘솔 절차는 그 위에 guard 두 개의 본문 판정이 더 붙는다. 반대로 화면 축은 두 콘솔이 같다 — 둘 다 `S-route` 와 `S-component` 뿐이고 `S-test` 가 0건이다. 그 사실은 이 문서 H1 직하 고지가 진다.

## 상태 표기 규약

기업 도메인 문서의 필드 상태 표기(색상 규칙)는 `admin-corp.temp.md` §14 를 따른다. 원문은 `docs/_meta/findings/11-index-code.md` §부록 A 에 보존돼 있고 이 창에서 그 절의 재현 명령으로 대조했다 — `row_count` 15 · `set_sha256` `b56564a39cc6036789237e16daf8b62041a5f79dd201a57c3e296155a6159abc` 로 일치한다.

**이 문서는 그 15행을 복제하지 않는다.** 규약의 정본은 보존된 부록이고 여기는 소비처다. 아래 절차에서 상태값을 부를 때 그 규약이 정한 축(정산상태 · 주문상태 · 기업구독 상태 · 기업회원 회원구분 · 소속조직)을 그대로 쓴다.

**대조를 빠뜨리면 16행 / `91ac9212…` 가 나온다.** 그것은 문서 오류가 아니라 헤더 제외를 빠뜨린 필터 오류다 — 그 사실이 부록 자체에 적혀 있고 외부 리뷰어 두 명이 실제로 그 값을 냈다.

**인증 층에는 이 문서가 검증하지 않은 우회 분기가 둘 있다.** `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 |
|---|---|---|---|
| corp-console-session-entry | 기업 콘솔에 들어가 자기 기업역할의 착지 화면을 받는다 | 기업담당자 | active-prod |
| corp-console-analysis-view | 소속 기업의 분석 상세를 콘솔에서 연다 | 기업담당자 | active-prod |

## 흐름

### corp-console-session-entry

**전제**

- 기업담당자 계정을 보유하고 있다 (`admin.role === 'corp'`)
- 로그인 화면은 기업·조직 전용 `corp-login` 이다. `corp-manage-login` 은 같은 라우터에 등록된 통합 진입 경로이며 경로 분리는 `48e52b5d` 가 넣었다
- 자기 계정의 기업역할(최고관리자 / 조직관리자)이 정해져 있다. 그 배분값은 DB 에만 있다

**절차**

1. `corp-login` 에서 자격 증명을 제출한다. 성공하면 `_aaT` 쿠키를 받는다.
2. 기업역할에 따라 착지 화면이 갈린다 — 최고관리자는 `corp-corp-organization`, 조직관리자는 `corp-user-corp-fc` 다.
3. 사이드바는 기업 메뉴 코드 계산 결과로 그려진다. 조직 미배정 사유를 구분해 안내하는 처리는 `a3b3694d` 가 넣었다.
4. 권한이 맞지 않는 요청은 서버가 거부하고 클라이언트가 `corp-403` 으로 보낸다.

**조직관리자는 같은 role 안에서 쓰기가 기본 차단된다.** `corp-readonly.guard.ts:21` 이 `admin.corpScope !== 'ORG'` 면 통과시키고, `ORG` 면 24행에서 GET 을 허용한 뒤 `@AllowOrgWrite` 예외만 통과시킨다. 그 예외 목록이 조직관리자의 실질 쓰기 범위다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — `_aaT` 쿠키를 검증한다 | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — `corp` 가 허용 목록에 있어야 한다 | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:21,24,33` — `corpScope === 'ORG'` 이고 비-GET 이며 `@AllowOrgWrite` 가 없으면 거부 | HTTP 403 조직관리자는 허용된 작업만 수행할 수 있습니다 |
| 기업 노출 등급 | 차단 | 해당 없음 — 이 흐름은 로그인·착지 구간이라 `@CorpAnalysisView` 가 걸린 핸들러를 지나지 않는다 (`corp-analysis-view.guard.ts:40`) | 해당 없음 |
| 메뉴 노출 | 비차단 | 기업 메뉴 코드 계산은 클라이언트 상수 필터다. 서버는 관여하지 않는다 | 사이드바에서 사라짐. URL 을 직접 치면 위 네 층만 통과하면 열린다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 로그인 제출 | 자격 증명 불일치 | 막다름. 로그인 화면에 머문다 |
| 로그인 성공 | 소속 조직 미배정 | 계속 사유를 구분해 안내한다 (`a3b3694d`) |
| 착지 | 기업역할 미제공 | 계속 제한이 강한 쪽으로 폴백한다 |
| 쓰기 요청 | 조직관리자이고 `@AllowOrgWrite` 없음 | HTTP 403 (`corp-readonly.guard.ts:33`) |
| 권한 밖 화면 | 서버가 403 을 낸다 | `./00-ia.md#corp-403` 으로 이탈 |

### corp-console-analysis-view

**전제**

- 기업 콘솔에 로그인되어 있다 (`corp-console-session-entry` 완료)
- 소속 기업의 `tn_agency.agc_analysis_view` 가 요구 등급 이상이다
- 목록 화면은 `corp-subscription-analysis` 이고 상세는 `corp-client-analysis-detail-param` 이다

**절차**

1. `corp-subscription-analysis` 에서 분석 목록을 조회한다.
2. 행을 열면 `corp-client-analysis-detail-param` 으로 이동하며 `GET /admin/analyze/:asbId/detail` 을 호출한다 (`analyze-admin.controller.ts:90`).
3. 서버가 guard 넷을 순서대로 통과시키면 상세가 반환된다. 요구 등급은 컨트롤러 클래스에 `@CorpAnalysisView(2)` 로 선언돼 있다 (`analyze-admin.controller.ts:45`).

**등급 게이팅은 이 흐름의 유일한 실질 차단이다.** `54473012` 가 `agc_analysis_view` 컬럼과 게이팅을 넣었고, `corp-analysis-view.guard.ts:47` 이 조회 결과가 없거나 등급이 모자라면 거부한다. **조회 결과가 없을 때 통과가 아니라 거부라는 것이 fail-closed 이며** 그 판정이 같은 행에 있다.

**권한**

| 층 | 차단성 | 이 흐름에서의 강제 지점 | 실패 시 |
|---|---|---|---|
| 인증 | 차단 | `admin/jwt.guard.ts:26` — 컨트롤러 전체에 `AdminJwtAuthGuard` (`analyze-admin.controller.ts:43`) | HTTP 401 |
| 계정 종류 | 차단 | `roles.guard.ts:27` — `@Roles('admin','sales','corp')` (`analyze-admin.controller.ts:44`) | HTTP 403 접근 권한이 없습니다 |
| 쓰기 스코프 | 차단 | `corp-readonly.guard.ts:24` — 이 흐름은 전 구간 GET 이라 항상 통과한다. 같은 컨트롤러의 쓰기 경로에서만 걸린다 | 해당 없음 — 이 흐름에서는 발생하지 않는다 |
| 기업 노출 등급 | 차단 | `corp-analysis-view.guard.ts:47` — `agcAnalysisView` 가 요구 등급 미만이면 거부 | HTTP 403 분석관리 화면 접근 권한이 없습니다 |
| 메뉴 노출 | 비차단 | 분석 관리 메뉴 노출은 클라이언트 상수 필터가 정한다. 서버는 관여하지 않는다 | 사이드바에서 사라짐. URL 을 직접 치면 네 층만 통과하면 열린다 |

**실패·이탈**

| 지점 | 조건 | 결과 |
|---|---|---|
| 목록 조회 | 등급이 목록 하한 미만 | 계속 메뉴가 사이드바에서 빠져 절차 시작 지점이 없다 |
| 상세 요청 | 등급이 요구 등급 미만 | HTTP 403. 목록 화면에 머문다 |
| 상세 요청 | `tn_agency` 조회 결과 없음 | fail-closed — 403 을 낸다 (`corp-analysis-view.guard.ts:47`) |
| 상세 요청 | 소유자 검증 실패 | 막다름. 소유자 검증은 `47d90c30` 가 넣었다 |

## 연혁

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

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2026-07-01 | `f0ce46c8` | 조직트리와 조직관리자 권한위임 — 기업역할 축이 생겼다 |
| — | 2026-07-24 | `54473012` | 기업 노출 등급 층 신설. 분석 상세 열람 절차에 네 번째 층이 붙었다 |
| — | 2026-07-28 | `47d90c30` | 분석·청구서 조회에 소유자 검증 추가 — 상세 요청의 이탈 경로가 하나 늘었다 |
| — | 2026-07-30 | `48e52b5d` | 로그인 경로 분리 — 기업·조직 전용이 `/login` 으로 갈렸다 |
| — | 2026-07-31 | `a3b3694d` | 소속 조직 미배정 사유 구분 안내 — 착지 단계의 이탈이 막다름에서 계속으로 바뀌었다 |
