From fb27e414d88b759680513711d0d8851693ce6070 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 2 Jul 2025 17:50:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9D=98=20=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=9C=EC=86=A1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/qna/save/route.ts | 13 ++-- src/app/api/qna/service.ts | 99 ++++++++++++++++++++++++++- src/components/inquiry/RegistForm.tsx | 12 ++-- src/libs/mailer.ts | 15 ++-- 4 files changed, 117 insertions(+), 22 deletions(-) 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..cbba527 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) @@ -86,4 +89,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..0011d9e 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, ) diff --git a/src/libs/mailer.ts b/src/libs/mailer.ts index 1b646c2..727e7a3 100644 --- a/src/libs/mailer.ts +++ b/src/libs/mailer.ts @@ -1,6 +1,7 @@ 'use server' import nodemailer from 'nodemailer' +import { Attachment } from 'nodemailer/lib/mailer' interface EmailParams { from: string @@ -8,9 +9,10 @@ interface EmailParams { cc?: string | string[] subject: string content: string + attachments?: Attachment[] } -export async function sendEmail({ from, to, cc, subject, content }: EmailParams): Promise { +export async function sendEmail({ from, to, cc, subject, content, attachments }: EmailParams): Promise { // Create a transporter using SMTP const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -27,6 +29,7 @@ export async function sendEmail({ from, to, cc, subject, content }: EmailParams) cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined, subject, html: content, + attachments: attachments || [], } try { @@ -37,13 +40,3 @@ export async function sendEmail({ from, to, cc, subject, content }: EmailParams) throw new Error('Failed to send email') } } - -async function sendEmailTest() { - await sendEmail({ - from: 'from@test.com', - to: 'test@test.com', - cc: 'test2@test.com', - subject: 'Test Email', - content: '

Hello

This is a test email.

', - }) -} From d79fc4a23a58f871b38519bcd0186b76a06c5072 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 3 Jul 2025 09:24:47 +0900 Subject: [PATCH 2/5] fix: add test email --- src/app/api/qna/service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/qna/service.ts b/src/app/api/qna/service.ts index cbba527..f86c16d 100644 --- a/src/app/api/qna/service.ts +++ b/src/app/api/qna/service.ts @@ -112,7 +112,8 @@ export class QnaService { return this.tryFunction(() => { return sendEmail({ from: 'test@test.com', - to: receivers, + // to: receivers, + to: 'keyy1315@interplug.co.kr', subject: `[HANASYS お問い合わせ] ${formData.get('title')}`, content: this.generateEmailContent(formData), attachments: attachments, From 56536efdf100aea0780dd9b055bcd5821f152542 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 3 Jul 2025 09:28:16 +0900 Subject: [PATCH 3/5] fix: delete test email --- src/app/api/qna/service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/api/qna/service.ts b/src/app/api/qna/service.ts index f86c16d..cbba527 100644 --- a/src/app/api/qna/service.ts +++ b/src/app/api/qna/service.ts @@ -112,8 +112,7 @@ export class QnaService { return this.tryFunction(() => { return sendEmail({ from: 'test@test.com', - // to: receivers, - to: 'keyy1315@interplug.co.kr', + to: receivers, subject: `[HANASYS お問い合わせ] ${formData.get('title')}`, content: this.generateEmailContent(formData), attachments: attachments, From fd35348694421299cdb51ea0e4930f3fe401e345 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 3 Jul 2025 09:55:52 +0900 Subject: [PATCH 4/5] refactor: update inquiry type list handling and adjust filtering logic in RegistForm --- src/app/api/qna/route.ts | 2 +- src/app/api/qna/service.ts | 16 +++++++++++++--- src/components/inquiry/RegistForm.tsx | 2 +- src/types/Inquiry.ts | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/api/qna/route.ts b/src/app/api/qna/route.ts index 767f9ca..0fc1046 100644 --- a/src/app/api/qna/route.ts +++ b/src/app/api/qna/route.ts @@ -38,7 +38,7 @@ async function getCommonCodeListData(): Promise { if (response instanceof ApiError) { return NextResponse.json({ error: response.message }, { status: response.statusCode }) } - const result = service.getInquiryTypeList(response.data.apiCommCdList) + const result = service.getInquiryTypeList(response.data) return NextResponse.json({ data: result }) } diff --git a/src/app/api/qna/service.ts b/src/app/api/qna/service.ts index cbba527..93774a9 100644 --- a/src/app/api/qna/service.ts +++ b/src/app/api/qna/service.ts @@ -57,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, diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 0011d9e..e6eca3d 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -153,7 +153,7 @@ export default function RegistForm() { 選択してください {commonCodeList - .filter((code) => code.headCd === '204200') + .filter((code) => code.headId === 'QNA_CLS_LRG_CD') .map((code) => (