import type { KisCredentialInput } from "@/lib/kis/config"; import { getKisConfig } from "@/lib/kis/config"; import { getKisAccessToken } from "@/lib/kis/token"; /** * @file lib/kis/client.ts * @description KIS REST 공통 클라이언트(실전/모의 공통) */ export interface KisApiEnvelope { rt_cd?: string; msg_cd?: string; msg1?: string; output?: TOutput; output1?: unknown; output2?: unknown; } /** * KIS GET 호출 * @param apiPath REST 경로 * @param trId KIS TR ID * @param params 쿼리 파라미터 * @param credentials 사용자 입력 키(선택) * @returns KIS 원본 응답 * @see lib/kis/domestic.ts getDomesticQuote/getDomesticDailyPrice - 대시보드 시세 데이터 소스 */ export async function kisGet( apiPath: string, trId: string, params: Record, credentials?: KisCredentialInput, ): Promise> { const config = getKisConfig(credentials); const token = await getKisAccessToken(credentials); const url = new URL(apiPath, config.baseUrl); Object.entries(params).forEach(([key, value]) => { if (value !== "") url.searchParams.set(key, value); }); const response = await fetch(url.toString(), { method: "GET", headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token}`, appkey: config.appKey, appsecret: config.appSecret, tr_id: trId, tr_cont: "", custtype: "P", }, cache: "no-store", }); const rawText = await response.text(); const payload = tryParseKisEnvelope(rawText); if (!response.ok) { const detail = payload.msg1 || rawText.slice(0, 200); throw new Error( detail ? `KIS API 요청 실패 (${response.status}): ${detail}` : `KIS API 요청 실패 (${response.status})`, ); } if (payload.rt_cd && payload.rt_cd !== "0") { const detail = [payload.msg1, payload.msg_cd].filter(Boolean).join(" / "); throw new Error(detail || "KIS API 비즈니스 오류가 발생했습니다."); } return payload; } /** * KIS POST 호출 (주문 등) * @param apiPath REST 경로 * @param trId KIS TR ID * @param body 요청 본문 * @param credentials 사용자 입력 키(선택) * @returns KIS 원본 응답 */ export async function kisPost( apiPath: string, trId: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any body: Record, credentials?: KisCredentialInput, ): Promise> { const config = getKisConfig(credentials); const token = await getKisAccessToken(credentials); const url = new URL(apiPath, config.baseUrl); const response = await fetch(url.toString(), { method: "POST", headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token}`, appkey: config.appKey, appsecret: config.appSecret, tr_id: trId, tr_cont: "", custtype: "P", }, body: JSON.stringify(body), cache: "no-store", }); const rawText = await response.text(); const payload = tryParseKisEnvelope(rawText); if (!response.ok) { const detail = payload.msg1 || rawText.slice(0, 200); throw new Error( detail ? `KIS API 요청 실패 (${response.status}): ${detail}` : `KIS API 요청 실패 (${response.status})`, ); } if (payload.rt_cd && payload.rt_cd !== "0") { const detail = [payload.msg1, payload.msg_cd].filter(Boolean).join(" / "); throw new Error(detail || "KIS API 비즈니스 오류가 발생했습니다."); } return payload; } /** * KIS 응답을 안전하게 JSON으로 파싱합니다. * @param rawText fetch 응답 원문 * @returns KisApiEnvelope * @see lib/kis/token.ts tryParseTokenResponse - 토큰 발급 응답 파싱 방식과 동일 패턴 */ function tryParseKisEnvelope( rawText: string, ): KisApiEnvelope { try { return JSON.parse(rawText) as KisApiEnvelope; } catch { return { msg1: rawText.slice(0, 200), }; } } // 하위 호환(alias) // 하위 호환(alias) export const kisMockGet = kisGet; export const kisMockPost = kisPost;