Files
KArtSell.Aegis/docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/docs/frontend/KBX-FE-Status-Canonical-Contract-Hardening-v60.md
T
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

6.8 KiB

KBX FE Status Canonical Contract Hardening v60

1. 결론

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

핵심 원칙은 단순하다.

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에서는 다음을 동시에 강제한다.

Order lifecycle = CONFIRMED
AND
Shipment projection = READY

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

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

Demo order row가 shipmentStatusstatus를 동시에 가지고 조회/카운터/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의 경계를 명시적으로 만든 것이 핵심이다.