# 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( 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 { 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 } ``` --- # 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 { id: TId code: string displayName: string secondaryText?: string status?: string metadata?: Record } ``` --- # 20. Lookup Search Request ```ts export interface KbxLookupSearchRequest { query?: string page: number pageSize: number filters?: Record } ``` --- # 21. Lookup Response ```ts export interface KbxLookupSearchResult { items: KbxLookupItem[] totalCount: number } ``` --- # 22. Lookup Provider ```ts export interface KbxLookupProvider { search( request: KbxLookupSearchRequest ): Promise> resolveById( id: TId ): Promise | null> resolveByCode( code: string ): Promise | null> } ``` --- # 23. Customer Provider ```ts export class CustomerLookupProvider implements KbxLookupProvider { 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 ``` 화면에서 다음을 직접 작성하지 않는다. ```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 { 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 { rows: T[] columns: KbxGridColumn[] rowKey: keyof T & string loading?: boolean selection?: | 'none' | 'single' | 'multiple' editable?: boolean clipboard?: boolean personalization?: boolean exportable?: boolean density?: 'compact' | 'comfortable' summary?: KbxGridSummary[] emptyText?: string } ``` --- # 31. Grid Event ```ts export interface KbxGridEvents { 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[] = [ { 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 { 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 ``` --- # 45. 더 강한 Schema 방식도 가능 단순 List에서는: ```vue ``` 만으로도 생성 가능하다. 하지만 모든 화면을 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 { 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 ``` --- # 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 Lines); ``` --- # 58. Endpoint ```csharp public sealed class RegisterOrderEndpoint : Endpoint { 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 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 ``` 이 정도가 Vertical Slice Page의 권장 복잡도다. --- # 97. `useOrderSearch` 업무 orchestration: ```ts export function useOrderSearch() { const search = reactive(createDefaultOrderSearch()) const selection = ref([]) 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 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가 가져야 할 최종 기술적 가치다.