import { prisma } from '@/libs/prisma' import { SurveyBasicInfo, SurveyRegistRequest, SurveySearchParams } from '@/types/Survey' import { convertToCamelCase, convertToSnakeCase } from '@/utils/common-utils' import { Prisma } from '@prisma/client' import type { SessionData } from '@/types/Auth' import { HttpStatusCode } from 'axios' import { ApiError } from 'next/dist/server/api-utils' import { ERROR_MESSAGE } from '@/hooks/useAlertMsg' import SurveySalePdf from '@/components/pdf/SurveySalePdf' import React from 'react' import { pdf, Document } from '@react-pdf/renderer' 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 private session?: SessionData /** * @description 생성자 * @param {SurveySearchParams} params 검색 파라미터 */ constructor(params: SurveySearchParams, session?: SessionData) { this.params = params this.session = session } /** * @description 권한 별 필수 값 존재 여부 확인, 없을 시 빈 데이터 반환 * @param {SearchParams} params 검색 파라미터 * @returns {NextResponse} 세션 체크 결과 */ checkSession(): ApiError | null { if (!this.session?.isLoggedIn || this.session?.role === null) { return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGE.UNAUTHORIZED) } if (this.session?.role === 'Builder' || this.session?.role === 'Partner') { if (this.params.builderId === null) { return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGE.FORBIDDEN) } } else { if (this.session?.storeId === null) { return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGE.FORBIDDEN) } } 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 : 임시저장되지 않은 전체 매물 조회 | T01 판매점에서 작성한 매물 조회 * @description Admin : 같은 판매점에서 작성된 매물, 2차점에게 제출받은 매물 조회 | 본인이 작성한 매물 조회 * @description Admin_Sub : 같은 판매점에서 작성된 매물, 시공권한 user에게 제출받은 매물 조회 | 본인이 작성한 매물 조회 * @description Builder : 같은 시공점에서 작성된 매물 조회 | 본인이 작성한 매물 조회 * @description Partner : 같은 시공점에서 작성된 매물 조회 | 본인이 작성한 매물 조회 */ private createRoleCondition(): WhereCondition { const where: WhereCondition = { AND: [] } switch (this.session?.role) { case 'Admin': case 'Admin_Sub': if (this.session?.storeId) { where.OR = [ { AND: [{ STORE_ID: { equals: this.session.storeId } }] }, { AND: [{ SUBMISSION_TARGET_ID: { equals: this.session.storeId } }, { SUBMISSION_STATUS: { equals: true } }] }, { AND: [{ SUBMISSION_TARGET_NM: { equals: this.session.storeNm } }, { SUBMISSION_STATUS: { equals: true } }] }, ] } else { where.AND.push({ REPRESENTATIVE_ID: { equals: this.session.userId } }) } break case 'Builder': case 'Partner': if (this.session?.builderNo) { where.AND.push({ CONSTRUCTION_POINT_ID: { equals: this.session?.builderNo } }) } else { where.AND.push({ REPRESENTATIVE_ID: { equals: this.session?.userId } }) } break case 'T01': where.OR = [{ NOT: { SRL_NO: { startsWith: '一時保存' } } }, { STORE_ID: { equals: this.session?.storeId } }] break default: where.AND.push({ ID: { equals: -1 } }) 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 { AND: [...where.AND, { DEL_YN: { equals: 'N' } }] } } /** * @description 조사 매물 검색 * @param {WhereCondition} where 조사 매물 검색 조건 * @returns {Promise<{ data: SurveyBasicInfo[], count: number }>} 조사 매물 데이터 */ 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({ 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, isPdf: boolean): Promise { // @ts-ignore const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ where: { ID: id, DEL_YN: 'N' }, include: { DETAIL_INFO: true }, }) if (!result) { return new ApiError(HttpStatusCode.NotFound, ERROR_MESSAGE.NOT_FOUND) } if (!isPdf) { if (!this.session?.isLoggedIn) return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGE.UNAUTHORIZED) if (!this.checkRole(result, this.session as SessionData)) return new ApiError(HttpStatusCode.Forbidden, ERROR_MESSAGE.FORBIDDEN) } return result } /** * @description 조사 매물 PDF 생성 * @param {SurveyBasicInfo} survey 조사 매물 데이터 * @returns {Promise} PDF Blob */ async createSurveyPdf(survey: SurveyBasicInfo): Promise { if (!survey) { return new ApiError(HttpStatusCode.NotFound, ERROR_MESSAGE.NOT_FOUND) } try { const convertedSurvey = convertToCamelCase(survey) as SurveyBasicInfo const content = React.createElement(Document, null, React.createElement(SurveySalePdf, { survey: convertedSurvey })) const pdfBlob = await pdf(content).toBlob() const arrayBuffer = await pdfBlob.arrayBuffer() return new Blob([arrayBuffer], { type: 'application/pdf' }) } catch (error) { return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.PDF_GENERATION_ERROR) } } /** * @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) : survey.srlNo // @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 await tx.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { DEL_YN: 'Y', UPT_DT: new Date(), }, }) }) } /** * @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) return false const roleChecks = { T01: () => this.checkT01Role(survey, session.userId, session.storeId), Admin: () => this.checkAdminRole(survey, session.storeId, session.storeNm), Admin_Sub: () => this.checkAdminRole(survey, session.storeId, session.storeNm), Partner: () => this.checkPartnerOrBuilderRole(survey, session.builderNo, session.userId), Builder: () => this.checkPartnerOrBuilderRole(survey, session.builderNo, session.userId), default: () => false, } return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false } /** * @description T01 권한 체크 * - 본인이 작성한 임시저장 매물 혹은 T01 판매점에서 작성한 매물, 임시저장 매물을 제외한 전 매물 조회 가능 * * @param {any} survey 조사 매물 데이터 * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) */ private checkT01Role(survey: any, userId: string | null, storeId: string | null): boolean { if (survey.REPRESENTATIVE_ID === userId || survey.STORE_ID === storeId) { return true } return survey.SRL_NO !== '一時保存' } /** * @description Admin, Admin_Sub 권한 체크 (1차점 - Order, 2차점 - Musubi) * - 같은 판매점에서 작성한 매물, 제출 받은 매물 조회 가능 * * @param {any} survey 조사 매물 데이터 * @param {string | null} storeId 판매점 ID * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) */ private checkAdminRole(survey: any, storeId: string | null, storeNm: string | null): boolean { if (!storeId) return survey.REPRESENTATIVE_ID === this.session?.userId return survey.SUBMISSION_STATUS ? survey.SUBMISSION_TARGET_ID === storeId || survey.SUBMISSION_TARGET_NM === storeNm || survey.STORE_ID === storeId : survey.STORE_ID === storeId } /** * @description Partner, Builder 권한 체크 * - 같은 시공점에서 작성한 매물 조회 가능 * - 시공점ID가 없다면 본인이 작성한 매물 조회 가능 * * @param {any} survey 조사 매물 데이터 * @param {string | null} builderId 시공점 ID * @param {string | null} userId 유저 ID * @returns {boolean} 해당 매물의 조회 권한 여부 (true: 권한 있음, false: 권한 없음) */ private checkPartnerOrBuilderRole(survey: any, builderId: string | null, userId: string | null): boolean { if (builderId) return survey.CONSTRUCTION_POINT_ID === builderId return survey.REPRESENTATIVE_ID === userId } /** * @description API ROUTE 에러 처리 * @param {any} error 에러 객체 * @returns {ApiError} 에러 객체 */ handleRouteError(error: any): ApiError { console.error('❌ API ROUTE ERROR : ', error) if ( error instanceof Prisma.PrismaClientInitializationError || error instanceof Prisma.PrismaClientUnknownRequestError || error instanceof Prisma.PrismaClientRustPanicError || error instanceof Prisma.PrismaClientValidationError || error instanceof Prisma.PrismaClientKnownRequestError ) { return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.PRISMA_ERROR) } return new ApiError(error.statusCode ?? HttpStatusCode.InternalServerError, error.message ?? ERROR_MESSAGE.FETCH_ERROR) } /** * @description 비동기 함수 try-catch 처리 함수 * @param {() => Promise} func 비동기 함수 * @returns {Promise} 에러 객체 또는 함수 결과 */ async tryFunction(func: () => Promise): Promise { try { return await func() } catch (error) { return this.handleRouteError(error) } } }