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 updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => void deleteSurvey: () => Promise submitSurvey: (params: { targetId?: string | null; targetNm?: string | null }) => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise refetchSurveyList: () => void refetchSurveyDetail: () => void getSubmitTarget: (params: { storeId: string; role: string }) => Promise downloadSurveyPdf: (id: number, filename: string) => Promise } { 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} API 응답 데이터 */ const tryFunction = async (func: () => Promise, isList?: boolean, isThrow?: boolean, isBlob?: boolean): Promise => { 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(`/api/survey-sales/${id}`), false, false) }, enabled: id !== 0 && id !== undefined && id !== null, }) /** * @description 조사 매물 PDF 다운로드 * * @param {number} id 조사 매물 ID * @param {string} filename 다운로드할 파일 이름 * @returns {Promise} 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} 생성된 조사 매물 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} 수정된 조사 매물 데이터 * @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(`/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} 삭제 성공 여부 * @throws {Error} id가 없는 경우 에러 발생 * * @example * * */ const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({ mutationFn: async () => { if (id === null) throw new Error() const resp = await axiosInstance(null).post(`/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} 제출 성공 여부 * @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(`/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} 우편번호 검색 결과 * @throws {Error} 우편번호 검색 실패 시 에러 발생 */ const getZipCode = async (zipCode: string): Promise => { const data = await tryFunction( () => axiosInstance(null).get(`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} 제출 대상 목록 */ const getSubmitTarget = async (params: { storeId: string; role: string }): Promise => { 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(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, } }