import { useAxios } from '@/hooks/useAxios' import { useMessage } from '@/hooks/useMessage' import { useSwal } from '@/hooks/useSwal' import { sessionStore } from '@/store/commonAtom' import { getQueryString } from '@/util/common-utils' import { atom, useRecoilState, useRecoilValue } from 'recoil' // API 요청을 저장할 모듈 레벨 변수 (Hook 외부) // 전역 Promise 캐시 const roofMaterialPromiseCache = new Map() let cacheKey = 'default' /** * 마스터 컨트롤러 훅 * @returns */ export function useMasterController() { const { get, post, put } = useAxios() const { getMessage } = useMessage() const { swalFire } = useSwal() const sessionState = useRecoilValue(sessionStore) /** * 지붕재 목록 조회 * @returns */ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const getRoofMaterialList = async () => { // 1. 이미 진행 중이거나 완료된 Promise가 있으면 그것을 반환 if (roofMaterialPromiseCache.has(cacheKey)) { console.log('🔄 getRoofMaterialList: Returning cached promise') return roofMaterialPromiseCache.get(cacheKey) } console.log('🚀 getRoofMaterialList: Creating new promise') const MAX_RETRY = 15 // 15번으로 증가 (일본 지역 대응) const RETRY_DELAY = 2000 // 2초 간격 const MAX_BACKOFF = 10000 // 최대 10초까지 지수 백오프 const fetchWithRetry = async () => { let lastError = null let retryCount = 0 for (let attempt = 0; attempt < MAX_RETRY; attempt++) { try { // 지수 백오프 적용: 2초, 4초, 8초, ... 최대 10초 const backoffDelay = Math.min(RETRY_DELAY * Math.pow(2, attempt), MAX_BACKOFF) if (attempt > 0) { console.log(`⏳ Waiting ${backoffDelay/1000}s before retry ${attempt + 1}/${MAX_RETRY}`) await delay(backoffDelay) } console.log(`🌐 API Request ${attempt + 1}/${MAX_RETRY}: /api/v1/master/getRoofMaterialList`) const res = await get({ url: '/api/v1/master/getRoofMaterialList' }) console.log('✅ getRoofMaterialList: Success on attempt', attempt + 1) // 재시도 중이었으면 성공 메시지 표시 if (retryCount > 0) { swalFire({ icon: 'success', title: getMessage('server.restored.title'), text: `${getMessage('server.restored.text')} (${retryCount}${getMessage('server.retries')})`, timer: 3000, // 메시지 표시 시간 증가 showConfirmButton: false }) } return res } catch (error) { lastError = error retryCount++ // 500 계열만 기다렸다가 재시도 const status = error?.response?.status if (status && status >= 500) { console.log(`⚠️ getRoofMaterialList: 500 error on attempt ${attempt + 1}, retrying...`) // 첫 번째 500 에러에만 메시지 표시 if (attempt === 0) { swalFire({ icon: 'warning', title: getMessage('server.error.title'), text: getMessage('server.error.text'), timer: 4000, // 경고 메시지 시간 증가 showConfirmButton: false }) } continue } // 네트워크 에러(ECONNRESET, ETIMEDOUT 등)도 재시도 if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') { console.log(`🌐 Network error on attempt ${attempt + 1}:`, error.code) if (attempt === 0) { swalFire({ icon: 'warning', title: getMessage('network.error.title'), text: getMessage('network.error.text'), timer: 4000, showConfirmButton: false }) } continue } // 500이나 네트워크 에러가 아니면 즉시 실패 console.log(`❌ getRoofMaterialList: Non-retryable error on attempt ${attempt + 1}:`, error) throw error } } // 모든 재시도 실패 시 console.log('💥 getRoofMaterialList: All retries failed') swalFire({ icon: 'error', title: getMessage('server.failed.title'), text: `${getMessage('server.failed.text')} (${MAX_RETRY}${getMessage('server.retries')})`, confirmButtonText: getMessage('common.ok') }) throw lastError } const promise = fetchWithRetry() .finally(() => { // 성공/실패 모두 끝나면 캐시에서 제거 console.log('🏁 getRoofMaterialList: Promise resolved, removing from cache') roofMaterialPromiseCache.delete(cacheKey) }) // 캐시에 Promise 저장 roofMaterialPromiseCache.set(cacheKey, promise) return promise } /** * 모듈 타입별 아이템 목록 조회 * @param {지붕재 코드 목록} arrRoofMatlCd * @returns */ const getModuleTypeItemList = async (paramArr) => { if (!Array.isArray(paramArr) || paramArr.length === 0 || paramArr.length > 4 || paramArr.some((item) => !item || item.trim() === '')) { swalFire({ text: getMessage('master.moduletypeitem.message.error'), type: 'alert', icon: 'error' }) return null } const paramString = `?${paramArr.map((item) => `arrRoofMatlCd=${item}`).join('&')}` return await get({ url: `/api/v1/master/getModuleTypeItemList${paramString}&storeId=${sessionState.oneTwoStoreId}` }).then((res) => { // console.log('🚀🚀 ~ getModuleTypeItemList ~ res:', res) return res }) } /** * 가대 목록 조회 * @param {모듈타입코드} moduleTpCd * @param {지붕재코드} roofMatlCd * @param {서까래기초코드} raftBaseCd * @param {가대메이커코드} trestleMkrCd * @param {공법코드} constMthdCd * @param {지붕기초코드} roofBaseCd * @returns */ const getTrestleList = async (params) => { const paramString = getQueryString(params) // console.log('🚀🚀 ~ getTrestleList ~ paramString:', paramString) return await get({ url: '/api/v1/master/getTrestleList' + paramString }).then((res) => { // console.log('🚀🚀 ~ getTrestleList ~ res:', res) return res }) } /** * 모듈 시공법 목록 조회 * @param {모듈타입코드} moduleTpCd * @param {지붕재코드} roofMatlCd * @param {가대메이커코드} trestleMkrCd * @param {공법코드} constMthdCd * @param {지붕기초코드} roofBaseCd * @param {면조도} illuminationTp * @param {설치높이} instHt * @param {풍속} stdWindSpeed * @param {적설량} stdSnowLd * @param {경사도코드} inclCd * @param {서까래기초코드} raftBaseCd * @param {하제(망둥어)피치} roofPitch * * @returns */ const getConstructionList = async (params) => { const paramString = getQueryString(params) // console.log('🚀🚀 ~ getConstructionList ~ paramString:', paramString) return await get({ url: '/api/v1/master/getConstructionList' + paramString }).then((res) => { // console.log('🚀🚀 ~ getConstructionList ~ res:', res) return res }) } /** * 가대 상세 조회 * @param {모듈타입코드} moduleTpCd * @param {지붕재코드} roofMatlCd * @param {가대메이커코드} trestleMkrCd * @param {공법코드} constMthdCd * @param {지붕기초코드} roofBaseCd * @param {면조도} illuminationTp * @param {설치높이} instHt * @param {풍속} stdWindSpeed * @param {적설량} stdSnowLd * @param {경사도코드} inclCd * @param {시공법} constTp * @param {혼합모듈번호} mixMatlNo * @param {하제(망둥어)피치}roofPitch * @returns */ const getTrestleDetailList = async (params) => { // const paramString = getQueryString(params) return await post({ url: '/api/v1/master/getTrestleDetailList', data: params }).then((res) => { return res }) } /** * PCS 메이커, 시리즈 목록 조회 * @param {PCS 메이커코드} pcsMkrCd * @param {혼합모듈번호} mixMatlNo * @returns */ const getPcsMakerList = async (params = null) => { let paramString = '' if (params) { paramString = getQueryString(params) } return await get({ url: '/api/v1/master/pcsMakerList' + paramString }).then((res) => { return res }) } /** * 모듈 타입별 아이템 목록 조회 * @param {PCS 메이커코드} pcsMkrCd * @param {PCS시리즈코드 목록} pcsSerList * @param {모듈아이템 ID 목록} moduleItemList * @returns */ const getPcsModelList = async (params = null) => { return await post({ url: '/api/v1/master/getPcsSeriesItemList', data: params }).then((res) => { return res }) } /** * 시리즈중 자동으로 추천 PCS 정보 조회 * @param {Max접속(과적)여부} maxConnYn * @param {동일회로도여부} smpCirYn * @param {한랭지여부} coldZoneYn * @param {사용된 모듈 아이템 List} useModuleItemList * @param {지붕면별 목록} roofSurfaceList * @param {PCS 제품 목록} pcsItemList * @returns */ const getPcsAutoRecommendList = async (params = null) => { return await post({ url: '/api/v1/master/getPcsAutoRecommendList', data: params }).then((res) => { return res }) } const getPcsManualConfChk = async (params = null) => { return await post({ url: '/api/v1/master/getPcsMenualConfChk', data: params }).then((res) => { return res }) } /** * PCS로 회로 구성 가능 여부 체크 * @param {Max접속(과적)여부} maxConnYn * @param {동일회로도여부} smpCirYn * @param {한랭지여부} coldZoneYn * @param {사용된 모듈 아이템 List} useModuleItemList * @param {지붕면별 목록} roofSurfaceList * @param {PCS 제품 목록} pcsItemList * @returns */ const getPcsVoltageChk = async (params = null) => { return await post({ url: '/api/v1/master/getPcsVoltageChk', data: params }).then((res) => { return res }) } /** * PCS 승압설정 정보 조회 * @param {Max접속(과적)여부} maxConnYn * @param {동일회로도여부} smpCirYn * @param {한랭지여부} coldZoneYn * @param {사용된 모듈아이템 목록} useModuleItemList * @param {지붕면 목록} roofSurfaceList * @param {PCS 아이템 목록} pcsItemList * @returns */ const getPcsVoltageStepUpList = async (params2 = null) => { // roofSurfaceList의 moduleList에서 circuit 속성을 제외 const modifiedRoofSurfaceList = params2.roofSurfaceList.map((surface) => ({ ...surface, moduleList: surface.moduleList.map((module) => ({ itemId: module.itemId, uniqueId: module.uniqueId, })), })) const params = { ...params2, maxConnYn: params2.maxConnYn, smpCirYn: params2.smpCirYn, coldZoneYn: params2.coldZoneYn, useModuleItemList: params2.useModuleItemList, roofSurfaceList: modifiedRoofSurfaceList, pcsItemList: params2.pcsItemList, } return await post({ url: '/api/v1/master/getPcsVoltageStepUpList', data: params }).then((res) => { return res }) } const getQuotationItem = async (params) => { return await post({ url: '/api/v1/master/getQuotationItem', data: params }) .then((res) => { return res }) .catch((error) => { console.log('🚀 ~ getQuotationItem ~ error:', error) setIsGlobalLoading(false) }) } /** * 물건 정보 업데이트 api 호출 * @param {objectNo standardWindSpeedId verticalSnowCover surfaceType installHeight userId} params * @param {물건번호 기준풍속코드 적설량 면조도구분 설치높이 작성자아아디 } params * @returns */ const updateObjectDate = async (params = null) => { return await put({ url: '/api/object/update-object-date', data: params }) } /** * PCS 접속함 및 옵션 목록 조회 * @param {사용된 모듈 아이템 목록} useModuleItemList * @param {PCS 제품 목록} pcsItemList * @returns */ const getPcsConnOptionItemList = async (params = null) => { return await post({ url: '/api/v1/master/getPcsConnOptionItemList', data: params }).then((res) => { return res }) } const quotationErrSave = async (params) => { return await post({ url: '/api/v1/master/quotationErrSave', data: params }) } return { getRoofMaterialList, getModuleTypeItemList, getTrestleList, getConstructionList, getTrestleDetailList, getPcsMakerList, getPcsModelList, getPcsAutoRecommendList, getPcsVoltageChk, getPcsManualConfChk, getPcsVoltageStepUpList, updateObjectDate, getQuotationItem, getPcsConnOptionItemList, quotationErrSave, } }