dev #827
@ -30,9 +30,26 @@ export default function ObjectSetting({ id, pos = { x: 50, y: 230 } }) {
|
||||
//오브젝트 배치로 넘어오면 면형상 선택 불가
|
||||
useEffect(() => {
|
||||
canvas.discardActiveObject()
|
||||
|
||||
// [ROOF-SELECTABLE-RESTORE 2026-05-08] 모달 닫힘 시 roof selectable 원복.
|
||||
// 누락되어 있어 한 번 진입 후엔 지붕 선택 불가 → 자동배치 단계에서 다른 hook 이 다시
|
||||
// selectable=true 로 set 해주기 전까진 클릭 무반응 상태가 지속됐음.
|
||||
// 무조건 true 로 복원하면 동시에 열린 다른 모드(roof.selectable=false 의도) 와 충돌 가능 →
|
||||
// 진입 시점의 원래 값을 snapshot 해두고 cleanup 시 그 값으로 복원(다른 모드의 의도 보존).
|
||||
const prevSelectable = surfaceShapePolygons.map((obj) => ({ obj, selectable: obj.selectable }))
|
||||
|
||||
surfaceShapePolygons.forEach((obj) => {
|
||||
obj.set({ selectable: false })
|
||||
})
|
||||
|
||||
return () => {
|
||||
prevSelectable.forEach(({ obj, selectable }) => {
|
||||
// 객체가 이미 캔버스에서 제거됐을 수도 있으므로 canvas 멤버십 확인 후 복원.
|
||||
if (canvas?.getObjects().includes(obj)) {
|
||||
obj.set({ selectable })
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
|
||||
@ -11,6 +11,7 @@ import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
||||
import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||
import { CalculatorInput } from '@/components/common/input/CalcInput'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
|
||||
export default function SizeSetting(props) {
|
||||
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
|
||||
@ -20,6 +21,7 @@ export default function SizeSetting(props) {
|
||||
const { closePopup } = usePopup()
|
||||
const { resizeObjectBatch } = useObjectBatch({})
|
||||
const { resizeSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||
const { swalFire } = useSwal()
|
||||
const widthRef = useRef(null)
|
||||
const heightRef = useRef(null)
|
||||
const [width, setWidth] = useState(target?.width ? (target.width * 10).toFixed() : 0)
|
||||
@ -46,6 +48,15 @@ export default function SizeSetting(props) {
|
||||
const width = widthRef.current.value
|
||||
const height = heightRef.current.value
|
||||
|
||||
// [OPENING-MIN-SIZE 2026-05-08] 0/빈값 입력 시 적용 차단. 0,0 으로 확정되면 4정점 동일
|
||||
// 0-dim 객체가 plan 에 저장되어 모듈배치 단계에서 turf throw → 화면 크래시.
|
||||
if (target.name === BATCH_TYPE.OPENING || target.name === BATCH_TYPE.SHADOW) {
|
||||
if (width === '' || height === '' || Number(width) <= 0 || Number(height) <= 0) {
|
||||
swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (target.name === BATCH_TYPE.OPENING || target.name === BATCH_TYPE.SHADOW) {
|
||||
resizeObjectBatch(settingTarget, target, width, height, id)
|
||||
} else if (target.name === POLYGON_TYPE.ROOF) {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -327,6 +327,15 @@ export function useModuleBasicSetting(tabNum) {
|
||||
|
||||
//도머등 오브젝트 객체가 있으면 아웃라인 낸다
|
||||
batchObjects.forEach((obj) => {
|
||||
// [OPENING-MIN-SIZE 2026-05-08] 기존 plan 에 저장된 0-면적 개구/그림자/도머 보호:
|
||||
// 모든 정점이 동일 좌표면 offsetPolygon → cleanSelfIntersectingPolygon → turf.polygon throw 발생.
|
||||
// 신규는 SizeSetting 가드로 막지만, 이미 저장된 데이터는 여기서 skip.
|
||||
const _pts = obj.getCurrentPoints?.() || obj.points || []
|
||||
if (_pts.length >= 2 && _pts.every((p) => p.x === _pts[0].x && p.y === _pts[0].y)) {
|
||||
console.warn('[OPENING-MIN-SIZE 2026-05-08] 0-dim batch object skipped (outline)', { id: obj.id, name: obj.name, parentId: obj.parentId })
|
||||
return
|
||||
}
|
||||
|
||||
//도머일때
|
||||
if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) {
|
||||
const groupPoints = obj.getCurrentPoints()
|
||||
@ -960,6 +969,12 @@ export function useModuleBasicSetting(tabNum) {
|
||||
//도머 객체를 가져옴
|
||||
if (batchObjects) {
|
||||
batchObjects.forEach((object) => {
|
||||
// [OPENING-MIN-SIZE 2026-05-08] 0-면적 객체는 turf 변환 시 throw 위험 → skip
|
||||
const _pts = object.getCurrentPoints?.() || object.points || []
|
||||
if (_pts.length >= 2 && _pts.every((p) => p.x === _pts[0].x && p.y === _pts[0].y)) {
|
||||
console.warn('[OPENING-MIN-SIZE 2026-05-08] 0-dim batch object skipped (intersect)', { id: object.id, name: object.name })
|
||||
return
|
||||
}
|
||||
let dormerTurfPolygon = polygonToTurfPolygon(object, true)
|
||||
const intersection = turf.intersect(turf.featureCollection([dormerTurfPolygon, tempTurfModule])) //겹치는지 확인
|
||||
//겹치면 안됨
|
||||
@ -3716,6 +3731,12 @@ export function useModuleBasicSetting(tabNum) {
|
||||
//도머 객체를 가져옴
|
||||
if (batchObjects) {
|
||||
batchObjects.forEach((object) => {
|
||||
// [OPENING-MIN-SIZE 2026-05-08] 0-면적 객체는 turf 변환 시 throw 위험 → skip
|
||||
const _pts = object.getCurrentPoints?.() || object.points || []
|
||||
if (_pts.length >= 2 && _pts.every((p) => p.x === _pts[0].x && p.y === _pts[0].y)) {
|
||||
console.warn('[OPENING-MIN-SIZE 2026-05-08] 0-dim batch object skipped (intersect)', { id: object.id, name: object.name })
|
||||
return
|
||||
}
|
||||
let dormerTurfPolygon
|
||||
|
||||
if (object.type === 'group') {
|
||||
|
||||
@ -287,6 +287,10 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
|
||||
isDown = false
|
||||
// [OPENING-COLLAPSE-DIAG 2026-05-08] 치수입력 mouse:up 시점 width/height 0 검출
|
||||
if (!rect.width || !rect.height) {
|
||||
console.warn('[OPENING-COLLAPSE-DIAG] dimension-input mouse:up rect collapsed:', { name: objName, w: rect.width, h: rect.height, sx: rect.scaleX, sy: rect.scaleY, left: rect.left, top: rect.top })
|
||||
}
|
||||
rect.set({ name: objName, parentId: selectedSurface.id, points: rectToPolygon(rect) })
|
||||
rect.setCoords()
|
||||
initEvent()
|
||||
@ -1430,6 +1434,10 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
addCanvasMouseEventListener('mouse:up', (e) => {
|
||||
//개구, 그림자 타입일 경우 폴리곤 타입 변경
|
||||
if (BATCH_TYPE.OPENING === obj.name || BATCH_TYPE.SHADOW === obj.name) {
|
||||
// [OPENING-COLLAPSE-DIAG 2026-05-08] 객체 이동 mouse:up 시점 width/height 0 검출
|
||||
if (!obj.width || !obj.height || !obj.scaleX || !obj.scaleY) {
|
||||
console.warn('[OPENING-COLLAPSE-DIAG] move mouse:up obj collapsed:', { id: obj.id, name: obj.name, w: obj.width, h: obj.height, sx: obj.scaleX, sy: obj.scaleY, left: obj.left, top: obj.top })
|
||||
}
|
||||
obj.set({
|
||||
points: rectToPolygon(obj),
|
||||
})
|
||||
@ -1532,6 +1540,10 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
addCanvasMouseEventListener('mouse:up', (e) => {
|
||||
//개구, 그림자 타입일 경우 폴리곤 타입 변경
|
||||
if (BATCH_TYPE.OPENING == obj.name || BATCH_TYPE.SHADOW == obj.name) {
|
||||
// [OPENING-COLLAPSE-DIAG 2026-05-08] 복제 mouse:up 시점 width/height 0 검출
|
||||
if (!clonedObj.width || !clonedObj.height || !clonedObj.scaleX || !clonedObj.scaleY) {
|
||||
console.warn('[OPENING-COLLAPSE-DIAG] copy mouse:up clonedObj collapsed:', { name: obj.name, w: clonedObj.width, h: clonedObj.height, sx: clonedObj.scaleX, sy: clonedObj.scaleY, left: clonedObj.left, top: clonedObj.top })
|
||||
}
|
||||
clonedObj.set({
|
||||
points: rectToPolygon(clonedObj),
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user