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:
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user