feat(fe): implement working ModelsList grid with mock data

- Simplified data fetching (removed TanStack Query for now)
- Added direct mock API client in component
- Fixed state management (isLoading, isError, modelsData)
- Grid now renders with 3 sample model records
- Updated v-if conditions for loading/error/empty states
- Added grid container height and filter styling

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:56:04 +09:00
parent 48a3da30d0
commit 8a3ee0a1d7
2 changed files with 115 additions and 64 deletions
@@ -188,12 +188,13 @@ export const modelQueryKeys = {
/** /**
* Fetch list of models with pagination * Fetch list of models with pagination
*/ */
export function useModelsList(params: ModelListParams = {}) { export function useModelsList(params?: ModelListParams) {
const finalParams = params || {}
return useQuery({ return useQuery({
queryKey: modelQueryKeys.list(params), queryKey: ['models', 'list', finalParams],
queryFn: () => apiClient.listModels(params), queryFn: () => apiClient.listModels(finalParams),
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000, // 10 minutes gcTime: 10 * 60 * 1000,
}) })
} }
+109 -59
View File
@@ -1,27 +1,92 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, onMounted } from 'vue'
import PageLayout from '../../../shared/ui/layouts/PageLayout.vue' import PageLayout from '../../../shared/ui/layouts/PageLayout.vue'
import { KsDataGrid, EmptyStatePlaceholder, SkeletonLoader } from '../../../shared/ui/components' import { KsDataGrid, EmptyStatePlaceholder, SkeletonLoader } from '../../../shared/ui/components'
import type { UiGridColumn } from '../../../shared/ui/adapter/contracts' import type { UiGridColumn } from '../../../shared/ui/adapter/contracts'
import { useModelsList, type Model } from '../composables/useModels' import type { Model, ModelListResponse } from '../composables/useModels'
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const searchQuery = ref('') const searchQuery = ref('')
const selectedPhase = ref('') const selectedPhase = ref('')
const queryParams = computed(() => ({ const isLoading = ref(true)
page: currentPage.value, const isError = ref(false)
pageSize: pageSize.value, const modelsData = ref<ModelListResponse | null>(null)
search: searchQuery.value,
phase: selectedPhase.value,
}))
const modelsQuery = useModelsList(queryParams.value)
const items = computed(() => { const items = computed(() => {
const data = modelsQuery.data.value as any return modelsData.value?.items || []
return data?.items || [] })
// Mock API client (same as in composable)
const apiClient = {
async listModels(params: any) {
await new Promise(resolve => setTimeout(resolve, 300))
const mockModels: Model[] = [
{
modelId: '00000000-0000-0000-0000-000000000001',
name: 'Alpha Strategy v1',
phase: 'Validate',
active: false,
lastValidation: '2026-08-10',
pbo: 15.2,
dsr: 96.5,
returnMtd: 12.5,
createdAt: '2026-06-15',
},
{
modelId: '00000000-0000-0000-0000-000000000002',
name: 'Beta Model v2',
phase: 'Review',
active: false,
lastValidation: '2026-08-09',
pbo: 18.3,
dsr: 94.2,
returnMtd: 8.3,
createdAt: '2026-07-01',
},
{
modelId: '00000000-0000-0000-0000-000000000003',
name: 'Gamma Arbitrage',
phase: 'Mature',
active: true,
lastValidation: '2026-08-08',
pbo: 8.5,
dsr: 98.1,
returnMtd: 18.7,
createdAt: '2026-05-10',
},
]
return {
items: mockModels,
total: mockModels.length,
page: 1,
pageSize: 20,
}
},
}
async function loadModels() {
try {
isLoading.value = true
isError.value = false
const data = await apiClient.listModels({
page: currentPage.value,
pageSize: pageSize.value,
search: searchQuery.value,
phase: selectedPhase.value,
})
modelsData.value = data
} catch (error) {
isError.value = true
console.error('Failed to load models:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadModels()
}) })
const columns: UiGridColumn[] = [ const columns: UiGridColumn[] = [
@@ -56,7 +121,8 @@ const columns: UiGridColumn[] = [
] ]
function handleSearch() { function handleSearch() {
modelsQuery.refetch() currentPage.value = 1
loadModels()
} }
</script> </script>
@@ -90,12 +156,12 @@ function handleSearch() {
</template> </template>
<!-- Loading State --> <!-- Loading State -->
<div v-if="modelsQuery.isPending.value" class="state-container"> <div v-if="isLoading" class="state-container">
<SkeletonLoader type="table" :rows="8" /> <SkeletonLoader type="table" :rows="8" />
</div> </div>
<!-- Error State --> <!-- Error State -->
<div v-else-if="modelsQuery.isError.value" class="state-container"> <div v-else-if="isError" class="state-container">
<EmptyStatePlaceholder title="데이터 로드 실패" description="모델 목록 데이터를 불러오지 못했습니다. 다시 시도해 주세요." /> <EmptyStatePlaceholder title="데이터 로드 실패" description="모델 목록 데이터를 불러오지 못했습니다. 다시 시도해 주세요." />
</div> </div>
@@ -117,65 +183,49 @@ function handleSearch() {
</template> </template>
<style scoped> <style scoped>
.models-page { .filters {
padding: 2rem; display: flex;
max-width: 1400px; gap: 1rem;
margin: 0 auto; margin-bottom: 1rem;
} }
.page-header { .search-input,
margin-bottom: 2rem; .status-select {
padding: 0.5rem;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
background-color: var(--color-background-primary);
color: var(--color-text-primary);
font-size: 0.875rem;
} }
.page-header h1 { .search-input {
margin: 0; flex: 1;
font-size: 2rem; min-width: 200px;
font-weight: 700;
} }
.page-header p { .status-select {
margin: 0.5rem 0 0 0; min-width: 150px;
color: var(--color-text-secondary);
} }
.loading-state, .state-container {
.error-state,
.empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
border: 1px solid var(--color-border-primary); border: 1px solid var(--color-border-primary);
border-radius: var(--border-radius-md); border-radius: 4px;
background-color: var(--color-background-secondary); background-color: var(--color-background-secondary);
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
} }
.models-grid { .grid-container {
border: 1px solid var(--color-border-primary); border: 1px solid var(--color-border-primary);
border-radius: var(--border-radius-md); border-radius: 4px;
overflow: hidden; overflow: hidden;
} height: 600px;
display: flex;
.models-table { flex-direction: column;
width: 100%;
border-collapse: collapse;
}
.models-table thead {
background-color: var(--color-background-secondary);
}
.models-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
border-bottom: 1px solid var(--color-border-primary);
}
.models-table td {
padding: 1rem;
border-bottom: 1px solid var(--color-border-primary);
}
.models-table tbody tr:hover {
background-color: var(--color-background-hover);
} }
</style> </style>