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}
/>
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()
})
}
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))
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',