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

3572 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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가 가져야 할 최종 기술적 가치다.