feat(wbs): expand full 52-section component & 11-template WBS roadmap and implement core Sprint 1-2 components
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-26 01:46:55 +09:00
parent 4d2ed60c6f
commit d286952392
7 changed files with 416 additions and 41 deletions
@@ -0,0 +1,41 @@
<!-- Business Composite Component Layer: AISuggestedField (WBS-COMP-4.2) -->
<template>
<div class="business-ai-suggested border border-indigo-200 bg-indigo-50/50 p-3 rounded-md flex flex-col gap-2">
<div class="flex justify-between items-center text-xs">
<span class="font-bold text-indigo-900 flex items-center gap-1">
AI AX 추천 초안 (<span class="font-mono text-indigo-600">{{ riskLevel }}</span>)
</span>
<span class="text-[10px] text-indigo-600 bg-indigo-100 font-bold px-2 py-0.5 rounded">
신뢰도 {{ (confidence * 100).toFixed(0) }}%
</span>
</div>
<div class="text-xs text-slate-800 bg-white p-2 rounded border border-indigo-100 flex justify-between items-center">
<div>
<span class="text-slate-500 text-[11px]">제안 :</span>
<strong class="text-indigo-950 font-semibold ml-1">{{ suggestedValue }}</strong>
</div>
<div class="flex gap-1">
<button type="button" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs px-2.5 py-1 rounded transition" @click="$emit('accept')">
적용
</button>
<button type="button" class="bg-slate-200 hover:bg-slate-300 text-slate-700 text-xs px-2 py-1 rounded transition" @click="$emit('reject')">
거절
</button>
</div>
</div>
<div v-if="rationale" class="text-[11px] text-indigo-800/90 leading-tight">
💡 근거: {{ rationale }}
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
suggestedValue: string;
confidence: number;
rationale?: string;
riskLevel?: 'R0' | 'R1' | 'R2' | 'R3' | 'R4';
}>();
defineEmits(['accept', 'reject']);
</script>
@@ -0,0 +1,61 @@
<!-- Domain Field Component Layer: BarcodeInput (WBS-COMP-3.3) -->
<template>
<div class="domain-barcode-input bg-slate-900 p-3 rounded-md text-white flex flex-col gap-2 shadow-md">
<div class="flex justify-between items-center text-xs">
<span class="font-bold flex items-center gap-1 text-emerald-400">
<i class="ti ti-barcode"></i> 산업용 바코드 스캐너 입력
</span>
<span class="text-[10px] px-2 py-0.5 rounded bg-emerald-950 text-emerald-300 font-mono">
100ms 연속 스캔 모드
</span>
</div>
<div class="relative">
<input
ref="barcodeInputRef"
type="text"
:value="modelValue"
placeholder="스캐너 빔을 바코드에 조준하세요..."
class="w-full h-10 px-3 pr-20 bg-slate-800 border border-slate-700 rounded text-xs text-emerald-300 font-mono focus:border-emerald-500 focus:outline-none"
@input="handleInput"
@keyup.enter="handleScanCommit"
/>
<button
type="button"
class="absolute right-1 top-1 bottom-1 px-3 bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs rounded transition"
@click="handleScanCommit"
>
스캔 확정
</button>
</div>
<div v-if="lastScannedCode" class="text-[11px] text-slate-300 flex justify-between bg-slate-800/80 px-2 py-1 rounded">
<span>최근 스캔: <strong class="text-emerald-400 font-mono">{{ lastScannedCode }}</strong></span>
<span class="text-slate-400 text-[10px]">{{ lastScannedTime }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps<{
modelValue?: string;
}>();
const emit = defineEmits(['update:modelValue', 'scan-committed']);
const barcodeInputRef = ref<HTMLInputElement | null>(null);
const lastScannedCode = ref('');
const lastScannedTime = ref('');
const handleInput = (e: Event) => {
emit('update:modelValue', (e.target as HTMLInputElement).value);
};
const handleScanCommit = () => {
if (!props.modelValue) return;
lastScannedCode.value = props.modelValue;
lastScannedTime.value = new Date().toLocaleTimeString('ko-KR');
emit('scan-committed', props.modelValue);
emit('update:modelValue', '');
barcodeInputRef.value?.focus();
};
</script>
@@ -0,0 +1,72 @@
<!-- Domain Field Component Layer: MoneyField (WBS-COMP-3.2) -->
<template>
<div class="domain-money-field flex flex-col gap-1">
<label v-if="label" class="text-xs font-bold text-slate-700">
{{ label }} <span v-if="required" class="text-red-500">*</span>
</label>
<div class="flex gap-2">
<input
type="text"
:value="formattedAmount"
placeholder="0"
:readonly="readonly"
:disabled="disabled"
class="flex-1 h-9 px-3 border border-slate-300 rounded text-right font-mono text-xs text-slate-800 font-bold focus:ring-2 focus:ring-blue-500 focus:outline-none"
@input="handleInput"
/>
<select
:value="currencyCode || 'KRW'"
:disabled="currencyReadonly || disabled"
class="h-9 px-2 border border-slate-300 rounded bg-slate-50 text-xs font-bold text-slate-700"
@change="handleCurrencyChange"
>
<option value="KRW">KRW ()</option>
<option value="USD">USD ($)</option>
<option value="EUR">EUR ()</option>
<option value="JPY">JPY (¥)</option>
</select>
</div>
<div v-if="formattedKrwSummary" class="text-[11px] text-blue-600 font-medium mt-0.5">
한화 환산: {{ formattedKrwSummary }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
label?: string;
amount: string;
currencyCode?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
currencyReadonly?: boolean;
}>();
const emit = defineEmits(['update:amount', 'update:currencyCode']);
const formattedAmount = computed(() => {
if (!props.amount) return '';
const num = Number(props.amount.replace(/,/g, ''));
if (isNaN(num)) return props.amount;
return num.toLocaleString('ko-KR');
});
const formattedKrwSummary = computed(() => {
if (!props.amount || props.currencyCode === 'KRW') return '';
const num = Number(props.amount.replace(/,/g, ''));
if (isNaN(num)) return '';
return (num * 1350).toLocaleString('ko-KR'); // 예시 환율
});
const handleInput = (e: Event) => {
const raw = (e.target as HTMLInputElement).value.replace(/,/g, '');
emit('update:amount', raw);
};
const handleCurrencyChange = (e: Event) => {
emit('update:currencyCode', (e.target as HTMLSelectElement).value);
};
</script>
@@ -0,0 +1,59 @@
<!-- Typed Field Component Layer: DateField (WBS-COMP-2.3) -->
<template>
<div class="typed-date-field flex flex-col gap-1">
<label v-if="label" :for="id" class="text-xs font-bold text-slate-700">
{{ label }} <span v-if="required" class="text-red-500">*</span>
</label>
<div class="flex gap-2 items-center">
<input
:id="id"
type="date"
:value="modelValue"
:min="minDate"
:max="maxDate"
:readonly="readonly"
:disabled="disabled"
class="h-9 px-3 border border-slate-300 rounded text-xs text-slate-800 focus:ring-2 focus:ring-blue-500 focus:outline-none"
@change="handleChange"
/>
<button
v-if="showTodayBtn && !readonly && !disabled"
type="button"
class="h-9 px-2.5 bg-slate-100 hover:bg-slate-200 border border-slate-300 rounded text-xs text-slate-700 font-medium transition"
@click="setToday"
>
오늘
</button>
</div>
<div v-if="errorMessage" class="text-[11px] text-red-600 mt-0.5">{{ errorMessage }}</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
id?: string;
label?: string;
modelValue?: string;
minDate?: string;
maxDate?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
showTodayBtn?: boolean;
errorMessage?: string;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const handleChange = (e: Event) => {
const val = (e.target as HTMLInputElement).value;
emit('update:modelValue', val);
emit('change', val);
};
const setToday = () => {
const todayIso = new Date().toISOString().substring(0, 10);
emit('update:modelValue', todayIso);
emit('change', todayIso);
};
</script>
@@ -0,0 +1,59 @@
<!-- Typed Field Component Layer: NumberField (WBS-COMP-2.2) -->
<template>
<div class="typed-number-field flex flex-col gap-1">
<label v-if="label" :for="id" class="text-xs font-bold text-slate-700">
{{ label }} <span v-if="required" class="text-red-500">*</span>
</label>
<div class="relative flex items-center">
<input
:id="id"
type="text"
:value="displayFormattedValue"
:placeholder="placeholder || '0'"
:readonly="readonly"
:disabled="disabled"
class="w-full h-9 px-3 border border-slate-300 rounded text-right font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
@input="handleInput"
@blur="handleBlur"
/>
</div>
<div v-if="errorMessage" class="text-[11px] text-red-600 mt-0.5">{{ errorMessage }}</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
const props = defineProps<{
id?: string;
label?: string;
modelValue?: string | number;
placeholder?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
grouping?: boolean;
errorMessage?: string;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
const isFocused = ref(false);
const displayFormattedValue = computed(() => {
if (props.modelValue === null || props.modelValue === undefined || props.modelValue === '') return '';
if (isFocused.value) return String(props.modelValue);
const num = Number(props.modelValue);
if (isNaN(num)) return String(props.modelValue);
return props.grouping !== false ? num.toLocaleString('ko-KR') : String(num);
});
const handleInput = (e: Event) => {
const val = (e.target as HTMLInputElement).value.replace(/,/g, '');
emit('update:modelValue', val);
emit('change', val);
};
const handleBlur = () => {
isFocused.value = false;
};
</script>
@@ -0,0 +1,64 @@
<!-- Enterprise Template Implementation: TPL-LIST-01 (목록·검색 템플릿) -->
<template>
<div class="tpl-list-01-container flex flex-col h-full w-full bg-slate-100 p-4 gap-3 overflow-hidden">
<!-- Page Header -->
<div class="page-header flex justify-between items-center bg-white p-3.5 rounded-md border border-slate-200 shadow-sm">
<div class="flex items-center gap-2">
<h2 class="text-base font-bold text-slate-800">OMS 주문 목록 (`TPL-LIST-01`)</h2>
<span class="px-2 py-0.5 rounded bg-emerald-100 text-emerald-800 text-xs font-bold">운영 레벨</span>
</div>
<button class="bg-blue-600 hover:bg-blue-700 text-white font-bold text-xs px-3.5 py-1.5 rounded transition shadow-sm">
+ 신규 주문 등록 (`TPL-CREATE-02`)
</button>
</div>
<!-- Summary Strip -->
<div class="summary-strip flex gap-3 text-xs bg-white p-3 rounded-md border border-slate-200 shadow-sm">
<div class="summary-item flex-1 bg-slate-50 p-2 rounded border border-slate-200">
<span class="text-slate-500 block">전체 주문</span>
<strong class="text-slate-800 text-sm font-mono">1,248 </strong>
</div>
<div class="summary-item flex-1 bg-amber-50 p-2 rounded border border-amber-200">
<span class="text-amber-700 block">처리 대기</span>
<strong class="text-amber-900 text-sm font-mono">83 </strong>
</div>
<div class="summary-item flex-1 bg-rose-50 p-2 rounded border border-rose-200">
<span class="text-rose-700 block">오류/차단</span>
<strong class="text-rose-900 text-sm font-mono">12 </strong>
</div>
<div class="summary-item flex-1 bg-emerald-50 p-2 rounded border border-emerald-200">
<span class="text-emerald-700 block">오늘 출고 완료</span>
<strong class="text-emerald-900 text-sm font-mono">126 </strong>
</div>
</div>
<!-- Main Data Grid Component Integration -->
<div class="flex-1 min-h-0 bg-white rounded-md border border-slate-200 overflow-hidden shadow-sm">
<QuantDataGrid
:columnDefs="columnDefs"
:rowData="rowData"
filename="oms_orders_export"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import QuantDataGrid from '../../components/QuantDataGrid.vue';
import type { ColDef } from 'ag-grid-community';
const columnDefs = ref<ColDef[]>([
{ field: 'orderNo', headerName: '주문번호', minWidth: 150, flex: 1.2, sortable: true, filter: true },
{ field: 'customerName', headerName: '거래처명', minWidth: 140, flex: 1.2 },
{ field: 'orderDate', headerName: '주문일자', minWidth: 110, flex: 1 },
{ field: 'totalAmount', headerName: '주문금액 (KRW)', minWidth: 130, flex: 1.2, sortable: true },
{ field: 'status', headerName: '상태 뱃지', minWidth: 110, flex: 1 }
]);
const rowData = ref([
{ orderNo: 'SO-2026-000128', customerName: '(주) 대한유통', orderDate: '2026-07-26', totalAmount: '1,250,000', status: 'READY_TO_SHIP' },
{ orderNo: 'SO-2026-000129', customerName: '삼성전자 물류센터', orderDate: '2026-07-26', totalAmount: '141,000,000', status: 'PROCESSING' },
{ orderNo: 'SO-2026-000130', customerName: 'SK하이닉스 반도체', orderDate: '2026-07-26', totalAmount: '127,500,000', status: 'COMPLETED' }
]);
</script>