# KBX FE Status Canonical Contract Hardening v60

## 1. 결론

v59에서 Grid 상태 표현을 표준화했지만, 주문 Aggregate/API/FE Workflow/Search Projection 사이에는 여전히 상태의 기계값과 표시값이 혼재했다. v60은 이 문제를 UI 장식이 아니라 **데이터 계약과 업무 사실성 문제**로 처리한다.

핵심 원칙은 단순하다.

```text
Domain / API canonical code
        ↓
Status Catalog
        ↓
사용자 Label + Semantic
```

한국어 Label은 사용자 표현이며 Domain/API machine state가 아니다.

## 2. 냉정한 진단

### 2.1 같은 주문이 화면마다 다른 상태 계약을 사용했다

- `oms.orders.status` DB 값은 이미 `DRAFT / CONFIRMED ...` canonical code였다.
- 주문 GET/Register/Confirm API는 이를 다시 `작성 / 확정`으로 번역해 반환했다.
- 주문 조회 Projection은 `NEW / READY / SHIPPED / HOLD`를 사용했다.
- FE Workflow는 일부 Korean display label을 machine state로 비교했다.

이 상태에서는 List/Detail/Transaction이 같은 Aggregate를 보면서도 상태 계약이 달라진다. 테스트와 Demo가 통과해도 신규 상태 추가나 저장조건 복원 시 분화가 발생하기 쉽다.

### 2.2 출고지시 Endpoint가 Aggregate와 Projection vocabulary를 혼합했다

기존 Ship Endpoint는 `oms.orders.status in ('NEW','DRAFT','READY')`를 사용했다. `NEW/READY`는 shipment projection 문법이고 `DRAFT/CONFIRMED`는 aggregate lifecycle 문법이다.

v60에서는 다음을 동시에 강제한다.

```text
Order lifecycle = CONFIRMED
AND
Shipment projection = READY
```

그리고 출고지시는 주문 Lifecycle을 다시 `CONFIRMED`로 바꾸지 않는다. Audit/Outbox event를 생성하는 별도 업무행동으로 유지한다.

### 2.3 Demo Read Model에도 중복 상태 Source of Truth가 있었다

Demo order row가 `shipmentStatus`와 `status`를 동시에 가지고 조회/카운터/Bulk가 서로 다른 필드를 읽었다. Ship 후 한쪽만 바뀌면 즉시 drift한다.

v60에서는 `shipmentStatus` 하나를 projection query/bulk truth로 사용한다.

## 3. 구현 변경

### Backend

- `OrderLifecycleStatus` canonical constants 추가
  - `DRAFT`
  - `CONFIRMED`
  - `ALLOCATED`
  - `PICKING`
  - `CHECKED`
  - `SHIPPED`
- `OrderShipmentStatus` projection constants 추가
  - `NEW / READY / CONFIRMED / HOLD / SHIPPED / ERROR`
- Order GET은 DB canonical lifecycle 상태를 그대로 반환한다.
- Register는 `DRAFT` 저장/반환.
- Confirm은 `DRAFT → CONFIRMED`만 허용하고 canonical 상태를 반환.
- Ship은 `aggregate CONFIRMED + projection READY` 이중 조건을 서버에서 검증하며 Aggregate lifecycle을 조작하지 않는다.
- Audit의 사용자 설명은 한국어를 유지한다. Audit description과 machine state를 혼동하지 않는다.

### Frontend

- `statusCatalog.orderLifecycle`을 canonical raw value + familiar Korean label 구조로 정리.
- `kbxStatusOptions()`로 Search Select option을 같은 Catalog에서 생성.
- `normalizeKbxStatusValue()`로 URL/User Preference status를 canonical catalog 기준으로 복원.
- 주문등록 Workflow/Command permission/status 정책을 canonical 값으로 변경.
- 화면 Context에서는 Resolver를 통해 `작성/확정/...` Label을 표시하고 Command는 raw code를 사용.
- Order Detail API Type도 canonical lifecycle type을 사용.

### Demo / Static Reference

- Demo register/get/confirm lifecycle을 canonical code로 통일.
- Projection 검색/카운터/Bulk는 `shipmentStatus` 단일 truth 사용.
- Static HTML/JavaScript도 canonical raw status를 저장하며 UI에만 한국어 Label을 표시.
- 기존 저장조건의 `신규/출고대기/...` display 문자열은 안전한 legacy migration으로 canonical code에 매핑한다.

## 4. QA / 회귀 방지

`validate-status-canonical-v60.mjs`에서 다음을 자동 검증한다.

- Backend lifecycle/shipment canonical constants
- GET 서버-side label translation 제거
- Register/Confirm canonical transition
- Ship aggregate/projection vocabulary 분리
- FE catalog coverage
- Workflow/Command canonical status
- Search option Catalog 일원화
- saved/route filter canonical normalization
- Demo single projection truth
- Static Reference canonical/display separation
- unit contract 존재

전체 `validate-kbx.mjs`는 v41~v60까지 PASS한다.

## 5. Breaking Change와 Migration

이 변경은 API status 소비자가 `작성`, `확정` 같은 한국어 문자열을 machine value로 비교했다면 영향을 준다. 따라서 release impact는 Major로 선언했다.

대표 migration:

| 기존 display-as-value | canonical value |
|---|---|
| 작성 | DRAFT |
| 확정 | CONFIRMED |
| 할당 | ALLOCATED |
| 피킹 | PICKING |
| 검수 | CHECKED |
| 출고완료 | SHIPPED |

UI는 계속 한국어 Label을 보여준다. 변경되는 것은 API/상태 비교 계약이다.

## 6. 보안·정합성 판단

FE 상태 Filter 또는 Disabled Button을 최종 방어선으로 사용하지 않는다. 특히 Ship Endpoint는 ID 직접선택에서도 서버가 Aggregate와 Projection 상태를 함께 검증한다. 이는 hidden/disabled UI만 믿는 취약한 상태전이 방식을 피하기 위한 조치다.

## 7. 현재 한계

### Browser Runtime Evidence는 아직 BLOCKED

현재 artifact 환경에는 다음이 없다.

- `pnpm-lock.yaml`
- `node_modules`
- 설치된 `@playwright/test`
- 설치된 `vitest`

`corepack pnpm --version` 실행도 `registry.npmjs.org` DNS `EAI_AGAIN`으로 실패했다. 따라서 Vite/Vitest/Playwright Runtime PASS를 주장하지 않는다.

또한 이 환경에는 `dotnet` CLI가 없어 Backend compile/test를 직접 실행하지 못했다. Backend 변경은 repository governance/static contract 수준에서 검증되었다.

## 8. 다음 P0

v60 이후 가장 먼저 볼 상태 부채는 `allocationStatus`다.

현재 Order Search Projection의 `allocation_status` 작성자/DB projection schema가 repository 안에서 완전하게 정의되어 있지 않고, Demo/FE Catalog에는 `정상 / 할당완료 / 부족 / 미확인` display 문자열이 raw value로 남아 있다.

다음 단계는 이를 섣불리 FE에서 임의 code로 바꾸는 것이 아니라:

1. Projection writer/source 계약 확인
2. canonical allocation status vocabulary 확정
3. Backend projection → Status Catalog → Search/Excel/E2E 일원화
4. 기존 saved preference migration

순으로 진행해야 한다. Source가 없는 상태에서 FE만 임의 canonicalization하면 또 다른 hallucinated contract가 된다.

## 9. 최종 판단

v60의 가치는 새로운 컴포넌트를 추가한 데 있지 않다. 같은 주문 상태를 List/Detail/Transaction/API/Demo가 서로 다른 언어로 해석하던 기술부채를 줄이고, **machine truth와 user presentation의 경계를 명시적으로 만든 것**이 핵심이다.
