diff --git a/src/_BacktestingPage/components/BacktestChart.tsx b/src/_BacktestingPage/components/BacktestChart.tsx index 6c90b87..2da2f44 100644 --- a/src/_BacktestingPage/components/BacktestChart.tsx +++ b/src/_BacktestingPage/components/BacktestChart.tsx @@ -69,6 +69,25 @@ const BacktestChart = ({ return null; }; + // Y축 포맷터 - 자산 추이의 경우 더 간결하게 표시 + const yAxisFormatter = (value: number): string => { + if (label === "월별 자산 추이") { + // 억 단위로 표시 + if (value >= 100000000) { + return `${(value / 100000000).toFixed(1)}억`; + } + // 천만 단위로 표시 + if (value >= 10000000) { + return `${(value / 10000000).toFixed(1)}천만`; + } + // 만 단위로 표시 + if (value >= 10000) { + return `${(value / 10000).toFixed(1)}만`; + } + } + return valueFormatter(value); + }; + return ( @@ -89,7 +108,7 @@ const BacktestChart = ({ stroke="#9aa0a6" style={{ fontSize: "12px" }} tick={{ fill: "#9aa0a6" }} - tickFormatter={valueFormatter} + tickFormatter={yAxisFormatter} /> } /> diff --git a/src/_BacktestingPage/hooks/useProgress.ts b/src/_BacktestingPage/hooks/useProgress.ts index 0ac5c4f..885132e 100644 --- a/src/_BacktestingPage/hooks/useProgress.ts +++ b/src/_BacktestingPage/hooks/useProgress.ts @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import type { ApiResponse } from "@/lib/apis/types"; interface UseProgressProps { isPending: boolean; @@ -10,6 +11,10 @@ export const useProgress = ({ isPending, data, error }: UseProgressProps) => { const [progress, setProgress] = useState(0); const [showResult, setShowResult] = useState(false); + // data가 ApiResponse 형식이고 isSuccess가 false인지 확인 + const isErrorResponse = + data && typeof data === "object" && "isSuccess" in data && data.isSuccess === false; + // Progress 진행률 관리 useEffect(() => { let timer1: NodeJS.Timeout | null = null; @@ -31,16 +36,19 @@ export const useProgress = ({ isPending, data, error }: UseProgressProps) => { timer2 = setTimeout(() => { setProgress(66); }, 500); - } else if (data && !error) { - // 응답이 오면 100%로 진행 - timer3 = setTimeout(() => { - setProgress(100); - // 100%가 된 후 데이터 렌더링 - timer4 = setTimeout(() => { - setShowResult(true); - }, 350); - }, 100); - } else if (error) { + } else if (data && !error && !isErrorResponse) { + // 응답이 오고 에러가 아니며 isSuccess가 true인 경우에만 100%로 진행 + const apiData = data as ApiResponse; + if (apiData.isSuccess !== false) { + timer3 = setTimeout(() => { + setProgress(100); + // 100%가 된 후 데이터 렌더링 + timer4 = setTimeout(() => { + setShowResult(true); + }, 350); + }, 100); + } + } else if (error || isErrorResponse) { // 에러 발생 시 progress 초기화 setProgress(0); setShowResult(false); @@ -52,7 +60,7 @@ export const useProgress = ({ isPending, data, error }: UseProgressProps) => { if (timer3) clearTimeout(timer3); if (timer4) clearTimeout(timer4); }; - }, [isPending, data, error]); + }, [isPending, data, error, isErrorResponse]); return { progress, showResult }; }; diff --git a/src/_BacktestingPage/utils/backtestFormSchema.ts b/src/_BacktestingPage/utils/backtestFormSchema.ts index 4d72bb9..09f9cb3 100644 --- a/src/_BacktestingPage/utils/backtestFormSchema.ts +++ b/src/_BacktestingPage/utils/backtestFormSchema.ts @@ -1,15 +1,15 @@ import { z } from "zod"; import { differenceInMonths, differenceInYears } from "date-fns"; -const MIN_DATE = new Date("1900-01-01"); const TODAY = new Date(); +const MIN_DATE = new Date("1990-01-01"); // 1990년 1월 1일 const MIN_MONTHS_DIFF = 3; // 최소 3개월 const MAX_YEARS_DIFF = 10; // 최대 10년 export const backtestFormSchema = z .object({ startDate: z.date().refine((date) => date >= MIN_DATE, { - message: "시작일은 1900-01-01 이후여야 합니다.", + message: "시작일은 1990년 1월 1일 이후여야 합니다.", }), endDate: z.date().refine((date) => date <= TODAY, { message: "종료일은 오늘 이전이어야 합니다.", diff --git a/src/lib/apis/getIndex.ts b/src/lib/apis/getIndex.ts index 4312376..01c556b 100644 --- a/src/lib/apis/getIndex.ts +++ b/src/lib/apis/getIndex.ts @@ -18,8 +18,9 @@ export const getIndexData = async (marketType: "KOSPI" | "KOSDAQ"): Promise { const response = await instance.get(API_ENDPOINTS.stockData(stockCode, startDate, endDate)); - if (!response.data.isSuccess) { - throw new Error(response.data.message); + if (response.data.isSuccess === false) { + const errorMessage = response.data.message || "주식 상세 데이터 조회 중 오류가 발생했습니다."; + throw new Error(errorMessage); } return response.data.result as StockData; diff --git a/src/lib/apis/getStockList.ts b/src/lib/apis/getStockList.ts index 9f99559..735f9d7 100644 --- a/src/lib/apis/getStockList.ts +++ b/src/lib/apis/getStockList.ts @@ -1,11 +1,16 @@ import { API_ENDPOINTS } from "@/constants/api"; import { instance } from "@/utils/instance"; +import type { ApiResponse } from "@/lib/apis/types"; +import type { StockListResponse } from "@/_MarketsPage/types/marketItem"; export const getStockList = async (page: number, size: number) => { - const response = await instance.get(API_ENDPOINTS.stockList(page, size)); + const response = await instance.get>( + API_ENDPOINTS.stockList(page, size) + ); - if (!response.data.isSuccess) { - throw new Error(response.data.message); + if (response.data.isSuccess === false) { + const errorMessage = response.data.message || "주식 목록 조회 중 오류가 발생했습니다."; + throw new Error(errorMessage); } return response.data.result; diff --git a/src/lib/apis/postBacktest.ts b/src/lib/apis/postBacktest.ts index e293e9d..a2ca469 100644 --- a/src/lib/apis/postBacktest.ts +++ b/src/lib/apis/postBacktest.ts @@ -2,30 +2,70 @@ import { API_ENDPOINTS } from "@/constants/api"; import { instance } from "@/utils/instance"; import type { BacktestRequest, BacktestResult } from "@/_BacktestingPage/types/backtestFormType"; import type { ApiResponse } from "@/lib/apis/types"; +import type { AxiosError } from "axios"; export const postBacktest = async (data: BacktestRequest): Promise> => { - const response = await instance.post>(API_ENDPOINTS.backtest(), data); + try { + const response = await instance.post>( + API_ENDPOINTS.backtest(), + data + ); - // response.data가 없거나 구조가 다른 경우 처리 - if (!response.data) { - throw new Error("응답 데이터가 없습니다."); - } + // response.data가 없거나 구조가 다른 경우 처리 + if (!response.data) { + throw new Error("응답 데이터가 없습니다."); + } - // 응답이 문자열인 경우 JSON 파싱 시도 - let parsedData = response.data; - if (typeof response.data === "string") { - try { - parsedData = JSON.parse(response.data); - } catch { - throw new Error("응답 데이터 파싱에 실패했습니다."); + // 응답이 문자열인 경우 JSON 파싱 시도 + let parsedData = response.data; + if (typeof response.data === "string") { + try { + parsedData = JSON.parse(response.data); + } catch { + throw new Error("응답 데이터 파싱에 실패했습니다."); + } } - } - // isSuccess가 false인 경우에만 에러 throw - if (parsedData.isSuccess === false) { - const errorMessage = parsedData.message || "백테스트 수행 중 오류가 발생했습니다."; - throw new Error(errorMessage); - } + // isSuccess가 false인 경우에만 에러 throw + if (parsedData.isSuccess === false) { + const errorMessage = parsedData.message || "백테스트 수행 중 오류가 발생했습니다."; + throw new Error(errorMessage); + } + + return parsedData; + } catch (error) { + // AxiosError인 경우 response.data에서 message 추출 시도 + const axiosError = error as AxiosError>; + if (axiosError.response?.data) { + let errorResponse = axiosError.response.data; - return parsedData; + // 응답이 문자열인 경우 JSON 파싱 시도 + if (typeof errorResponse === "string") { + try { + errorResponse = JSON.parse(errorResponse); + } catch { + // 파싱 실패 시 그대로 진행 + } + } + + // ApiResponse 형식이고 isSuccess가 false인 경우 + if ( + errorResponse && + typeof errorResponse === "object" && + "isSuccess" in errorResponse && + errorResponse.isSuccess === false + ) { + const errorMessage = errorResponse.message || "백테스트 수행 중 오류가 발생했습니다."; + throw new Error(errorMessage); + } + } + + // 이미 Error 객체인 경우 (우리가 throw한 Error 또는 다른 Error) + if (error instanceof Error) { + throw error; + } + + // 그 외의 경우 기본 에러 메시지 + throw new Error("백테스트 수행 중 오류가 발생했습니다."); + } }; diff --git a/src/lib/apis/searchAssets.ts b/src/lib/apis/searchAssets.ts index f51ed06..fef239a 100644 --- a/src/lib/apis/searchAssets.ts +++ b/src/lib/apis/searchAssets.ts @@ -12,8 +12,9 @@ export const searchAssets = async (keyword: string) => { API_ENDPOINTS.searchAssets(keyword.trim()) ); - if (!response.data.isSuccess) { - throw new Error(response.data.message); + if (response.data.isSuccess === false) { + const errorMessage = response.data.message || "종목 검색 중 오류가 발생했습니다."; + throw new Error(errorMessage); } return response.data.result; }; diff --git a/src/pages/BacktestingPage.tsx b/src/pages/BacktestingPage.tsx index b55a2d0..18129e2 100644 --- a/src/pages/BacktestingPage.tsx +++ b/src/pages/BacktestingPage.tsx @@ -40,18 +40,29 @@ const BacktestingPage = () => { const resultRef = useRef(null); const errorRef = useRef(null); + // isSuccess가 false인 경우도 에러로 처리 + const hasError = error || (data && data.isSuccess === false); + const errorMessage = error + ? error instanceof Error + ? error.message + : (error as AxiosError).response?.data?.detail || + "알 수 없는 오류가 발생했습니다." + : data && data.isSuccess === false + ? data.message || "백테스트 수행 중 오류가 발생했습니다." + : ""; + // 에러나 결과가 나오면 스크롤을 아래로 내리기 useEffect(() => { if (showResult && resultRef.current) { setTimeout(() => { resultRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 100); - } else if (error && !isPending && errorRef.current) { + } else if (hasError && !isPending && errorRef.current) { setTimeout(() => { errorRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 100); } - }, [showResult, error, isPending]); + }, [showResult, hasError, isPending]); const handleSubmit = form.handleSubmit((formData) => { // 버튼이 화면 상단에 오도록 스크롤 @@ -112,8 +123,8 @@ const BacktestingPage = () => { )} - {/* 에러 상태 */} - {error && !isPending && progress === 0 && ( + {/* 에러 상태 - error가 있거나 data.isSuccess가 false인 경우 */} + {hasError && !isPending && progress === 0 && (
@@ -122,12 +133,7 @@ const BacktestingPage = () => {

백테스트 수행 중 오류가 발생했습니다

-

- {error instanceof Error - ? error.message - : (error as AxiosError).response?.data?.detail || - "알 수 없는 오류가 발생했습니다."} -

+

{errorMessage}