---
template: policy-doc@1
doc_id: 04-state-transitions
title: 상태 전이 계약 — 분석·구독·결제 상태의 순서와 세 계층의 소비 방식
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: state:progress-and-outcome-separate
    status: active-prod
    evidence: [S-schema, A-comment]
    limitations:
      - 결과 칸이 실제로 얼마나 채워져 있는지는 운영 데이터에만 있다. 이 문서는 그 데이터를 확보하지 않았다
    anchors:
      - insuniverse-server/drizzle/schema.ts:1634
      - insuniverse-server/drizzle/schema.ts:1635
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:43
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:44
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:49
  - id: state:analysis-lifecycle
    status: active-prod
    evidence: [S-contract, S-schema]
    limitations:
      - 어느 낱말이 실제로 쓰이는지 이 문서는 세지 않았다. 저장된 행 기준의 분포는 부록 정책이 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:5
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:6
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:13
      - insuniverse-server/apps/api/src/modules/treat/treat.service.ts:576
      - insuniverse-server/drizzle/schema.ts:1634
  - id: state:elapsed-time-forces-outcome
    status: active-prod
    evidence: [A-scheduler, A-comment]
    limitations:
      - 경과 임계와 실행 주기의 값은 형제 문서의 소유라 여기서는 사실만 든다. 그 값이 바뀌어도 이 문서는 붉어지지 않는다
    anchors:
      - insuniverse-server/apps/api/src/schedules/analysis-status.schedule.ts:11
      - insuniverse-server/apps/api/src/schedules/analysis-status.schedule.ts:25
      - insuniverse-server/apps/api/src/schedules/analysis-stall.schedule.ts:11
      - insuniverse-server/apps/api/src/schedules/analysis-stall.schedule.ts:24
  - id: state:subscription-lifecycle
    status: active-prod
    evidence: [S-contract, S-schema]
    anchors:
      - insuniverse-server/apps/api/src/modules/subscribe/subscribe/subscribe.entity.ts:5
      - insuniverse-server/apps/api/src/modules/subscribe/subscribe/subscribe.entity.ts:9
      - insuniverse-server/apps/api/src/modules/subscribe/subscribe-failed/subscribe-failed.repository.ts:194
      - insuniverse-server/apps/api/src/modules/subscribe/subscribe-failed/subscribe-failed.repository.ts:210
      - insuniverse-server/drizzle/schema.ts:1914
  - id: state:payment-outcome-decided-outside
    status: active-prod
    evidence: [S-contract, S-schema, A-vendor]
    limitations:
      - 결제사가 실제로 어떤 값을 언제 돌려주는지는 벤더 축 문서의 소유다. 이 문서는 그 값을 우리가 어떻게 접는지만 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/payment/payment.entity.ts:6
      - insuniverse-server/apps/api/src/modules/payment/payment.service.ts:57
      - insuniverse-server/apps/api/src/modules/payment/payment.service.ts:151
      - insuniverse-server/drizzle/schema.ts:1659
  - id: state:analysis-type-is-not-a-state
    status: active-prod
    evidence: [S-contract, S-schema]
    limitations:
      - 소비 측에만 있는 갈래가 왜 그 자리에 있는지는 판정하지 않았다. 존재만 관측했다
    anchors:
      - insuniverse-server/apps/api/src/modules/analyze/analyze.dto.ts:35
      - insuniverse-server/apps/api/src/modules/analyze/analyze.dto.ts:51
      - insuniverse-server/apps/api/src/modules/analyze/analyze.dto.ts:63
      - insuniverse-server/drizzle/schema.ts:1634
      - insuniverse-site/front/src/features/analysisDetailView/constants/analysisDetail.const.ts:200
      - insuniverse-site/front/src/entities/analysis/model/types.ts:16
  - id: state:publisher-owns-the-meaning
    status: active-prod
    evidence: [S-contract, S-component, A-comment]
    limitations:
      - 어느 쪽이 고쳐져야 하는지 이 문서는 정하지 않는다. 관측만 남긴다
    anchors:
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:29
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.controller.ts:58
      - insuniverse-site/front/src/api/orderDetailApi.ts:54
      - insuniverse-site/front/src/api/orderDetailApi.ts:55
      - insuniverse-site/front/src/api/orderDetailApi.ts:85
      - insuniverse-admin/src/pages/Admin/B2c/Analysis/DetailAnalysisPage.tsx:229
      - insuniverse-admin/src/pages/Admin/B2c/Analysis/DetailAnalysisPage.tsx:232
      - insuniverse-admin/src/pages/Admin/B2c/Analysis/DetailAnalysisPage.tsx:545
  - id: state:external-projection-separate-vocabulary
    status: active-prod
    evidence: [S-contract, S-schema]
    limitations:
      - 대외 요청·응답 계약과 오류 규약은 형제 마일스톤의 소유다. 여기서는 어휘의 존재와 비대응까지만 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/order-detail/client-order-detail.entity.ts:5
      - insuniverse-server/apps/api/src/modules/client/order-detail/client-order-detail.entity.ts:10
      - insuniverse-server/drizzle/schema.ts:1110
  - id: state:stored-distribution-unknown
    status: data-dependent-unknown
    evidence: [S-schema, A-comment]
    unknown_reason: 어떤 상태가 실제로 얼마나 저장돼 있고 결과 칸이 채워진 행이 얼마나 되는지는 운영 데이터에만 있다. 결과 칸은 비울 수 있고 저장 계층은 낱말 목록을 강제하지 않으며 이 조사는 그 데이터를 확보하지 않았다
    anchors:
      - insuniverse-server/drizzle/schema.ts:1635
      - insuniverse-server/drizzle/schema.ts:1110
      - insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.entity.ts:43
---

# 상태 전이 계약 — 분석·구독·결제 상태의 순서와 세 계층의 소비 방식

이 문서는 **낱말이 어떤 순서로 바뀌는가**와 **그 낱말을 각 계층이 어떻게 소비하는가** 두 축만
다룬다. 낱말을 담는 칸이 어떤 제약을 지는지는 데이터 모델 문서가, 요청 값이 받아들여지는
조건은 입력 검증 문서가, 응답의 모양은 응답 계약 문서가 든다.

읽는 사람이 여기서 얻어야 하는 것은 **한 저장소만 봐서는 전이가 닫히지 않는다**는 사실이다.
낱말을 발행하는 쪽과 그것을 화면에 옮기는 쪽이 어긋난 자리가 실재하며, 이 문서는 그 자리를
판정 없이 사실로 적는다. 어느 쪽이 고쳐져야 하는지는 이 문서의 소관이 아니다.

여기 없는 것: 금액과 빈도의 한도, 경과 임계와 실행 주기의 값, 오류 낱말, 고객사 대외 계약의
요청·응답 규약, 낱말 갈래를 가르는 판정 규칙. 각각 다른 문서의 소유다.

## 정책 (기획)

### state:progress-and-outcome-separate

분석 한 건의 상태는 칸 하나에 담기지 않는다. **진행 칸과 결과 칸이 따로 있다.** 진행 칸은 늘
채워져 있고 결과 칸은 비어 있을 수 있다. 결과 칸이 채워지면 사용자와 관리자에게 보이는 것은
결과 칸이다.

두 칸은 같은 낱말 묶음을 쓴다. 이름이 비슷해 한 칸의 다른 표기로 오해하기 쉬우나 별개의
칸이다.

이 구별이 만드는 결과: **진행 칸만 보면 그 분석이 어떻게 끝났는지 알 수 없다.** 관리자가 강제로
종결하거나 시간이 지나 자동으로 종결될 때는 결과 칸만 채워지고 진행 칸은 원래 값 그대로 남는다.
관리자 화면에서 원본 진행 상태를 보존하기 위한 것이며 사용자에게 보이는 값과 관리자가 보는 값이
갈리는 지점이다.

### state:analysis-lifecycle

분석은 인증 요청으로 시작한다. 요청이 만들어지는 자리는 하나뿐이고 그 자리에서 진행 칸이 처음
채워진다. 이후 자료 수집과 자료 분석을 거쳐 끝난다.

끝나는 갈래는 넷이다 — 인증 실패, 자료 수집 실패, 분석 실패, 그리고 분석 완료. 추가 인증
요청은 끝이 아니라 중간 갈래이며 사용자에게 한 번 더 인증을 요구한다.

어느 낱말이 실제로 얼마나 쓰이는지는 이 문서가 세지 않는다. **낱말이 묶음에 있다는 것과 그
낱말을 가진 행이 있다는 것은 다른 주장**이며, 뒤엣것은 부록이 판정 불가로 든다.

### state:elapsed-time-forces-outcome

사용자가 아무것도 하지 않아도 상태가 바뀌는 경로가 있다. 진행 칸이 특정 값에 머문 채 시간이
지나고 결과 칸이 비어 있으면, 정기 작업이 **결과 칸에 실패를 박고** 그 분석에 잡혀 있던 예약
수를 돌려준다. 한 건에 두 번 일어나지 않도록 표시를 남긴다.

두 갈래가 있다. 인증 단계에서 멈춘 것과 수집·분석 단계에서 멈춘 것이며 박히는 실패 낱말이
서로 다르다.

얼마나 지나야 하는지, 그 작업이 얼마나 자주 도는지는 이 문서가 적지 않는다 — 시간 값과 주기는
다른 문서의 소유다.

이용자에게 만드는 결과: 화면에서 아무 조작도 하지 않았는데 분석이 실패로 바뀌어 있을 수 있고,
그때 예약 수는 이미 돌아와 있다. 진행 칸은 그대로이므로 관리자에게는 멈춘 자리가 그대로 보인다.

### state:subscription-lifecycle

구독 상태의 낱말은 넷이다 — 구독, 해지, 변경, 실패.

**이 넷은 종결 어휘가 아니다.** 실패는 되돌아온다. 결제가 실패해 실패로 표시된 구독은 그 실패
기록이 지워질 때 다시 구독으로 돌아간다. 두 변경은 한 묶음으로 일어나며 한쪽만 남는 상태가
없다.

해지는 결제 취소와 함께 일어난다. 결제를 취소하면 같은 자리에서 구독이 해지로 바뀌고 해지
시각이 함께 기록된다.

이용자에게 만드는 결과: 실패를 마지막 상태로 읽으면 틀린다. 실패로 보이는 구독이 다음에 다시
살아 있을 수 있다.

### state:payment-outcome-decided-outside

결제 결과는 우리 쪽이 정하지 않는다. **결제사가 돌려준 값이 확정을 뜻하는 낱말이면 완료이고 그
밖의 무엇이든 실패다.** 보류나 대기에 해당하는 낱말이 이 어휘에 없다.

그래서 결제사가 응답하지 않거나 응답이 늦어 끊긴 경우도 실패로 기록된다. 나중에 결제사 쪽에
승인이 잡혀 있더라도 우리 기록은 실패다. 그 차이를 메우는 경로는 이 문서가 확인하지 않았다.

취소는 완료·실패와 다른 경로로 들어온다. 결제사를 통해 실제로 취소하는 갈래와 대리점처럼
기록만 바꾸는 갈래가 있으며 둘 다 같은 취소 낱말을 남긴다.

이용자에게 만드는 결과: 결제 화면에서 보이는 실패가 반드시 카드 승인 거절을 뜻하지는 않는다.
통신이 끊긴 것도 같은 낱말로 보인다.

### state:analysis-type-is-not-a-state

분석 **종류**를 가리키는 낱말 묶음이 따로 있다. 이 묶음은 상태가 아니다. 순서가 없고, 전이가
없고, 상태를 담는 칸에 저장되지도 않는다. 조회할 때 어떤 분석 화면을 볼지 고르는 값이다.

헷갈리기 쉬운 이유는 낱말의 모양이 상태 낱말과 같기 때문이다. 모양만 보고 상태로 읽으면 있지도
않은 전이를 그리게 된다. 갈래를 무엇으로 가르는지는 다른 문서의 소유이며, 여기서 필요한 결론은
하나다 — **이 묶음을 상태로 읽지 않는다.**

사용자 화면 쪽에는 발행 쪽에 없는 종류가 더 있다. 화면이 한 갈래를 둘로 쪼개어 보여주는
자리와, 발행 쪽에서 아직 낱말로 서지 않은 갈래가 그것이다. 반대로 발행 쪽에만 있고 화면 쪽에
없는 낱말도 있다. 그 차이가 실재한다는 것만 여기 적고 어느 쪽이 옳은지는 정하지 않는다.

관리자 화면 쪽도 이 묶음을 만진다. 갈래마다 화면 조각이 따로 있고 조회 요청에 그 갈래를 실어
보낸다. 이 문서가 어긋남을 되짚은 곳은 사용자 화면 쪽뿐이며 관리자 화면 쪽의 갈래 목록이
발행 쪽과 어떻게 다른지는 되짚지 않았다.

### state:publisher-owns-the-meaning

낱말의 뜻은 **그 낱말을 발행하는 쪽이 정한다.** 소비하는 쪽이 붙인 이름은 계약이 아니다.

실측이 그것을 강하게 말한다. 사용자 화면 쪽 저장소에는 분석 상태 낱말마다 이름을 매긴 설명이
있는데, 설명이 다루는 낱말 전부에 대해 발행 쪽 이름과 하나도 겹치지 않는다. 같은 자리의 선언은
낱말이 아니라 이름의 목록이라서 실제로 오가는 값과 모양 자체가 다르다.

관리자 화면 쪽은 한 화면 안에서도 갈린다. 목록을 거르는 자리와 값을 이름으로 바꾸는 자리가 같은
구독 낱말에 서로 다른 이름을 준다. 거르는 자리가 발행 쪽과 같고 바꾸는 자리가 다르다.

게다가 사용자 화면 쪽은 그 이름 체계로 값을 **보내기까지 한다.** 보내는 낱말은 발행 쪽이 인증
실패로 문서화한 것인데 그 자리에 붙은 설명은 분석 실패라고 적는다. 무엇이 일어나는지는 발행
쪽이 정본이다.

이용자에게 만드는 결과: 화면에 뜨는 이름과 실제로 저장된 뜻이 다를 수 있다. 소비하는 저장소만
읽고 낱말의 뜻을 배우면 틀리게 배운다. 어느 쪽이 고쳐져야 하는지는 이 문서가 정하지 않는다.

### state:external-projection-separate-vocabulary

고객사에게 나가는 분석 상태는 **별도 어휘**다. 내부 어휘와 낱말 수가 다르고 일대일로
대응하지 않는다. 내부에만 있는 낱말이 있고 밖에만 있는 낱말도 있어 어느 쪽도 다른 쪽을 품지
않는다.

그 값을 담는 칸은 비울 수 있다. 내부 분석의 진행 칸이 늘 채워지는 것과 다르다.

요청과 응답의 계약, 오류 규약은 여기 없다 — 대외 계약 문서의 소유다. 이 문서는 어휘가 둘이고
서로 포개지지 않는다는 사실만 든다.

이 어휘는 고객사 쪽에만 머물지 않는다. 관리자 화면이 이 낱말을 이름에 매겨 보여주고 목록을
거르는 데 쓰며, 사용자 화면은 이 어휘를 담은 칸을 읽어 **내부 진행 칸의 이름을 가진 자리에
넣는다.** 두 어휘가 한 이름 아래 섞이는 자리가 실재한다는 뜻이다. 어느 쪽이 고쳐져야 하는지는
이 문서가 정하지 않는다.

이용자에게 만드는 결과: 고객사가 보는 상태와 내부에서 보는 상태를 같은 이름으로 대조하면
안 된다. 이름이 같아 보여도 같은 낱말이 아니다.

## 강제 지점 (개발)

### state:progress-and-outcome-separate

- 진행 칸: `schema.ts:1634` — 비울 수 없는 칸이다. 결과 칸: `schema.ts:1635` — 비울 수 있다. 같은 표의 이웃한 두 줄이며 **같은 어휘를 쓰는 별개 컬럼**이다.
- 노출 우선순위의 정본은 응답 표현의 인접 설명이다: `order-detail.entity.ts:43` 이 결과 칸이 관리자 강제 종결에서만 설정된다고 적고, `order-detail.entity.ts:44` 가 사용자 노출에서 결과 칸이 진행 칸보다 앞선다고 적는다. 선언은 `order-detail.entity.ts:49` 이며 선택 속성이다.
- 결속 집합 — state_columns: `odd_state` · `odd_final_state` · `sub_state` · `pmt_state` · `cod_state`
- 위 집합은 각 어휘를 담는 속성 선언을 엔티티에서 되짚어 얻는다. 게이트가 저장 정의의 컬럼 집합에 그 이름이 실재하는지 확인하며, 되짚히지 않는 속성이 하나라도 있으면 실패다. 코드 인덱스는 이 대조에 쓰지 않는다.

### state:analysis-lifecycle

- 낱말 묶음의 소재지: `order-detail.entity.ts:5` 가 선언을 열고 `order-detail.entity.ts:6` 이 첫 낱말, `order-detail.entity.ts:13` 이 마지막 낱말이다.
- 진입은 한 자리뿐이다: `treat.service.ts:576` — 인증 요청 낱말로 진행 칸을 처음 채운다.
- 저장되는 자리: `schema.ts:1634`.
- 결속 집합 — odd_values: `ODD001` · `ODD002` · `ODD003` · `ODD004` · `ODD005` · `ODD006` · `ODD007` · `ODD008`
- 위 집합은 발행 선언에서 다시 뽑은 것이며 문서 문면과 **양방향**으로 대조한다. 한쪽에만 있는 원소가 하나라도 있으면 실패다.

### state:elapsed-time-forces-outcome

- 인증 단계 정기 작업: 조건은 `analysis-status.schedule.ts:11` 이 적고 호출은 `analysis-status.schedule.ts:25` 다.
- 수집·분석 단계 정기 작업: 조건은 `analysis-stall.schedule.ts:11`, 호출은 `analysis-stall.schedule.ts:24`.
- 두 자리 모두 결과 칸만 쓰고 진행 칸은 건드리지 않는다. 그 규약이 인접 설명에 적혀 있다.
- 경과 임계와 실행 주기의 값은 여기 옮기지 않는다. 그 수치의 소유는 한도 축과 런타임 축 문서다.

### state:subscription-lifecycle

- 낱말 묶음: `subscribe.entity.ts:5` 가 선언을 열고 `subscribe.entity.ts:9` 가 실패 낱말이다.
- 실패를 박는 자리: `subscribe-failed.repository.ts:194`. 되돌리는 자리: `subscribe-failed.repository.ts:210`. 두 자리는 각각 하나의 트랜잭션 안에서 기록 삽입·삭제와 함께 일어난다.
- 저장되는 자리: `schema.ts:1914`.
- 결속 집합 — sub_values: `SUB001` · `SUB002` · `SUB003` · `SUB004`

### state:payment-outcome-decided-outside

- 낱말 묶음: `payment.entity.ts:6`.
- 결과를 가르는 자리: `payment.service.ts:57` — 결제사 응답이 확정 문자열과 같으면 완료, 아니면 실패다. 조건에 세 번째 갈래가 없다.
- 취소 경로: `payment.service.ts:151` — 완료·실패를 가르는 자리와 다른 곳이며 구독 해지를 함께 남긴다.
- 저장되는 자리: `schema.ts:1659`.
- 결속 집합 — pay_values: `PAY001` · `PAY002` · `PAY003`

### state:analysis-type-is-not-a-state

- 분석 종류 낱말의 소재지는 조회 요청 선언 셋이다: `analyze.dto.ts:35` · `analyze.dto.ts:51` · `analyze.dto.ts:63`. 세 선언은 서로 겹치는 원소를 갖는다.
- 상태를 담는 칸에는 이 낱말이 들어가지 않는다: `schema.ts:1634` 가 담는 것은 분석 상태 어휘다. 저장 정의 전체에서 이 종류 낱말을 담는 칸이 없다.
- 소비 측 선언: `analysisDetail.const.ts:200` 이 화면이 아는 종류의 목록이고 `types.ts:16` 이 그중 한 갈래의 하위 탭 낱말을 따로 선언한다.
- 결속 집합 — ans_values: `ANS001` · `ANS002` · `ANS003` · `ANS004` · `ANS005` · `ANS006` · `ANS007` · `ANS008` · `ANS009` · `ANS010` · `ANS011` · `ANS012` · `ANS013`
- 불일치 재유도 — site-analysis-type-extra: `ANS003A` · `ANS003B` · `ANS004_2` · `ANS004_5` · `ANS014` · `ANSCAR`
- 위 목록은 소비 측 선언에서 발행 측 셋의 합집합을 뺀 결과를 게이트가 다시 만든 것이다. 소비 측에만 있는 원소가 없어지면 그 주장이 사실이 아니게 되므로 게이트가 뒤집힌다.

### state:publisher-owns-the-meaning

- 발행 측 이름의 정본: `order-detail.entity.ts:29` — 응답 표현의 인접 설명이 낱말마다 이름을 적는다. 낱말 자체의 선언은 같은 파일의 묶음이다.
- 소비 측 사전: `orderDetailApi.ts:54` — 같은 자리의 인접 설명이 낱말마다 다른 이름을 적는다.
- 소비 측 선언의 모양: `orderDetailApi.ts:55` — 낱말이 아니라 이름의 목록이다.
- 소비 측이 값을 보내는 자리: `orderDetailApi.ts:85`. 서버 쪽 문서화는 `order-detail.controller.ts:58` 이며 그 낱말을 인증 실패로 적는다. 보내는 자리에 붙은 설명은 분석 실패라고 적는다 — 이름과 동작이 갈리며 동작은 서버 쪽이 정본이다.
- 관리자 화면의 두 자리: 값을 이름으로 바꾸는 자리가 `DetailAnalysisPage.tsx:229` 에서 열려 `DetailAnalysisPage.tsx:232` 부터 낱말을 이름에 매기고, 목록을 거르는 자리는 `DetailAnalysisPage.tsx:545` 부터다.
- 불일치 재유도 — site-order-detail-dictionary: `ODD001` · `ODD002` · `ODD003` · `ODD004` · `ODD005` · `ODD006`
- 불일치 재유도 — site-order-detail-type-shape: `발행:code` · `소비:label`
- 불일치 재유도 — admin-subscription-label: `SUB003` · `SUB004`
- 불일치 재유도 — admin-subscription-self: `SUB003` · `SUB004`
- 위 넷은 게이트가 **양측을 각각 동결 기준점의 소스에서 읽어** 차집합을 다시 만든 결과다. 문서가 든 목록이 그 차집합보다 적어도, 많아도 실패다. 양측이 같아져 차집합이 비면 그 주장이 더 이상 사실이 아니므로 역시 실패다 — 자기 단언이 아니라 외부 기준 대조다.

### state:external-projection-separate-vocabulary

- 낱말 묶음: `client-order-detail.entity.ts:5` 가 선언을 열고 `client-order-detail.entity.ts:10` 이 마지막 낱말이다.
- 저장되는 자리: `schema.ts:1110` — 비울 수 있는 칸이다.
- 결속 집합 — cod_values: `COD001` · `COD002` · `COD003` · `COD004` · `COD005`
- 이 어휘의 접두는 코드 인덱스의 상태 갈래 목록에 없다. 인덱스를 분모로 삼으면 이 축이 통째로 사라지므로 게이트는 발행 선언에서만 집합을 뽑는다.
- 대외 요청·응답 계약과 오류 규약의 앵커는 여기 두지 않는다. 그 축은 대외 계약 문서의 소유다.

### state:stored-distribution-unknown

- 결과 칸이 비울 수 있음: `schema.ts:1635`. 대외 상태 칸이 비울 수 있음: `schema.ts:1110`.
- 결과 칸이 언제 채워지는지의 서술: `order-detail.entity.ts:43`.
- 저장된 값을 세는 경로가 이 조사에 없다. 게이트는 이 정책이 부록에 있고 판정 불가 사유를 갖는지만 확인하며, 분포 자체를 확인하지 않는다.

## 적용 범위 (actor × 서비스)

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| state:progress-and-outcome-separate | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:analysis-lifecycle | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:elapsed-time-forces-outcome | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:subscription-lifecycle | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:payment-outcome-decided-outside | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:analysis-type-is-not-a-state | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:publisher-owns-the-meaning | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:external-projection-separate-vocabulary | 적용 | 미적용 | 적용 | 미적용 | 적용 | 강제 지점 | 소비 | 소비 |
| state:stored-distribution-unknown | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 소비 | 소비 |

actor 축이 대체로 전건 `적용` 인 이유는 상태 전이가 요청을 보낸 주체를 구별하지 않기 때문이다.
어느 낱말이 어느 actor 의 조작으로 생기는지는 이 문서가 판정하지 않으며 도메인 문서의 소유다.

대외 어휘 행만 다르다. 그 낱말은 고객사 축의 조회에서만 나가므로 개인 자격으로 들어온 주체는
이 어휘를 만나지 않는다.

서비스 열의 `소비` 는 그 저장소에 이 어휘를 읽거나 쓰는 자리가 동결 기준점에 실재한다는
뜻이고 `—` 는 그런 자리를 찾지 못했다는 뜻이다. **실측에서 `—` 는 한 칸도 나오지 않았다** —
세 저장소가 다섯 어휘를 전부 만진다. 대외 어휘조차 사용자 화면 쪽이 담는 칸의 이름으로 읽어
내부 진행 칸의 이름에 넣는 자리가 있다. 이 열이 상수라는 사실 자체가 관측이며, 계층별로
무엇이 다른지는 이 표가 아니라 앞의 강제 지점 절이 든다.

이 열은 기계가 대조한다. 정책마다 어휘 축을 잡고 그 축의 값과 그것을 담는 속성 이름을 세
저장소의 동결 트리에서 찾아 `소비` 와 `—` 를 다시 만든다. 날조된 `소비` 도 날조된 `—` 도 같이
떨어진다. actor 다섯 열은 그 대조 밖이다 — 요청 주체의 권한 모델을 동결 트리에서 되짚는 경로가
이 조사에 없다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| state:progress-and-outcome-separate | 결과 칸이 비어 있음 | 진행 칸이 그대로 보인다. 아직 종결되지 않았다는 뜻이며 종결이 실패했다는 뜻이 아니다 | `schema.ts:1635` |
| state:progress-and-outcome-separate | 결과 칸과 진행 칸이 다른 낱말을 가짐 | 사용자와 관리자에게 결과 칸이 앞선다 | `order-detail.entity.ts:44` |
| state:analysis-lifecycle | 인증이 한 번 더 필요한 경우 | 중간 낱말로 남고 사용자 조작을 기다린다. 종결이 아니다 | `order-detail.entity.ts:5` |
| state:analysis-lifecycle | 낱말 묶음 밖의 값이 저장된 경우 | 저장 계층이 막지 않는다. 실제로 그런 행이 있는지는 부록 정책이 판정 불가로 든다 | `schema.ts:1634` |
| state:elapsed-time-forces-outcome | 결과 칸이 이미 채워져 있음 | 정기 작업이 건너뛴다. 표시가 남아 있어 두 번 처리되지 않는다 | `analysis-stall.schedule.ts:11` |
| state:elapsed-time-forces-outcome | 정기 작업이 돌지 않는 환경 | 진행 칸에 머문 채 종결되지 않는다. 실행 여부는 배포 환경이 정하며 이 문서가 판정하지 않는다 | `analysis-status.schedule.ts:25` |
| state:subscription-lifecycle | 실패로 표시된 구독의 실패 기록이 지워짐 | 같은 행이 구독으로 돌아간다. 실패는 종결이 아니다 | `subscribe-failed.repository.ts:210` |
| state:subscription-lifecycle | 결제 취소로 해지가 일어남 | 구독 낱말과 해지 시각이 같은 갱신에서 함께 기록된다 | `payment.service.ts:151` |
| state:payment-outcome-decided-outside | 결제사가 확정 낱말이 아닌 값을 돌려줌 | 실패로 기록된다. 보류에 해당하는 낱말이 없다 | `payment.service.ts:57` |
| state:payment-outcome-decided-outside | 결제사 응답이 없거나 끊김 | 위와 같다 — 실패다. 나중에 결제사 쪽 승인이 잡혀 있어도 우리 기록은 실패이며 그 차이를 메우는 경로는 확인하지 않았다 | `payment.entity.ts:6` |
| state:analysis-type-is-not-a-state | 종류 낱말을 상태로 읽는 경우 | 존재하지 않는 전이를 그리게 된다. 이 묶음은 상태 칸에 저장되지 않는다 | `analyze.dto.ts:35` |
| state:analysis-type-is-not-a-state | 소비 측에만 있는 종류가 요청으로 들어오는 경우 | 발행 측 선언에 없으므로 조회 요청 검증에서 걸린다. 화면이 그 값을 어떻게 쓰는지는 화면 쪽에만 있다 | `analysisDetail.const.ts:200` |
| state:publisher-owns-the-meaning | 소비 측 이름이 발행 측과 다름 | 발행 측이 정본이다. 소비 측 이름은 계약이 아니다 | `orderDetailApi.ts:54` |
| state:publisher-owns-the-meaning | 한 화면 안에서 이름이 갈림 | 이 문서는 판정하지 않는다. 관측만 남긴다 | `DetailAnalysisPage.tsx:232` · `DetailAnalysisPage.tsx:545` |
| state:external-projection-separate-vocabulary | 대외 상태 칸이 비어 있음 | 아직 값이 없다는 뜻이며 내부 진행 칸과 달리 비어 있음이 정상 상태다 | `schema.ts:1110` |
| state:external-projection-separate-vocabulary | 내부 낱말을 대외 낱말로 그대로 옮기려는 경우 | 대응이 성립하지 않는다. 두 어휘는 서로를 품지 않는다 | `client-order-detail.entity.ts:5` |
| state:stored-distribution-unknown | 저장된 분포를 알아야 하는 경우 | 확인하지 않았다 — 운영 데이터를 확보하지 않아 이 문서는 그 상태를 판정하지 못한다 | `schema.ts:1635` |

`확인하지 않았다` 와 `막지 않는다` 는 다르다. 앞엣것은 볼 수단을 확보하지 못했다는 뜻이고
뒤엣것은 보았고 막는 장치가 없음을 확인했다는 뜻이다.

## 부록

이 절의 정책 하나는 **저장된 값이 무엇인지 코드에서 판정할 수 없어서** 본문에서 격리된다.
본문에 두면 독자가 지금 확정된 사실로 읽는다. 운영 데이터를 확보해 판정이 서면 표기가 바뀌고
배치도 함께 본문으로 옮긴다.

### state:stored-distribution-unknown

어떤 상태가 실제로 얼마나 저장돼 있는지, 결과 칸이 채워진 행이 얼마나 되는지는 **코드만
읽어서는 알 수 없다.** 저장 계층은 낱말 목록을 강제하지 않고 결과 칸과 대외 상태 칸은 비울 수
있으며, 이 조사는 운영 데이터를 확보하지 않았다.

따라서 목록 밖의 낱말이 남아 있는 행이 있는지, 종결되지 않은 채 오래된 행이 얼마나 되는지 이
문서는 없다고도 있다고도 말하지 않는다. 쓰기 자리 기준으로 쓰이지 않는 낱말이 없다는 사실과,
저장된 행 기준으로 그렇다는 주장은 다른 것이다.

이용자에게 만드는 결과: 예전에 쓰이다 사라진 낱말이 화면에 뜰 수 있고, 그것을 화면이 어떻게
보여줄지는 정해져 있지 않다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋으로 추적한다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2024-12-03 | `12bc68b7` | 분석 진행 상태를 만드는 자리가 지금 형태로 정리됐다 |
| — | 2025-03-26 | `60831114` | 구독 낱말에 실패가 들어왔다 |
| — | 2025-04-01 | `873bc9ba` | 고객사 조회용 별도 상태 어휘가 신설됐다 |
| — | 2025-04-16 | `0056ad0d` | 구독 실패를 박는 자리와 되돌리는 자리가 한 파일에 나란히 놓였다 |
| — | 2026-05-20 | `b0df343b` | 결과 칸이 신설되고 환불이 한 번만 일어나도록 표시가 붙었다 |
| — | 2026-05-22 | `3775e2cc` | 수집·분석 단계에서 멈춘 분석을 시간 경과로 종결하는 정기 작업이 생겼다 |
| — | 2026-05-26 | `a268bc33` | 인증 단계 정기 작업이 진행 칸 대신 결과 칸을 쓰도록 바뀌었다 |
| — | 2026-06-19 | `142c746c` | 자료 수집 실패가 별도 낱말로 갈라져 나왔다 |
