From 8b6298f9ef6658ff736e2bdbe0ad4c30b957e5a7 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 00:43:10 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20candle=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20api=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/apis/candle.api.ts | 26 +++++++++++++++++++ src/entities/market/model/apis/index.ts | 1 + src/entities/market/model/hooks/index.ts | 1 + .../market/model/hooks/useGetCandle.ts | 18 +++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 src/entities/market/model/apis/candle.api.ts create mode 100644 src/entities/market/model/hooks/useGetCandle.ts diff --git a/src/entities/market/model/apis/candle.api.ts b/src/entities/market/model/apis/candle.api.ts new file mode 100644 index 0000000..29bb0b0 --- /dev/null +++ b/src/entities/market/model/apis/candle.api.ts @@ -0,0 +1,26 @@ +import { UPBIT_URL } from "@/shared"; + +interface CandleInterface { + market: string; + to?: string; + count: number; +} + +export const candleAPI = async ({ market, to, count }: CandleInterface) => { + const params = new URLSearchParams({ + market: market, + count: count.toString(), + }); + + if (to) { + params.append("to", to); + } + + const response = await fetch(`${UPBIT_URL}/candles/seconds?${params.toString()}`); + if (!response.ok) { + throw new Error(`Candle API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return data; +}; diff --git a/src/entities/market/model/apis/index.ts b/src/entities/market/model/apis/index.ts index 0c0333e..d83889a 100644 --- a/src/entities/market/model/apis/index.ts +++ b/src/entities/market/model/apis/index.ts @@ -1,3 +1,4 @@ +export * from "./candle.api"; export * from "./market-all.api"; export * from "./market-order.api"; export * from "./ticker.api"; diff --git a/src/entities/market/model/hooks/index.ts b/src/entities/market/model/hooks/index.ts index a81e6bb..0ae7419 100644 --- a/src/entities/market/model/hooks/index.ts +++ b/src/entities/market/model/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./useGetMarketOrder"; export * from "./useMarketOrderRealtime"; export * from "./useMarket"; +export * from "./useGetCandle"; diff --git a/src/entities/market/model/hooks/useGetCandle.ts b/src/entities/market/model/hooks/useGetCandle.ts new file mode 100644 index 0000000..9e7511b --- /dev/null +++ b/src/entities/market/model/hooks/useGetCandle.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; + +import { candleAPI } from "../apis"; + +type CandleAPIParams = { + market: string; + to?: string; + count: number; +}; + +export const useGetCandle = ({ market, to, count }: CandleAPIParams) => { + return useQuery({ + queryKey: ["candle", market, to, count], + queryFn: () => candleAPI({ market, to, count }), + + refetchInterval: 500, + }); +}; From 76a9ba6554d5b11684afca1f31f1272e5626173b Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 00:43:20 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20chart=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/coin-chart/CoinChartDisplay.tsx | 492 +++++------------- 1 file changed, 137 insertions(+), 355 deletions(-) diff --git a/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx b/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx index ff40e44..acf42fa 100644 --- a/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx +++ b/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx @@ -2,381 +2,163 @@ import dynamic from "next/dynamic"; -import type { ApexOptions } from "apexcharts"; +import { useMemo } from "react"; -// ReactApexChart를 dynamic import로 변경 (SSR 비활성화) -const ReactApexChart = dynamic(() => import("react-apexcharts"), { ssr: false }); - -// 더미 데이터: 이미지와 유사한 패턴 (30분 간격, 약 48시간) -const ohlcData = [ - // 11/9 6:00부터 시작 - 초기 하락 구간 - { x: new Date("2025-11-09T06:00:00").getTime(), y: [152300000, 152500000, 151800000, 152000000] }, - { x: new Date("2025-11-09T06:30:00").getTime(), y: [152000000, 152200000, 151600000, 151700000] }, - { x: new Date("2025-11-09T07:00:00").getTime(), y: [151700000, 151900000, 151400000, 151500000] }, - { x: new Date("2025-11-09T07:30:00").getTime(), y: [151500000, 151700000, 151200000, 151400000] }, - { x: new Date("2025-11-09T08:00:00").getTime(), y: [151400000, 151600000, 151100000, 151300000] }, - { x: new Date("2025-11-09T08:30:00").getTime(), y: [151300000, 151500000, 151000000, 151200000] }, - { x: new Date("2025-11-09T09:00:00").getTime(), y: [151200000, 151400000, 150900000, 151000000] }, - { x: new Date("2025-11-09T09:30:00").getTime(), y: [151000000, 151200000, 150700000, 150900000] }, - { x: new Date("2025-11-09T10:00:00").getTime(), y: [150900000, 151100000, 150600000, 150800000] }, - { x: new Date("2025-11-09T10:30:00").getTime(), y: [150800000, 151000000, 150500000, 150700000] }, - { x: new Date("2025-11-09T11:00:00").getTime(), y: [150700000, 150900000, 150400000, 150600000] }, - { x: new Date("2025-11-09T11:30:00").getTime(), y: [150600000, 150800000, 150300000, 150500000] }, - - // 횡보 구간 - { x: new Date("2025-11-09T12:00:00").getTime(), y: [150500000, 150700000, 150300000, 150600000] }, - { x: new Date("2025-11-09T12:30:00").getTime(), y: [150600000, 150800000, 150400000, 150500000] }, - { x: new Date("2025-11-09T13:00:00").getTime(), y: [150500000, 150700000, 150300000, 150600000] }, - { x: new Date("2025-11-09T13:30:00").getTime(), y: [150600000, 150800000, 150400000, 150500000] }, - { x: new Date("2025-11-09T14:00:00").getTime(), y: [150500000, 150700000, 150300000, 150400000] }, - { x: new Date("2025-11-09T14:30:00").getTime(), y: [150400000, 150600000, 150200000, 150500000] }, - { x: new Date("2025-11-09T15:00:00").getTime(), y: [150500000, 150700000, 150300000, 150400000] }, - { x: new Date("2025-11-09T15:30:00").getTime(), y: [150400000, 150600000, 150200000, 150300000] }, - - // 급등 구간 시작 - { x: new Date("2025-11-09T16:00:00").getTime(), y: [150300000, 151500000, 150200000, 151200000] }, - { x: new Date("2025-11-09T16:30:00").getTime(), y: [151200000, 152000000, 151000000, 151800000] }, - { x: new Date("2025-11-09T17:00:00").getTime(), y: [151800000, 152500000, 151600000, 152300000] }, - { x: new Date("2025-11-09T17:30:00").getTime(), y: [152300000, 153000000, 152100000, 152800000] }, - { x: new Date("2025-11-09T18:00:00").getTime(), y: [152800000, 153500000, 152600000, 153200000] }, - - // 조정 후 재상승 - { x: new Date("2025-11-09T18:30:00").getTime(), y: [153200000, 153400000, 152800000, 153000000] }, - { x: new Date("2025-11-09T19:00:00").getTime(), y: [153000000, 153200000, 152600000, 152800000] }, - { x: new Date("2025-11-09T19:30:00").getTime(), y: [152800000, 153000000, 152400000, 152700000] }, - { x: new Date("2025-11-09T20:00:00").getTime(), y: [152700000, 152900000, 152300000, 152600000] }, - { x: new Date("2025-11-09T20:30:00").getTime(), y: [152600000, 152800000, 152200000, 152500000] }, - { x: new Date("2025-11-09T21:00:00").getTime(), y: [152500000, 152700000, 152100000, 152400000] }, - { x: new Date("2025-11-09T21:30:00").getTime(), y: [152400000, 152600000, 152000000, 152300000] }, - { x: new Date("2025-11-09T22:00:00").getTime(), y: [152300000, 152500000, 151900000, 152200000] }, - - // 11/10 - 상승 추세 지속 - { x: new Date("2025-11-10T00:00:00").getTime(), y: [152200000, 152800000, 152100000, 152600000] }, - { x: new Date("2025-11-10T00:30:00").getTime(), y: [152600000, 153200000, 152500000, 153000000] }, - { x: new Date("2025-11-10T01:00:00").getTime(), y: [153000000, 153600000, 152900000, 153400000] }, - { x: new Date("2025-11-10T01:30:00").getTime(), y: [153400000, 154000000, 153300000, 153800000] }, - { x: new Date("2025-11-10T02:00:00").getTime(), y: [153800000, 154400000, 153700000, 154200000] }, - { x: new Date("2025-11-10T02:30:00").getTime(), y: [154200000, 154800000, 154100000, 154600000] }, - { x: new Date("2025-11-10T03:00:00").getTime(), y: [154600000, 155200000, 154500000, 155000000] }, - { x: new Date("2025-11-10T03:30:00").getTime(), y: [155000000, 155600000, 154900000, 155400000] }, - { x: new Date("2025-11-10T04:00:00").getTime(), y: [155400000, 156000000, 155300000, 155800000] }, - - // 조정 및 횡보 - { x: new Date("2025-11-10T06:00:00").getTime(), y: [155800000, 156000000, 155400000, 155600000] }, - { x: new Date("2025-11-10T06:30:00").getTime(), y: [155600000, 155800000, 155200000, 155400000] }, - { x: new Date("2025-11-10T07:00:00").getTime(), y: [155400000, 155600000, 155000000, 155200000] }, - { x: new Date("2025-11-10T07:30:00").getTime(), y: [155200000, 155400000, 154800000, 155000000] }, - { x: new Date("2025-11-10T08:00:00").getTime(), y: [155000000, 155200000, 154600000, 154800000] }, - { x: new Date("2025-11-10T08:30:00").getTime(), y: [154800000, 155000000, 154400000, 154600000] }, - { x: new Date("2025-11-10T09:00:00").getTime(), y: [154600000, 154800000, 154200000, 154400000] }, - - // 급락 구간 - { x: new Date("2025-11-10T09:30:00").getTime(), y: [154400000, 154500000, 153500000, 153800000] }, - { x: new Date("2025-11-10T10:00:00").getTime(), y: [153800000, 154000000, 152800000, 153200000] }, - { x: new Date("2025-11-10T10:30:00").getTime(), y: [153200000, 153400000, 152400000, 152800000] }, - - // 회복 및 상승 - { x: new Date("2025-11-10T11:00:00").getTime(), y: [152800000, 153400000, 152700000, 153200000] }, - { x: new Date("2025-11-10T11:30:00").getTime(), y: [153200000, 153800000, 153100000, 153600000] }, - { x: new Date("2025-11-10T12:00:00").getTime(), y: [153600000, 154200000, 153500000, 154000000] }, - { x: new Date("2025-11-10T12:30:00").getTime(), y: [154000000, 154600000, 153900000, 154400000] }, - { x: new Date("2025-11-10T13:00:00").getTime(), y: [154400000, 155000000, 154300000, 154800000] }, - { x: new Date("2025-11-10T13:30:00").getTime(), y: [154800000, 155400000, 154700000, 155200000] }, - { x: new Date("2025-11-10T14:00:00").getTime(), y: [155200000, 155800000, 155100000, 155600000] }, - { x: new Date("2025-11-10T14:30:00").getTime(), y: [155600000, 156200000, 155500000, 156000000] }, - { x: new Date("2025-11-10T15:00:00").getTime(), y: [156000000, 156600000, 155900000, 156400000] }, +import { useGetCandle, useGetMarket } from "@/entities"; +import { Skeleton } from "@/shared"; +import { ApexOptions } from "apexcharts"; - // 11/11 - 최근 상승 및 조정 - { x: new Date("2025-11-11T00:00:00").getTime(), y: [156400000, 157000000, 156300000, 156800000] }, - { x: new Date("2025-11-11T00:30:00").getTime(), y: [156800000, 157400000, 156700000, 157200000] }, - { x: new Date("2025-11-11T01:00:00").getTime(), y: [157200000, 157800000, 157100000, 157600000] }, - { x: new Date("2025-11-11T01:30:00").getTime(), y: [157600000, 158200000, 157500000, 158000000] }, - { x: new Date("2025-11-11T02:00:00").getTime(), y: [158000000, 158600000, 157900000, 158400000] }, - { x: new Date("2025-11-11T02:30:00").getTime(), y: [158400000, 159000000, 158300000, 158800000] }, - { x: new Date("2025-11-11T03:00:00").getTime(), y: [158800000, 159400000, 158700000, 159200000] }, - { x: new Date("2025-11-11T03:30:00").getTime(), y: [159200000, 159800000, 159100000, 159600000] }, - { x: new Date("2025-11-11T04:00:00").getTime(), y: [159600000, 160200000, 159500000, 160000000] }, - { x: new Date("2025-11-11T04:30:00").getTime(), y: [160000000, 160600000, 159900000, 160400000] }, - { x: new Date("2025-11-11T05:00:00").getTime(), y: [160400000, 161000000, 160300000, 160800000] }, - { x: new Date("2025-11-11T05:30:00").getTime(), y: [160800000, 161400000, 160700000, 161200000] }, - { x: new Date("2025-11-11T06:00:00").getTime(), y: [161200000, 161800000, 161100000, 161600000] }, - { x: new Date("2025-11-11T06:30:00").getTime(), y: [161600000, 162200000, 161500000, 162000000] }, - { x: new Date("2025-11-11T07:00:00").getTime(), y: [162000000, 162600000, 161900000, 162400000] }, - { x: new Date("2025-11-11T07:30:00").getTime(), y: [162400000, 163000000, 162300000, 162800000] }, - { x: new Date("2025-11-11T08:00:00").getTime(), y: [162800000, 163400000, 162700000, 163200000] }, - { x: new Date("2025-11-11T08:30:00").getTime(), y: [163200000, 163800000, 163100000, 163600000] }, - { x: new Date("2025-11-11T09:00:00").getTime(), y: [163600000, 164200000, 163500000, 164000000] }, - { x: new Date("2025-11-11T09:30:00").getTime(), y: [164000000, 164600000, 163900000, 164400000] }, - { x: new Date("2025-11-11T10:00:00").getTime(), y: [164400000, 165000000, 164300000, 164800000] }, - { x: new Date("2025-11-11T10:30:00").getTime(), y: [164800000, 165400000, 164700000, 165200000] }, - { x: new Date("2025-11-11T11:00:00").getTime(), y: [165200000, 165800000, 165100000, 165600000] }, - { x: new Date("2025-11-11T11:30:00").getTime(), y: [165600000, 166200000, 165500000, 166000000] }, - { x: new Date("2025-11-11T12:00:00").getTime(), y: [166000000, 166600000, 165900000, 166400000] }, -]; - -const volumeData = [ - { x: new Date("2025-11-09T06:00:00").getTime(), y: 45 }, - { x: new Date("2025-11-09T06:30:00").getTime(), y: 50 }, - { x: new Date("2025-11-09T07:00:00").getTime(), y: 55 }, - { x: new Date("2025-11-09T07:30:00").getTime(), y: 48 }, - { x: new Date("2025-11-09T08:00:00").getTime(), y: 52 }, - { x: new Date("2025-11-09T08:30:00").getTime(), y: 58 }, - { x: new Date("2025-11-09T09:00:00").getTime(), y: 62 }, - { x: new Date("2025-11-09T09:30:00").getTime(), y: 70 }, - { x: new Date("2025-11-09T10:00:00").getTime(), y: 65 }, - { x: new Date("2025-11-09T10:30:00").getTime(), y: 60 }, - { x: new Date("2025-11-09T11:00:00").getTime(), y: 55 }, - { x: new Date("2025-11-09T11:30:00").getTime(), y: 50 }, - { x: new Date("2025-11-09T12:00:00").getTime(), y: 48 }, - { x: new Date("2025-11-09T12:30:00").getTime(), y: 45 }, - { x: new Date("2025-11-09T13:00:00").getTime(), y: 42 }, - { x: new Date("2025-11-09T13:30:00").getTime(), y: 40 }, - { x: new Date("2025-11-09T14:00:00").getTime(), y: 38 }, - { x: new Date("2025-11-09T14:30:00").getTime(), y: 35 }, - { x: new Date("2025-11-09T15:00:00").getTime(), y: 40 }, - { x: new Date("2025-11-09T15:30:00").getTime(), y: 45 }, - { x: new Date("2025-11-09T16:00:00").getTime(), y: 180 }, - { x: new Date("2025-11-09T16:30:00").getTime(), y: 150 }, - { x: new Date("2025-11-09T17:00:00").getTime(), y: 140 }, - { x: new Date("2025-11-09T17:30:00").getTime(), y: 130 }, - { x: new Date("2025-11-09T18:00:00").getTime(), y: 120 }, - { x: new Date("2025-11-09T18:30:00").getTime(), y: 110 }, - { x: new Date("2025-11-09T19:00:00").getTime(), y: 100 }, - { x: new Date("2025-11-09T19:30:00").getTime(), y: 95 }, - { x: new Date("2025-11-09T20:00:00").getTime(), y: 90 }, - { x: new Date("2025-11-09T20:30:00").getTime(), y: 85 }, - { x: new Date("2025-11-09T21:00:00").getTime(), y: 80 }, - { x: new Date("2025-11-09T21:30:00").getTime(), y: 75 }, - { x: new Date("2025-11-09T22:00:00").getTime(), y: 70 }, - { x: new Date("2025-11-10T00:00:00").getTime(), y: 88 }, - { x: new Date("2025-11-10T00:30:00").getTime(), y: 95 }, - { x: new Date("2025-11-10T01:00:00").getTime(), y: 102 }, - { x: new Date("2025-11-10T01:30:00").getTime(), y: 110 }, - { x: new Date("2025-11-10T02:00:00").getTime(), y: 118 }, - { x: new Date("2025-11-10T02:30:00").getTime(), y: 125 }, - { x: new Date("2025-11-10T03:00:00").getTime(), y: 132 }, - { x: new Date("2025-11-10T03:30:00").getTime(), y: 140 }, - { x: new Date("2025-11-10T04:00:00").getTime(), y: 135 }, - { x: new Date("2025-11-10T06:00:00").getTime(), y: 85 }, - { x: new Date("2025-11-10T06:30:00").getTime(), y: 80 }, - { x: new Date("2025-11-10T07:00:00").getTime(), y: 75 }, - { x: new Date("2025-11-10T07:30:00").getTime(), y: 70 }, - { x: new Date("2025-11-10T08:00:00").getTime(), y: 68 }, - { x: new Date("2025-11-10T08:30:00").getTime(), y: 65 }, - { x: new Date("2025-11-10T09:00:00").getTime(), y: 72 }, - { x: new Date("2025-11-10T09:30:00").getTime(), y: 165 }, - { x: new Date("2025-11-10T10:00:00").getTime(), y: 155 }, - { x: new Date("2025-11-10T10:30:00").getTime(), y: 145 }, - { x: new Date("2025-11-10T11:00:00").getTime(), y: 128 }, - { x: new Date("2025-11-10T11:30:00").getTime(), y: 120 }, - { x: new Date("2025-11-10T12:00:00").getTime(), y: 112 }, - { x: new Date("2025-11-10T12:30:00").getTime(), y: 105 }, - { x: new Date("2025-11-10T13:00:00").getTime(), y: 98 }, - { x: new Date("2025-11-10T13:30:00").getTime(), y: 92 }, - { x: new Date("2025-11-10T14:00:00").getTime(), y: 88 }, - { x: new Date("2025-11-10T14:30:00").getTime(), y: 85 }, - { x: new Date("2025-11-10T15:00:00").getTime(), y: 82 }, - { x: new Date("2025-11-11T00:00:00").getTime(), y: 95 }, - { x: new Date("2025-11-11T00:30:00").getTime(), y: 102 }, - { x: new Date("2025-11-11T01:00:00").getTime(), y: 108 }, - { x: new Date("2025-11-11T01:30:00").getTime(), y: 115 }, - { x: new Date("2025-11-11T02:00:00").getTime(), y: 122 }, - { x: new Date("2025-11-11T02:30:00").getTime(), y: 128 }, - { x: new Date("2025-11-11T03:00:00").getTime(), y: 135 }, - { x: new Date("2025-11-11T03:30:00").getTime(), y: 142 }, - { x: new Date("2025-11-11T04:00:00").getTime(), y: 148 }, - { x: new Date("2025-11-11T04:30:00").getTime(), y: 155 }, - { x: new Date("2025-11-11T05:00:00").getTime(), y: 162 }, - { x: new Date("2025-11-11T05:30:00").getTime(), y: 170 }, - { x: new Date("2025-11-11T06:00:00").getTime(), y: 178 }, - { x: new Date("2025-11-11T06:30:00").getTime(), y: 185 }, - { x: new Date("2025-11-11T07:00:00").getTime(), y: 192 }, - { x: new Date("2025-11-11T07:30:00").getTime(), y: 200 }, - { x: new Date("2025-11-11T08:00:00").getTime(), y: 195 }, - { x: new Date("2025-11-11T08:30:00").getTime(), y: 188 }, - { x: new Date("2025-11-11T09:00:00").getTime(), y: 180 }, - { x: new Date("2025-11-11T09:30:00").getTime(), y: 175 }, - { x: new Date("2025-11-11T10:00:00").getTime(), y: 168 }, - { x: new Date("2025-11-11T10:30:00").getTime(), y: 160 }, - { x: new Date("2025-11-11T11:00:00").getTime(), y: 155 }, - { x: new Date("2025-11-11T11:30:00").getTime(), y: 150 }, - { x: new Date("2025-11-11T12:00:00").getTime(), y: 145 }, -]; - -const series = [ - { - type: "candlestick" as const, - name: "BTC/KRW", - data: ohlcData, - }, - { - type: "bar" as const, - name: "Volume", - data: volumeData, - }, -]; +// SSR 비활성화 +const ReactApexChart = dynamic(() => import("react-apexcharts"), { ssr: false }); -const options: ApexOptions = { - chart: { - type: "candlestick" as const, - height: 500, - id: "candles", - toolbar: { - show: true, - tools: { - download: true, - selection: true, - zoom: true, - zoomin: true, - zoomout: true, - pan: true, - reset: true, +export const CoinChartDisplay = () => { + const { market } = useGetMarket(); + + const apiMarket = useMemo(() => { + if (!market) return "KRW-BTC"; // 기본값 설정 (방어 코드) + + // "/"가 포함된 경우 (BTC/KRW 형식일 때) + if (market.includes("/")) { + const [coin, currency] = market.split("/"); // ["BTC", "KRW"] + return `${currency}-${coin}`; // "KRW-BTC" + } + + // 만약 이미 "KRW-BTC" 형식이거나 다른 형식이면 그대로 반환 + return market; + }, [market]); + + const { data: candleData, isLoading } = useGetCandle({ market: apiMarket, count: 50 }); + + // 데이터 변환 로직 (Memoization) + const { prices, volume } = useMemo(() => { + if (!candleData || candleData.length === 0) { + return { prices: [], volume: [] }; + } + + const sortedData = [...candleData].sort( + (a, b) => new Date(a.candle_date_time_kst).getTime() - new Date(b.candle_date_time_kst).getTime(), + ); + + const parseKstToUtc = (kstString: string) => { + return new Date(kstString + "Z").getTime(); + }; + + const pricesData = sortedData.map((item) => ({ + x: parseKstToUtc(item.candle_date_time_kst), + y: [ + item.opening_price, // Open + item.high_price, // High + item.low_price, // Low + item.trade_price, // Close (현재가/종가) + ], + })); + + // 3. 거래량 데이터 변환 + const volumeData = sortedData.map((item) => ({ + x: parseKstToUtc(item.candle_date_time_kst), + y: item.candle_acc_trade_volume.toFixed(3), // 소수점 정리 + })); + + return { prices: pricesData, volume: volumeData }; + }, [candleData]); + + const candleOptions: ApexOptions = { + chart: { + type: "candlestick", + id: "candles", + toolbar: { autoSelected: "pan", show: false }, + zoom: { enabled: false }, + background: "transparent", + }, + theme: { mode: "dark" }, + plotOptions: { + candlestick: { + colors: { + upward: "var(--increase)", + downward: "var(--decrease)", // 하락 (초록/파랑) + }, }, }, - zoom: { enabled: true }, - background: "transparent", - }, - grid: { - show: true, - borderColor: "#2d2d2d", - strokeDashArray: 0, - position: "back", xaxis: { - lines: { - show: true, - }, + type: "datetime", + axisBorder: { show: false }, + axisTicks: { show: false }, + labels: { show: true }, // 메인 차트 X축 라벨 보이기 }, yaxis: { - lines: { - show: true, - }, - }, - }, - plotOptions: { - bar: { - columnWidth: "50%", - colors: { - ranges: [{ from: 0, to: 300, color: "#4a9eff" }], - }, - }, - candlestick: { - colors: { - upward: "#ef5350", - downward: "#26a69a", - }, - wick: { - useFillColor: true, + tooltip: { enabled: true }, + labels: { + formatter: (val) => Math.floor(val).toLocaleString(), }, }, - }, - xaxis: { - type: "datetime" as const, - labels: { - style: { - colors: "#9ca3af", - fontSize: "11px", + grid: { borderColor: "#333" }, + }; + + const barOptions: ApexOptions = { + chart: { + type: "bar", + id: "brush", + brush: { + enabled: true, + target: "candles", }, - datetimeFormatter: { - year: "yyyy", - month: "MMM 'yy", - day: "dd MMM", - hour: "HH:mm", + selection: { + enabled: true, + fill: { color: "#ccc", opacity: 0.4 }, + stroke: { color: "#0D47A1" }, + // 초기 선택 범위: 데이터의 마지막 20% 구간 + xaxis: { + min: prices.length > 30 ? prices[prices.length - 30].x : undefined, + max: prices.length > 0 ? prices[prices.length - 1].x : undefined, + }, }, + background: "transparent", }, - axisBorder: { - show: true, - color: "#2d2d2d", - }, - axisTicks: { - show: true, - color: "#2d2d2d", - }, - }, - yaxis: [ - { - seriesName: "BTC/KRW", - opposite: true, - tooltip: { enabled: false }, - labels: { - style: { - colors: "#9ca3af", - fontSize: "11px", + theme: { mode: "dark" }, + dataLabels: { enabled: false }, + plotOptions: { + bar: { + columnWidth: "100%", + colors: { + ranges: [{ from: 0, to: 1000000000, color: "#555" }], // 거래량 색상 통일 }, - formatter: (value) => { - return `${(value / 1000000).toFixed(1)}M`; - }, - }, - axisBorder: { - show: true, - color: "#2d2d2d", }, }, - { - seriesName: "Volume", - show: false, - min: 0, - max: 400, - }, - ], - tooltip: { - enabled: true, - shared: false, - theme: "dark", - style: { - fontSize: "12px", + stroke: { width: 0 }, + xaxis: { + type: "datetime", + tooltip: { enabled: false }, + axisBorder: { offsetX: 13 }, }, - custom: function ({ seriesIndex, dataPointIndex, w }) { - const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex]; - if (seriesIndex === 0 && data?.y) { - const [open, high, low, close] = data.y; - const change = close - open; - const changePercent = ((change / open) * 100).toFixed(2); - const color = change >= 0 ? "#ef5350" : "#26a69a"; + yaxis: { labels: { show: false } }, + grid: { show: false }, + }; - return ` -
-
BTC/KRW
-
- 시가: - ₩${open.toLocaleString()} -
-
- 고가: - ₩${high.toLocaleString()} -
-
- 저가: - ₩${low.toLocaleString()} -
-
- 종가: - ₩${close.toLocaleString()} -
-
- ${change >= 0 ? "▲" : "▼"} ${Math.abs(change).toLocaleString()} (${changePercent}%) -
-
- `; - } - return ""; - }, - }, - legend: { - show: false, - }, - theme: { - mode: "dark", - }, - annotations: { - xaxis: [], - yaxis: [], - }, -}; + // 로딩 중이거나 데이터가 없을 때 처리 + if (isLoading) return ; + if (!candleData) return null; -export const CoinChartDisplay = () => { return ( -
- +
+
+ +
+ +
+ +
); }; From 5cad1db83db0d4d15a46b51b8d1e3e0e9137dc85 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 03:18:14 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20market=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EB=B3=80=EA=B2=BD=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/utils/formatted-market-name.ts | 12 ++++++++++++ src/features/home/utils/index.ts | 1 + 2 files changed, 13 insertions(+) create mode 100644 src/features/home/utils/formatted-market-name.ts create mode 100644 src/features/home/utils/index.ts diff --git a/src/features/home/utils/formatted-market-name.ts b/src/features/home/utils/formatted-market-name.ts new file mode 100644 index 0000000..24c01da --- /dev/null +++ b/src/features/home/utils/formatted-market-name.ts @@ -0,0 +1,12 @@ +export const formattedMarketName = (market: string | undefined): string => { + if (!market) { + return "KRW-BTC"; // 기본값 설정 + } + + if (market.includes("/")) { + const [coin, currency] = market.split("/"); + return `${currency}-${coin}`; + } + + return market; +}; diff --git a/src/features/home/utils/index.ts b/src/features/home/utils/index.ts new file mode 100644 index 0000000..49d397c --- /dev/null +++ b/src/features/home/utils/index.ts @@ -0,0 +1 @@ +export * from "./formatted-market-name"; From 1c1f98b240af31c616688e021abc15024a37d6ae Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 03:18:29 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20chart=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=83=80=EC=9E=85=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/types/chart.type.ts | 23 +++++++++++++++++++ src/entities/market/model/types/index.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 src/entities/market/model/types/chart.type.ts diff --git a/src/entities/market/model/types/chart.type.ts b/src/entities/market/model/types/chart.type.ts new file mode 100644 index 0000000..9dcd526 --- /dev/null +++ b/src/entities/market/model/types/chart.type.ts @@ -0,0 +1,23 @@ +export type CandleData = { + market: string; + candle_date_time_utc: string; + candle_date_time_kst: string; + opening_price: number; + high_price: number; + low_price: number; + trade_price: number; + timestamp: number; + candle_acc_trade_price: number; + candle_acc_trade_volume: number; + unit: number; +}; + +export type ChartDataPoint = { + x: number; + y: number | number[] | string; +}; + +export type ChartSeriesData = { + prices: ChartDataPoint[]; + volume: ChartDataPoint[]; +}; diff --git a/src/entities/market/model/types/index.ts b/src/entities/market/model/types/index.ts index c4906b2..23db4c4 100644 --- a/src/entities/market/model/types/index.ts +++ b/src/entities/market/model/types/index.ts @@ -1,3 +1,4 @@ +export * from "./chart.type"; export * from "./market-info.type"; export * from "./orderbook-units.type"; export * from "./trade.type"; From 8c1a4b500db231703244020fbcb285b0dbacf0b1 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 03:19:05 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20custom=20hook=EC=9D=84=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20ui=EC=99=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/coin-chart/CoinChartDisplay.tsx | 57 +------------------ src/features/home/hooks/index.ts | 2 + src/features/home/hooks/useChartData.ts | 28 +++++++++ src/features/home/hooks/useChartViewModel.ts | 30 ++++++++++ 4 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 src/features/home/hooks/useChartData.ts create mode 100644 src/features/home/hooks/useChartViewModel.ts diff --git a/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx b/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx index acf42fa..49cfb06 100644 --- a/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx +++ b/src/features/home/components/features/coin-chart/CoinChartDisplay.tsx @@ -2,65 +2,16 @@ import dynamic from "next/dynamic"; -import { useMemo } from "react"; - -import { useGetCandle, useGetMarket } from "@/entities"; import { Skeleton } from "@/shared"; import { ApexOptions } from "apexcharts"; +import { useCoinChartViewModel } from "../../../hooks"; + // SSR 비활성화 const ReactApexChart = dynamic(() => import("react-apexcharts"), { ssr: false }); export const CoinChartDisplay = () => { - const { market } = useGetMarket(); - - const apiMarket = useMemo(() => { - if (!market) return "KRW-BTC"; // 기본값 설정 (방어 코드) - - // "/"가 포함된 경우 (BTC/KRW 형식일 때) - if (market.includes("/")) { - const [coin, currency] = market.split("/"); // ["BTC", "KRW"] - return `${currency}-${coin}`; // "KRW-BTC" - } - - // 만약 이미 "KRW-BTC" 형식이거나 다른 형식이면 그대로 반환 - return market; - }, [market]); - - const { data: candleData, isLoading } = useGetCandle({ market: apiMarket, count: 50 }); - - // 데이터 변환 로직 (Memoization) - const { prices, volume } = useMemo(() => { - if (!candleData || candleData.length === 0) { - return { prices: [], volume: [] }; - } - - const sortedData = [...candleData].sort( - (a, b) => new Date(a.candle_date_time_kst).getTime() - new Date(b.candle_date_time_kst).getTime(), - ); - - const parseKstToUtc = (kstString: string) => { - return new Date(kstString + "Z").getTime(); - }; - - const pricesData = sortedData.map((item) => ({ - x: parseKstToUtc(item.candle_date_time_kst), - y: [ - item.opening_price, // Open - item.high_price, // High - item.low_price, // Low - item.trade_price, // Close (현재가/종가) - ], - })); - - // 3. 거래량 데이터 변환 - const volumeData = sortedData.map((item) => ({ - x: parseKstToUtc(item.candle_date_time_kst), - y: item.candle_acc_trade_volume.toFixed(3), // 소수점 정리 - })); - - return { prices: pricesData, volume: volumeData }; - }, [candleData]); + const { prices, volume, isLoading } = useCoinChartViewModel(50); const candleOptions: ApexOptions = { chart: { @@ -134,9 +85,7 @@ export const CoinChartDisplay = () => { grid: { show: false }, }; - // 로딩 중이거나 데이터가 없을 때 처리 if (isLoading) return ; - if (!candleData) return null; return (
diff --git a/src/features/home/hooks/index.ts b/src/features/home/hooks/index.ts index 1cfbdc1..5526ff1 100644 --- a/src/features/home/hooks/index.ts +++ b/src/features/home/hooks/index.ts @@ -1,3 +1,5 @@ +export * from "./useChartData"; +export * from "./useChartViewModel"; export * from "./useGetMarketInfo"; export * from "./useRealtimeTicker"; export * from "./useMarketInfo"; diff --git a/src/features/home/hooks/useChartData.ts b/src/features/home/hooks/useChartData.ts new file mode 100644 index 0000000..e83a4fc --- /dev/null +++ b/src/features/home/hooks/useChartData.ts @@ -0,0 +1,28 @@ +import { CandleData, ChartSeriesData } from "@/entities"; + +export const useChartData = (candleData: CandleData[] | undefined): ChartSeriesData => { + // 1. 방어 코드: 데이터가 없으면 빈 배열 반환 + if (!candleData || candleData.length === 0) { + return { prices: [], volume: [] }; + } + + const sortedData = [...candleData].sort( + (a, b) => new Date(a.candle_date_time_kst).getTime() - new Date(b.candle_date_time_kst).getTime(), + ); + + const parseKstToUtc = (kstString: string) => { + return new Date(kstString + "Z").getTime(); + }; + + const prices = sortedData.map((item) => ({ + x: parseKstToUtc(item.candle_date_time_kst), + y: [item.opening_price, item.high_price, item.low_price, item.trade_price], + })); + + const volume = sortedData.map((item) => ({ + x: parseKstToUtc(item.candle_date_time_kst), + y: item.candle_acc_trade_volume.toFixed(3), + })); + + return { prices, volume }; +}; diff --git a/src/features/home/hooks/useChartViewModel.ts b/src/features/home/hooks/useChartViewModel.ts new file mode 100644 index 0000000..fc4f48d --- /dev/null +++ b/src/features/home/hooks/useChartViewModel.ts @@ -0,0 +1,30 @@ +import { useGetCandle, useGetMarket } from "@/entities"; + +import { formattedMarketName } from "../utils"; + +import { useChartData } from "./useChartData"; + +export const useCoinChartViewModel = (count: number = 50) => { + const { market } = useGetMarket(); + + const currentMarket = formattedMarketName(market); + + const { + data: candleData, + isLoading, + isError, + } = useGetCandle({ + market: currentMarket, + count, + }); + + const { prices, volume } = useChartData(candleData); + + return { + prices, + volume, + isLoading, + isError, + currentMarket: market, + }; +}; From 8f2a40d3915eb4409eef56e781c7f90b6bf54eae Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 03:19:19 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20candle=20data=20type=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/apis/candle.api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/entities/market/model/apis/candle.api.ts b/src/entities/market/model/apis/candle.api.ts index 29bb0b0..1988737 100644 --- a/src/entities/market/model/apis/candle.api.ts +++ b/src/entities/market/model/apis/candle.api.ts @@ -1,12 +1,14 @@ import { UPBIT_URL } from "@/shared"; +import { CandleData } from "../types"; + interface CandleInterface { market: string; to?: string; count: number; } -export const candleAPI = async ({ market, to, count }: CandleInterface) => { +export const candleAPI = async ({ market, to, count }: CandleInterface): Promise => { const params = new URLSearchParams({ market: market, count: count.toString(), @@ -21,6 +23,6 @@ export const candleAPI = async ({ market, to, count }: CandleInterface) => { throw new Error(`Candle API error: ${response.status} ${response.statusText}`); } - const data = await response.json(); + const data: CandleData[] = await response.json(); return data; }; From 6568ba87c8a4735d9b26c6bcf6307a1bc0d3cd6f Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 17:37:18 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=EA=B0=81=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20API=20=EA=B0=80=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/apis/candle.api.ts | 30 ++++++++++++------- .../market/model/hooks/useGetCandle.ts | 18 ++++++----- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/entities/market/model/apis/candle.api.ts b/src/entities/market/model/apis/candle.api.ts index 1988737..82a28e5 100644 --- a/src/entities/market/model/apis/candle.api.ts +++ b/src/entities/market/model/apis/candle.api.ts @@ -1,24 +1,32 @@ import { UPBIT_URL } from "@/shared"; -import { CandleData } from "../types"; +import { CandleData, CandleParams, MinutesCandleParams } from "../types"; -interface CandleInterface { - market: string; - to?: string; - count: number; -} +export const candleAPI = async (params: CandleParams): Promise => { + const { market, to, count, type } = params; -export const candleAPI = async ({ market, to, count }: CandleInterface): Promise => { - const params = new URLSearchParams({ - market: market, + // URL 파라미터 구성 + const urlParams = new URLSearchParams({ + market, count: count.toString(), }); if (to) { - params.append("to", to); + urlParams.append("to", to); } - const response = await fetch(`${UPBIT_URL}/candles/seconds?${params.toString()}`); + // 타입별 엔드포인트 구성 + let endpoint = `${UPBIT_URL}/candles/${type}`; + + // 분 캔들의 경우 unit을 Path 파라미터로 추가 + if (type === "minutes") { + const unit = (params as MinutesCandleParams).unit; + endpoint = `${UPBIT_URL}/candles/minutes/${unit}`; + } + + const url = `${endpoint}?${urlParams.toString()}`; + + const response = await fetch(url); if (!response.ok) { throw new Error(`Candle API error: ${response.status} ${response.statusText}`); } diff --git a/src/entities/market/model/hooks/useGetCandle.ts b/src/entities/market/model/hooks/useGetCandle.ts index 9e7511b..8ee16bb 100644 --- a/src/entities/market/model/hooks/useGetCandle.ts +++ b/src/entities/market/model/hooks/useGetCandle.ts @@ -1,17 +1,19 @@ import { useQuery } from "@tanstack/react-query"; import { candleAPI } from "../apis"; +import type { CandleParams, MinutesCandleParams } from "../types"; -type CandleAPIParams = { - market: string; - to?: string; - count: number; -}; +export const useGetCandle = (params: CandleParams) => { + const { market, count, type } = params; + + // 분 캔들일 때만 unit을 queryKey에 포함 + const unit = type === "minutes" ? (params as MinutesCandleParams).unit : undefined; + + const queryKey = unit ? ["candle", market, count, type, unit] : ["candle", market, count, type]; -export const useGetCandle = ({ market, to, count }: CandleAPIParams) => { return useQuery({ - queryKey: ["candle", market, to, count], - queryFn: () => candleAPI({ market, to, count }), + queryKey, + queryFn: () => candleAPI(params), refetchInterval: 500, }); From 2f26f99aec8fcb6fe9cd44608e001b9b79afcb9c Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 17:37:29 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20candle=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8B=A4=EC=9E=85=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market/model/types/candle.type.ts | 36 +++++++++++++++++++ src/entities/market/model/types/index.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 src/entities/market/model/types/candle.type.ts diff --git a/src/entities/market/model/types/candle.type.ts b/src/entities/market/model/types/candle.type.ts new file mode 100644 index 0000000..ce71215 --- /dev/null +++ b/src/entities/market/model/types/candle.type.ts @@ -0,0 +1,36 @@ +/** + * 캔들 타입 + * - seconds: 초 단위 (1초 고정) + * - minutes: 분 단위 (1, 3, 5, 10, 15, 30, 60, 240분) + * - days: 일 단위 + * - weeks: 주 단위 + * - months: 월 단위 + */ +export type CandleType = "seconds" | "minutes" | "days" | "weeks" | "months"; + +/** + * 분 단위 캔들 옵션 + * Upbit API에서 지원하는 분 단위 + */ +export type MinuteUnit = 1 | 3 | 5 | 10 | 15 | 30 | 60 | 240; + +type SecondsCandleParams = { + type: "seconds"; +} & BaseCandleParams; + +export type MinutesCandleParams = { + type: "minutes"; + unit: MinuteUnit; +} & BaseCandleParams; + +type OtherCandleParams = { + type: "days" | "weeks" | "months"; +} & BaseCandleParams; + +type BaseCandleParams = { + market: string; + to?: string; + count: number; +}; + +export type CandleParams = SecondsCandleParams | MinutesCandleParams | OtherCandleParams; diff --git a/src/entities/market/model/types/index.ts b/src/entities/market/model/types/index.ts index 23db4c4..e35e8a1 100644 --- a/src/entities/market/model/types/index.ts +++ b/src/entities/market/model/types/index.ts @@ -2,3 +2,4 @@ export * from "./chart.type"; export * from "./market-info.type"; export * from "./orderbook-units.type"; export * from "./trade.type"; +export * from "./candle.type"; From 791bd898bd22e369d46cbc390a5008e54f71cd8d Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 17:38:11 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20button=EC=9D=98=20=EA=B0=92?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=8B=A4=EB=A5=B8=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=9D=98=20=EC=B0=A8=ED=8A=B8=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/button/TimeframeButton.tsx | 8 +++++--- .../features/coin-chart/ChartControls.tsx | 8 ++++---- src/features/home/hooks/useChartViewModel.ts | 13 +++++-------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/features/home/components/common/button/TimeframeButton.tsx b/src/features/home/components/common/button/TimeframeButton.tsx index c5ec380..64c5d03 100644 --- a/src/features/home/components/common/button/TimeframeButton.tsx +++ b/src/features/home/components/common/button/TimeframeButton.tsx @@ -1,9 +1,11 @@ import { Button, Separator } from "@/shared"; +import type { ChartTimeframe } from "../../../store"; + type Props = { - timeframe: string; - selectedTimeframe: string; - setSelectedTimeframe: (timeframe: string) => void; + timeframe: ChartTimeframe; + selectedTimeframe: ChartTimeframe; + setSelectedTimeframe: (timeframe: ChartTimeframe) => void; }; export const TimeframeButton = ({ timeframe, selectedTimeframe, setSelectedTimeframe }: Props) => { diff --git a/src/features/home/components/features/coin-chart/ChartControls.tsx b/src/features/home/components/features/coin-chart/ChartControls.tsx index 09b3255..7265adb 100644 --- a/src/features/home/components/features/coin-chart/ChartControls.tsx +++ b/src/features/home/components/features/coin-chart/ChartControls.tsx @@ -1,15 +1,15 @@ "use client"; -import { useState } from "react"; - import { Separator } from "@/shared"; +import { type ChartTimeframe, useChartStore } from "../../../store"; import { ChartOptionButton, TimeframeButton } from "../../common"; -const timeframes = ["1초", "1분", "5분", "30분", "1시간", "주", "달", "년"]; +const timeframes: ChartTimeframe[] = ["1초", "1분", "5분", "30분", "1시간", "주", "달", "년"]; export const ChartControls = () => { - const [selectedTimeframe, setSelectedTimeframe] = useState("1초"); + const selectedTimeframe = useChartStore((state) => state.selectedTimeframe); + const setSelectedTimeframe = useChartStore((state) => state.setSelectedTimeframe); return (
diff --git a/src/features/home/hooks/useChartViewModel.ts b/src/features/home/hooks/useChartViewModel.ts index fc4f48d..35ab93a 100644 --- a/src/features/home/hooks/useChartViewModel.ts +++ b/src/features/home/hooks/useChartViewModel.ts @@ -1,5 +1,6 @@ import { useGetCandle, useGetMarket } from "@/entities"; +import { useCandleParams } from "../store"; import { formattedMarketName } from "../utils"; import { useChartData } from "./useChartData"; @@ -9,14 +10,10 @@ export const useCoinChartViewModel = (count: number = 50) => { const currentMarket = formattedMarketName(market); - const { - data: candleData, - isLoading, - isError, - } = useGetCandle({ - market: currentMarket, - count, - }); + // 현재 선택된 타임프레임에 따른 API 파라미터 자동 생성 + const candleParams = useCandleParams(currentMarket, count); + + const { data: candleData, isLoading, isError } = useGetCandle(candleParams); const { prices, volume } = useChartData(candleData); From 492f5bdbb12deab46ce9a02e52fae122472fba8b Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 17:39:35 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20chart=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=EC=9D=84=20=EC=A0=80=EC=9E=A5=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20store=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/store/index.ts | 1 + src/features/home/store/useChartStore.ts | 66 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/features/home/store/index.ts create mode 100644 src/features/home/store/useChartStore.ts diff --git a/src/features/home/store/index.ts b/src/features/home/store/index.ts new file mode 100644 index 0000000..b430f9e --- /dev/null +++ b/src/features/home/store/index.ts @@ -0,0 +1 @@ +export { useChartStore, useCandleParams, type ChartTimeframe } from "./useChartStore"; diff --git a/src/features/home/store/useChartStore.ts b/src/features/home/store/useChartStore.ts new file mode 100644 index 0000000..0ffbceb --- /dev/null +++ b/src/features/home/store/useChartStore.ts @@ -0,0 +1,66 @@ +import type { CandleParams } from "@/entities"; +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +export type ChartTimeframe = "1초" | "1분" | "5분" | "30분" | "1시간" | "주" | "달" | "년"; + +interface ChartState { + selectedTimeframe: ChartTimeframe; + setSelectedTimeframe: (timeframe: ChartTimeframe) => void; + // 현재 선택된 타임프레임에 따른 API 파라미터 생성 + getCandleParams: (market: string, count: number) => CandleParams; +} + +/** + * 타임프레임을 API 파라미터로 변환 + */ +const timeframeToAPIParams = (timeframe: ChartTimeframe, market: string, count: number): CandleParams => { + const baseParams = { market, count }; + + switch (timeframe) { + case "1초": + return { ...baseParams, type: "seconds" }; + case "1분": + return { ...baseParams, type: "minutes", unit: 1 }; + case "5분": + return { ...baseParams, type: "minutes", unit: 5 }; + case "30분": + return { ...baseParams, type: "minutes", unit: 30 }; + case "1시간": + return { ...baseParams, type: "minutes", unit: 60 }; + case "주": + return { ...baseParams, type: "weeks" }; + case "달": + return { ...baseParams, type: "months" }; + case "년": + return { ...baseParams, type: "days" }; + default: + return { ...baseParams, type: "seconds" }; + } +}; + +export const useChartStore = create()( + devtools( + (set) => ({ + selectedTimeframe: "1초", + setSelectedTimeframe: (timeframe: ChartTimeframe) => set({ selectedTimeframe: timeframe }), + getCandleParams: (market: string, count: number) => + timeframeToAPIParams( + // state에서 현재 timeframe을 가져오기 위해서는 별도 처리 필요 + // 이 함수는 직접 사용하지 않고, useCandleParams hook 사용 권장 + "1초", + market, + count, + ), + }), + { name: "ChartStore" }, + ), +); + +/** + * 현재 선택된 타임프레임을 API 파라미터로 변환하는 hook + */ +export const useCandleParams = (market: string, count: number): CandleParams => { + const selectedTimeframe = useChartStore((state) => state.selectedTimeframe); + return timeframeToAPIParams(selectedTimeframe, market, count); +}; From 7c2e6e5d043c766ffe189e8cc3ef7df6de1e6703 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 4 Dec 2025 17:53:38 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20api=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/useGetCandle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/market/model/hooks/useGetCandle.ts b/src/entities/market/model/hooks/useGetCandle.ts index 8ee16bb..a55d241 100644 --- a/src/entities/market/model/hooks/useGetCandle.ts +++ b/src/entities/market/model/hooks/useGetCandle.ts @@ -15,6 +15,6 @@ export const useGetCandle = (params: CandleParams) => { queryKey, queryFn: () => candleAPI(params), - refetchInterval: 500, + refetchInterval: type === "seconds" || type === "minutes" ? 500 : false, }); };