diff --git a/.gitignore b/.gitignore index bbffc8a..0ebef99 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,8 @@ next-env.d.ts bun.lockb pnpm-lock.yaml -pnpm-workspace.yaml \ No newline at end of file +pnpm-workspace.yaml + +# logs +logs/ +*.log diff --git a/next.config.ts b/next.config.ts index 58db81e..6733d06 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,7 +6,12 @@ const nextConfig: NextConfig = { sassOptions: { includePaths: [path.join(__dirname, './src/styles')], }, - serverExternalPackages: ['@react-pdf/renderer'], + serverExternalPackages: ['@react-pdf/renderer', 'pino'], + logging: { + fetches: { + fullUrl: true, + }, + }, async rewrites() { return [ { diff --git a/package.json b/package.json index 439c5df..e309ff6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "next": "15.2.4", "nodemailer": "^7.0.3", "pdf-lib": "^1.17.1", + "pino": "^9.7.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-to-pdf": "^2.0.0", diff --git a/src/app/api/auth/chg-pwd/route.ts b/src/app/api/auth/chg-pwd/route.ts index 71e9f6b..321a69f 100644 --- a/src/app/api/auth/chg-pwd/route.ts +++ b/src/app/api/auth/chg-pwd/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' import { axiosInstance } from '@/libs/axios' -export async function POST(req: Request) { +async function setChgPwd(req: Request): Promise { const { loginId, email, pwd, chgPwd } = await req.json() console.log('πŸš€ ~ POST ~ loginId:', loginId) console.log('πŸš€ ~ POST ~ email:', email) @@ -19,3 +20,5 @@ export async function POST(req: Request) { return NextResponse.json({ code: 200, data: result.data }) } + +export const POST = loggerWrapper(setChgPwd) diff --git a/src/app/api/comm-code/route.ts b/src/app/api/comm-code/route.ts index d000836..6148a33 100644 --- a/src/app/api/comm-code/route.ts +++ b/src/app/api/comm-code/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' import type { CommCode } from '@/types/CommCode' -export async function GET(request: NextRequest) { +async function getCommCode(request: NextRequest): Promise { try { const searchParams = request.nextUrl.searchParams const headCode = searchParams.get('headCode') @@ -20,24 +21,26 @@ export async function GET(request: NextRequest) { if (!headCd) { return NextResponse.json({ error: `${headCode}λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€` }, { status: 404 }) } - // @ts-ignore - const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ - where: { - HEAD_CD: headCd.HEAD_CD, - }, - select: { - HEAD_CD: true, - CODE: true, - CODE_JP: true, - }, - orderBy: { - CODE: 'asc', - }, - }) + if (headCode === 'SALES_OFFICE_CD') { return getSaleOffice(headCd.HEAD_CD) + } else { + // @ts-ignore + const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ + where: { + HEAD_CD: headCd.HEAD_CD, + }, + select: { + HEAD_CD: true, + CODE: true, + CODE_JP: true, + }, + orderBy: { + CODE: 'asc', + }, + }) + return NextResponse.json(roofMaterials) } - return NextResponse.json(roofMaterials) } catch (error) { console.error('❌ 데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€:', error) return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) @@ -60,3 +63,5 @@ const getSaleOffice = async (headCode: string) => { }) return NextResponse.json(commCodeSaleOffice) } + +export const GET = loggerWrapper(getCommCode) diff --git a/src/app/api/qna/detail/route.ts b/src/app/api/qna/detail/route.ts index 315c18e..1ac937c 100644 --- a/src/app/api/qna/detail/route.ts +++ b/src/app/api/qna/detail/route.ts @@ -1,8 +1,9 @@ import { queryStringFormatter } from '@/utils/common-utils' import axios from 'axios' import { NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' -export async function GET(request: Request) { +async function getQnaDetail(request: Request): Promise { const { searchParams } = new URL(request.url) const params = { compCd: searchParams.get('compCd'), @@ -22,3 +23,5 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'route error' }, { status: 500 }) } } + +export const GET = loggerWrapper(getQnaDetail) diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts index 602bbd6..49c7209 100644 --- a/src/app/api/qna/file/route.ts +++ b/src/app/api/qna/file/route.ts @@ -1,5 +1,6 @@ import axios from 'axios' import { NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' // export async function GET(request: Request) { // const { searchParams } = new URL(request.url) @@ -38,7 +39,7 @@ import { NextResponse } from 'next/server' // } // } -export async function GET(request: Request) { +async function downloadFile(request: Request): Promise { const { searchParams } = new URL(request.url) const encodeFileNo = searchParams.get('encodeFileNo') const srcFileNm = searchParams.get('srcFileNm') || 'downloaded-file' @@ -71,3 +72,5 @@ export async function GET(request: Request) { return NextResponse.json({ error: error.response?.data || 'Failed to download file' }, { status: 500 }) } } + +export const GET = loggerWrapper(downloadFile) diff --git a/src/app/api/qna/list/route.ts b/src/app/api/qna/list/route.ts index 9708e4f..12cf96e 100644 --- a/src/app/api/qna/list/route.ts +++ b/src/app/api/qna/list/route.ts @@ -3,10 +3,11 @@ import { NextResponse } from 'next/server' import { queryStringFormatter } from '@/utils/common-utils' import { getIronSession } from 'iron-session' import { cookies } from 'next/headers' +import { loggerWrapper } from '@/libs/api-wrapper' import { sessionOptions } from '@/libs/session' import { SessionData } from '@/types/Auth' -export async function GET(request: Request) { +async function getQnaList(request: Request): Promise { const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) @@ -37,3 +38,5 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'route error' }, { status: 500 }) } } + +export const GET = loggerWrapper(getQnaList) diff --git a/src/app/api/qna/route.ts b/src/app/api/qna/route.ts index 994a86a..2773761 100644 --- a/src/app/api/qna/route.ts +++ b/src/app/api/qna/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from 'next/server' import axios from 'axios' import { CommonCode } from '@/types/Inquiry' +import { loggerWrapper } from '@/libs/api-wrapper' -export async function GET() { +async function getCommonCodeListData(request: Request): Promise { const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/system/commonCodeListData`) const codeList: CommonCode[] = [] response.data.data.apiCommCdList.forEach((item: any) => { @@ -17,3 +18,5 @@ export async function GET() { }) return NextResponse.json({ data: codeList }) } + +export const GET = loggerWrapper(getCommonCodeListData) diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index b440ef8..8cd1ad5 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -1,7 +1,8 @@ import axios from 'axios' import { NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' -export async function POST(request: Request) { +async function setQna(request: Request): Promise { const formData = await request.formData() console.log(formData) try { @@ -19,3 +20,5 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Failed to save qna' }, { status: 500 }) } } + +export const POST = loggerWrapper(setQna) diff --git a/src/app/api/submission/admin-sub/route.ts b/src/app/api/submission/admin-sub/route.ts index c8c30f3..28e3627 100644 --- a/src/app/api/submission/admin-sub/route.ts +++ b/src/app/api/submission/admin-sub/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@/libs/prisma' import { NextRequest, NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' type AdminSubPerson = { storeId: string @@ -7,8 +8,8 @@ type AdminSubPerson = { eMail: string authority: string } -// 2차점이 μžμ‹ μ—κ²Œ λ§€ν•‘ 된 1μ°¨ 판맀점과 κ΄€λ¦¬μž 정보 쑰회 -export async function GET(request: NextRequest) { +// 2차점이 μžμ‹ μ—κ²Œ λ§€ν•‘ 된 1μ°¨ 판맀점과 κ΄€λ¦¬μž 정보 쑰회 +async function getSubMissionAdminSub(request: NextRequest): Promise { try { const { searchParams } = new URL(request.url) const id = searchParams.get('id') @@ -40,3 +41,5 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) } } + +export const GET = loggerWrapper(getSubMissionAdminSub) diff --git a/src/app/api/submission/admin/route.ts b/src/app/api/submission/admin/route.ts index 0b52031..eae6881 100644 --- a/src/app/api/submission/admin/route.ts +++ b/src/app/api/submission/admin/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@/libs/prisma' import { NextRequest, NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' type SuperPerson = { storeId: string @@ -8,7 +9,7 @@ type SuperPerson = { toEmail: string } -export async function GET(request: NextRequest) { +async function getSubmissionAdmin(request: NextRequest): Promise { try { const { searchParams } = new URL(request.url) const id = searchParams.get('id') @@ -44,3 +45,5 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) } } + +export const GET = loggerWrapper(getSubmissionAdmin) diff --git a/src/app/api/submission/builder/route.ts b/src/app/api/submission/builder/route.ts index 41c66ce..f1f0f93 100644 --- a/src/app/api/submission/builder/route.ts +++ b/src/app/api/submission/builder/route.ts @@ -1,6 +1,7 @@ import { prisma } from '@/libs/prisma' import { SubmitTargetResponse } from '@/types/Survey' import { NextRequest, NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' type BuilderPerson = { agencyStoreId: string @@ -11,7 +12,7 @@ type BuilderPerson = { // 2차점의 μ‹œκ³΅κΆŒν•œ userκ°€ ν•΄λ‹Ή 판맀점의 κ΄€λ¦¬μž 정보 쑰회 // N == μΌλ°˜μœ μ €, S == μˆ˜νΌμœ μ €, B == μ‹œκ³΅κΆŒν•œμœ μ € -export async function GET(request: NextRequest) { +async function getSubmissionBuilder(request: NextRequest): Promise { try { const { searchParams } = new URL(request.url) const id = searchParams.get('id') @@ -46,3 +47,5 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) } } + +export const GET = loggerWrapper(getSubmissionBuilder) diff --git a/src/app/api/submission/super/route.ts b/src/app/api/submission/super/route.ts index 6eed5c7..faa66cf 100644 --- a/src/app/api/submission/super/route.ts +++ b/src/app/api/submission/super/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@/libs/prisma' import { NextRequest, NextResponse } from 'next/server' +import { loggerWrapper } from '@/libs/api-wrapper' type SuperPerson = { storeId: string @@ -7,7 +8,7 @@ type SuperPerson = { eMail: string } -export async function GET(request: NextRequest) { +async function getSubmissionSuper(request: NextRequest): Promise { try { const { searchParams } = new URL(request.url) const id = searchParams.get('id') @@ -37,3 +38,5 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€' }, { status: 500 }) } } + +export const GET = loggerWrapper(getSubmissionSuper) diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index c668153..c8b3719 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,4 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' +import { HttpStatusCode } from 'axios' +import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' import { type Suitable } from '@/types/Suitable' @@ -41,7 +43,7 @@ import { type Suitable } from '@/types/Suitable' * } * ] */ -export async function GET(request: NextRequest) { +async function getSuitableList(request: NextRequest): Promise { try { const searchParams = request.nextUrl.searchParams const pageNumber = parseInt(searchParams.get('pageNumber') || '0') @@ -51,7 +53,7 @@ export async function GET(request: NextRequest) { /* νŒŒλΌλ―Έν„° 체크 */ if (pageNumber === 0 || itemPerPage === 0) { - return NextResponse.json({ error: 'νŽ˜μ΄μ§€ λ²ˆν˜Έμ™€ νŽ˜μ΄μ§€λ‹Ή μ•„μ΄ν…œ μˆ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€' }, { status: 400 }) + return NextResponse.json({ error: 'νŽ˜μ΄μ§€ λ²ˆν˜Έμ™€ νŽ˜μ΄μ§€λ‹Ή μ•„μ΄ν…œ μˆ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€' }, { status: HttpStatusCode.BadRequest }) } let query = ` @@ -108,9 +110,12 @@ export async function GET(request: NextRequest) { headers: { 'spinner-state': 'true', }, + status: HttpStatusCode.Ok, }) } catch (error) { console.error(`데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}`) - return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: 500 }) + return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: HttpStatusCode.InternalServerError }) } } + +export const GET = loggerWrapper(getSuitableList) diff --git a/src/app/api/suitable/pdf/route.ts b/src/app/api/suitable/pdf/route.ts index 768f81e..ccef573 100644 --- a/src/app/api/suitable/pdf/route.ts +++ b/src/app/api/suitable/pdf/route.ts @@ -1,7 +1,9 @@ import React from 'react' import { NextRequest, NextResponse } from 'next/server' +import { HttpStatusCode } from 'axios' import { pdf, Document } from '@react-pdf/renderer' import { PDFDocument } from 'pdf-lib' +import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' import { type Suitable } from '@/types/Suitable' import SuitablePdf from '@/components/pdf/SuitablePdf' @@ -28,7 +30,7 @@ import SuitablePdf from '@/components/pdf/SuitablePdf' * * @apiSuccess {File} μ§€λΆ•μž¬ 적합성 PDF 파일 */ -export async function POST(request: NextRequest) { +async function createSuitablePdf(request: NextRequest): Promise { const formData = await request.formData() const ids = formData.get('ids') as string const detailIds = formData.get('detailIds') as string @@ -36,7 +38,7 @@ export async function POST(request: NextRequest) { /* νŒŒλΌλ―Έν„° 체크 */ if (ids === '' || detailIds === '' || fileTitle === '') { - return NextResponse.json({ error: 'ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€' }, { status: 400 }) + return NextResponse.json({ error: 'ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€' }, { status: HttpStatusCode.BadRequest }) } try { @@ -121,7 +123,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { console.error(`데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}`) - return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: 500 }) + return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: HttpStatusCode.InternalServerError }) } } @@ -142,3 +144,5 @@ async function mergePdfBuffers(buffers: Uint8Array[]) { const mergedPdfBytes = await mergedPdf.save() return mergedPdfBytes } + +export const POST = loggerWrapper(createSuitablePdf) diff --git a/src/app/api/suitable/pick/route.ts b/src/app/api/suitable/pick/route.ts index 2865777..8d345ce 100644 --- a/src/app/api/suitable/pick/route.ts +++ b/src/app/api/suitable/pick/route.ts @@ -1,23 +1,25 @@ import { NextRequest, NextResponse } from 'next/server' +import { HttpStatusCode } from 'axios' +import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' /** * @api {get} /api/suitable/pick μ§€λΆ•μž¬ 적합성 데이터 아이디 쑰회 API * @apiName GetSuitablePick * @apiGroup Suitable - * + * * @apiDescription * μ§€λΆ•μž¬ 적합성 λ°μ΄ν„°μ˜ 검색 쑰건에 λ§žλŠ” main_id와 detail_idλ₯Ό 쑰회 - * + * * @apiParam {String} [category] μ§€λΆ•μž¬κ·Έλ£Ήμ½”λ“œ (예: RMG001) * @apiParam {String} [keyword] κ²€μƒ‰ν‚€μ›Œλ“œ - * + * * @apiExample {curl} Example usage: * curl -X GET \ * -G "category=RMG001" \ * -G "keyword=κ²€μƒ‰ν‚€μ›Œλ“œ" \ * http://localhost:3000/api/suitable/pick - * + * * @apiSuccess {Array} suitableIdSet μ§€λΆ•μž¬ 적합성 λ°μ΄ν„°μ˜ 아이디 λͺ©λ‘ * @apiSuccessExample {json} Success-Response: * [ @@ -27,7 +29,7 @@ import { prisma } from '@/libs/prisma' * } * ] */ -export async function GET(request: NextRequest) { +async function getSuitablePick(request: NextRequest): Promise { try { const searchParams = request.nextUrl.searchParams const category = searchParams.get('category') @@ -72,6 +74,8 @@ export async function GET(request: NextRequest) { return NextResponse.json(suitableIdSet) } catch (error) { console.error(`데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}`) - return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: 500 }) + return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: HttpStatusCode.InternalServerError }) } } + +export const GET = loggerWrapper(getSuitablePick) diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index c9ca8c4..9227cfc 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -1,4 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' +import { HttpStatusCode } from 'axios' +import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' import { Suitable } from '@/types/Suitable' @@ -40,7 +42,7 @@ import { Suitable } from '@/types/Suitable' * } * ] */ -export async function POST(request: NextRequest) { +async function getSuitable(request: NextRequest): Promise { try { const body: Record = await request.json() const ids = body.ids @@ -48,7 +50,7 @@ export async function POST(request: NextRequest) { /* νŒŒλΌλ―Έν„° 체크 */ if (ids === '') { - return NextResponse.json({ error: 'ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€' }, { status: 400 }) + return NextResponse.json({ error: 'ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€' }, { status: HttpStatusCode.BadRequest }) } let query = ` @@ -96,6 +98,8 @@ export async function POST(request: NextRequest) { return NextResponse.json(suitable) } catch (error) { console.error(`데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}`) - return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: 500 }) + return NextResponse.json({ error: `데이터 쑰회 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error}` }, { status: HttpStatusCode.InternalServerError }) } } + +export const POST = loggerWrapper(getSuitable) diff --git a/src/libs/api-wrapper.ts b/src/libs/api-wrapper.ts new file mode 100644 index 0000000..4caad47 --- /dev/null +++ b/src/libs/api-wrapper.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server' +import { writeApiLog } from './logger' + +export function loggerWrapper(handler: (req: NextRequest) => Promise): (req: NextRequest) => Promise { + return async function (req: NextRequest) { + const reqClone = req.clone() + + const response = await handler(req) + + await writeApiLog( + new NextRequest(req.url, { + method: req.method, + headers: req.headers, + body: reqClone.body ? await reqClone.text() : undefined, + }), + response.status, + ) + + return response + } +} diff --git a/src/libs/logger.ts b/src/libs/logger.ts new file mode 100644 index 0000000..d75083d --- /dev/null +++ b/src/libs/logger.ts @@ -0,0 +1,92 @@ +import { NextRequest } from 'next/server' +import { join } from 'path' +import pino from 'pino' + +/* 둜그 데이터 μΈν„°νŽ˜μ΄μŠ€ */ +interface ApiLogData { + responseStatus: number + method: string + url: string + // headers: { [k: string]: string } + query: { [k: string]: string } + body: string | undefined +} + +/* λ‚ μ§œλ³„ 둜그 파일 경둜 생성 ν•¨μˆ˜ */ +const getLogFilePath = (): string => { + const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD ν˜•μ‹ + return join(process.cwd(), 'logs', `onsite-survey-${today}.log`) +} + +/* λ‚ μ§œλ³„ 둜거 생성 클래슀 */ +class DailyLogger { + private currentDate: string + private logger: pino.Logger + private destination: ReturnType + + constructor() { + this.currentDate = new Date().toISOString().split('T')[0] + this.destination = pino.destination({ + dest: getLogFilePath(), + mkdir: true, + sync: false, + }) + this.logger = this.createLogger() + + /* kill signal ν•Έλ“€λŸ¬ 등둝 */ + process.on('SIGTERM', this.handleShutdown.bind(this)) + process.on('SIGINT', this.handleShutdown.bind(this)) + } + + private async handleShutdown(): Promise { + this.destination.flushSync() + this.destination.end() + } + + private createLogger(): pino.Logger { + return pino( + { + level: 'info', + timestamp: pino.stdTimeFunctions.isoTime, + }, + this.destination, + ) + } + + public info(obj: any, msg?: string): void { + const today = new Date().toISOString().split('T')[0] + + if (today !== this.currentDate) { + /* κΈ°μ‘΄ destination μ’…λ£Œ */ + this.destination.flushSync() + this.destination.end() + + /* μƒˆλ‘œμš΄ destination 생성 */ + this.destination = pino.destination({ + dest: getLogFilePath(), + mkdir: true, + sync: false, + }) + this.currentDate = today + this.logger = this.createLogger() + } + + this.logger.info(obj, msg) + } +} + +/* 둜거 μΈμŠ€ν„΄μŠ€ */ +const dailyLogger = new DailyLogger() + +/* API 둜그 기둝 ν•¨μˆ˜ */ +export const writeApiLog = async (request: NextRequest, responseStatus: number): Promise => { + const logData: ApiLogData = { + responseStatus: responseStatus, + method: request.method, + url: request.url, + // headers: Object.fromEntries(request.headers), + query: Object.fromEntries(new URL(request.url).searchParams), + body: request.body ? await request.text() : undefined, + } + dailyLogger.info(logData, 'API Request') +}