# KBX Implementation Contract v1.0
## `@kbx/ui` · Vue 3 · TypeScript · AG Grid · PrimeVue · FastEndpoints · Zod

---

# 1. 목표

KBX 구현 계층의 핵심 목적은 다음과 같다.

```text
업무 화면 개발자
        │
        │ 업무 의미만 정의
        ▼
Screen Definition
        │
        ▼
@kbx/ui
        │
        ├─ 화면 배치
        ├─ Grid UX
        ├─ Keyboard
        ├─ Lookup
        ├─ Excel
        ├─ Validation UX
        ├─ Audit UX
        ├─ Help
        └─ AI Proposal
```

Vertical Slice가 직접 결정하지 않는 것:

- 버튼 위치
- 조회영역 배치
- Grid 기본 옵션
- F2/F3/F8 처리
- Excel Import UX
- Loading UX
- Validation 표시방식
- Empty State
- Bulk Action 위치
- 변경이력 UI
- AI Panel 위치

Vertical Slice가 결정하는 것:

- 어떤 데이터를 조회하는가
- 어떤 Command가 존재하는가
- 업무 상태
- 업무 규칙
- 권한
- 어떤 예외가 존재하는가

---

# 2. 패키지 구성

권장 Monorepo 구조:

```text
apps/
 ├─ web/
 │   └─ src/
 │       ├─ app/
 │       └─ modules/
 │
packages/
 ├─ kbx-ui/
 ├─ kbx-contracts/
 ├─ kbx-icons/
 └─ kbx-testing/
```

---

# 3. `@kbx/ui`

```text
packages/kbx-ui/
 └─ src/
     ├─ primitives/
     ├─ form/
     ├─ lookup/
     ├─ grid/
     ├─ command/
     ├─ search/
     ├─ excel/
     ├─ feedback/
     ├─ audit/
     ├─ help/
     ├─ ai/
     ├─ template/
     ├─ keyboard/
     ├─ composables/
     ├─ tokens/
     └─ index.ts
```

---

# 4. `@kbx/contracts`

UI 라이브러리와 업무 모듈이 공유할 **TypeScript 계약**을 둔다.

```text
packages/kbx-contracts/
 └─ src/
     ├─ screen.ts
     ├─ field.ts
     ├─ command.ts
     ├─ grid.ts
     ├─ lookup.ts
     ├─ validation.ts
     ├─ excel.ts
     ├─ audit.ts
     └─ ai.ts
```

UI Component 구현과 업무 모듈 정의를 분리한다.

---

# 5. Public API 제한

업무 모듈에서는 가능하면 다음만 import한다.

```ts
import {
  KbxListPage,
  KbxTransactionPage,
  KbxLookup,
  KbxDataGrid,
  defineKbxScreen,
} from '@kbx/ui'
```

다음은 금지에 가깝게 관리한다.

```ts
import InputText from 'primevue/inputtext'
import Button from 'primevue/button'
import { AgGridVue } from 'ag-grid-vue3'
```

예외는 ADR 또는 코드리뷰 근거가 있어야 한다.

---

# 6. Screen ID 규칙

```text
{MODULE}-{AREA}-{NUMBER}
```

예:

```text
OMS-ORD-001
OMS-SHP-001

ERP-MST-ITEM-001
ERP-PUR-001
ERP-INV-001

WMS-PICK-001
WMS-CHECK-001
```

Screen ID는 다음의 공통 Key가 된다.

- Help
- Permission
- Telemetry
- User Preference
- Grid Layout
- 사용자 제안
- AI Context
- Screenshot/Test
- Screen Version

---

# 7. Screen Definition

모든 화면은 최소한 하나의 명시적인 Screen Definition을 가진다.

```ts
export type KbxScreenType =
  | 'list'
  | 'master'
  | 'transaction'
  | 'fast-entry'
  | 'master-detail'
  | 'queue'
  | 'reconcile'
  | 'import'
  | 'wms-mobile'
```

---

# 8. 기본 Screen 계약

```ts
export interface KbxScreenDefinition {
  id: string
  version: string

  module: 'OMS' | 'ERP' | 'WMS' | 'COMMON'

  type: KbxScreenType

  title: string
  description?: string

  permissions?: string[]

  commands?: KbxCommandDefinition[]

  helpKey?: string

  telemetry?: {
    enabled: boolean
  }
}
```

---

# 9. Helper

```ts
export function defineKbxScreen<T extends KbxScreenDefinition>(
  definition: T
): T {
  return definition
}
```

사용:

```ts
export const orderListScreen = defineKbxScreen({
  id: 'OMS-ORD-001',
  version: '1.0.0',
  module: 'OMS',
  type: 'list',
  title: '주문관리',
  helpKey: 'OMS-ORD-001',
  telemetry: {
    enabled: true,
  },
})
```

---

# 10. Field Dictionary

제품 전체에서 동일 개념에 동일 Key를 사용한다.

```ts
export type FieldKey =
  | 'orderId'
  | 'orderNo'
  | 'orderDate'
  | 'customerId'
  | 'customerCode'
  | 'customerName'
  | 'itemId'
  | 'itemCode'
  | 'itemName'
  | 'warehouseId'
  | 'warehouseCode'
  | 'warehouseName'
  | 'orderQty'
  | 'allocatedQty'
  | 'pickedQty'
  | 'shippedQty'
```

대형 시스템에서는 수작업 Union보다 Generated Contract를 사용하는 것도 가능하다.

---

# 11. Field Definition

```ts
export interface KbxFieldDefinition<T = unknown> {
  key: string
  label: string

  aliases?: string[]

  dataType:
    | 'text'
    | 'code'
    | 'integer'
    | 'decimal'
    | 'quantity'
    | 'money'
    | 'date'
    | 'datetime'
    | 'boolean'
    | 'lookup'
    | 'status'

  required?: boolean

  maxLength?: number

  precision?: number
  scale?: number

  readonly?: boolean

  importable?: boolean
  exportable?: boolean

  sensitive?: boolean

  defaultValue?: T

  lookup?: KbxLookupDefinition

  helpText?: string
}
```

---

# 12. 중요한 원칙

Field Metadata가 Domain Model을 대신하지 않는다.

예:

```text
Field Definition
```

은

```text
수량 필드
필수
소수점 2자리
```

정도를 정의한다.

반면:

```text
출고완료 이후 수량 수정 불가
```

는 Domain Rule이다.

이것을 UI Metadata에 무리하게 넣지 않는다.

---

# 13. Command Contract

```ts
export type KbxCommandGroup =
  | 'query'
  | 'edit'
  | 'workflow'
  | 'output'
  | 'more'
```

```ts
export interface KbxCommandDefinition {
  id: string

  label: string

  group: KbxCommandGroup

  variant?:
    | 'primary'
    | 'secondary'
    | 'danger'
    | 'ghost'

  shortcut?: string

  permission?: string

  icon?: string

  requiresSelection?: boolean

  minSelection?: number
  maxSelection?: number
}
```

---

# 14. 주문관리 Command

```ts
const orderCommands: KbxCommandDefinition[] = [
  {
    id: 'search',
    label: '조회',
    group: 'query',
    shortcut: 'F3',
  },
  {
    id: 'new',
    label: '신규',
    group: 'edit',
  },
  {
    id: 'ship',
    label: '출고지시',
    group: 'workflow',
    requiresSelection: true,
    minSelection: 1,
    permission: 'oms.order.ship',
  },
  {
    id: 'excel',
    label: '엑셀',
    group: 'output',
  },
]
```

---

# 15. Command 실행과 UI 정의 분리

Command Definition이 API를 직접 호출하지 않는다.

```text
Command Definition
        ↓
Command Handler
        ↓
Application API
```

예:

```ts
const handlers = {
  search: executeSearch,
  new: createOrder,
  ship: shipSelectedOrders,
}
```

---

# 16. Keyboard Manager

단축키는 화면마다 `keydown` Event를 등록하지 않는다.

공통 Manager:

```ts
export interface KbxShortcut {
  key: string

  scope:
    | 'application'
    | 'page'
    | 'grid'
    | 'dialog'
    | 'editor'

  priority?: number

  enabled?: () => boolean

  execute(): void | Promise<void>
}
```

---

# 17. Scope 우선순위

```text
Editor
 ↓
Dialog
 ↓
Grid
 ↓
Page
 ↓
Application
```

예:

Grid Cell 편집 중 `Enter`는 Page Command로 넘어가지 않는다.

---

# 18. 기본 Shortcut

```text
F2      Lookup
F3      조회
F8      저장/확정
Ctrl+S  저장
Esc     현재 Context 취소
```

Browser Shortcut Override 금지:

```text
F5
Ctrl+R
Ctrl+L
Ctrl+T
Ctrl+W
```

---

# 19. KbxLookup Contract

```ts
export interface KbxLookupItem<TId = string> {
  id: TId

  code: string

  displayName: string

  secondaryText?: string

  status?: string

  metadata?: Record<string, unknown>
}
```

---

# 20. Lookup Search Request

```ts
export interface KbxLookupSearchRequest {
  query?: string

  page: number
  pageSize: number

  filters?: Record<string, unknown>
}
```

---

# 21. Lookup Response

```ts
export interface KbxLookupSearchResult<TId = string> {
  items: KbxLookupItem<TId>[]

  totalCount: number
}
```

---

# 22. Lookup Provider

```ts
export interface KbxLookupProvider<TId = string> {
  search(
    request: KbxLookupSearchRequest
  ): Promise<KbxLookupSearchResult<TId>>

  resolveById(
    id: TId
  ): Promise<KbxLookupItem<TId> | null>

  resolveByCode(
    code: string
  ): Promise<KbxLookupItem<TId> | null>
}
```

---

# 23. Customer Provider

```ts
export class CustomerLookupProvider
  implements KbxLookupProvider<string>
{
  async search(request: KbxLookupSearchRequest) {
    return customerApi.searchLookup(request)
  }

  async resolveById(id: string) {
    return customerApi.getLookupById(id)
  }

  async resolveByCode(code: string) {
    return customerApi.getLookupByCode(code)
  }
}
```

---

# 24. KbxLookup 사용

```vue
<KbxLookup
  v-model="form.customerId"
  entity="customer"
  label="거래처"
  required
/>
```

화면에서 다음을 직접 작성하지 않는다.

```text
코드 Input
+
이름 Input
+
검색 Button
+
Dialog
+
Grid
```

---

# 25. Lookup Registry

```ts
export const lookupRegistry = {
  customer: new CustomerLookupProvider(),
  item: new ItemLookupProvider(),
  warehouse: new WarehouseLookupProvider(),
}
```

KbxLookup:

```text
entity="customer"
```

이면 Registry에서 Provider를 찾는다.

---

# 26. Lookup Server Endpoint

예:

```text
GET /api/lookups/customers
GET /api/lookups/customers/{id}
GET /api/lookups/customers/by-code/{code}
```

그러나 Module마다 REST Pattern을 강제하기보다 Frontend Provider가 차이를 흡수할 수 있다.

---

# 27. Lookup Read Model

Lookup API가 전체 Entity를 반환하면 안 된다.

```csharp
public sealed record CustomerLookupRow(
    Guid Id,
    string Code,
    string Name,
    string? BusinessNumber,
    bool IsActive);
```

Lookup에 필요한 최소 데이터만 제공한다.

---

# 28. KbxDataGrid

AG Grid Wrapper의 핵심 목표는 AG Grid 기능 축소가 아니다.

**우리 제품에서 허용하는 AG Grid 사용법을 고정**하는 것이다.

---

# 29. Grid Column

```ts
export type KbxGridType =
  | 'text'
  | 'code'
  | 'integer'
  | 'decimal'
  | 'quantity'
  | 'money'
  | 'percent'
  | 'date'
  | 'datetime'
  | 'boolean'
  | 'status'
  | 'lookup'
  | 'link'
```

```ts
export interface KbxGridColumn<T> {
  field: keyof T & string

  header: string

  type?: KbxGridType

  width?: number
  minWidth?: number
  maxWidth?: number

  pinned?: 'left' | 'right'

  editable?: boolean | ((row: T) => boolean)

  sortable?: boolean
  filterable?: boolean

  permission?: string

  lookup?: KbxLookupDefinition
}
```

---

# 30. Grid Props

```ts
export interface KbxDataGridProps<T> {
  rows: T[]

  columns: KbxGridColumn<T>[]

  rowKey: keyof T & string

  loading?: boolean

  selection?:
    | 'none'
    | 'single'
    | 'multiple'

  editable?: boolean

  clipboard?: boolean

  personalization?: boolean

  exportable?: boolean

  density?: 'compact' | 'comfortable'

  summary?: KbxGridSummary<T>[]

  emptyText?: string
}
```

---

# 31. Grid Event

```ts
export interface KbxGridEvents<T> {
  rowClicked: T

  rowDoubleClicked: T

  selectionChanged: T[]

  cellChanged: {
    row: T
    field: keyof T
    oldValue: unknown
    newValue: unknown
  }

  validationChanged: KbxGridValidationState
}
```

---

# 32. Grid 내부 AG Grid 설정

`KbxDataGrid`에서 중앙 통제한다.

예:

```ts
const defaultColDef = {
  sortable: true,
  resizable: true,
  suppressMovable: false,
}
```

다음도 공통처리한다.

- Header Style
- Focus Style
- Clipboard
- Keyboard
- Validation Renderer
- Status Renderer
- Row Selection
- Column Preference
- Loading Overlay
- Empty Overlay

---

# 33. 업무 화면에서 금지

```ts
gridOptions = {
  ...
}
```

를 각 화면에서 80줄씩 만드는 방식.

업무 화면은 주로 Column Schema만 정의한다.

---

# 34. 주문 Grid 예

```ts
interface OrderSearchRow {
  id: string

  orderNo: string

  channelName: string

  orderedAt: string

  customerName: string

  itemSummary: string

  totalQty: number

  amount: number

  allocationStatus: string

  shipmentStatus: string

  exceptionCount: number
}
```

---

# 35. Column Schema

```ts
const columns: KbxGridColumn<OrderSearchRow>[] = [
  {
    field: 'orderNo',
    header: '주문번호',
    type: 'link',
    width: 150,
    pinned: 'left',
  },
  {
    field: 'channelName',
    header: '판매채널',
    width: 110,
  },
  {
    field: 'orderedAt',
    header: '주문일시',
    type: 'datetime',
    width: 160,
  },
  {
    field: 'customerName',
    header: '주문자',
    width: 120,
  },
  {
    field: 'itemSummary',
    header: '대표상품',
    width: 240,
  },
  {
    field: 'totalQty',
    header: '수량',
    type: 'quantity',
    width: 90,
  },
  {
    field: 'amount',
    header: '금액',
    type: 'money',
    width: 130,
  },
]
```

---

# 36. Type 기반 자동 동작

`type: 'money'`

이면 자동:

```text
오른쪽 정렬
천단위 표시
숫자 Filter
Excel numeric export
```

`type: 'date'`

이면:

```text
날짜 정렬
날짜 포맷
날짜 Filter
```

화면마다 Formatter를 작성하지 않는다.

---

# 37. Row Selection

대량 업무에서는 반드시 Selection 의미를 명확하게 관리한다.

```ts
interface KbxSelectionState<TId> {
  selectedIds: TId[]

  mode:
    | 'explicit'
    | 'all-filtered'

  excludedIds?: TId[]
}
```

---

# 38. 매우 중요한 대량 선택

10만 건 조회 결과에서:

```text
전체 선택
```

을 10만 ID를 Browser Memory에 올리는 방식으로 구현하지 않는다.

다음 개념을 지원한다.

```text
현재 페이지 선택

또는

현재 검색조건 82,415건 전체 선택
```

후자의 경우 Server-side Bulk Command를 사용한다.

---

# 39. Server-side Bulk Request

```ts
interface BulkSelectionRequest {
  mode: 'ids' | 'filter'

  ids?: string[]

  filter?: OrderSearchFilter

  excludedIds?: string[]
}
```

대량 OMS에서 중요하다.

---

# 40. KbxSearchPanel Contract

```ts
export type KbxSearchFieldType =
  | 'text'
  | 'date'
  | 'dateRange'
  | 'select'
  | 'lookup'
  | 'checkbox'
```

```ts
export interface KbxSearchField {
  key: string

  label: string

  type: KbxSearchFieldType

  primary?: boolean

  width?: 'sm' | 'md' | 'lg'

  defaultValue?: unknown

  options?: unknown[]

  lookup?: KbxLookupDefinition
}
```

---

# 41. Order Search Definition

```ts
export const orderSearchFields: KbxSearchField[] = [
  {
    key: 'period',
    label: '주문기간',
    type: 'dateRange',
    primary: true,
  },
  {
    key: 'channelId',
    label: '판매채널',
    type: 'lookup',
    primary: true,
    lookup: {
      entity: 'salesChannel',
    },
  },
  {
    key: 'status',
    label: '상태',
    type: 'select',
    primary: true,
  },
  {
    key: 'keyword',
    label: '통합검색',
    type: 'text',
    primary: true,
    width: 'lg',
  },
]
```

---

# 42. TanStack Query 경계

조회 상태는 기본적으로 TanStack Query가 책임진다.

```ts
const query = useQuery({
  queryKey: ['orders', searchCondition],
  queryFn: () => orderApi.search(searchCondition),
})
```

Pinia에 조회 결과를 복제해서 보관하지 않는다.

---

# 43. Pinia가 적합한 영역

Pinia:

- Workspace Tabs
- 사용자 UI Preference
- App-wide 상태
- 현재 Tenant
- 사용자 Context
- Notification Center

TanStack Query:

- 서버 데이터
- 조회 캐시
- Mutation
- Invalidations

구분한다.

---

# 44. KbxListPage API

개념 사용:

```vue
<KbxListPage
  :screen="screen"
  :commands="commands"
>
  <template #search>
    <KbxSearchPanel
      v-model="search"
      :fields="searchFields"
      @search="executeSearch"
    />
  </template>

  <template #content>
    <KbxDataGrid
      :rows="orders"
      :columns="columns"
      row-key="id"
      selection="multiple"
    />
  </template>
</KbxListPage>
```

---

# 45. 더 강한 Schema 방식도 가능

단순 List에서는:

```vue
<KbxListPage
  :definition="definition"
/>
```

만으로도 생성 가능하다.

하지만 모든 화면을 Dynamic JSON Engine으로 만들지는 않는다.

---

# 46. 권장 비율

```text
70%
표준 Template + Schema

20%
Template + explicit Vue slots

10%
Custom 업무 UI
```

---

# 47. KbxTransactionPage

Transaction의 표준 구조:

```text
Page Header
Command Bar
Header Form
Detail Grid
Summary
Status
Audit
```

---

# 48. Transaction Context

```ts
export interface KbxTransactionContext<THeader, TLine> {
  header: THeader

  lines: TLine[]

  mode:
    | 'new'
    | 'edit'
    | 'view'

  status: string

  dirty: boolean

  version?: number
}
```

---

# 49. 주문등록 Type

```ts
interface OrderHeaderForm {
  orderDate: string

  customerId: string | null

  warehouseId: string | null

  receiverName: string

  phone: string

  postalCode: string

  address1: string

  address2?: string
}
```

```ts
interface OrderLineForm {
  clientId: string

  itemId: string | null

  itemCode?: string

  itemName?: string

  quantity: number

  unitPrice: number

  amount: number
}
```

`clientId`는 신규 Grid Row 식별용이다.

DB ID와 혼동하지 않는다.

---

# 50. Transaction Page

```vue
<KbxTransactionPage
  :screen="screen"
  :status="order.status"
  :dirty="dirty"
>
  <template #header>
    <OrderHeaderForm
      v-model="order.header"
    />
  </template>

  <template #detail>
    <KbxDataGrid
      :rows="order.lines"
      :columns="lineColumns"
      row-key="clientId"
      editable
    />
  </template>

  <template #summary>
    <OrderSummary
      :lines="order.lines"
    />
  </template>
</KbxTransactionPage>
```

---

# 51. 계산값

예:

```text
금액 = 수량 × 단가
```

UI 계산은 사용자 피드백을 위해 즉시 수행 가능하다.

하지만 Server가 최종 값을 재검증/재계산한다.

금액을 Client 값 그대로 신뢰하지 않는다.

---

# 52. Zod 역할

Zod:

- Required
- Format
- Type
- Length
- 간단한 Cross-field UX Validation

적합.

---

# 53. Zod 주문 Schema

```ts
import { z } from 'zod'

export const orderLineSchema = z.object({
  itemId: z.string().uuid({
    message: '품목을 선택하세요.',
  }),

  quantity: z
    .number()
    .positive('수량은 0보다 커야 합니다.'),

  unitPrice: z
    .number()
    .nonnegative('단가는 0 이상이어야 합니다.'),
})
```

---

# 54. Header Schema

```ts
export const orderHeaderSchema = z.object({
  orderDate: z.string().min(1, '주문일을 입력하세요.'),

  customerId: z
    .string()
    .uuid('거래처를 선택하세요.'),

  warehouseId: z
    .string()
    .uuid('출고창고를 선택하세요.'),

  receiverName: z
    .string()
    .min(1, '수취인을 입력하세요.'),
})
```

---

# 55. Client Validation Pipeline

```text
사용자 입력
   ↓
Component Validation
   ↓
Zod
   ↓
Save Attempt
   ↓
API
```

Zod가 통과했다고 업무가 유효하다고 판단하면 안 된다.

---

# 56. Server Domain Validation

Server:

```text
Endpoint Validation
       ↓
Application Validation
       ↓
Domain Rule
       ↓
Database Constraint
```

예:

```text
quantity > 0
```

는 Client에서도 확인 가능.

하지만:

```text
현재 출고 가능한 재고가 충분한가?
주문이 수정 가능한 상태인가?
창고가 현재 사용 가능한가?
```

는 Server가 최종 책임진다.

---

# 57. FastEndpoints Request

```csharp
public sealed record RegisterOrderRequest(
    DateOnly OrderDate,
    Guid CustomerId,
    Guid WarehouseId,
    string ReceiverName,
    string Phone,
    string Address1,
    IReadOnlyList<RegisterOrderLineRequest> Lines);
```

---

# 58. Endpoint

```csharp
public sealed class RegisterOrderEndpoint
    : Endpoint<RegisterOrderRequest, RegisterOrderResponse>
{
    public override void Configure()
    {
        Post("/api/oms/orders");
        Permissions("oms.order.create");
    }

    public override async Task HandleAsync(
        RegisterOrderRequest req,
        CancellationToken ct)
    {
        // Application Command 실행
    }
}
```

Endpoint 안에 Domain 로직을 길게 넣지 않는다.

---

# 59. Validation Error Contract

Frontend가 FastEndpoints 내부 표현에 강하게 결합되지 않도록 **KBX 표준 오류 계약**을 만든다.

```json
{
  "type": "validation",
  "title": "입력값을 확인하세요.",
  "errors": [
    {
      "field": "customerId",
      "code": "CUSTOMER_REQUIRED",
      "message": "거래처를 선택하세요."
    }
  ]
}
```

---

# 60. TypeScript

```ts
export interface KbxValidationError {
  field?: string

  rowKey?: string

  code: string

  message: string
}
```

```ts
export interface KbxValidationProblem {
  type: 'validation'

  title: string

  errors: KbxValidationError[]
}
```

---

# 61. Grid Row 오류

Detail 행 오류에는 `rowKey`를 포함한다.

```json
{
  "field": "quantity",
  "rowKey": "line-7",
  "code": "INSUFFICIENT_STOCK",
  "message": "출고 가능 수량은 8개입니다."
}
```

Frontend는 자동으로 해당 Cell에 Error를 연결한다.

---

# 62. Field Path에 배열 Index만 쓰지 않는 이유

```text
lines[7].quantity
```

만 보내면 정렬/삭제 후 화면 Row와 불일치할 수 있다.

가능하면 Client-generated Row Key 또는 안정적인 Line ID를 함께 사용한다.

---

# 63. Business Problem Contract

Field 오류가 아닌 업무 오류:

```json
{
  "type": "business-rule",
  "code": "ORDER_NOT_EDITABLE",
  "title": "주문을 수정할 수 없습니다.",
  "detail": "이미 피킹이 시작된 주문입니다.",
  "actions": [
    {
      "id": "viewPicking",
      "label": "피킹 작업 보기"
    }
  ]
}
```

---

# 64. Conflict Contract

Optimistic Concurrency:

```json
{
  "type": "conflict",
  "code": "ORDER_VERSION_CONFLICT",
  "title": "다른 사용자가 주문을 변경했습니다.",
  "currentVersion": 17
}
```

Frontend:

```text
최신 내용 보기
```

Action 제공.

---

# 65. Version

Request:

```json
{
  "orderId": "...",
  "version": 16
}
```

Server:

```text
현재 version = 17
```

이면 Update 거부.

Silent overwrite 금지.

---

# 66. Database Constraint

정합성 마지막 방어선.

예:

```text
UNIQUE
FOREIGN KEY
CHECK
NOT NULL
```

Application Validation만 믿지 않는다.

---

# 67. PostgreSQL Transaction

하나의 Domain Transaction에서 필요한 Write는 Transaction으로 묶는다.

Dapper 사용 여부와 관계없이:

```text
Begin Transaction
   ↓
Read required state
   ↓
Validate
   ↓
Write
   ↓
Outbox
   ↓
Audit
   ↓
Commit
```

---

# 68. Outbox

Domain Commit과 Integration Event 생성이 분리되어 유실되지 않게 한다.

```text
Order Confirm
    │
    ├─ Order Update
    ├─ Audit
    └─ Outbox Event
```

동일 DB Transaction.

---

# 69. UI Integration Status

외부 연계가 즉시 끝났다고 거짓 표시하지 않는다.

```text
주문 확정
완료

WMS 연계
전송 대기
```

사용자는 Business State와 Integration State를 구분할 수 있어야 한다.

---

# 70. TanStack Mutation

```ts
const mutation = useMutation({
  mutationFn: orderApi.save,

  onSuccess(result) {
    toast.success('저장했습니다.')

    queryClient.invalidateQueries({
      queryKey: ['orders'],
    })
  },

  onError(error) {
    handleKbxApiError(error)
  },
})
```

---

# 71. 공통 Error Handler

```ts
handleKbxApiError()
```

가 다음을 분기한다.

```text
validation
business-rule
conflict
permission
not-found
integration
unexpected
```

화면마다 Axios Error를 직접 Parsing하지 않는다.

---

# 72. Axios Interceptor 역할

Interceptor에서 가능한 것:

- Correlation ID
- Authentication
- 공통 Problem parsing
- Network 상태

Interceptor에서 하지 말아야 할 것:

- 특정 업무 오류 Toast
- 주문 상태 판단
- 화면 navigation 강제

업무 오류는 호출 Context에서 처리한다.

---

# 73. Excel Contract

```ts
export interface KbxImportDefinition {
  id: string

  screenId: string

  entity: string

  fields: KbxFieldDefinition[]

  allowCreate: boolean

  allowUpdate: boolean

  maxFileSize?: number

  maxRows?: number
}
```

---

# 74. Mapping

```ts
export interface KbxImportMapping {
  sourceColumn: string

  targetField: string | null

  source:
    | 'exact'
    | 'alias'
    | 'saved'
    | 'ai'
    | 'manual'

  confidence?: number
}
```

---

# 75. Import Job

대용량 Excel은 HTTP Request 하나에서 끝내지 않는다.

```text
Upload
 ↓
Import Session
 ↓
Hangfire Job
 ↓
Staging
 ↓
Validation
 ↓
Preview
 ↓
Commit Job
```

---

# 76. Import Session

예:

```csharp
ImportSession
{
    Id
    TenantId
    UserId
    ImportType
    FileName
    Status
    TotalRows
    ValidRows
    InvalidRows
    CreatedAt
}
```

---

# 77. Import Row

Staging:

```text
ImportSessionId
RowNumber
RawData
NormalizedData
ValidationState
Errors
```

원본 Row Number를 반드시 유지한다.

---

# 78. Excel 데이터가 Domain DB로 바로 들어가지 않는 이유

필수:

- 재현성
- 오류보고
- 부분 성공
- Mapping 확인
- 중복 검증
- 사용자 Preview
- Audit
- Retry

때문이다.

---

# 79. Import Commit

Commit은 가능한 한 정상 Row만 대상으로 수행 가능하게 한다.

결과:

```text
17,894 성공
336 오류
```

단 업무적으로 All-or-Nothing이어야 하는 Import는 별도 Policy를 둔다.

---

# 80. Idempotency

Excel 재시도나 네트워크 Retry로 같은 업무가 중복 생성되지 않게 한다.

예:

```text
Idempotency-Key
```

또는 업무 Key.

특히:

- 주문수집
- 출고지시
- 송장
- WMS Scan
- 외부 API 연동

에 중요하다.

---

# 81. KbxStatus Contract

Domain 상태와 UI Semantic 상태를 분리한다.

```ts
export interface KbxStatusDefinition {
  value: string

  label: string

  semantic:
    | 'draft'
    | 'pending'
    | 'processing'
    | 'completed'
    | 'hold'
    | 'warning'
    | 'error'
    | 'cancelled'
}
```

---

# 82. OMS 예

```ts
export const orderStatuses = {
  New: {
    label: '신규',
    semantic: 'pending',
  },

  Allocated: {
    label: '재고할당',
    semantic: 'processing',
  },

  Shipped: {
    label: '출고완료',
    semantic: 'completed',
  },

  Hold: {
    label: '보류',
    semantic: 'hold',
  },
}
```

---

# 83. State Machine

중요 Transaction은 상태 전이를 명시한다.

```text
Draft
 ↓
Confirmed
 ↓
Allocated
 ↓
Picking
 ↓
Checked
 ↓
Shipped
```

허용되지 않는 전이는 Domain에서 차단.

---

# 84. UI는 상태에 따라 Command를 표현

예:

`Draft`

```text
[저장]
[확정]
```

`Picking`

```text
[피킹작업 보기]
```

`Shipped`

```text
[변경이력]
```

하지만 UI 숨김이 보안/업무규칙의 최종 방어선은 아니다.

---

# 85. Permission

Frontend:

UX 표현.

Backend:

최종 Enforcement.

```ts
export interface KbxPermissionContext {
  has(permission: string): boolean
}
```

---

# 86. Permission에 따른 Command

```ts
{
  id: 'cancel',
  label: '주문취소',
  permission: 'oms.order.cancel'
}
```

KbxCommandBar가 자동 처리한다.

---

# 87. Audit Contract

```ts
export interface KbxAuditEntry {
  id: string

  occurredAt: string

  actor: {
    type:
      | 'user'
      | 'system'
      | 'api'
      | 'import'
      | 'ai'

    displayName: string
  }

  action: string

  changes?: {
    field: string
    label: string
    before?: unknown
    after?: unknown
  }[]

  reason?: string
}
```

---

# 88. 기술 Audit

사용자용 Audit와 별도로 Server에서:

```text
CorrelationId
RequestId
CommandId
EventId
OutboxId
JobId
```

추적 가능하게 한다.

---

# 89. Serilog / OTel 연결

업무 요청 하나를:

```text
Browser
 ↓
API
 ↓
Command
 ↓
DB
 ↓
Outbox
 ↓
Hangfire
 ↓
External API
```

까지 Correlation 가능하게 한다.

UX 문제 재현성과 운영 안정성에 직접 연결된다.

---

# 90. KbxAiContext

AI에 전체 화면 DOM을 던지지 않는다.

```ts
export interface KbxAiScreenContext {
  screenId: string

  screenVersion: string

  entityId?: string

  selectedIds?: string[]

  filters?: Record<string, unknown>

  allowedCapabilities: string[]
}
```

---

# 91. AI Action

```ts
export interface KbxAiProposal {
  id: string

  type: string

  title: string

  explanation: string

  targets: {
    entityType: string
    entityId: string
  }[]

  proposedChanges?: KbxAiFieldChange[]

  confidence?: number

  evidence?: KbxAiEvidence[]

  requiredPermission?: string
}
```

---

# 92. AI Action 실행

금지:

```text
LLM text
→ Parse
→ DB Update
```

권장:

```text
LLM
 ↓
Structured Proposal
 ↓
Schema Validation
 ↓
Entity Resolve
 ↓
Permission
 ↓
Domain Validation
 ↓
User Confirm
 ↓
Command
```

---

# 93. AI Entity Resolve

예:

AI:

```text
대한상사
```

실행 전에:

```text
CustomerId
```

가 실제 Domain DB에서 Resolve되어야 한다.

AI가 존재하지 않는 거래처 ID를 만들 수 없어야 한다.

---

# 94. AI와 화면 UX

AI가 제안:

```text
재고가 있는 인천센터로
3건의 출고창고를 변경할 수 있습니다.
```

화면:

```text
AI 제안

3건

ORD001
서울 → 인천

ORD002
서울 → 인천

ORD003
서울 → 인천

[취소]
[상세보기]
[변경안 적용]
```

공통 `KbxProposalPanel` 사용.

---

# 95. Screen Definition 확장 예

OMS 주문조회 전체 계약:

```ts
export const orderListDefinition = defineKbxScreen({
  id: 'OMS-ORD-001',

  version: '1.0.0',

  module: 'OMS',

  type: 'list',

  title: '주문관리',

  helpKey: 'OMS-ORD-001',

  permissions: [
    'oms.order.read',
  ],

  commands: [
    {
      id: 'search',
      label: '조회',
      group: 'query',
      shortcut: 'F3',
    },
    {
      id: 'new',
      label: '신규',
      group: 'edit',
      permission: 'oms.order.create',
    },
    {
      id: 'ship',
      label: '출고지시',
      group: 'workflow',
      requiresSelection: true,
      permission: 'oms.order.ship',
    },
    {
      id: 'excel',
      label: '엑셀',
      group: 'output',
    },
  ],

  telemetry: {
    enabled: true,
  },
})
```

---

# 96. 실제 Page

```vue
<script setup lang="ts">
import {
  KbxListPage,
  KbxSearchPanel,
  KbxDataGrid,
} from '@kbx/ui'

import { useOrderSearch } from './useOrderSearch'

import {
  orderListDefinition,
  orderSearchFields,
  orderColumns,
} from './order-list.definition'

const {
  search,
  rows,
  loading,
  selection,
  executeSearch,
  executeCommand,
} = useOrderSearch()
</script>

<template>
  <KbxListPage
    :screen="orderListDefinition"
    @command="executeCommand"
  >
    <template #search>
      <KbxSearchPanel
        v-model="search"
        :fields="orderSearchFields"
        @search="executeSearch"
      />
    </template>

    <template #content>
      <KbxDataGrid
        v-model:selection="selection"
        :rows="rows"
        :columns="orderColumns"
        row-key="id"
        selection="multiple"
        :loading="loading"
        personalization
        exportable
      />
    </template>
  </KbxListPage>
</template>
```

이 정도가 Vertical Slice Page의 권장 복잡도다.

---

# 97. `useOrderSearch`

업무 orchestration:

```ts
export function useOrderSearch() {
  const search = reactive(createDefaultOrderSearch())

  const selection = ref<string[]>([])

  const query = useQuery({
    queryKey: computed(() => [
      'orders',
      toRaw(search),
    ]),

    queryFn: () =>
      orderApi.search(search),

    enabled: false,
  })

  async function executeSearch() {
    selection.value = []

    await query.refetch()
  }

  async function executeCommand(commandId: string) {
    switch (commandId) {
      case 'search':
        return executeSearch()

      case 'ship':
        return shipOrders(selection.value)

      case 'new':
        return openNewOrder()
    }
  }

  return {
    search,
    rows: computed(() => query.data.value?.items ?? []),
    loading: query.isFetching,
    selection,
    executeSearch,
    executeCommand,
  }
}
```

---

# 98. switch가 커지는 경우

Command가 많아지면:

```ts
const commandHandlers = {
  search: executeSearch,
  ship: executeShip,
  hold: executeHold,
  export: executeExport,
}
```

Registry 형태로 분리한다.

거대한 `switch`도 또 다른 기술부채가 될 수 있다.

---

# 99. FastEndpoints Vertical Slice

예:

```text
Modules/
 └─ OMS/
     └─ Orders/
         ├─ Search/
         │   ├─ Endpoint.cs
         │   ├─ Request.cs
         │   ├─ Response.cs
         │   ├─ Handler.cs
         │   └─ Sql.cs
         │
         ├─ Register/
         ├─ Confirm/
         ├─ Hold/
         └─ Cancel/
```

---

# 100. Read Query

Dapper 활용:

```csharp
public sealed class SearchOrdersHandler(
    NpgsqlDataSource dataSource)
{
    public async Task<SearchOrdersResponse> HandleAsync(
        SearchOrdersRequest request,
        CancellationToken cancellationToken)
    {
        await using var connection =
            await dataSource.OpenConnectionAsync(cancellationToken);

        // 검색 전용 Projection Query
    }
}
```

Read Model을 Domain Entity로 재구성할 필요가 없다.

---

# 101. 조회 전용 Projection

```csharp
public sealed record OrderSearchRow(
    Guid Id,
    string OrderNo,
    string ChannelName,
    DateTimeOffset OrderedAt,
    string CustomerName,
    string ItemSummary,
    decimal TotalQty,
    decimal Amount,
    string AllocationStatus,
    string ShipmentStatus,
    int ExceptionCount);
```

Grid가 필요한 값을 한 번에 반환한다.

---

# 102. N+1 방지

금지:

```text
Orders 100개 조회
↓
Customer API 100번
↓
Item API 100번
```

OMS Grid는 필요한 Projection을 Server에서 만든다.

---

# 103. Client-side Join도 최소화

Frontend가:

```text
orders
customers
items
warehouses
```

를 각각 Query 후 Join하는 방식은 업무 Grid에 부적절하다.

Server Read Model 사용.

---

# 104. 반대로 Master 전체를 Join하지 않는다

조회 Projection에 필요한 필드만 선택한다.

```text
필요한 역정규화
```

와

```text
무분별한 데이터 복제
```

를 구분한다.

---

# 105. Optimistic UI 제한

일반 Consumer App처럼 모든 Domain Mutation을 Optimistic Update하지 않는다.

적합:

```text
즐겨찾기
Grid Layout
개인 Preference
```

주의:

```text
재고
출고
입고
주문상태
```

이런 강한 정합성 업무는 Server 결과 확인 후 화면을 확정하는 것을 기본으로 한다.

---

# 106. Loading UX

Mutation 중 버튼:

```text
[저장 중...]
```

기존 화면을 완전히 사라지게 하지 않는다.

Double Submit 방지.

---

# 107. Job과 Command 구분

즉시 처리:

```text
주문 1건 저장
```

Command.

대량:

```text
Excel 30,000건 Import
```

Job.

대량 출고지시도 건수가 많거나 외부 연계가 길면 Job으로 전환 가능.

---

# 108. Job Threshold

숫자를 UI 코드에서 임의 지정하지 않는다.

Server Policy:

```text
BulkOperationPolicy
```

가 결정한다.

예:

```text
500건 이하 → synchronous
500건 초과 → background job
```

이 숫자는 업무별 운영 데이터를 보고 조정한다.

---

# 109. WMS Barcode Contract

```ts
export interface KbxBarcodeEvent {
  rawValue: string

  normalizedValue: string

  source:
    | 'keyboard-wedge'
    | 'camera'
    | 'manual'

  occurredAt: number
}
```

---

# 110. Scan Command

```text
Scan
 ↓
Client normalization
 ↓
API Command
 ↓
Idempotency
 ↓
Domain validation
 ↓
Response
 ↓
Sound/Vibration
```

---

# 111. WMS에서는 서버 응답을 기다리지 않고 무조건 성공음 금지

실제 Domain 결과 확인 전 Success로 표현하면 현장 데이터가 오염될 수 있다.

네트워크 지연 UX와 실제 성공 상태를 구분한다.

---

# 112. Offline 가능 Command

각 Command에 명시:

```ts
interface WmsCommandPolicy {
  offlineAllowed: boolean

  idempotent: boolean

  requiresServerValidation: boolean
}
```

---

# 113. 사용자 Preference

저장 가능:

```text
Grid Column
Column Width
Sort
Density
Search Defaults
Favorite
Page Size
```

Key:

```text
Tenant
User
ScreenId
ScreenVersion
```

---

# 114. Screen Version이 필요한 이유

화면 Column 구조가 크게 변경된 뒤 기존 사용자의 Grid Layout을 그대로 적용하면 깨질 수 있다.

Preference Migration 전략이 필요하다.

---

# 115. Screen Definition은 코드 우선

초기에는 TypeScript Definition을 권장한다.

금지에 가까운 초기 설계:

```text
모든 화면을 DB JSON으로 저장
↓
Runtime UI 생성
```

이 방식은:

- Type Safety 감소
- Debug 어려움
- IDE 지원 감소
- 테스트 어려움
- 복잡한 예외처리 증가

문제를 만든다.

---

# 116. Metadata는 어디까지 사용할 것인가

강하게 Metadata화:

- Field
- Grid Column
- Search Field
- Excel Mapping
- Status
- Permission
- Help Key

코드로 유지:

- 복잡 업무 Flow
- Domain Rule
- 복잡 Form Interaction
- WMS Scanner State
- AI Action orchestration

---

# 117. Screen Manifest

Build 시 Screen Definition을 모아서 Manifest 생성 가능.

```json
[
  {
    "id": "OMS-ORD-001",
    "version": "1.0.0",
    "type": "list"
  },
  {
    "id": "ERP-MST-ITEM-001",
    "version": "1.0.0",
    "type": "master"
  }
]
```

활용:

- Help
- 권한관리
- 사용자제안
- AI Grounding
- 테스트
- 운영관리

---

# 118. AI Coding에도 Manifest 사용

AI Agent가 새 화면을 만들 때:

```text
사용 가능한 Template
사용 가능한 Component
Screen Definition Contract
Field Dictionary
기존 유사화면
```

을 먼저 제공한다.

---

# 119. Component Manifest

```json
{
  "KbxLookup": {
    "purpose": "업무 코드 조회 및 선택",
    "keyboard": [
      "F2",
      "Enter",
      "Esc"
    ],
    "allowed": [
      "customer",
      "item",
      "warehouse"
    ]
  }
}
```

AI가 존재하지 않는 Component를 상상해서 만들 가능성을 줄인다.

---

# 120. ESLint 규칙 권장

가능하다면 Custom ESLint Rule로:

```text
modules/**에서
primevue/*
ag-grid-vue3
```

직접 import 경고 또는 금지.

예외:

```text
// kbx-exception: ...
```

명시 요구.

표준은 문서보다 **자동 검증**되는 것이 강하다.

---

# 121. Dependency Rule

```text
OMS/WMS/ERP
     ↓
@kbx/ui
     ↓
PrimeVue / AG Grid
```

반대 방향 금지.

```text
@kbx/ui
 ↓
OMS module
```

금지.

---

# 122. 테스트 구조

```text
packages/kbx-ui/
 └─ tests/

apps/web/
 └─ tests/
     ├─ component/
     ├─ integration/
     └─ e2e/
```

---

# 123. KbxLookup Contract Test

```text
F2
→ Popup Open

Esc
→ Close

Enter
→ Resolve

Arrow
→ Move

Enter
→ Select

Select
→ Focus Restore
```

Vitest로 고정.

---

# 124. KbxDataGrid Contract Test

```text
Copy/Paste
Keyboard 이동
Number formatting
Validation indication
Selection
Column preference
Readonly
Editable
```

검증.

---

# 125. Playwright 주문등록

Mouse 없이:

```text
화면 진입
↓
F2
↓
거래처 검색
↓
Enter
↓
품목코드 입력
↓
수량 입력
↓
F8
↓
저장 완료
```

E2E 테스트.

---

# 126. Playwright OMS Bulk

```text
주문조회
↓
F3
↓
3건 선택
↓
출고지시
↓
결과
```

---

# 127. Excel E2E

```text
Upload
↓
Mapping
↓
Validation
↓
Commit
↓
Result
```

오류 Scenario 포함.

---

# 128. WMS E2E

가상 Scanner Event:

```text
Location Scan
↓
Item Scan
↓
Quantity +1
↓
Item Complete
↓
Next Item
```

중복 Scan도 별도 Scenario로 검증.

---

# 129. API Contract Test

Frontend와 Backend 오류 계약을 고정한다.

예:

```text
KbxValidationProblem
KbxBusinessProblem
KbxConflictProblem
```

백엔드 변경으로 Frontend 오류 UX가 깨지는 것을 방지한다.

---

# 130. 운영 Telemetry

Screen 단위:

```text
screen.open

search.execute

command.execute

command.failed

lookup.open

lookup.select

excel.import.start

excel.import.failed

validation.failed

ai.proposal.open

ai.proposal.accept
```

---

# 131. Telemetry에서 금지

다음 값을 그대로 넣지 않는다.

```text
주민번호
전화번호
주소
실제 주문 상세
민감 고객정보
```

업무 상태와 Event Metadata 중심.

---

# 132. UX 개선 근거

Telemetry를 통해:

```text
OMS-ORD-001

평균 조회 6.3회
출고지시 전 평균 4.8 click
재고오류 해결 평균 93초
```

등을 측정할 수 있다.

감이 아니라 실제 데이터를 바탕으로 UX 개선.

---

# 133. 사용자 제안 데이터

```ts
interface UserSuggestionContext {
  screenId: string

  screenVersion: string

  route: string

  appVersion: string

  userRole: string

  activeFilters?: string[]

  gridLayoutVersion?: string
}
```

실제 업무 Data 자체는 기본 첨부하지 않는다.

---

# 134. 사용자 제안과 AI

AI는:

- 요약
- 분류
- 중복 제안 탐색
- 영향 Screen 식별
- 개선 후보 요약

까지.

자동으로 제품 Requirement를 확정하지 않는다.

---

# 135. 기술부채 관리

KBX 우회 시 반드시 기록한다.

```ts
/**
 * KBX-EXCEPTION:
 * WMS Bluetooth Scale integration requires
 * direct input lifecycle handling.
 *
 * Review: 2026-Q4
 */
```

---

# 136. 공통화 판단

3번 반복되었다고 모든 코드를 즉시 공통화하지 않는다.

다음 세 조건을 같이 본다.

```text
동일한 의미인가?
동일한 변화 이유를 가지는가?
동일 UX 계약을 가져야 하는가?
```

셋이 맞을 때 공통화한다.

---

# 137. 정공법

빠른 구현을 위해 다음을 생략하지 않는다.

```text
Server Validation
DB Constraint
Idempotency
Audit
Concurrency
Import Staging
Error Contract
```

이 영역은 나중에 붙이기가 훨씬 어렵다.

---

# 138. 반대로 초기부터 만들지 않아도 되는 것

과유불급 방지:

```text
초대형 Low-code UI Builder
모든 화면 Runtime JSON Engine
자체 Grid Engine
자체 DatePicker
자체 UI Framework
AI가 전체 화면 생성하는 시스템
```

검증된 PrimeVue/AG Grid를 감싸고 업무 표준에 집중한다.

---

# 139. 초기 구현 우선순위

가장 먼저 완성도가 높아야 하는 Component:

```text
1. KbxButton
2. KbxInput
3. KbxDateField
4. KbxLookup
5. KbxCommandBar
6. KbxSearchPanel
7. KbxDataGrid
8. KbxBulkActionBar
9. KbxStatus
10. KbxListPage
11. KbxTransactionPage
12. KbxExcelImport
```

이 12개가 전체 제품 UX의 중심이다.

---

# 140. 첫 Reference 구현 화면

KBX 자체를 개발할 때 Component Demo보다 실제 업무화면을 동시에 만든다.

권장 첫 5개:

```text
OMS 주문관리
OMS 주문등록
ERP 품목관리
ERP 재고현황
WMS Picking Mobile
```

이 다섯 화면이면:

- 조회
- CRUD
- Transaction
- Master-Detail
- Bulk
- Lookup
- Grid
- Excel
- Keyboard
- Desktop
- Mobile/PDA

대부분의 표준을 검증할 수 있다.

---

# 141. 첫 번째 Golden Screen

가장 먼저 완성시킬 화면은:

```text
OMS 주문관리
OMS-ORD-001
```

로 권장한다.

이 화면에서:

```text
Search
Grid
Bulk
Drawer
Status
Excel
Keyboard
AI
Help
User Suggestion
```

을 모두 검증할 수 있기 때문이다.

이 화면을 **Golden Screen**으로 삼는다.

---

# 142. 두 번째 Golden Screen

```text
OMS 주문등록
OMS-ORD-002
```

여기서:

```text
Form
Lookup
Header-Detail
Fast Entry
Validation
Save
Concurrency
Audit
```

을 검증한다.

---

# 143. 세 번째 Golden Screen

```text
WMS Picking
WMS-PICK-001
```

여기서:

```text
Barcode
Touch
Network
Idempotency
Exception
Realtime Feedback
```

을 검증한다.

---

# 144. 이 세 화면이 통과하면

나머지 화면은 상당 부분 반복 구현이 된다.

```text
주문관리
→ 출고관리
→ 구매조회
→ 입고조회

주문등록
→ 구매등록
→ 재고이동
→ 입고등록

Picking
→ Putaway
→ Checking
→ Inventory Count
```

패턴이 확장된다.

---

# 145. 최종 Architecture

```text
┌─────────────────────────────────────┐
│ Vertical Slice                      │
│ Order / Shipment / Inventory        │
└──────────────────┬──────────────────┘
                   │
                   │ Screen Definition
                   │ Commands / Fields
                   ▼
┌─────────────────────────────────────┐
│ @kbx/ui                             │
│                                     │
│ Templates                           │
│ Components                          │
│ Grid                                │
│ Keyboard                            │
│ Excel                               │
│ Feedback                            │
│ Audit / Help / AI                   │
└──────────────────┬──────────────────┘
                   │
       ┌───────────┴───────────┐
       ▼                       ▼
   PrimeVue                  AG Grid
```

Backend:

```text
Vue Screen
   ↓
FastEndpoint
   ↓
Application Handler
   ↓
Domain
   ↓
PostgreSQL

   ├─ Audit
   ├─ Outbox
   ├─ Inbox
   ├─ Hangfire
   └─ Projection
```

---

# 146. 핵심 경계

가장 중요한 한 줄은 이것이다.

> **KBX가 사용방법을 결정하고, Domain이 가능한 업무를 결정한다.**

예를 들어:

```text
F8을 누르면 저장한다.
```

는 KBX.

```text
출고완료 주문은 수정할 수 없다.
```

는 Domain.

```text
오류는 해당 Cell 아래에 표시한다.
```

는 KBX.

```text
현재 재고가 8개뿐이어서 10개 출고할 수 없다.
```

는 Domain.

이 경계가 무너지지 않아야 한다.

---

# 147. 구현 완료 조건

`@kbx/ui`의 첫 번째 실사용 버전은 최소 다음 조건을 만족해야 한다.

### 구조

- PrimeVue 직접 노출 차단
- AG Grid 직접 노출 차단
- Screen Definition 존재
- Field Dictionary 존재

### UX

- F2/F3/F8
- Tab/Enter
- Mouse
- Focus Restore
- Loading
- Empty
- Error
- Bulk Action

### 데이터

- Zod
- Server Validation
- DB Constraint
- Concurrency

### Excel

- Download
- Template
- Upload
- Mapping
- Validation
- Staging
- Result

### 안정성

- Idempotency
- Audit
- Outbox
- Job
- Error Contract

### AX

- Structured AI Proposal
- Domain Entity Resolve
- User Permission
- Server Validation
- Audit

### 검증

- Vitest
- Playwright
- API Contract Test

이를 충족한 후부터 KBX를 OMS·WMS·ERP의 실제 공통 기반으로 확장한다.

---

# 148. 최종 구현 원칙

KBX의 성공은 컴포넌트 개수로 판단하지 않는다.

성공한 KBX는 신규 화면을 만드는 개발자가 다음을 고민하지 않게 한다.

```text
버튼 높이가 몇 px인가?
조회 버튼은 어디에 놓는가?
F2를 어떻게 구현하는가?
Grid 금액은 어떻게 정렬하는가?
Excel Upload는 어떻게 만드는가?
오류 메시지를 어디에 보여주는가?
AI Action을 어떻게 Confirm하는가?
```

그 대신 개발자는 다음만 고민한다.

```text
이 업무의 정상 흐름은 무엇인가?
어떤 예외가 존재하는가?
사용자가 정말 입력해야 할 값은 무엇인가?
어떤 판단을 시스템이 대신할 수 있는가?
정합성을 어디서 보장해야 하는가?
```

그 상태가 KBX가 가져야 할 최종 기술적 가치다.