Merge pull request 'feat: api logger 추가' (#69) from feature/log into dev

Reviewed-on: #69
This commit is contained in:
swyoo 2025-06-17 11:24:40 +09:00
commit 870e6ad02d
20 changed files with 222 additions and 44 deletions

6
.gitignore vendored
View File

@ -45,4 +45,8 @@ next-env.d.ts
bun.lockb bun.lockb
pnpm-lock.yaml pnpm-lock.yaml
pnpm-workspace.yaml pnpm-workspace.yaml
# logs
logs/
*.log

View File

@ -6,7 +6,12 @@ const nextConfig: NextConfig = {
sassOptions: { sassOptions: {
includePaths: [path.join(__dirname, './src/styles')], includePaths: [path.join(__dirname, './src/styles')],
}, },
serverExternalPackages: ['@react-pdf/renderer'], serverExternalPackages: ['@react-pdf/renderer', 'pino'],
logging: {
fetches: {
fullUrl: true,
},
},
async rewrites() { async rewrites() {
return [ return [
{ {

View File

@ -29,6 +29,7 @@
"next": "15.2.4", "next": "15.2.4",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pino": "^9.7.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-to-pdf": "^2.0.0", "react-to-pdf": "^2.0.0",

View File

@ -1,7 +1,8 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
import { axiosInstance } from '@/libs/axios' import { axiosInstance } from '@/libs/axios'
export async function POST(req: Request) { async function setChgPwd(req: Request): Promise<NextResponse> {
const { loginId, email, pwd, chgPwd } = await req.json() const { loginId, email, pwd, chgPwd } = await req.json()
console.log('🚀 ~ POST ~ loginId:', loginId) console.log('🚀 ~ POST ~ loginId:', loginId)
console.log('🚀 ~ POST ~ email:', email) console.log('🚀 ~ POST ~ email:', email)
@ -19,3 +20,5 @@ export async function POST(req: Request) {
return NextResponse.json({ code: 200, data: result.data }) return NextResponse.json({ code: 200, data: result.data })
} }
export const POST = loggerWrapper(setChgPwd)

View File

@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import type { CommCode } from '@/types/CommCode' import type { CommCode } from '@/types/CommCode'
export async function GET(request: NextRequest) { async function getCommCode(request: NextRequest): Promise<NextResponse> {
try { try {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const headCode = searchParams.get('headCode') const headCode = searchParams.get('headCode')
@ -20,24 +21,26 @@ export async function GET(request: NextRequest) {
if (!headCd) { if (!headCd) {
return NextResponse.json({ error: `${headCode}를 찾을 수 없습니다` }, { status: 404 }) 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') { if (headCode === 'SALES_OFFICE_CD') {
return getSaleOffice(headCd.HEAD_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) { } catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
@ -60,3 +63,5 @@ const getSaleOffice = async (headCode: string) => {
}) })
return NextResponse.json(commCodeSaleOffice) return NextResponse.json(commCodeSaleOffice)
} }
export const GET = loggerWrapper(getCommCode)

View File

@ -1,8 +1,9 @@
import { queryStringFormatter } from '@/utils/common-utils' import { queryStringFormatter } from '@/utils/common-utils'
import axios from 'axios' import axios from 'axios'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
export async function GET(request: Request) { async function getQnaDetail(request: Request): Promise<NextResponse> {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const params = { const params = {
compCd: searchParams.get('compCd'), compCd: searchParams.get('compCd'),
@ -22,3 +23,5 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'route error' }, { status: 500 }) return NextResponse.json({ error: 'route error' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getQnaDetail)

View File

@ -1,5 +1,6 @@
import axios from 'axios' import axios from 'axios'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
// export async function GET(request: Request) { // export async function GET(request: Request) {
// const { searchParams } = new URL(request.url) // 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<NextResponse> {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const encodeFileNo = searchParams.get('encodeFileNo') const encodeFileNo = searchParams.get('encodeFileNo')
const srcFileNm = searchParams.get('srcFileNm') || 'downloaded-file' 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 }) return NextResponse.json({ error: error.response?.data || 'Failed to download file' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(downloadFile)

View File

@ -3,10 +3,11 @@ import { NextResponse } from 'next/server'
import { queryStringFormatter } from '@/utils/common-utils' import { queryStringFormatter } from '@/utils/common-utils'
import { getIronSession } from 'iron-session' import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers' import { cookies } from 'next/headers'
import { loggerWrapper } from '@/libs/api-wrapper'
import { sessionOptions } from '@/libs/session' import { sessionOptions } from '@/libs/session'
import { SessionData } from '@/types/Auth' import { SessionData } from '@/types/Auth'
export async function GET(request: Request) { async function getQnaList(request: Request): Promise<NextResponse> {
const cookieStore = await cookies() const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions) const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
@ -37,3 +38,5 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'route error' }, { status: 500 }) return NextResponse.json({ error: 'route error' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getQnaList)

View File

@ -1,8 +1,9 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import axios from 'axios' import axios from 'axios'
import { CommonCode } from '@/types/Inquiry' import { CommonCode } from '@/types/Inquiry'
import { loggerWrapper } from '@/libs/api-wrapper'
export async function GET() { async function getCommonCodeListData(request: Request): Promise<NextResponse> {
const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/system/commonCodeListData`) const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/system/commonCodeListData`)
const codeList: CommonCode[] = [] const codeList: CommonCode[] = []
response.data.data.apiCommCdList.forEach((item: any) => { response.data.data.apiCommCdList.forEach((item: any) => {
@ -17,3 +18,5 @@ export async function GET() {
}) })
return NextResponse.json({ data: codeList }) return NextResponse.json({ data: codeList })
} }
export const GET = loggerWrapper(getCommonCodeListData)

View File

@ -1,7 +1,8 @@
import axios from 'axios' import axios from 'axios'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
export async function POST(request: Request) { async function setQna(request: Request): Promise<NextResponse> {
const formData = await request.formData() const formData = await request.formData()
console.log(formData) console.log(formData)
try { try {
@ -19,3 +20,5 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Failed to save qna' }, { status: 500 }) return NextResponse.json({ error: 'Failed to save qna' }, { status: 500 })
} }
} }
export const POST = loggerWrapper(setQna)

View File

@ -1,5 +1,6 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
type AdminSubPerson = { type AdminSubPerson = {
storeId: string storeId: string
@ -7,8 +8,8 @@ type AdminSubPerson = {
eMail: string eMail: string
authority: string authority: string
} }
// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회 // 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회
export async function GET(request: NextRequest) { async function getSubMissionAdminSub(request: NextRequest): Promise<NextResponse> {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const id = searchParams.get('id') const id = searchParams.get('id')
@ -40,3 +41,5 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSubMissionAdminSub)

View File

@ -1,5 +1,6 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
type SuperPerson = { type SuperPerson = {
storeId: string storeId: string
@ -8,7 +9,7 @@ type SuperPerson = {
toEmail: string toEmail: string
} }
export async function GET(request: NextRequest) { async function getSubmissionAdmin(request: NextRequest): Promise<NextResponse> {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const id = searchParams.get('id') const id = searchParams.get('id')
@ -44,3 +45,5 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSubmissionAdmin)

View File

@ -1,6 +1,7 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { SubmitTargetResponse } from '@/types/Survey' import { SubmitTargetResponse } from '@/types/Survey'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
type BuilderPerson = { type BuilderPerson = {
agencyStoreId: string agencyStoreId: string
@ -11,7 +12,7 @@ type BuilderPerson = {
// 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회 // 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회
// N == 일반유저, S == 수퍼유저, B == 시공권한유저 // N == 일반유저, S == 수퍼유저, B == 시공권한유저
export async function GET(request: NextRequest) { async function getSubmissionBuilder(request: NextRequest): Promise<NextResponse> {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const id = searchParams.get('id') const id = searchParams.get('id')
@ -46,3 +47,5 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSubmissionBuilder)

View File

@ -1,5 +1,6 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
type SuperPerson = { type SuperPerson = {
storeId: string storeId: string
@ -7,7 +8,7 @@ type SuperPerson = {
eMail: string eMail: string
} }
export async function GET(request: NextRequest) { async function getSubmissionSuper(request: NextRequest): Promise<NextResponse> {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const id = searchParams.get('id') const id = searchParams.get('id')
@ -37,3 +38,5 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSubmissionSuper)

View File

@ -1,4 +1,6 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { HttpStatusCode } from 'axios'
import { loggerWrapper } from '@/libs/api-wrapper'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { type Suitable } from '@/types/Suitable' 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<NextResponse> {
try { try {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const pageNumber = parseInt(searchParams.get('pageNumber') || '0') const pageNumber = parseInt(searchParams.get('pageNumber') || '0')
@ -51,7 +53,7 @@ export async function GET(request: NextRequest) {
/* 파라미터 체크 */ /* 파라미터 체크 */
if (pageNumber === 0 || itemPerPage === 0) { if (pageNumber === 0 || itemPerPage === 0) {
return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 }) return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: HttpStatusCode.BadRequest })
} }
let query = ` let query = `
@ -108,9 +110,12 @@ export async function GET(request: NextRequest) {
headers: { headers: {
'spinner-state': 'true', 'spinner-state': 'true',
}, },
status: HttpStatusCode.Ok,
}) })
} catch (error) { } catch (error) {
console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`)
return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: HttpStatusCode.InternalServerError })
} }
} }
export const GET = loggerWrapper(getSuitableList)

View File

@ -1,7 +1,9 @@
import React from 'react' import React from 'react'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { HttpStatusCode } from 'axios'
import { pdf, Document } from '@react-pdf/renderer' import { pdf, Document } from '@react-pdf/renderer'
import { PDFDocument } from 'pdf-lib' import { PDFDocument } from 'pdf-lib'
import { loggerWrapper } from '@/libs/api-wrapper'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { type Suitable } from '@/types/Suitable' import { type Suitable } from '@/types/Suitable'
import SuitablePdf from '@/components/pdf/SuitablePdf' import SuitablePdf from '@/components/pdf/SuitablePdf'
@ -28,7 +30,7 @@ import SuitablePdf from '@/components/pdf/SuitablePdf'
* *
* @apiSuccess {File} PDF * @apiSuccess {File} PDF
*/ */
export async function POST(request: NextRequest) { async function createSuitablePdf(request: NextRequest): Promise<NextResponse> {
const formData = await request.formData() const formData = await request.formData()
const ids = formData.get('ids') as string const ids = formData.get('ids') as string
const detailIds = formData.get('detailIds') as string const detailIds = formData.get('detailIds') as string
@ -36,7 +38,7 @@ export async function POST(request: NextRequest) {
/* 파라미터 체크 */ /* 파라미터 체크 */
if (ids === '' || detailIds === '' || fileTitle === '') { if (ids === '' || detailIds === '' || fileTitle === '') {
return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: HttpStatusCode.BadRequest })
} }
try { try {
@ -121,7 +123,7 @@ export async function POST(request: NextRequest) {
}) })
} catch (error) { } catch (error) {
console.error(`데이터 조회 중 오류가 발생했습니다: ${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() const mergedPdfBytes = await mergedPdf.save()
return mergedPdfBytes return mergedPdfBytes
} }
export const POST = loggerWrapper(createSuitablePdf)

View File

@ -1,23 +1,25 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { HttpStatusCode } from 'axios'
import { loggerWrapper } from '@/libs/api-wrapper'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
/** /**
* @api {get} /api/suitable/pick API * @api {get} /api/suitable/pick API
* @apiName GetSuitablePick * @apiName GetSuitablePick
* @apiGroup Suitable * @apiGroup Suitable
* *
* @apiDescription * @apiDescription
* main_id와 detail_id를 * main_id와 detail_id를
* *
* @apiParam {String} [category] (: RMG001) * @apiParam {String} [category] (: RMG001)
* @apiParam {String} [keyword] * @apiParam {String} [keyword]
* *
* @apiExample {curl} Example usage: * @apiExample {curl} Example usage:
* curl -X GET \ * curl -X GET \
* -G "category=RMG001" \ * -G "category=RMG001" \
* -G "keyword=검색키워드" \ * -G "keyword=검색키워드" \
* http://localhost:3000/api/suitable/pick * http://localhost:3000/api/suitable/pick
* *
* @apiSuccess {Array} suitableIdSet * @apiSuccess {Array} suitableIdSet
* @apiSuccessExample {json} Success-Response: * @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<NextResponse> {
try { try {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const category = searchParams.get('category') const category = searchParams.get('category')
@ -72,6 +74,8 @@ export async function GET(request: NextRequest) {
return NextResponse.json(suitableIdSet) return NextResponse.json(suitableIdSet)
} catch (error) { } catch (error) {
console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`)
return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: HttpStatusCode.InternalServerError })
} }
} }
export const GET = loggerWrapper(getSuitablePick)

View File

@ -1,4 +1,6 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { HttpStatusCode } from 'axios'
import { loggerWrapper } from '@/libs/api-wrapper'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { Suitable } from '@/types/Suitable' 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<NextResponse> {
try { try {
const body: Record<string, string> = await request.json() const body: Record<string, string> = await request.json()
const ids = body.ids const ids = body.ids
@ -48,7 +50,7 @@ export async function POST(request: NextRequest) {
/* 파라미터 체크 */ /* 파라미터 체크 */
if (ids === '') { if (ids === '') {
return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: HttpStatusCode.BadRequest })
} }
let query = ` let query = `
@ -96,6 +98,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json(suitable) return NextResponse.json(suitable)
} catch (error) { } catch (error) {
console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`)
return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: HttpStatusCode.InternalServerError })
} }
} }
export const POST = loggerWrapper(getSuitable)

21
src/libs/api-wrapper.ts Normal file
View File

@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from 'next/server'
import { writeApiLog } from './logger'
export function loggerWrapper(handler: (req: NextRequest) => Promise<NextResponse>): (req: NextRequest) => Promise<NextResponse> {
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
}
}

95
src/libs/logger.ts Normal file
View File

@ -0,0 +1,95 @@
import { NextRequest } from 'next/server'
import { join } from 'path'
import pino from 'pino'
/* 실행 모드 */
const isProduction = process.env.NODE_ENV === 'production'
/* 로그 데이터 인터페이스 */
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<typeof pino.destination>
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<void> {
this.destination.flushSync()
this.destination.end()
}
private createLogger(): pino.Logger {
return pino(
{
level: isProduction ? 'info' : 'silent',
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<void> => {
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')
}