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