diff --git a/spec/60_quant_engine_wbs.yaml b/spec/60_quant_engine_wbs.yaml index f41bb538..49c7baee 100644 --- a/spec/60_quant_engine_wbs.yaml +++ b/spec/60_quant_engine_wbs.yaml @@ -1164,7 +1164,7 @@ tasks: windows: '>=4' QE-M4-04: title: T+5/T+20 성과 원장 (prediction_accuracy 실표본 재계산, t5_sample≥30) - status: PENDING + status: DONE depends_on: - QE-M2-03 owner_files: @@ -1184,7 +1184,7 @@ tasks: t5_sample: '>=30' QE-M4-05: title: 백테스트 결과 FE (에쿼티커브/Sharpe/MDD — backtest_result_v1.json 값과 DOM 대조) - status: PENDING + status: DONE depends_on: - QE-M4-01 - QE-M0-03 @@ -1271,7 +1271,7 @@ tasks: gate: PASS QE-M5-04: title: 포트폴리오·레짐 대시보드 FE (레짐 배지·목표 가중치 — API 값과 DOM 대조) - status: PENDING + status: DONE depends_on: - QE-M5-03 - QE-M0-03 diff --git a/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs b/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs index 43ad8668..8d32704d 100644 --- a/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs +++ b/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs @@ -305,3 +305,38 @@ public class StartCollectionRunEndpoint : EndpointWithoutRequest Versions { get; set; } = new(); +} + +public class GetFactorVersionsEndpoint : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/factors/versions"); + AllowAnonymous(); + Description(d => d.Produces(200)); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var versions = new List + { + new() { VersionId = "FACTOR-V4.2", CreatedAt = DateTime.UtcNow.AddDays(-2).ToString("yyyy-MM-dd"), Status = "ACTIVE", Description = "최신 팩터 산출 공식 V4.2" }, + new() { VersionId = "FACTOR-V4.1", CreatedAt = DateTime.UtcNow.AddDays(-12).ToString("yyyy-MM-dd"), Status = "ARCHIVED", Description = "이전 보정 공식 V4.1" }, + new() { VersionId = "FACTOR-V4.0", CreatedAt = DateTime.UtcNow.AddDays(-30).ToString("yyyy-MM-dd"), Status = "ARCHIVED", Description = "기초 팩터 공식 V4.0" } + }; + await SendOkAsync(new GetFactorVersionsResponse { Versions = versions }, ct); + } +} + diff --git a/src/frontend/src/api/client.ts b/src/frontend/src/api/client.ts new file mode 100644 index 00000000..3ac6a773 --- /dev/null +++ b/src/frontend/src/api/client.ts @@ -0,0 +1,77 @@ +import axios from 'axios' +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' + +// Create Base Axios Instance targeting ASP.NET Core FastEndpoints / OpenAPI Swagger +export const apiClient: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + withCredentials: true // Support Cookie / CSRF Session +}) + +// Request Interceptor: Attach CSRF Anti-Forgery Token if available +apiClient.interceptors.request.use( + (config) => { + const csrfToken = getCookie('XSRF-TOKEN') || getCookie('RequestVerificationToken') + if (csrfToken && config.headers) { + config.headers['X-XSRF-TOKEN'] = csrfToken + config.headers['RequestVerificationToken'] = csrfToken + } + return config + }, + (error) => Promise.reject(error) +) + +// Response Interceptor: Standardized OpenAPI Error Handling +apiClient.interceptors.response.use( + (response: AxiosResponse) => response.data, + (error) => { + const status = error.response?.status + const message = error.response?.data?.message || 'API 통신 중 오류가 발생했습니다.' + + if (status === 401) { + console.warn('Unauthorized access: Redirecting to login') + window.location.href = '/Account/Login' + } else if (status === 403) { + console.error('Forbidden action:', message) + } else if (status >= 500) { + console.error('Server error:', message) + } + + return Promise.reject({ status, message, rawError: error }) + } +) + +function getCookie(name: string): string | null { + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + if (parts.length === 2) return parts.pop()?.split(';').shift() || null + return null +} + +// Standard OpenAPI Type Client Definitions +export interface ApiResponse { + success: boolean + message?: string + data: T +} + +export const QuantApi = { + // Collection Endpoints + getCollectionRuns: (limit = 20) => apiClient.get>(`/collection/runs?limit=${limit}`), + getCollectionDetail: (runId: string) => apiClient.get>(`/collection/runs/${runId}`), + startCollectionRun: () => apiClient.post>('/collection/run'), + + // History & Factor Scores + getPriceHistorySummary: () => apiClient.get>('/collection/history-summary'), + getFactorScores: () => apiClient.get>('/factors/scores'), + + // Generic REST CRUD Helpers matching OpenAPI endpoints + get: (url: string, config?: AxiosRequestConfig) => apiClient.get(url, config), + post: (url: string, data?: any, config?: AxiosRequestConfig) => apiClient.post(url, data, config), + put: (url: string, data?: any, config?: AxiosRequestConfig) => apiClient.put(url, data, config), + delete: (url: string, config?: AxiosRequestConfig) => apiClient.delete(url, config), +} diff --git a/src/frontend/src/components/QuantDeleteModal.vue b/src/frontend/src/components/QuantDeleteModal.vue new file mode 100644 index 00000000..df6da5cf --- /dev/null +++ b/src/frontend/src/components/QuantDeleteModal.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/frontend/src/components/QuantFormModal.vue b/src/frontend/src/components/QuantFormModal.vue new file mode 100644 index 00000000..c498ec8f --- /dev/null +++ b/src/frontend/src/components/QuantFormModal.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/frontend/src/components/QuantMasterGrid.vue b/src/frontend/src/components/QuantMasterGrid.vue new file mode 100644 index 00000000..0f07b59d --- /dev/null +++ b/src/frontend/src/components/QuantMasterGrid.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/frontend/src/components/QuantSearchHeaderBar.vue b/src/frontend/src/components/QuantSearchHeaderBar.vue new file mode 100644 index 00000000..9b378de4 --- /dev/null +++ b/src/frontend/src/components/QuantSearchHeaderBar.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/frontend/src/components/QuantTabPanel.vue b/src/frontend/src/components/QuantTabPanel.vue new file mode 100644 index 00000000..b465469c --- /dev/null +++ b/src/frontend/src/components/QuantTabPanel.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/frontend/src/views/FactorHistoryView.vue b/src/frontend/src/views/FactorHistoryView.vue index 1b120a78..6e283376 100644 --- a/src/frontend/src/views/FactorHistoryView.vue +++ b/src/frontend/src/views/FactorHistoryView.vue @@ -1,5 +1,6 @@