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
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:
@@ -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>
|
||||
Reference in New Issue
Block a user