import { z } from "zod"; import { db } from "@/lib/db"; import { logger } from "@/lib/logger"; import { ApiError } from "@/lib/errors"; import { REQUEST_STATUSES } from "@/lib/constants"; import type { Prisma } from "@/lib/generated/prisma/client"; import { calculateExtendedAvailableDate, getAccountTypeLabel, } from "@/lib/utils"; const pageSizeOptions = [10, 20, 50, 100] as const; const extensionRequestSearchSchema = z.object({ page: z.coerce.number().int().min(1).catch(1), pageSize: z.coerce .number() .int() .refine((value) => pageSizeOptions.includes(value as PageSize)) .catch(10), q: z.string().trim().catch(""), status: z .union([z.literal("all"), z.coerce.number().int().min(0)]) .optional() .catch(REQUEST_STATUSES.REQUESTED), }); type PageSize = (typeof pageSizeOptions)[number]; export type RawSearchParams = | URLSearchParams | Record; export type ExtensionRequestSearchParams = z.infer< typeof extensionRequestSearchSchema >; export type ExtensionRequestListItem = { maestroExtensionID: number; maestroID: number; maestroName: string; requestedAccountType: number; currentAccountType: number; activateStatus: number; requestedDateTime: string; availableActivateDateTime: string; status: number; }; export type ExtensionRequestListResult = { items: ExtensionRequestListItem[]; page: number; pageSize: number; totalCount: number; totalPages: number; q: string; status?: number | "all"; }; export async function getExtensionRequests( rawSearchParams: RawSearchParams = {} ): Promise { const t0 = Date.now(); const params = parseExtensionRequestSearchParams(rawSearchParams); const where = buildExtensionRequestWhere(params); const skip = (params.page - 1) * params.pageSize; const [totalCount, requests] = await Promise.all([ db.maestro_extension.count({ where }), db.maestro_extension.findMany({ where, orderBy: { MaestroExtensionID: "desc" }, skip, take: params.pageSize, select: { MaestroExtensionID: true, MaestroID: true, AccountType: true, RequestedDateTime: true, Status: true, maestro: { select: { Name: true, AccountType: true, ActivateStatus: true, AvailableActivateDateTime: true, }, }, }, }), ]); logger.info("extension-requests", "list fetched", { totalCount, page: params.page, duration: Date.now() - t0, }); return { items: requests.map(toExtensionRequestListItem), page: params.page, pageSize: params.pageSize, totalCount, totalPages: Math.max(1, Math.ceil(totalCount / params.pageSize)), q: params.q, status: params.status, }; } export async function approveExtensionRequest( maestroExtensionID: number ): Promise<{ availableActivateDateTime: string }> { return db.$transaction(async (tx) => { const request = await getActionTarget(tx, maestroExtensionID); const availableActivateDateTime = calculateExtendedAvailableDate( request.maestro.AvailableActivateDateTime ); await tx.maestro.update({ where: { MaestroID: request.MaestroID }, data: { ActivateStatus: 2, AvailableActivateDateTime: availableActivateDateTime, }, }); await tx.maestro_extension.updateMany({ where: { MaestroID: request.MaestroID, Status: REQUEST_STATUSES.REQUESTED, }, data: { Status: REQUEST_STATUSES.CLOSED }, }); await tx.maestro_extension.update({ where: { MaestroExtensionID: request.MaestroExtensionID }, data: { Status: REQUEST_STATUSES.APPLIED }, }); await tx.maestro_log.create({ data: { Type: "extension_maestro", MaestroID: request.MaestroID, LogDateTime: new Date(), Remark: buildExtensionLogRemark( request.maestro.Name, request.AccountType ), }, }); return { availableActivateDateTime: availableActivateDateTime.toISOString(), }; }); } export async function cancelExtensionRequest( maestroExtensionID: number ): Promise { await db.$transaction(async (tx) => { const request = await getActionTarget(tx, maestroExtensionID); await tx.maestro_extension.update({ where: { MaestroExtensionID: request.MaestroExtensionID }, data: { Status: REQUEST_STATUSES.CLOSED }, }); await tx.maestro_log.create({ data: { Type: "cancel_extension_maestro", MaestroID: request.MaestroID, LogDateTime: new Date(), Remark: buildExtensionLogRemark( request.maestro.Name, request.AccountType ), }, }); }); } export function parseExtensionRequestSearchParams( rawSearchParams: RawSearchParams ): ExtensionRequestSearchParams { return extensionRequestSearchSchema.parse( searchParamsToRecord(rawSearchParams) ); } async function getActionTarget( tx: Prisma.TransactionClient, maestroExtensionID: number ) { const request = await tx.maestro_extension.findUnique({ where: { MaestroExtensionID: maestroExtensionID }, include: { maestro: true }, }); if (!request) { throw new ApiError("연장 신청을 찾을 수 없습니다.", 404); } if (request.Status !== REQUEST_STATUSES.REQUESTED) { throw new ApiError("이미 처리된 연장 신청입니다.", 409); } return request; } function buildExtensionRequestWhere( params: ExtensionRequestSearchParams ): Prisma.maestro_extensionWhereInput { const conditions: Prisma.maestro_extensionWhereInput[] = []; if (params.q) { const keywordConditions: Prisma.maestro_extensionWhereInput[] = [ { maestro: { Name: { contains: params.q } } }, ]; const numericQuery = Number(params.q); if (Number.isInteger(numericQuery) && numericQuery > 0) { keywordConditions.unshift( { MaestroExtensionID: numericQuery }, { MaestroID: numericQuery } ); } conditions.push({ OR: keywordConditions }); } if (typeof params.status === "number" && Number.isInteger(params.status)) { conditions.push({ Status: params.status }); } return conditions.length > 0 ? { AND: conditions } : {}; } function searchParamsToRecord( rawSearchParams: RawSearchParams ): Record { if (rawSearchParams instanceof URLSearchParams) { return Object.fromEntries(rawSearchParams.entries()); } return Object.fromEntries( Object.entries(rawSearchParams).map(([key, value]) => [ key, Array.isArray(value) ? value[0] : value, ]) ); } function toExtensionRequestListItem(request: { MaestroExtensionID: number; MaestroID: number; AccountType: number; RequestedDateTime: Date; Status: number; maestro: { Name: string; AccountType: number; ActivateStatus: number; AvailableActivateDateTime: Date; }; }): ExtensionRequestListItem { return { maestroExtensionID: request.MaestroExtensionID, maestroID: request.MaestroID, maestroName: request.maestro.Name.trim(), requestedAccountType: request.AccountType, currentAccountType: request.maestro.AccountType, activateStatus: request.maestro.ActivateStatus, requestedDateTime: request.RequestedDateTime.toISOString(), availableActivateDateTime: request.maestro.AvailableActivateDateTime.toISOString(), status: request.Status, }; } function buildExtensionLogRemark(maestroName: string, accountType: number) { return `${maestroName.trim()}(${getAccountTypeLabel(accountType)})`.slice( 0, 100 ); }