feature/survey-pdf-mail : 조사매물 pdf 메일 첨부 기능 구현 #94
@ -7,6 +7,7 @@ import { getIronSession } from 'iron-session'
|
|||||||
import { SessionData } from '@/types/Auth'
|
import { SessionData } from '@/types/Auth'
|
||||||
import { sessionOptions } from '@/libs/session'
|
import { sessionOptions } from '@/libs/session'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
|
import { ERROR_MESSAGE } from '@/hooks/useAlertMsg'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {POST} /api/qna/save 문의 저장 API
|
* @api {POST} /api/qna/save 문의 저장 API
|
||||||
@ -37,7 +38,7 @@ async function setQna(request: Request): Promise<NextResponse> {
|
|||||||
const formData = await request.formData()
|
const formData = await request.formData()
|
||||||
const mailResult = await service.sendMail(formData)
|
const mailResult = await service.sendMail(formData)
|
||||||
if (mailResult instanceof ApiError) {
|
if (mailResult instanceof ApiError) {
|
||||||
return NextResponse.json({ error: mailResult.message }, { status: mailResult.statusCode })
|
return NextResponse.json({ error: ERROR_MESSAGE.EMAIL_SEND_ERROR }, { status: HttpStatusCode.InternalServerError })
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await service.tryFunction(() =>
|
const result = await service.tryFunction(() =>
|
||||||
|
|||||||
@ -35,6 +35,9 @@ export class QnaService {
|
|||||||
if (shouldThrowResult) return response
|
if (shouldThrowResult) return response
|
||||||
return this.handleResult(response)
|
return this.handleResult(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (shouldThrowResult) {
|
||||||
|
return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.SERVER_ERROR)
|
||||||
|
}
|
||||||
return this.handleRouteError(error)
|
return this.handleRouteError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useSessionStore } from '@/store/session'
|
|||||||
import { InquiryRequest } from '@/types/Inquiry'
|
import { InquiryRequest } from '@/types/Inquiry'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { CONFIRM_MESSAGE, ERROR_MESSAGE, SUCCESS_MESSAGE, useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg'
|
import { CONFIRM_MESSAGE, SUCCESS_MESSAGE, useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg'
|
||||||
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
|
import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
|
||||||
|
|
||||||
export default function RegistForm() {
|
export default function RegistForm() {
|
||||||
@ -106,12 +106,10 @@ export default function RegistForm() {
|
|||||||
showConfirm(
|
showConfirm(
|
||||||
CONFIRM_MESSAGE.SAVE_INQUIRY_CONFIRM,
|
CONFIRM_MESSAGE.SAVE_INQUIRY_CONFIRM,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
const res = await saveInquiry(formData)
|
||||||
const res = await saveInquiry(formData)
|
if (!isSavingInquiry) {
|
||||||
showSuccessAlert(SUCCESS_MESSAGE.SAVE_SUCCESS)
|
showSuccessAlert(SUCCESS_MESSAGE.SAVE_SUCCESS)
|
||||||
router.push(`/inquiry/${res.qnaNo}`)
|
router.push(`/inquiry/${res.qnaNo}`)
|
||||||
} catch (error) {
|
|
||||||
showErrorAlert(ERROR_MESSAGE.SERVER_ERROR)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => null,
|
() => null,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ interface SubmitFormData {
|
|||||||
reference: string[] | null
|
reference: string[] | null
|
||||||
title: string
|
title: string
|
||||||
contents: string | null
|
contents: string | null
|
||||||
|
srlNo: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormField {
|
interface FormField {
|
||||||
@ -35,7 +36,7 @@ export default function SurveySaleSubmitPopup() {
|
|||||||
|
|
||||||
const { setIsShow } = useSpinnerStore()
|
const { setIsShow } = useSpinnerStore()
|
||||||
const { getCommCode } = useCommCode()
|
const { getCommCode } = useCommCode()
|
||||||
const { surveyDetail, getSubmitTarget } = useSurvey(Number(routeId))
|
const { surveyDetail, getSubmitTarget, isSubmittingSurvey, submitSurvey } = useSurvey(Number(routeId))
|
||||||
const { showErrorAlert, showSuccessAlert, showConfirm } = useAlertMsg()
|
const { showErrorAlert, showSuccessAlert, showConfirm } = useAlertMsg()
|
||||||
|
|
||||||
const [submitData, setSubmitData] = useState<SubmitFormData>({
|
const [submitData, setSubmitData] = useState<SubmitFormData>({
|
||||||
@ -47,6 +48,7 @@ export default function SurveySaleSubmitPopup() {
|
|||||||
reference: null,
|
reference: null,
|
||||||
title: '',
|
title: '',
|
||||||
contents: '',
|
contents: '',
|
||||||
|
srlNo: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [commCodeList, setCommCodeList] = useState<CommCode[]>([])
|
const [commCodeList, setCommCodeList] = useState<CommCode[]>([])
|
||||||
@ -56,6 +58,7 @@ export default function SurveySaleSubmitPopup() {
|
|||||||
const baseUpdate = {
|
const baseUpdate = {
|
||||||
sender: session?.email ?? '',
|
sender: session?.email ?? '',
|
||||||
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
|
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
|
||||||
|
srlNo: surveyDetail?.srlNo ?? null,
|
||||||
}
|
}
|
||||||
/** Admin 제출 폼 데이터 삽입 - 1차 판매점*/
|
/** Admin 제출 폼 데이터 삽입 - 1차 판매점*/
|
||||||
if (session?.role === 'Admin') {
|
if (session?.role === 'Admin') {
|
||||||
@ -106,8 +109,6 @@ export default function SurveySaleSubmitPopup() {
|
|||||||
{ id: 'contents', name: '内容', required: false },
|
{ id: 'contents', name: '内容', required: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
const { submitSurvey, isSubmittingSurvey } = useSurvey(Number(routeId))
|
|
||||||
|
|
||||||
const handleInputChange = (field: keyof SubmitFormData, value: string) => {
|
const handleInputChange = (field: keyof SubmitFormData, value: string) => {
|
||||||
setSubmitData((prev) => ({ ...prev, [field]: value }))
|
setSubmitData((prev) => ({ ...prev, [field]: value }))
|
||||||
}
|
}
|
||||||
@ -140,11 +141,15 @@ export default function SurveySaleSubmitPopup() {
|
|||||||
cc: submitData.reference ?? '',
|
cc: submitData.reference ?? '',
|
||||||
subject: submitData.title,
|
subject: submitData.title,
|
||||||
content: generateEmailContent(),
|
content: generateEmailContent(),
|
||||||
|
surveyPdf: {
|
||||||
|
id: surveyDetail?.id ?? 0,
|
||||||
|
filename: surveyDetail?.srlNo ?? 'hanasys_survey',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm })
|
||||||
if (!isSubmittingSurvey) {
|
if (!isSubmittingSurvey) {
|
||||||
showSuccessAlert(SUCCESS_MESSAGE.SUBMIT_SUCCESS)
|
showSuccessAlert(SUCCESS_MESSAGE.SUBMIT_SUCCESS)
|
||||||
submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm })
|
|
||||||
popupController.setSurveySaleSubmitPopup(false)
|
popupController.setSurveySaleSubmitPopup(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -48,10 +48,12 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
|
|||||||
|
|
||||||
const isSubmit = data.basic.submissionStatus
|
const isSubmit = data.basic.submissionStatus
|
||||||
|
|
||||||
const { deleteSurvey, updateSurvey, isDeletingSurvey, isUpdatingSurvey } = useSurvey(id)
|
const { deleteSurvey, updateSurvey, isDeletingSurvey, isUpdatingSurvey, isSubmittingSurvey, isLoadingSurveyDetail } = useSurvey(id)
|
||||||
const { validateSurveyDetail, createSurvey, isCreatingSurvey } = useSurvey()
|
const { validateSurveyDetail, createSurvey, isCreatingSurvey } = useSurvey()
|
||||||
const { showErrorAlert, showSuccessAlert, showConfirm } = useAlertMsg()
|
const { showErrorAlert, showSuccessAlert, showConfirm } = useAlertMsg()
|
||||||
|
|
||||||
|
const buttonDisabled = isLoadingSurveyDetail || isSubmittingSurvey || isCreatingSurvey || isUpdatingSurvey || isDeletingSurvey
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.isLoggedIn) return
|
if (!session?.isLoggedIn) return
|
||||||
|
|
||||||
@ -237,9 +239,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
|
|||||||
<div className="sale-form-btn-wrap">
|
<div className="sale-form-btn-wrap">
|
||||||
<div className="btn-flex-wrap">
|
<div className="btn-flex-wrap">
|
||||||
<ListButton />
|
<ListButton />
|
||||||
{(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && <EditButton setMode={setMode} />}
|
{(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && <EditButton setMode={setMode} disabled={buttonDisabled} />}
|
||||||
{(permissions.isWriter || (permissions.isReceiver && isSubmit)) && <DeleteButton handleDelete={handleDelete} />}
|
{(permissions.isWriter || (permissions.isReceiver && isSubmit)) && <DeleteButton handleDelete={handleDelete} disabled={buttonDisabled} />}
|
||||||
{!isSubmit && permissions.isSubmiter && <SubmitButton handleSubmit={handleSubmit} />}
|
{!isSubmit && permissions.isSubmiter && <SubmitButton handleSubmit={handleSubmit} disabled={buttonDisabled} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -252,9 +254,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
|
|||||||
<div className="sale-form-btn-wrap">
|
<div className="sale-form-btn-wrap">
|
||||||
<div className="btn-flex-wrap">
|
<div className="btn-flex-wrap">
|
||||||
<ListButton />
|
<ListButton />
|
||||||
<TempButton handleSave={() => handleSave(true, false)} />
|
<TempButton handleSave={() => handleSave(true, false)} disabled={buttonDisabled} />
|
||||||
<SaveButton handleSave={() => handleSave(false, false)} />
|
<SaveButton handleSave={() => handleSave(false, false)} disabled={buttonDisabled} />
|
||||||
{!isSubmit && permissions.isSubmiter && <SubmitButton handleSubmit={handleSubmit} />}
|
{!isSubmit && permissions.isSubmiter && <SubmitButton handleSubmit={handleSubmit} disabled={buttonDisabled} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -274,7 +276,7 @@ const ListButton = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditButton = ({ setMode }: { setMode: (mode: Mode) => void }) => {
|
const EditButton = ({ setMode, disabled }: { setMode: (mode: Mode) => void; disabled: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<div className="btn-bx">
|
<div className="btn-bx">
|
||||||
<button
|
<button
|
||||||
@ -282,6 +284,7 @@ const EditButton = ({ setMode }: { setMode: (mode: Mode) => void }) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode('EDIT')
|
setMode('EDIT')
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
修正<i className="btn-arr"></i>
|
修正<i className="btn-arr"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -289,33 +292,33 @@ const EditButton = ({ setMode }: { setMode: (mode: Mode) => void }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubmitButton = ({ handleSubmit }: { handleSubmit: () => void }) => (
|
const SubmitButton = ({ handleSubmit, disabled }: { handleSubmit: () => void; disabled: boolean }) => (
|
||||||
<div className="btn-bx">
|
<div className="btn-bx">
|
||||||
<button className="btn-frame red icon" onClick={handleSubmit}>
|
<button className="btn-frame red icon" onClick={handleSubmit} disabled={disabled}>
|
||||||
提出<i className="btn-arr"></i>
|
提出<i className="btn-arr"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const DeleteButton = ({ handleDelete }: { handleDelete: () => void }) => (
|
const DeleteButton = ({ handleDelete, disabled }: { handleDelete: () => void; disabled: boolean }) => (
|
||||||
<div className="btn-bx">
|
<div className="btn-bx">
|
||||||
<button className="btn-frame n-blue icon" onClick={handleDelete}>
|
<button className="btn-frame n-blue icon" onClick={handleDelete} disabled={disabled}>
|
||||||
削除<i className="btn-arr"></i>
|
削除<i className="btn-arr"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const SaveButton = ({ handleSave }: { handleSave: () => void }) => (
|
const SaveButton = ({ handleSave, disabled }: { handleSave: () => void; disabled: boolean }) => (
|
||||||
<div className="btn-bx">
|
<div className="btn-bx">
|
||||||
<button className="btn-frame n-blue icon" onClick={handleSave}>
|
<button className="btn-frame n-blue icon" onClick={handleSave} disabled={disabled}>
|
||||||
保存<i className="btn-arr"></i>
|
保存<i className="btn-arr"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const TempButton = ({ handleSave }: { handleSave: () => void }) => (
|
const TempButton = ({ handleSave, disabled }: { handleSave: () => void; disabled: boolean }) => (
|
||||||
<div className="btn-bx">
|
<div className="btn-bx">
|
||||||
<button className="btn-frame n-blue icon" onClick={handleSave}>
|
<button className="btn-frame n-blue icon" onClick={handleSave} disabled={disabled}>
|
||||||
一時保存<i className="btn-arr"></i>
|
一時保存<i className="btn-arr"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useInquiryFilterStore } from '@/store/inquiryFilterStore'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useAlertMsg } from '@/hooks/useAlertMsg'
|
import { useAlertMsg } from '@/hooks/useAlertMsg'
|
||||||
import { CommCode } from '@/types/CommCode'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 문의사항 관련 기능을 제공하는 커스텀 훅
|
* @description 문의사항 관련 기능을 제공하는 커스텀 훅
|
||||||
|
|||||||
@ -3,6 +3,15 @@
|
|||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { Attachment } from 'nodemailer/lib/mailer'
|
import { Attachment } from 'nodemailer/lib/mailer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 이메일 파라미터 인터페이스
|
||||||
|
* @param {string} from 발신자
|
||||||
|
* @param {string | string[]} to 수신자
|
||||||
|
* @param {string | string[]} cc 참조
|
||||||
|
* @param {string} subject 제목
|
||||||
|
* @param {string} content 내용
|
||||||
|
* @param {Attachment[]} attachments 첨부파일
|
||||||
|
*/
|
||||||
interface EmailParams {
|
interface EmailParams {
|
||||||
from: string
|
from: string
|
||||||
to: string | string[]
|
to: string | string[]
|
||||||
@ -10,10 +19,32 @@ interface EmailParams {
|
|||||||
subject: string
|
subject: string
|
||||||
content: string
|
content: string
|
||||||
attachments?: Attachment[]
|
attachments?: Attachment[]
|
||||||
|
surveyPdf?: {
|
||||||
|
id: number
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendEmail({ from, to, cc, subject, content, attachments }: EmailParams): Promise<void> {
|
export async function sendEmail({ from, to, cc, subject, content, attachments, surveyPdf }: EmailParams): Promise<void> {
|
||||||
// Create a transporter using SMTP
|
/**
|
||||||
|
* @description 조사매물 pdf blob 및 buffer 생성
|
||||||
|
*/
|
||||||
|
let surveyPdfBuffer: Buffer | null = null
|
||||||
|
if (surveyPdf) {
|
||||||
|
const resp = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/survey-sales/${surveyPdf.id}?isPdf=true`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const pdfBlob = await resp.blob()
|
||||||
|
surveyPdfBuffer = Buffer.from(await pdfBlob.arrayBuffer())
|
||||||
|
}
|
||||||
|
const surveyPdfAttachment = surveyPdfBuffer ? [{ filename: '[HANASYS現地調査]' + surveyPdf?.filename + '.pdf', content: surveyPdfBuffer }] : []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SMTP 트랜스포터 생성
|
||||||
|
*/
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: process.env.SMTP_HOST,
|
host: process.env.SMTP_HOST,
|
||||||
port: Number(process.env.SMTP_PORT),
|
port: Number(process.env.SMTP_PORT),
|
||||||
@ -22,21 +53,17 @@ export async function sendEmail({ from, to, cc, subject, content, attachments }:
|
|||||||
tls: { rejectUnauthorized: false },
|
tls: { rejectUnauthorized: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Email options
|
/**
|
||||||
|
* @description 이메일 옵션
|
||||||
|
*/
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from,
|
from,
|
||||||
to: Array.isArray(to) ? to.join(', ') : to,
|
to: Array.isArray(to) ? to.join(', ') : to,
|
||||||
cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined,
|
cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined,
|
||||||
subject,
|
subject,
|
||||||
html: content,
|
html: content,
|
||||||
attachments: attachments || [],
|
attachments: surveyPdf ? surveyPdfAttachment : attachments || [],
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await transporter.sendMail(mailOptions)
|
||||||
// Send email
|
|
||||||
await transporter.sendMail(mailOptions)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending email:', error)
|
|
||||||
throw new Error('Failed to send email')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user