107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import {
|
|
getDomesticOrderBook,
|
|
KisDomesticOrderBookOutput,
|
|
} from "@/lib/kis/domestic";
|
|
import { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types";
|
|
import {
|
|
KisCredentialInput,
|
|
hasKisConfig,
|
|
normalizeTradingEnv,
|
|
} from "@/lib/kis/config";
|
|
|
|
/**
|
|
* @file app/api/kis/domestic/orderbook/route.ts
|
|
* @description 국내주식 호가 조회 API
|
|
*/
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const { searchParams } = new URL(request.url);
|
|
const symbol = (searchParams.get("symbol") ?? "").trim();
|
|
|
|
if (!/^\d{6}$/.test(symbol)) {
|
|
return NextResponse.json(
|
|
{ error: "symbol은 6자리 숫자여야 합니다." },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const credentials = readKisCredentialsFromHeaders(request.headers);
|
|
|
|
if (!hasKisConfig(credentials)) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "KIS API 키 설정이 필요합니다.",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
const raw = await getDomesticOrderBook(symbol, credentials);
|
|
|
|
const levels = Array.from({ length: 10 }, (_, i) => {
|
|
const idx = i + 1;
|
|
return {
|
|
askPrice: readOrderBookNumber(raw, `askp${idx}`),
|
|
bidPrice: readOrderBookNumber(raw, `bidp${idx}`),
|
|
askSize: readOrderBookNumber(raw, `askp_rsqn${idx}`),
|
|
bidSize: readOrderBookNumber(raw, `bidp_rsqn${idx}`),
|
|
};
|
|
});
|
|
|
|
const response: DashboardStockOrderBookResponse = {
|
|
symbol,
|
|
source: "kis",
|
|
levels,
|
|
totalAskSize: readOrderBookNumber(raw, "total_askp_rsqn"),
|
|
totalBidSize: readOrderBookNumber(raw, "total_bidp_rsqn"),
|
|
tradingEnv: normalizeTradingEnv(credentials.tradingEnv),
|
|
fetchedAt: new Date().toISOString(),
|
|
};
|
|
|
|
return NextResponse.json(response, {
|
|
headers: {
|
|
"cache-control": "no-store",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "호가 조회 중 오류가 발생했습니다.";
|
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
function readKisCredentialsFromHeaders(headers: Headers): KisCredentialInput {
|
|
const appKey = headers.get("x-kis-app-key")?.trim();
|
|
const appSecret = headers.get("x-kis-app-secret")?.trim();
|
|
const tradingEnv = normalizeTradingEnv(
|
|
headers.get("x-kis-trading-env") ?? undefined,
|
|
);
|
|
|
|
return {
|
|
appKey,
|
|
appSecret,
|
|
tradingEnv,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @description 호가 응답 필드를 대소문자 모두 허용해 숫자로 읽습니다.
|
|
* @see app/api/kis/domestic/orderbook/route.ts GET에서 output/output1 키 차이 방어 로직으로 사용합니다.
|
|
*/
|
|
function readOrderBookNumber(raw: KisDomesticOrderBookOutput, key: string) {
|
|
const record = raw as Record<string, unknown>;
|
|
const direct = record[key];
|
|
const upper = record[key.toUpperCase()];
|
|
const value = direct ?? upper ?? "0";
|
|
const normalized =
|
|
typeof value === "string"
|
|
? value.replaceAll(",", "").trim()
|
|
: String(value ?? "0");
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|