From 910974f81b3b0d720055fa7f09daaa9ad918df7f Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 31 Mar 2026 10:18:46 +0900 Subject: [PATCH] =?UTF-8?q?getRoofMaterialList=20api=EC=97=90=EB=9F=AC10?= =?UTF-8?q?=EB=B2=88=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useMasterController.js | 124 +++++++++++++++++++++--- src/hooks/option/useCanvasSetting.js | 28 ++++-- src/hooks/useAxios.js | 24 +++-- src/locales/ja.json | 9 ++ src/locales/ko.json | 9 ++ 5 files changed, 164 insertions(+), 30 deletions(-) diff --git a/src/hooks/common/useMasterController.js b/src/hooks/common/useMasterController.js index b46adc81..8eb302d1 100644 --- a/src/hooks/common/useMasterController.js +++ b/src/hooks/common/useMasterController.js @@ -7,7 +7,10 @@ import { atom, useRecoilState, useRecoilValue } from 'recoil' // API 요청을 저장할 모듈 레벨 변수 (Hook 외부) -let roofMaterialPromise = null; +// 전역 Promise 캐시 +const roofMaterialPromiseCache = new Map() +let cacheKey = 'default' + /** * 마스터 컨트롤러 훅 * @returns @@ -22,24 +25,117 @@ export function useMasterController() { * 지붕재 목록 조회 * @returns */ + + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + const getRoofMaterialList = async () => { // 1. 이미 진행 중이거나 완료된 Promise가 있으면 그것을 반환 - if (roofMaterialPromise) { - return roofMaterialPromise; + if (roofMaterialPromiseCache.has(cacheKey)) { + console.log('🔄 getRoofMaterialList: Returning cached promise') + return roofMaterialPromiseCache.get(cacheKey) } - // 2. 처음 호출될 때 Promise를 생성하여 변수에 할당 - roofMaterialPromise = get({ url: '/api/v1/master/getRoofMaterialList' }) - .then((res) => { - return res; - }) - .catch((error) => { - // 에러 발생 시 다음 호출을 위해 초기화 - roofMaterialPromise = null; - throw error; - }); + console.log('🚀 getRoofMaterialList: Creating new promise') - return roofMaterialPromise; + 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 } /** diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index f80b2902..8f339c47 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -153,12 +153,12 @@ export function useCanvasSetting(executeEffect = true) { return } - /** 초 1회만 실행하도록 처리 */ - addRoofMaterials() + // Roof materials are now initialized in Header.jsx to avoid duplicate calls }, [executeEffect]) /** - * 지붕재 초기세팅 + * 지붕재 초기세팅 (fallback - primary initialization is in Header.jsx) + * This function is only called when roof materials are needed but not yet loaded */ const addRoofMaterials = async () => { if (roofMaterials.length !== 0) { @@ -320,12 +320,26 @@ export function useCanvasSetting(executeEffect = true) { // objectNo가 전달되면 사용, 아니면 Recoil state의 correntObjectNo 사용 const targetObjectNo = objectNo || correntObjectNo - // 지붕재 데이터가 없으면 먼저 로드 + // 지붕재 데이터가 없으면 Header.jsx에서 로드될 때까지 기다림 let materials = roofMaterials if (!materials || materials.length === 0) { - logger.log('Waiting for roofMaterials to be loaded...') - materials = await addRoofMaterials() - logger.log('roofMaterials loaded:', materials) + logger.log('Waiting for roofMaterials to be loaded from Header.jsx...') + // 최대 2초간 기다림 (20번 × 100ms) + let waitCount = 0 + while ((!materials || materials.length === 0) && waitCount < 20) { + await new Promise(resolve => setTimeout(resolve, 100)) + materials = roofMaterials + waitCount++ + } + + if (!materials || materials.length === 0) { + logger.log('roofMaterials still not loaded after 2 seconds, proceeding without them') + // 비상시에만 addRoofMaterials 호출 (fallback) + materials = await addRoofMaterials() + logger.log('roofMaterials loaded via fallback:', materials) + } else { + logger.log('roofMaterials loaded from Header.jsx:', materials) + } } try { diff --git a/src/hooks/useAxios.js b/src/hooks/useAxios.js index a5c1708a..a2ddc1dc 100644 --- a/src/hooks/useAxios.js +++ b/src/hooks/useAxios.js @@ -86,15 +86,21 @@ export function useAxios(lang = '') { .get(url, option) .then((res) => res.data) .catch((error) => { - console.error('🚨 GET Error:', { - url, - method: 'GET', - timestamp: new Date().toISOString(), - errorMessage: error.message, - status: error.response?.status, - statusText: error.response?.statusText, - config: error.config - }) + // 500 에러는 재시도되므로 로깅을 줄임 + const status = error?.response?.status + if (status === 500) { + console.warn('🔄 Server Error (500): Will retry...', url) + } else { + console.error('🚨 GET Error:', { + url, + method: 'GET', + timestamp: new Date().toISOString(), + errorMessage: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + config: error.config + }) + } throw error }) } diff --git a/src/locales/ja.json b/src/locales/ja.json index feb9945f..13435c2a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -567,6 +567,15 @@ "common.finish": "完了", "common.ok": "確認", "common.cancel": "キャンセル", + "server.error.title": "サーバーエラー", + "server.error.text": "サーバーで一時的な問題が発生しました。自動的に再試行します...", + "server.restored.title": "サーバー接続復旧", + "server.restored.text": "サーバー接続が復旧しました", + "server.failed.title": "サーバー接続失敗", + "server.failed.text": "サーバー接続に失敗しました。しばらくしてから再度お試しください", + "server.retries": "回再試行", + "network.error.title": "ネットワーク接続不安定", + "network.error.text": "インターネット接続が不安定です。自動的に再試行します...", "commons.west": "西", "commons.east": "東", "commons.south": "南", diff --git a/src/locales/ko.json b/src/locales/ko.json index 7e871e60..e3403f02 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -567,6 +567,15 @@ "common.finish": "완료", "common.ok": "확인", "common.cancel": "취소", + "server.error.title": "서버 오류", + "server.error.text": "서버에 일시적인 문제가 발생했습니다. 자동으로 재시도합니다...", + "server.restored.title": "서버 연결 복구", + "server.restored.text": "서버 연결이 복구되었습니다", + "server.failed.title": "서버 연결 실패", + "server.failed.text": "서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요", + "server.retries": "회 재시도", + "network.error.title": "네트워크 연결 불안", + "network.error.text": "인터넷 연결이 불안정합니다. 자동으로 재시도합니다...", "commons.west": "서", "commons.east": "동", "commons.south": "남",