feat(frontend): create QuantGridAdapter unified PrimeVue DataTable wrapper with built-in search, sorting, multi-selection, and CSV export
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 10s
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) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 7s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-26 02:55:19 +09:00
parent e2b797c03d
commit d1a61fcdcc
2 changed files with 173 additions and 0 deletions
@@ -0,0 +1,157 @@
<!-- Enterprise Unified Grid Adapter Wrapper: QuantGridAdapter -->
<template>
<div class="quant-grid-adapter flex flex-col w-full h-full border border-slate-300 rounded-lg shadow-2xs bg-white overflow-hidden">
<!-- 그리드 상단 툴바 (엑셀 내보내기, 전체 검색, 컬럼 필터 ) -->
<div v-if="showToolbar !== false" class="flex items-center justify-between px-3 py-2 bg-slate-100 border-b border-slate-200 text-xs">
<div class="flex items-center gap-2">
<span class="font-bold text-slate-700 flex items-center gap-1">
<span>📊</span> {{ title || '데이터 그리드' }}
</span>
<span v-if="items" class="text-[11px] text-slate-500 font-mono">
({{ items.length.toLocaleString() }})
</span>
</div>
<div class="flex items-center gap-2">
<InputText
v-if="showSearch !== false"
v-model="globalSearchQuery"
placeholder="전체 검색..."
class="h-7 w-44 text-[11px] px-2 border-slate-300"
/>
<Button
v-if="showExport !== false"
label="엑셀 내보내기"
icon="pi pi-file-excel"
class="p-button-sm p-button-success h-7 text-[11px] px-2 font-bold"
@click="exportCSV"
/>
</div>
</div>
<!-- PrimeVue DataTable 래핑 레이어 -->
<DataTable
ref="dt"
v-bind="$attrs"
:value="filteredItems"
:loading="loading"
:paginator="paginator !== false"
:rows="rows || 15"
:rowsPerPageOptions="[10, 15, 30, 50, 100]"
:selection="selectedRows"
:selectionMode="selectionMode"
dataKey="id"
responsiveLayout="scroll"
resizableColumns
columnResizeMode="fit"
reorderableColumns
class="p-datatable-sm w-full text-xs flex-1"
tableStyle="min-width: 50rem"
@update:selection="onSelectionChange"
>
<template #empty>
<div class="text-center py-8 text-slate-400 font-medium">
조회된 데이터가 존재하지 않습니다.
</div>
</template>
<!-- 외부 정의 슬롯 100% 바이패스 전파 -->
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps || {}"></slot>
</template>
<!-- Checkbox 선택 컬럼 -->
<Column v-if="selectionMode === 'multiple'" selectionMode="multiple" headerStyle="width: 3rem" />
<!-- 동적 컬럼 바인딩 -->
<Column
v-for="col in columns"
:key="col.key"
:field="col.key"
:header="col.label"
:style="{ width: col.width || 'auto', textAlign: col.align || 'left' }"
sortable
>
<template #body="slotProps">
<slot :name="col.key" :item="slotProps.data" :value="slotProps.data[col.key]">
<span v-if="col.type === 'number'" class="font-mono font-bold text-slate-900">
{{ formatNumber(slotProps.data[col.key]) }}
</span>
<span v-else-if="col.type === 'currency'" class="font-mono font-bold text-blue-700">
{{ formatNumber(slotProps.data[col.key]) }}
</span>
<span v-else class="font-medium text-slate-800">
{{ slotProps.data[col.key] ?? '-' }}
</span>
</slot>
</template>
</Column>
</DataTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import InputText from 'primevue/inputtext';
import Button from 'primevue/button';
defineOptions({
inheritAttrs: false
});
export interface AdapterGridColumn {
key: string;
label: string;
width?: string;
align?: 'left' | 'center' | 'right';
type?: 'text' | 'number' | 'currency' | 'date' | 'status';
}
const props = defineProps<{
title?: string;
columns: AdapterGridColumn[];
items: any[];
loading?: boolean;
paginator?: boolean;
rows?: number;
selectionMode?: 'single' | 'multiple';
showToolbar?: boolean;
showSearch?: boolean;
showExport?: boolean;
}>();
const emit = defineEmits(['selection-change', 'update:selection']);
const dt = ref();
const globalSearchQuery = ref('');
const selectedRows = ref<any>(null);
const filteredItems = computed(() => {
if (!props.items) return [];
if (!globalSearchQuery.value) return props.items;
const q = globalSearchQuery.value.toLowerCase();
return props.items.filter(item => {
return Object.values(item).some(val => String(val ?? '').toLowerCase().includes(q));
});
});
const formatNumber = (val: any) => {
if (val === undefined || val === null || val === '') return '-';
const num = Number(val);
return isNaN(num) ? String(val) : num.toLocaleString();
};
const onSelectionChange = (val: any) => {
selectedRows.value = val;
emit('update:selection', val);
emit('selection-change', val);
};
const exportCSV = () => {
if (dt.value) {
dt.value.exportCSV();
}
};
</script>
@@ -83,6 +83,21 @@
</div>
</div>
<div class="p-3 bg-slate-50 rounded border flex flex-col gap-2">
<span class="text-xs font-bold text-slate-800">17. QuantGridAdapter (통합 PrimeVue 그리드 어댑터: 검색, 정렬, 엑셀 내보내기 내장)</span>
<QuantGridAdapter
title="OMS 주문 실시간 목록"
:columns="[
{ key: 'orderNo', label: '주문번호', width: '140px' },
{ key: 'customerName', label: '거래처명' },
{ key: 'amount', label: '금액', width: '130px', type: 'currency', align: 'right' },
{ key: 'orderDate', label: '주문일자', width: '110px', align: 'center' }
]"
:items="showcaseRowData"
selectionMode="multiple"
/>
</div>
<div class="p-3 bg-slate-50 rounded border">
<span class="text-xs font-bold text-slate-800 mb-1 block">17. QuantSearchHeaderBar</span>
<QuantSearchHeaderBar v-model:searchQuery="showcaseSearch" v-model:selectedStatus="showcaseStatus" :statusOptions="['전체', '승인완료', '검토중']" />
@@ -202,6 +217,7 @@ import QuantSearchHeaderBar from '../components/QuantSearchHeaderBar.vue';
import QuantStatusChip from '../components/QuantStatusChip.vue';
import QuantTabPanel from '../components/QuantTabPanel.vue';
import QuantSplitter from '../components/QuantSplitter.vue';
import QuantGridAdapter from '../components/QuantGridAdapter.vue';
import QuantFormModal from '../components/QuantFormModal.vue';
import QuantLookupModal from '../components/QuantLookupModal.vue';