Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/_BacktestingPage/components/BacktestChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Card className="bg-white/5 border-white/10 text-white">
<CardHeader>
Expand All @@ -89,15 +108,15 @@ const BacktestChart = ({
stroke="#9aa0a6"
style={{ fontSize: "12px" }}
tick={{ fill: "#9aa0a6" }}
tickFormatter={valueFormatter}
tickFormatter={yAxisFormatter}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={2}
dot={{ fill: color, r: 4 }}
dot={false}
activeDot={{ r: 6 }}
/>
</LineChart>
Expand Down
30 changes: 19 additions & 11 deletions src/_BacktestingPage/hooks/useProgress.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import type { ApiResponse } from "@/lib/apis/types";

interface UseProgressProps {
isPending: boolean;
Expand All @@ -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;
Expand All @@ -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<unknown>;
if (apiData.isSuccess !== false) {
timer3 = setTimeout(() => {
setProgress(100);
// 100%가 된 후 데이터 렌더링
timer4 = setTimeout(() => {
setShowResult(true);
}, 350);
}, 100);
}
} else if (error || isErrorResponse) {
// 에러 발생 시 progress 초기화
setProgress(0);
setShowResult(false);
Expand All @@ -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 };
};
4 changes: 2 additions & 2 deletions src/_BacktestingPage/utils/backtestFormSchema.ts
Original file line number Diff line number Diff line change
@@ -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: "종료일은 오늘 이전이어야 합니다.",
Expand Down
5 changes: 3 additions & 2 deletions src/lib/apis/getIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ export const getIndexData = async (marketType: "KOSPI" | "KOSDAQ"): Promise<Mark
API_ENDPOINTS.indexPeriod(marketType, 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);
}

// ApiResponse의 result 필드 반환
Expand Down
5 changes: 3 additions & 2 deletions src/lib/apis/getStockDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export const getStockDetail = async (stockCode: string, period: Period) => {

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;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/apis/getStockList.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<StockListResponse>>(
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;
Expand Down
78 changes: 59 additions & 19 deletions src/lib/apis/postBacktest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiResponse<BacktestResult>> => {
const response = await instance.post<ApiResponse<BacktestResult>>(API_ENDPOINTS.backtest(), data);
try {
const response = await instance.post<ApiResponse<BacktestResult>>(
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<ApiResponse<BacktestResult>>;
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("백테스트 수행 중 오류가 발생했습니다.");
}
};
5 changes: 3 additions & 2 deletions src/lib/apis/searchAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
26 changes: 16 additions & 10 deletions src/pages/BacktestingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,29 @@ const BacktestingPage = () => {
const resultRef = useRef<HTMLDivElement>(null);
const errorRef = useRef<HTMLDivElement>(null);

// isSuccess가 false인 경우도 에러로 처리
const hasError = error || (data && data.isSuccess === false);
const errorMessage = error
? error instanceof Error
? error.message
: (error as AxiosError<ApiErrorResponse>).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) => {
// 버튼이 화면 상단에 오도록 스크롤
Expand Down Expand Up @@ -112,8 +123,8 @@ const BacktestingPage = () => {
</div>
)}

{/* 에러 상태 */}
{error && !isPending && progress === 0 && (
{/* 에러 상태 - error가 있거나 data.isSuccess가 false인 경우 */}
{hasError && !isPending && progress === 0 && (
<div ref={errorRef}>
<Card className="bg-white/5 border-white/10 text-white">
<CardContent>
Expand All @@ -122,12 +133,7 @@ const BacktestingPage = () => {
<p className="font-semibold text-red-400 text-xl">
백테스트 수행 중 오류가 발생했습니다
</p>
<p className="text-red-300 text-center">
{error instanceof Error
? error.message
: (error as AxiosError<ApiErrorResponse>).response?.data?.detail ||
"알 수 없는 오류가 발생했습니다."}
</p>
<p className="text-red-300 text-center">{errorMessage}</p>
</div>
</div>
</CardContent>
Expand Down