Files
auto-trade/lib/kis/client.ts

168 lines
4.5 KiB
TypeScript

import type { KisCredentialInput } from "@/lib/kis/config";
import { getKisConfig } from "@/lib/kis/config";
import { buildKisErrorDetail } from "@/lib/kis/error-codes";
import { getKisAccessToken } from "@/lib/kis/token";
/**
* @file lib/kis/client.ts
* @description KIS REST 공통 클라이언트(실전/모의 공통)
*/
export interface KisApiEnvelope<TOutput> {
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<TOutput>(
apiPath: string,
trId: string,
params: Record<string, string>,
credentials?: KisCredentialInput,
): Promise<KisApiEnvelope<TOutput>> {
const config = getKisConfig(credentials);
const token = await getKisAccessToken(credentials);
const url = new URL(apiPath, config.baseUrl);
Object.entries(params).forEach(([key, value]) => {
if (value != null) 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<TOutput>(rawText);
if (!response.ok) {
const detail = buildKisErrorDetail({
message: payload.msg1,
msgCode: payload.msg_cd,
extraMessages: 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 = buildKisErrorDetail({
message: payload.msg1,
msgCode: payload.msg_cd,
});
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<TOutput>(
apiPath: string,
trId: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: Record<string, any>,
credentials?: KisCredentialInput,
): Promise<KisApiEnvelope<TOutput>> {
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<TOutput>(rawText);
if (!response.ok) {
const detail = buildKisErrorDetail({
message: payload.msg1,
msgCode: payload.msg_cd,
extraMessages: 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 = buildKisErrorDetail({
message: payload.msg1,
msgCode: payload.msg_cd,
});
throw new Error(detail || "KIS API 비즈니스 오류가 발생했습니다.");
}
return payload;
}
/**
* KIS 응답을 안전하게 JSON으로 파싱합니다.
* @param rawText fetch 응답 원문
* @returns KisApiEnvelope
* @see lib/kis/token.ts tryParseTokenResponse - 토큰 발급 응답 파싱 방식과 동일 패턴
*/
function tryParseKisEnvelope<TOutput>(
rawText: string,
): KisApiEnvelope<TOutput> {
try {
return JSON.parse(rawText) as KisApiEnvelope<TOutput>;
} catch {
return {
msg1: rawText.slice(0, 200),
};
}
}
// 하위 호환(alias)
// 하위 호환(alias)
export const kisMockGet = kisGet;
export const kisMockPost = kisPost;