Files
KArtSell.Aegis/docs/Design/KBX Implementation Contract v1.0.md
T

47 KiB
Raw Blame History

KBX Implementation Contract v1.0

@kbx/ui · Vue 3 · TypeScript · AG Grid · PrimeVue · FastEndpoints · Zod


1. 목표

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

업무 화면 개발자
        │
        │ 업무 의미만 정의
        ▼
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 구조:

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

3. @kbx/ui

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 계약을 둔다.

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한다.

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

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

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

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


6. Screen ID 규칙

{MODULE}-{AREA}-{NUMBER}

예:

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을 가진다.

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

8. 기본 Screen 계약

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

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

사용:

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를 사용한다.

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

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을 대신하지 않는다.

예:

Field Definition

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

정도를 정의한다.

반면:

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

는 Domain Rule이다.

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


13. Command Contract

export type KbxCommandGroup =
  | 'query'
  | 'edit'
  | 'workflow'
  | 'output'
  | 'more'
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

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를 직접 호출하지 않는다.

Command Definition
        ↓
Command Handler
        ↓
Application API

예:

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

16. Keyboard Manager

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

공통 Manager:

export interface KbxShortcut {
  key: string

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

  priority?: number

  enabled?: () => boolean

  execute(): void | Promise<void>
}

17. Scope 우선순위

Editor
 ↓
Dialog
 ↓
Grid
 ↓
Page
 ↓
Application

예:

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


18. 기본 Shortcut

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

Browser Shortcut Override 금지:

F5
Ctrl+R
Ctrl+L
Ctrl+T
Ctrl+W

19. KbxLookup Contract

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

  code: string

  displayName: string

  secondaryText?: string

  status?: string

  metadata?: Record<string, unknown>
}

20. Lookup Search Request

export interface KbxLookupSearchRequest {
  query?: string

  page: number
  pageSize: number

  filters?: Record<string, unknown>
}

21. Lookup Response

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

  totalCount: number
}

22. Lookup Provider

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

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 사용

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

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

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

25. Lookup Registry

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

KbxLookup:

entity="customer"

이면 Registry에서 Provider를 찾는다.


26. Lookup Server Endpoint

예:

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를 반환하면 안 된다.

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

export type KbxGridType =
  | 'text'
  | 'code'
  | 'integer'
  | 'decimal'
  | 'quantity'
  | 'money'
  | 'percent'
  | 'date'
  | 'datetime'
  | 'boolean'
  | 'status'
  | 'lookup'
  | 'link'
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

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

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에서 중앙 통제한다.

예:

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. 업무 화면에서 금지

gridOptions = {
  ...
}

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

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


34. 주문 Grid 예

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

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'

이면 자동:

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

type: 'date'

이면:

날짜 정렬
날짜 포맷
날짜 Filter

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


37. Row Selection

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

interface KbxSelectionState<TId> {
  selectedIds: TId[]

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

  excludedIds?: TId[]
}

38. 매우 중요한 대량 선택

10만 건 조회 결과에서:

전체 선택

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

다음 개념을 지원한다.

현재 페이지 선택

또는

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

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


39. Server-side Bulk Request

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

  ids?: string[]

  filter?: OrderSearchFilter

  excludedIds?: string[]
}

대량 OMS에서 중요하다.


40. KbxSearchPanel Contract

export type KbxSearchFieldType =
  | 'text'
  | 'date'
  | 'dateRange'
  | 'select'
  | 'lookup'
  | 'checkbox'
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

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가 책임진다.

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

개념 사용:

<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에서는:

<KbxListPage
  :definition="definition"
/>

만으로도 생성 가능하다.

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


46. 권장 비율

70%
표준 Template + Schema

20%
Template + explicit Vue slots

10%
Custom 업무 UI

47. KbxTransactionPage

Transaction의 표준 구조:

Page Header
Command Bar
Header Form
Detail Grid
Summary
Status
Audit

48. Transaction Context

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

  lines: TLine[]

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

  status: string

  dirty: boolean

  version?: number
}

49. 주문등록 Type

interface OrderHeaderForm {
  orderDate: string

  customerId: string | null

  warehouseId: string | null

  receiverName: string

  phone: string

  postalCode: string

  address1: string

  address2?: string
}
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

<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. 계산값

예:

금액 = 수량 × 단가

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

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

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


52. Zod 역할

Zod:

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

적합.


53. Zod 주문 Schema

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

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

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

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


56. Server Domain Validation

Server:

Endpoint Validation
       ↓
Application Validation
       ↓
Domain Rule
       ↓
Database Constraint

예:

quantity > 0

는 Client에서도 확인 가능.

하지만:

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

는 Server가 최종 책임진다.


57. FastEndpoints Request

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

58. Endpoint

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 표준 오류 계약을 만든다.

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

60. TypeScript

export interface KbxValidationError {
  field?: string

  rowKey?: string

  code: string

  message: string
}
export interface KbxValidationProblem {
  type: 'validation'

  title: string

  errors: KbxValidationError[]
}

61. Grid Row 오류

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

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

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


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

lines[7].quantity

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

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


63. Business Problem Contract

Field 오류가 아닌 업무 오류:

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

64. Conflict Contract

Optimistic Concurrency:

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

Frontend:

최신 내용 보기

Action 제공.


65. Version

Request:

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

Server:

현재 version = 17

이면 Update 거부.

Silent overwrite 금지.


66. Database Constraint

정합성 마지막 방어선.

예:

UNIQUE
FOREIGN KEY
CHECK
NOT NULL

Application Validation만 믿지 않는다.


67. PostgreSQL Transaction

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

Dapper 사용 여부와 관계없이:

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

68. Outbox

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

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

동일 DB Transaction.


69. UI Integration Status

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

주문 확정
완료

WMS 연계
전송 대기

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


70. TanStack Mutation

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

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

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

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

71. 공통 Error Handler

handleKbxApiError()

가 다음을 분기한다.

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

export interface KbxImportDefinition {
  id: string

  screenId: string

  entity: string

  fields: KbxFieldDefinition[]

  allowCreate: boolean

  allowUpdate: boolean

  maxFileSize?: number

  maxRows?: number
}

74. Mapping

export interface KbxImportMapping {
  sourceColumn: string

  targetField: string | null

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

  confidence?: number
}

75. Import Job

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

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

76. Import Session

예:

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

77. Import Row

Staging:

ImportSessionId
RowNumber
RawData
NormalizedData
ValidationState
Errors

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


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

필수:

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

때문이다.


79. Import Commit

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

결과:

17,894 성공
336 오류

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


80. Idempotency

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

예:

Idempotency-Key

또는 업무 Key.

특히:

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

에 중요하다.


81. KbxStatus Contract

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

export interface KbxStatusDefinition {
  value: string

  label: string

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

82. OMS 예

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

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

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

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

83. State Machine

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

Draft
 ↓
Confirmed
 ↓
Allocated
 ↓
Picking
 ↓
Checked
 ↓
Shipped

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


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

예:

Draft

[저장]
[확정]

Picking

[피킹작업 보기]

Shipped

[변경이력]

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


85. Permission

Frontend:

UX 표현.

Backend:

최종 Enforcement.

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

86. Permission에 따른 Command

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

KbxCommandBar가 자동 처리한다.


87. Audit Contract

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에서:

CorrelationId
RequestId
CommandId
EventId
OutboxId
JobId

추적 가능하게 한다.


89. Serilog / OTel 연결

업무 요청 하나를:

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

까지 Correlation 가능하게 한다.

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


90. KbxAiContext

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

export interface KbxAiScreenContext {
  screenId: string

  screenVersion: string

  entityId?: string

  selectedIds?: string[]

  filters?: Record<string, unknown>

  allowedCapabilities: string[]
}

91. AI Action

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 실행

금지:

LLM text
→ Parse
→ DB Update

권장:

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

93. AI Entity Resolve

예:

AI:

대한상사

실행 전에:

CustomerId

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

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


94. AI와 화면 UX

AI가 제안:

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

화면:

AI 제안

3건

ORD001
서울 → 인천

ORD002
서울 → 인천

ORD003
서울 → 인천

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

공통 KbxProposalPanel 사용.


95. Screen Definition 확장 예

OMS 주문조회 전체 계약:

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

<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:

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가 많아지면:

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

Registry 형태로 분리한다.

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


99. FastEndpoints Vertical Slice

예:

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

100. Read Query

Dapper 활용:

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

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 방지

금지:

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

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


103. Client-side Join도 최소화

Frontend가:

orders
customers
items
warehouses

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

Server Read Model 사용.


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

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

필요한 역정규화

무분별한 데이터 복제

를 구분한다.


105. Optimistic UI 제한

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

적합:

즐겨찾기
Grid Layout
개인 Preference

주의:

재고
출고
입고
주문상태

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


106. Loading UX

Mutation 중 버튼:

[저장 중...]

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

Double Submit 방지.


107. Job과 Command 구분

즉시 처리:

주문 1건 저장

Command.

대량:

Excel 30,000건 Import

Job.

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


108. Job Threshold

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

Server Policy:

BulkOperationPolicy

가 결정한다.

예:

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

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


109. WMS Barcode Contract

export interface KbxBarcodeEvent {
  rawValue: string

  normalizedValue: string

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

  occurredAt: number
}

110. Scan Command

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

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

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

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


112. Offline 가능 Command

각 Command에 명시:

interface WmsCommandPolicy {
  offlineAllowed: boolean

  idempotent: boolean

  requiresServerValidation: boolean
}

113. 사용자 Preference

저장 가능:

Grid Column
Column Width
Sort
Density
Search Defaults
Favorite
Page Size

Key:

Tenant
User
ScreenId
ScreenVersion

114. Screen Version이 필요한 이유

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

Preference Migration 전략이 필요하다.


115. Screen Definition은 코드 우선

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

금지에 가까운 초기 설계:

모든 화면을 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 생성 가능.

[
  {
    "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가 새 화면을 만들 때:

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

을 먼저 제공한다.


119. Component Manifest

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

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


120. ESLint 규칙 권장

가능하다면 Custom ESLint Rule로:

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

직접 import 경고 또는 금지.

예외:

// kbx-exception: ...

명시 요구.

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


121. Dependency Rule

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

반대 방향 금지.

@kbx/ui
 ↓
OMS module

금지.


122. 테스트 구조

packages/kbx-ui/
 └─ tests/

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

123. KbxLookup Contract Test

F2
→ Popup Open

Esc
→ Close

Enter
→ Resolve

Arrow
→ Move

Enter
→ Select

Select
→ Focus Restore

Vitest로 고정.


124. KbxDataGrid Contract Test

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

검증.


125. Playwright 주문등록

Mouse 없이:

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

E2E 테스트.


126. Playwright OMS Bulk

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

127. Excel E2E

Upload
↓
Mapping
↓
Validation
↓
Commit
↓
Result

오류 Scenario 포함.


128. WMS E2E

가상 Scanner Event:

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

중복 Scan도 별도 Scenario로 검증.


129. API Contract Test

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

예:

KbxValidationProblem
KbxBusinessProblem
KbxConflictProblem

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


130. 운영 Telemetry

Screen 단위:

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에서 금지

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

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

업무 상태와 Event Metadata 중심.


132. UX 개선 근거

Telemetry를 통해:

OMS-ORD-001

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

등을 측정할 수 있다.

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


133. 사용자 제안 데이터

interface UserSuggestionContext {
  screenId: string

  screenVersion: string

  route: string

  appVersion: string

  userRole: string

  activeFilters?: string[]

  gridLayoutVersion?: string
}

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


134. 사용자 제안과 AI

AI는:

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

까지.

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


135. 기술부채 관리

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

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

136. 공통화 판단

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

다음 세 조건을 같이 본다.

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

셋이 맞을 때 공통화한다.


137. 정공법

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

Server Validation
DB Constraint
Idempotency
Audit
Concurrency
Import Staging
Error Contract

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


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

과유불급 방지:

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

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


139. 초기 구현 우선순위

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

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개:

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

이 다섯 화면이면:

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

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


141. 첫 번째 Golden Screen

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

OMS 주문관리
OMS-ORD-001

로 권장한다.

이 화면에서:

Search
Grid
Bulk
Drawer
Status
Excel
Keyboard
AI
Help
User Suggestion

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

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


142. 두 번째 Golden Screen

OMS 주문등록
OMS-ORD-002

여기서:

Form
Lookup
Header-Detail
Fast Entry
Validation
Save
Concurrency
Audit

을 검증한다.


143. 세 번째 Golden Screen

WMS Picking
WMS-PICK-001

여기서:

Barcode
Touch
Network
Idempotency
Exception
Realtime Feedback

을 검증한다.


144. 이 세 화면이 통과하면

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

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

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

Picking
→ Putaway
→ Checking
→ Inventory Count

패턴이 확장된다.


145. 최종 Architecture

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

Backend:

Vue Screen
   ↓
FastEndpoint
   ↓
Application Handler
   ↓
Domain
   ↓
PostgreSQL

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

146. 핵심 경계

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

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

예를 들어:

F8을 누르면 저장한다.

는 KBX.

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

는 Domain.

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

는 KBX.

현재 재고가 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는 신규 화면을 만드는 개발자가 다음을 고민하지 않게 한다.

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

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

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

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