diff --git a/src/_MainPage/apis/getIndex.ts b/src/_MainPage/apis/getIndex.ts new file mode 100644 index 0000000..03c9115 --- /dev/null +++ b/src/_MainPage/apis/getIndex.ts @@ -0,0 +1,23 @@ +import { format, subDays } from "date-fns"; +import { API_ENDPOINTS } from "@/constants/api"; +import { instance } from "@/utils/instance"; +import type { ApiResponse } from "@/lib/apis/types"; +import type { MarketIndex } from "@/_MainPage/types/MarketIndexType"; + +/** + * 지수 데이터 조회 API + * @param marketType - 시장 타입 ("KOSPI" | "KOSDAQ") + * @returns Promise - 지수 데이터 + */ +export const getIndexData = async (marketType: "KOSPI" | "KOSDAQ"): Promise => { + const today = new Date(); + const endDate = format(today, "yyyy-MM-dd"); + const startDate = format(subDays(today, 14), "yyyy-MM-dd"); + + const response = await instance.get>( + API_ENDPOINTS.indexPeriod(marketType, startDate, endDate) + ); + + // ApiResponse의 result 필드 반환 + return response.data.result; +}; diff --git a/src/_MainPage/components/MarketIndexCard.tsx b/src/_MainPage/components/MarketIndexCard.tsx index fc1122b..00c947a 100644 --- a/src/_MainPage/components/MarketIndexCard.tsx +++ b/src/_MainPage/components/MarketIndexCard.tsx @@ -1,79 +1,146 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { YAxis, AreaChart, Area, ResponsiveContainer } from "recharts"; +import { Spinner } from "@/components/ui/spinner"; import type { MarketType, MarketIndex } from "@/_MainPage/types/MarketIndexType"; +import type { AxiosError } from "axios"; +import type { ApiErrorResponse } from "@/lib/apis/types"; type MarketIndexCardProps = { marketType: MarketType; - marketIndex: MarketIndex; + marketIndex: MarketIndex | null; + isLoading: boolean; + error: unknown; }; -const getChangeColor = (direction: MarketIndex["direction"]) => { - // 변동성 색상을 결정하는 함수 - switch (direction) { - case "up": - return "text-red-500"; - case "down": - return "text-blue-500"; - default: - return "text-gray-500"; - } +const getChangeColor = (changeRate: number) => { + if (changeRate < 0) { + return "text-blue-500"; + } else return "text-red-500"; }; -const getChartColor = (direction: MarketIndex["direction"]) => { - // 차트 색상을 결정하는 함수 - switch (direction) { - case "up": - return "#EF4444"; - case "down": - return "#3B82F6"; - default: - return "#9CA3AF"; + +const getChartColor = (changeRate: number) => { + if (changeRate < 0) { + return "#3B82F6"; + } else { + return "#EF4444"; } }; -const MarketIndexCard = ({ marketType, marketIndex }: MarketIndexCardProps) => { - const isUp = marketIndex.direction === "up"; +const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIndexCardProps) => { + // 로딩 상태 + if (isLoading) { + return ( + + + {marketType} + + +
+ +
+
+
+ ); + } + + // 에러 상태 + if (error) { + const axiosError = error as AxiosError; + const errorMessage = + axiosError.response?.data?.detail || "데이터를 불러오는 중 오류가 발생했습니다."; + + return ( + + + {marketType} + + +
+

{errorMessage}

+
+
+
+ ); + } + + // 데이터가 없는 경우 + if (!marketIndex) { + return ( + + + {marketType} + + +
+

데이터가 없습니다.

+
+
+
+ ); + } + + // 가장 최근 데이터 (배열의 마지막 항목) + const latestData = marketIndex.data[marketIndex.data.length - 1]; + + if (!latestData) { + return ( + + + {marketType} + + +
+

데이터가 없습니다.

+
+
+
+ ); + } + + const isUp = latestData.changeRate > 0; const arrow = isUp ? "▲" : "▼"; - const sign = isUp ? "+" : "-"; + const sign = isUp ? "+" : ""; + + // 차트 데이터를 recharts 형식으로 변환 + const chartData = marketIndex.data.map((item) => ({ + date: item.baseDate, + value: item.closePrice, + })); + return ( - {/* 마켓 종류(KOSPI/KOSDAQ) */} {marketType} -

{marketIndex.value}

- {/* 상승여부에 따라 다른 색상으로 변동성 렌더링 */} -

- {arrow} {marketIndex.change.toFixed(2)} ({sign} - {marketIndex.changePercent.toFixed(2)}%) +

{latestData.closePrice.toLocaleString()}

+

+ {arrow} {Math.abs(latestData.changeAmount).toFixed(2)} ({sign} + {latestData.changeRate.toFixed(2)}%)

- {/* 차트 영역 */} - - {/* 그래프의 변동성을 더 크게 하기 위함 */} + - {/* 차트 아래 그래디언트 색상 정의 */} - {/* 차트 선 영역 */} getIndexData("KOSPI"), + }); + + // KOSDAQ 데이터 조회 - 독립적으로 실행 + const { + data: kosdaqData, + isLoading: isKosdaqLoading, + error: kosdaqError, + } = useQuery({ + queryKey: ["indexData", "KOSDAQ"], + queryFn: () => getIndexData("KOSDAQ"), + }); + + // 에러 처리 (개발 환경에서만 로깅) + if (__DEV__ && kospiError) { + const error = kospiError as AxiosError; + if (error.response?.data) { + console.error("KOSPI 데이터 조회 에러:", error.response.data.detail); + } + } + + if (__DEV__ && kosdaqError) { + const error = kosdaqError as AxiosError; + if (error.response?.data) { + console.error("KOSDAQ 데이터 조회 에러:", error.response.data.detail); + } + } + return (
- {/* KOSPI 카드 */} - - {/* KOSDAQ 카드 */} - + {/* KOSPI 카드 - 독립적인 로딩/에러 상태 */} + + {/* KOSDAQ 카드 - 독립적인 로딩/에러 상태 */} +
); diff --git a/src/_MainPage/mocks/marketData.ts b/src/_MainPage/mocks/marketData.ts deleted file mode 100644 index 9dc8c91..0000000 --- a/src/_MainPage/mocks/marketData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { MarketIndexData } from "@/_MainPage/types/MarketIndexType"; - -export const marketData: MarketIndexData = { - KOSPI: { - value: 2716.26, - change: 3.18, - changePercent: 0.12, - direction: "up", - chartData: [ - { date: "2025-06-06", value: 2703 }, - { date: "2025-06-07", value: 2707 }, - { date: "2025-06-08", value: 2713 }, - { date: "2025-06-09", value: 2710 }, - { date: "2025-06-10", value: 2716.26 }, - ], - }, - KOSDAQ: { - value: 868.43, - change: 1.12, - changePercent: 0.18, - direction: "down", - chartData: [ - { date: "2025-06-06", value: 872 }, - { date: "2025-06-07", value: 869 }, - { date: "2025-06-08", value: 870 }, - { date: "2025-06-09", value: 872 }, - { date: "2025-06-10", value: 868.43 }, - ], - }, -}; diff --git a/src/_MainPage/types/MarketIndexType.ts b/src/_MainPage/types/MarketIndexType.ts index abd6e75..f8f375b 100644 --- a/src/_MainPage/types/MarketIndexType.ts +++ b/src/_MainPage/types/MarketIndexType.ts @@ -1,12 +1,17 @@ export type MarketType = "KOSPI" | "KOSDAQ"; -export type ChartDataPoint = { date: string; value: number }; - export type MarketIndex = { - value: number; - change: number; - changePercent: number; - direction: "up" | "down"; - chartData: ChartDataPoint[]; + marketType: MarketType; + startDate: string; + endDate: string; + data: { + marketType: MarketType; + baseDate: string; + openPrice: number; + closePrice: number; + highPrice: number; + lowPrice: number; + changeAmount: number; + changeRate: number; + }[]; }; -export type MarketIndexData = Record; diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..055ac76 --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ); +} + +export { Spinner };