[지붕재] select 폴링 stale closure 수정 — 데이터 도착 즉시 종료 + 매칭 0건 에러 안내

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
ysCha 2026-06-29 12:34:00 +09:00
parent b031e703ec
commit d0c3110801
3 changed files with 28 additions and 9 deletions

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
import {
adsorptionPointModeState,
@ -119,6 +119,12 @@ export function useCanvasSetting(executeEffect = true) {
const [basicSetting, setBasicSettings] = useRecoilState(basicSettingState)
const { getRoofMaterialList, getModuleTypeItemList } = useMasterController()
const [roofMaterials, setRoofMaterials] = useRecoilState(roofMaterialsAtom)
// [ROOFMAT-POLL-FIX 2026-06-29] fetchBasicSettings 의 폴링 루프가 stale closure 에 묶여
// Header.jsx 가 채운 최신 roofMaterials 를 못 읽던 문제 → ref 미러로 최신값을 읽어 데이터 도착 즉시 종료.
const roofMaterialsRef = useRef(roofMaterials)
useEffect(() => {
roofMaterialsRef.current = roofMaterials
}, [roofMaterials])
const [addedRoofs, setAddedRoofs] = useRecoilState(addedRoofsState)
const setCurrentMenu = useSetRecoilState(currentMenuState)
@ -321,20 +327,22 @@ export function useCanvasSetting(executeEffect = true) {
const targetObjectNo = objectNo || correntObjectNo
// 지붕재 데이터가 없으면 Header.jsx에서 로드될 때까지 기다림
let materials = roofMaterials
// [ROOFMAT-POLL-FIX 2026-06-29] roofMaterialsRef 로 최신값을 폴링 → 데이터가 들어오는 즉시 종료.
// (이전엔 stale closure 로 항상 풀카운트를 소모해 select 가 늦게 떴음)
let materials = roofMaterialsRef.current
if (!materials || materials.length === 0) {
logger.log('Waiting for roofMaterials to be loaded from Header.jsx...')
// 최대 2초간 기다림 (20번 × 100ms)
// 최대 1초간 기다림 (10번 × 100ms), 데이터 도착 시 즉시 break
let waitCount = 0
while ((!materials || materials.length === 0) && waitCount < 20) {
await new Promise(resolve => setTimeout(resolve, 100))
materials = roofMaterials
while ((!materials || materials.length === 0) && waitCount < 10) {
await new Promise((resolve) => setTimeout(resolve, 100))
materials = roofMaterialsRef.current
waitCount++
}
if (!materials || materials.length === 0) {
logger.log('roofMaterials still not loaded after 2 seconds, proceeding without them')
// 비상시에만 addRoofMaterials 호출 (fallback)
logger.log('roofMaterials still not loaded after 1 second, proceeding via fallback')
// 10회 안에 못 받으면 fallback 직접 호출 (getRoofMaterialList promise cache 로 중복 네트워크 방지)
materials = await addRoofMaterials()
logger.log('roofMaterials loaded via fallback:', materials)
} else {
@ -485,6 +493,15 @@ export function useCanvasSetting(executeEffect = true) {
})
}
}
} else {
// [ROOFMAT-TOPMENU-DIAG 2026-06-29] 저장 roofMatlCd 가 마스터에 없거나 마스터 미로드로 매칭 0건 →
// top 메뉴 지붕재 select(CanvasMenu.jsx:648)가 렌더되지 않으므로 사용자에게 에러 안내.
logger.warn('[ROOFMAT-TOPMENU-DIAG] addRoofs 0건 — select 미표시', {
objectNo: targetObjectNo,
materialsCount: materials?.length ?? 0,
savedRoofCodes: roofsArray.map((r) => r.roofMatlCd),
})
swalFire({ icon: 'error', text: getMessage('canvas.roof.material.not.found') })
}
})
} catch (error) {

View File

@ -60,6 +60,7 @@
"modal.placement.initial.setting.roof.angle": "角度",
"modal.placement.initial.setting.roof.material": "屋根材選択単位mm",
"modal.placement.initial.setting.roof.material.info": "対応可能な屋根材や足場は限定されますので、必ず事前マニュアルをご確認ください。",
"canvas.roof.material.not.found": "屋根材情報を読み込めませんでした。ページを再読み込みするか、屋根材設定をご確認ください。",
"modal.placement.initial.setting.rafter": "垂木",
"modal.roof.shape.setting": "屋根形状の設定",
"modal.roof.shape.setting.ridge": "棟",

View File

@ -60,6 +60,7 @@
"modal.placement.initial.setting.roof.angle": "각도",
"modal.placement.initial.setting.roof.material": "지붕재 선택(단위mm)",
"modal.placement.initial.setting.roof.material.info": "대응 가능한 지붕재 및 발판은 한정되므로 반드시 사전 매뉴얼을 확인하십시오.",
"canvas.roof.material.not.found": "지붕재 정보를 불러오지 못했습니다. 페이지를 새로고침하거나 지붕재 설정을 확인해 주세요.",
"modal.placement.initial.setting.rafter": "서까래",
"modal.roof.shape.setting": "지붕형상 설정",
"modal.roof.shape.setting.ridge": "용마루",