feat: surveySale pdf 생성 방식 변경

- pdf 생성 시 response에 blob으로 반환하도록 방식 변경
- 제출 시 메일 href pdf 생성 페이지 링크에서 request url로 변경
This commit is contained in:
Dayoung 2025-06-30 17:45:08 +09:00
parent c57bf8eebd
commit 5120764108
13 changed files with 700 additions and 1129 deletions

View File

@ -7,6 +7,7 @@ import { HttpStatusCode } from 'axios'
import { loggerWrapper } from '@/libs/api-wrapper'
import { SurveySalesService } from '../service'
import { ApiError } from 'next/dist/server/api-utils'
import { SurveyBasicInfo } from '@/types/Survey'
/**
* @api {GET} /api/survey-sales/:id API
@ -17,7 +18,7 @@ import { ApiError } from 'next/dist/server/api-utils'
* @apiParam {Number} id PRIMARY KEY ID (required)
* @apiParam {Boolean} isPdf pdf (optional, default: false)
*
* @apiSuccess {Object} SurveySaleBasicInfo
* @apiSuccess {Object} SurveySaleBasicInfo | Blob
*
* @apiError {Number} 401 ( )
* @apiError {Number} 403
@ -56,6 +57,18 @@ async function getSurveySaleDetail(request: NextRequest): Promise<NextResponse>
if (result instanceof ApiError) {
return NextResponse.json({ error: result.message }, { status: result.statusCode })
}
if (isPdf) {
const pdfBlob = await service.createSurveyPdf(result as SurveyBasicInfo)
if (pdfBlob instanceof ApiError) {
return NextResponse.json({ error: pdfBlob.message }, { status: pdfBlob.statusCode })
}
return new NextResponse(pdfBlob, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename=${result.SRL_NO}.pdf; filename*=UTF-8''${encodeURIComponent(result.SRL_NO)}.pdf`,
},
})
}
return NextResponse.json(result)
}

View File

@ -1,11 +1,14 @@
import { prisma } from '@/libs/prisma'
import { SurveyBasicInfo, SurveyRegistRequest, SurveySearchParams } from '@/types/Survey'
import { convertToSnakeCase } from '@/utils/common-utils'
import { convertToCamelCase, convertToSnakeCase } from '@/utils/common-utils'
import { Prisma } from '@prisma/client'
import type { SessionData } from '@/types/Auth'
import { HttpStatusCode } from 'axios'
import { ApiError } from 'next/dist/server/api-utils'
import { ERROR_MESSAGE } from '@/hooks/useAlertMsg'
import SurveySalePdf from '@/components/pdf/SurveySalePdf'
import React from 'react'
import { pdf, Document } from '@react-pdf/renderer'
type WhereCondition = {
AND: any[]
@ -115,6 +118,7 @@ export class SurveySalesService {
where.OR = [
{ AND: [{ STORE_ID: { equals: this.session?.storeId } }] },
{ AND: [{ SUBMISSION_TARGET_ID: { equals: this.session?.storeId } }, { SUBMISSION_STATUS: { equals: true } }] },
{ AND: [{ SUBMISSION_TARGET_NM: { equals: this.session?.storeNm } }, { SUBMISSION_STATUS: { equals: true } }] },
]
break
case 'Admin_Sub':
@ -130,6 +134,9 @@ export class SurveySalesService {
{ SUBMISSION_STATUS: { equals: true } },
],
},
{
AND: [{ SUBMISSION_TARGET_NM: { equals: this.session?.storeNm } }, { SUBMISSION_STATUS: { equals: true } }],
},
]
break
case 'Builder':
@ -259,7 +266,7 @@ export class SurveySalesService {
* @param {number} id ID
* @returns {Promise<SurveyBasicInfo>}
*/
async fetchSurvey(id: number, isPdf: boolean): Promise<SurveyBasicInfo | ApiError> {
async fetchSurvey(id: number, isPdf: boolean): Promise<SurveyBasicInfo | ApiError | Blob> {
// @ts-ignore
const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({
where: { ID: id },
@ -275,6 +282,23 @@ export class SurveySalesService {
return result
}
/**
* @description PDF
* @param {SurveyBasicInfo} survey
* @returns {Promise<Blob>} PDF Blob
*/
async createSurveyPdf(survey: SurveyBasicInfo): Promise<Blob | ApiError> {
const convertedSurvey = convertToCamelCase(survey) as SurveyBasicInfo
const content = React.createElement(Document, null, React.createElement(SurveySalePdf, { survey: convertedSurvey }))
try {
const pdfBlob = await pdf(content).toBlob()
const arrayBuffer = await pdfBlob.arrayBuffer()
return new Blob([arrayBuffer], { type: 'application/pdf' })
} catch (error) {
return new ApiError(HttpStatusCode.InternalServerError, ERROR_MESSAGE.PDF_GENERATION_ERROR)
}
}
/**
* @description
* @param {number} id ID
@ -364,9 +388,9 @@ export class SurveySalesService {
if (!survey || !session) return false
const roleChecks = {
T01: () => this.checkT01Role(survey),
Admin: () => this.checkAdminRole(survey, session.storeId),
Admin_Sub: () => this.checkAdminSubRole(survey, session.storeId),
T01: () => this.checkT01Role(survey, session.userId),
Admin: () => this.checkAdminRole(survey, session.storeId, session.storeNm),
Admin_Sub: () => this.checkAdminSubRole(survey, session.storeId, session.storeNm),
Partner: () => this.checkPartnerOrBuilderRole(survey, session.builderId),
Builder: () => this.checkPartnerOrBuilderRole(survey, session.builderId),
}
@ -381,8 +405,8 @@ export class SurveySalesService {
* @param {any} survey
* @returns {boolean} (true: , false: )
*/
private checkT01Role(survey: any): boolean {
if (survey.REPRESENTATIVE_ID === this.session?.userId) {
private checkT01Role(survey: any, userId: string | null): boolean {
if (survey.REPRESENTATIVE_ID === userId) {
return true
}
return survey.SRL_NO !== '一時保存'
@ -396,9 +420,11 @@ export class SurveySalesService {
* @param {string | null} storeId ID
* @returns {boolean} (true: , false: )
*/
private checkAdminRole(survey: any, storeId: string | null): boolean {
private checkAdminRole(survey: any, storeId: string | null, storeNm: string | null): boolean {
if (!storeId) return false
return survey.SUBMISSION_STATUS ? survey.SUBMISSION_TARGET_ID === storeId || survey.STORE_ID === storeId : survey.STORE_ID === storeId
return survey.SUBMISSION_STATUS
? survey.SUBMISSION_TARGET_ID === storeId || survey.SUBMISSION_TARGET_NM === storeNm || survey.STORE_ID === storeId
: survey.STORE_ID === storeId
}
/**
@ -409,10 +435,12 @@ export class SurveySalesService {
* @param {string | null} storeId ID
* @returns {boolean} (true: , false: )
*/
private checkAdminSubRole(survey: any, storeId: string | null): boolean {
private checkAdminSubRole(survey: any, storeId: string | null, storeNm: string | null): boolean {
if (!storeId) return false
return survey.SUBMISSION_STATUS
? survey.SUBMISSION_TARGET_ID === storeId || (survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID)
? survey.SUBMISSION_TARGET_ID === storeId ||
survey.SUBMISSION_TARGET_NM === storeNm ||
(survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID)
: survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID
}

View File

@ -1,9 +0,0 @@
import SurveySaleDownloadPdf from '@/components/pdf/SurveySaleDownloadPdf'
export default function page() {
return (
<>
<SurveySaleDownloadPdf />
</>
)
}

View File

@ -1,849 +0,0 @@
'use client'
import { useEffect, useRef } from 'react'
import generatePDF, { Margin, Resolution } from 'react-to-pdf'
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'
import { useSessionStore } from '@/store/session'
import { ERROR_MESSAGE, SUCCESS_MESSAGE, useAlertMsg } from '@/hooks/useAlertMsg'
export default function SurveySaleDownloadPdf() {
const params = useParams()
const id = params.id
const router = useRouter()
const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id), true)
const { showErrorAlert, showSuccessAlert } = useAlertMsg()
const { setIsShow } = useSpinnerStore()
const { session } = useSessionStore()
const targetRef = useRef<HTMLDivElement>(null)
const isGeneratedRef = useRef(false)
/** 페이지 랜더링 이후 PDF 생성 */
useEffect(() => {
if (isLoadingSurveyDetail || isGeneratedRef.current) return
isGeneratedRef.current = true
handleDownPdf()
}, [surveyDetail?.id, isLoadingSurveyDetail])
const handleDownPdf = () => {
setIsShow(true)
const options = {
method: 'open' as const,
resolution: Resolution.HIGH,
page: {
margin: Margin.SMALL,
format: 'A4',
orientation: 'portrait' as const,
},
canvas: {
mimeType: 'image/png' as const,
qualityRatio: 1,
},
overrides: {
pdf: {
compress: true,
},
canvas: {
useCORS: true,
},
},
}
/** PDF 생성 이후 세션 여부에 따른 라우팅 처리 */
generatePDF(targetRef, options)
.then(() => {
setIsShow(false)
if (session?.isLoggedIn) {
router.replace(`/survey-sale/${id}`)
} else {
router.replace('/')
}
showSuccessAlert(SUCCESS_MESSAGE.PDF_GENERATION_SUCCESS)
})
.catch((error: any) => {
console.error('❌ PDF GENERATION ERROR', error)
showErrorAlert(ERROR_MESSAGE.PDF_GENERATION_ERROR)
})
}
return (
<>
<div
ref={targetRef}
style={{
width: '794px',
minHeight: '1123px',
transform: 'scale(1.0)',
transformOrigin: 'top left',
padding: '20px',
boxSizing: 'border-box',
backgroundColor: '#fff',
fontSize: '12px',
}}
>
<div>
<div style={{ padding: '20px 20px 50px', borderBottom: '2px solid #2E3A59' }}>
<div style={{ float: 'left', verticalAlign: 'middle', fontSize: '18px', color: '#101010', fontWeight: 600, fontFamily: 'M-Gothic' }}>
HWJ 調1/2
</div>
<div style={{ float: 'right', overflow: 'hidden' }}>
<div style={{ float: 'left', marginRight: '20px' }}>
<p style={{ margin: 0, padding: 0, fontSize: '13px', color: '#101010', fontWeight: 500, fontFamily: 'M-Gothic', textAlign: 'right' }}>
</p>
<p style={{ margin: 0, padding: 0, fontSize: '13px', color: '#FF5656', fontWeight: 400, fontFamily: 'M-Gothic' }}>
{surveyDetail?.store ?? '-'}
</p>
</div>
<div style={{ float: 'right' }}>
<p style={{ margin: 0, padding: 0, fontSize: '13px', color: '#101010', fontWeight: 500, fontFamily: 'M-Gothic' }}></p>
<p style={{ margin: 0, padding: 0, fontSize: '13px', color: '#FF5656', fontWeight: 400, fontFamily: 'M-Gothic' }}>
{surveyDetail?.investigationDate ?? '-'}
</p>
</div>
</div>
</div>
<div style={{ padding: '24px 20px 12px' }}>
<table
style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontFamily: 'M-Gothic', WebkitPrintColorAdjust: 'exact' }}
>
<tbody>
<tr>
<th
style={{
padding: '10px',
width: '73px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.customerName ?? '-'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '73px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.postCode ? `(${surveyDetail?.postCode}) ${surveyDetail?.address} ${surveyDetail?.addressDetail}` : '-'}
</td>
</tr>
</tbody>
</table>
</div>
<div style={{ padding: '12px 20px 12px' }}>
<div style={{ fontSize: '14px', fontFamily: 'M-Gothic', color: '#101010', fontWeight: 500, marginBottom: '10px' }}></div>
<table
style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontFamily: 'M-Gothic', WebkitPrintColorAdjust: 'exact' }}
>
<tbody>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.contractCapacity ?? '-'}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.retailCompany ?? '-'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.supplementaryFacilities
? supplementaryFacilities
.filter((facility) => surveyDetail?.detailInfo?.supplementaryFacilities?.includes(facility.id.toString()))
.map((facility) => facility.name)
.join(', ') +
(surveyDetail?.detailInfo?.supplementaryFacilitiesEtc ? `, ${surveyDetail?.detailInfo?.supplementaryFacilitiesEtc}` : '')
: surveyDetail?.detailInfo?.supplementaryFacilitiesEtc
? `${surveyDetail?.detailInfo?.supplementaryFacilitiesEtc}`
: '-'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{/* {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}
</td>
</tr>
</tbody>
</table>
</div>
<div style={{ padding: '12px 20px 12px' }}>
<div style={{ fontSize: '14px', fontFamily: 'M-Gothic', color: '#101010', fontWeight: 500, marginBottom: '10px' }}></div>
<table
style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontFamily: 'M-Gothic', WebkitPrintColorAdjust: 'exact' }}
>
<tbody>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.constructionYear === '1'
? '新築'
: surveyDetail?.detailInfo?.constructionYearEtc
? `既築 (${surveyDetail?.detailInfo?.constructionYear}年)`
: '-'}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{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}` : ''}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{selectBoxOptions.roofShape.find((shape) => shape.id.toString() === surveyDetail?.detailInfo?.roofShape)?.name ??
(surveyDetail?.detailInfo?.roofShapeEtc ? ` ${surveyDetail?.detailInfo?.roofShapeEtc}` : '-')}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.roofSlope ? `${surveyDetail?.detailInfo?.roofSlope}` : '-'}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{radioEtcData.houseStructure.find((structure) => structure.id.toString() === surveyDetail?.detailInfo?.houseStructure)?.label ??
(surveyDetail?.detailInfo?.houseStructureEtc ? ` ${surveyDetail?.detailInfo?.houseStructureEtc}` : '-')}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
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}` : '-')}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{selectBoxOptions.rafterSize.find((size) => size.id.toString() === surveyDetail?.detailInfo?.rafterSize)?.name ??
(surveyDetail?.detailInfo?.rafterSizeEtc ? ` ${surveyDetail?.detailInfo?.rafterSizeEtc}` : '-')}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{selectBoxOptions.rafterPitch.find((pitch) => pitch.id.toString() === surveyDetail?.detailInfo?.rafterPitch)?.name ??
(surveyDetail?.detailInfo?.rafterPitchEtc ? ` ${surveyDetail?.detailInfo?.rafterPitchEtc}` : '-')}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{radioEtcData.rafterDirection.find((direction) => direction.id.toString() === surveyDetail?.detailInfo?.rafterDirection)?.label ??
'-'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{selectBoxOptions.openFieldPlateKind.find((kind) => kind.id.toString() === surveyDetail?.detailInfo?.openFieldPlateKind)?.name ??
(surveyDetail?.detailInfo?.openFieldPlateKindEtc ? `${surveyDetail?.detailInfo?.openFieldPlateKindEtc}` : '-')}
</td>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={3}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.openFieldPlateThickness ? `${surveyDetail?.detailInfo?.openFieldPlateThickness}mm` : '-'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={5}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{surveyDetail?.detailInfo?.leakTrace ? 'あり' : 'なし'}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={5}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{radioEtcData.waterproofMaterial.find((material) => material.id.toString() === surveyDetail?.detailInfo?.waterproofMaterial)
?.label ?? (surveyDetail?.detailInfo?.waterproofMaterialEtc ? ` ${surveyDetail?.detailInfo?.waterproofMaterialEtc}` : '-')}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={5}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{
radioEtcData.insulationPresence.find((presence) => presence.id.toString() === surveyDetail?.detailInfo?.insulationPresence)
?.label
}
{surveyDetail?.detailInfo?.insulationPresenceEtc ? `, ${surveyDetail?.detailInfo?.insulationPresenceEtc}` : ''}
</td>
</tr>
<tr>
<th
style={{
padding: '10px',
width: '126px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={5}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{radioEtcData.structureOrder.find((order) => order.id.toString() === surveyDetail?.detailInfo?.structureOrder)?.label ??
(surveyDetail?.detailInfo?.structureOrderEtc ? `${surveyDetail?.detailInfo?.structureOrderEtc}` : '-')}
</td>
</tr>
</tbody>
</table>
</div>
<div style={{ padding: '12px 20px' }}>
<table
style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontFamily: 'M-Gothic', WebkitPrintColorAdjust: 'exact' }}
>
<tbody>
<tr>
<th
style={{
padding: '10px',
width: '160px',
border: '1px solid #2E3A59',
backgroundColor: '#F5F6FA',
fontSize: '13px',
fontWeight: 500,
color: '#101010',
textAlign: 'left',
boxSizing: 'border-box',
}}
>
</th>
<td
colSpan={5}
style={{
padding: '10px',
border: '1px solid #2E3A59',
fontSize: '13px',
fontWeight: 500,
color: '#FF5656',
boxSizing: 'border-box',
}}
>
{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}` : ''}
</td>
</tr>
</tbody>
</table>
</div>
<div style={{ padding: '12px 20px 12px' }}>
<div style={{ fontSize: '14px', fontFamily: 'M-Gothic', color: '#101010', fontWeight: 500, marginBottom: '10px' }}></div>
<div
style={{
boxSizing: 'border-box',
padding: '10px',
fontSize: '13px',
fontWeight: 400,
fontFamily: 'M-Gothic',
color: '#FF5656',
border: '1px solid #2E3A59',
height: '150px',
}}
>
{surveyDetail?.detailInfo?.memo ?? '-'}
</div>
</div>
</div>
</div>
</>
)
}

View File

@ -0,0 +1,330 @@
import fs from 'fs'
import path from 'path'
import { Font, Page, Text, View, StyleSheet } from '@react-pdf/renderer'
import { radioEtcData, selectBoxOptions, supplementaryFacilities, roofMaterial } from '@/types/Survey'
import { SurveyBasicInfo } from '@/types/Survey'
Font.register({
family: 'NotoSansJP',
src: `data:font/ttf;base64,${fs.readFileSync(path.resolve(process.cwd(), 'src/components/pdf/NotoSansJP-Regular.ttf')).toString('base64')}`,
})
const styles = StyleSheet.create({
page: {
padding: 15,
fontFamily: 'NotoSansJP',
fontSize: 8,
backgroundColor: '#fff',
},
header: {
padding: '15px 15px 15px',
borderBottom: '2px solid #2E3A59',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 14,
color: '#101010',
fontWeight: 'bold',
fontFamily: 'NotoSansJP',
},
headerRight: {
flexDirection: 'row',
alignItems: 'flex-start',
},
headerLeft: {
marginRight: 15,
alignItems: 'flex-end',
},
headerLabel: {
fontSize: 9,
color: '#101010',
fontWeight: 'bold',
fontFamily: 'NotoSansJP',
textAlign: 'right',
margin: 0,
},
headerValue: {
fontSize: 9,
color: '#FF5656',
fontWeight: 400,
fontFamily: 'NotoSansJP',
margin: 0,
},
section: {
padding: '20px 15px 8px',
},
sectionTitle: {
fontSize: 10,
fontFamily: 'NotoSansJP',
color: '#101010',
fontWeight: 'bold',
marginBottom: 8,
},
table: {
width: '100%',
},
tableRow: {
flexDirection: 'row',
minHeight: 28,
},
tableHeader: {
padding: 6,
backgroundColor: '#F5F6FA',
fontSize: 9,
fontWeight: 'bold',
color: '#101010',
border: '1px solid #2E3A59',
fontFamily: 'NotoSansJP',
justifyContent: 'flex-start',
alignItems: 'center',
},
tableCell: {
padding: 6,
fontSize: 9,
fontWeight: 500,
color: '#FF5656',
border: '1px solid #2E3A59',
fontFamily: 'NotoSansJP',
justifyContent: 'flex-start',
alignItems: 'center',
},
tableHeaderSmall: {
width: 50,
},
tableHeaderMedium: {
width: 85,
},
tableHeaderLarge: {
width: 110,
},
tableCellFlex1: {
flex: 1,
},
tableCellFlex2: {
flex: 2,
},
tableCellFlex07: {
flex: 0.5745,
},
memoBox: {
padding: 6,
fontSize: 9,
fontWeight: 400,
fontFamily: 'NotoSansJP',
color: '#FF5656',
border: '1px solid #2E3A59',
minHeight: 100,
width: '100%',
},
sectionNoPadding: {
padding: '10px 15px',
},
marginL: {
marginLeft: -1,
},
marginT: {
marginTop: -1,
}
})
export default function SurveySalePdf({ survey }: { survey: SurveyBasicInfo }) {
return (
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>HWJ 調1/2</Text>
<View style={styles.headerRight}>
<View style={styles.headerLeft}>
<Text style={styles.headerLabel}></Text>
<Text style={styles.headerValue}>{survey?.store ?? '-'}</Text>
</View>
<View>
<Text style={styles.headerLabel}></Text>
<Text style={styles.headerValue}>{survey?.investigationDate ?? '-'}</Text>
</View>
</View>
</View>
{/* Customer Info */}
<View style={styles.section}>
<View style={styles.table}>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderSmall]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL]}>{survey?.customerName ?? '-'}</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderSmall, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{survey?.postCode ? `(${survey?.postCode}) ${survey?.address} ${survey?.addressDetail}` : '-'}
</Text>
</View>
</View>
</View>
{/* Electric Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.table}>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL]}>{survey?.detailInfo?.contractCapacity ?? '-'}</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL]}>{survey?.detailInfo?.retailCompany ?? '-'}</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{survey?.detailInfo?.supplementaryFacilities
? supplementaryFacilities
.filter((facility) => survey?.detailInfo?.supplementaryFacilities?.includes(facility.id.toString()))
.map((facility) => facility.name)
.join(', ') + (survey?.detailInfo?.supplementaryFacilitiesEtc ? `, ${survey?.detailInfo?.supplementaryFacilitiesEtc}` : '')
: survey?.detailInfo?.supplementaryFacilitiesEtc
? `${survey?.detailInfo?.supplementaryFacilitiesEtc}`
: '-'}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{survey?.detailInfo?.installationSystem === null && survey?.detailInfo?.installationSystemEtc === null
? '-'
: survey?.detailInfo?.installationSystemEtc
? `${survey?.detailInfo?.installationSystemEtc}`
: selectBoxOptions.installationSystem.find((system) => system.id.toString() === survey?.detailInfo?.installationSystem)?.name}
</Text>
</View>
</View>
</View>
{/* Roof Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.table}>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL]}>
{survey?.detailInfo?.constructionYear === '1'
? '新築'
: survey?.detailInfo?.constructionYearEtc
? `既築 (${survey?.detailInfo?.constructionYear}年)`
: '-'}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex07, styles.marginL, styles.marginL]}>
{survey?.detailInfo?.roofMaterial === null && survey?.detailInfo?.roofMaterialEtc === null
? '-'
: roofMaterial
.filter((material) => survey?.detailInfo?.roofMaterial?.includes(material.id.toString()))
.map((material) => material.name)
.join(', ')}
{survey?.detailInfo?.roofMaterialEtc ? `, ${survey?.detailInfo?.roofMaterialEtc}` : ''}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex07, styles.marginL]}>
{selectBoxOptions.roofShape.find((shape) => shape.id.toString() === survey?.detailInfo?.roofShape)?.name ??
(survey?.detailInfo?.roofShapeEtc ? ` ${survey?.detailInfo?.roofShapeEtc}` : '-')}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT, styles.marginL]}>
{survey?.detailInfo?.roofSlope ? `${survey?.detailInfo?.roofSlope}` : '-'}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex2, styles.marginL, styles.marginL, styles.marginT]}>
{radioEtcData.houseStructure.find((structure) => structure.id.toString() === survey?.detailInfo?.houseStructure)?.label ??
(survey?.detailInfo?.houseStructureEtc ? ` ${survey?.detailInfo?.houseStructureEtc}` : '-')}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{radioEtcData.rafterMaterial.find((material) => material.id.toString() === survey?.detailInfo?.rafterMaterial)?.label ??
(survey?.detailInfo?.rafterMaterialEtc ? ` ${survey?.detailInfo?.rafterMaterialEtc}` : '-')}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex2, styles.marginL, styles.marginL, styles.marginT]}>
{selectBoxOptions.rafterSize.find((size) => size.id.toString() === survey?.detailInfo?.rafterSize)?.name ??
(survey?.detailInfo?.rafterSizeEtc ? ` ${survey?.detailInfo?.rafterSizeEtc}` : '-')}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{selectBoxOptions.rafterPitch.find((pitch) => pitch.id.toString() === survey?.detailInfo?.rafterPitch)?.name ??
(survey?.detailInfo?.rafterPitchEtc ? ` ${survey?.detailInfo?.rafterPitchEtc}` : '-')}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex2, styles.marginL, styles.marginT]}>
{radioEtcData.rafterDirection.find((direction) => direction.id.toString() === survey?.detailInfo?.rafterDirection)?.label ?? '-'}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{selectBoxOptions.openFieldPlateKind.find((kind) => kind.id.toString() === survey?.detailInfo?.openFieldPlateKind)?.name ??
(survey?.detailInfo?.openFieldPlateKindEtc ? `${survey?.detailInfo?.openFieldPlateKindEtc}` : '-')}
</Text>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginL, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex2, styles.marginL, styles.marginT]}>
{survey?.detailInfo?.openFieldPlateThickness ? `${survey?.detailInfo?.openFieldPlateThickness}mm` : '-'}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>{survey?.detailInfo?.leakTrace ? 'あり' : 'なし'}</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{radioEtcData.waterproofMaterial.find((material) => material.id.toString() === survey?.detailInfo?.waterproofMaterial)?.label ??
(survey?.detailInfo?.waterproofMaterialEtc ? ` ${survey?.detailInfo?.waterproofMaterialEtc}` : '-')}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{radioEtcData.insulationPresence.find((presence) => presence.id.toString() === survey?.detailInfo?.insulationPresence)?.label}
{survey?.detailInfo?.insulationPresenceEtc ? `, ${survey?.detailInfo?.insulationPresenceEtc}` : ''}
</Text>
</View>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderMedium, styles.marginT]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL, styles.marginT]}>
{radioEtcData.structureOrder.find((order) => order.id.toString() === survey?.detailInfo?.structureOrder)?.label ??
(survey?.detailInfo?.structureOrderEtc ? `${survey?.detailInfo?.structureOrderEtc}` : '-')}
</Text>
</View>
</View>
</View>
{/* Installation Availability */}
<View style={styles.sectionNoPadding}>
<View style={styles.table}>
<View style={styles.tableRow}>
<Text style={[styles.tableHeader, styles.tableHeaderLarge]}></Text>
<Text style={[styles.tableCell, styles.tableCellFlex1, styles.marginL]}>
{survey?.detailInfo?.installationAvailability === null && survey.detailInfo?.installationAvailabilityEtc === null
? '-'
: selectBoxOptions.installationAvailability.find(
(availability) => availability.id.toString() === survey?.detailInfo?.installationAvailability,
)?.name}
{survey?.detailInfo?.installationAvailabilityEtc ? `, ${survey?.detailInfo?.installationAvailabilityEtc}` : ''}
</Text>
</View>
</View>
</View>
{/* Memo */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.memoBox}>
<Text>{survey?.detailInfo?.memo ?? '-'}</Text>
</View>
</View>
</Page>
)
}

View File

@ -181,7 +181,7 @@ export default function SurveySaleSubmitPopup() {
<p>
<a
style="font-size: 13px; font-weight: 400; color: #1259CB; margin-bottom: 5px; text-decoration: underline;"
href="${process.env.NEXT_PUBLIC_API_URL}/pdf/survey-sale/${surveyDetail?.id}"
href="${process.env.NEXT_PUBLIC_API_URL}/api/survey-sales/${surveyDetail?.id}?isPdf=true"
target="_blank"
>
調PDFダウンロード

View File

@ -68,7 +68,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
const calculatePermissions = (session: any, basicData: SurveyBasicRequest): PermissionState => {
const isSubmiter = calculateSubmitPermission(session, basicData)
const isWriter = session.userId === basicData.representativeId
const isReceiver = session?.storeId === basicData.submissionTargetId
const isReceiver = session?.storeId === basicData.submissionTargetId || session?.storeNm === basicData.submissionTargetNm
return { isSubmiter, isWriter, isReceiver }
}

View File

@ -2,6 +2,7 @@
import { useRouter } from 'next/navigation'
import { SurveyBasicInfo } from '@/types/Survey'
import { useSurvey } from '@/hooks/useSurvey'
export default function DataTable({ surveyDetail }: { surveyDetail: SurveyBasicInfo }) {
const router = useRouter()
@ -9,21 +10,19 @@ export default function DataTable({ surveyDetail }: { surveyDetail: SurveyBasicI
/** 제출 상태 처리 */
const submitStatus = () => {
const { submissionTargetNm, submissionTargetId } = surveyDetail ?? {}
if (!submissionTargetNm) {
return null
if (!submissionTargetId && !submissionTargetNm) {
return <div>( Hanwha Japan )</div>
}
if (!submissionTargetId) {
return <div>{submissionTargetNm}</div>
if (!submissionTargetId && submissionTargetNm) {
return <div>( {submissionTargetNm} )</div>
}
return (
<div>
({submissionTargetNm} - {submissionTargetId})
</div>
)
}
const { downloadSurveyPdf } = useSurvey()
return (
<>
@ -68,7 +67,7 @@ export default function DataTable({ surveyDetail }: { surveyDetail: SurveyBasicI
<tr>
<th></th>
<td>
<button className="data-down" onClick={() => router.push(`/pdf/survey-sale/${surveyDetail.id}`)}>
<button className="data-down" onClick={() => downloadSurveyPdf(surveyDetail.id, `${surveyDetail.srlNo}.pdf`)}>
HWJ現地調査票確認<i className="down-icon"></i>
</button>
</td>

View File

@ -74,7 +74,7 @@ export default function DetailForm() {
const modeset = !Number.isNaN(Number(id)) ? 'READ' : 'CREATE'
const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id))
const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id), false)
const { session } = useSessionStore()
const searchParams = useSearchParams()
const popupController = usePopupController()

View File

@ -1,239 +1,7 @@
import { useState } from 'react'
import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey'
import { useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg'
type RadioEtcKeys =
| 'structureOrder'
| 'houseStructure'
| 'rafterMaterial'
| 'waterproofMaterial'
| 'insulationPresence'
| 'rafterDirection'
| 'leakTrace'
type SelectBoxKeys =
| 'installationSystem'
| 'constructionYear'
| 'roofShape'
| 'rafterPitch'
| 'rafterSize'
| 'openFieldPlateKind'
| 'installationAvailability'
export const supplementaryFacilities = [
/** 에코큐트 */
{ id: 1, name: 'エコキュート' },
/** 에네팜 */
{ id: 2, name: 'エネパーム' },
/** 축전지시스템 */
{ id: 3, name: '蓄電池システム' },
/** 태양광발전 */
{ id: 4, name: '太陽光発電' },
]
export const roofMaterial = [
/** 슬레이트 */
{ id: 1, name: 'スレート' },
/** 아스팔트 싱글 */
{ id: 2, name: 'アスファルトシングル' },
/** 기와 */
{ id: 3, name: '瓦' },
/** 금속지붕 */
{ id: 4, name: '金属屋根' },
]
export const selectBoxOptions: Record<SelectBoxKeys, { id: number; name: string }[]> = {
installationSystem: [
{
/** 태양광발전 */
id: 1,
name: '太陽光発電',
},
{
/** 하이브리드축전지시스템 */
id: 2,
name: 'ハイブリッド蓄電システム',
},
{
/** 축전지시스템 */
id: 3,
name: '蓄電池システム',
},
],
constructionYear: [
{
/** 신축 */
id: 1,
name: '新築',
},
{
/** 기축 */
id: 2,
name: '既築',
},
],
roofShape: [
{
/** 박공지붕 */
id: 1,
name: '切妻',
},
{
/** 기동 */
id: 2,
name: '寄棟',
},
{
/** 한쪽흐름 */
id: 3,
name: '片流れ',
},
],
rafterSize: [
{
/** 35mm 이상×48mm 이상 */
id: 1,
name: '幅35mm以上×高さ48mm以上',
},
{
/** 36mm 이상×46mm 이상 */
id: 2,
name: '幅36mm以上×高さ46mm以上',
},
{
/** 37mm 이상×43mm 이상 */
id: 3,
name: '幅37mm以上×高さ43mm以上',
},
{
/** 38mm 이상×40mm 이상 */
id: 4,
name: '幅38mm以上×高さ40mm以上',
},
],
rafterPitch: [
{
/** 455mm 이하 */
id: 1,
name: '455mm以下',
},
{
/** 500mm 이하 */
id: 2,
name: '500mm以下',
},
{
/** 606mm 이하 */
id: 3,
name: '606mm以下',
},
],
openFieldPlateKind: [
{
/** 구조용합판 */
id: 1,
name: '構造用合板',
},
{
/** OSB */
id: 2,
name: 'OSB',
},
{
/** 파티클보드 */
id: 3,
name: 'パーティクルボード',
},
{
/** 소판 */
id: 4,
name: '小幅板',
},
],
installationAvailability: [
{
/** 확인완료 */
id: 1,
name: '確認済み',
},
{
/** 미확인 */
id: 2,
name: '未確認',
},
],
}
export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]> = {
structureOrder: [
{
/** 지붕재 - 방수재 - 지붕의기초 - 서까래 */
id: 1,
label: '屋根材 > 防水材 > 屋根の基礎 > 垂木',
},
],
houseStructure: [
{
/** 목재 */
id: 1,
label: '木製',
},
],
rafterMaterial: [
{
/** 목재 */
id: 1,
label: '木製',
},
{
/** 강재 */
id: 2,
label: '強制',
},
],
waterproofMaterial: [
{
/** 아스팔트 지붕 940(22kg 이상) */
id: 1,
label: 'アスファルト屋根94022kg以上',
},
],
insulationPresence: [
{
/** 없음 */
id: 1,
label: 'なし',
},
{
/** 있음 */
id: 2,
label: 'あり',
},
],
rafterDirection: [
{
/** 수직 */
id: 1,
label: '垂直垂木',
},
{
/** 수평 */
id: 2,
label: '水平垂木',
},
],
leakTrace: [
{
/** 있음 */
id: 1,
label: 'あり',
},
{
/** 없음 */
id: 2,
label: 'なし',
},
],
}
import { radioEtcData, selectBoxOptions, supplementaryFacilities, roofMaterial } from '@/types/Survey'
const makeNumArr = (value: string) => {
return value

View File

@ -59,7 +59,6 @@ type ZipCode = {
* @description
*
* @param {number} [id] ID
* @param {boolean} [isPdf] PDF
* @returns {Object}
* @returns {SurveyBasicInfo[]} surveyList -
* @returns {SurveyBasicInfo} surveyDetail -
@ -75,7 +74,7 @@ type ZipCode = {
*/
export function useSurvey(
id?: number,
isPdf?: boolean,
isList?: boolean,
): {
surveyList: { data: SurveyBasicInfo[]; count: number } | {}
surveyDetail: SurveyBasicInfo | null
@ -94,6 +93,7 @@ export function useSurvey(
refetchSurveyList: () => void
refetchSurveyDetail: () => void
getSubmitTarget: (params: { storeId: string; role: string }) => Promise<SubmitTargetResponse[] | null>
downloadSurveyPdf: (id: number, filename: string) => Promise<Blob>
} {
const queryClient = useQueryClient()
const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore()
@ -148,11 +148,15 @@ export function useSurvey(
* @param {boolean} isThrow
* @returns {Promise<any>} API
*/
const tryFunction = async (func: () => Promise<any>, isList?: boolean, isThrow?: boolean): Promise<any> => {
const tryFunction = async (func: () => Promise<any>, isList?: boolean, isThrow?: boolean, isBlob?: boolean): Promise<any> => {
try {
const resp = await func()
return resp.data
return isBlob ? resp : resp.data
} catch (error) {
if (isBlob) {
showErrorAlert(ERROR_MESSAGE.PDF_GENERATION_ERROR)
return null
}
handleError(error, isThrow)
if (isList) {
return { data: [], count: 0 }
@ -192,7 +196,7 @@ export function useSurvey(
false,
)
},
enabled: !isPdf,
enabled: isList,
})
/**
@ -225,19 +229,47 @@ export function useSurvey(
queryKey: ['survey', id],
queryFn: async () => {
if (Number.isNaN(id) || id === undefined || id === 0) return null
return await tryFunction(
return await tryFunction(() => axiosInstance(null).get<SurveyBasicInfo>(`/api/survey-sales/${id}`), false, false)
},
enabled: id !== 0 && id !== undefined && id !== null,
})
/**
* @description PDF
*
* @param {number} id ID
* @param {string} filename
* @returns {Promise<Blob>} PDF
*/
const downloadSurveyPdf = async (id: number, filename: string) => {
const resp = await tryFunction(
() =>
axiosInstance(null).get<SurveyBasicInfo>(`/api/survey-sales/${id}`, {
params: {
isPdf: isPdf,
fetch(`/api/survey-sales/${id}?isPdf=true`, {
method: 'GET',
headers: {
'Content-Type': 'application/pdf',
},
}),
false,
false,
true,
)
},
enabled: id !== 0 && id !== undefined && id !== null,
})
console.log(resp)
const blob = await resp.blob()
if (!blob || blob.size === 0) {
showErrorAlert(ERROR_MESSAGE.PDF_GENERATION_ERROR)
return null
}
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${filename}.pdf`
a.click()
window.URL.revokeObjectURL(url)
return blob
}
/**
* @description
@ -450,5 +482,6 @@ export function useSurvey(
getSubmitTarget,
refetchSurveyList,
refetchSurveyDetail,
downloadSurveyPdf,
}
}

View File

@ -323,3 +323,237 @@ export type SurveySearchParams = {
/** 시공점 ID */
builderId?: string | null
}
type RadioEtcKeys =
| 'structureOrder'
| 'houseStructure'
| 'rafterMaterial'
| 'waterproofMaterial'
| 'insulationPresence'
| 'rafterDirection'
| 'leakTrace'
type SelectBoxKeys =
| 'installationSystem'
| 'constructionYear'
| 'roofShape'
| 'rafterPitch'
| 'rafterSize'
| 'openFieldPlateKind'
| 'installationAvailability'
export const supplementaryFacilities = [
/** 에코큐트 */
{ id: 1, name: 'エコキュート' },
/** 에네팜 */
{ id: 2, name: 'エネパーム' },
/** 축전지시스템 */
{ id: 3, name: '蓄電池システム' },
/** 태양광발전 */
{ id: 4, name: '太陽光発電' },
]
export const roofMaterial = [
/** 슬레이트 */
{ id: 1, name: 'スレート' },
/** 아스팔트 싱글 */
{ id: 2, name: 'アスファルトシングル' },
/** 기와 */
{ id: 3, name: '瓦' },
/** 금속지붕 */
{ id: 4, name: '金属屋根' },
]
export const selectBoxOptions: Record<SelectBoxKeys, { id: number; name: string }[]> = {
installationSystem: [
{
/** 태양광발전 */
id: 1,
name: '太陽光発電',
},
{
/** 하이브리드축전지시스템 */
id: 2,
name: 'ハイブリッド蓄電システム',
},
{
/** 축전지시스템 */
id: 3,
name: '蓄電池システム',
},
],
constructionYear: [
{
/** 신축 */
id: 1,
name: '新築',
},
{
/** 기축 */
id: 2,
name: '既築',
},
],
roofShape: [
{
/** 박공지붕 */
id: 1,
name: '切妻',
},
{
/** 기동 */
id: 2,
name: '寄棟',
},
{
/** 한쪽흐름 */
id: 3,
name: '片流れ',
},
],
rafterSize: [
{
/** 35mm 이상×48mm 이상 */
id: 1,
name: '幅35mm以上×高さ48mm以上',
},
{
/** 36mm 이상×46mm 이상 */
id: 2,
name: '幅36mm以上×高さ46mm以上',
},
{
/** 37mm 이상×43mm 이상 */
id: 3,
name: '幅37mm以上×高さ43mm以上',
},
{
/** 38mm 이상×40mm 이상 */
id: 4,
name: '幅38mm以上×高さ40mm以上',
},
],
rafterPitch: [
{
/** 455mm 이하 */
id: 1,
name: '455mm以下',
},
{
/** 500mm 이하 */
id: 2,
name: '500mm以下',
},
{
/** 606mm 이하 */
id: 3,
name: '606mm以下',
},
],
openFieldPlateKind: [
{
/** 구조용합판 */
id: 1,
name: '構造用合板',
},
{
/** OSB */
id: 2,
name: 'OSB',
},
{
/** 파티클보드 */
id: 3,
name: 'パーティクルボード',
},
{
/** 소판 */
id: 4,
name: '小幅板',
},
],
installationAvailability: [
{
/** 확인완료 */
id: 1,
name: '確認済み',
},
{
/** 미확인 */
id: 2,
name: '未確認',
},
],
}
export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]> = {
structureOrder: [
{
/** 지붕재 - 방수재 - 지붕의기초 - 서까래 */
id: 1,
label: '屋根材 > 防水材 > 屋根の基礎 > 垂木',
},
],
houseStructure: [
{
/** 목재 */
id: 1,
label: '木製',
},
],
rafterMaterial: [
{
/** 목재 */
id: 1,
label: '木製',
},
{
/** 강재 */
id: 2,
label: '強制',
},
],
waterproofMaterial: [
{
/** 아스팔트 지붕 940(22kg 이상) */
id: 1,
label: 'アスファルト屋根94022kg以上',
},
],
insulationPresence: [
{
/** 없음 */
id: 1,
label: 'なし',
},
{
/** 있음 */
id: 2,
label: 'あり',
},
],
rafterDirection: [
{
/** 수직 */
id: 1,
label: '垂直垂木',
},
{
/** 수평 */
id: 2,
label: '水平垂木',
},
],
leakTrace: [
{
/** 있음 */
id: 1,
label: 'あり',
},
{
/** 없음 */
id: 2,
label: 'なし',
},
],
}

View File

@ -209,3 +209,27 @@ export const convertToSnakeCase = (obj) => {
return obj
}
// 스네이크케이스를 카멜케이스로 변환하는 함수
export const toCamelCase = (str) => {
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
}
// 객체의 키를 카멜케이스로 변환하는 함수
export const convertToCamelCase = (obj) => {
if (obj === null || obj === undefined) return obj
if (Array.isArray(obj)) {
return obj.map((item) => convertToCamelCase(item))
}
if (typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const camelKey = toCamelCase(key.toLowerCase())
acc[camelKey] = convertToCamelCase(obj[key])
return acc
}, {})
}
return obj
}