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; 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; }