Skip to content
Merged
36 changes: 36 additions & 0 deletions src/entities/market/model/apis/candle.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { UPBIT_URL } from "@/shared";

import { CandleData, CandleParams, MinutesCandleParams } from "../types";

export const candleAPI = async (params: CandleParams): Promise<CandleData[]> => {
const { market, to, count, type } = params;

// URL 파라미터 구성
const urlParams = new URLSearchParams({
market,
count: count.toString(),
});

if (to) {
urlParams.append("to", to);
}

// 타입별 엔드포인트 구성
let endpoint = `${UPBIT_URL}/candles/${type}`;

// 분 캔들의 경우 unit을 Path 파라미터로 추가
if (type === "minutes") {
const unit = (params as MinutesCandleParams).unit;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

if (type === "minutes") 조건문 덕분에 이 블록 안에서 params의 타입이 MinutesCandleParams로 추론됩니다. 따라서 as MinutesCandleParams와 같은 타입 단언 없이 params.unit으로 직접 접근할 수 있습니다. 타입 단언을 제거하면 코드가 더 간결해지고 타입 안정성도 높아집니다.

Suggested change
const unit = (params as MinutesCandleParams).unit;
const unit = params.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}`);
}

const data: CandleData[] = await response.json();
return data;
};
1 change: 1 addition & 0 deletions src/entities/market/model/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./candle.api";
export * from "./market-all.api";
export * from "./market-order.api";
export * from "./ticker.api";
Expand Down
1 change: 1 addition & 0 deletions src/entities/market/model/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./useGetMarketOrder";
export * from "./useMarketOrderRealtime";
export * from "./useMarket";
export * from "./useGetCandle";
20 changes: 20 additions & 0 deletions src/entities/market/model/hooks/useGetCandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useQuery } from "@tanstack/react-query";

import { candleAPI } from "../apis";
import type { CandleParams, MinutesCandleParams } from "../types";

export const useGetCandle = (params: CandleParams) => {
const { market, count, type } = params;

// 분 캔들일 때만 unit을 queryKey에 포함
const unit = type === "minutes" ? (params as MinutesCandleParams).unit : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

candle.api.ts 파일과 마찬가지로, type === "minutes" 조건으로 인해 params 타입이 MinutesCandleParams로 좁혀지므로 타입 단언이 필요하지 않습니다. 타입스크립트의 제어 흐름 분석을 활용하여 더 안전하고 간결한 코드를 작성할 수 있습니다.

Suggested change
const unit = type === "minutes" ? (params as MinutesCandleParams).unit : undefined;
const unit = type === "minutes" ? params.unit : undefined;


const queryKey = unit ? ["candle", market, count, type, unit] : ["candle", market, count, type];

return useQuery({
queryKey,
queryFn: () => candleAPI(params),

refetchInterval: type === "seconds" || type === "minutes" ? 500 : false,
});
};
36 changes: 36 additions & 0 deletions src/entities/market/model/types/candle.type.ts
Original file line number Diff line number Diff line change
@@ -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;
23 changes: 23 additions & 0 deletions src/entities/market/model/types/chart.type.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CandleData type includes a unit field that may not be present in all API responses. According to Upbit API documentation, the unit field is only returned for minute candles, not for seconds/days/weeks/months candles. This could cause runtime errors when accessing unit on non-minute candle data. Consider making this field optional (unit?: number) or using discriminated unions for different candle response types.

Suggested change
unit: number;
unit?: number;

Copilot uses AI. Check for mistakes.
};

export type ChartDataPoint = {
x: number;
y: number | number[] | string;
};

export type ChartSeriesData = {
prices: ChartDataPoint[];
volume: ChartDataPoint[];
};
2 changes: 2 additions & 0 deletions src/entities/market/model/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./chart.type";
export * from "./market-info.type";
export * from "./orderbook-units.type";
export * from "./trade.type";
export * from "./candle.type";
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>("1초");
const selectedTimeframe = useChartStore((state) => state.selectedTimeframe);
const setSelectedTimeframe = useChartStore((state) => state.setSelectedTimeframe);

return (
<div className='flex items-center gap-2 border-b border-white/10 p-2'>
Expand Down
Loading
Loading