import { NextResponse } from "next/server"; import { z } from "zod"; import { AUTOTRADE_API_ERROR_CODE, createAutotradeErrorResponse, getAutotradeSession, getAutotradeUserId, readJsonBody, sanitizeAutotradeError, upsertAutotradeSession, } from "@/app/api/autotrade/_shared"; const heartbeatRequestSchema = z.object({ sessionId: z.string().uuid(), leaderTabId: z.string().trim().min(1).max(100).optional(), }); export async function POST(request: Request) { const userId = await getAutotradeUserId(request.headers); if (!userId) { return createAutotradeErrorResponse({ status: 401, code: AUTOTRADE_API_ERROR_CODE.AUTH_REQUIRED, message: "로그인이 필요합니다.", }); } const rawBody = await readJsonBody(request); const parsed = heartbeatRequestSchema.safeParse(rawBody); if (!parsed.success) { return createAutotradeErrorResponse({ status: 400, code: AUTOTRADE_API_ERROR_CODE.INVALID_REQUEST, message: parsed.error.issues[0]?.message ?? "heartbeat 요청값이 올바르지 않습니다.", }); } try { const session = getAutotradeSession(userId); if (!session || session.runtimeState !== "RUNNING") { return createAutotradeErrorResponse({ status: 404, code: AUTOTRADE_API_ERROR_CODE.SESSION_NOT_FOUND, message: "실행 중인 자동매매 세션이 없습니다.", }); } if (session.sessionId !== parsed.data.sessionId) { return createAutotradeErrorResponse({ status: 409, code: AUTOTRADE_API_ERROR_CODE.CONFLICT, message: "세션 식별자가 일치하지 않습니다.", }); } const updated = upsertAutotradeSession({ ...session, lastHeartbeatAt: new Date().toISOString(), leaderTabId: parsed.data.leaderTabId ?? session.leaderTabId, }); return NextResponse.json({ ok: true, session: updated, }); } catch (error) { return createAutotradeErrorResponse({ status: 500, code: AUTOTRADE_API_ERROR_CODE.INTERNAL, message: sanitizeAutotradeError(error, "heartbeat 처리 중 오류가 발생했습니다."), }); } }