import { onRequest } from "firebase-functions/https"; import type { Request } from "firebase-functions/https"; import { requireAuth } from "../middleware/auth"; import { HttpError, sendError } from "../middleware/errors"; import { getMessages, getQuota, getSuggestions, reportMessage, sendMessage, } from "../services/chatService"; import type { ChatHistoryPageDto, ChatQuotaDto, ChatReportDto, ChatSendResultDto, ChatSuggestionsDto, } from "../types/dto/chatDto"; /** * AI 채팅(짹) 엔드포인트(§3). `/chat/*` 전체가 인증 필요 API다. * * - POST /chat/messages 메시지 전송(비스트리밍, §3.1) * - GET /chat/messages 대화 이력 조회(§3.2) * - GET /chat/quota 오늘 잔여 횟수(§3.3) * - POST /chat/messages/{messageId}/report 메시지 신고(§3.4) * - GET /chat/suggestions 추천 질문(§3.5) * * 기본 AI 벤더는 Vertex AI(Gemini, ADC 인증 — 별도 키 불필요)다(§8). * 벤더 전환은 config/chat.provider.name으로: "mock" | "vertex" | "anthropic". * anthropic 전환 시 `firebase functions:secrets:set ANTHROPIC_API_KEY` 등록과 함께 * 아래 onRequest 옵션에 `secrets: ["ANTHROPIC_API_KEY"]`를 추가한다(§8.2). */ /** 401 응답에 에러 코드 UNAUTHENTICATED를 포함한다(§3.1 에러 표 계약). */ async function requireChatAuth(req: Request): Promise { try { return await requireAuth(req); } catch (err) { if (err instanceof HttpError && err.status === 401 && !err.code) { throw new HttpError(401, err.message, "UNAUTHENTICATED"); } throw err; } } export const chat = onRequest({ timeoutSeconds: 60 }, async (req, res) => { const segs = req.path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean); try { if (segs[0] === "messages" && segs.length === 1) { const uid = await requireChatAuth(req); if (req.method === "POST") { const result: ChatSendResultDto = await sendMessage(uid, req.body ?? {}); res.status(200).json(result); return; } if (req.method === "GET") { const cursor = req.query.cursor != null ? String(req.query.cursor) : undefined; const limit = req.query.limit != null ? String(req.query.limit) : undefined; const result: ChatHistoryPageDto = await getMessages(uid, cursor, limit); res.status(200).json(result); return; } } if (segs[0] === "messages" && segs.length === 3 && segs[2] === "report" && req.method === "POST") { const uid = await requireChatAuth(req); const result: ChatReportDto = await reportMessage(uid, segs[1], req.body ?? {}); res.status(200).json(result); return; } if (segs[0] === "quota" && req.method === "GET") { const uid = await requireChatAuth(req); const result: ChatQuotaDto = await getQuota(uid); res.status(200).json(result); return; } if (segs[0] === "suggestions" && req.method === "GET") { const uid = await requireChatAuth(req); const result: ChatSuggestionsDto = await getSuggestions(uid); res.status(200).json(result); return; } res.status(404).json({ error: `Unknown: ${req.method} ${req.path}` }); } catch (err) { sendError(res, err); } });