diff --git a/src/app/api/comm-code/route.ts b/src/app/api/comm-code/route.ts index 6148a33..26340b6 100644 --- a/src/app/api/comm-code/route.ts +++ b/src/app/api/comm-code/route.ts @@ -1,12 +1,15 @@ import { NextRequest, NextResponse } from 'next/server' import { loggerWrapper } from '@/libs/api-wrapper' import { prisma } from '@/libs/prisma' -import type { CommCode } from '@/types/CommCode' +import { CommonCode } from '@/types/Inquiry' async function getCommCode(request: NextRequest): Promise { try { const searchParams = request.nextUrl.searchParams const headCode = searchParams.get('headCode') + if (headCode === 'QNA_CD') { + return getQnaCd() + } // @ts-ignore const headCd = await prisma.BC_COMM_H.findFirst({ @@ -24,23 +27,22 @@ async function getCommCode(request: NextRequest): Promise { if (headCode === 'SALES_OFFICE_CD') { return getSaleOffice(headCd.HEAD_CD) - } else { - // @ts-ignore - const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ - where: { - HEAD_CD: headCd.HEAD_CD, - }, - select: { - HEAD_CD: true, - CODE: true, - CODE_JP: true, - }, - orderBy: { - CODE: 'asc', - }, - }) - return NextResponse.json(roofMaterials) } + // @ts-ignore + const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ + where: { + HEAD_CD: headCd.HEAD_CD, + }, + select: { + HEAD_CD: true, + CODE: true, + CODE_JP: true, + }, + orderBy: { + CODE: 'asc', + }, + }) + return NextResponse.json(roofMaterials) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) @@ -64,4 +66,50 @@ const getSaleOffice = async (headCode: string) => { return NextResponse.json(commCodeSaleOffice) } +/** + * @description QNA 공통 코드 조회 + * @returns {CommonCode[]} QNA 공통 코드 목록 + */ +const getQnaCd = async () => { + // @ts-ignore + const headCdList: { HEAD_CD: string; HEAD_ID: string }[] = await prisma.BC_COMM_H.findMany({ + where: { + OR: [{ HEAD_ID: { in: ['QNA_CLS_LRG_CD'] } }, { HEAD_ID: { in: ['QNA_CLS_MID_CD'] } }, { HEAD_ID: { in: ['QNA_CLS_SML_CD'] } }], + }, + select: { + HEAD_CD: true, + HEAD_ID: true, + }, + }) + const result: CommonCode[] = [] + // @ts-ignore + const commCodeQna: CommCode[] = await prisma.BC_COMM_L.findMany({ + where: { + HEAD_CD: { + in: headCdList.map((item) => item.HEAD_CD).filter(Boolean), + }, + }, + select: { + HEAD_CD: true, + CODE: true, + CODE_JP: true, + REF_CHR1: true, + }, + }) + commCodeQna.forEach((item) => { + result.push({ + // @ts-ignore + headCd: item.HEAD_CD, + // @ts-ignore + headId: headCdList.find((headCd) => headCd.HEAD_CD === item.HEAD_CD)?.HEAD_ID ?? '', + // @ts-ignore + code: item.CODE, + // @ts-ignore + name: item.CODE_JP, + // @ts-ignore + refChar1: item.REF_CHR1 ?? '', + }) + }) + return NextResponse.json(result) +} export const GET = loggerWrapper(getCommCode) diff --git a/src/app/api/qna/route.ts b/src/app/api/qna/route.ts deleted file mode 100644 index 767f9ca..0000000 --- a/src/app/api/qna/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextResponse } from 'next/server' -import axios from 'axios' -import { loggerWrapper } from '@/libs/api-wrapper' -import { QnaService } from './service' -import { ApiError } from 'next/dist/server/api-utils' - -/** - * @api {GET} /api/qna 문의 유형 목록 조회 API - * @apiName GET /api/qna - * @apiGroup Qna - * @apiDescription 문의 유형 목록 조회 API - * - * @apiSuccess {Object} data 문의 유형 목록 - * @apiSuccess {String} data.headCd 문의 유형 헤드 코드 - * @apiSuccess {String} data.code 문의 유형 코드 - * @apiSuccess {String} data.codeJp 문의 유형 이름 - 일본어 - * @apiSuccess {String} data.refChr1 문의 유형 참조 - 유형 상위 구분 - * - * @apiExample {curl} Example usage: - * curl -X GET http://localhost:3000/api/qna - * - * @apiSuccessExample {json} Success-Response: - * { - * "data": [ - * { - * "headCd": "204200", - * "code": "1", - * "codeJp": "1", - * "refChr1": "1" - * } - * ], - * ... - * } - */ -async function getCommonCodeListData(): Promise { - const service = new QnaService() - const response = await service.tryFunction(() => axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/system/commonCodeListData`)) - if (response instanceof ApiError) { - return NextResponse.json({ error: response.message }, { status: response.statusCode }) - } - const result = service.getInquiryTypeList(response.data.apiCommCdList) - return NextResponse.json({ data: result }) -} - -export const GET = loggerWrapper(getCommonCodeListData) diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index d042658..05d0595 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -1,4 +1,4 @@ -import axios from 'axios' +import axios, { HttpStatusCode } from 'axios' import { NextResponse } from 'next/server' import { loggerWrapper } from '@/libs/api-wrapper' import { QnaService } from '../service' @@ -12,9 +12,9 @@ import { cookies } from 'next/headers' * @api {POST} /api/qna/save 문의 저장 API * @apiName POST /api/qna/save * @apiGroup Qna - * @apiDescription 문의 저장 API + * @apiDescription 문의 메일 발송 후 문의 저장 API * - * @apiBody {InquiryRequest} inquiryRequest 문의 저장 요청 파라미터 + * @apiBody {FormData} formData 문의 저장 요청 파라미터 * * @apiExample {curl} Example usage: * curl -X POST http://localhost:3000/api/qna/save @@ -32,9 +32,14 @@ import { cookies } from 'next/headers' async function setQna(request: Request): Promise { const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) - const service = new QnaService(session) + const formData = await request.formData() + const mailResult = await service.sendMail(formData) + if (mailResult instanceof ApiError) { + return NextResponse.json({ error: mailResult.message }, { status: mailResult.statusCode }) + } + const result = await service.tryFunction(() => axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData, { headers: { diff --git a/src/app/api/qna/service.ts b/src/app/api/qna/service.ts index 2788c97..93774a9 100644 --- a/src/app/api/qna/service.ts +++ b/src/app/api/qna/service.ts @@ -1,8 +1,10 @@ import { SessionData } from '@/types/Auth' -import { CommonCode } from '@/types/Inquiry' +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 @@ -15,6 +17,7 @@ export class QnaService { * @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) } @@ -23,13 +26,13 @@ export class QnaService { * @param {() => Promise} func 비동기 함수 * @returns {Promise} 에러 객체 또는 함수 결과 */ - async tryFunction(func: () => Promise, isFile?: boolean): Promise { + async tryFunction(func: () => Promise, shouldThrowResult?: boolean): Promise { if (this.session !== undefined && !this.session?.isLoggedIn) { return new ApiError(HttpStatusCode.Unauthorized, ERROR_MESSAGE.UNAUTHORIZED) } try { const response = await func() - if (isFile) return response + if (shouldThrowResult) return response return this.handleResult(response) } catch (error) { return this.handleRouteError(error) @@ -54,12 +57,22 @@ export class QnaService { * @param {string[]} responseList 문의 유형 타입 목록 * @returns {CommonCode[]} 문의 유형 타입 목록 */ - getInquiryTypeList(responseList: string[]): CommonCode[] { + getInquiryTypeList(responseList: any): CommonCode[] { const codeList: CommonCode[] = [] - responseList.forEach((item: any) => { - if (item.headCd === '204200' || item.headCd === '204300' || item.headCd === '204400') { + const headCdList: { headCd: string; headId: string }[] = [] + responseList.apiHeadCdList.forEach((item: any) => { + if (item.headId === 'QNA_CLS_LRG_CD' || item.headId === 'QNA_CLS_MID_CD' || item.headId === 'QNA_CLS_SML_CD') { + headCdList.push({ + headCd: item.headCd, + headId: item.headId, + }) + } + }) + responseList.apiCommCdList.forEach((item: any) => { + if (headCdList.some((headCd) => headCd.headCd === item.headCd)) { codeList.push({ headCd: item.headCd, + headId: headCdList.find((headCd) => headCd.headCd === item.headCd)?.headId ?? '', code: item.code, name: item.codeJp, refChar1: item.refChr1, @@ -86,4 +99,94 @@ export class QnaService { }) return params } + + /** + * @description 문의 메일 발송 + * @param {FormData} formData 문의 메일 발송 요청 파라미터 - qnaClsLrgCd, title, contents, files + * @returns {ApiError | null} 에러 객체 또는 null + */ + async sendMail(formData: FormData): Promise { + 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 { + 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) => ` +
+

+ お問い合わせが登録されました。
+ { QSP > System mgt. > お問い合わせ } でお問い合わせ内容を確認してください。 +

+

+ -登録者: ${formData.get('regUserNm')} +

+

+ -販売店ID: + + ${formData.get('storeId')} + +

+

+ -販売店名: + ${this.session?.storeNm ?? ' - '} +

+

+ -お問い合わせ内容: +

+

+ ${formData.get('contents')} +

+
+ ` } diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 2f5f677..1624822 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -5,7 +5,7 @@ import { useSessionStore } from '@/store/session' import { InquiryRequest } from '@/types/Inquiry' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import { CONFIRM_MESSAGE, SUCCESS_MESSAGE, useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg' +import { CONFIRM_MESSAGE, ERROR_MESSAGE, SUCCESS_MESSAGE, useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' export default function RegistForm() { @@ -106,9 +106,13 @@ export default function RegistForm() { showConfirm( CONFIRM_MESSAGE.SAVE_INQUIRY_CONFIRM, async () => { - const res = await saveInquiry(formData) - showSuccessAlert(SUCCESS_MESSAGE.SAVE_SUCCESS) - router.push(`/inquiry/${res.qnaNo}`) + try { + const res = await saveInquiry(formData) + showSuccessAlert(SUCCESS_MESSAGE.SAVE_SUCCESS) + router.push(`/inquiry/${res.qnaNo}`) + } catch (error) { + showErrorAlert(ERROR_MESSAGE.SERVER_ERROR) + } }, () => null, ) @@ -149,7 +153,7 @@ export default function RegistForm() { 選択してください {commonCodeList - .filter((code) => code.headCd === '204200') + .filter((code) => code.headId === 'QNA_CLS_LRG_CD') .map((code) => (