From 5dc9f32d3e2aa64f9ce6fd200e9a88184939944f Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 16:39:53 +0900 Subject: [PATCH 01/11] =?UTF-8?q?type:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=97=90=20=EB=A7=9E=EA=B2=8C=20api=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_MarketDetailPage/datas/stockSample.ts | 2 +- src/_MarketDetailPage/types/stockDataType.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_MarketDetailPage/datas/stockSample.ts b/src/_MarketDetailPage/datas/stockSample.ts index a26bd08..ca60ae0 100644 --- a/src/_MarketDetailPage/datas/stockSample.ts +++ b/src/_MarketDetailPage/datas/stockSample.ts @@ -9,7 +9,7 @@ export const sampleData: StockData = { listedShares: 5969782550, marketCap: 437200000000000, currentPrice: 109800, - priceHistory: [ + stockPriceList: [ { baseDate: "20251021", openPrice: 108000, diff --git a/src/_MarketDetailPage/types/stockDataType.ts b/src/_MarketDetailPage/types/stockDataType.ts index 503d9ee..19a1e2f 100644 --- a/src/_MarketDetailPage/types/stockDataType.ts +++ b/src/_MarketDetailPage/types/stockDataType.ts @@ -1,4 +1,4 @@ -export interface PriceHistory { +export interface StockPriceList { baseDate: string; openPrice: number; closePrice: number; @@ -17,7 +17,7 @@ export interface StockData { listedShares: number; marketCap: number; currentPrice: number; - priceHistory: PriceHistory[]; + stockPriceList: StockPriceList[]; } export type Period = "1W" | "1M" | "1Y" | "10Y"; From 23d3b46c6996fd4495f027b78faa497501e8a68f Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 16:41:07 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=A2=85=EB=AA=A9=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20API=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20=ED=9B=85=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/apis/getStockDetail.ts | 33 ++++++++++++++++++++++++++++++ src/lib/hooks/useGetStockDetail.ts | 12 +++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/lib/apis/getStockDetail.ts create mode 100644 src/lib/hooks/useGetStockDetail.ts diff --git a/src/lib/apis/getStockDetail.ts b/src/lib/apis/getStockDetail.ts new file mode 100644 index 0000000..e092e01 --- /dev/null +++ b/src/lib/apis/getStockDetail.ts @@ -0,0 +1,33 @@ +import type { Period, StockData } from "@/_MarketDetailPage/types/stockDataType"; +import { API_ENDPOINTS } from "@/constants/api"; +import { instance } from "@/utils/instance"; +import { format, subDays, subMonths, subYears } from "date-fns"; + +export const getStockDetail = async (stockCode: string, period: Period) => { + const endDate = format(new Date(), "yyyy-MM-dd"); + let startDate: string; + switch (period) { + case "1W": + startDate = format(subDays(new Date(), 7), "yyyy-MM-dd"); + break; + case "1M": + startDate = format(subMonths(new Date(), 1), "yyyy-MM-dd"); + break; + case "1Y": + startDate = format(subYears(new Date(), 1), "yyyy-MM-dd"); + break; + case "10Y": + startDate = format(subYears(new Date(), 10), "yyyy-MM-dd"); + break; + default: + startDate = format(subMonths(new Date(), 1), "yyyy-MM-dd"); + } + + const response = await instance.get(API_ENDPOINTS.stockData(stockCode, startDate, endDate)); + + if (!response.data.isSuccess) { + throw new Error(response.data.message); + } + + return response.data.result as StockData; +}; diff --git a/src/lib/hooks/useGetStockDetail.ts b/src/lib/hooks/useGetStockDetail.ts new file mode 100644 index 0000000..738cff2 --- /dev/null +++ b/src/lib/hooks/useGetStockDetail.ts @@ -0,0 +1,12 @@ +import type { Period } from "@/_MarketDetailPage/types/stockDataType"; +import { getStockDetail } from "@/lib/apis/getStockDetail"; +import { useQuery } from "@tanstack/react-query"; + +export const useGetStockDetail = (stockCode: string, period: Period) => { + const { data, isLoading, error } = useQuery({ + queryKey: ["stockDetail", stockCode, period], + queryFn: () => getStockDetail(stockCode, period), + }); + + return { data, isLoading, error }; +}; From 27bb181966ab01af01db130ecefeafccc7c574f2 Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 16:41:27 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/StockChart.tsx | 39 ++--- src/pages/MarketDetailPage.tsx | 141 +++++++++++------- 2 files changed, 104 insertions(+), 76 deletions(-) diff --git a/src/_MarketDetailPage/components/StockChart.tsx b/src/_MarketDetailPage/components/StockChart.tsx index 2aab303..2371d08 100644 --- a/src/_MarketDetailPage/components/StockChart.tsx +++ b/src/_MarketDetailPage/components/StockChart.tsx @@ -1,30 +1,23 @@ -import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar"; import ReactECharts from "echarts-for-react"; import { type ChartType, type Period, - type PriceHistory, + type StockPriceList, } from "@/_MarketDetailPage/types/stockDataType"; -import { useState, useRef } from "react"; +import { useRef, useEffect } from "react"; interface StockChartProps { - stockData: PriceHistory[]; + stockData: StockPriceList[]; + chartType: ChartType; + period?: Period; } -const StockChart = ({ stockData }: StockChartProps) => { - const [period, setPeriod] = useState("1M"); - const [chartType, setChartType] = useState("candlestick"); +const StockChart = ({ stockData, chartType, period = "1M" }: StockChartProps) => { const isFirstRender = useRef(true); - const handleChangePeriod = (value: string) => { - setPeriod(value as Period); + useEffect(() => { isFirstRender.current = false; - }; - - const handleChangeChartType = (value: string) => { - setChartType(value as ChartType); - isFirstRender.current = false; - }; + }, [chartType, period]); const formatDate = (dateString: string) => { const year = dateString.substring(0, 4); @@ -38,15 +31,17 @@ const StockChart = ({ stockData }: StockChartProps) => { } }; - const dates = stockData.map((item) => formatDate(item.baseDate)); - const candleData = stockData.map((item) => [ + const reversedData = [...stockData].reverse(); + + const dates = reversedData.map((item) => formatDate(item.baseDate)); + const candleData = reversedData.map((item) => [ item.openPrice, item.closePrice, item.lowPrice, item.highPrice, ]); - const lows = stockData.map((d) => d.lowPrice); - const highs = stockData.map((d) => d.highPrice); + const lows = reversedData.map((d) => d.lowPrice); + const highs = reversedData.map((d) => d.highPrice); const option = { animation: isFirstRender.current, @@ -99,7 +94,7 @@ const StockChart = ({ stockData }: StockChartProps) => { }, series: { type: chartType === "candlestick" ? "candlestick" : "line", - data: chartType === "candlestick" ? candleData : stockData.map((item) => item.closePrice), + data: chartType === "candlestick" ? candleData : reversedData.map((item) => item.closePrice), smooth: false, itemStyle: chartType === "candlestick" @@ -138,10 +133,6 @@ const StockChart = ({ stockData }: StockChartProps) => { }; return (
-
); diff --git a/src/pages/MarketDetailPage.tsx b/src/pages/MarketDetailPage.tsx index 946b271..120e458 100644 --- a/src/pages/MarketDetailPage.tsx +++ b/src/pages/MarketDetailPage.tsx @@ -1,53 +1,100 @@ -import { useState, useEffect, useMemo } from "react"; -import { format, subDays } from "date-fns"; -import { type PriceHistory, type StockData } from "@/_MarketDetailPage/types/stockDataType"; +import { useState, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { type Period, type ChartType } from "@/_MarketDetailPage/types/stockDataType"; import DetailItem from "@/_MarketDetailPage/components/DetailItem"; -import { sampleData } from "@/_MarketDetailPage/datas/stockSample"; import StockChart from "@/_MarketDetailPage/components/StockChart"; +import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar"; +import { useGetStockDetail } from "@/lib/hooks/useGetStockDetail"; + const MarketDetailPage = () => { - const [stockData, setStockData] = useState(null); - const [todayData, setTodayData] = useState(); - const [prevData, setPrevData] = useState(); - - // 데이터 로딩 (실제로는 API 호출) - useEffect(() => { - setStockData(sampleData); - const today = format(new Date(), "yyyyMMdd"); - const yesterday = format(subDays(new Date(), 1), "yyyyMMdd"); - - const foundTodayData = sampleData.priceHistory.find((data) => data.baseDate === today); - if (foundTodayData) { - setTodayData(foundTodayData); + const { code } = useParams<{ code: string }>(); + const [period, setPeriod] = useState("1M"); + const [chartType, setChartType] = useState("candlestick"); + + const { data: stockData, isLoading, error } = useGetStockDetail(code || "", period); + + // API 데이터의 가장 최근 데이터와 그 직전 데이터 찾기 + const { latestData, previousData } = useMemo(() => { + if (!stockData) { + return { latestData: null, previousData: null }; } - const foundPrevData = sampleData.priceHistory.find((data) => data.baseDate === yesterday); - if (foundPrevData) { - setPrevData(foundPrevData); + // stockPriceList 필드명이 다를 수 있으므로 여러 가능성 확인 + const stockPriceList = + stockData.stockPriceList || + (stockData as any).stock_price_list || + (stockData as any)["stock-price-list"] || + (stockData as any).priceHistory || + null; + + if (!stockPriceList) { + return { latestData: null, previousData: null }; } - }, []); + + if (!Array.isArray(stockPriceList)) { + return { latestData: null, previousData: null }; + } + + if (stockPriceList.length === 0) { + return { latestData: null, previousData: null }; + } + + // API 데이터가 이미 현재-과거 순서로 오므로 정렬하지 않고 0, 1번째 인덱스 사용 + const latest = stockPriceList[0] || null; + const previous = stockPriceList[1] || null; + + return { + latestData: latest, + previousData: previous, + }; + }, [stockData]); // 등락 정보에 따른 스타일 계산 const changeInfo = useMemo(() => { - if (!stockData || !todayData) return null; + if (!stockData || !latestData) return null; - if (todayData.changeRate > 0) { + if (latestData.changeRate > 0) { return { color: "text-red-500", icon: "▲" }; - } else if (todayData.changeRate < 0) { + } else if (latestData.changeRate < 0) { return { color: "text-blue-500", icon: "▼" }; } else { return { color: "text-white", icon: null }; } - }, [stockData, todayData]); + }, [stockData, latestData]); - if (!stockData || !changeInfo || !todayData || !prevData) { + // 로딩 상태 + if (isLoading) { return (
- Loading... +
Loading...
); } - const highPriceColor = todayData.highPrice > prevData.highPrice ? "text-red-500" : "text-white"; - const lowPriceColor = todayData.lowPrice < prevData.lowPrice ? "text-blue-500" : "text-white"; + + // 에러 상태 + if (error || !stockData) { + return ( +
+
+ {error ? "데이터를 불러오는 중 오류가 발생했습니다." : "데이터를 찾을 수 없습니다."} +
+
+ ); + } + + // 데이터가 없을 때 + if (!changeInfo || !latestData || !previousData) { + return ( +
+
데이터가 없습니다.
+
+ ); + } + + const highPriceColor = + latestData.highPrice > previousData.highPrice ? "text-red-500" : "text-white"; + const lowPriceColor = + latestData.lowPrice < previousData.lowPrice ? "text-blue-500" : "text-white"; return (
@@ -63,45 +110,35 @@ const MarketDetailPage = () => {
{/* 좌측: 현재가 정보 */}
-
- {stockData.currentPrice.toLocaleString()} -
+
{latestData.closePrice}
전일대비 {changeInfo.icon} - {todayData.changeAmount.toLocaleString()} - ({todayData.changeRate.toFixed(2)}%) + {latestData.changeAmount} + ({latestData.changeRate.toFixed(2)}%)
{/* 우측: 상세 거래 정보 */}
- - - - - + + + + +
{/* 하단: 차트 */}
+ setPeriod(value as Period)} + onChangeChartType={(value) => setChartType(value as ChartType)} + />
- +
From 00e4bbd76d37565de5065da0daa102562f6a2937 Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 16:43:52 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=A9=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=201=EC=B2=9C=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils.ts | 12 ++++++++++++ src/pages/MarketDetailPage.tsx | 36 ++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a5ef193..502c38b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,15 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** + * 숫자를 천 단위 구분자(쉼표)가 포함된 문자열로 포맷팅 + * @param value - 포맷팅할 숫자 + * @returns 포맷팅된 문자열 (예: 1000 -> "1,000") + */ +export function formatNumber(value: number | string | null | undefined): string { + if (value === null || value === undefined) return ""; + const num = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(num)) return String(value); + return num.toLocaleString("ko-KR"); +} diff --git a/src/pages/MarketDetailPage.tsx b/src/pages/MarketDetailPage.tsx index 120e458..7adec80 100644 --- a/src/pages/MarketDetailPage.tsx +++ b/src/pages/MarketDetailPage.tsx @@ -5,6 +5,7 @@ import DetailItem from "@/_MarketDetailPage/components/DetailItem"; import StockChart from "@/_MarketDetailPage/components/StockChart"; import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar"; import { useGetStockDetail } from "@/lib/hooks/useGetStockDetail"; +import { formatNumber } from "@/lib/utils"; const MarketDetailPage = () => { const { code } = useParams<{ code: string }>(); @@ -20,12 +21,7 @@ const MarketDetailPage = () => { } // stockPriceList 필드명이 다를 수 있으므로 여러 가능성 확인 - const stockPriceList = - stockData.stockPriceList || - (stockData as any).stock_price_list || - (stockData as any)["stock-price-list"] || - (stockData as any).priceHistory || - null; + const stockPriceList = stockData.stockPriceList; if (!stockPriceList) { return { latestData: null, previousData: null }; @@ -110,11 +106,13 @@ const MarketDetailPage = () => {
{/* 좌측: 현재가 정보 */}
-
{latestData.closePrice}
+
+ {formatNumber(latestData.closePrice)} +
전일대비 {changeInfo.icon} - {latestData.changeAmount} + {formatNumber(latestData.changeAmount)} ({latestData.changeRate.toFixed(2)}%)
@@ -122,11 +120,23 @@ const MarketDetailPage = () => { {/* 우측: 상세 거래 정보 */}
- - - - - + + + + +
From 559ec45a54200aa6ec9ff66361303f095a1fe8df Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:07:16 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=B0=A8=ED=8A=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20API=20=EC=9A=94=EC=B2=AD=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=A1=9C=EB=94=A9=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ChartFilterBar.tsx | 14 +++- .../components/StockChart.tsx | 72 ++++++++++++++----- src/pages/MarketDetailPage.tsx | 12 ++-- 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/_MarketDetailPage/components/ChartFilterBar.tsx b/src/_MarketDetailPage/components/ChartFilterBar.tsx index d1e1f88..c23f727 100644 --- a/src/_MarketDetailPage/components/ChartFilterBar.tsx +++ b/src/_MarketDetailPage/components/ChartFilterBar.tsx @@ -1,15 +1,23 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChartCandlestick, ChartLine } from "lucide-react"; +import { type Period, type ChartType } from "@/_MarketDetailPage/types/stockDataType"; interface ChartFilterBarProps { + period: Period; + chartType: ChartType; onChangePeriod: (value: string) => void; onChangeChartType: (value: string) => void; } -const ChartFilterBar = ({ onChangePeriod, onChangeChartType }: ChartFilterBarProps) => { +const ChartFilterBar = ({ + period, + chartType, + onChangePeriod, + onChangeChartType, +}: ChartFilterBarProps) => { return (
- + - + { +const StockChart = ({ stockCode, period, chartType }: StockChartProps) => { const isFirstRender = useRef(true); + // period에 해당하는 API 요청 + const { data: stockData, isLoading } = useGetStockDetail(stockCode, period); + useEffect(() => { isFirstRender.current = false; }, [chartType, period]); const formatDate = (dateString: string) => { - const year = dateString.substring(0, 4); - const month = dateString.substring(4, 6); - const day = dateString.substring(6, 8); + const parts = dateString.split("-"); + const year = parts[0]; + const month = parts[1]; + const day = parts[2]; if (period === "10Y") { return `${year}.${month}`; @@ -31,17 +33,48 @@ const StockChart = ({ stockData, chartType, period = "1M" }: StockChartProps) => } }; - const reversedData = [...stockData].reverse(); + // period에 따라 데이터 필터링 (useMemo는 early return 전에 호출) + const filteredDataByPeriod = useMemo(() => { + if (!stockData || !stockData.stockPriceList) { + return []; + } + + const reversedData = [...stockData.stockPriceList].reverse(); + + // 10Y 기간일 때 3개월 단위로 필터링 (1월, 4월, 7월, 11월 1일) + if (period === "10Y") { + return reversedData.filter((item) => { + const parts = item.baseDate.split("-"); + const month = parts[1]; + const day = parts[2]; + + return ( + (month === "01" || month === "04" || month === "07" || month === "11") && day === "01" + ); + }); + } + + return reversedData; + }, [stockData, period]); - const dates = reversedData.map((item) => formatDate(item.baseDate)); - const candleData = reversedData.map((item) => [ + // API 데이터가 없으면 로딩 표시 + if (isLoading || !stockData || !stockData.stockPriceList || filteredDataByPeriod.length === 0) { + return ( +
+ +
+ ); + } + + const dates = filteredDataByPeriod.map((item) => formatDate(item.baseDate)); + const candleData = filteredDataByPeriod.map((item) => [ item.openPrice, item.closePrice, item.lowPrice, item.highPrice, ]); - const lows = reversedData.map((d) => d.lowPrice); - const highs = reversedData.map((d) => d.highPrice); + const lows = filteredDataByPeriod.map((d) => d.lowPrice); + const highs = filteredDataByPeriod.map((d) => d.highPrice); const option = { animation: isFirstRender.current, @@ -94,7 +127,10 @@ const StockChart = ({ stockData, chartType, period = "1M" }: StockChartProps) => }, series: { type: chartType === "candlestick" ? "candlestick" : "line", - data: chartType === "candlestick" ? candleData : reversedData.map((item) => item.closePrice), + data: + chartType === "candlestick" + ? candleData + : filteredDataByPeriod.map((item) => item.closePrice), smooth: false, itemStyle: chartType === "candlestick" diff --git a/src/pages/MarketDetailPage.tsx b/src/pages/MarketDetailPage.tsx index 7adec80..439b6e8 100644 --- a/src/pages/MarketDetailPage.tsx +++ b/src/pages/MarketDetailPage.tsx @@ -3,16 +3,18 @@ import { useParams } from "react-router-dom"; import { type Period, type ChartType } from "@/_MarketDetailPage/types/stockDataType"; import DetailItem from "@/_MarketDetailPage/components/DetailItem"; import StockChart from "@/_MarketDetailPage/components/StockChart"; -import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar"; import { useGetStockDetail } from "@/lib/hooks/useGetStockDetail"; import { formatNumber } from "@/lib/utils"; +import ChartFilterBar from "@/_MarketDetailPage/components/ChartFilterBar"; +import { Spinner } from "@/components/ui/spinner"; const MarketDetailPage = () => { const { code } = useParams<{ code: string }>(); const [period, setPeriod] = useState("1M"); const [chartType, setChartType] = useState("candlestick"); - const { data: stockData, isLoading, error } = useGetStockDetail(code || "", period); + // 항상 오늘 기준 7일 전 ~ 오늘 데이터 요청 (고정) + const { data: stockData, isLoading, error } = useGetStockDetail(code || "", "1W"); // API 데이터의 가장 최근 데이터와 그 직전 데이터 찾기 const { latestData, previousData } = useMemo(() => { @@ -62,7 +64,7 @@ const MarketDetailPage = () => { if (isLoading) { return (
-
Loading...
+
); } @@ -144,11 +146,13 @@ const MarketDetailPage = () => { {/* 하단: 차트 */}
setPeriod(value as Period)} onChangeChartType={(value) => setChartType(value as ChartType)} />
- +
From 2ff0bd7247095af7811c6a3495159c2fb083eb7f Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:08:46 +0900 Subject: [PATCH 06/11] =?UTF-8?q?chore:=2010=EB=85=84=20=EC=A3=BC=EA=B0=80?= =?UTF-8?q?=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_MarketDetailPage/components/StockChart.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_MarketDetailPage/components/StockChart.tsx b/src/_MarketDetailPage/components/StockChart.tsx index 718f2b2..e5f9af1 100644 --- a/src/_MarketDetailPage/components/StockChart.tsx +++ b/src/_MarketDetailPage/components/StockChart.tsx @@ -41,16 +41,13 @@ const StockChart = ({ stockCode, period, chartType }: StockChartProps) => { const reversedData = [...stockData.stockPriceList].reverse(); - // 10Y 기간일 때 3개월 단위로 필터링 (1월, 4월, 7월, 11월 1일) + // 10Y 기간일 때 매 달 1일, 15일, 30일 데이터만 필터링 if (period === "10Y") { return reversedData.filter((item) => { const parts = item.baseDate.split("-"); - const month = parts[1]; const day = parts[2]; - return ( - (month === "01" || month === "04" || month === "07" || month === "11") && day === "01" - ); + return day === "01" || day === "15" || day === "30"; }); } From 82c0ac7a8aa91acf873274487063f924391835ef Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:12:06 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore:=2010=EB=85=84=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=91=90=20=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_MarketDetailPage/components/StockChart.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/_MarketDetailPage/components/StockChart.tsx b/src/_MarketDetailPage/components/StockChart.tsx index e5f9af1..c3a0563 100644 --- a/src/_MarketDetailPage/components/StockChart.tsx +++ b/src/_MarketDetailPage/components/StockChart.tsx @@ -39,19 +39,8 @@ const StockChart = ({ stockCode, period, chartType }: StockChartProps) => { return []; } - const reversedData = [...stockData.stockPriceList].reverse(); - - // 10Y 기간일 때 매 달 1일, 15일, 30일 데이터만 필터링 - if (period === "10Y") { - return reversedData.filter((item) => { - const parts = item.baseDate.split("-"); - const day = parts[2]; - - return day === "01" || day === "15" || day === "30"; - }); - } - - return reversedData; + // 모든 데이터를 역순으로 반환 (10년 데이터도 모두 렌더링) + return [...stockData.stockPriceList].reverse(); }, [stockData, period]); // API 데이터가 없으면 로딩 표시 From e5b46f4b54cac35d4be8b7e9ea79e5e021378d39 Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:12:23 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20=EB=9D=BC=EC=9D=B8=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EA=B8=B0=EB=B3=B8=20=EA=B0=92=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MarketDetailPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/MarketDetailPage.tsx b/src/pages/MarketDetailPage.tsx index 439b6e8..67e7bce 100644 --- a/src/pages/MarketDetailPage.tsx +++ b/src/pages/MarketDetailPage.tsx @@ -11,7 +11,7 @@ import { Spinner } from "@/components/ui/spinner"; const MarketDetailPage = () => { const { code } = useParams<{ code: string }>(); const [period, setPeriod] = useState("1M"); - const [chartType, setChartType] = useState("candlestick"); + const [chartType, setChartType] = useState("line"); // 항상 오늘 기준 7일 전 ~ 오늘 데이터 요청 (고정) const { data: stockData, isLoading, error } = useGetStockDetail(code || "", "1W"); From 2a5a058ab945db47722f4f5f89a5c8f016a91075 Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:49:06 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=EC=A3=BC=EA=B0=80=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20?= =?UTF-8?q?=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_MarketsPage/types/marketItem.ts | 33 ++++++++++++++++++++++------ src/lib/apis/getStockList.ts | 12 ++++++++++ src/lib/hooks/useGetStockList.ts | 11 ++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 src/lib/apis/getStockList.ts create mode 100644 src/lib/hooks/useGetStockList.ts diff --git a/src/_MarketsPage/types/marketItem.ts b/src/_MarketsPage/types/marketItem.ts index d4cad07..75d90e6 100644 --- a/src/_MarketsPage/types/marketItem.ts +++ b/src/_MarketsPage/types/marketItem.ts @@ -1,8 +1,27 @@ -export type MarketItem = { - id: string; - name: string; - code: string; - price: number; +// API 응답 타입 +export interface StockPriceData { + baseDate: string; + openPrice: number; + highPrice: number; + lowPrice: number; + closePrice: number; + changeAmount: number; changeRate: number; - tradeVolume: string; -}; +} + +export interface StockListItem { + stockName: string; + stockCode: string; + isinCode: string; + listedDate: string; + listedShared: number; + marketCap: number; + stockPriceList: StockPriceData[]; + rank: number; +} + +export interface StockListResponse { + content: StockListItem[]; + totalElements?: number; + totalPages?: number; +} diff --git a/src/lib/apis/getStockList.ts b/src/lib/apis/getStockList.ts new file mode 100644 index 0000000..9f99559 --- /dev/null +++ b/src/lib/apis/getStockList.ts @@ -0,0 +1,12 @@ +import { API_ENDPOINTS } from "@/constants/api"; +import { instance } from "@/utils/instance"; + +export const getStockList = async (page: number, size: number) => { + const response = await instance.get(API_ENDPOINTS.stockList(page, size)); + + if (!response.data.isSuccess) { + throw new Error(response.data.message); + } + + return response.data.result; +}; diff --git a/src/lib/hooks/useGetStockList.ts b/src/lib/hooks/useGetStockList.ts new file mode 100644 index 0000000..0e9a9d5 --- /dev/null +++ b/src/lib/hooks/useGetStockList.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { getStockList } from "@/lib/apis/getStockList"; + +export const useGetStockList = (page: number, size: number) => { + const { data, isLoading, error } = useQuery({ + queryKey: ["stockList", page, size], + queryFn: () => getStockList(page, size), + }); + + return { data, isLoading, error }; +}; From d897c2723bc4fad2f42092181d390e97ea7b4ac0 Mon Sep 17 00:00:00 2001 From: jjamming Date: Mon, 17 Nov 2025 17:49:24 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20API=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_MarketsPage/components/MarketList.tsx | 27 ++++---- src/components/Pagination.tsx | 17 ++++- src/pages/MarketsPage.tsx | 81 ++++++++++++++++------ 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/_MarketsPage/components/MarketList.tsx b/src/_MarketsPage/components/MarketList.tsx index d061f2e..cfe670b 100644 --- a/src/_MarketsPage/components/MarketList.tsx +++ b/src/_MarketsPage/components/MarketList.tsx @@ -1,8 +1,9 @@ -import type { MarketItem } from "@/_MarketsPage/types/marketItem"; +import type { StockListItem } from "@/_MarketsPage/types/marketItem"; import { useNavigate } from "react-router-dom"; +import { formatNumber } from "@/lib/utils"; interface MarketListProps { - items: MarketItem[]; + items: StockListItem[]; currentPage: number; itemsPerPage: number; } @@ -24,39 +25,41 @@ const MarketList = ({ items, currentPage, itemsPerPage }: MarketListProps) => { 자산명 현재가 등락률 - 거래대금 + 시가총액 {items.map((item, index) => { - const { className, icon } = getChangeInfo(item.changeRate); + const latestPrice = item.stockPriceList?.[0]; + const changeRate = latestPrice?.changeRate ?? 0; + const { className, icon } = getChangeInfo(changeRate); return ( navigate(`/markets/${item.code}`)} + onClick={() => navigate(`/markets/${item.stockCode}`)} > {/* 순번 */} {startIndex + index + 1} {/* 종목명과 코드 */} -
{item.name}
-
{item.code}
+
{item.stockName}
+
{item.stockCode}
{/* 현재가 */} - {item.price.toLocaleString()}원 + {formatNumber(latestPrice?.closePrice ?? 0)}원 {/* 등락률 */} - {icon} {Math.abs(item.changeRate).toFixed(2)}% + {icon} {Math.abs(changeRate).toFixed(2)}% - {/* 거래대금 */} - {item.tradeVolume} + {/* 시가총액 */} + {formatNumber(item.marketCap)} ); })} diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index 7e2208b..35fcf6f 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -5,7 +5,20 @@ interface PaginationProps { } const Pagination = ({ currentPage, totalPages, onPageChange }: PaginationProps) => { - const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1); + // 현재 페이지가 속한 5단위 그룹 계산 + const getVisiblePages = () => { + const groupSize = 5; + // 현재 페이지가 속한 그룹 번호 (1부터 시작) + const groupNumber = Math.ceil(currentPage / groupSize); + // 그룹의 시작 페이지 + const startPage = (groupNumber - 1) * groupSize + 1; + // 그룹의 끝 페이지 (totalPages를 넘지 않도록) + const endPage = Math.min(groupNumber * groupSize, totalPages); + + return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i); + }; + + const visiblePages = getVisiblePages(); return (