2026-02-11 16:31:28 +09:00
|
|
|
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
|
2026-02-10 11:16:39 +09:00
|
|
|
import type {
|
|
|
|
|
DashboardChartTimeframe,
|
|
|
|
|
DashboardStockCashOrderRequest,
|
|
|
|
|
DashboardStockCashOrderResponse,
|
|
|
|
|
DashboardStockChartResponse,
|
|
|
|
|
DashboardStockOrderBookResponse,
|
|
|
|
|
DashboardStockOverviewResponse,
|
|
|
|
|
DashboardStockSearchResponse,
|
2026-02-11 16:31:28 +09:00
|
|
|
} from "@/features/trade/types/trade.types";
|
2026-02-11 11:18:15 +09:00
|
|
|
import {
|
|
|
|
|
DOMESTIC_KIS_SESSION_OVERRIDE_HEADER,
|
|
|
|
|
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,
|
|
|
|
|
parseDomesticKisSession,
|
|
|
|
|
} from "@/lib/kis/domestic-market-session";
|
2026-02-10 11:16:39 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 종목 검색 API 호출
|
|
|
|
|
* @param keyword 검색어
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchStockSearch(
|
|
|
|
|
keyword: string,
|
|
|
|
|
signal?: AbortSignal,
|
|
|
|
|
): Promise<DashboardStockSearchResponse> {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`/api/kis/domestic/search?q=${encodeURIComponent(keyword)}`,
|
|
|
|
|
{
|
|
|
|
|
cache: "no-store",
|
|
|
|
|
signal,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const payload = (await response.json()) as
|
|
|
|
|
| DashboardStockSearchResponse
|
|
|
|
|
| { error?: string };
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"error" in payload ? payload.error : "종목 검색 중 오류가 발생했습니다.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload as DashboardStockSearchResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 종목 상세 개요 조회 API 호출
|
|
|
|
|
* @param symbol 종목코드
|
|
|
|
|
* @param credentials KIS 인증 정보
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchStockOverview(
|
|
|
|
|
symbol: string,
|
|
|
|
|
credentials: KisRuntimeCredentials,
|
|
|
|
|
): Promise<DashboardStockOverviewResponse> {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`/api/kis/domestic/overview?symbol=${encodeURIComponent(symbol)}`,
|
|
|
|
|
{
|
|
|
|
|
method: "GET",
|
2026-02-11 11:18:15 +09:00
|
|
|
headers: buildKisRequestHeaders(credentials),
|
2026-02-10 11:16:39 +09:00
|
|
|
cache: "no-store",
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const payload = (await response.json()) as
|
|
|
|
|
| DashboardStockOverviewResponse
|
|
|
|
|
| { error?: string };
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"error" in payload ? payload.error : "종목 조회 중 오류가 발생했습니다.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload as DashboardStockOverviewResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 종목 호가 조회 API 호출
|
|
|
|
|
* @param symbol 종목코드
|
|
|
|
|
* @param credentials KIS 인증 정보
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchStockOrderBook(
|
|
|
|
|
symbol: string,
|
|
|
|
|
credentials: KisRuntimeCredentials,
|
|
|
|
|
signal?: AbortSignal,
|
|
|
|
|
): Promise<DashboardStockOrderBookResponse> {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`/api/kis/domestic/orderbook?symbol=${encodeURIComponent(symbol)}`,
|
|
|
|
|
{
|
|
|
|
|
method: "GET",
|
2026-02-11 11:18:15 +09:00
|
|
|
headers: buildKisRequestHeaders(credentials),
|
|
|
|
|
cache: "no-store",
|
2026-02-10 11:16:39 +09:00
|
|
|
signal,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const payload = (await response.json()) as
|
|
|
|
|
| DashboardStockOrderBookResponse
|
|
|
|
|
| { error?: string };
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"error" in payload ? payload.error : "호가 조회 중 오류가 발생했습니다.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload as DashboardStockOrderBookResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 종목 차트(분봉/일봉/주봉) 조회 API 호출
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchStockChart(
|
|
|
|
|
symbol: string,
|
|
|
|
|
timeframe: DashboardChartTimeframe,
|
|
|
|
|
credentials: KisRuntimeCredentials,
|
|
|
|
|
cursor?: string,
|
|
|
|
|
): Promise<DashboardStockChartResponse> {
|
|
|
|
|
const query = new URLSearchParams({
|
|
|
|
|
symbol,
|
|
|
|
|
timeframe,
|
|
|
|
|
});
|
|
|
|
|
if (cursor) query.set("cursor", cursor);
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/kis/domestic/chart?${query.toString()}`, {
|
|
|
|
|
method: "GET",
|
2026-02-11 11:18:15 +09:00
|
|
|
headers: buildKisRequestHeaders(credentials),
|
2026-02-10 11:16:39 +09:00
|
|
|
cache: "no-store",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const payload = (await response.json()) as
|
|
|
|
|
| DashboardStockChartResponse
|
|
|
|
|
| { error?: string };
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"error" in payload ? payload.error : "차트 조회 중 오류가 발생했습니다.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload as DashboardStockChartResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 주식 현금 주문 API 호출
|
|
|
|
|
* @param request 주문 요청 데이터
|
|
|
|
|
* @param credentials KIS 인증 정보
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchOrderCash(
|
|
|
|
|
request: DashboardStockCashOrderRequest,
|
|
|
|
|
credentials: KisRuntimeCredentials,
|
|
|
|
|
): Promise<DashboardStockCashOrderResponse> {
|
|
|
|
|
const response = await fetch("/api/kis/domestic/order-cash", {
|
|
|
|
|
method: "POST",
|
2026-02-11 11:18:15 +09:00
|
|
|
headers: buildKisRequestHeaders(credentials, { jsonContentType: true }),
|
2026-02-10 11:16:39 +09:00
|
|
|
body: JSON.stringify(request),
|
|
|
|
|
cache: "no-store",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const payload = (await response.json()) as DashboardStockCashOrderResponse;
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(payload.message || "주문 전송 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
2026-02-11 11:18:15 +09:00
|
|
|
|
|
|
|
|
function buildKisRequestHeaders(
|
|
|
|
|
credentials: KisRuntimeCredentials,
|
|
|
|
|
options?: { jsonContentType?: boolean },
|
|
|
|
|
) {
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
"x-kis-app-key": credentials.appKey,
|
|
|
|
|
"x-kis-app-secret": credentials.appSecret,
|
|
|
|
|
"x-kis-trading-env": credentials.tradingEnv,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (options?.jsonContentType) {
|
|
|
|
|
headers["content-type"] = "application/json";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sessionOverride = readSessionOverrideForDev();
|
|
|
|
|
if (sessionOverride) {
|
|
|
|
|
headers[DOMESTIC_KIS_SESSION_OVERRIDE_HEADER] = sessionOverride;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return headers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readSessionOverrideForDev() {
|
|
|
|
|
if (typeof window === "undefined") return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(
|
|
|
|
|
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,
|
|
|
|
|
);
|
|
|
|
|
return parseDomesticKisSession(raw);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|