From a47f5f657a1c34ee22844e5127e4385aa79fbebb Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 30 Jan 2026 10:54:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useContextMenu.js | 71 ++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index 1f6a33b0..df22287c 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.js @@ -159,10 +159,6 @@ export function useContextMenu() { } break case 'roof': - case 'auxiliaryLine': - case 'hip': - case 'ridge': - case 'eaveHelpLine': if (selectedMenu === 'surface') { setContextMenu([ [ @@ -249,6 +245,73 @@ export function useContextMenu() { }, }, ], + ]) + } + + break + case 'auxiliaryLine': + case 'hip': + case 'ridge': + case 'eaveHelpLine': + if (selectedMenu === 'surface') { + setContextMenu([ + [ + { + id: 'sizeEdit', + name: getMessage('contextmenu.size.edit'), + component: , + }, + { + id: 'rotate', + name: `${getMessage('contextmenu.rotate')}`, + fn: () => rotateSurfaceShapeBatch(), + }, + { + id: 'roofMaterialRemove', + shortcut: ['d', 'D'], + name: `${getMessage('contextmenu.remove')}(D)`, + fn: () => deleteObject(), + }, + { + id: 'roofMaterialMove', + shortcut: ['m', 'M'], + name: `${getMessage('contextmenu.move')}(M)`, + fn: () => moveSurfaceShapeBatch(), + }, + { + id: 'roofMaterialCopy', + shortcut: ['c', 'C'], + name: `${getMessage('contextmenu.copy')}(C)`, + fn: () => copyObject(), + }, + ], + [ + { + id: 'roofMaterialEdit', + name: getMessage('contextmenu.roof.material.edit'), + component: , + }, + { + id: 'linePropertyEdit', + name: getMessage('contextmenu.line.property.edit'), + fn: () => { + if (+canvasSetting.roofSizeSet === 3) { + swalFire({ text: getMessage('contextmenu.line.property.edit.roof.size.3') }) + } else { + addPopup(popupId, 1, ) + } + }, + // component: , + }, + { + id: 'flowDirectionEdit', + name: getMessage('contextmenu.flow.direction.edit'), + component: , + }, + ], + ]) + } else if (selectedMenu === 'outline') { + setContextMenu([ [ { id: 'sizeEdit', From 7973c654b5b45bb645191eb124901ad70da8d787 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 30 Jan 2026 11:16:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EC=A7=80=EB=B6=95=EB=A9=B4=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=20=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useCommonUtils.js | 189 +++++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 26 deletions(-) diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index 9fe5a221..0b028f32 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -13,6 +13,7 @@ import { usePolygon } from '@/hooks/usePolygon' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { BATCH_TYPE } from '@/common/common' import { useMouse } from '@/hooks/useMouse' +import { QPolygon } from '@/components/fabric/QPolygon' export function useCommonUtils() { const canvas = useRecoilValue(canvasState) @@ -617,6 +618,168 @@ export function useCommonUtils() { const buttonAct = dormerName == BATCH_TYPE.TRIANGLE_DORMER ? 3 : 4 applyDormers(dormerParams, buttonAct) + } else if (obj.name === 'roof' && obj.type === 'QPolygon') { + // roof(QPolygon) 객체는 순환 참조(lines[].parent -> polygon)로 인해 + // fabric.clone() 사용 시 Maximum call stack size exceeded 에러 발생 + // getCurrentPoints()를 사용하여 새 QPolygon을 직접 생성 + + // 원본 객체의 line attributes 복사 (순환 참조 제거) + const lineAttributes = obj.lines.map((line) => ({ + type: line.attributes?.type, + offset: line.attributes?.offset, + actualSize: line.attributes?.actualSize, + planeSize: line.attributes?.planeSize, + })) + + // 원본 roof의 자식 오브젝트들 찾기 (개구, 그림자, 도머 등) + const childObjectTypes = [BATCH_TYPE.OPENING, BATCH_TYPE.SHADOW, BATCH_TYPE.TRIANGLE_DORMER, BATCH_TYPE.PENTAGON_DORMER] + const childObjects = canvas.getObjects().filter( + (o) => o.parentId === obj.id && childObjectTypes.includes(o.name) + ) + + // 원본 roof 중심점 계산 + const originalPoints = obj.getCurrentPoints() + const originalCenterX = originalPoints.reduce((sum, p) => sum + p.x, 0) / originalPoints.length + const originalCenterY = originalPoints.reduce((sum, p) => sum + p.y, 0) / originalPoints.length + + let clonedObj = null + let clonedChildren = [] + + addCanvasMouseEventListener('mouse:move', (e) => { + const pointer = canvas?.getPointer(e.e) + + // 이전 임시 객체들 제거 + canvas + .getObjects() + .filter((o) => o.name === 'clonedObj' || o.name === 'clonedChildTemp') + .forEach((o) => canvas?.remove(o)) + + // 새 QPolygon 생성 (매 move마다 생성하여 위치 업데이트) + const currentPoints = obj.getCurrentPoints() + const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length + const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length + + // 이동 오프셋 계산 + const offsetX = pointer.x - centerX + const offsetY = pointer.y - centerY + + // 포인터 위치로 이동된 새 points 계산 + const newPoints = currentPoints.map((p) => ({ + x: p.x + offsetX, + y: p.y + offsetY, + })) + + clonedObj = new QPolygon(newPoints, { + fill: obj.fill || 'transparent', + stroke: obj.stroke || 'black', + strokeWidth: obj.strokeWidth || 1, + fontSize: 0, // 이동 중에는 lengthText 생성하지 않음 (fontSize=0이면 addLengthText가 스킵됨) + selectable: true, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'clonedObj', + originX: 'center', + originY: 'center', + pitch: obj.pitch, + surfaceId: obj.surfaceId, + sort: false, + }, canvas) + + canvas.add(clonedObj) + + // 자식 오브젝트들도 이동해서 미리보기 표시 + clonedChildren = [] + childObjects.forEach((child) => { + child.clone((clonedChild) => { + clonedChild.set({ + left: child.left + offsetX, + top: child.top + offsetY, + name: 'clonedChildTemp', + selectable: false, + evented: false, + }) + clonedChildren.push({ original: child, cloned: clonedChild }) + canvas.add(clonedChild) + }) + }) + + canvas.renderAll() + }) + + addCanvasMouseEventListener('mouse:down', (e) => { + if (!clonedObj) return + + const newRoofId = uuidv4() + + clonedObj.set({ + lockMovementX: true, + lockMovementY: true, + name: 'roof', + editable: false, + selectable: true, + id: newRoofId, + direction: obj.direction, + directionText: obj.directionText, + roofMaterial: obj.roofMaterial, + stroke: 'black', + evented: true, + isFixed: false, + fontSize: lengthTextFont.fontSize.value, // 최종 확정 시 fontSize 설정 + }) + + // line attributes 복원 + lineAttributes.forEach((attr, index) => { + if (clonedObj.lines[index]) { + clonedObj.lines[index].set({ attributes: attr }) + } + }) + + // 임시 자식 오브젝트들 제거 + canvas + .getObjects() + .filter((o) => o.name === 'clonedChildTemp') + .forEach((o) => canvas?.remove(o)) + + // 자식 오브젝트들 최종 복사 (새 roof의 id를 parentId로 설정) + const pointer = canvas?.getPointer(e.e) + const currentPoints = obj.getCurrentPoints() + const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length + const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length + const offsetX = pointer.x - centerX + const offsetY = pointer.y - centerY + + childObjects.forEach((child) => { + child.clone((clonedChild) => { + clonedChild.set({ + left: child.left + offsetX, + top: child.top + offsetY, + id: uuidv4(), + parentId: newRoofId, // 새 roof의 id를 부모로 설정 + name: child.name, + selectable: true, + evented: true, + }) + // 그룹 객체인 경우 groupId도 새로 설정 + if (clonedChild.type === 'group') { + clonedChild.set({ groupId: uuidv4() }) + } + canvas.add(clonedChild) + }) + }) + + clonedObj.fire('polygonMoved') + clonedObj.fire('modified') + clonedObj.setCoords() + canvas.setActiveObject(clonedObj) + canvas.renderAll() + addLengthText(clonedObj) // fontSize가 설정된 후 lengthText 추가 + drawDirectionArrow(clonedObj) + + initEvent() + }) } else { let clonedObj = null @@ -655,32 +818,6 @@ export function useCommonUtils() { //객체가 그룹일 경우에는 그룹 아이디를 따로 넣어준다 if (clonedObj.type === 'group') clonedObj.set({ groupId: uuidv4() }) - //배치면일 경우 - if (obj.name === 'roof') { - clonedObj.canvas = canvas // canvas 참조 설정 - clonedObj.set({ - direction: obj.direction, - directionText: obj.directionText, - roofMaterial: obj.roofMaterial, - stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정 - selectable: true, // 선택 가능하도록 설정 - evented: true, // 마우스 이벤트를 받을 수 있도록 설정 - isFixed: false, // containsPoint에서 특별 처리 방지 - }) - - obj.lines.forEach((line, index) => { - clonedObj.lines[index].set({ attributes: line.attributes }) - }) - - clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset) - clonedObj.fire('modified') - clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트 - canvas.setActiveObject(clonedObj) - canvas.renderAll() - addLengthText(clonedObj) //수치 추가 - drawDirectionArrow(clonedObj) //방향 화살표 추가 - } - initEvent() }) } From 1207195d04809acdff47653c1326a6073d8161d6 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 30 Jan 2026 11:21:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EC=98=A4=EB=B8=8C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EC=9D=B4=EC=83=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B7=B8=EB=A6=BC=EC=9E=90=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/object/useObjectBatch.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/hooks/object/useObjectBatch.js b/src/hooks/object/useObjectBatch.js index ef2cf587..2af636a5 100644 --- a/src/hooks/object/useObjectBatch.js +++ b/src/hooks/object/useObjectBatch.js @@ -152,11 +152,17 @@ export function useObjectBatch({ isHidden, setIsHidden }) { rect.set({ width: Math.abs(width), height: Math.abs(height) }) + // 마우스를 왼쪽으로 드래그한 경우 left를 현재 포인터 위치로 설정 if (width < 0) { - rect.set({ left: Math.abs(pointer.x) }) + rect.set({ left: pointer.x }) + } else { + rect.set({ left: origX }) } + // 마우스를 위쪽으로 드래그한 경우 top을 현재 포인터 위치로 설정 if (height < 0) { - rect.set({ top: Math.abs(pointer.y) }) + rect.set({ top: pointer.y }) + } else { + rect.set({ top: origY }) } canvas?.renderAll() @@ -179,7 +185,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) { } } - if (!isCrossChecked) { + // 그림자(SHADOW)는 중복 설치 허용, 개구(OPENING)만 중복 체크 + if (!isCrossChecked && buttonAct === 1) { const preObjects = canvas?.getObjects().filter((obj) => obj.name === BATCH_TYPE.OPENING || obj.name === BATCH_TYPE.SHADOW) const preObjectsArray = preObjects.map((obj) => rectToPolygon(obj)) const isCross = preObjectsArray.some((object) => turf.booleanOverlap(pointsToTurfPolygon(object), rectPolygon)) @@ -266,7 +273,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) { } } - if (!isCrossChecked) { + // 그림자(SHADOW)는 중복 설치 허용, 개구(OPENING)만 중복 체크 + if (!isCrossChecked && buttonAct === 1) { const preObjects = canvas?.getObjects().filter((obj) => obj.name === BATCH_TYPE.OPENING || obj.name === BATCH_TYPE.SHADOW) const preObjectsArray = preObjects.map((obj) => rectToPolygon(obj)) const isCross = preObjectsArray.some((object) => turf.booleanOverlap(pointsToTurfPolygon(object), rectPolygon)) From a4b20b71c58d8c14c16ab85f1d43ddc9ed1725d9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 30 Jan 2026 11:38:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=ED=8C=A9=EC=8A=A4=20-=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/Join.jsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/auth/Join.jsx b/src/components/auth/Join.jsx index f414cd62..4062d313 100644 --- a/src/components/auth/Join.jsx +++ b/src/components/auth/Join.jsx @@ -98,6 +98,10 @@ export default function Join() { alert(getMessage('common.message.required.data', [getMessage('join.sub1.fax')])) faxRef.current.focus() return false + }else if (!telRegex.test(fax)) { + alert(getMessage('join.validation.check1', [getMessage('join.sub1.fax')])) + faxRef.current.focus() + return false } const bizNo = formData.get('bizNo') @@ -174,6 +178,10 @@ export default function Join() { alert(getMessage('common.message.required.data', [getMessage('join.sub2.fax')])) userFaxRef.current.focus() return false + } else if (!telRegex.test(userFax)) { + alert(getMessage('join.validation.check1', [getMessage('join.sub2.fax')])) + userFaxRef.current.focus() + return false } return true @@ -349,7 +357,15 @@ export default function Join() { {getMessage('join.sub1.fax')}*
- +
@@ -466,7 +482,8 @@ export default function Join() { name="userFax" className="input-light" maxLength={15} - onChange={inputNumberCheck} + placeholder={getMessage('join.sub1.telNo_placeholder')} + onChange={inputTelNumberCheck} ref={userFaxRef} />