183 lines
6.6 KiB
TypeScript
183 lines
6.6 KiB
TypeScript
import { SessionData } from '@/types/Auth'
|
||
import { CommonCode, InquiryRequest } from '@/types/Inquiry'
|
||
import { ERROR_MESSAGE } from '@/hooks/useAlertMsg'
|
||
import { HttpStatusCode } from 'axios'
|
||
import { ApiError } from 'next/dist/server/api-utils'
|
||
import { prisma } from '@/libs/prisma'
|
||
import { sendEmail } from '@/libs/mailer'
|
||
|
||
export class QnaService {
|
||
private session?: SessionData
|
||
constructor(session?: SessionData) {
|
||
this.session = session
|
||
}
|
||
/**
|
||
* @description API ROUTE 에러 처리
|
||
* @param {any} error 에러 객체
|
||
* @returns {ApiError} 에러 객체
|
||
*/
|
||
private handleRouteError(error: any): ApiError {
|
||
if (error === undefined) return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.SERVER_ERROR)
|
||
console.error('❌ API ROUTE ERROR : ', error)
|
||
return new ApiError(error.response.status ?? HttpStatusCode.InternalServerError, error.response.data.result.message ?? ERROR_MESSAGE.FETCH_ERROR)
|
||
}
|
||
/**
|
||
* @description 비동기 함수 try-catch 처리 함수
|
||
* @param {() => Promise<any>} func 비동기 함수
|
||
* @returns {Promise<ApiError | any>} 에러 객체 또는 함수 결과
|
||
*/
|
||
async tryFunction(func: () => Promise<any>, shouldThrowResult?: boolean): Promise<ApiError | any> {
|
||
if (this.session !== undefined && !this.session?.isLoggedIn) {
|
||
return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGE.UNAUTHORIZED)
|
||
}
|
||
try {
|
||
const response = await func()
|
||
if (shouldThrowResult) return response
|
||
return this.handleResult(response)
|
||
} catch (error) {
|
||
return this.handleRouteError(error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @description 함수 결과 처리 함수
|
||
* @param {any} result 함수 결과
|
||
* @returns {ApiError | any} 에러 객체 또는 함수 결과
|
||
*/
|
||
private handleResult(response: any): ApiError | any {
|
||
if (response.status === HttpStatusCode.Ok) {
|
||
if (response.data.data !== null) return response.data
|
||
return new ApiError(HttpStatusCode.NotFound, ERROR_MESSAGE.NOT_FOUND)
|
||
}
|
||
return new ApiError(response.result.code, response.result.message)
|
||
}
|
||
|
||
/**
|
||
* @description 문의 유형 타입 목록 조회
|
||
* @param {string[]} responseList 문의 유형 타입 목록
|
||
* @returns {CommonCode[]} 문의 유형 타입 목록
|
||
*/
|
||
getInquiryTypeList(responseList: string[]): CommonCode[] {
|
||
const codeList: CommonCode[] = []
|
||
responseList.forEach((item: any) => {
|
||
if (item.headCd === '204200' || item.headCd === '204300' || item.headCd === '204400') {
|
||
codeList.push({
|
||
headCd: item.headCd,
|
||
code: item.code,
|
||
name: item.codeJp,
|
||
refChar1: item.refChr1,
|
||
})
|
||
}
|
||
})
|
||
return codeList
|
||
}
|
||
|
||
/**
|
||
* @description 문의 목록 조회 파라미터 처리
|
||
* @param {URLSearchParams} searchParams URLSearchParams 객체
|
||
* @returns {Record<string, string>} 문의 목록 조회 파라미터
|
||
*/
|
||
getSearchParams(searchParams: URLSearchParams): Record<string, string> {
|
||
const params: Record<string, string> = {}
|
||
searchParams.forEach((value, key) => {
|
||
const match = key.match(/inquiryListRequest\[(.*)\]/)
|
||
if (match) {
|
||
params[match[1]] = value
|
||
} else {
|
||
params[key] = value
|
||
}
|
||
})
|
||
return params
|
||
}
|
||
|
||
/**
|
||
* @description 문의 메일 발송
|
||
* @param {FormData} formData 문의 메일 발송 요청 파라미터 - qnaClsLrgCd, title, contents, files
|
||
* @returns {ApiError | null} 에러 객체 또는 null
|
||
*/
|
||
async sendMail(formData: FormData): Promise<ApiError | void> {
|
||
const receivers: string[] = await this.getReceiver(formData.get('qnaClsLrgCd') as string)
|
||
if (receivers.length === 0) {
|
||
return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.EMAIL_SEND_ERROR)
|
||
}
|
||
const files = formData.getAll('files') as File[]
|
||
const fileBuffers = await Promise.all(files.map((file) => file.arrayBuffer()))
|
||
|
||
const attachments = files.map((file, index) => ({
|
||
filename: file.name,
|
||
content: Buffer.from(fileBuffers[index]),
|
||
contentType: file.type || 'application/octet-stream',
|
||
}))
|
||
|
||
return this.tryFunction(() => {
|
||
return sendEmail({
|
||
from: 'test@test.com',
|
||
to: receivers,
|
||
subject: `[HANASYS お問い合わせ] ${formData.get('title')}`,
|
||
content: this.generateEmailContent(formData),
|
||
attachments: attachments,
|
||
})
|
||
}, true)
|
||
}
|
||
|
||
/**
|
||
* @description 문의 수신자 조회
|
||
* @param {string} qnaClsLrgCd 문의 유형 대분류 코드
|
||
* @returns {string[]} 문의 수신자 목록
|
||
*/
|
||
async getReceiver(qnaClsLrgCd: string): Promise<string[]> {
|
||
const query = `
|
||
OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP;
|
||
SELECT CONVERT(NVARCHAR(100), DecryptByKey(bu.e_mail)) AS email
|
||
FROM BC_USER bu
|
||
WHERE bu.user_id IN (
|
||
SELECT user_id
|
||
FROM SY_POLICY_U spu
|
||
WHERE policy_cd = (
|
||
SELECT bcl.ref_chr2
|
||
FROM BC_COMM_L bcl
|
||
WHERE bcl.head_cd = (SELECT head_cd FROM BC_COMM_H bch WHERE head_id = 'QNA_CLS_MID_CD')
|
||
AND bcl.ref_chr1 = '${qnaClsLrgCd}'
|
||
GROUP BY bcl.ref_chr2
|
||
)
|
||
)
|
||
CLOSE SYMMETRIC KEY SYMMETRICKEY;`
|
||
|
||
const receivers: { email: string }[] = await prisma.$queryRawUnsafe(query)
|
||
return receivers.map((receiver) => receiver.email)
|
||
}
|
||
|
||
/**
|
||
* @description 문의 이메일 내용 생성
|
||
* @param {FormData} formData 문의 메일 발송 요청 파라미터 - qnaClsLrgCd, title, contents, files
|
||
* @returns {string} 문의 이메일 내용
|
||
*/
|
||
generateEmailContent = (formData: FormData) => `
|
||
<div>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59; margin-bottom: 15px;">
|
||
お問い合わせが登録されました。<br/>
|
||
{ QSP > System mgt. > お問い合わせ } でお問い合わせ内容を確認してください。
|
||
</p>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59; margin-bottom: 3px;">
|
||
-登録者: <span style="color: #417DDC;">${formData.get('regUserNm')}</span>
|
||
</p>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59; margin-bottom: 3px;">
|
||
-販売店ID:
|
||
<span style="color: #417DDC;">
|
||
${formData.get('storeId')}
|
||
</span>
|
||
</p>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59; margin-bottom: 15px;">
|
||
-販売店名:
|
||
<span style="color: #417DDC;">${this.session?.storeNm ?? ' - '}</span>
|
||
</p>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59; margin-bottom: 15px;">
|
||
-お問い合わせ内容:
|
||
</p>
|
||
<p style="font-size: 13px; font-weight: 400; color: #2e3a59;">
|
||
${formData.get('contents')}
|
||
</p>
|
||
</div>
|
||
`
|
||
}
|