47 KiB
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가 가져야 할 최종 기술적 가치다.