style(comp): polish UI quality with 1-click clear, LOT pulse expiry badges, and VAT 10% auto-summary
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) / 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) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-26 02:26:58 +09:00
parent 863164221c
commit 882ab5a777
3 changed files with 94 additions and 52 deletions
@@ -1,18 +1,18 @@
<!-- 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">
<div class="business-order-line-editor border border-slate-300 rounded-md bg-white overflow-hidden shadow-sm flex flex-col gap-2 select-none">
<!-- 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>📦 주문 품목 라인 고성능 인라인 편집기</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">
+ 추가
<div class="flex gap-1.5">
<button type="button" class="bg-blue-600 hover:bg-blue-700 text-white font-bold px-3 py-1 rounded transition shadow-xs flex items-center gap-1" @click="addLine">
<span>+ 추가</span>
</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 type="button" class="bg-rose-100 hover:bg-rose-200 text-rose-800 font-bold px-2.5 py-1 rounded transition flex items-center gap-1" @click="clearSelected">
<span>선택 삭제</span>
</button>
</div>
</div>
@@ -28,27 +28,31 @@
<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 border-r text-right">VAT (10%)</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">
<tr v-for="(line, idx) in lines" :key="line.id" class="border-b hover:bg-blue-50/40 transition">
<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" />
<input type="text" v-model="line.itemCode" class="w-full px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500 uppercase" />
</td>
<td class="p-2 border-r">
<input type="text" v-model="line.itemName" class="w-full px-1.5 py-1 border rounded" />
<input type="text" v-model="line.itemName" class="w-full px-2 py-1 border border-slate-300 rounded text-slate-900 font-medium bg-white focus:ring-1 focus:ring-blue-500" />
</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)" />
<input type="number" v-model.number="line.quantity" class="w-20 text-right px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500" @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)" />
<input type="number" v-model.number="line.price" class="w-28 text-right px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500" @input="recalculateLine(line)" />
</td>
<td class="p-2 border-r text-right font-mono font-bold text-slate-800">
<td class="p-2 border-r text-right font-mono font-bold text-slate-900">
{{ line.amount.toLocaleString() }}
</td>
<td class="p-2 border-r text-right font-mono text-slate-500">
{{ Math.floor(line.amount * 0.1).toLocaleString() }}
</td>
<td class="p-2 text-center">
<button type="button" class="text-rose-600 font-bold hover:underline" @click="removeLine(idx)">삭제</button>
</td>
@@ -59,9 +63,13 @@
<!-- 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 class="flex items-center gap-3">
<span class="text-slate-600">라인 실시간 상태: <strong class="text-emerald-700"> 자동 연산 정합성 보장</strong></span>
<span class="text-slate-400">|</span>
<span class="text-slate-600">부가세 합계: <strong class="font-mono text-slate-800">{{ totalVatSummary.toLocaleString() }} KRW</strong></span>
</div>
<div class="font-bold text-slate-900 text-sm">
합계 금액 (VAT 포함): <span class="text-blue-700 font-mono text-base ml-1 font-extrabold">{{ totalWithVatSummary.toLocaleString() }} KRW</span>
</div>
</div>
</div>
@@ -95,6 +103,14 @@ const totalAmountSummary = computed(() => {
return lines.value.reduce((sum, item) => sum + (item.amount || 0), 0);
});
const totalVatSummary = computed(() => {
return Math.floor(totalAmountSummary.value * 0.1);
});
const totalWithVatSummary = computed(() => {
return totalAmountSummary.value + totalVatSummary.value;
});
const recalculateLine = (line: OrderLineItem) => {
line.amount = (line.quantity || 0) * (line.price || 0);
emit('update:lines', lines.value);
@@ -1,38 +1,40 @@
<!-- Domain Field Component Layer: LotField (WBS-COMP-3.4) -->
<template>
<div class="domain-lot-field flex flex-col gap-1 border border-slate-200 p-2.5 rounded-md bg-white">
<div class="domain-lot-field flex flex-col gap-1 w-full bg-slate-50 p-2.5 rounded-md border border-slate-300">
<div class="flex justify-between items-center text-xs">
<label class="font-bold text-slate-700">
{{ label || '로트(LOT) 번호 및 FEFO 관리' }} <span v-if="required" class="text-red-500">*</span>
<label class="font-bold text-slate-800 flex items-center gap-1">
<span>🏷 LOT 번호 / FEFO 유효기간</span>
<span v-if="required" class="text-rose-600">*</span>
</label>
<span :class="['text-[10px] px-2 py-0.5 rounded font-bold font-mono', isExpired ? 'bg-rose-100 text-rose-800' : 'bg-emerald-100 text-emerald-800']">
{{ isExpired ? '🚨 로트 만료' : '✓ 출고 가능 (FEFO)' }}
<span v-if="remainingDays !== null" :class="['px-2 py-0.5 rounded text-[11px] font-bold font-mono shadow-xs', expiryStatusClass]">
{{ expiryStatusText }}
</span>
</div>
<div class="flex gap-2 mt-1">
<input
type="text"
:value="lotNumber"
placeholder="LOT-20260726-001"
:readonly="readonly"
:disabled="disabled"
class="flex-1 h-9 px-3 border border-slate-300 rounded font-mono text-xs text-slate-800 font-bold focus:ring-2 focus:ring-blue-500 focus:outline-none"
@input="handleLotInput"
/>
<input
type="date"
:value="expirationDate"
:readonly="readonly"
:disabled="disabled"
class="h-9 px-2 border border-slate-300 rounded bg-slate-50 text-xs font-mono font-bold text-slate-700"
@change="handleDateChange"
/>
</div>
<div class="flex justify-between items-center text-[11px] text-slate-500 mt-0.5">
<span>권장 출고 순위: <strong class="text-blue-700 font-mono">FEFO #1 (유효기간 상위)</strong></span>
<span>유효기간: <strong class="font-mono text-slate-700">{{ expirationDate || '미지정' }}</strong></span>
<div class="grid grid-cols-2 gap-2 mt-1">
<div class="flex flex-col gap-0.5">
<span class="text-[10px] font-bold text-slate-500">LOT 배치 번호</span>
<input
type="text"
:value="lotNumber"
placeholder="LOT-20260725-01"
:readonly="readonly"
:disabled="disabled"
class="h-8 px-2 border border-slate-300 rounded font-mono text-xs text-slate-900 font-bold bg-white focus:ring-2 focus:ring-blue-500 uppercase"
@input="handleLotInput"
/>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-[10px] font-bold text-slate-500">유효기간 (Expiration Date)</span>
<input
type="date"
:value="expirationDate"
:readonly="readonly"
:disabled="disabled"
class="h-8 px-2 border border-slate-300 rounded font-mono text-xs text-slate-900 font-bold bg-white focus:ring-2 focus:ring-blue-500"
@change="handleDateChange"
/>
</div>
</div>
</div>
</template>
@@ -41,7 +43,6 @@
import { computed } from 'vue';
const props = defineProps<{
label?: string;
lotNumber: string;
expirationDate: string;
required?: boolean;
@@ -51,15 +52,31 @@ const props = defineProps<{
const emit = defineEmits(['update:lotNumber', 'update:expirationDate']);
const isExpired = computed(() => {
if (!props.expirationDate) return false;
const remainingDays = computed(() => {
if (!props.expirationDate) return null;
const exp = new Date(props.expirationDate).getTime();
const today = new Date().getTime();
return exp < today;
const diff = Math.ceil((exp - today) / (1000 * 3600 * 24));
return diff;
});
const expiryStatusClass = computed(() => {
if (remainingDays.value === null) return 'bg-slate-100 text-slate-700';
if (remainingDays.value <= 0) return 'bg-rose-600 text-white animate-pulse';
if (remainingDays.value <= 30) return 'bg-rose-100 text-rose-800 border border-rose-300 font-bold';
if (remainingDays.value <= 90) return 'bg-amber-100 text-amber-800 border border-amber-300 font-bold';
return 'bg-emerald-100 text-emerald-800 border border-emerald-300 font-bold';
});
const expiryStatusText = computed(() => {
if (remainingDays.value === null) return '유효기간 미지정';
if (remainingDays.value <= 0) return `🚨 만료됨 (D+${Math.abs(remainingDays.value.valueOf())})`;
if (remainingDays.value <= 30) return `⚠️ 출하임계 (D-${remainingDays.value})`;
return `✓ 안전 (D-${remainingDays.value})`;
});
const handleLotInput = (e: Event) => {
emit('update:lotNumber', (e.target as HTMLInputElement).value);
emit('update:lotNumber', (e.target as HTMLInputElement).value.toUpperCase());
};
const handleDateChange = (e: Event) => {
@@ -1,6 +1,6 @@
<!-- Primitive UI Components Layer -->
<!-- Primitive UI Components Layer: TextInput -->
<template>
<div class="primitive-input-wrapper w-full">
<div class="primitive-input-wrapper relative w-full flex items-center">
<input
:id="id"
:type="type || 'text'"
@@ -8,9 +8,18 @@
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
class="primitive-input w-full h-9 px-3 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 disabled:cursor-not-allowed readonly:bg-slate-50 readonly:text-slate-700"
class="primitive-input w-full h-9 pl-3 pr-8 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 disabled:cursor-not-allowed readonly:bg-slate-50 readonly:text-slate-700 transition shadow-xs"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<button
v-if="modelValue && !disabled && !readonly"
type="button"
class="absolute right-2 text-slate-400 hover:text-slate-600 text-xs font-bold w-4 h-4 rounded-full flex items-center justify-center bg-slate-100 hover:bg-slate-200"
title="Clear text"
@click="$emit('update:modelValue', '')"
>
</button>
</div>
</template>