Compare commits

..

22 Commits

Author SHA1 Message Date
fcf87c153e refactor: 지붕재적합성 에러처리 추가 2025-06-17 15:40:14 +09:00
54cd61c8b2 Merge pull request 'feature/survey' (#71) from feature/survey into dev
Reviewed-on: #71
2025-06-17 13:43:24 +09:00
17d306bb11 fix: change submit data set logic
- 제출 폼 데이터 삽입 안되는 오류 해결
- api error 메세지 설정
- api 응답 객체 변경
2025-06-17 13:20:18 +09:00
870e6ad02d Merge pull request 'feat: api logger 추가' (#69) from feature/log into dev
Reviewed-on: #69
2025-06-17 11:24:40 +09:00
e806358d0e fix: update route at useTitle
- useTitle 헤더 제목, url 변경
- 임시저장 로직 수정
2025-06-17 11:01:03 +09:00
bc73d36588 feat: api logger 실행모드 production 으로 한정 2025-06-17 10:52:16 +09:00
cd28667f3c feat: add route error handling for invalid URL access in DetailForm component 2025-06-16 17:59:03 +09:00
19a11783d6 fix: fix type error 2025-06-16 17:38:34 +09:00
5d5ba2e82a feat: add api logger to survey-sale api 2025-06-16 17:31:47 +09:00
a2e6c2343a Merge branch 'feature/log' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-06-16 17:01:46 +09:00
b783ca92d7 feat: 기 개발된 api에 api logger 적용 2025-06-16 16:49:59 +09:00
c212ed0ad6 fix: api log wrapper 수정 2025-06-16 16:03:38 +09:00
70c5af70f5 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-06-16 09:20:45 +09:00
ed8c6187d3 feat: api log wrapper 추가 2025-06-13 17:26:34 +09:00
7597700e7b feat: logger 날짜 분리 추가 2025-06-13 16:50:15 +09:00
8fc88f526d Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/log 2025-06-13 16:12:36 +09:00
77895c220d Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-06-13 13:17:46 +09:00
042096c682 refactor: add an error alert conditional 2025-06-13 13:17:37 +09:00
298b563977 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/log 2025-06-13 13:02:22 +09:00
36a06c226b Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/log 2025-06-13 13:01:28 +09:00
3eb5974414 feat: api logger 적용 2025-06-12 18:05:29 +09:00
a573d7ffb1 feat: api logger 추가 2025-06-12 18:03:24 +09:00
33 changed files with 364 additions and 132 deletions

6
.gitignore vendored
View File

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

View File

@ -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 [
{

View File

@ -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",

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
const cookieStore = await cookies()
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 })
}
}
export const GET = loggerWrapper(getQnaList)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -1,14 +1,10 @@
import { prisma } from '@/libs/prisma'
import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper'
import { SubmitTargetResponse } from '@/types/Survey'
type AdminSubPerson = {
storeId: string
userId: string
eMail: string
authority: string
}
// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회
export async function GET(request: NextRequest) {
// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회
async function getSubMissionAdminSub(request: NextRequest): Promise<NextResponse> {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
@ -32,11 +28,12 @@ export async function GET(request: NextRequest) {
AND MCS.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY;
`
const suitable: AdminSubPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json({ message: 'Hello, world!' })
const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
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 { 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<NextResponse> {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
@ -36,11 +37,12 @@ export async function GET(request: NextRequest) {
;
CLOSE SYMMETRIC KEY SYMMETRICKEY;
`
const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json({ message: 'Hello, world!' })
const data: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 })
}
}
export const GET = loggerWrapper(getSubmissionAdmin)

View File

@ -1,17 +1,11 @@
import { prisma } from '@/libs/prisma'
import { SubmitTargetResponse } from '@/types/Survey'
import { NextRequest, NextResponse } from 'next/server'
type BuilderPerson = {
agencyStoreId: string
userId: string
eMail: string
userAuthCd: string
}
import { loggerWrapper } from '@/libs/api-wrapper'
// 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회
// N == 일반유저, S == 수퍼유저, B == 시공권한유저
export async function GET(request: NextRequest) {
async function getSubmissionBuilder(request: NextRequest): Promise<NextResponse> {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
@ -36,13 +30,12 @@ export async function GET(request: NextRequest) {
AND MCAS.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY;
`
// const suitable: BuilderPerson[] = await prisma.$queryRawUnsafe(query)
// return NextResponse.json({ message: 'Hello, world!' })
const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
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 { 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<NextResponse> {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
@ -29,11 +30,12 @@ export async function GET(request: NextRequest) {
AND MCSA.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY;
`
const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json({ message: 'Hello, world!' })
const data: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error);
return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 });
}
}
export const GET = loggerWrapper(getSubmissionSuper)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
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)

View File

@ -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<NextResponse> {
try {
const body: Record<string, string> = 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)

View File

@ -6,6 +6,7 @@ import { sessionOptions } from '@/libs/session'
import { cookies } from 'next/headers'
import type { SessionData } from '@/types/Auth'
import { Prisma } from '@prisma/client'
import { loggerWrapper } from '@/libs/api-wrapper'
/**
* @description
@ -128,11 +129,11 @@ const fetchSurvey = async (id: number) => {
* ...
* }
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
async function getSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
const { id } = await params
const id = request.nextUrl.pathname.split('/').pop() ?? ''
const { searchParams } = new URL(request.url)
const isPdf = searchParams.get('isPdf') === 'true'
@ -228,9 +229,9 @@ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => {
* ...
* }
* */
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
async function updateSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try {
const { id } = await params
const id = request.nextUrl.pathname.split('/').pop() ?? ''
const body = await request.json()
const { detailInfo, ...basicInfo } = body.survey
@ -257,6 +258,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
}
}
/**
* @api {DELETE} /api/survey-sales/:id API
* @apiName DELETE /api/survey-sales/:id
@ -275,9 +277,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
* {
* "message": "success"
*/
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
async function deleteSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try {
const { id } = await params
const id = request.nextUrl.pathname.split('/').pop() ?? ''
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
// @ts-ignore
@ -343,9 +345,9 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
* }
* }
*/
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
async function submitSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try {
const { id } = await params
const id = request.nextUrl.pathname.split('/').pop() ?? ''
const body = await request.json()
// @ts-ignore
const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
@ -364,3 +366,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
}
}
export const GET = loggerWrapper(getSurveySaleDetail)
export const PUT = loggerWrapper(updateSurveySaleDetail)
export const DELETE = loggerWrapper(deleteSurveySaleDetail)
export const PATCH = loggerWrapper(submitSurveySaleDetail)

View File

@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import { convertToSnakeCase } from '@/utils/common-utils'
import { loggerWrapper } from '@/libs/api-wrapper'
/**
* @description
*/
@ -192,7 +193,7 @@ const checkSession = (params: SearchParams) => {
* }
*
*/
export async function GET(request: Request) {
async function getSurveySales(request: Request) {
try {
/** URL 파라미터 파싱 */
const { searchParams } = new URL(request.url)
@ -273,7 +274,7 @@ export async function GET(request: Request) {
*
* @apiError {Number} 500
*/
export async function PUT(request: Request) {
async function updateSurveySales(request: Request) {
try {
/** 요청 바디 파싱 */
const body = await request.json()
@ -335,7 +336,7 @@ export async function PUT(request: Request) {
*
* @apiError {Number} 500
*/
export async function POST(request: Request) {
async function createSurveySales(request: Request) {
try {
const body = await request.json()
@ -393,3 +394,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
}
}
export const GET = loggerWrapper(getSurveySales)
export const PUT = loggerWrapper(updateSurveySales)
export const POST = loggerWrapper(createSurveySales)

View File

@ -9,7 +9,7 @@ import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/
export default function SuitableDetailPopup() {
const popupController = usePopupController()
const { toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo, getSelectedSuitables } = useSuitable()
const { toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo, getSelectedSuitables, suitableErrorAlert } = useSuitable()
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const [selectedSuitables, setSelectedSuitables] = useState<Suitable[]>([])
@ -24,9 +24,13 @@ export default function SuitableDetailPopup() {
}, [])
useEffect(() => {
getSelectedSuitables().then((res) => {
setSelectedSuitables(res)
})
getSelectedSuitables()
.then((res) => {
setSelectedSuitables(res)
})
.catch(() => {
suitableErrorAlert()
})
}, [])
return (

View File

@ -48,34 +48,43 @@ export default function SurveySaleSubmitPopup() {
})
const [commCodeList, setCommCodeList] = useState<CommCode[]>([])
/** 제출 타겟 데이터 조회 및 제출 폼 데이터 삽입 */
useEffect(() => {
if (!session?.isLoggedIn || !surveyDetail?.id) return
const baseUpdate = {
sender: session?.email ?? '',
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
}
if (session?.role === 'Admin') {
getCommCode('SALES_OFFICE_CD').then((codes) => {
setCommCodeList(codes)
})
setSubmitData((prev) => ({
...prev,
...baseUpdate,
}))
} else if (session?.role === 'Builder' || session?.role === 'Admin_Sub') {
getSubmitTarget({ storeId: surveyDetail?.storeId ?? '', role: session?.role ?? '' }).then((data) => {
if (data) {
setSubmitData({
...submitData,
targetId: data[0].targetStoreId,
targetNm: data[0].targetStoreNm,
})
data.length > 1 &&
setSubmitData({
...submitData,
receiver: data.filter((item) => item.auth === 'S').map((item) => item.repUserEmail),
reference: data.filter((item) => item.auth === 'N').map((item) => item.repUserEmail),
})
if (!data) return
if (data && data.length > 0) {
const updateData: Partial<SubmitFormData> = {
sender: session?.email ?? '',
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
targetId: data[0]?.targetStoreId ?? '',
targetNm: data[0]?.targetStoreNm ?? '',
receiver: data.filter((item) => item.auth === 'S').map((item) => item.repUserEmail),
reference: data.filter((item) => item.auth === 'N').map((item) => item.repUserEmail),
saleBase: null,
contents: '',
}
setSubmitData((prev) => ({
...prev,
...updateData,
}))
}
})
}
setSubmitData({
...submitData,
sender: session?.email ?? '',
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
})
}, [session, surveyDetail])
const FORM_FIELDS: FormField[] = [
@ -118,6 +127,7 @@ export default function SurveySaleSubmitPopup() {
setIsShow(true)
sendEmail({
to: submitData.receiver,
cc: submitData.reference ?? '',
subject: submitData.title,
content: contentsRef.current?.innerHTML ?? '',
})

View File

@ -6,12 +6,16 @@ import { useSuitableStore } from '@/store/useSuitableStore'
export default function SuitableButton() {
const popupController = usePopupController()
const { getSuitableIds, clearSuitableStore, downloadSuitablePdf } = useSuitable()
const { getSuitableIds, clearSuitableStore, downloadSuitablePdf, suitableErrorAlert } = useSuitable()
const { selectedItems, addAllSelectedItem } = useSuitableStore()
/* 데이터 전체 선택 */
const handleSelectAll = async () => {
addAllSelectedItem(await getSuitableIds())
try {
addAllSelectedItem(await getSuitableIds())
} catch (error) {
suitableErrorAlert()
}
}
/* 상세 팝업 열기 */

View File

@ -19,7 +19,9 @@ export default function SuitableList() {
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
suitableCheckIcon,
suitableErrorAlert,
} = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
@ -144,6 +146,11 @@ export default function SuitableList() {
useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage)
}, [isLoading, isFetchingNextPage])
/* 조회 데이터 오류 처리 */
useEffect(() => {
if (isError) suitableErrorAlert()
}, [isError])
/* 조회 데이터 없는 경우 */
if (!suitableList.length) return <SuitableNoData />

View File

@ -90,10 +90,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 저장 로직 */
const handleSave = (isTemporary: boolean, isSubmitProcess: boolean) => {
const emptyField = validateSurveyDetail(data.roof)
const hasEmptyField = emptyField?.trim() !== ''
if (isTemporary) {
hasEmptyField ? tempSaveProcess() : saveProcess(emptyField, false)
tempSaveProcess()
} else {
saveProcess(emptyField, isSubmitProcess)
}

View File

@ -94,6 +94,14 @@ export default function DetailForm() {
}))
const [roofInfoData, setRoofInfoData] = useState<SurveyDetailRequest>(roofInfoForm)
/** route 에러 처리 - 잘못된 URL 접근 시 생성 페이지로 리다이렉트 */
useEffect(() => {
if (modeset === 'CREATE' && pathname !== '/survey-sale/regist') {
router.replace('/survey-sale/regist')
}
return
}, [modeset, id, pathname])
/** 제출 팝업 처리 - createSurvey 이후 popup 처리 시 노드 삽입 오류로 인해 별도 처리 */
useEffect(() => {
const show = searchParams.get('show')

View File

@ -9,7 +9,7 @@ export function useCommCode() {
return response.data
} catch (error) {
console.error(`common code (${headCode}) load failed:`, error)
return []
throw error
}
}

View File

@ -48,7 +48,9 @@ export function useInquiry(
*/
const errorRouter = (error: any) => {
const status = error.response?.status
alert(error.response?.data.error)
if (error.response?.data.error) {
alert(error.response?.data.error)
}
switch (status) {
// session 없는 경우
case 401:

View File

@ -51,7 +51,7 @@ export function useSuitable() {
return response.data
} catch (error) {
console.error(`지붕재 적합성 데이터 조회 실패: ${error}`)
return []
throw error
}
}
@ -69,7 +69,7 @@ export function useSuitable() {
return response.data
} catch (error) {
console.error(`지붕재 적합성 데이터 아이디 조회 실패: ${error}`)
return []
throw error
}
}
@ -89,7 +89,7 @@ export function useSuitable() {
return response.data
} catch (error) {
console.error(`지붕재 적합성 상세 데이터 조회 실패: ${error}`)
return []
throw error
}
}
@ -101,9 +101,13 @@ export function useSuitable() {
const getSuitableCommCode = (): void => {
const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[]
for (const code of headCodes) {
getCommCode(code).then((res) => {
setSuitableCommCode(code, res)
})
getCommCode(code)
.then((res) => {
setSuitableCommCode(code, res)
})
.catch(() => {
suitableErrorAlert()
})
}
}
@ -169,8 +173,8 @@ export function useSuitable() {
hasNextPage,
isFetchingNextPage,
isLoading,
// isError,
// error,
isError,
error,
} = useInfiniteQuery<Suitable[]>({
queryKey: ['suitables', 'list', searchCategory, searchKeyword],
queryFn: async (context) => {
@ -189,6 +193,8 @@ export function useSuitable() {
staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10,
enabled: searchCategory !== '' || searchKeyword !== '',
retry: 1,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
/**
@ -263,8 +269,12 @@ export function useSuitable() {
* @returns {Promise<Suitable[]>}
*/
const getSelectedSuitables = async (): Promise<Suitable[]> => {
const { ids, detailIds } = serializeSelectedItems()
return await getSuitableDetails(ids, detailIds)
try {
const { ids, detailIds } = serializeSelectedItems()
return await getSuitableDetails(ids, detailIds)
} catch (error) {
throw error
}
}
/**
@ -308,11 +318,20 @@ export function useSuitable() {
document.body.removeChild(form)
} catch (error) {
console.error(`지붕재 적합성 상세 데이터 pdf 다운로드 실패: ${error}`)
suitableErrorAlert()
}
}
/**
* @description
*
* @returns {void}
*/
const suitableErrorAlert = (): void => {
alert('一時的なエラーが発生しました。 継続的な場合は、管理者に連絡してください。')
}
return {
getSuitables,
getSuitableIds,
getSuitableCommCode,
toCodeName,
@ -323,10 +342,13 @@ export function useSuitable() {
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
error,
getSelectedSuitables,
clearSuitableStore,
suitableCheckIcon,
suitableCheckMemo,
downloadSuitablePdf,
suitableErrorAlert,
}
}

View File

@ -108,7 +108,9 @@ export function useSurvey(
*/
const errorRouter = (error: any) => {
const status = error.response?.status
alert(error.response?.data.error)
if (error.response?.data.error) {
alert(error.response?.data.error)
}
switch (status) {
/** session 없는 경우 */
case 401:
@ -259,6 +261,9 @@ export function useSurvey(
queryClient.invalidateQueries({ queryKey: ['survey', id] })
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
onError: (error: any) => {
alert(error.response?.data.error)
},
})
/**
@ -269,7 +274,7 @@ export function useSurvey(
*
* @example
*
*
*
*/
const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({
mutationFn: async () => {
@ -370,8 +375,9 @@ export function useSurvey(
`https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`,
)
return data.results
} catch (e) {
console.error('Failed to fetch zipcode data:', e)
} catch (error: any) {
console.error('Failed to fetch zipcode data:', error)
alert(error.response?.data.error)
throw new Error('Failed to fetch zipcode data')
}
}

View File

@ -1,8 +1,8 @@
export const useTitle = () => {
const getTitle = (pathname: string) => {
// Handle dynamic routes first
if (pathname.startsWith('/survey-sale/') && pathname !== '/survey-sale/basic-info' && pathname !== '/survey-sale/roof-info') {
return '調査物件一覧'
if (pathname.startsWith('/survey-sale/') && pathname !== '/survey-sale/regist') {
return '調査物件詳細'
}
if (pathname.startsWith('/inquiry/') && pathname !== '/inquiry/list' && pathname !== '/inquiry/regist') {
@ -17,10 +17,8 @@ export const useTitle = () => {
return '屋根材適合性の確認'
case '/survey-sale':
return '調査物件一覧'
case '/survey-sale/basic-info':
case '/survey-sale/regist':
return '調査物件登録'
case '/survey-sale/roof-info':
return '調査物件新規登録'
case '/inquiry/list':
return '1:1お問い合わせ'
case '/inquiry/regist':

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')
}