diff --git a/.env.development b/.env.development index b446848..a62742c 100644 --- a/.env.development +++ b/.env.development @@ -2,10 +2,11 @@ NEXT_PUBLIC_RUN_MODE=development # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.65:3000 +NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 +# NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 @@ -17,4 +18,11 @@ DB_HOST=202.218.61.226 DB_USER=readonly DB_PASSWORD=aAjmFW12iHKW84l1 DB_DATABASE=qpartners -DB_PORT=3306 \ No newline at end of file +DB_PORT=3306 + +SMTP_HOST=autodiscover.qcells.com +SMTP_PORT=25 +SMTP_SECURE=false +SMTP_USER=hss404.u021@cleverse.dev +SMTP_PASSWORD=0000 +SMTP_FROM=qsalesplatform@qcells.com \ No newline at end of file diff --git a/.env.localhost b/.env.localhost index ba9def8..2935f56 100644 --- a/.env.localhost +++ b/.env.localhost @@ -2,10 +2,11 @@ NEXT_PUBLIC_RUN_MODE=local # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.65:3000 +NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 +# NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 @@ -16,4 +17,11 @@ DB_HOST=202.218.61.226 DB_USER=readonly DB_PASSWORD=aAjmFW12iHKW84l1 DB_DATABASE=qpartners -DB_PORT=3306 \ No newline at end of file +DB_PORT=3306 + +SMTP_HOST=autodiscover.qcells.com +SMTP_PORT=25 +SMTP_SECURE=false +SMTP_USER=hss404.u021@cleverse.dev +SMTP_PASSWORD=0000 +SMTP_FROM=qsalesplatform@qcells.com diff --git a/.env.production b/.env.production index ce308e8..72caa0d 100644 --- a/.env.production +++ b/.env.production @@ -3,7 +3,8 @@ NEXT_PUBLIC_RUN_MODE=production NEXT_PUBLIC_API_URL=http://1.248.227.176:3000 #qsp 로그인 api -NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 +# NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 +NEXT_PUBLIC_QSP_API_URL=https://jp.qsalesplatform.com #1:1문의 api NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 @@ -13,4 +14,11 @@ DB_HOST=202.218.61.226 DB_USER=readonly DB_PASSWORD=aAjmFW12iHKW84l1 DB_DATABASE=qpartners -DB_PORT=3306 \ No newline at end of file +DB_PORT=3306 + +SMTP_HOST=autodiscover.qcells.com +SMTP_PORT=25 +SMTP_SECURE=true +SMTP_USER=hss404.u021@cleverse.dev +SMTP_PASSWORD=0000 +SMTP_FROM=qsalesplatform@qcells.com \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 73dbbf1..6a3c17f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "mssql": "^11.0.1", "mysql2": "^3.14.1", "next": "15.2.4", + "nodemailer": "^7.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-to-pdf": "^2.0.0", @@ -3700,6 +3701,15 @@ "license": "MIT", "optional": true }, + "node_modules/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", diff --git a/package.json b/package.json index 53ed0e8..5b0953b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@prisma/client": "^6.7.0", "@tanstack/react-query": "^5.71.0", "@tanstack/react-query-devtools": "^5.71.0", + "@types/nodemailer": "^6.4.17", "axios": "^1.8.4", "env-cmd": "^10.1.0", "iron-session": "^8.0.4", @@ -25,6 +26,7 @@ "mssql": "^11.0.1", "mysql2": "^3.14.1", "next": "15.2.4", + "nodemailer": "^7.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-to-pdf": "^2.0.0", diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 4a77ad6..af2c3fd 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -83,6 +83,10 @@ export default function Login() { loginId: account.loginId, pwd: account.pwd, }) + // const { data } = await axiosInstance(`${process.env.NEXT_PUBLIC_QSP_API_URL}`).post(`/api/user/login`, { + // loginId: account.loginId, + // pwd: account.pwd, + // }) return data }, diff --git a/src/components/pdf/SurveySaleDownloadPdf.tsx b/src/components/pdf/SurveySaleDownloadPdf.tsx index 0802308..02ba732 100644 --- a/src/components/pdf/SurveySaleDownloadPdf.tsx +++ b/src/components/pdf/SurveySaleDownloadPdf.tsx @@ -2,22 +2,29 @@ import { useEffect, useRef } from 'react' import generatePDF, { Margin, Resolution } from 'react-to-pdf' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { useSurvey } from '@/hooks/useSurvey' import { radioEtcData, roofMaterial, selectBoxOptions, supplementaryFacilities } from '../survey-sale/detail/RoofForm' +import { useSpinnerStore } from '@/store/spinnerStore' export default function SurveySaleDownloadPdf() { const params = useParams() const id = params.id + const router = useRouter() const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id)) - - useEffect(() => { - if (isLoadingSurveyDetail) return - handleDownPdf() - }, [surveyDetail, isLoadingSurveyDetail]) + const { setIsShow } = useSpinnerStore() const targetRef = useRef(null) + const isGeneratedRef = useRef(false) + + useEffect(() => { + setIsShow(true) + if (isLoadingSurveyDetail || !surveyDetail || isGeneratedRef.current) return + isGeneratedRef.current = true + handleDownPdf() + }, [surveyDetail?.id, isLoadingSurveyDetail]) + const handleDownPdf = () => { const options = { method: 'open' as const, @@ -41,14 +48,31 @@ export default function SurveySaleDownloadPdf() { }, } - generatePDF(targetRef, options) - // generatePDF(targetRef, { filename: 'page.pdf' }) + generatePDF(targetRef, options).then(() => { + setIsShow(false) + router.push(`/survey-sale/${id}`) + }) } + const supplementList = supplementaryFacilities + .filter((facility) => surveyDetail?.detailInfo?.supplementaryFacilities?.includes(facility.id.toString())) + .map((facility) => facility.name) + return ( <> - {/* */} -
-
+
+
HWJ 現地調査シート1/2 @@ -59,13 +83,13 @@ export default function SurveySaleDownloadPdf() { 現地明登施工店名

- {surveyDetail?.store} + {surveyDetail?.store ?? '-'}

現地阴買日

- {surveyDetail?.investigationDate} + {surveyDetail?.investigationDate ?? '-'}

@@ -101,7 +125,7 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {surveyDetail?.customerName} + {surveyDetail?.customerName ?? '-'} @@ -168,7 +192,7 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {surveyDetail?.detailInfo?.contractCapacity} + {surveyDetail?.detailInfo?.contractCapacity ?? '-'} - {surveyDetail?.detailInfo?.retailCompany} + {surveyDetail?.detailInfo?.retailCompany ?? '-'} @@ -225,13 +249,11 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {supplementaryFacilities - .filter((facility) => surveyDetail?.detailInfo?.supplementaryFacilities?.includes(facility.id.toString())) - .map((facility) => facility.name) - .join(', ')} - {surveyDetail?.detailInfo?.supplementaryFacilitiesEtc - ? `, (その他) ${surveyDetail?.detailInfo?.supplementaryFacilitiesEtc}` - : '-'} + {supplementList === null && surveyDetail?.detailInfo?.supplementaryFacilitiesEtc === null + ? '-' + : surveyDetail?.detailInfo?.supplementaryFacilitiesEtc + ? `${supplementList.join(', ')}, ${surveyDetail?.detailInfo?.supplementaryFacilitiesEtc}` + : supplementList.join(', ')} @@ -261,11 +283,16 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - { - selectBoxOptions.installationSystem.find((system) => system.id.toString() === surveyDetail?.detailInfo?.installationSystem) - ?.name - } - {surveyDetail?.detailInfo?.installationSystemEtc ? `, (その他) ${surveyDetail?.detailInfo?.installationSystemEtc}` : '-'} + {/* {selectBoxOptions.installationSystem.find ((system) => system.id.toString() === surveyDetail?.detailInfo?.installationSystem) + ?.name ?? surveyDetail?.detailInfo?.installationSystemEtc !== null + ? `${surveyDetail?.detailInfo?.installationSystemEtc}` + : '-'} */} + {surveyDetail?.detailInfo?.installationSystem === null && surveyDetail?.detailInfo?.installationSystemEtc === null + ? '-' + : surveyDetail?.detailInfo?.installationSystemEtc + ? `${surveyDetail?.detailInfo?.installationSystemEtc}` + : selectBoxOptions.installationSystem.find((system) => system.id.toString() === surveyDetail?.detailInfo?.installationSystem) + ?.name} @@ -330,11 +357,13 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {roofMaterial - .filter((material) => surveyDetail?.detailInfo?.roofMaterial?.includes(material.id.toString())) - .map((material) => material.name) - .join(', ')} - {surveyDetail?.detailInfo?.roofMaterialEtc ? `, (その他) ${surveyDetail?.detailInfo?.roofMaterialEtc}` : '-'} + {surveyDetail?.detailInfo?.roofMaterial === null && surveyDetail?.detailInfo?.roofMaterialEtc === null + ? '-' + : roofMaterial + .filter((material) => surveyDetail?.detailInfo?.roofMaterial?.includes(material.id.toString())) + .map((material) => material.name) + .join(', ')} + {surveyDetail?.detailInfo?.roofMaterialEtc ? `, ${surveyDetail?.detailInfo?.roofMaterialEtc}` : ''} - {selectBoxOptions.roofShape.find((shape) => shape.id.toString() === surveyDetail?.detailInfo?.roofShape)?.name} + {selectBoxOptions.roofShape.find((shape) => shape.id.toString() === surveyDetail?.detailInfo?.roofShape)?.name ?? + (surveyDetail?.detailInfo?.roofShapeEtc ? ` ${surveyDetail?.detailInfo?.roofShapeEtc}` : '-')} @@ -390,7 +420,7 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {`${surveyDetail?.detailInfo?.roofSlope} 寸`} + {surveyDetail?.detailInfo?.roofSlope ? `${surveyDetail?.detailInfo?.roofSlope} 寸` : '-'} - {`${surveyDetail?.detailInfo?.houseStructure ? '木製' : '(その他)'} ${surveyDetail?.detailInfo?.houseStructureEtc}`} + {radioEtcData.houseStructure.find((structure) => structure.id.toString() === surveyDetail?.detailInfo?.houseStructure)?.label ?? + (surveyDetail?.detailInfo?.houseStructureEtc ? ` ${surveyDetail?.detailInfo?.houseStructureEtc}` : '-')} @@ -447,8 +478,12 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > + {/* {surveyDetail?.detailInfo?.rafterMaterial === null && surveyDetail?.detailInfo?.rafterMaterialEtc === null + ? '-' + : radioEtcData.rafterMaterial.find((material) => material.id.toString() === surveyDetail?.detailInfo?.rafterMaterial)?.label ?? + surveyDetail?.detailInfo?.rafterMaterialEtc} */} {radioEtcData.rafterMaterial.find((material) => material.id.toString() === surveyDetail?.detailInfo?.rafterMaterial)?.label ?? - (surveyDetail?.detailInfo?.rafterMaterialEtc ? `(その他) ${surveyDetail?.detailInfo?.rafterMaterialEtc}` : '-')} + (surveyDetail?.detailInfo?.rafterMaterialEtc ? ` ${surveyDetail?.detailInfo?.rafterMaterialEtc}` : '-')} {selectBoxOptions.rafterSize.find((size) => size.id.toString() === surveyDetail?.detailInfo?.rafterSize)?.name ?? - (surveyDetail?.detailInfo?.rafterSizeEtc ? `(その他) ${surveyDetail?.detailInfo?.rafterSizeEtc}` : '-')} + (surveyDetail?.detailInfo?.rafterSizeEtc ? ` ${surveyDetail?.detailInfo?.rafterSizeEtc}` : '-')} @@ -507,7 +542,7 @@ export default function SurveySaleDownloadPdf() { }} > {selectBoxOptions.rafterPitch.find((pitch) => pitch.id.toString() === surveyDetail?.detailInfo?.rafterPitch)?.name ?? - (surveyDetail?.detailInfo?.rafterPitchEtc ? `(その他) ${surveyDetail?.detailInfo?.rafterPitchEtc}` : '-')} + (surveyDetail?.detailInfo?.rafterPitchEtc ? ` ${surveyDetail?.detailInfo?.rafterPitchEtc}` : '-')} {selectBoxOptions.openFieldPlateKind.find((kind) => kind.id.toString() === surveyDetail?.detailInfo?.openFieldPlateKind)?.name ?? - (surveyDetail?.detailInfo?.openFieldPlateKindEtc ? `(その他) ${surveyDetail?.detailInfo?.openFieldPlateKindEtc}` : '-')} + (surveyDetail?.detailInfo?.openFieldPlateKindEtc ? `${surveyDetail?.detailInfo?.openFieldPlateKindEtc}` : '-')} {radioEtcData.waterproofMaterial.find((material) => material.id.toString() === surveyDetail?.detailInfo?.waterproofMaterial) - ?.label ?? - (surveyDetail?.detailInfo?.waterproofMaterialEtc ? `(その他) ${surveyDetail?.detailInfo?.waterproofMaterialEtc}` : '-')} + ?.label ?? (surveyDetail?.detailInfo?.waterproofMaterialEtc ? ` ${surveyDetail?.detailInfo?.waterproofMaterialEtc}` : '-')} @@ -690,7 +724,7 @@ export default function SurveySaleDownloadPdf() { radioEtcData.insulationPresence.find((presence) => presence.id.toString() === surveyDetail?.detailInfo?.insulationPresence) ?.label } - {surveyDetail?.detailInfo?.insulationPresenceEtc ? `(その他) ${surveyDetail?.detailInfo?.insulationPresenceEtc}` : '-'} + {surveyDetail?.detailInfo?.insulationPresenceEtc ? `, ${surveyDetail?.detailInfo?.insulationPresenceEtc}` : ''} @@ -720,8 +754,8 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - {selectBoxOptions.structureOrder.find((order) => order.id.toString() === surveyDetail?.detailInfo?.structureOrder)?.name ?? - (surveyDetail?.detailInfo?.structureOrderEtc ? `(その他) ${surveyDetail?.detailInfo?.structureOrderEtc}` : '-')} + {radioEtcData.structureOrder.find((order) => order.id.toString() === surveyDetail?.detailInfo?.structureOrder)?.label ?? + (surveyDetail?.detailInfo?.structureOrderEtc ? `${surveyDetail?.detailInfo?.structureOrderEtc}` : '-')} @@ -759,14 +793,12 @@ export default function SurveySaleDownloadPdf() { boxSizing: 'border-box', }} > - { - selectBoxOptions.installationAvailability.find( - (availability) => availability.id.toString() === surveyDetail?.detailInfo?.installationAvailability, - )?.name - } - {surveyDetail?.detailInfo?.installationAvailabilityEtc - ? `(その他) ${surveyDetail?.detailInfo?.installationAvailabilityEtc}` - : '-'} + {surveyDetail?.detailInfo?.installationAvailability === null && surveyDetail.detailInfo?.installationAvailabilityEtc === null + ? '-' + : selectBoxOptions.installationAvailability.find( + (availability) => availability.id.toString() === surveyDetail?.detailInfo?.installationAvailability, + )?.name} + {surveyDetail?.detailInfo?.installationAvailabilityEtc ? `, ${surveyDetail?.detailInfo?.installationAvailabilityEtc}` : ''} diff --git a/src/components/popup/SurveySaleSubmitPopup.tsx b/src/components/popup/SurveySaleSubmitPopup.tsx index 426d49c..7a50406 100644 --- a/src/components/popup/SurveySaleSubmitPopup.tsx +++ b/src/components/popup/SurveySaleSubmitPopup.tsx @@ -6,12 +6,14 @@ import { useEffect, useState } from 'react' import { useSessionStore } from '@/store/session' import { useCommCode } from '@/hooks/useCommCode' import { CommCode } from '@/types/CommCode' +import { sendEmail } from '@/libs/mailer' +import { useSpinnerStore } from '@/store/spinnerStore' interface SubmitFormData { saleBase: string | null store: string sender: string - receiver: string[] + receiver: string[] | string reference: string | null title: string contents: string @@ -29,6 +31,7 @@ export default function SurveySaleSubmitPopup() { const params = useParams() const routeId = params.id + const { setIsShow } = useSpinnerStore() const [commCodeList, setCommCodeList] = useState([]) const { getCommCode } = useCommCode() @@ -86,10 +89,25 @@ export default function SurveySaleSubmitPopup() { const handleSubmit = () => { if (validateData(submitData)) { window.neoConfirm('送信しますか? 送信後は変更・修正することはできません。', () => { + setIsShow(true) submitSurvey({ targetId: submitData.store }) + sendEmail({ + to: submitData.receiver, + subject: submitData.title, + content: submitData.contents, + }) + .then(() => { if (!isSubmittingSurvey) { popupController.setSurveySaleSubmitPopup(false) } + }) + .catch((error) => { + console.error('Error sending email:', error) + alert('メール送信に失敗しました。') + }) + .finally(() => { + setIsShow(false) + }) }) } } diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index d0ca7bf..93d314e 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -1,13 +1,14 @@ 'use client' import { useSurvey } from '@/hooks/useSurvey' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { useEffect } from 'react' import DetailForm from './DetailForm' export default function DataTable() { const params = useParams() const id = params.id + const router = useRouter() useEffect(() => { if (Number.isNaN(Number(id))) { @@ -67,7 +68,7 @@ export default function DataTable() { ダウンロード - diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 3e8dcc2..4760808 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,7 +1,14 @@ import { useState } from 'react' import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' -type RadioEtcKeys = 'houseStructure' | 'rafterMaterial' | 'waterproofMaterial' | 'insulationPresence' | 'rafterDirection' | 'leakTrace' +type RadioEtcKeys = + | 'structureOrder' + | 'houseStructure' + | 'rafterMaterial' + | 'waterproofMaterial' + | 'insulationPresence' + | 'rafterDirection' + | 'leakTrace' type SelectBoxKeys = | 'installationSystem' | 'constructionYear' @@ -9,7 +16,6 @@ type SelectBoxKeys = | 'rafterPitch' | 'rafterSize' | 'openFieldPlateKind' - | 'structureOrder' | 'installationAvailability' export const supplementaryFacilities = [ @@ -115,24 +121,6 @@ export const selectBoxOptions: Record = { + structureOrder: [ + { + id: 1, + label: '屋根材 - 防水材 - 屋根の基礎 - 垂木', //지붕재 방수재 지붕의기초 서까래 + }, + ], houseStructure: [ { id: 1, @@ -438,7 +432,7 @@ export default function RoofForm(props: {
{/* 지붕 구조의 순서 */}
屋根構造の順序
- +
{/* 지붕 제품명 설치 가능 여부 확인 */} diff --git a/src/config/config.local.ts b/src/config/config.local.ts index ac02c97..8fbf68b 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -1,7 +1,7 @@ import getConfigs from '@/config/config.common' // 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 local 환경에 맞는 값을 지정합니다.) -const baseUrl = 'http://localhost:3000' +const baseUrl = 'http://172.30.1.23:3000' const mode = 'local' // 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다. diff --git a/src/libs/mailer.ts b/src/libs/mailer.ts new file mode 100644 index 0000000..0636df2 --- /dev/null +++ b/src/libs/mailer.ts @@ -0,0 +1,50 @@ +'use server' + +import nodemailer from 'nodemailer' + +interface EmailParams { + to: string | string[] + cc?: string | string[] + subject: string + content: string +} + +export async function sendEmail({ to, cc, subject, content }: EmailParams): Promise { + // Create a transporter using SMTP + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), + secure: process.env.SMTP_SECURE === 'true', + requireTLS: true, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + }) + + // Email options + const mailOptions = { + from: process.env.SMTP_USER, + to: Array.isArray(to) ? to.join(', ') : to, + cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined, + subject, + html: content, + } + + try { + // Send email + await transporter.sendMail(mailOptions) + } catch (error) { + console.error('Error sending email:', error) + throw new Error('Failed to send email') + } +} + +async function sendEmailTest() { + await sendEmail({ + to: 'test@test.com', + cc: 'test2@test.com', + subject: 'Test Email', + content: '

Hello

This is a test email.

', + }) +}