Files

125 lines
3.7 KiB
TypeScript
Raw Permalink Normal View History

2026-03-12 09:26:27 +09:00
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,
});
}
}