keyy1315 cbef015fff fix: 2차점 시공권한 계정 조사매물 조회 및 생성 로직 변경
- 기존 builderId 에서 builderNo로 조건절 및 데이터 입력값 변경
2025-08-05 16:12:44 +09:00

461 lines
16 KiB
TypeScript

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<SurveyBasicInfo>} 생성된 조사 매물 데이터
*/
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<string>} 새로운 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<SurveyBasicInfo>} 조사 매물 데이터
*/
async fetchSurvey(id: number, isPdf: boolean): Promise<SurveyBasicInfo | ApiError | Blob> {
// @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<Blob>} PDF Blob
*/
async createSurveyPdf(survey: SurveyBasicInfo): Promise<Blob | ApiError> {
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<SurveyBasicInfo>} 수정된 조사 매물 데이터
*/
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<SurveyBasicInfo>} 제출된 조사 매물 데이터
*/
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<any>} func 비동기 함수
* @returns {Promise<ApiError | any>} 에러 객체 또는 함수 결과
*/
async tryFunction(func: () => Promise<any>): Promise<ApiError | any> {
try {
return await func()
} catch (error) {
return this.handleRouteError(error)
}
}
}