461 lines
16 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|