feat(wbs): implement OrderLineEditor (WBS-COMP-4.3) and integrate into TPL-CREATE-02 template view
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
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) / CI Workflow Lint (push) Failing after 9s

This commit is contained in:
2026-07-26 01:59:28 +09:00
parent 6bab9950e0
commit cf5dd51e5a
2 changed files with 139 additions and 34 deletions
@@ -0,0 +1,131 @@
<!-- Business Composite Component Layer: OrderLineEditor (WBS-COMP-4.3) -->
<template>
<div class="business-order-line-editor border border-slate-300 rounded-md bg-white overflow-hidden shadow-sm flex flex-col gap-2">
<!-- Editor Header Toolbar -->
<div class="bg-slate-100 p-2.5 border-b border-slate-300 flex justify-between items-center text-xs">
<div class="flex items-center gap-2 font-bold text-slate-800">
<span>📦 주문 품목 라인 인라인 편집기</span>
<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-800 font-mono"> {{ lines.length }} 라인</span>
</div>
<div class="flex gap-1">
<button type="button" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold px-2.5 py-1 rounded transition" @click="addLine">
+ 추가
</button>
<button type="button" class="bg-rose-100 hover:bg-rose-200 text-rose-800 font-bold px-2 py-1 rounded transition" @click="clearSelected">
선택 삭제
</button>
</div>
</div>
<!-- Line Table Body -->
<div class="overflow-x-auto p-2">
<table class="w-full text-xs text-left border border-slate-200">
<thead class="bg-slate-50 text-slate-700 font-bold border-b">
<tr>
<th class="p-2 border-r w-10 text-center"><input type="checkbox" @change="toggleSelectAll" /></th>
<th class="p-2 border-r">품목코드</th>
<th class="p-2 border-r">품목명</th>
<th class="p-2 border-r text-right">수량</th>
<th class="p-2 border-r text-right">단가 (KRW)</th>
<th class="p-2 border-r text-right">공급가액 (KRW)</th>
<th class="p-2 text-center w-16">작업</th>
</tr>
</thead>
<tbody>
<tr v-for="(line, idx) in lines" :key="line.id" class="border-b hover:bg-slate-50">
<td class="p-2 border-r text-center"><input type="checkbox" v-model="line.selected" /></td>
<td class="p-2 border-r font-mono">
<input type="text" v-model="line.itemCode" class="w-full px-1.5 py-1 border rounded font-mono font-bold text-blue-700" />
</td>
<td class="p-2 border-r">
<input type="text" v-model="line.itemName" class="w-full px-1.5 py-1 border rounded" />
</td>
<td class="p-2 border-r text-right font-mono">
<input type="number" v-model.number="line.quantity" class="w-20 text-right px-1.5 py-1 border rounded font-mono" @input="recalculateLine(line)" />
</td>
<td class="p-2 border-r text-right font-mono">
<input type="number" v-model.number="line.price" class="w-24 text-right px-1.5 py-1 border rounded font-mono" @input="recalculateLine(line)" />
</td>
<td class="p-2 border-r text-right font-mono font-bold text-slate-800">
{{ line.amount.toLocaleString() }}
</td>
<td class="p-2 text-center">
<button type="button" class="text-rose-600 font-bold hover:underline" @click="removeLine(idx)">삭제</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Total Summary Strip -->
<div class="bg-slate-50 p-2.5 border-t border-slate-300 flex justify-between items-center text-xs">
<span class="text-slate-500">라인 재계산: <strong class="text-emerald-700">실시간 유효성 검증 완료</strong></span>
<div class="font-bold text-slate-900">
합계 금액: <span class="text-blue-700 font-mono text-sm ml-1">{{ totalAmountSummary.toLocaleString() }} KRW</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
export interface OrderLineItem {
id: number;
itemCode: string;
itemName: string;
quantity: number;
price: number;
amount: number;
selected?: boolean;
}
const props = defineProps<{
initialLines?: OrderLineItem[];
}>();
const emit = defineEmits(['update:lines']);
const lines = ref<OrderLineItem[]>(props.initialLines || [
{ id: 1, itemCode: 'ITEM-000660', itemName: 'SK하이닉스 HBM3E 24GB', quantity: 1000, price: 127500, amount: 127500000, selected: false },
{ id: 2, itemCode: 'ITEM-005930', itemName: '삼성전자 DDR5 64GB', quantity: 1000, price: 141000, amount: 141000000, selected: false }
]);
const totalAmountSummary = computed(() => {
return lines.value.reduce((sum, item) => sum + (item.amount || 0), 0);
});
const recalculateLine = (line: OrderLineItem) => {
line.amount = (line.quantity || 0) * (line.price || 0);
emit('update:lines', lines.value);
};
const addLine = () => {
const newId = lines.value.length > 0 ? Math.max(...lines.value.map(l => l.id)) + 1 : 1;
lines.value.push({
id: newId,
itemCode: `ITEM-00${newId}00`,
itemName: '신규 주문 품목',
quantity: 1,
price: 10000,
amount: 10000,
selected: false
});
emit('update:lines', lines.value);
};
const removeLine = (index: number) => {
lines.value.splice(index, 1);
emit('update:lines', lines.value);
};
const clearSelected = () => {
lines.value = lines.value.filter(l => !l.selected);
emit('update:lines', lines.value);
};
const toggleSelectAll = (e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
lines.value.forEach(l => l.selected = checked);
};
</script>
@@ -22,43 +22,16 @@
</div>
</div>
<!-- Line Grid Section -->
<!-- Line Item Composite Component Integration (OrderLineEditor) -->
<div class="bg-white p-4 rounded-md border border-slate-200 shadow-sm flex flex-col gap-3">
<div class="flex justify-between items-center border-b pb-2">
<h3 class="font-bold text-xs text-slate-700">2. 주문 품목 라인 (Line Items)</h3>
<button class="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold px-3 py-1 rounded transition">
+ 추가
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs text-left border border-slate-200">
<thead class="bg-slate-50 text-slate-700 font-bold border-b">
<tr>
<th class="p-2 border-r">품목코드</th>
<th class="p-2 border-r">품목명</th>
<th class="p-2 border-r text-right">수량</th>
<th class="p-2 border-r text-right">단가 (KRW)</th>
<th class="p-2 text-right">공급가액 (KRW)</th>
</tr>
</thead>
<tbody>
<tr v-for="line in lines" :key="line.id" class="border-b hover:bg-slate-50">
<td class="p-2 border-r font-mono font-bold text-blue-700">{{ line.itemCode }}</td>
<td class="p-2 border-r">{{ line.itemName }}</td>
<td class="p-2 border-r text-right font-mono">{{ line.quantity }} EA</td>
<td class="p-2 border-r text-right font-mono">{{ line.price }}</td>
<td class="p-2 text-right font-mono font-bold text-slate-800">{{ line.amount }}</td>
</tr>
</tbody>
</table>
</div>
<h3 class="font-bold text-xs text-slate-700">2. 주문 품목 라인 (OrderLineEditor Composite)</h3>
<OrderLineEditor v-model:lines="orderLines" />
</div>
<!-- Sticky Action Bar -->
<div class="sticky bottom-0 bg-white p-3 rounded-md border border-slate-200 shadow-md flex justify-between items-center">
<div class="text-xs text-slate-600">
공급가액: <strong class="font-mono text-slate-900">268,500,000 KRW</strong> · 세액: <strong class="font-mono text-slate-900">26,850,000 KRW</strong>
라인 항목 : <strong class="font-mono text-slate-900">{{ orderLines.length }}</strong>
</div>
<div class="flex gap-2">
<button class="px-3.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-bold rounded">
@@ -76,13 +49,14 @@
import { ref } from 'vue';
import StringField from '../../components/fields/StringField.vue';
import DateField from '../../components/fields/DateField.vue';
import OrderLineEditor from '../../components/business-composites/OrderLineEditor.vue';
const customerName = ref('SK하이닉스 반도체');
const orderDate = ref('2026-07-26');
const warehouse = ref('WH-SEOUL-01 (서울중앙창고)');
const lines = ref([
{ id: 1, itemCode: 'ITEM-000660', itemName: 'SK하이닉스 HBM3E 24GB', quantity: '1,000', price: '127,500', amount: '127,500,000' },
{ id: 2, itemCode: 'ITEM-005930', itemName: '삼성전자 DDR5 64GB', quantity: '1,000', price: '141,000', amount: '141,000,000' }
const orderLines = ref([
{ id: 1, itemCode: 'ITEM-000660', itemName: 'SK하이닉스 HBM3E 24GB', quantity: 1000, price: 127500, amount: 127500000, selected: false },
{ id: 2, itemCode: 'ITEM-005930', itemName: '삼성전자 DDR5 64GB', quantity: 1000, price: 141000, amount: 141000000, selected: false }
]);
</script>