From d690c3e7746cf1d46c37e111852a1c0991a9be7e Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 18 Jun 2025 15:12:45 +0900 Subject: [PATCH] refactor: improve error handling in API routes and services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 에러 시 반환 타입 통일 - Prisma 에러 검증 추가 --- src/app/api/submission/route.ts | 8 +- src/app/api/submission/service.ts | 48 +++++-- src/app/api/survey-sales/[id]/route.ts | 41 +++--- src/app/api/survey-sales/route.ts | 28 +++-- src/app/api/survey-sales/service.ts | 117 ++++++++++++++---- src/components/pdf/SurveySaleDownloadPdf.tsx | 13 +- .../survey-sale/detail/ButtonForm.tsx | 24 ++-- .../survey-sale/detail/RoofForm.tsx | 6 +- src/hooks/useSurvey.ts | 20 +-- src/types/Survey.ts | 5 +- src/utils/common-utils.js | 23 ++-- 11 files changed, 221 insertions(+), 112 deletions(-) diff --git a/src/app/api/submission/route.ts b/src/app/api/submission/route.ts index 8e1acd1..00b06aa 100644 --- a/src/app/api/submission/route.ts +++ b/src/app/api/submission/route.ts @@ -3,6 +3,7 @@ import { SubmissionService } from './service' import { HttpStatusCode } from 'axios' import { ERROR_MESSAGES } from '@/utils/common-utils' import { loggerWrapper } from '@/libs/api-wrapper' +import { ApiError } from 'next/dist/server/api-utils' /** * @api {GET} /api/submission 제출 대상 조회 @@ -50,8 +51,11 @@ async function getSubmitTargetData(request: NextRequest): Promise } const submissionService = new SubmissionService(storeId, role) - const data = await submissionService.tryFunction(() => submissionService.getSubmissionTarget()) - return NextResponse.json(data) + const result = await submissionService.tryFunction(() => submissionService.getSubmissionTarget()) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) + } + return NextResponse.json(result) } export const GET = loggerWrapper(getSubmitTargetData) diff --git a/src/app/api/submission/service.ts b/src/app/api/submission/service.ts index 12ccbb8..431436c 100644 --- a/src/app/api/submission/service.ts +++ b/src/app/api/submission/service.ts @@ -2,7 +2,8 @@ import { prisma } from '@/libs/prisma' import { ERROR_MESSAGES } from '@/utils/common-utils' import { SubmitTargetResponse } from '@/types/Survey' import { HttpStatusCode } from 'axios' -import { NextResponse } from 'next/server' +import { ApiError } from 'next/dist/server/api-utils' +import { Prisma } from '@prisma/client' export class SubmissionService { private storeId: string @@ -18,18 +19,26 @@ export class SubmissionService { this.role = role } - async getSubmissionTarget(): Promise { + /** + * @description 제출 대상 조회 + * @returns {Promise} 제출 대상 데이터 + */ + async getSubmissionTarget(): Promise { switch (this.role) { case 'Admin_Sub': return this.getSubmissionTargetAdminSub() case 'Builder': return this.getSubmissionTargetBuilder() default: - return null + return new ApiError(HttpStatusCode.BadRequest, ERROR_MESSAGES.BAD_REQUEST) } } - private async getSubmissionTargetAdminSub(): Promise { + /** + * @description 2차점의 매핑 된 제출 대상 판매점 조회 (Admin_Sub - Musubi) + * @returns {Promise} 제출 대상 데이터 + */ + private async getSubmissionTargetAdminSub(): Promise { const query = ` SELECT MCS.STORE_ID AS targetStoreId @@ -51,7 +60,11 @@ export class SubmissionService { return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId) } - private async getSubmissionTargetBuilder(): Promise { + /** + * @description 2차점 시공권한 user의 매핑 된 제출 대상 판매점 조회 (Builder - Musubi) + * @returns {Promise} 제출 대상 데이터 + */ + private async getSubmissionTargetBuilder(): Promise { const query = ` SELECT MCAS.AGENCY_STORE_ID AS targetStoreId @@ -74,12 +87,31 @@ export class SubmissionService { return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId) } - handleRouteError(error: unknown): NextResponse { + /** + * @description API ROUTE 에러 처리 + * @param error 에러 객체 + * @returns {ApiError} 에러 객체 + */ + handleRouteError(error: any): ApiError { console.error('❌ API ROUTE ERROR : ', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + if ( + error instanceof Prisma.PrismaClientInitializationError || + error instanceof Prisma.PrismaClientUnknownRequestError || + error instanceof Prisma.PrismaClientRustPanicError || + error instanceof Prisma.PrismaClientValidationError || + error instanceof Prisma.PrismaClientUnknownRequestError + ) { + return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGES.PRISMA_ERROR) + } + return new ApiError(error.statusCode ?? HttpStatusCode.InternalServerError, error.message ?? ERROR_MESSAGES.FETCH_ERROR) } - async tryFunction(func: () => Promise): Promise { + /** + * @description 비동기 함수 try-catch 처리 함수 + * @param func 비동기 함수 + * @returns {Promise} 에러 객체 또는 함수 결과 + */ + async tryFunction(func: () => Promise): Promise { try { return await func() } catch (error) { diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 29e1635..9b33d86 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -4,9 +4,9 @@ import { sessionOptions } from '@/libs/session' import { cookies } from 'next/headers' import type { SessionData } from '@/types/Auth' import { HttpStatusCode } from 'axios' -import { ERROR_MESSAGES } from '@/utils/common-utils' import { loggerWrapper } from '@/libs/api-wrapper' import { SurveySalesService } from '../service' +import { ApiError } from 'next/dist/server/api-utils' /** * @api {GET} /api/survey-sales/:id 조사 매물 조회 API @@ -51,22 +51,12 @@ async function getSurveySaleDetail(request: NextRequest): Promise const { searchParams } = new URL(request.url) const isPdf = searchParams.get('isPdf') === 'true' - const service = new SurveySalesService({}) - const survey = await service.tryFunction(() => service.fetchSurvey(Number(id))) - - if (!survey) { - return NextResponse.json({ error: ERROR_MESSAGES.NOT_FOUND }, { status: HttpStatusCode.NotFound }) + const service = new SurveySalesService({}, session) + const result = await service.tryFunction(() => service.fetchSurvey(Number(id), isPdf)) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) } - - if (isPdf || service.checkRole(survey, session)) { - return NextResponse.json(survey) - } - - if (!session?.isLoggedIn || session?.role === null) { - return NextResponse.json({ error: ERROR_MESSAGES.UNAUTHORIZED }, { status: HttpStatusCode.Unauthorized }) - } - - return NextResponse.json({ error: ERROR_MESSAGES.NO_PERMISSION }, { status: HttpStatusCode.Forbidden }) + return NextResponse.json(result) } /** @@ -107,8 +97,11 @@ async function updateSurveySaleDetail(request: NextRequest): Promise service.updateSurvey(Number(id), body.survey, body.isTemporary, body.storeId, body.role)) - return NextResponse.json(survey) + const result = await service.tryFunction(() => service.updateSurvey(Number(id), body.survey, body.isTemporary, body.storeId, body.role)) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) + } + return NextResponse.json(result) } /** @@ -132,7 +125,10 @@ async function updateSurveySaleDetail(request: NextRequest): Promise { const id = request.nextUrl.pathname.split('/').pop() ?? '' const service = new SurveySalesService({}) - await service.tryFunction(() => service.deleteSurvey(Number(id))) + const result = await service.tryFunction(() => service.deleteSurvey(Number(id))) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) + } return NextResponse.json({ status: HttpStatusCode.Ok }) } @@ -176,8 +172,11 @@ async function submitSurveySaleDetail(request: NextRequest): Promise service.submitSurvey(Number(id), body.targetId, body.targetNm)) - return NextResponse.json({ status: HttpStatusCode.Ok, data: survey }) + const result = await service.tryFunction(() => service.submitSurvey(Number(id), body.targetId, body.targetNm)) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) + } + return NextResponse.json({ status: HttpStatusCode.Ok, data: result }) } export const GET = loggerWrapper(getSurveySaleDetail) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 783d07f..c0acf3e 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -1,9 +1,12 @@ import { NextResponse } from 'next/server' -import { ERROR_MESSAGES } from '@/utils/common-utils' import { loggerWrapper } from '@/libs/api-wrapper' -import { HttpStatusCode } from 'axios' import { SurveySearchParams } from '@/types/Survey' import { SurveySalesService } from './service' +import { ApiError } from 'next/dist/server/api-utils' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' +import { cookies } from 'next/headers' +import { SessionData } from '@/types/Auth' /** * @api {GET} /api/survey-sales 설문 목록 조회 API @@ -48,7 +51,9 @@ import { SurveySalesService } from './service' * */ async function getSurveySales(request: Request) { - /** URL 파라미터 파싱 */ + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + const { searchParams } = new URL(request.url) const params: SurveySearchParams = { keyword: searchParams.get('keyword'), @@ -56,19 +61,13 @@ async function getSurveySales(request: Request) { isMySurvey: searchParams.get('isMySurvey'), sort: searchParams.get('sort'), offset: searchParams.get('offset'), - role: searchParams.get('role'), - storeId: searchParams.get('storeId'), - builderId: searchParams.get('builderId'), } - const surveySalesService = new SurveySalesService(params) + const surveySalesService = new SurveySalesService(params, session) - /** 세션 체크 결과 처리 */ - const sessionCheckResult = surveySalesService.checkSession() - if (sessionCheckResult) { - return sessionCheckResult + const result = await surveySalesService.tryFunction(() => surveySalesService.getSurveySales()) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) } - const where = surveySalesService.createFilterSurvey() - const result = await surveySalesService.tryFunction(() => surveySalesService.getSurveySales(where)) return NextResponse.json({ data: result }) } @@ -112,6 +111,9 @@ async function createSurveySales(request: Request) { const body = await request.json() const surveySalesService = new SurveySalesService({}) const result = await surveySalesService.tryFunction(() => surveySalesService.createSurvey(body.survey, body.role, body.storeId)) + if (result instanceof ApiError) { + return NextResponse.json({ error: result.message }, { status: result.statusCode }) + } return NextResponse.json(result) } diff --git a/src/app/api/survey-sales/service.ts b/src/app/api/survey-sales/service.ts index 0e60556..8b75fb0 100644 --- a/src/app/api/survey-sales/service.ts +++ b/src/app/api/survey-sales/service.ts @@ -1,10 +1,10 @@ import { prisma } from '@/libs/prisma' import { SurveyBasicInfo, SurveyRegistRequest, SurveySearchParams } from '@/types/Survey' import { convertToSnakeCase, ERROR_MESSAGES } from '@/utils/common-utils' -import { NextResponse } from 'next/server' import { Prisma } from '@prisma/client' import type { SessionData } from '@/types/Auth' import { HttpStatusCode } from 'axios' +import { ApiError } from 'next/dist/server/api-utils' type WhereCondition = { AND: any[] @@ -36,13 +36,15 @@ const ITEMS_PER_PAGE = 10 */ export class SurveySalesService { private params!: SurveySearchParams + private session?: SessionData /** * @description 생성자 * @param {SurveySearchParams} params 검색 파라미터 */ - constructor(params: SurveySearchParams) { + constructor(params: SurveySearchParams, session?: SessionData) { this.params = params + this.session = session } /** @@ -50,17 +52,17 @@ export class SurveySalesService { * @param {SearchParams} params 검색 파라미터 * @returns {NextResponse} 세션 체크 결과 */ - checkSession() { - if (this.params.role === null) { - return NextResponse.json({ data: [], count: 0 }) + checkSession(): ApiError | null { + if (!this.session?.isLoggedIn || this.session?.role === null) { + return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGES.UNAUTHORIZED) } - if (this.params.role === 'Builder' || this.params.role === 'Partner') { + if (this.session?.role === 'Builder' || this.session?.role === 'Partner') { if (this.params.builderId === null) { - return NextResponse.json({ data: [], count: 0 }) + return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGES.NO_PERMISSION) } } else { - if (this.params.storeId === null) { - return NextResponse.json({ data: [], count: 0 }) + if (this.session?.storeId === null) { + return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGES.NO_PERMISSION) } } return null @@ -107,19 +109,21 @@ export class SurveySalesService { private createRoleCondition(): WhereCondition { const where: WhereCondition = { AND: [] } - switch (this.params.role) { + switch (this.session?.role) { case 'Admin': where.OR = [ - { AND: [{ STORE_ID: { equals: this.params.storeId } }] }, - { AND: [{ SUBMISSION_TARGET_ID: { equals: this.params.storeId } }, { SUBMISSION_STATUS: { equals: true } }] }, + { AND: [{ STORE_ID: { equals: this.session?.storeId } }] }, + { AND: [{ SUBMISSION_TARGET_ID: { equals: this.session?.storeId } }, { SUBMISSION_STATUS: { equals: true } }] }, ] break case 'Admin_Sub': where.OR = [ - { AND: [{ STORE_ID: { equals: this.params.storeId } }, { CONSTRUCTION_POINT_ID: { equals: this.params.builderId } }] }, + { + AND: [{ STORE_ID: { equals: this.session?.storeId } }, { CONSTRUCTION_POINT_ID: { equals: this.session?.builderId } }], + }, { AND: [ - { SUBMISSION_TARGET_ID: { equals: this.params.storeId } }, + { SUBMISSION_TARGET_ID: { equals: this.session?.storeId } }, { CONSTRUCTION_POINT_ID: { not: null } }, { CONSTRUCTION_POINT_ID: { not: '' } }, { SUBMISSION_STATUS: { equals: true } }, @@ -129,10 +133,10 @@ export class SurveySalesService { break case 'Builder': case 'Partner': - where.AND.push({ CONSTRUCTION_POINT_ID: { equals: this.params.builderId } }) + where.AND.push({ CONSTRUCTION_POINT_ID: { equals: this.session?.builderId } }) break case 'T01': - where.OR = [{ NOT: { SRL_NO: { startsWith: '一時保存' } } }, { STORE_ID: { equals: this.params.storeId } }] + where.OR = [{ NOT: { SRL_NO: { startsWith: '一時保存' } } }, { STORE_ID: { equals: this.session?.storeId } }] break } return where @@ -162,7 +166,6 @@ export class SurveySalesService { if (Object.keys(roleCondition).length > 0) { where.AND.push(roleCondition) } - return where } @@ -171,7 +174,13 @@ export class SurveySalesService { * @param {WhereCondition} where 조사 매물 검색 조건 * @returns {Promise<{ data: SurveyBasicInfo[], count: number }>} 조사 매물 데이터 */ - async getSurveySales(where: WhereCondition) { + async getSurveySales(): Promise<{ data: SurveyBasicInfo[]; count: number } | ApiError> { + const sessionCheckResult = this.checkSession() + if (sessionCheckResult) { + return sessionCheckResult + } + + const where = this.createFilterSurvey() /** 조사 매물 조회 */ //@ts-ignore const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ @@ -245,12 +254,20 @@ export class SurveySalesService { * @param {number} id 조사 매물 ID * @returns {Promise} 조사 매물 데이터 */ - async fetchSurvey(id: number) { + async fetchSurvey(id: number, isPdf: boolean): Promise { // @ts-ignore - return (await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ where: { ID: id }, include: { DETAIL_INFO: true }, - })) as SurveyBasicInfo + }) + if (!result) { + return new ApiError(HttpStatusCode.NotFound, ERROR_MESSAGES.NOT_FOUND) + } + if (!isPdf) { + if (!this.session?.isLoggedIn) return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGES.UNAUTHORIZED) + if (!this.checkRole(result, this.session as SessionData)) return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGES.NO_PERMISSION) + } + return result } /** @@ -339,7 +356,7 @@ export class SurveySalesService { * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) */ checkRole(survey: any, session: SessionData): boolean { - if (!survey || !session.isLoggedIn) return false + if (!survey || !session) return false const roleChecks = { T01: () => this.checkT01Role(survey), @@ -352,15 +369,38 @@ export class SurveySalesService { return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false } + /** + * @description T01 권한 체크 + * - 임시저장 매물을 제외한 전 매물 조회 가능 + * + * @param {any} survey 조사 매물 데이터 + * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) + */ private checkT01Role(survey: any): boolean { return survey.SRL_NO !== '一時保存' } + /** + * @description Admin 권한 체크 (1차점 - Order) + * - 같은 판매점에서 작성한 매물, 제출 받은 매물 조회 가능 + * + * @param {any} survey 조사 매물 데이터 + * @param {string | null} storeId 판매점 ID + * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) + */ private checkAdminRole(survey: any, storeId: string | null): boolean { if (!storeId) return false return survey.SUBMISSION_STATUS ? survey.SUBMISSION_TARGET_ID === storeId || survey.STORE_ID === storeId : survey.STORE_ID === storeId } + /** + * @description Admin_Sub 권한 체크 (2차점 - Musubi) + * - 같은 판매점에서 작성한 매물, 시공권한 user에게 제출받은 매물 조회 가능 + * + * @param {any} survey 조사 매물 데이터 + * @param {string | null} storeId 판매점 ID + * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) + */ private checkAdminSubRole(survey: any, storeId: string | null): boolean { if (!storeId) return false return survey.SUBMISSION_STATUS @@ -368,17 +408,44 @@ export class SurveySalesService { : survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID } + /** + * @description Partner 또는 Builder 권한 체크 + * - 같은 시공점에서 작성한 매물 조회 가능 + * + * @param {any} survey 조사 매물 데이터 + * @param {string | null} builderId 시공점 ID + * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) + */ private checkPartnerOrBuilderRole(survey: any, builderId: string | null): boolean { if (!builderId) return false return survey.CONSTRUCTION_POINT_ID === builderId } - handleRouteError(error: unknown): NextResponse { + /** + * @description API ROUTE 에러 처리 + * @param {any} error 에러 객체 + * @returns {ApiError} 에러 객체 + */ + handleRouteError(error: any): ApiError { console.error('❌ API ROUTE ERROR : ', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + if ( + error instanceof Prisma.PrismaClientInitializationError || + error instanceof Prisma.PrismaClientUnknownRequestError || + error instanceof Prisma.PrismaClientRustPanicError || + error instanceof Prisma.PrismaClientValidationError || + error instanceof Prisma.PrismaClientUnknownRequestError + ) { + return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGES.PRISMA_ERROR) + } + return new ApiError(error.statusCode ?? HttpStatusCode.InternalServerError, error.message ?? ERROR_MESSAGES.FETCH_ERROR) } - async tryFunction(func: () => Promise): Promise { + /** + * @description 비동기 함수 try-catch 처리 함수 + * @param {() => Promise} func 비동기 함수 + * @returns {Promise} 에러 객체 또는 함수 결과 + */ + async tryFunction(func: () => Promise): Promise { try { return await func() } catch (error) { diff --git a/src/components/pdf/SurveySaleDownloadPdf.tsx b/src/components/pdf/SurveySaleDownloadPdf.tsx index 0030612..0999906 100644 --- a/src/components/pdf/SurveySaleDownloadPdf.tsx +++ b/src/components/pdf/SurveySaleDownloadPdf.tsx @@ -7,13 +7,14 @@ import { useSurvey } from '@/hooks/useSurvey' import { radioEtcData, roofMaterial, selectBoxOptions, supplementaryFacilities } from '../survey-sale/detail/RoofForm' import { useSpinnerStore } from '@/store/spinnerStore' import { useSessionStore } from '@/store/session' +import { SURVEY_ALERT_MSG } from '@/types/Survey' export default function SurveySaleDownloadPdf() { const params = useParams() const id = params.id const router = useRouter() - const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id), true) + const { surveyDetail, isLoadingSurveyDetail, showSurveyAlert } = useSurvey(Number(id), true) const { setIsShow } = useSpinnerStore() const { session } = useSessionStore() @@ -23,11 +24,6 @@ export default function SurveySaleDownloadPdf() { /** 페이지 랜더링 이후 PDF 생성 */ useEffect(() => { if (isLoadingSurveyDetail || isGeneratedRef.current) return - if (surveyDetail === null) { - alert('データが見つかりません。') - router.replace('/') - return - } isGeneratedRef.current = true handleDownPdf() }, [surveyDetail?.id, isLoadingSurveyDetail]) @@ -65,10 +61,11 @@ export default function SurveySaleDownloadPdf() { } else { router.replace('/') } - alert('PDFの生成が完了しました。 ポップアップウィンドウからダウンロードしてください。') + showSurveyAlert('PDFの生成が完了しました。 ポップアップウィンドウからダウンロードしてください。') }) .catch((error: any) => { - console.error('error', error) + console.error('❌ PDF GENERATION ERROR', error) + showSurveyAlert(SURVEY_ALERT_MSG.PDF_GENERATION_ERROR) }) } diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 5ca80a5..c731d98 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { requiredFields, useSurvey } from '@/hooks/useSurvey' import { usePopupController } from '@/store/popupController' -import { ALERT_MESSAGES } from '@/types/Survey' +import { SURVEY_ALERT_MSG } from '@/types/Survey' interface ButtonFormProps { mode: Mode @@ -117,7 +117,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { router.push(`/survey-sale/${savedId}`) } } - showSurveyAlert(ALERT_MESSAGES.TEMP_SAVE_SUCCESS) + showSurveyAlert(SURVEY_ALERT_MSG.TEMP_SAVE_SUCCESS) } /** 입력 필드 포커스 처리 */ @@ -130,7 +130,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => { if (emptyField?.trim() === '') { if (!isSubmitProcess) { - showSurveyConfirm(ALERT_MESSAGES.SAVE_CONFIRM, async () => { + showSurveyConfirm(SURVEY_ALERT_MSG.SAVE_CONFIRM, async () => { await handleSuccessfulSave(isSubmitProcess) }) } else { @@ -155,7 +155,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { if (isSubmitProcess) { popupController.setSurveySaleSubmitPopup(true) } else { - showSurveyAlert(ALERT_MESSAGES.SAVE_SUCCESS) + showSurveyAlert(SURVEY_ALERT_MSG.SAVE_SUCCESS) } } } else { @@ -165,7 +165,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { await router.push(`/survey-sale/${savedId}?show=true`) } else { await router.push(`/survey-sale/${savedId}`) - showSurveyAlert(ALERT_MESSAGES.SAVE_SUCCESS) + showSurveyAlert(SURVEY_ALERT_MSG.SAVE_SUCCESS) } } } @@ -173,10 +173,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { /** 필수값 미입력 처리 */ const handleFailedSave = (emptyField: string | null) => { if (emptyField?.includes('Unit')) { - showSurveyAlert(ALERT_MESSAGES.UNIT_REQUIRED) + showSurveyAlert(SURVEY_ALERT_MSG.UNIT_REQUIRED) } else { const fieldInfo = requiredFields.find((field) => field.field === emptyField) - showSurveyAlert(ALERT_MESSAGES.REQUIRED_FIELD, fieldInfo?.name || '') + showSurveyAlert(SURVEY_ALERT_MSG.REQUIRED_FIELD, fieldInfo?.name || '') } focusInput(emptyField as keyof SurveyDetailInfo) } @@ -184,10 +184,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { /** 삭제 로직 */ const handleDelete = async () => { if (!Number.isNaN(id)) { - showSurveyConfirm(ALERT_MESSAGES.DELETE_CONFIRM, async () => { + showSurveyConfirm(SURVEY_ALERT_MSG.DELETE_CONFIRM, async () => { await deleteSurvey() if (!isDeletingSurvey) { - showSurveyAlert(ALERT_MESSAGES.DELETE_SUCCESS) + showSurveyAlert(SURVEY_ALERT_MSG.DELETE_SUCCESS) router.push('/survey-sale') } }) @@ -197,16 +197,16 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { /** 제출 로직 */ const handleSubmit = async () => { if (data.basic.srlNo?.startsWith('一時保存') && Number.isNaN(id)) { - showSurveyAlert(ALERT_MESSAGES.TEMP_SAVE_SUBMIT_ERROR) + showSurveyAlert(SURVEY_ALERT_MSG.TEMP_SAVE_SUBMIT_ERROR) return } if (mode === 'READ') { - showSurveyConfirm(ALERT_MESSAGES.SUBMIT_CONFIRM, async () => { + showSurveyConfirm(SURVEY_ALERT_MSG.SUBMIT_CONFIRM, async () => { popupController.setSurveySaleSubmitPopup(true) }) } else { - showSurveyConfirm(ALERT_MESSAGES.SAVE_AND_SUBMIT_CONFIRM, async () => { + showSurveyConfirm(SURVEY_ALERT_MSG.SAVE_AND_SUBMIT_CONFIRM, async () => { handleSave(false, true) }) } diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 36527ae..35d7828 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' import { useSurvey } from '@/hooks/useSurvey' -import { ALERT_MESSAGES } from '@/types/Survey' +import { SURVEY_ALERT_MSG } from '@/types/Survey' type RadioEtcKeys = | 'structureOrder' @@ -755,7 +755,7 @@ const MultiCheck = ({ if (isRoofMaterial) { const totalSelected = selectedValues.length + (isOtherSelected || isOtherCheck ? 1 : 0) if (totalSelected >= 2) { - showSurveyAlert(ALERT_MESSAGES.ROOF_MATERIAL_MAX_SELECT_ERROR) + showSurveyAlert(SURVEY_ALERT_MSG.ROOF_MATERIAL_MAX_SELECT_ERROR) return } } @@ -769,7 +769,7 @@ const MultiCheck = ({ if (isRoofMaterial) { const currentSelected = selectedValues.length if (!isOtherCheck && currentSelected >= 2) { - showSurveyAlert(ALERT_MESSAGES.ROOF_MATERIAL_MAX_SELECT_ERROR) + showSurveyAlert(SURVEY_ALERT_MSG.ROOF_MATERIAL_MAX_SELECT_ERROR) return } } diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 345d4a4..98821a1 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,4 +1,4 @@ -import { ALERT_MESSAGES, type SubmitTargetResponse, type SurveyBasicInfo, type SurveyDetailRequest, type SurveyRegistRequest } from '@/types/Survey' +import { SURVEY_ALERT_MSG, type SubmitTargetResponse, type SurveyBasicInfo, type SurveyDetailRequest, type SurveyRegistRequest } from '@/types/Survey' import { useMemo } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useSurveyFilterStore } from '@/store/surveyFilterStore' @@ -7,6 +7,8 @@ import { useAxios } from './useAxios' import { queryStringFormatter } from '@/utils/common-utils' import { useRouter } from 'next/navigation' + + export const requiredFields = [ { field: 'installationSystem', @@ -93,7 +95,7 @@ export function useSurvey( refetchSurveyList: () => void refetchSurveyDetail: () => void getSubmitTarget: (params: { storeId: string; role: string }) => Promise - showSurveyAlert: (message: (typeof ALERT_MESSAGES)[keyof typeof ALERT_MESSAGES] | string, requiredField?: string) => void + showSurveyAlert: (message: (typeof SURVEY_ALERT_MSG)[keyof typeof SURVEY_ALERT_MSG] | string, requiredField?: string) => void showSurveyConfirm: (message: string, onConfirm: () => void, onCancel?: () => void) => void } { const queryClient = useQueryClient() @@ -106,7 +108,8 @@ export function useSurvey( * @description 조사 매물 목록, 상세 데이터 조회 에러 처리 * * @param {any} error 에러 객체 - * @returns {void} 라우팅 처리 + * @param {boolean} isThrow 에러 Throw 처리 여부 + * @returns {void} 라우팅 처리 / 에러 Throw 처리 */ const handleError = (error: any, isThrow?: boolean) => { const status = error.response?.status @@ -175,7 +178,7 @@ export function useSurvey( isLoading: isLoadingSurveyList, refetch: refetchSurveyList, } = useQuery({ - queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role], + queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset], queryFn: async () => { return await tryFunction( () => @@ -186,15 +189,13 @@ export function useSurvey( isMySurvey, sort, offset, - storeId: session?.storeId, - builderId: session?.builderId, - role: session?.role, }, }), true, false, ) }, + enabled: !isPdf, }) /** @@ -436,7 +437,10 @@ export function useSurvey( * @param {string} message 알림 메시지 * @param {string} [requiredField] 필수 필드 이름 */ - const showSurveyAlert = (message: (typeof ALERT_MESSAGES)[keyof typeof ALERT_MESSAGES] | string, requiredField?: (typeof requiredFields)[number]['field']) => { + const showSurveyAlert = ( + message: (typeof SURVEY_ALERT_MSG)[keyof typeof SURVEY_ALERT_MSG] | string, + requiredField?: (typeof requiredFields)[number]['field'], + ) => { if (requiredField) { alert(`${requiredField} ${message}`) } else { diff --git a/src/types/Survey.ts b/src/types/Survey.ts index c2d80b6..3802916 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -325,7 +325,7 @@ export type SurveySearchParams = { } -export const ALERT_MESSAGES = { +export const SURVEY_ALERT_MSG = { /** 기본 메세지 */ /** 저장 성공 - "저장되었습니다." */ SAVE_SUCCESS: '保存されました。', @@ -351,4 +351,7 @@ export const ALERT_MESSAGES = { REQUIRED_FIELD: '項目が空です。', /** 최대 선택 오류 메세지 - "지붕재는 최대 2개까지 선택할 수 있습니다." */ ROOF_MATERIAL_MAX_SELECT_ERROR: '屋根材は最大2個まで選択できます。', + + /** PDF 생성 오류 - "PDF 생성에 실패했습니다." */ + PDF_GENERATION_ERROR: 'PDF 生成に失敗しました。', } \ No newline at end of file diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 51be0e6..e4eb144 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -155,7 +155,7 @@ export const unescapeString = (str) => { */ while (regex.test(str)) { - str = str.replace(regex, (matched) => chars[matched] || matched); + str = str.replace(regex, (matched) => chars[matched] || matched) } return str } @@ -186,29 +186,28 @@ function isObject(value) { return value !== null && typeof value === 'object' } - // 카멜케이스를 스네이크케이스로 변환하는 함수 export const toSnakeCase = (str) => { - return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) } // 객체의 키를 스네이크케이스로 변환하는 함수 export const convertToSnakeCase = (obj) => { - if (obj === null || obj === undefined) return obj; - + if (obj === null || obj === undefined) return obj + if (Array.isArray(obj)) { return obj.map((item) => convertToSnakeCase(item)) } if (typeof obj === 'object') { return Object.keys(obj).reduce((acc, key) => { - const snakeKey = toSnakeCase(key).toUpperCase(); - acc[snakeKey] = convertToSnakeCase(obj[key]); - return acc; - }, {}); + const snakeKey = toSnakeCase(key).toUpperCase() + acc[snakeKey] = convertToSnakeCase(obj[key]) + return acc + }, {}) } - - return obj; + + return obj } /** @@ -225,4 +224,6 @@ export const ERROR_MESSAGES = { FETCH_ERROR: 'データの取得に失敗しました。', /** 잘못된 요청입니다. */ BAD_REQUEST: '間違ったリクエストです。', + /** 데이터베이스 오류가 발생했습니다. */ + PRISMA_ERROR: 'データベース エラーが発生しました。', }