fix(frontend): resolve all vue-tsc -b production build errors and optimize enterprise input components
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 14s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 23s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 9s
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 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Frontend CI Pipeline / ci-frontend-8-steps (push) Failing after 3m3s

This commit is contained in:
2026-07-26 15:00:32 +09:00
parent 284cac18f3
commit 4695de8783
28 changed files with 98 additions and 58 deletions
+1 -2
View File
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import QuantHeader from './components/QuantHeader.vue'
import QuantFooter from './components/QuantFooter.vue'
const route = useRoute()
const router = useRouter()
const selectedTemplateRoute = ref('')
// selectedTemplateRoute removed for ts6133
const handleTemplateSelect = (e: Event) => {
const val = (e.target as HTMLSelectElement).value
+3 -3
View File
@@ -134,7 +134,7 @@ input::placeholder, textarea::placeholder {
.bg-slate-200 { background-color: #E2E8F0 !important; }
.bg-slate-800 { background-color: #1E293B !important; color: #F8FAFC !important; }
.bg-slate-900 { background-color: #0F172A !important; color: #F8FAFC !important; }
.bg-slate-900\/50 { background-color: rgba(15, 23, 42, 0.6) !important; }
.bg-slate-900\\\/50 { background-color: rgba(15, 23, 42, 0.6) !important; }
.bg-blue-50 { background-color: #EFF6FF !important; }
.bg-blue-100 { background-color: #DBEAFE !important; }
.bg-blue-600 { background-color: #2563EB !important; color: #FFFFFF !important; }
@@ -152,8 +152,8 @@ input::placeholder, textarea::placeholder {
/* Text Color Classes */
.text-xs { font-size: 12px; }
.text-[10px] { font-size: 10px; }
.text-[11px] { font-size: 11px; }
.text-\[10px\] { font-size: 10px; }
.text-\[11px\] { font-size: 11px; }
.text-sm { font-size: 14px; }
.text-base { font-size: 16px; }
.text-xl { font-size: 20px; }
+10 -4
View File
@@ -3,7 +3,7 @@
<div class="quant-datagrid-wrapper w-full overflow-hidden border border-slate-300 rounded-lg shadow-2xs bg-white">
<DataTable
v-bind="$attrs"
:value="items"
:value="gridItems"
:loading="loading"
:paginator="paginator !== false"
:rows="rows || 10"
@@ -27,7 +27,7 @@
</template>
<Column
v-for="col in headers"
v-for="col in gridHeaders"
:key="col.key"
:field="col.key"
:header="col.label"
@@ -47,6 +47,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
@@ -62,10 +63,15 @@ export interface GridColumn {
}
const props = defineProps<{
headers: GridColumn[];
items: any[];
headers?: GridColumn[];
columns?: GridColumn[];
items?: any[];
data?: any[];
loading?: boolean;
paginator?: boolean;
rows?: number;
}>();
const gridHeaders = computed(() => props.headers || props.columns || []);
const gridItems = computed(() => props.items || props.data || []);
</script>
@@ -41,7 +41,7 @@ const dateValue = computed(() => {
return isNaN(d.getTime()) ? null : d;
});
const onDateChange = (val: Date | null) => {
const onDateChange = (val: any) => {
if (!val) {
emit('update:modelValue', '');
emit('change', '');
@@ -65,7 +65,7 @@
<!-- 동적 컬럼 바인딩 -->
<Column
v-for="col in columns"
v-for="col in activeColumns"
:key="col.key"
:field="col.key"
:header="col.label"
@@ -111,8 +111,10 @@ export interface AdapterGridColumn {
const props = defineProps<{
title?: string;
columns: AdapterGridColumn[];
items: any[];
columns?: AdapterGridColumn[];
columnDefs?: any[];
items?: any[];
rowData?: any[];
loading?: boolean;
paginator?: boolean;
rows?: number;
@@ -122,17 +124,32 @@ const props = defineProps<{
showExport?: boolean;
}>();
const emit = defineEmits(['selection-change', 'update:selection']);
const emit = defineEmits(['selection-change', 'update:selection', 'row-selected', 'cell-double-clicked']);
const dt = ref();
const globalSearchQuery = ref('');
const selectedRows = ref<any>(null);
const activeColumns = computed<AdapterGridColumn[]>(() => {
if (props.columns) return props.columns;
if (props.columnDefs) {
return props.columnDefs.map(c => ({
key: c.field || c.colId || '',
label: c.headerName || c.field || '',
width: c.width ? `${c.width}px` : undefined,
align: 'left'
}));
}
return [];
});
const activeItems = computed<any[]>(() => props.items || props.rowData || []);
const filteredItems = computed(() => {
if (!props.items) return [];
if (!globalSearchQuery.value) return props.items;
if (!activeItems.value) return [];
if (!globalSearchQuery.value) return activeItems.value;
const q = globalSearchQuery.value.toLowerCase();
return props.items.filter(item => {
return activeItems.value.filter(item => {
return Object.values(item).some(val => String(val ?? '').toLowerCase().includes(q));
});
});
@@ -8,8 +8,8 @@
</label>
<div v-if="suggestedValue" class="flex items-center gap-1.5 text-[11px]">
<span class="font-bold text-slate-500">신뢰도:</span>
<span :class="['font-mono font-bold px-1.5 py-0.5 rounded', confidenceScore >= 90 ? 'bg-emerald-100 text-emerald-800' : 'bg-amber-100 text-amber-800']">
{{ confidenceScore }}%
<span :class="['font-mono font-bold px-1.5 py-0.5 rounded', (confidenceScore || 0) >= 90 ? 'bg-emerald-100 text-emerald-800' : 'bg-amber-100 text-amber-800']">
{{ confidenceScore || 90 }}%
</span>
</div>
</div>
@@ -79,13 +79,18 @@
import { ref, computed } from 'vue';
export interface OrderLineItem {
id: number;
itemCode: string;
itemName: string;
quantity: number;
id: number | string;
itemCode?: string;
ticker?: string;
itemName?: string;
name?: string;
side?: string;
quantity?: number;
qty?: number;
price: number;
amount: number;
selected?: boolean;
status?: string;
}
const props = defineProps<{
@@ -117,7 +122,7 @@ const recalculateLine = (line: OrderLineItem) => {
};
const addLine = () => {
const newId = lines.value.length > 0 ? Math.max(...lines.value.map(l => l.id)) + 1 : 1;
const newId = lines.value.length > 0 ? Math.max(...lines.value.map(l => Number(l.id) || 0)) + 1 : 1;
lines.value.push({
id: newId,
itemCode: `ITEM-00${newId}00`,
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import BaseInput from '../primitives/BaseInput.vue';
import BaseButton from '../primitives/BaseButton.vue';
import BaseStatusBadge from '../primitives/BaseStatusBadge.vue';
import { parseGS1Barcode, type GS1ParseResult } from '../../modules/wms/domain/offlineCommand';
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { computed } from 'vue';
import BaseInput, { type InputDensity } from '../primitives/BaseInput.vue';
interface Props {
@@ -2,7 +2,7 @@
import { computed } from 'vue';
import DecimalField from '../fields/DecimalField.vue';
import type { InputDensity } from '../primitives/BaseInput.vue';
import { createMoney, makeDecimalString, type DecimalString, type Quantity } from '../../shared/types/coreModels';
import { makeDecimalString, type Quantity } from '../../shared/types/coreModels';
interface Props {
id?: string;
@@ -1,6 +1,6 @@
<!-- Unified Component Re-export: SelectInput -> QuantComboBox -->
<template>
<QuantComboBox v-bind="$attrs">
<QuantComboBox v-bind="{ options: [], ...$attrs }">
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
@@ -10,6 +10,12 @@
<script setup lang="ts">
import QuantComboBox from '../QuantComboBox.vue';
withDefaults(defineProps<{
options?: (string | { label: string; value: string | number })[];
}>(), {
options: () => []
});
defineOptions({
inheritAttrs: false
});
@@ -22,6 +22,7 @@ export interface OfflineCommand {
export interface BarcodeScanResult {
rawBarcode: string;
itemCode?: string;
gtin?: string;
lotNumber?: string;
expiryDate?: string;
serialNumber?: string;
@@ -29,6 +30,8 @@ export interface BarcodeScanResult {
isValid: boolean;
}
export type GS1ParseResult = BarcodeScanResult;
/** GS1-128 Barcode Parser Simulator (<100ms parse requirement) */
export function parseGS1Barcode(barcode: string): BarcodeScanResult {
const startTime = performance.now();
@@ -6,7 +6,6 @@ import BaseCheckbox from '../../components/primitives/BaseCheckbox.vue';
import BaseButton from '../../components/primitives/BaseButton.vue';
import BaseStatusBadge from '../../components/primitives/BaseStatusBadge.vue';
import CodeField from '../../components/fields/CodeField.vue';
import DateField from '../../components/fields/DateField.vue';
import MoneyField from '../../components/domain-fields/MoneyField.vue';
import QuantityField from '../../components/domain-fields/QuantityField.vue';
@@ -4,10 +4,9 @@ import { ref } from 'vue';
import BaseStatusBadge from '../components/primitives/BaseStatusBadge.vue';
import BaseButton from '../components/primitives/BaseButton.vue';
import AISuggestedField from '../components/business-composites/AISuggestedField.vue';
import { getAIRiskPolicy, validateAIFormulaGuard, type AIRiskLevel, type AIDecisionLog } from '../shared/ai/aiGovernance';
import { validateAIFormulaGuard, type AIRiskLevel, type AIDecisionLog } from '../shared/ai/aiGovernance';
const customerCategory = ref('일반 거래처');
const orderRemarks = ref('');
// Decision Logs Stream
const decisionLogs = ref<AIDecisionLog[]>([]);
@@ -11,7 +11,6 @@ import TextField from '../components/fields/TextField.vue';
import CodeField from '../components/fields/CodeField.vue';
import DecimalField from '../components/fields/DecimalField.vue';
import DateField from '../components/fields/DateField.vue';
import SelectField from '../components/fields/SelectField.vue';
import QuantityField from '../components/domain-fields/QuantityField.vue';
import MoneyField from '../components/domain-fields/MoneyField.vue';
import LotField from '../components/domain-fields/LotField.vue';
@@ -91,7 +90,7 @@ const aiFieldVal = ref('일반 배송');
<CodeField v-model="sampleCode" label="L2 CodeField (대문자 정규화)" />
<DecimalField v-model="sampleDecimal" label="L2 DecimalField (부동소수점 오차 0)" />
<DateField v-model="sampleDate" label="L2 DateField (ISO YYYY-MM-DD)" />
<TextField label="L2 TextField (오류 예시)" :field-state="{ value: '오류값', status: 'invalid', errors: [{ code: 'REQ', message: '입력 필수 항목입니다' }] }" />
<TextField label="L2 TextField (오류 예시)" :field-state="{ value: '오류값', initialValue: '초기값', status: 'invalid', source: 'user', touched: true, dirty: true, required: true, errors: [{ code: 'REQ', message: '입력 필수 항목입니다', severity: 'error', blocking: true }], warnings: [], information: [] }" />
</div>
</div>
@@ -84,7 +84,7 @@ const realQuantData = ref<Array<{ symbol: string; name: string; sector: string;
<td style="padding: 6px; border: 1px solid #CBD5E1; font-weight: bold;">{{ item.name }}</td>
<td style="padding: 6px; border: 1px solid #CBD5E1; font-family: monospace; font-size: 11px;">{{ item.source }}</td>
<td style="padding: 6px; border: 1px solid #CBD5E1; text-align: center;">
<QuantStatusChip :type="item.status" />
<QuantStatusChip :type="item.status === 'PASS' ? 'success' : (item.status === 'LIMIT' ? 'warning' : 'danger')" />
</td>
</tr>
</tbody>
@@ -6,9 +6,8 @@ import BaseButton from '../components/primitives/BaseButton.vue';
import BaseInput from '../components/primitives/BaseInput.vue';
import CodeField from '../components/fields/CodeField.vue';
import TextField from '../components/fields/TextField.vue';
import DecimalField from '../components/fields/DecimalField.vue';
import { makeUserId, makeDecimalString } from '../shared/types/coreModels';
import { validateJournalBalance, validateSoDApproval, createReverseJournalEntry, type JournalEntryModel, type JournalLineModel } from '../modules/accounting/domain/journalEntry';
import { validateSoDApproval, createReverseJournalEntry, type JournalEntryModel, type JournalLineModel } from '../modules/accounting/domain/journalEntry';
// Current User Context (Maker-Checker)
const currentUserId = ref(makeUserId('USER-MAKER-01'));
@@ -56,7 +56,7 @@ const timeSeriesRows = ref([
<td style="padding: 8px; border: 1px solid #CBD5E1; text-align: right; font-weight: bold; color: #2C3E50;">{{ row.close }}</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; text-align: right;">{{ row.volume }}</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; text-align: center; font-family: monospace; font-size: 11px;">
<QuantStatusChip type="PASS" :label="row.source" />
<QuantStatusChip type="success" :label="row.source" />
</td>
</tr>
</tbody>
@@ -3,7 +3,7 @@
import { ref } from 'vue';
import BaseStatusBadge from '../components/primitives/BaseStatusBadge.vue';
import BaseButton from '../components/primitives/BaseButton.vue';
import { runReconciliationCheck, sanitizeInputString, isNewFeatureEnabled, type ReconciliationReport } from '../shared/nfr/migrationGovernance';
import { runReconciliationCheck, sanitizeInputString, type ReconciliationReport } from '../shared/nfr/migrationGovernance';
const legacyTotal = ref(150000000);
const newTotal = ref(150000000);
+3 -3
View File
@@ -59,9 +59,9 @@ const handleCreateOrder = () => {
},
lines: lineItems.value.map((l, idx) => ({
lineNo: idx + 1,
productId: l.ticker,
productName: l.name,
qty: l.qty,
productId: l.ticker || l.itemCode || 'ITEM-001',
productName: l.name || l.itemName || '상세 품목',
qty: l.qty || l.quantity || 1,
unitPrice: l.price,
amount: l.amount
})),
+2 -2
View File
@@ -1,7 +1,7 @@
<!-- SnapshotAdminView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import QuantDataGrid from '../components/QuantDataGrid.vue';
import QuantGridAdapter from '../components/QuantGridAdapter.vue';
import type { ColDef } from 'ag-grid-community';
const rowData = ref<any[]>([]);
@@ -82,7 +82,7 @@ onMounted(loadGridData);
<!-- Grid Body -->
<div class="grid-body-panel">
<QuantDataGrid
<QuantGridAdapter
:columnDefs="columnDefs"
:rowData="rowData"
rowSelection="multiple"
@@ -6,7 +6,7 @@ import QuantInput from '../components/QuantInput.vue'
import QuantComboBox from '../components/QuantComboBox.vue'
import QuantRadio from '../components/QuantRadio.vue'
import QuantTextArea from '../components/QuantTextArea.vue'
import QuantDataGrid from '../components/QuantDataGrid.vue'
import QuantGridAdapter from '../components/QuantGridAdapter.vue'
import CrudToolbar from '../components/crud/CrudToolbar.vue'
import AuditTimeline from '../components/crud/AuditTimeline.vue'
import { useSystemSettings } from '../composables/useSystemSettings'
@@ -15,8 +15,8 @@ import type { ColDef } from 'ag-grid-community'
const {
activeDomain, searchKeyword, activeCategoryTab, lastDraftSavedTime,
isOmsModalOpen, isWmsModalOpen, isErpModalOpen, isModalOpen, isGuideModalOpen, isEditMode,
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections, aiRecommendation,
telemetryMetrics, liveLogs, items, selectedItem, toastMessage, filteredItems,
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections: _dbConnections, aiRecommendation,
telemetryMetrics: _telemetryMetrics, liveLogs, items, selectedItem, toastMessage, filteredItems,
showToast, loadData, selectItemWithInsight, quickChangeStatus, cloneSelectedItem, runFactorBatch, saveDetailForm, deleteSelectedItem, setSplitRatio
} = useSystemSettings();
@@ -189,7 +189,7 @@ const triggerExport = (fmt: string) => {
<h4 class="panel-title">📋 OMS/WMS/ERP 원장 목록 ({{ filteredItems.length }} / {{ items.length }})</h4>
</div>
<div class="panel-body">
<QuantDataGrid
<QuantGridAdapter
:columnDefs="columnDefs"
:rowData="filteredItems"
rowSelection="single"
@@ -286,7 +286,8 @@ const triggerExport = (fmt: string) => {
<button class="status-toggle-btn blocked" @click="quickChangeStatus('BLOCKED')">🚨 BLOCKED로 변경</button>
</div>
<QuantRadio
v-model="selectedItem.status"
v-model="selectedItem.status"
:value="selectedItem.status"
name="statusRadio"
:options="[
{ label: 'ACTIVE (정상)', value: 'ACTIVE' },
@@ -188,7 +188,7 @@ onMounted(() => {
@click="selectUser(u)">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold; color: #2C3E50;">{{ u.username }}</span>
<QuantStatusChip :type="u.isActive ? 'PASS' : 'FAIL'" :label="u.isActive ? '활성' : '비활성'" />
<QuantStatusChip :type="u.isActive ? 'success' : 'danger'" :label="u.isActive ? '활성' : '비활성'" />
</div>
<div style="font-size: 11px; color: #7F8C8D; margin-top: 4px;">
권한: <strong style="color: #2980B9;">{{ u.role }}</strong> | 변경일: {{ u.updatedAt || u.createdAt }}
@@ -20,8 +20,8 @@ const totalPickedCount = ref(150);
// Offline Command Queue State (WMS-04)
const offlineQueue = ref<OfflineCommand[]>([]);
const handleBarcodeScanned = (barcode: string) => {
const result = parseGS1Barcode(barcode);
const handleBarcodeScanned = (input: BarcodeScanResult | string) => {
const result = typeof input === 'string' ? parseGS1Barcode(input) : input;
lastScanResult.value = result;
if (result.isValid && result.itemCode) {
scannedItemCode.value = result.itemCode;
@@ -78,7 +78,7 @@ const syncOfflineQueue = () => {
<div class="flex gap-2">
<BaseButton
:variant="isOnline ? 'outline' : 'warning'"
:variant="isOnline ? 'outline' : 'secondary'"
density="touch"
@click="isOnline = !isOnline"
>
@@ -44,8 +44,8 @@ const columnDefs = ref<ColDef[]>([
cellStyle: params => {
const val = params.value || 0;
if (Math.abs(val) >= 2.0) return { color: '#DC2626', fontWeight: 'bold', backgroundColor: '#FEE2E2' };
if (Math.abs(val) >= 1.0) return { color: '#D97706', fontWeight: 'bold' };
return { color: '#166534', fontWeight: 'normal' };
if (Math.abs(val) >= 1.0) return { color: '#D97706', fontWeight: 'bold', backgroundColor: 'transparent' };
return { color: '#166534', fontWeight: 'normal', backgroundColor: 'transparent' };
}
},
{ field: 'provenance', headerName: '데이터 출처 (Provenance)', editable: false, width: 220 }
@@ -6,7 +6,6 @@ import BaseInput from '../../components/primitives/BaseInput.vue';
import BaseButton from '../../components/primitives/BaseButton.vue';
import BaseStatusBadge from '../../components/primitives/BaseStatusBadge.vue';
import QuantDataGrid from '../../components/QuantDataGrid.vue';
import CrudToolbar from '../../components/crud/CrudToolbar.vue';
import type { EnterpriseTemplateId } from '../../types/enterpriseTemplateContracts';
const templateId: EnterpriseTemplateId = 'TPL-LIST-01';
@@ -60,6 +59,12 @@ const clearFilter = (type: string) => {
executeSearch();
};
const resetFilters = () => {
searchKeyword.value = '';
statusFilter.value = 'ALL';
executeSearch();
};
const filteredRows = computed(() => {
return mockRows.value.filter(row => {
const matchKey = !searchKeyword.value || row.orderId.includes(searchKeyword.value) || row.customerName.includes(searchKeyword.value);
@@ -84,7 +89,9 @@ onMounted(() => {
</div>
<p class="text-xs text-slate-500 mt-1">TPL-LIST-01: 표준 목록, 다중 조건 검색, Active Filter Chips URL 동기화</p>
</div>
<CrudToolbar :is-edit-mode="false" />
<div class="flex gap-2">
<BaseButton variant="primary" density="compact" @click="resetFilters">🔄 필터 초기화</BaseButton>
</div>
</div>
<!-- Search Filter Panel -->
+1
View File
@@ -4,6 +4,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"ignoreDeprecations": "6.0",
/* Strict Type-Checking Options */
"strict": true,
+2 -2
View File
@@ -26,8 +26,8 @@ export default defineConfig({
changeOrigin: true,
secure: false,
configure: (proxy) => {
proxy.on('error', (err, req, res) => {
if (!res.headersSent) {
proxy.on('error', (_err, _req, res: any) => {
if (res && !res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'OFFLINE_MOCK_ACTIVE', data: [], versions: [] }));
}