From 6c1bd8775c835a3a051cc11c1ec7dc221b0f6c53 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 17 Jun 2025 18:21:02 +0900 Subject: [PATCH] refactor: implement SurveySalesService for survey sales management --- src/app/api/qna/save/route.ts | 1 - src/app/api/submission/route.ts | 36 +++ src/app/api/submission/service.ts | 77 +---- src/app/api/survey-sales/[id]/route.ts | 248 +++------------- src/app/api/survey-sales/route.ts | 334 ++------------------- src/app/api/survey-sales/service.ts | 388 +++++++++++++++++++++++++ src/hooks/useSurvey.ts | 35 ++- src/types/Survey.ts | 22 ++ 8 files changed, 541 insertions(+), 600 deletions(-) create mode 100644 src/app/api/survey-sales/service.ts diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index 429db83..4575479 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -5,7 +5,6 @@ import { ERROR_MESSAGES } from '@/utils/common-utils' async function setQna(request: Request): Promise { const formData = await request.formData() - console.log(formData) try { const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData, { headers: { diff --git a/src/app/api/submission/route.ts b/src/app/api/submission/route.ts index bc4fc4a..e0b62ee 100644 --- a/src/app/api/submission/route.ts +++ b/src/app/api/submission/route.ts @@ -4,6 +4,42 @@ import { HttpStatusCode } from 'axios' import { ERROR_MESSAGES } from '@/utils/common-utils' import { loggerWrapper } from '@/libs/api-wrapper' +/** + * @api {GET} /api/submission 제출 대상 조회 + * @apiName GET /api/submission + * @apiGroup Submission + * @apiDescription 제출 대상 조회 + * + * @param {String} storeId 판매점 ID (required) + * @param {String} role 권한 (required) + * + * @apiSuccess {Object} data 제출 대상 목록 + * @apiSuccess {String} data.targetStoreId 판매점 ID + * @apiSuccess {String} data.salesOfficeCd 영업소 코드 + * @apiSuccess {String} data.fromEmail 발신자 이메일 + * @apiSuccess {String} data.toEmail 수신자 이메일 + * + * @returns {Promise} 제출 대상 목록 + * @apiSuccessExample {json} Success-Response: + * { + * "data": [ + * { + * "targetStoreId": "1234567890", + * "salesOfficeCd": "1234567890", + * "fromEmail": "1234567890", + * "toEmail": "1234567890" + * } + * ] + * } + * + * @apiExample {curl} Example usage: + * curl -X GET \ + * -H "Content-Type: application/json" \ + * http://localhost:3000/api/submission?storeId=1234567890&role=admin + * + * @apiError {Object} error 에러 객체 + * @apiError {String} error.message 에러 메시지 + */ async function getSubmitTargetData(request: NextRequest): Promise { try { const { searchParams } = new URL(request.url) diff --git a/src/app/api/submission/service.ts b/src/app/api/submission/service.ts index 4431553..7f7c86e 100644 --- a/src/app/api/submission/service.ts +++ b/src/app/api/submission/service.ts @@ -4,6 +4,11 @@ import { SubmitTargetResponse } from '@/types/Survey' export class SubmissionService { private storeId: string private role: string + private readonly BASE_QUERY = ` + DECLARE @storeId NVARCHAR(50); + SET @storeId = @p1; + OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; + ` constructor(storeId: string, role: string) { this.storeId = storeId @@ -12,51 +17,18 @@ export class SubmissionService { async getSubmissionTarget(): Promise { switch (this.role) { - case 'Admin': - return this.getSubmissionTargetAdmin() case 'Admin_Sub': return this.getSubmissionTargetAdminSub() case 'Builder': return this.getSubmissionTargetBuilder() - case 'Super': - return this.getSubmissionTargetSuper() default: return null } } - private async getSubmissionTargetAdmin(): Promise { - const query = ` - OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; - SELECT - MCSA.STORE_ID AS targetStoreId - , BCL.CODE AS salesOfficeCd - , REF_CHR1 AS fromEmail - , REF_CHR2 AS toEmail - FROM MS_CUST_STOREID_ADDITNL MCSA WITH(NOLOCK) - LEFT OUTER JOIN IF_PERSON_OFFICE_MAPPING IPOM WITH(NOLOCK) - ON MCSA.KAM_ID = IPOM.LIFNR - AND IF_STS = 'R' - AND VKBUR IS NOT NULL - AND VKBUR != '' - INNER JOIN BC_COMM_L BCL WITH(NOLOCK) - ON BCL.CODE = IPOM.VKBUR - AND BCL.HEAD_CD = '103200' - AND BCL.DEL_YN = 'N' - WHERE MCSA.COMP_CD = '5200' - AND MCSA.STORE_ID = '${this.storeId}' - AND MCSA.DEL_YN = 'N'; - CLOSE SYMMETRIC KEY SYMMETRICKEY; - ` - const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) - return data - } - - private async getSubmissionTargetAdminSub(): Promise { const query = ` - OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; - SELECT + SELECT MCS.STORE_ID AS targetStoreId , MCS.STORE_QCAST_NM AS targetStoreNm , MCP.EOS_LOGIN_ID AS repUserId @@ -68,18 +40,16 @@ export class SubmissionService { AND MCS.STORE_ID = MCP.STORE_ID AND MCP.DEL_YN = 'N' WHERE MCS.COMP_CD = '5200' - AND MCS.STORE_ID = (SELECT STORE_ID FROM MS_CUST_AGENCY_STOREID WHERE AGENCY_STORE_ID = '${this.storeId}' AND DEL_YN = 'N') - AND MCP.EMAIL IS NOT NULL + AND MCS.STORE_ID IN (SELECT STORE_ID FROM MS_CUST_AGENCY_STOREID WHERE AGENCY_STORE_ID = @storeId AND DEL_YN = 'N') + AND MCP.EMAIL IS NOT NULL AND MCS.DEL_YN = 'N'; CLOSE SYMMETRIC KEY SYMMETRICKEY; - ` - const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) - return data + ` + return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId) } private async getSubmissionTargetBuilder(): Promise { const query = ` - OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; SELECT MCAS.AGENCY_STORE_ID AS targetStoreId , MCAS.AGENCY_QCAST_NM AS targetStoreNm @@ -92,35 +62,12 @@ export class SubmissionService { AND MCAS.AGENCY_STORE_ID = BQU.AGENCY_STORE_ID AND MCAS.DEL_YN = 'N' WHERE MCAS.COMP_CD = '5200' - AND MCAS.AGENCY_STORE_ID = '${this.storeId}' + AND MCAS.AGENCY_STORE_ID = @storeId AND BQU.EMAIL IS NOT NULL AND BQU.USER_AUTH_CD != 'B' AND MCAS.DEL_YN = 'N'; CLOSE SYMMETRIC KEY SYMMETRICKEY; ` - const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) - return data - } - - private async getSubmissionTargetSuper(): Promise { - const query = ` - OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; - SELECT - MCSA.STORE_ID AS targetStoreId - , BU.USER_ID AS repUserId - , CONVERT(NVARCHAR(100), DecryptByKey(BU.E_MAIL)) AS repUserEmail - FROM MS_CUST_STOREID_ADDITNL MCSA WITH(NOLOCK) - LEFT OUTER JOIN BC_USER bu WITH(NOLOCK) - ON MCSA.COMP_CD = BU.COMP_CD - AND MCSA.KAM_ID = BU.KAM_ID - AND BU.STAT_CD = 'A' - AND BU.DEL_YN = 'N' - WHERE MCSA.COMP_CD = '5200' - AND MCSA.STORE_ID = '${this.storeId}' - AND MCSA.DEL_YN = 'N'; - CLOSE SYMMETRIC KEY SYMMETRICKEY; - ` - const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) - return data + return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId) } } diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index a50c146..29e1635 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,88 +1,12 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' -import { convertToSnakeCase, ERROR_MESSAGES } from '@/utils/common-utils' import { getIronSession } from 'iron-session' 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' import { HttpStatusCode } from 'axios' - -/** - * @description T01 조회 권한 체크 - * @param {any} survey 조사 매물 데이터 - * @returns {boolean} 조사 매물 임시 저장 여부 - */ -const checkT01Role = (survey: any): boolean => survey.SRL_NO !== '一時保存' - -/** - * @description Admin (1차 판매점) 권한 체크 - * @param {any} survey 조사 매물 데이터 - * @param {string | null} storeId 판매점 ID - * @returns {boolean} 권한 존재 여부 - */ -const 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차 판매점) 권한 체크 - * @param {any} survey 조사 매물 데이터 - * @param {string | null} storeId 판매점 ID - * @returns {boolean} 권한 존재 여부 - */ -const checkAdminSubRole = (survey: any, storeId: string | null): boolean => { - if (!storeId) return false - return survey.SUBMISSION_STATUS - ? survey.SUBMISSION_TARGET_ID === storeId || (survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID) - : survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID -} - -/** - * @description Partner (파트너) 또는 Builder (2차 판매점의 시공권한 회원) 권한 체크 - * @param {any} survey 조사 매물 데이터 - * @param {string | null} builderId 시공점 ID - * @returns {boolean} 권한 존재 여부 - */ -const checkPartnerOrBuilderRole = (survey: any, builderId: string | null): boolean => { - if (!builderId) return false - return survey.CONSTRUCTION_POINT_ID === builderId -} - -/** - * @description 권한 체크 - * @param {any} survey 조사 매물 데이터 - * @param {any} session 세션 데이터 - * @returns {boolean} 권한 존재 여부 - */ -const checkRole = (survey: any, session: any): boolean => { - if (!survey || !session.isLoggedIn) return false - - const roleChecks = { - T01: () => checkT01Role(survey), - Admin: () => checkAdminRole(survey, session.storeId), - Admin_Sub: () => checkAdminSubRole(survey, session.storeId), - Partner: () => checkPartnerOrBuilderRole(survey, session.builderId), - Builder: () => checkPartnerOrBuilderRole(survey, session.builderId), - } - - return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false -} - -/** - * @description 조사 매물 조회 - * @param {number} id 조사 매물 ID - * @returns {Promise} 조사 매물 데이터 - */ -const fetchSurvey = async (id: number) => { - // @ts-ignore - return await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ - where: { ID: id }, - include: { DETAIL_INFO: true }, - }) -} +import { ERROR_MESSAGES } from '@/utils/common-utils' +import { loggerWrapper } from '@/libs/api-wrapper' +import { SurveySalesService } from '../service' /** * @api {GET} /api/survey-sales/:id 조사 매물 조회 API @@ -121,70 +45,28 @@ const fetchSurvey = async (id: number) => { * } */ async function getSurveySaleDetail(request: NextRequest): Promise { - try { - const cookieStore = await cookies() - const session = await getIronSession(cookieStore, sessionOptions) - const id = request.nextUrl.pathname.split('/').pop() ?? '' - const { searchParams } = new URL(request.url) - const isPdf = searchParams.get('isPdf') === 'true' + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + const id = request.nextUrl.pathname.split('/').pop() ?? '' + const { searchParams } = new URL(request.url) + const isPdf = searchParams.get('isPdf') === 'true' - const survey = await fetchSurvey(Number(id)) - if (!survey) { - return NextResponse.json({ error: ERROR_MESSAGES.NOT_FOUND }, { status: HttpStatusCode.NotFound }) - } + const service = new SurveySalesService({}) + const survey = await service.tryFunction(() => service.fetchSurvey(Number(id))) - /** pdf 데이터 요청 여부, 권한 여부 확인 */ - if (isPdf || 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 }) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + if (!survey) { + return NextResponse.json({ error: ERROR_MESSAGES.NOT_FOUND }, { status: HttpStatusCode.NotFound }) } -} -/** - * @description 새로운 SRL_NO 생성 - * @param {string} srlNo 기존 SRL_NO - * @param {string} storeId 판매점 ID - * @param {string} role 권한 - * @returns {Promise} 새로운 SRL_NO - */ -const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { - const srlRole = role === 'T01' || role === 'Admin' ? 'HO' : role === 'Admin_Sub' || role === 'Builder' ? 'HM' : '' - - let newSrlNo = srlNo - if (srlNo.startsWith('一時保存')) { - //@ts-ignore - const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ - where: { - SRL_NO: { - startsWith: srlRole, - }, - }, - orderBy: { - ID: 'desc', - }, - }) - const lastNo = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 - - newSrlNo = - srlRole + - storeId + - new Date().getFullYear().toString().slice(-2) + - (new Date().getMonth() + 1).toString().padStart(2, '0') + - new Date().getDate().toString().padStart(2, '0') + - (lastNo + 1).toString().padStart(3, '0') + if (isPdf || service.checkRole(survey, session)) { + return NextResponse.json(survey) } - return newSrlNo + + 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 }) } /** @@ -221,33 +103,12 @@ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { * } * */ async function updateSurveySaleDetail(request: NextRequest): Promise { - try { - const id = request.nextUrl.pathname.split('/').pop() ?? '' - const body = await request.json() - const { detailInfo, ...basicInfo } = body.survey + const id = request.nextUrl.pathname.split('/').pop() ?? '' + const body = await request.json() + const service = new SurveySalesService({}) - // PUT 요청 시 임시저장 여부 확인 후 임시저장 시 기존 SRL_NO 사용, 기본 저장 시 새로운 SRL_NO 생성 - const newSrlNo = body.isTemporary ? body.survey.srlNo : await getNewSrlNo(body.survey.srlNo, body.storeId, body.role) - // @ts-ignore - const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - ...convertToSnakeCase(basicInfo), - SRL_NO: newSrlNo, - UPT_DT: new Date(), - DETAIL_INFO: { - update: convertToSnakeCase(detailInfo), - }, - }, - include: { - DETAIL_INFO: true, - }, - }) - return NextResponse.json(survey) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) - } + const survey = await service.tryFunction(() => service.updateSurvey(Number(id), body.survey, body.isTemporary, body.storeId, body.role)) + return NextResponse.json(survey) } /** @@ -269,36 +130,10 @@ async function updateSurveySaleDetail(request: NextRequest): Promise { - try { - const id = request.nextUrl.pathname.split('/').pop() ?? '' - - await prisma.$transaction(async (tx: Prisma.TransactionClient) => { - // @ts-ignore - const detailData = await tx.SD_SURVEY_SALES_BASIC_INFO.findUnique({ - where: { ID: Number(id) }, - select: { - DETAIL_INFO: true, - }, - }) - - if (detailData?.DETAIL_INFO?.ID) { - // @ts-ignore - await tx.SD_SURVEY_SALES_DETAIL_INFO.delete({ - where: { ID: Number(detailData.DETAIL_INFO.ID) }, - }) - } - - // @ts-ignore - await tx.SD_SURVEY_SALES_BASIC_INFO.delete({ - where: { ID: Number(id) }, - }) - }) - - return NextResponse.json({ status: HttpStatusCode.Ok }) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) - } + const id = request.nextUrl.pathname.split('/').pop() ?? '' + const service = new SurveySalesService({}) + await service.tryFunction(() => service.deleteSurvey(Number(id))) + return NextResponse.json({ status: HttpStatusCode.Ok }) } /** @@ -337,25 +172,12 @@ async function deleteSurveySaleDetail(request: NextRequest): Promise { - try { - const id = request.nextUrl.pathname.split('/').pop() ?? '' - const body = await request.json() - // @ts-ignore - const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - SUBMISSION_STATUS: true, - SUBMISSION_DATE: new Date(), - SUBMISSION_TARGET_ID: body.targetId, - SUBMISSION_TARGET_NM: body.targetNm, - UPT_DT: new Date(), - }, - }) - return NextResponse.json({ status: HttpStatusCode.Ok, data: survey }) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) - } + const id = request.nextUrl.pathname.split('/').pop() ?? '' + const body = await request.json() + const service = new SurveySalesService({}) + + const survey = await service.tryFunction(() => service.submitSurvey(Number(id), body.targetId, body.targetNm)) + return NextResponse.json({ status: HttpStatusCode.Ok, data: survey }) } export const GET = loggerWrapper(getSurveySaleDetail) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 0f31da9..783d07f 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -1,156 +1,9 @@ import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' -import { convertToSnakeCase, ERROR_MESSAGES } from '@/utils/common-utils' +import { ERROR_MESSAGES } from '@/utils/common-utils' import { loggerWrapper } from '@/libs/api-wrapper' import { HttpStatusCode } from 'axios' -/** - * @description 검색 파라미터 타입 - */ -type SearchParams = { - keyword?: string | null - searchOption?: string | null - isMySurvey?: string | null - sort?: string | null - offset?: string | null - role?: string | null - storeId?: string | null - builderId?: string | null -} - -type WhereCondition = { - AND: any[] - OR?: any[] - [key: string]: any -} - -/** 검색 가능한 필드 옵션 */ -const SEARCH_OPTIONS = [ - 'BUILDING_NAME', - 'REPRESENTATIVE', - 'STORE', - 'STORE_ID', - 'CONSTRUCTION_POINT', - 'CONSTRUCTION_POINT_ID', - 'CUSTOMER_NAME', - 'POST_CODE', - 'ADDRESS', - 'ADDRESS_DETAIL', - 'SRL_NO', -] as const - -/** 페이지당 기본 항목 수 */ -const ITEMS_PER_PAGE = 10 - -/** - * @description 키워드 검색 조건 생성 함수 - * @param {string} keyword 검색 키워드 - * @param {string} searchOption 검색 옵션 - * @returns {WhereCondition} 검색 조건 객체 - */ -const createKeywordSearchCondition = (keyword: string, searchOption: string): WhereCondition => { - const where: WhereCondition = { AND: [] } - - if (searchOption === 'all') { - /** 모든 필드 검색 시 OR 조건 사용 */ - where.OR = [] - - where.OR.push( - ...SEARCH_OPTIONS.map((field) => ({ - [field]: { contains: keyword }, - })), - ) - } else if (SEARCH_OPTIONS.includes(searchOption.toUpperCase() as any)) { - /** 특정 필드 검색 */ - where[searchOption.toUpperCase()] = { contains: keyword } - } - return where -} - -/** - * @description 회원 역할별 검색 조건 생성 함수 - * @param {SearchParams} params 검색 파라미터 - * @returns {WhereCondition} 검색 조건 객체 - */ -const createMemberRoleCondition = (params: SearchParams): WhereCondition => { - const where: WhereCondition = { AND: [] } - - switch (params.role) { - case 'Admin': - where.OR = [ - { - AND: [{ STORE_ID: { equals: params.storeId } }], - }, - { - AND: [{ SUBMISSION_TARGET_ID: { equals: params.storeId } }, { SUBMISSION_STATUS: { equals: true } }], - }, - ] - break - - case 'Admin_Sub': - where.OR = [ - { - AND: [{ STORE_ID: { equals: params.storeId } }, { CONSTRUCTION_POINT_ID: { equals: params.builderId } }], - }, - { - AND: [ - { SUBMISSION_TARGET_ID: { equals: params.storeId } }, - { CONSTRUCTION_POINT_ID: { not: null } }, - { CONSTRUCTION_POINT_ID: { not: '' } }, - { SUBMISSION_STATUS: { equals: true } }, - ], - }, - ] - break - - case 'Builder': - case 'Partner': - where.AND?.push({ - CONSTRUCTION_POINT_ID: { equals: params.builderId }, - }) - break - - case 'T01': - where.OR = [ - { - NOT: { - SRL_NO: { - startsWith: '一時保存', - }, - }, - }, - { - STORE_ID: { - equals: params.storeId, - }, - }, - ] - break - case 'User': - break - } - - return where -} -/** - * @description 권한 별 필수 값 존재 여부 확인, 없을 시 빈 데이터 반환 - * @param {SearchParams} params 검색 파라미터 - * @returns {NextResponse} 세션 체크 결과 - */ -const checkSession = (params: SearchParams) => { - if (params.role === null) { - return NextResponse.json({ data: [], count: 0 }) - } - if (params.role === 'Builder' || params.role === 'Partner') { - if (params.builderId === null) { - return NextResponse.json({ data: [], count: 0 }) - } - } else { - if (params.storeId === null) { - return NextResponse.json({ data: [], count: 0 }) - } - } - return null -} +import { SurveySearchParams } from '@/types/Survey' +import { SurveySalesService } from './service' /** * @api {GET} /api/survey-sales 설문 목록 조회 API @@ -195,108 +48,28 @@ const checkSession = (params: SearchParams) => { * */ async function getSurveySales(request: Request) { - try { - /** URL 파라미터 파싱 */ - const { searchParams } = new URL(request.url) - const params: SearchParams = { - keyword: searchParams.get('keyword'), - searchOption: searchParams.get('searchOption'), - 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 sessionCheckResult = checkSession(params) - if (sessionCheckResult) { - return sessionCheckResult - } - - /** 검색 조건 구성 */ - const where: WhereCondition = { AND: [] } - - /** 내가 작성한 매물 조건 적용 */ - if (params.isMySurvey) { - where.AND.push({ REPRESENTATIVE_ID: params.isMySurvey }) - } - - /** 키워드 검색 조건 적용 */ - if (params.keyword && params.searchOption) { - where.AND.push(createKeywordSearchCondition(params.keyword, params.searchOption)) - } - - /** 회원 유형 조건 적용 */ - const roleCondition = createMemberRoleCondition(params) - if (Object.keys(roleCondition).length > 0) { - where.AND.push(roleCondition) - } - /** 페이지네이션 데이터 조회 */ - //@ts-ignore - const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ - where, - orderBy: params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, - skip: Number(params.offset), - take: ITEMS_PER_PAGE, - }) - /** 전체 개수만 조회 */ - //@ts-ignore - const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) - return NextResponse.json({ data: { data: surveys, count: count } }) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + /** URL 파라미터 파싱 */ + const { searchParams } = new URL(request.url) + const params: SurveySearchParams = { + keyword: searchParams.get('keyword'), + searchOption: searchParams.get('searchOption'), + 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) -/** - * @api {PUT} /api/survey-sales 설문 상세 정보 추가 API - * @apiName PUT /api/survey-sales - * @apiGroup SurveySales - * @apiDescription 설문 상세 정보 추가 API - * - * @apiParam {Number} id 설문 목록 ID (required) - * @apiBody {Object} detail_info 상세 정보 (required) - * - * @apiSuccess {String} message 성공 메시지 - * - * @apiExample {curl} Example usage: - * curl -X PUT \ - * -H "Content-Type: application/json" \ - * -d '{"id": 1, "detail_info": {"memo": "1234567890"}}' \ - * http://localhost:3000/api/survey-sales - * - * @apiSuccessExample {json} Success-Response: - * { - * "message": "Success Update Survey" - * } - * - * @apiError {Number} 500 서버 오류 - */ -async function updateSurveySales(request: Request) { - try { - /** 요청 바디 파싱 */ - const body = await request.json() - - /** 상세 정보 생성을 위한 데이터 구성 */ - const detailInfo = { - ...body.detail_info, - BASIC_INFO_ID: body.id, - } - - /** 상세 정보 생성 */ - //@ts-ignore - await prisma.SD_SURVEY_SALES_DETAIL_INFO.create({ - data: detailInfo, - }) - - return NextResponse.json({ status: HttpStatusCode.Ok }) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + /** 세션 체크 결과 처리 */ + const sessionCheckResult = surveySalesService.checkSession() + if (sessionCheckResult) { + return sessionCheckResult } + const where = surveySalesService.createFilterSurvey() + const result = await surveySalesService.tryFunction(() => surveySalesService.getSurveySales(where)) + return NextResponse.json({ data: result }) } /** @@ -336,64 +109,11 @@ async function updateSurveySales(request: Request) { * @apiError {Number} 500 서버 오류 */ async function createSurveySales(request: Request) { - try { - const body = await request.json() - - const role = - body.role === 'T01' || body.role === 'Admin' - ? 'HO' - : body.role === 'Admin_Sub' || body.role === 'Builder' - ? 'HM' - : body.role === 'Partner' - ? '' - : null - - /** 임시 저장 시 임시저장으로 저장 */ - /** 기본 저장 시 (HO/HM) + 판매점ID + yyMMdd + 000 으로 저장 */ - const baseSrlNo = - body.survey.srlNo ?? - role + - body.storeId + - new Date().getFullYear().toString().slice(-2) + - (new Date().getMonth() + 1).toString().padStart(2, '0') + - new Date().getDate().toString().padStart(2, '0') - - // @ts-ignore - const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ - where: { - SRL_NO: { - startsWith: role + body.storeId, - }, - }, - orderBy: { - SRL_NO: 'desc', - }, - }) - - /** 마지막 번호 추출 */ - const lastNumber = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 - - /** 새로운 srlNo 생성 - 임시저장일 경우 '임시저장' 으로 저장 */ - const newSrlNo = baseSrlNo.startsWith('一時保存') ? baseSrlNo : baseSrlNo + (lastNumber + 1).toString().padStart(3, '0') - - const { detailInfo, ...basicInfo } = body.survey - // @ts-ignore - const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ - data: { - ...convertToSnakeCase(basicInfo), - SRL_NO: newSrlNo, - DETAIL_INFO: { - create: convertToSnakeCase(detailInfo), - }, - }, - }) - return NextResponse.json(result) - } catch (error) { - console.error('❌ API ROUTE ERROR:', error) - return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) - } + const body = await request.json() + const surveySalesService = new SurveySalesService({}) + const result = await surveySalesService.tryFunction(() => surveySalesService.createSurvey(body.survey, body.role, body.storeId)) + return NextResponse.json(result) } export const GET = loggerWrapper(getSurveySales) -export const PUT = loggerWrapper(updateSurveySales) -export const POST = loggerWrapper(createSurveySales) \ No newline at end of file +export const POST = loggerWrapper(createSurveySales) diff --git a/src/app/api/survey-sales/service.ts b/src/app/api/survey-sales/service.ts new file mode 100644 index 0000000..0e60556 --- /dev/null +++ b/src/app/api/survey-sales/service.ts @@ -0,0 +1,388 @@ +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' + +type WhereCondition = { + AND: any[] + OR?: any[] + [key: string]: any +} + +/** 검색 옵션 */ +const SEARCH_OPTIONS = [ + 'BUILDING_NAME', + 'REPRESENTATIVE', + 'STORE', + 'STORE_ID', + 'CONSTRUCTION_POINT', + 'CONSTRUCTION_POINT_ID', + 'CUSTOMER_NAME', + 'POST_CODE', + 'ADDRESS', + 'ADDRESS_DETAIL', + 'SRL_NO', +] as const + +/** 페이지당 기본 항목 수 */ +const ITEMS_PER_PAGE = 10 + +/** + * @description 조사 매물 서비스 + * @param {SurveySearchParams} params 검색 파라미터 + */ +export class SurveySalesService { + private params!: SurveySearchParams + + /** + * @description 생성자 + * @param {SurveySearchParams} params 검색 파라미터 + */ + constructor(params: SurveySearchParams) { + this.params = params + } + + /** + * @description 권한 별 필수 값 존재 여부 확인, 없을 시 빈 데이터 반환 + * @param {SearchParams} params 검색 파라미터 + * @returns {NextResponse} 세션 체크 결과 + */ + checkSession() { + if (this.params.role === null) { + return NextResponse.json({ data: [], count: 0 }) + } + if (this.params.role === 'Builder' || this.params.role === 'Partner') { + if (this.params.builderId === null) { + return NextResponse.json({ data: [], count: 0 }) + } + } else { + if (this.params.storeId === null) { + return NextResponse.json({ data: [], count: 0 }) + } + } + return null + } + + /** + * @description 내가 작성한 매물 조건 생성 + * @returns {WhereCondition} 내가 작성한 매물 조건 + */ + private createMySurveyCondition(): WhereCondition { + if (!this.params.isMySurvey) return { AND: [] } + return { AND: [{ REPRESENTATIVE_ID: this.params.isMySurvey }] } + } + + /** + * @description 키워드 검색 조건 생성 + * @returns {WhereCondition} 키워드 검색 조건 + */ + private createKeywordCondition(): WhereCondition { + if (!this.params.keyword || !this.params.searchOption) return { AND: [] } + + const where: WhereCondition = { AND: [] } + if (this.params.searchOption === 'all') { + where.OR = SEARCH_OPTIONS.map((field) => ({ + [field]: { contains: this.params.keyword }, + })) + } else if (SEARCH_OPTIONS.includes(this.params.searchOption?.toUpperCase() as (typeof SEARCH_OPTIONS)[number])) { + where[this.params.searchOption?.toUpperCase() as (typeof SEARCH_OPTIONS)[number]] = { contains: this.params.keyword } + } + return where + } + + /** + * @description 역할 기반 조건 생성 + * @returns {WhereCondition} 역할 기반 조건 + * @exampleResult { AND: [{ STORE_ID: { equals: '1234567890' } }] } + * + * @description T01 : 임시저장되지 않은 전체 매물 조회 + * @description Admin : 같은 판매점에서 작성된 매물, 2차점에게 제출받은 매물 조회 + * @description Admin_Sub : 같은 판매점에서 작성된 매물, 시공권한 user에게 제출받은 매물 조회 + * @description Builder : 같은 시공점에서 작성된 매물 조회 + * @description Partner : 같은 시공점에서 작성된 매물 조회 + */ + private createRoleCondition(): WhereCondition { + const where: WhereCondition = { AND: [] } + + switch (this.params.role) { + case 'Admin': + where.OR = [ + { AND: [{ STORE_ID: { equals: this.params.storeId } }] }, + { AND: [{ SUBMISSION_TARGET_ID: { equals: this.params.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: [ + { SUBMISSION_TARGET_ID: { equals: this.params.storeId } }, + { CONSTRUCTION_POINT_ID: { not: null } }, + { CONSTRUCTION_POINT_ID: { not: '' } }, + { SUBMISSION_STATUS: { equals: true } }, + ], + }, + ] + break + case 'Builder': + case 'Partner': + where.AND.push({ CONSTRUCTION_POINT_ID: { equals: this.params.builderId } }) + break + case 'T01': + where.OR = [{ NOT: { SRL_NO: { startsWith: '一時保存' } } }, { STORE_ID: { equals: this.params.storeId } }] + break + } + return where + } + + /** + * @description 조사 매물 검색 조건 생성 + * @returns {WhereCondition} 조사 매물 검색 조건 + */ + createFilterSurvey(): WhereCondition { + const where: WhereCondition = { AND: [] } + + /** 내가 작성한 매물 조건 */ + const mySurveyCondition = this.createMySurveyCondition() + if (mySurveyCondition.AND.length > 0) { + where.AND.push(mySurveyCondition) + } + + /** 키워드 검색 조건 */ + const keywordCondition = this.createKeywordCondition() + if (Object.keys(keywordCondition).length > 0) { + where.AND.push(keywordCondition) + } + + /** 역할 기반 조건 */ + const roleCondition = this.createRoleCondition() + if (Object.keys(roleCondition).length > 0) { + where.AND.push(roleCondition) + } + + return where + } + + /** + * @description 조사 매물 검색 + * @param {WhereCondition} where 조사 매물 검색 조건 + * @returns {Promise<{ data: SurveyBasicInfo[], count: number }>} 조사 매물 데이터 + */ + async getSurveySales(where: WhereCondition) { + /** 조사 매물 조회 */ + //@ts-ignore + const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ + where, + orderBy: this.params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, + skip: Number(this.params.offset), + take: ITEMS_PER_PAGE, + }) + + /** 조사 매물 개수 조회 */ + //@ts-ignore + const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) + return { data: surveys as SurveyBasicInfo[], count } + } + + /** + * @description 조사 매물 생성 + * @param {SurveyRegistRequest} survey 조사 매물 데이터 + * @param {string} role 권한 + * @param {string} storeId 판매점 ID + * @returns {Promise} 생성된 조사 매물 데이터 + */ + async createSurvey(survey: SurveyRegistRequest, role: string, storeId: string) { + const { detailInfo, ...basicInfo } = survey + const newSrlNo = survey.srlNo ?? (await this.getNewSrlNo(storeId, role)) + // @ts-ignore + return await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ + data: { + ...convertToSnakeCase(basicInfo), + SRL_NO: newSrlNo, + DETAIL_INFO: { + create: convertToSnakeCase(detailInfo), + }, + }, + }) + } + + /** + * @description 새로운 srlNo 생성 함수 + * @param {string} role 세션에 저장된 권한 + * @param {string} tempSrlNo 임시 srlNo (임시저장 시 사용) + * @param {string} storeId 세션에 저장된 판매점 ID + * @returns {Promise} 새로운 srlNo + * + * @exampleResult HO250617001 (HO + 250617 + 001) + */ + async getNewSrlNo(storeId: string, role: string) { + const srlRole = role === 'T01' || role === 'Admin' ? 'HO' : role === 'Admin_Sub' || role === 'Builder' ? 'HM' : '' + + //@ts-ignore + const index = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ + where: { + SRL_NO: { + startsWith: srlRole + storeId, + }, + }, + }) + + return ( + srlRole + + storeId + + new Date().getFullYear().toString().slice(-2) + + (new Date().getMonth() + 1).toString().padStart(2, '0') + + new Date().getDate().toString().padStart(2, '0') + + (index + 1).toString().padStart(3, '0') + ) + } + + /** + * @description 조사 매물 상세 조회 + * @param {number} id 조사 매물 ID + * @returns {Promise} 조사 매물 데이터 + */ + async fetchSurvey(id: number) { + // @ts-ignore + return (await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + where: { ID: id }, + include: { DETAIL_INFO: true }, + })) as SurveyBasicInfo + } + + /** + * @description 조사 매물 수정 + * @param {number} id 조사 매물 ID + * @param {SurveyRegistRequest} survey 조사 매물 데이터 + * @param {boolean} isTemporary 임시 저장 여부 + * @param {string} storeId 판매점 ID + * @param {string} role 권한 + * @returns {Promise} 수정된 조사 매물 데이터 + */ + async updateSurvey(id: number, survey: SurveyRegistRequest, isTemporary: boolean, storeId: string, role: string) { + const { detailInfo, ...basicInfo } = survey + const newSrlNo = isTemporary ? survey.srlNo ?? '一時保存' : await this.getNewSrlNo(storeId, role) + + // @ts-ignore + return (await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + where: { ID: Number(id) }, + data: { + ...convertToSnakeCase(basicInfo), + SRL_NO: newSrlNo, + UPT_DT: new Date(), + DETAIL_INFO: { + update: convertToSnakeCase(detailInfo), + }, + }, + include: { + DETAIL_INFO: true, + }, + })) as SurveyBasicInfo + } + + /** + * @description 조사 매물 삭제 + * @param {number} id 조사 매물 ID + */ + async deleteSurvey(id: number) { + await prisma.$transaction(async (tx: Prisma.TransactionClient) => { + // @ts-ignore + const detailData = await tx.SD_SURVEY_SALES_BASIC_INFO.findUnique({ + where: { ID: Number(id) }, + select: { + DETAIL_INFO: true, + }, + }) + + if (detailData?.DETAIL_INFO?.ID) { + // @ts-ignore + await tx.SD_SURVEY_SALES_DETAIL_INFO.delete({ + where: { ID: Number(detailData.DETAIL_INFO.ID) }, + }) + } + + // @ts-ignore + await tx.SD_SURVEY_SALES_BASIC_INFO.delete({ + where: { ID: Number(id) }, + }) + }) + } + + /** + * @description 조사 매물 제출 + * @param {number} id 조사 매물 ID + * @param {string} targetId 제출 대상 ID + * @param {string} targetNm 제출 대상 이름 + * @returns {Promise} 제출된 조사 매물 데이터 + */ + async submitSurvey(id: number, targetId: string, targetNm: string) { + // @ts-ignore + return (await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + where: { ID: Number(id) }, + data: { + SUBMISSION_STATUS: true, + SUBMISSION_DATE: new Date(), + SUBMISSION_TARGET_ID: targetId, + SUBMISSION_TARGET_NM: targetNm, + UPT_DT: new Date(), + }, + })) as SurveyBasicInfo + } + + /** + * @description 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {SessionData} session 세션 데이터 + * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) + */ + checkRole(survey: any, session: SessionData): boolean { + if (!survey || !session.isLoggedIn) return false + + const roleChecks = { + T01: () => this.checkT01Role(survey), + Admin: () => this.checkAdminRole(survey, session.storeId), + Admin_Sub: () => this.checkAdminSubRole(survey, session.storeId), + Partner: () => this.checkPartnerOrBuilderRole(survey, session.builderId), + Builder: () => this.checkPartnerOrBuilderRole(survey, session.builderId), + } + + return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false + } + + private checkT01Role(survey: any): boolean { + return survey.SRL_NO !== '一時保存' + } + + 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 + } + + private checkAdminSubRole(survey: any, storeId: string | null): boolean { + if (!storeId) return false + return survey.SUBMISSION_STATUS + ? survey.SUBMISSION_TARGET_ID === storeId || (survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID) + : survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID + } + + private checkPartnerOrBuilderRole(survey: any, builderId: string | null): boolean { + if (!builderId) return false + return survey.CONSTRUCTION_POINT_ID === builderId + } + + handleRouteError(error: unknown): NextResponse { + console.error('❌ API ROUTE ERROR : ', error) + return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError }) + } + + async tryFunction(func: () => Promise): Promise { + try { + return await func() + } catch (error) { + return this.handleRouteError(error) + } + } +} diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index d6581f9..99bd072 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -106,10 +106,15 @@ export function useSurvey( * @param {any} error 에러 객체 * @returns {void} 라우팅 처리 */ - const errorRouter = (error: any) => { + const handleError = (error: any, isThrow?: boolean) => { const status = error.response?.status - if (error.response?.data.error) { - alert(error.response?.data.error) + const errorMsg = error.response?.data.error + console.error('❌ API ERROR : ', error) + if (errorMsg) { + alert(errorMsg) + } + if (isThrow) { + throw new Error(error) } switch (status) { /** session 없는 경우 */ @@ -164,7 +169,7 @@ export function useSurvey( }) return resp.data } catch (error: any) { - errorRouter(error) + handleError(error, false) return { data: [], count: 0 } } }, @@ -208,7 +213,7 @@ export function useSurvey( }) return resp.data } catch (error: any) { - errorRouter(error) + handleError(error, false) return null } }, @@ -234,6 +239,9 @@ export function useSurvey( queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) queryClient.invalidateQueries({ queryKey: ['survey', id] }) }, + onError: (error: any) => { + handleError(error, true) + }, }) /** @@ -248,7 +256,7 @@ export function useSurvey( */ const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => { - if (id === undefined) throw new Error('id is required') + if (id === undefined) throw new Error() const resp = await axiosInstance(null).put(`/api/survey-sales/${id}`, { survey: survey, isTemporary: isTemporary, @@ -262,7 +270,7 @@ export function useSurvey( queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) }, onError: (error: any) => { - alert(error.response?.data.error) + handleError(error, true) }, }) @@ -278,7 +286,7 @@ export function useSurvey( */ const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({ mutationFn: async () => { - if (id === null) throw new Error('id is required') + if (id === null) throw new Error() const resp = await axiosInstance(null).delete(`/api/survey-sales/${id}`) return resp.data }, @@ -301,7 +309,7 @@ export function useSurvey( */ const { mutateAsync: submitSurvey, isPending: isSubmittingSurvey } = useMutation({ mutationFn: async ({ targetId, targetNm }: { targetId?: string | null; targetNm?: string | null }) => { - if (!id) throw new Error('id is required') + if (!id) throw new Error() const resp = await axiosInstance(null).patch(`/api/survey-sales/${id}`, { targetId, targetNm, @@ -313,7 +321,7 @@ export function useSurvey( queryClient.invalidateQueries({ queryKey: ['survey', id] }) }, onError: (error: any) => { - alert(error.response?.data.error) + handleError(error, true) }, }) @@ -376,9 +384,8 @@ export function useSurvey( ) return data.results } catch (error: any) { - console.error('Failed to fetch zipcode data:', error) - alert(error.response?.data.error) - throw new Error('Failed to fetch zipcode data') + handleError(error, true) + return null } } @@ -406,7 +413,7 @@ export function useSurvey( const { data } = await axiosInstance(null).get(endpoint) return data } catch (error: any) { - alert(error.response?.data.error) + handleError(error, true) return null } } diff --git a/src/types/Survey.ts b/src/types/Survey.ts index d46dae7..a653163 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -301,3 +301,25 @@ export type SubmitTargetResponse = { /* 권한 */ auth: string } + +/** + * @description 조사매물 검색 파라미터 타입 + */ +export type SurveySearchParams = { + /** 검색 키워드 */ + keyword?: string | null + /** 검색 옵션 */ + searchOption?: string | null + /** 내 조사매물 여부 */ + isMySurvey?: string | null + /** 정렬 옵션 */ + sort?: string | null + /** 페이지 번호 */ + offset?: string | null + /** 권한 */ + role?: string | null + /** 판매점 ID */ + storeId?: string | null + /** 시공점 ID */ + builderId?: string | null +}