feat(components): implement 4-layer input component architecture and 52-section specification with harness CLI v3.0 verification
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been cancelled
Validators (Pushes and Pull Requests) / Security & Secrets (push) Has been cancelled
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been cancelled
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been cancelled
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Has been cancelled
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been cancelled
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been cancelled
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Has been cancelled
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 10s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 10s

This commit is contained in:
2026-07-26 01:43:50 +09:00
parent 1e37e715e9
commit 70d9029184
7 changed files with 456 additions and 180 deletions
@@ -0,0 +1,22 @@
<!-- Business Composite Component Layer -->
<template>
<div class="business-address-editor border rounded p-4 bg-slate-50">
<h4 class="font-bold text-sm text-slate-800 mb-2">주소 편집기 (AddressEditor)</h4>
<div class="flex gap-2 mb-2">
<input type="text" :value="postalCode" placeholder="우편번호" readonly class="w-32 border px-2 py-1 bg-white text-xs" />
<button type="button" class="bg-blue-600 text-white text-xs px-3 py-1 rounded" @click="$emit('search-postal')">우편번호 검색</button>
</div>
<input type="text" :value="address1" placeholder="기본주소" readonly class="w-full border px-2 py-1 bg-white text-xs mb-2" />
<input type="text" :value="address2" placeholder="상세주소 입력" class="w-full border px-2 py-1 bg-white text-xs" @input="$emit('update:address2', ($event.target as HTMLInputElement).value)" />
</div>
</template>
<script setup lang="ts">
defineProps<{
postalCode?: string;
address1?: string;
address2?: string;
}>();
defineEmits(['search-postal', 'update:address2']);
</script>
@@ -0,0 +1,46 @@
<!-- Domain Field Component Layer -->
<template>
<div class="domain-quantity-field">
<label class="domain-label">{{ label || '수량' }} <span v-if="required" class="req">*</span></label>
<div class="input-unit-group flex gap-2">
<input
type="text"
:value="amount"
placeholder="0"
class="quantity-input flex-1 border rounded px-3 py-1 text-right font-mono"
@input="handleAmountInput"
/>
<select :value="unitCode" class="unit-select border rounded px-2" @change="handleUnitChange">
<option v-for="unit in availableUnits" :key="unit" :value="unit">{{ unit }}</option>
</select>
</div>
<div v-if="stockContext" class="stock-info text-xs text-slate-500 mt-1">
가용재고: <strong>{{ stockContext.availableAmount }} {{ unitCode }}</strong>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
label?: string;
required?: boolean;
amount: string;
unitCode: string;
availableUnits: string[];
stockContext?: { availableAmount: string };
}>();
const emit = defineEmits(['update:amount', 'update:unitCode']);
const handleAmountInput = (e: Event) => {
emit('update:amount', (e.target as HTMLInputElement).value);
};
const handleUnitChange = (e: Event) => {
emit('update:unitCode', (e.target as HTMLSelectElement).value);
};
</script>
<style scoped>
.req { color: #DC2626; }
</style>
@@ -0,0 +1,59 @@
<!-- Typed Field Component Layer -->
<template>
<div class="typed-string-field">
<label v-if="label" :for="id" class="field-label">{{ label }}</label>
<input
:id="id"
type="text"
:value="modelValue"
:placeholder="placeholder"
:readonly="readonly"
:disabled="disabled"
class="string-input"
@input="handleInput"
/>
<div v-if="errorMessage" class="field-error-msg">{{ errorMessage }}</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
id?: string;
label?: string;
modelValue?: string;
placeholder?: string;
readonly?: boolean;
disabled?: boolean;
errorMessage?: string;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const handleInput = (e: Event) => {
const val = (e.target as HTMLInputElement).value;
emit('update:modelValue', val);
emit('change', val);
};
</script>
<style scoped>
.field-label {
display: block;
font-size: 12px;
font-weight: 700;
color: #334155;
margin-bottom: 4px;
}
.string-input {
width: 100%;
height: 36px;
padding: 0 10px;
border: 1px solid #CBD5E1;
border-radius: 4px;
}
.field-error-msg {
font-size: 11px;
color: #DC2626;
margin-top: 2px;
}
</style>
@@ -0,0 +1,39 @@
<!-- Primitive UI Components Layer -->
<template>
<div class="primitive-input-wrapper">
<input
:id="id"
:type="type || 'text'"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
class="primitive-input"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</div>
</template>
<script setup lang="ts">
defineProps<{
id?: string;
type?: string;
modelValue?: string | number;
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
}>();
defineEmits(['update:modelValue']);
</script>
<style scoped>
.primitive-input {
width: 100%;
height: 36px;
padding: 0 12px;
border: 1px solid #CBD5E1;
border-radius: 4px;
font-size: 13px;
}
</style>
@@ -1,6 +1,6 @@
/**
* src/frontend/src/types/enterpriseTemplateContracts.ts
* OMS·WMS·ERP 공통 CRUD 화면 템플릿 표준 계약 및 타입 정의
* OMS·WMS·ERP 공통 CRUD 화면 템플릿 & 입력 컴포넌트 4계층 표준 계약 및 타입 정의
* Specification: docs/ENTERPRISE_CRUD_DESIGN_SPECIFICATION.md
* Roadmap: docs/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md
*/
@@ -19,20 +19,41 @@ export type EnterpriseTemplateId =
| 'TPL-APPROVAL-01' // 승인·반려
| 'TPL-HISTORY-01'; // 변경 이력
/** 입력 필드 공통 상태 계약 */
/** 13가지 입력 필드 공통 상태 계약 */
export type FieldStatus =
| 'idle'
| 'focused'
| 'dirty'
| 'validating'
| 'valid'
| 'warning'
| 'invalid'
| 'saving'
| 'saved'
| 'conflict'
| 'blocked'
| 'readonly'
| 'disabled';
| 'disabled'
| 'blocked';
/** 8가지 입력 값 출처 계약 */
export type ValueSource =
| 'user'
| 'default'
| 'scanner'
| 'import'
| 'integration'
| 'system'
| 'calculation'
| 'ai';
export interface FieldMessage {
code: string;
severity: 'error' | 'warning' | 'information';
message: string;
remediation?: string;
blocking: boolean;
rejectedValue?: unknown;
}
export interface FieldError {
code: string;
@@ -49,22 +70,25 @@ export interface FieldWarning {
message: string;
}
export interface FieldState<T = unknown> {
value: T | null;
export interface FieldState<TValue = unknown> {
value: TValue | null;
initialValue: TValue | null;
rawValue?: string;
displayValue?: string;
initialValue: T | null;
normalizedValue?: TValue | null;
status: FieldStatus;
dirty: boolean;
touched: boolean;
dirty: boolean;
required: boolean;
source: 'user' | 'scanner' | 'import' | 'api' | 'system' | 'ai';
source: ValueSource;
sourceReference?: string;
confidence?: number;
errors: FieldError[];
warnings: FieldWarning[];
recordVersion?: string;
errors: FieldMessage[];
warnings: FieldMessage[];
information: FieldMessage[];
lastChangedAt?: string;
lastChangedBy?: string;
recordVersion?: number;
}
/** Grid 컬럼 정의 계약 */
@@ -94,7 +118,13 @@ export interface GridColumnDefinition {
permission?: string;
}
/** 변경 감지 및 동시 수정 3-Way Diff 모델 */
/** 4계층 컴포넌트 아키텍처 분류 */
export type ComponentLayer =
| 'primitive'
| 'typed-field'
| 'domain-field'
| 'business-composite';
export interface ChangeFieldDiff {
path: string;
before: unknown;
@@ -110,7 +140,6 @@ export interface ChangeSet {
reasonText?: string;
}
/** 표준 API 오류 응답 계약 */
export interface ApiErrorResponse {
code: string;
message: string;
@@ -135,7 +164,6 @@ export interface ApiErrorResponse {
occurredAt: string;
}
/** 감사 및 이력 이벤트 계약 */
export interface AuditEvent {
eventId: string;
correlationId: string;