From dfa90812f4f072bd27411804bb28ad6d530f9e55 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 1 Jul 2026 09:34:17 +0900 Subject: [PATCH] =?UTF-8?q?[2296-2]=20=EB=AA=A8=EB=93=88=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=8B=9C=20=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E9=96=93=E9=9B=A2=E9=9A=94(=EA=B0=84=EA=B2=A9)=20?= =?UTF-8?q?=EB=AF=B8=EA=B2=80=EC=A6=9D=20=EC=88=98=EC=A0=95=20+=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=B0=BD=20Del=ED=82=A4=20=EB=A7=89=ED=9E=98?= =?UTF-8?q?(preventDefault)=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useModule.js | 56 +++++++++++++++++++++++++++++++++-- src/hooks/useCanvasEvent.js | 6 ++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/hooks/module/useModule.js b/src/hooks/module/useModule.js index d3794dec..d72f43a6 100644 --- a/src/hooks/module/useModule.js +++ b/src/hooks/module/useModule.js @@ -113,6 +113,9 @@ export function useModule() { const moduleMove = (length, direction) => { const selectedObj = canvas.getActiveObjects() //선택된 객체들을 가져옴 + // [MOVE-CLEARANCE 2026-07-01] 경고 후 선택 해제 상태에서 재시도 시 selectedObj[0] undefined 크래시 방지 + // (형제 함수 moduleMultiMove 와 동일한 선택 없음 가드) + if (selectedObj.length === 0) return const selectedIds = selectedObj.map((obj) => obj.id) // selectedObj의 ID 추출 if (selectedObj[0].circuit) { swalFire({ @@ -144,7 +147,9 @@ export function useModule() { canvas.renderAll() - if (isOverlapOtherModules(module, isSetupModules) || isOverlapObjects(module, objects) || isOutsideSurface(module, setupSurface)) { + // [MOVE-CLEARANCE 2026-07-01] 이동 시 モジュール間離隔 검증 추가 (isTooCloseToOtherModules) + const _tooClose = isTooCloseToOtherModules(module, isSetupModules, setupSurface) + if (isOverlapOtherModules(module, isSetupModules) || isOverlapObjects(module, objects) || isOutsideSurface(module, setupSurface) || _tooClose) { isWarning = true module.set({ fill: 'red' }) } @@ -222,7 +227,13 @@ export function useModule() { module.setCoords() canvas.renderAll() - if (isOverlapOtherModules(module, otherModules) || isOverlapObjects(module, objects) || isOutsideSurface(module, moduleSetupSurface)) { + // [MOVE-CLEARANCE 2026-07-01] 행/열 이동 시 モジュール間離隔 검증 추가 + if ( + isOverlapOtherModules(module, otherModules) || + isOverlapObjects(module, objects) || + isOutsideSurface(module, moduleSetupSurface) || + isTooCloseToOtherModules(module, otherModules, moduleSetupSurface) + ) { isWarning = true module.set({ fill: 'red' }) } @@ -1052,6 +1063,47 @@ export function useModule() { ) } + // [MOVE-CLEARANCE 2026-07-01] 이동 시 モジュール間離隔(縦 moduleIntvlVer / 横 moduleIntvlHor) 미검증 보완. + // isOverlapOtherModules 는 실제 겹침만 보므로, 이동으로 모듈을 인접 모듈에 30/3mm 미만으로 붙여도 통과한다. + // 같은 열(X-range 겹침)→세로 간격>=Ver, 같은 행(Y-range 겹침)→가로 간격>=Hor 요구. + // + // ⚠ 간격은 반드시 stroke 를 뺀 실제 폴리곤 정점(getCurrentPoints)으로 잰다. + // getBoundingRect 는 strokeWidth 를 포함해 모듈이 실제보다 커 보이고(1606→1609mm), + // 정상 격자(정확히 30/3mm)도 측정 간격이 축소돼 위반 오판 → 모든 방향 이동 차단(회귀) 발생. + const isTooCloseToOtherModules = (module, otherModules, surface) => { + if (!otherModules || otherModules.length === 0) return false + const gapVer = Number(surface?.trestleDetail?.moduleIntvlVer ?? 0) / 10 // mm → canvas unit + const gapHor = Number(surface?.trestleDetail?.moduleIntvlHor ?? 0) / 10 + if (gapVer <= 0 && gapHor <= 0) return false + const EPS = 0.05 // 0.5mm — 좌표 반올림 잔차(±0.02) 흡수, 의미있는 위반은 통과 못함 + const bboxOf = (o) => { + const pts = o.getCurrentPoints ? o.getCurrentPoints() : null + if (pts && pts.length) { + const xs = pts.map((p) => p.x) + const ys = pts.map((p) => p.y) + return { l: Math.min(...xs), r: Math.max(...xs), t: Math.min(...ys), b: Math.max(...ys) } + } + const r = o.getBoundingRect(true, true) + return { l: r.left, r: r.left + r.width, t: r.top, b: r.top + r.height } + } + const mb = bboxOf(module) + return otherModules.some((o) => { + if (o.id === module.id) return false + const nb = bboxOf(o) + const overlapX = mb.l < nb.r - EPS && nb.l < mb.r - EPS + const overlapY = mb.t < nb.b - EPS && nb.t < mb.b - EPS + if (overlapX) { + const vGap = mb.t >= nb.b ? mb.t - nb.b : nb.t >= mb.b ? nb.t - mb.b : 0 + if (vGap < gapVer - EPS) return true + } + if (overlapY) { + const hGap = mb.l >= nb.r ? mb.l - nb.r : nb.l >= mb.r ? nb.l - mb.r : 0 + if (hGap < gapHor - EPS) return true + } + return false + }) + } + const isOverlapObjects = (module, objects) => { return !checkModuleDisjointObjects(polygonToTurfPolygon(module, true), objects) } diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index 1cbd8d9c..7cb4883a 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -372,6 +372,12 @@ export function useCanvasEvent() { * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values */ const handleKeyDown = (e) => { + // [INPUT-GUARD 2026-07-01] 입력창/텍스트에어리어 포커스 중에는 캔버스 단축키 처리를 건너뛴다. + // Delete/Backspace 케이스가 switch 에 매칭돼 아래 preventDefault 까지 도달, 필드 내 문자삭제를 막던 버그. + // (QContextMenu wrappedKeyup 과 동일한 편집요소 가드) + const el = document.activeElement + if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return + const key = e.key switch (key) {