From a5e794b3eccd0c2458e0dd19447a841ffd0c224d Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 8 May 2026 17:39:58 +0900 Subject: [PATCH] =?UTF-8?q?[2204]=20=EC=86=90=EC=83=81=20plan=20InvalidSta?= =?UTF-8?q?teError=203=EB=8B=A8=20=EB=B0=A9=EC=96=B4=20(entry/cache/save?= =?UTF-8?q?=20guard)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useRoofFn.js | 12 ++++++++++++ src/hooks/useCanvas.js | 24 ++++++++++++++++++++++++ src/hooks/usePlan.js | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/hooks/common/useRoofFn.js b/src/hooks/common/useRoofFn.js index 4721d581..88c5f6ea 100644 --- a/src/hooks/common/useRoofFn.js +++ b/src/hooks/common/useRoofFn.js @@ -48,6 +48,18 @@ export function useRoofFn() { roofMaterial = polygon.roofMaterial ?? selectedRoofMaterial } + // [ROOF-PATTERN-GUARD 2026-05-08] polygon bbox 0 또는 roofMaterial 결손 시 + // 0×0 patternSourceCanvas 가 만들어져 drawImage InvalidStateError 로 페이지 500 진입. + // 손상 plan(예 S203X460260507001) 로드 케이스 차단. + if (!polygon.width || !polygon.height) { + console.warn('[ROOF-PATTERN-GUARD] skip pattern, 0-dim polygon:', polygon.id, polygon.name, polygon.width, polygon.height) + return + } + if (!roofMaterial || !roofMaterial.layout) { + console.warn('[ROOF-PATTERN-GUARD] skip pattern, invalid roofMaterial:', polygon.id, roofMaterial) + return + } + const ratio = window.devicePixelRatio || 1 const layout = roofMaterial.layout diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index 9a6fb14b..237cd82b 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -139,6 +139,30 @@ export function useCanvas(id) { const initialize = () => { canvas.getObjects().length > 0 && canvas?.clear() + // [BG-CACHE-DIAG 2026-05-08] drawCacheOnCanvas 0-dim cache 차단 + 식별 + // 운영 S203X460260507001 등 손상된 plan 로드 시 InvalidStateError 방지. + // 객체 width/height 가 정상이어도 zoom/scale/bbox 조합으로 _cacheCanvas 가 + // 0×0 이 될 수 있어 객체 단위 가드로는 부족. drawCacheOnCanvas 직전에 + // 캐시 캔버스 dim 검사 + 로그 + 캐시 비활성으로 차단. + if (!fabric.Object.prototype.__bgCacheDiagPatched) { + const origDrawCache = fabric.Object.prototype.drawCacheOnCanvas + fabric.Object.prototype.drawCacheOnCanvas = function (ctx) { + const c = this._cacheCanvas + if (!c || !c.width || !c.height) { + console.warn('[BG-CACHE-DIAG] skip 0-dim cache:', { + type: this.type, name: this.name, id: this.id, + w: this.width, h: this.height, + sx: this.scaleX, sy: this.scaleY, + visible: this.visible, opacity: this.opacity, + cacheW: c?.width, cacheH: c?.height, + }) + this.objectCaching = false + return + } + return origDrawCache.call(this, ctx) + } + fabric.Object.prototype.__bgCacheDiagPatched = true + } // settings for all canvas in the app fabric.Object.prototype.transparentCorners = false fabric.Object.prototype.selectable = true diff --git a/src/hooks/usePlan.js b/src/hooks/usePlan.js index 150be261..d0b4b05d 100644 --- a/src/hooks/usePlan.js +++ b/src/hooks/usePlan.js @@ -177,6 +177,24 @@ export function usePlan(params = {}) { * @param {boolean} saveAlert - 저장 완료 알림 표시 여부 */ const saveCanvas = async (saveAlert = true) => { + // [ROOF-MATERIAL-SAVE-GUARD 2026-05-08] 면(roof) 에 시각적 지붕재 패턴은 칠해져 있는데 + // 데이터(roofMaterial) 가 비어있는 "불일치" 케이스만 저장 차단. + // - fill 비어있고 roofMaterial 도 없음 → 미선택 상태, 저장 허용 + // - fill 이 fabric.Pattern 인데 roofMaterial 누락 → 저장 시 누락 JSON 이 DB 로 가고 + // 재로드 시 setSurfaceShapePattern 0×0 patternCanvas → drawImage InvalidStateError + const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.wall) + const inconsistent = roofs.filter((r) => { + const f = r.fill + const hasPatternFill = f && typeof f === 'object' && (f.source || f.type === 'pattern') + return hasPatternFill && !r.roofMaterial + }) + if (inconsistent.length > 0) { + console.warn('[ROOF-MATERIAL-SAVE-GUARD] pattern drawn but roofMaterial missing:', + inconsistent.map((r) => ({ id: r.id, name: r.name, fillType: r.fill?.type }))) + swalFire({ text: '面に屋根材パターンが描画されていますが、屋根材データが保存されていません。\n屋根材を再設定してから保存してください。', icon: 'warning' }) + return + } + // 저장 전 선택되어 있는 object 제거 const setupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)