onsitesurvey/src/hooks/useSurvey.ts
2025-07-01 10:47:51 +09:00

487 lines
16 KiB
TypeScript

import type { SubmitTargetResponse, SurveyBasicInfo, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey'
import { useMemo } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useSessionStore } from '@/store/session'
import { useAxios } from './useAxios'
import { queryStringFormatter } from '@/utils/common-utils'
import { useRouter } from 'next/navigation'
import { ERROR_MESSAGE, useAlertMsg } from './useAlertMsg'
export const requiredFields = [
{
field: 'installationSystem',
name: '設置希望システム',
},
{
field: 'constructionYear',
name: '建築年数',
},
{
field: 'rafterSize',
name: '垂木サイズ',
},
{
field: 'rafterPitch',
name: '垂木傾斜',
},
{
field: 'waterproofMaterial',
name: '防水材',
},
{
field: 'insulationPresence',
name: '断熱材有無',
},
{
field: 'structureOrder',
name: '屋根構造の順序',
},
]
interface ZipCodeResponse {
status: number
message: string | null
results: ZipCode[] | null
}
type ZipCode = {
zipcode: string
prefcode: string
address1: string
address2: string
address3: string
kana1: string
kana2: string
kana3: string
}
/**
* @description 조사 매물 관련 기능을 제공하는 커스텀 훅
*
* @param {number} [id] 조사 매물 ID
* @returns {Object} 조사 매물 관련 기능과 데이터
* @returns {SurveyBasicInfo[]} surveyList - 조사 매물 목록 데이터
* @returns {SurveyBasicInfo} surveyDetail - 조사 매물 상세 데이터
* @returns {boolean} isLoadingSurveyList - 조사 매물 목록 로딩 상태
* @returns {boolean} isLoadingSurveyDetail - 조사 매물 상세 데이터 로딩 상태
* @returns {boolean} isCreatingSurvey - 조사 매물 생성 중 상태
* @returns {boolean} isUpdatingSurvey - 조사 매물 수정 중 상태
* @returns {boolean} isDeletingSurvey - 조사 매물 삭제 중 상태
* @returns {boolean} isSubmittingSurvey - 조사 매물 제출 중 상태
* @returns {Function} createSurvey - 조사 매물 생성 함수
* @returns {Function} updateSurvey - 조사 매물 수정 함수
* @returns {Function} deleteSurvey - 조사 매물 삭제 함수
*/
export function useSurvey(
id?: number,
isList?: boolean,
): {
surveyList: { data: SurveyBasicInfo[]; count: number } | {}
surveyDetail: SurveyBasicInfo | null
isLoadingSurveyList: boolean
isLoadingSurveyDetail: boolean
isCreatingSurvey: boolean
isUpdatingSurvey: boolean
isDeletingSurvey: boolean
isSubmittingSurvey: boolean
createSurvey: (survey: SurveyRegistRequest) => Promise<number>
updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => void
deleteSurvey: () => Promise<boolean>
submitSurvey: (params: { targetId?: string | null; targetNm?: string | null }) => void
validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string
getZipCode: (zipCode: string) => Promise<ZipCode[] | null>
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()
const { session } = useSessionStore()
const { axiosInstance } = useAxios()
const router = useRouter()
const { showErrorAlert } = useAlertMsg()
/**
* @description 조사 매물 목록, 상세 데이터 조회 에러 처리
*
* @param {any} error 에러 객체
* @param {boolean} isThrow 에러 Throw 처리 여부
* @returns {void} 라우팅 처리 / 에러 Throw 처리
*/
const handleError = (error: any, isThrow?: boolean) => {
const status = error.response?.status
const errorMsg = error.response?.data.error
console.error('❌ AXIOS INSTANCE ERROR : ', error)
if (errorMsg) {
showErrorAlert(errorMsg)
}
if (isThrow) {
throw new Error(error)
}
switch (status) {
/** session 없는 경우 */
case 401:
router.replace('/login')
break
/** 조회 권한 없는 경우 */
case 403:
router.replace('/survey-sale')
break
/** 데이터 DB상 존재하지 않는 경우 */
case 404:
router.replace('/survey-sale')
break
/** 서버 오류 */
case 500:
router.back()
break
default:
break
}
}
/**
* @description 조사 매물 try catch 처리 함수
*
* @param {Function} func 조사 매물 API 함수
* @param {boolean} isList 조사 매물 목록 여부
* @param {boolean} isThrow 조사 매물 데이터 조회 에러 처리 여부
* @returns {Promise<any>} API 응답 데이터
*/
const tryFunction = async (func: () => Promise<any>, isList?: boolean, isThrow?: boolean, isBlob?: boolean): Promise<any> => {
try {
const resp = await func()
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 }
}
return null
}
}
/**
* @description 조사 매물 목록 조회
*
* @returns {Object} 조사 매물 목록 데이터
* @returns {SurveyBasicInfo[]} 조사 매물 목록 데이터
* @returns {number} 조건에 맞는 조사 매물 총 개수
* @returns {() => void} 조사 매물 목록 데이터 새로고침 함수
* @returns {boolean} 조사 매물 목록 로딩 상태
*/
const {
data: surveyListData,
isLoading: isLoadingSurveyList,
refetch: refetchSurveyList,
} = useQuery({
queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset],
queryFn: async () => {
return await tryFunction(
() =>
axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', {
params: {
keyword,
searchOption,
isMySurvey,
sort,
offset,
},
}),
true,
false,
)
},
enabled: isList,
})
/**
* @description 조사 매물 목록 데이터 메모이제이션
*
* @returns {Object} 메모이제이션된 조사 매물 목록 데이터
* @returns {number} count - 조건에 맞는 조사 매물 총 개수
* @returns {SurveyBasicInfo[]} data - 조사 매물 목록 데이터
*/
const surveyData = useMemo(() => {
if (!surveyListData) return { count: 0, data: [] }
return {
...surveyListData,
}
}, [surveyListData])
/**
* @description 조사 매물 상세 데이터 조회
*
* @returns {Object} 조사 매물 상세 데이터
* @returns {SurveyBasicInfo} surveyDetail - 조사 매물 상세 데이터
* @returns {boolean} isLoadingSurveyDetail - 조사 매물 상세 데이터 로딩 상태
* @returns {() => void} refetchSurveyDetail - 조사 매물 상세 데이터 새로고침 함수
*/
const {
data: surveyDetail,
isLoading: isLoadingSurveyDetail,
refetch: refetchSurveyDetail,
} = useQuery({
queryKey: ['survey', id],
queryFn: async () => {
if (Number.isNaN(id) || id === undefined || id === 0) return null
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(
() =>
fetch(`/api/survey-sales/${id}?isPdf=true`, {
method: 'GET',
headers: {
'Content-Type': 'application/pdf',
},
}),
false,
false,
true,
)
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}`
a.click()
window.URL.revokeObjectURL(url)
return blob
}
/**
* @description 조사 매물 생성
*
* @param {SurveyRegistRequest} survey 생성할 조사 매물 데이터
* @returns {Promise<number>} 생성된 조사 매물 ID
*/
const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({
mutationFn: async (survey: SurveyRegistRequest) => {
const resp = await axiosInstance(null).post<{ id: number }>('/api/survey-sales', {
survey: survey,
storeId: session?.role === 'Partner' ? session?.builderId ?? null : session?.storeId ?? null,
role: session?.role ?? null,
})
return resp.data.id ?? 0
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
queryClient.invalidateQueries({ queryKey: ['survey', id] })
},
onError: (error: any) => {
handleError(error, true)
},
})
/**
* @description 조사 매물 수정
*
* @param {Object} params 수정할 데이터
* @param {SurveyRegistRequest} params.survey 수정할 조사 매물 데이터
* @param {boolean} params.isTemporary 임시 저장 여부
* @param {string|null} [params.storeId] 판매점 ID
* @returns {Promise<SurveyRegistRequest>} 수정된 조사 매물 데이터
* @throws {Error} id가 없는 경우 에러 발생
*/
const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({
mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => {
if (id === undefined) throw new Error()
const resp = await axiosInstance(null).post<SurveyRegistRequest>(`/api/survey-sales/${id}`, {
method: 'PUT',
survey: survey,
isTemporary: isTemporary,
storeId: storeId,
role: session?.role ?? null,
})
return resp.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', id] })
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
onError: (error: any) => {
handleError(error, true)
},
})
/**
* @description 조사 매물 삭제
*
* @returns {Promise<boolean>} 삭제 성공 여부
* @throws {Error} id가 없는 경우 에러 발생
*
* @example
*
*
*/
const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({
mutationFn: async () => {
if (id === null) throw new Error()
const resp = await axiosInstance(null).post<boolean>(`/api/survey-sales/${id}`, {
method: 'DELETE',
})
return resp.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
},
onError: (error: any) => {
handleError(error, true)
},
})
/**
* @description 조사 매물 제출
*
* @param {Object} params 제출할 데이터
* @param {string|null} [params.targetId] 제출 대상 ID
* @param {string|null} [params.targetNm] 제출 대상 이름
* @returns {Promise<boolean>} 제출 성공 여부
* @throws {Error} id가 없는 경우 에러 발생
*/
const { mutateAsync: submitSurvey, isPending: isSubmittingSurvey } = useMutation({
mutationFn: async ({ targetId, targetNm }: { targetId?: string | null; targetNm?: string | null }) => {
if (!id) throw new Error()
const resp = await axiosInstance(null).post<boolean>(`/api/survey-sales/${id}`, {
method: 'PATCH',
targetId,
targetNm,
})
return resp.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
queryClient.invalidateQueries({ queryKey: ['survey', id] })
},
onError: (error: any) => {
handleError(error, true)
},
})
/**
* @description 조사 매물 상세 데이터 유효성 검사
*
* @param {SurveyDetailRequest} surveyDetail 검사할 조사 매물 상세 데이터
* @returns {string} 빈 필드 이름 또는 빈 문자열
*/
const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => {
const ETC_FIELDS = ['installationSystem', 'rafterSize', 'rafterPitch', 'waterproofMaterial', 'structureOrder'] as const
const SPECIAL_CONDITIONS = ['constructionYear', 'insulationPresence'] as const
const isEmptyValue = (value: any): boolean => {
return value === null || value?.toString().trim() === ''
}
const checkRequiredField = (field: string): string => {
if (ETC_FIELDS.includes(field as (typeof ETC_FIELDS)[number])) {
if (
isEmptyValue(surveyDetail[field as keyof SurveyDetailRequest]) &&
isEmptyValue(surveyDetail[`${field}Etc` as keyof SurveyDetailRequest])
) {
return field
}
} else if (SPECIAL_CONDITIONS.includes(field as (typeof SPECIAL_CONDITIONS)[number])) {
if (surveyDetail[field as keyof SurveyDetailRequest] === '2' && isEmptyValue(surveyDetail[`${field}Etc` as keyof SurveyDetailRequest])) {
return `${field}Etc`
} else if (isEmptyValue(surveyDetail[field as keyof SurveyDetailRequest])) {
return field
}
}
return ''
}
// 필수 필드 체크
const emptyField = requiredFields.find((field) => checkRequiredField(field.field))
if (emptyField) return emptyField.field
// 계약 용량 단위 체크
const contractCapacity = surveyDetail.contractCapacity
if (contractCapacity?.trim() && contractCapacity.split(' ').length === 1) {
return 'contractCapacityUnit'
}
return ''
}
/**
* @description 우편번호 검색
*
* @param {string} zipCode 검색할 우편번호
* @returns {Promise<ZipCode[]|null>} 우편번호 검색 결과
* @throws {Error} 우편번호 검색 실패 시 에러 발생
*/
const getZipCode = async (zipCode: string): Promise<ZipCode[] | null> => {
const data = await tryFunction(
() => axiosInstance(null).get<ZipCodeResponse>(`https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`),
false,
true,
)
return data ? data.results : null
}
/**
* @description 제출 대상 조회
*
* @param {Object} params 조회할 데이터
* @param {string} params.storeId 판매점 ID
* @param {string} params.role 사용자 권한
* @returns {Promise<SubmitTargetResponse[]|null>} 제출 대상 목록
*/
const getSubmitTarget = async (params: { storeId: string; role: string }): Promise<SubmitTargetResponse[] | null> => {
if (!params.storeId) {
/** 판매점 ID 없는 경우 */
showErrorAlert(ERROR_MESSAGE.BAD_REQUEST)
return null
}
const endpoint = `/api/submission?storeId=${params.storeId}&role=${params.role}`
if (!endpoint) {
/** 권한 오류 */
showErrorAlert(ERROR_MESSAGE.FORBIDDEN)
return null
}
return await tryFunction(() => axiosInstance(null).get<SubmitTargetResponse[]>(endpoint), false, true)
}
return {
surveyList: surveyData.data,
surveyDetail: surveyDetail as SurveyBasicInfo | null,
isLoadingSurveyList,
isLoadingSurveyDetail,
isCreatingSurvey,
isUpdatingSurvey,
isDeletingSurvey,
isSubmittingSurvey,
createSurvey,
updateSurvey,
deleteSurvey,
submitSurvey,
validateSurveyDetail,
getZipCode,
getSubmitTarget,
refetchSurveyList,
refetchSurveyDetail,
downloadSurveyPdf,
}
}