import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { executeInquireOrderableCash } from "@/lib/kis/trade"; import type { DashboardStockOrderableCashResponse } from "@/features/trade/types/trade.types"; import { hasKisApiSession } from "@/app/api/kis/_session"; import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config"; import { createKisApiErrorResponse, KIS_API_ERROR_CODE, toKisApiErrorMessage, } from "@/app/api/kis/_response"; import { readKisAccountParts, readKisCredentialsFromHeaders, } from "@/app/api/kis/domestic/_shared"; /** * @file app/api/kis/domestic/orderable-cash/route.ts * @description 국내주식 매수가능금액(주문가능현금) 조회 API */ const orderableCashBodySchema = z.object({ symbol: z .string() .trim() .regex(/^\d{6}$/, "종목코드는 6자리 숫자여야 합니다."), price: z.coerce.number().positive("기준 가격은 0보다 커야 합니다."), orderType: z.enum(["limit", "market"]).default("market"), }); export async function POST(request: NextRequest) { const credentials = readKisCredentialsFromHeaders(request.headers); const tradingEnv = normalizeTradingEnv(credentials.tradingEnv); const hasSession = await hasKisApiSession(); if (!hasSession) { return createKisApiErrorResponse({ status: 401, code: KIS_API_ERROR_CODE.AUTH_REQUIRED, message: "로그인이 필요합니다.", tradingEnv, }); } if (!hasKisConfig(credentials)) { return createKisApiErrorResponse({ status: 400, code: KIS_API_ERROR_CODE.CREDENTIAL_REQUIRED, message: "KIS API 키 설정이 필요합니다.", tradingEnv, }); } const account = readKisAccountParts(request.headers); if (!account) { return createKisApiErrorResponse({ status: 400, code: KIS_API_ERROR_CODE.ACCOUNT_REQUIRED, message: "계좌번호가 필요합니다. 설정에서 계좌번호(예: 12345678-01)를 입력해 주세요.", tradingEnv, }); } try { let rawBody: unknown = {}; try { rawBody = (await request.json()) as unknown; } catch { return createKisApiErrorResponse({ status: 400, code: KIS_API_ERROR_CODE.INVALID_REQUEST, message: "요청 본문(JSON)을 읽을 수 없습니다.", tradingEnv, }); } const parsed = orderableCashBodySchema.safeParse(rawBody); if (!parsed.success) { return createKisApiErrorResponse({ status: 400, code: KIS_API_ERROR_CODE.INVALID_REQUEST, message: parsed.error.issues[0]?.message ?? "요청값이 올바르지 않습니다.", tradingEnv, }); } const result = await executeInquireOrderableCash( { symbol: parsed.data.symbol, price: parsed.data.price, orderType: parsed.data.orderType, accountNo: account.accountNo, accountProductCode: account.accountProductCode, }, credentials, ); const response: DashboardStockOrderableCashResponse = { ok: true, tradingEnv, orderableCash: result.orderableCash, noReceivableBuyAmount: result.noReceivableBuyAmount, maxBuyAmount: result.maxBuyAmount, maxBuyQuantity: result.maxBuyQuantity, noReceivableBuyQuantity: result.noReceivableBuyQuantity, fetchedAt: new Date().toISOString(), }; return NextResponse.json(response, { headers: { "cache-control": "no-store", }, }); } catch (error) { return createKisApiErrorResponse({ status: 500, code: KIS_API_ERROR_CODE.UPSTREAM_FAILURE, message: toKisApiErrorMessage(error, "매수가능금액 조회 중 오류가 발생했습니다."), tradingEnv, }); } }