diff --git a/src/components/auth/Join.jsx b/src/components/auth/Join.jsx index f414cd62..94f7fa3e 100644 --- a/src/components/auth/Join.jsx +++ b/src/components/auth/Join.jsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation' import { useMessage } from '@/hooks/useMessage' import Cookies from 'js-cookie' -import { isObjectNotEmpty, inputTelNumberCheck, inputNumberCheck } from '@/util/common-utils' +import { isObjectNotEmpty, inputTelNumberCheck, inputNumberCheck, inputUserIdCheck } from '@/util/common-utils' import GlobalSpinner from '@/components/common/spinner/GlobalSpinner' @@ -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') @@ -129,6 +133,13 @@ export default function Join() { alert(getMessage('common.message.required.data', [getMessage('join.sub2.userId')])) userIdRef.current.focus() return false + } else { + const userIdRegex = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]+$/ + if (!userIdRegex.test(userId)) { + alert(getMessage('join.validation.check1', [getMessage('join.sub2.userId')])) + userIdRef.current.focus() + return false + } } // 담당자 정보 - 이메일 주소 @@ -174,6 +185,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 +364,15 @@ export default function Join() { {getMessage('join.sub1.fax')}*
- +
@@ -420,7 +443,15 @@ export default function Join() {
- +
@@ -466,7 +497,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/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx index 64759f8c..d7a475a0 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx @@ -46,9 +46,9 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { setType, arrow1Ref, arrow2Ref, - outerLineDiagonalLength, - setOuterLineDiagonalLength, - outerLineDiagonalLengthRef, + auxiliaryLineDiagonalLength, + setAuxiliaryLineDiagonalLength, + auxiliaryLineDiagonalLengthRef, handleRollback, handleFix, buttonAct, @@ -123,9 +123,9 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { length2, setLength2, length2Ref, - outerLineDiagonalLength, - setOuterLineDiagonalLength, - outerLineDiagonalLengthRef, + diagonalLength: auxiliaryLineDiagonalLength, + setDiagonalLength: setAuxiliaryLineDiagonalLength, + diagonalLengthRef: auxiliaryLineDiagonalLengthRef, arrow1, setArrow1, arrow2, diff --git a/src/components/floor-plan/modal/basic/step/ModuleTabContents.jsx b/src/components/floor-plan/modal/basic/step/ModuleTabContents.jsx index 7cdef358..cda87230 100644 --- a/src/components/floor-plan/modal/basic/step/ModuleTabContents.jsx +++ b/src/components/floor-plan/modal/basic/step/ModuleTabContents.jsx @@ -212,7 +212,7 @@ export default function ModuleTabContents({ tabIndex, addRoof, setAddedRoofs, ro type="checkbox" id={`ch01_${tabIndex}`} disabled={cvrYn === 'N' ? true : false} - checked={cvrChecked || false} + checked={cvrYn === 'N' ? false : cvrChecked ?? true} onChange={handleCvrChecked} /> diff --git a/src/components/floor-plan/modal/basic/step/Trestle.jsx b/src/components/floor-plan/modal/basic/step/Trestle.jsx index a1061a87..e208a286 100644 --- a/src/components/floor-plan/modal/basic/step/Trestle.jsx +++ b/src/components/floor-plan/modal/basic/step/Trestle.jsx @@ -413,7 +413,7 @@ const Trestle = forwardRef((props, ref) => { setCvrYn(constructionList[index].cvrYn) setSnowGdPossYn(constructionList[index].snowGdPossYn) - setCvrChecked(false) + setCvrChecked(true) setSnowGdChecked(false) } } @@ -859,7 +859,7 @@ const Trestle = forwardRef((props, ref) => { type="checkbox" id={`ch01`} disabled={!cvrYn || cvrYn === 'N'} - checked={cvrChecked || false} + checked={!cvrYn || cvrYn === 'N' ? false : cvrChecked ?? true} // onChange={() => dispatch({ type: 'SET_TRESTLE_DETAIL', roof: { ...trestleState, cvrChecked: !trestleState.cvrChecked } })} onChange={() => setCvrChecked(!cvrChecked)} /> diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 4c4ea028..02e9d171 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -124,6 +124,9 @@ export default function CircuitTrestleSetting({ id }) { */ const validateModuleSizeForRack = (surface) => { const { modules, direction, trestleDetail } = surface + if (!trestleDetail) { + return true // 상세 정보 없음 + } const { rackYn, moduleIntvlHor, moduleIntvlVer } = trestleDetail if (rackYn === 'N' || !modules || modules.length < 2) { diff --git a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx index e4e8a615..f8ae00ad 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx @@ -125,7 +125,8 @@ export default function StepUp(props) { setSeletedMainOption(optionList[0]) } } - const selectedSerQty = pcsItem.serQtyList.find((serQty) => serQty.selected) + const serQtyList = pcsItem.serQtyList ?? [] + const selectedSerQty = serQtyList.find((serQty) => serQty.selected) if (selectedSerQty) { selectedSerQty.roofSurfaceList.forEach((roofSurface) => { const targetSurface = canvas.getObjects().filter((obj) => obj.id === roofSurface.roofSurfaceId)[0] diff --git a/src/components/floor-plan/modal/flowDirection/FlowDirectionSetting.jsx b/src/components/floor-plan/modal/flowDirection/FlowDirectionSetting.jsx index 4c1f0991..e9f57869 100644 --- a/src/components/floor-plan/modal/flowDirection/FlowDirectionSetting.jsx +++ b/src/components/floor-plan/modal/flowDirection/FlowDirectionSetting.jsx @@ -21,6 +21,7 @@ export default function FlowDirectionSetting(props) { const canvas = useRecoilValue(canvasState) const { getMessage } = useMessage() const { setSurfaceShapePattern } = useRoofFn() + const { setPolygonLinesActualSize } = usePolygon() const { changeSurfaceLineType } = useSurfaceShapeBatch({}) @@ -85,6 +86,8 @@ export default function FlowDirectionSetting(props) { drawDirectionArrow(roof) canvas?.renderAll() changeSurfaceLineType(roof) + setPolygonLinesActualSize(roof, true) + canvas.renderAll() closePopup(id) } diff --git a/src/components/floor-plan/modal/lineTypes/Diagonal.jsx b/src/components/floor-plan/modal/lineTypes/Diagonal.jsx index b5b50ab6..7ab0e3ec 100644 --- a/src/components/floor-plan/modal/lineTypes/Diagonal.jsx +++ b/src/components/floor-plan/modal/lineTypes/Diagonal.jsx @@ -12,9 +12,9 @@ export default function Diagonal({ props }) { length2, setLength2, length2Ref, - outerLineDiagonalLength, - setOuterLineDiagonalLength, - outerLineDiagonalLengthRef, + diagonalLength, + setDiagonalLength, + diagonalLengthRef, arrow1, setArrow1, arrow2, @@ -45,11 +45,11 @@ export default function Diagonal({ props }) { name="" label="" className="input-origin block" - value={outerLineDiagonalLength} - ref={outerLineDiagonalLengthRef} - onChange={(value) => setOuterLineDiagonalLength(value)} + value={diagonalLength} + ref={diagonalLengthRef} + onChange={(value) => setDiagonalLength(value)} placeholder="3000" - onFocus={() => (outerLineDiagonalLengthRef.current.value = '')} + onFocus={() => (diagonalLengthRef.current.value = '')} options={{ allowNegative: false, allowDecimal: false @@ -59,7 +59,7 @@ export default function Diagonal({ props }) { diff --git a/src/components/floor-plan/modal/outerlinesetting/WallLineSetting.jsx b/src/components/floor-plan/modal/outerlinesetting/WallLineSetting.jsx index 72308b15..8d79f223 100644 --- a/src/components/floor-plan/modal/outerlinesetting/WallLineSetting.jsx +++ b/src/components/floor-plan/modal/outerlinesetting/WallLineSetting.jsx @@ -104,9 +104,9 @@ export default function WallLineSetting(props) { length2, setLength2, length2Ref, - outerLineDiagonalLength, - setOuterLineDiagonalLength, - outerLineDiagonalLengthRef, + diagonalLength: outerLineDiagonalLength, + setDiagonalLength: setOuterLineDiagonalLength, + diagonalLengthRef: outerLineDiagonalLengthRef, arrow1, setArrow1, arrow2, diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeDrawing.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeDrawing.jsx index 04b63f1d..8010675d 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeDrawing.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeDrawing.jsx @@ -107,9 +107,9 @@ export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) { length2, setLength2, length2Ref, - outerLineDiagonalLength, - setOuterLineDiagonalLength, - outerLineDiagonalLengthRef, + diagonalLength: outerLineDiagonalLength, + setDiagonalLength: setOuterLineDiagonalLength, + diagonalLengthRef: outerLineDiagonalLengthRef, arrow1, setArrow1, arrow2, diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 4ec45dd5..95442b8a 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -63,7 +63,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla const roofSizeSetArray = [ { id: 'ra01', name: 'roofSizeSet', value: '1', message: 'modal.placement.initial.setting.size.roof' }, { id: 'ra02', name: 'roofSizeSet', value: '2', message: 'modal.placement.initial.setting.size.actual' }, - { id: 'ra03', name: 'roofSizeSet', value: '3', message: 'modal.placement.initial.setting.size.none.pitch' }, + // { id: 'ra03', name: 'roofSizeSet', value: '3', message: 'modal.placement.initial.setting.size.none.pitch' }, ] /** diff --git a/src/components/floor-plan/modal/setting01/GridOption.jsx b/src/components/floor-plan/modal/setting01/GridOption.jsx index 5931b561..76b8fc1c 100644 --- a/src/components/floor-plan/modal/setting01/GridOption.jsx +++ b/src/components/floor-plan/modal/setting01/GridOption.jsx @@ -152,12 +152,16 @@ export default function GridOption(props) {

{getMessage('modal.canvas.setting.grid')}

- {gridOptions?.map((option) => ( - - ))} + {gridOptions?.map((option) => + option.id === 2 ? ( + <> + ) : ( + + ), + )}
{/**/} diff --git a/src/components/floor-plan/modal/setting01/SettingModal01.jsx b/src/components/floor-plan/modal/setting01/SettingModal01.jsx index b3e6a0df..f3a9e611 100644 --- a/src/components/floor-plan/modal/setting01/SettingModal01.jsx +++ b/src/components/floor-plan/modal/setting01/SettingModal01.jsx @@ -100,11 +100,11 @@ export default function SettingModal01(props) { - {/*{canGridOptionSeletorValue && ( + {canGridOptionSeletorValue && ( - )}*/} + )} {buttonAct === 1 && } {buttonAct === 2 && } 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/common/useTurf.js b/src/hooks/common/useTurf.js index a9c1f202..95fa709c 100644 --- a/src/hooks/common/useTurf.js +++ b/src/hooks/common/useTurf.js @@ -8,7 +8,7 @@ export const useTurf = () => { * @param spare * @returns */ - const checkModuleDisjointSurface = (module, surface, spare = 1) => { + const checkModuleDisjointSurface = (module, surface, spare = 0) => { // 표면 영역을 spare만큼 수동 확장 const expandedSurface = { type: 'Polygon', diff --git a/src/hooks/floorPlan/useImgLoader.js b/src/hooks/floorPlan/useImgLoader.js index 242206db..81383f13 100644 --- a/src/hooks/floorPlan/useImgLoader.js +++ b/src/hooks/floorPlan/useImgLoader.js @@ -58,7 +58,12 @@ export function useImgLoader() { canvas.renderAll() const formData = new FormData() - const dataUrl = canvas.toDataURL('image/png') + // 고해상도 캡처를 위해 multiplier 옵션 추가 (2배 해상도) + const multiplier = 2 + const dataUrl = canvas.toDataURL({ + format: 'png', + multiplier: multiplier, + }) const blobBin = atob(dataUrl.split(',')[1]) const array = [] for (let i = 0; i < blobBin.length; i++) { @@ -69,13 +74,13 @@ export function useImgLoader() { formData.append('objectNo', currentCanvasPlan.objectNo) formData.append('planNo', currentCanvasPlan.planNo) formData.append('type', type) - /** 이미지 크롭 좌표 계산 */ + /** 이미지 크롭 좌표 계산 (multiplier 배율 적용) */ const positionObj = getImageCoordinates() console.log('🚀 ~ handleCanvasToPng ~ positionObj:', positionObj) - formData.append('width', Math.round(positionObj[1].x - positionObj[0].x + 100)) - formData.append('height', Math.round(positionObj[1].y - positionObj[0].y + 100)) - formData.append('left', Math.round(positionObj[0].x)) - formData.append('top', Math.round(positionObj[0].y)) + formData.append('width', Math.round((positionObj[1].x - positionObj[0].x + 100) * multiplier)) + formData.append('height', Math.round((positionObj[1].y - positionObj[0].y + 100) * multiplier)) + formData.append('left', Math.round(positionObj[0].x * multiplier)) + formData.append('top', Math.round(positionObj[0].y * multiplier)) console.log('🚀 ~ handleCanvasToPng ~ formData:', formData) /** 이미지 크롭 요청 */ diff --git a/src/hooks/module/useModule.js b/src/hooks/module/useModule.js index 45b3b50f..8e59a1e7 100644 --- a/src/hooks/module/useModule.js +++ b/src/hooks/module/useModule.js @@ -749,7 +749,7 @@ export function useModule() { const copyModules = [] const moduleSetupSurface = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === activeModule.surfaceId) let isWarning = false - const { moduleIntvlHor, moduleIntvlVer } = moduleSetupSurface.trestleDetail + const { moduleIntvlHor = 0, moduleIntvlVer = 0 } = moduleSetupSurface.trestleDetail || {} canvas.discardActiveObject() targetModules.forEach((module) => { const { top, left } = getPosotion(module, type, moduleIntvlHor, true) @@ -859,7 +859,7 @@ export function useModule() { const copyModules = [] const moduleSetupSurface = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === activeModule.surfaceId) let isWarning = false - const { moduleIntvlHor, moduleIntvlVer } = moduleSetupSurface.trestleDetail + const { moduleIntvlHor = 0, moduleIntvlVer = 0 } = moduleSetupSurface.trestleDetail || {} canvas.discardActiveObject() targetModules.forEach((module) => { const { top, left } = getPosotion(module, type, moduleIntvlVer, true) diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index f298c916..c2b0398d 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -17,7 +17,7 @@ import { import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util' import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가 import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom' -import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils' +import offsetPolygon, { calculateAngle, cleanSelfIntersectingPolygon, createLinesFromPolygon } from '@/util/qpolygon-utils' import { QPolygon } from '@/components/fabric/QPolygon' import { useEvent } from '@/hooks/useEvent' import { BATCH_TYPE, LINE_TYPE, MODULE_SETUP_TYPE, POLYGON_TYPE } from '@/common/common' @@ -338,10 +338,27 @@ export function useModuleBasicSetting(tabNum) { }) let isNorth = false + const defaultTrestleDetail = { + rackYn: 'N', + moduleIntvlHor: +roofSizeSet === 3 ? 300 : 0, + moduleIntvlVer: +roofSizeSet === 3 ? 100 : 0, + rack: null, + rackQty: 0, + rackIntvlPct: 0, + cvrPlvrYn: 'N', + lessSupFitIntvlPct: 0, + lessSupFitQty: 0, + } const isExistSurface = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.parentId === roof.id) + const normalizedTrestleDetail = trestleDetail + ? { ...defaultTrestleDetail, ...trestleDetail } + : isExistSurface?.trestleDetail + ? { ...defaultTrestleDetail, ...isExistSurface.trestleDetail } + : defaultTrestleDetail if (isExistSurface) { + isExistSurface.set({ trestleDetail: normalizedTrestleDetail }) if (canvasSetting.roofSizeSet != '3') { //북면이 있지만 if (roof.directionText && roof.directionText.indexOf('北') > -1) { @@ -384,6 +401,8 @@ export function useModuleBasicSetting(tabNum) { } else { offsetPoints = createPaddingPolygon(polygon, roof.lines).vertices } + // 자기교차(꼬임) 제거 + offsetPoints = cleanSelfIntersectingPolygon(offsetPoints) } //모듈설치영역?? 생성 @@ -422,7 +441,7 @@ export function useModuleBasicSetting(tabNum) { originY: 'center', modules: [], roofMaterial: roof.roofMaterial, - trestleDetail: trestleDetail, + trestleDetail: normalizedTrestleDetail, isNorth: isNorth, perPixelTargetFind: true, isSaleStoreNorthFlg: moduleSelectionData.common.saleStoreNorthFlg == '1' ? true : false, //북면설치가능점 여부 @@ -2103,7 +2122,7 @@ export function useModuleBasicSetting(tabNum) { } //흐름 방향이 남쪽(아래) - const downFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) => { + const downFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) => { let setupModule = [] const trestleDetailData = moduleSetupSurface.trestleDetail @@ -2182,6 +2201,11 @@ export function useModuleBasicSetting(tabNum) { calcAreaHeight = isNaN(calcAreaHeight) ? moduleSetupSurface.height : calcAreaHeight let calcModuleHeightCount = calcAreaHeight / (height + intvVer) + // 대칭 지붕을 위해 south의 calcAreaWidth 저장 (north에서 참조) + if (symmetricWidthRef && moduleIndex === 0) { + symmetricWidthRef.south = calcAreaWidth + } + if (type === MODULE_SETUP_TYPE.LAYOUT) { calcModuleWidthCount = layoutCol > calcModuleWidthCount ? calcModuleWidthCount : layoutCol calcModuleHeightCount = layoutRow @@ -2205,7 +2229,7 @@ export function useModuleBasicSetting(tabNum) { //첫번재 모듈 설치 후 두번째 모듈을 몇개까지 설치 할 수 있는지 계산 if (installedModuleHeightCount > 0) { // moduleMaxRows = totalModuleMaxRows - installedModuleHeightCount //두번째 모듈일때 - isChidoriLine = installedModuleHeightCount % 2 != 0 ? true : false //첫번째에서 짝수에서 끝났으면 홀수는 치도리가 아님 짝수는 치도리 + isChidoriLine = installedModuleHeightCount % 2 !== 0 //첫번째에서 짝수에서 끝났으면 홀수는 치도리가 아님 짝수는 치도리 } for (let i = 0; i < calcModuleHeightCount; i++) { @@ -2225,7 +2249,7 @@ export function useModuleBasicSetting(tabNum) { widthMargin = j === 0 ? 0 : intvHor * j // 가로 마진값 chidoriLength = 0 //치도리가 아니여도 기본값을 5정도 준다 if (isChidori) { - chidoriLength = installedModuleHeightCount % 2 == 0 ? 0 : width / 2 - intvHor + chidoriLength = installedModuleHeightCount % 2 === 0 ? 0 : width / 2 - intvHor } //치도리 일때 는 짝수(1 기준) 일때만 치도리 라인으로 본다 @@ -2285,7 +2309,7 @@ export function useModuleBasicSetting(tabNum) { } } - const topFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) => { + const topFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) => { let setupModule = [] const trestleDetailData = moduleSetupSurface.trestleDetail @@ -2364,9 +2388,22 @@ export function useModuleBasicSetting(tabNum) { //육지붕이 아닐때만 넣는다 육지붕일땐 클릭 이벤트에 별도로 넣어놓음 const moduleArray = [] - let calcAreaWidth = flowLines.right.x1 - flowLines.left.x1 //오른쪽 x에서 왼쪽 x를 뺀 가운데를 찾는 로직 + // 북쪽: 남쪽과 동일한 방식으로 계산 (대칭을 위해) + let calcAreaWidth = Math.abs(flowLines.right.x1 - flowLines.left.x1) //오른쪽 x에서 왼쪽 x를 뺀 가운데를 찾는 로직 + + // 대칭 지붕: south의 calcAreaWidth가 있고 north의 값이 south보다 10% 이상 작으면 south 값 사용 + if (symmetricWidthRef?.south && calcAreaWidth < symmetricWidthRef.south * 0.9) { + // flowLines 좌표도 보정 (중심점 유지하면서 너비 확장) + const center = (flowLines.right.x1 + flowLines.left.x1) / 2 + const halfWidth = symmetricWidthRef.south / 2 + flowLines.left.x1 = center - halfWidth + flowLines.right.x1 = center + halfWidth + + calcAreaWidth = symmetricWidthRef.south + } + let calcModuleWidthCount = calcAreaWidth / (width + intvHor) //뺀 공간에서 모듈을 몇개를 넣을수 있는지 확인하는 로직 - let calcAreaHeight = flowLines.bottom.y1 - flowLines.top.y1 + let calcAreaHeight = Math.abs(flowLines.bottom.y1 - flowLines.top.y1) let calcModuleHeightCount = calcAreaHeight / (height + intvVer) //단수지정 자동이면 @@ -2467,7 +2504,7 @@ export function useModuleBasicSetting(tabNum) { //남, 북과 같은 로직으로 적용하려면 좌우는 열 -> 행 으로 그려야함 //변수명은 bottom 기준으로 작성하여 동일한 방향으로 진행한다 - const leftFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) => { + const leftFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) => { let setupModule = [] const trestleDetailData = moduleSetupSurface.trestleDetail //가대 상세 데이터 @@ -2555,6 +2592,11 @@ export function useModuleBasicSetting(tabNum) { let calcAreaHeight = Math.abs(flowLines.right.x1 - flowLines.left.x1) let calcModuleHeightCount = calcAreaHeight / (width + intvVer) + // 대칭 지붕을 위해 west의 calcAreaWidth 저장 (east에서 참조) + if (symmetricWidthRef && moduleIndex === 0) { + symmetricWidthRef.west = calcAreaWidth + } + //단수지정 자동이면 if (type === MODULE_SETUP_TYPE.LAYOUT) { calcModuleWidthCount = layoutCol > calcModuleWidthCount ? calcModuleWidthCount : layoutCol @@ -2653,7 +2695,7 @@ export function useModuleBasicSetting(tabNum) { } } - const rightFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) => { + const rightFlowSetupModule = (maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) => { let setupModule = [] const trestleDetailData = moduleSetupSurface.trestleDetail //가대 상세 데이터 @@ -2734,9 +2776,22 @@ export function useModuleBasicSetting(tabNum) { //육지붕이 아닐때만 넣는다 육지붕일땐 클릭 이벤트에 별도로 넣어놓음 const moduleArray = [] - let calcAreaWidth = flowLines.bottom.y1 - flowLines.top.y1 //아래에서 y에서 위를 y를 뺀 가운데를 찾는 로직 + // 동쪽: 서쪽과 동일한 방식으로 계산 (대칭을 위해) + let calcAreaWidth = Math.abs(flowLines.bottom.y1 - flowLines.top.y1) //아래에서 y에서 위를 y를 뺀 가운데를 찾는 로직 + + // 대칭 지붕: west의 calcAreaWidth가 있고 east의 값이 west보다 10% 이상 작으면 west 값 사용 + if (symmetricWidthRef?.west && calcAreaWidth < symmetricWidthRef.west * 0.9) { + // flowLines 좌표도 보정 (중심점 유지하면서 높이 확장) + const center = (flowLines.bottom.y1 + flowLines.top.y1) / 2 + const halfHeight = symmetricWidthRef.west / 2 + flowLines.top.y1 = center - halfHeight + flowLines.bottom.y1 = center + halfHeight + + calcAreaWidth = symmetricWidthRef.west + } + let calcModuleWidthCount = calcAreaWidth / (height + intvHor) //뺀 공간에서 모듈을 몇개를 넣을수 있는지 확인하는 로직 - let calcAreaHeight = flowLines.right.x1 - flowLines.left.x1 + let calcAreaHeight = Math.abs(flowLines.right.x1 - flowLines.left.x1) let calcModuleHeightCount = calcAreaHeight / (width + intvVer) //단수지정 자동이면 @@ -2746,15 +2801,14 @@ export function useModuleBasicSetting(tabNum) { } let calcMaxModuleWidthCount = calcModuleWidthCount > moduleMaxCols ? moduleMaxCols : calcModuleWidthCount //최대 모듈 단수가 있기 때문에 최대 단수보다 카운트가 크면 최대 단수로 씀씀 - // let totalModuleWidthCount = isChidori ? Math.abs(calcMaxModuleWidthCount) : Math.floor(calcMaxModuleWidthCount) //치조배치일경우는 한개 더 넣는다 - let totalModuleWidthCount = Math.floor(calcMaxModuleWidthCount) //치조배치일경우는 한개 더 넣는다 + let totalModuleWidthCount = Math.floor(calcMaxModuleWidthCount) let calcStartPoint = flowLines.top.type === 'flat' ? (calcAreaWidth - totalModuleWidthCount * height) / 2 : 0 //반씩 나눠서 중앙에 맞춤 left 높이 기준으로 양변이 직선일때만 가운데 정렬 - let startPointX = flowLines.bottom.y2 - calcStartPoint //시작점을 만든다 + let startPointX = flowLines.bottom.y1 - calcStartPoint //시작점을 만든다 //근데 양변이 곡선이면 중앙에 맞추기 위해 아래와 위의 길이를 재서 모듈의 길이를 나눠서 들어갈수 있는 갯수가 동일하면 가운데로 정렬 시킨다 if (flowLines.top.type === 'curve' && flowLines.bottom.type === 'curve') { - startPointX = flowLines.bottom.y2 - (calcAreaWidth - totalModuleWidthCount * height) / 2 + startPointX = flowLines.bottom.y1 - (calcAreaWidth - totalModuleWidthCount * height) / 2 } let heightMargin = 0 @@ -2841,7 +2895,18 @@ export function useModuleBasicSetting(tabNum) { } } - moduleSetupSurfaces.forEach((moduleSetupSurface, index) => { + // 대칭 지붕을 위한 calcAreaWidth 공유 객체 (south→north, west→east) + const symmetricWidthRef = { south: null, west: null } + + // 대칭 보정을 위해 south/west가 north/east보다 먼저 처리되도록 정렬 + const directionOrder = { south: 0, west: 1, north: 2, east: 3 } + const sortedModuleSetupSurfaces = [...moduleSetupSurfaces].sort((a, b) => { + const orderA = directionOrder[a.direction] ?? 4 + const orderB = directionOrder[b.direction] ?? 4 + return orderA - orderB + }) + + sortedModuleSetupSurfaces.forEach((moduleSetupSurface, index) => { moduleSetupSurface.fire('mousedown') const moduleSetupArray = [] @@ -2867,30 +2932,30 @@ export function useModuleBasicSetting(tabNum) { if (setupLocation === 'eaves') { // 흐름방향이 남쪽일때 if (moduleSetupSurface.direction === 'south') { - downFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + downFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'west') { - leftFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + leftFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'east') { - rightFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + rightFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'north') { - topFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + topFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } } else if (setupLocation === 'ridge') { //용마루 if (moduleSetupSurface.direction === 'south') { - topFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + topFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'west') { - rightFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + rightFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'east') { - leftFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + leftFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } if (moduleSetupSurface.direction === 'north') { - downFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer) + downFlowSetupModule(maxLengthLine, moduleSetupArray, moduleSetupSurface, containsBatchObjects, intvHor, intvVer, symmetricWidthRef) } } @@ -3070,13 +3135,26 @@ export function useModuleBasicSetting(tabNum) { type: 'flat', } } else { - rtnObj = { - target: index === 0 ? 'bottom' : 'top', - x1: pointX1, - y1: pointY1, - x2: pointX2, - y2: pointY2, - type: 'curve', + // NaN 체크: offset이 너무 커서 꼭짓점을 넘어가면 NaN 발생 + if (isNaN(pointX1) || isNaN(pointX2) || isNaN(pointY1) || isNaN(pointY2)) { + // NaN이면 꼭짓점 좌표 사용 (모듈 설치 영역 없음) + rtnObj = { + target: index === 0 ? 'bottom' : 'top', + x1: center.x1, + y1: center.y1, + x2: center.x1, + y2: center.y1, + type: 'curve', + } + } else { + rtnObj = { + target: index === 0 ? 'bottom' : 'top', + x1: pointX1, + y1: pointY1, + x2: pointX2, + y2: pointY2, + type: 'curve', + } } } @@ -3202,13 +3280,26 @@ export function useModuleBasicSetting(tabNum) { type: 'flat', } } else { - rtnObj = { - target: index === 0 ? 'left' : 'right', - x1: pointX1, - y1: pointY1, - x2: pointX2, - y2: pointY2, - type: 'curve', + // NaN 체크: offset이 너무 커서 꼭짓점을 넘어가면 NaN 발생 + if (isNaN(pointX1) || isNaN(pointX2) || isNaN(pointY1) || isNaN(pointY2)) { + // NaN이면 꼭짓점 좌표 사용 (모듈 설치 영역 없음) + rtnObj = { + target: index === 0 ? 'left' : 'right', + x1: center.x1, + y1: center.y1, + x2: center.x1, + y2: center.y1, + type: 'curve', + } + } else { + rtnObj = { + target: index === 0 ? 'left' : 'right', + x1: pointX1, + y1: pointY1, + x2: pointX2, + y2: pointY2, + type: 'curve', + } } } rtnObjArray.push(rtnObj) @@ -4099,6 +4190,7 @@ export function useModuleBasicSetting(tabNum) { left: leftRightFlowLine(moduleSetupSurface, length).find((obj) => obj.target === 'left'), right: leftRightFlowLine(moduleSetupSurface, length).find((obj) => obj.target === 'right'), } + return flowLines } diff --git a/src/hooks/module/useModuleTrestle.js b/src/hooks/module/useModuleTrestle.js index fc588d09..0d219401 100644 --- a/src/hooks/module/useModuleTrestle.js +++ b/src/hooks/module/useModuleTrestle.js @@ -61,7 +61,7 @@ export function useModuleTrestle(props) { const [lengthBase, setLengthBase] = useState(0) const [hajebichi, setHajebichi] = useState(0) const [cvrYn, setCvrYn] = useState('N') - const [cvrChecked, setCvrChecked] = useState(false) + const [cvrChecked, setCvrChecked] = useState(true) const [snowGdPossYn, setSnowGdPossYn] = useState('N') const [snowGdChecked, setSnowGdChecked] = useState(false) const [eavesMargin, setEavesMargin] = useState(0) @@ -88,7 +88,7 @@ export function useModuleTrestle(props) { setKerabaMargin(selectedRoof?.kerabaMargin ?? 0) setLengthBase(Math.round(selectedRoof?.length ?? 0)) setCvrYn(selectedRoof?.construction?.cvrYn ?? 'N') - setCvrChecked(selectedRoof?.construction?.cvrChecked ?? false) + setCvrChecked(selectedRoof?.construction?.cvrChecked ?? true) setSnowGdPossYn(selectedRoof?.construction?.snowGdPossYn ?? 'N') setSnowGdChecked(selectedRoof?.construction?.snowGdChecked ?? false) setTrestleDetail(selectedRoof?.trestleDetail) diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 88980cad..e4ee52a8 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -65,6 +65,10 @@ export const useTrestle = () => { if (+roofSizeSet === 3) { return } + const trestleDetail = surface.trestleDetail + if (!trestleDetail) { + return + } const construction = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex).construction if (!construction) { return @@ -76,8 +80,8 @@ export const useTrestle = () => { let isSnowGuard = construction.setupSnowCover let cvrLmtRow = construction.cvrLmtRow const direction = parent.direction - const rack = surface.trestleDetail.rack - let { rackQty, rackIntvlPct, rackYn, cvrPlvrYn, lessSupFitIntvlPct, lessSupFitQty } = surface.trestleDetail + const rack = trestleDetail.rack + let { rackQty, rackIntvlPct, rackYn, cvrPlvrYn, lessSupFitIntvlPct, lessSupFitQty } = trestleDetail if (!rack && lessSupFitIntvlPct === 0 && lessSupFitQty === 0) { //25/02/06 가대없음의 경우 랙정보가 없음 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/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index 072a2987..88c32d5f 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -179,7 +179,7 @@ export function useCanvasSetting(executeEffect = true) { layout: ['ROOF_ID_SLATE', 'ROOF_ID_SINGLE'].includes(item.roofMatlCd) ? ROOF_MATERIAL_LAYOUT.STAIRS : ROOF_MATERIAL_LAYOUT.PARALLEL, hajebichi: item.roofPchBase && parseInt(item.roofPchBase), pitch: item.inclBase ? parseInt(item.inclBase) : 4, - angle: getDegreeByChon(item.inclBase ? parseInt(item.inclBase): 4) //item.angle ? parseInt(item.angle) : 21.8, + angle: getDegreeByChon(item.inclBase ? parseInt(item.inclBase) : 4), //item.angle ? parseInt(item.angle) : 21.8, })) setRoofMaterials(roofLists) return roofLists @@ -231,8 +231,9 @@ export function useCanvasSetting(executeEffect = true) { setPolygonLinesActualSize(roof) }) changeCorridorDimensionText() + canvas.renderAll() } - }, [corridorDimension]) + }, [corridorDimension, canvasSetting]) useEffect(() => { if (!executeEffect) { @@ -801,25 +802,25 @@ export function useCanvasSetting(executeEffect = true) { /** 문자 글꼴 설정 */ wordFont: globalFont.commonText.fontFamily?.value ?? 'MS PGothic', wordFontStyle: globalFont.commonText.fontWeight?.value ?? 'normal', - wordFontSize: globalFont.commonText.fontSize?.value ?? 16, + wordFontSize: globalFont.commonText.fontSize?.value ?? 28, wordFontColor: globalFont.commonText.fontColor?.value ?? 'black', /** 흐름방향 글꼴 설정 */ flowFont: globalFont.flowText.fontFamily?.value ?? 'MS PGothic', flowFontStyle: globalFont.flowText.fontWeight?.value ?? 'normal', - flowFontSize: globalFont.flowText.fontSize?.value ?? 16, + flowFontSize: globalFont.flowText.fontSize?.value ?? 28, flowFontColor: globalFont.flowText.fontColor?.value ?? 'black', /** 치수 글꼴 설정 */ dimensioFont: globalFont.dimensionLineText.fontFamily?.value ?? 'MS PGothic', dimensioFontStyle: globalFont.dimensionLineText.fontWeight?.value ?? 'normal', - dimensioFontSize: globalFont.dimensionLineText.fontSize?.value ?? 16, + dimensioFontSize: globalFont.dimensionLineText.fontSize?.value ?? 28, dimensioFontColor: globalFont.dimensionLineText.fontColor?.value ?? 'black', /** 회로번호 글꼴 설정 */ circuitNumFont: globalFont.circuitNumberText.fontFamily?.value ?? 'MS PGothic', circuitNumFontStyle: globalFont.circuitNumberText.fontWeight?.value ?? 'normal', - circuitNumFontSize: globalFont.circuitNumberText.fontSize?.value ?? 16, + circuitNumFontSize: globalFont.circuitNumberText.fontSize?.value ?? 36, circuitNumFontColor: globalFont.circuitNumberText.fontColor?.value ?? 'black', /** 치수선 글꼴 설정 */ diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index bbf3628a..c84e00a7 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -75,13 +75,14 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { }, [arrow2]) useEffect(() => { - typeRef.current = type - clear() if (type === null) { initEvent() return } + initEvent() + typeRef.current = type + clear() addCanvasMouseEventListener('mouse:move', mouseMove) addCanvasMouseEventListener('mouse:down', mouseDown) addDocumentEventListener('keydown', document, keydown[type]) @@ -96,6 +97,10 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { return } + if (type === null) { + return + } + // 지붕의 각 꼭지점을 흡착점으로 설정 const roofsPoints = roofs.map((roof) => roof.points).flat() roofAdsorptionPoints.current = [...roofsPoints] @@ -113,6 +118,9 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { }, []) useEffect(() => { + if (type === null) { + return + } const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) if (roofs.length === 0) { return @@ -488,6 +496,10 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { mousePointerArr.current.push({ x: lastPoint.x + length1Value / 10, y: lastPoint.y + length2Value / 10 }) } else if (arrow1Value === '→' && arrow2Value === '↑') { mousePointerArr.current.push({ x: lastPoint.x + length1Value / 10, y: lastPoint.y - length2Value / 10 }) + } else if (arrow1Value === '←' && arrow2Value === '↓') { + mousePointerArr.current.push({ x: lastPoint.x - length1Value / 10, y: lastPoint.y + length2Value / 10 }) + } else if (arrow1Value === '←' && arrow2Value === '↑') { + mousePointerArr.current.push({ x: lastPoint.x - length1Value / 10, y: lastPoint.y - length2Value / 10 }) } drawLine() } diff --git a/src/hooks/roofcover/useOuterLineWall.js b/src/hooks/roofcover/useOuterLineWall.js index 850a761a..dbd9f104 100644 --- a/src/hooks/roofcover/useOuterLineWall.js +++ b/src/hooks/roofcover/useOuterLineWall.js @@ -678,6 +678,32 @@ export function useOuterLineWall(id, propertiesId) { }, ] }) + } else if (arrow1Value === '←' && arrow2Value === '↓') { + setPoints((prev) => { + if (prev.length === 0) { + return [] + } + return [ + ...prev, + { + x: prev[prev.length - 1].x - length1Value / 10, + y: prev[prev.length - 1].y + length2Value / 10, + }, + ] + }) + } else if (arrow1Value === '←' && arrow2Value === '↑') { + setPoints((prev) => { + if (prev.length === 0) { + return [] + } + return [ + ...prev, + { + x: prev[prev.length - 1].x - length1Value / 10, + y: prev[prev.length - 1].y - length2Value / 10, + }, + ] + }) } } } diff --git a/src/hooks/roofcover/useRoofShapePassivitySetting.js b/src/hooks/roofcover/useRoofShapePassivitySetting.js index ac36779c..6149ca39 100644 --- a/src/hooks/roofcover/useRoofShapePassivitySetting.js +++ b/src/hooks/roofcover/useRoofShapePassivitySetting.js @@ -48,9 +48,9 @@ export function useRoofShapePassivitySetting(id) { useEffect(() => { const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') if (!canvas.outerLineFix || outerLines.length === 0) { - swalFire({ text: getMessage('wall.line.not.found') }) + swalFire({ text: getMessage('wall.line.not.found'),icon: 'warning' }) closePopup(id) - return + //return } setIsLoading(true) }, []) @@ -142,8 +142,9 @@ export function useRoofShapePassivitySetting(id) { const handleConfirm = () => { if (!currentLineRef.current) { - alert('선택된 외곽선이 없습니다.') - return + //alert('선택된 외곽선이 없습니다.') + swalFire({ text: getMessage('wall.line.not.selected'), icon: 'warning' }) + //return } let attributes const offset = Number(offsetRef.current.value) / 10 @@ -210,8 +211,8 @@ export function useRoofShapePassivitySetting(id) { }) if (!checkedAllSetting) { - swalFire({ text: '설정이 완료되지 않은 외벽선이 있습니다.', icon: 'warning' }) - return + swalFire({ text: getMessage('modal.canvas.setting.roofline.properties.setting.not.setting'), icon: 'warning' }) + //return } exceptObjs.forEach((obj) => { diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index 3dfea465..1da292e8 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -241,6 +241,7 @@ export function usePlacementShapeDrawing(id) { originY: 'center', direction: 'south', pitch: globalPitch, + from: 'surface', }) setSurfaceShapePattern(roof, roofDisplay.column) @@ -680,6 +681,32 @@ export function usePlacementShapeDrawing(id) { }, ] }) + } else if (arrow1Value === '←' && arrow2Value === '↓') { + setPoints((prev) => { + if (prev.length === 0) { + return [] + } + return [ + ...prev, + { + x: prev[prev.length - 1].x - length1Value / 10, + y: prev[prev.length - 1].y + length2Value / 10, + }, + ] + }) + } else if (arrow1Value === '←' && arrow2Value === '↑') { + setPoints((prev) => { + if (prev.length === 0) { + return [] + } + return [ + ...prev, + { + x: prev[prev.length - 1].x - length1Value / 10, + y: prev[prev.length - 1].y - length2Value / 10, + }, + ] + }) } } } diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index d485b719..de5534a5 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -1045,6 +1045,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { newPolygon.set({ originWidth: width, originHeight: height, + from: 'surface', }) // 캔버스에 추가 diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index 1f6a33b0..25dd2a1a 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.js @@ -117,7 +117,6 @@ export function useContextMenu() { useEffect(() => { currentMenuSetting() }, [gridColor, currentMenu]) - useEffect(() => { if (currentContextMenu?.component) addPopup(popupId, 1, currentContextMenu?.component) }, [currentContextMenu]) @@ -158,11 +157,40 @@ export function useContextMenu() { ]) } break + + case 'outerLine': { + setContextMenu([ + [ + { + id: 'roofMaterialPlacement', + name: getMessage('contextmenu.roof.material.placement'), + component: , + }, + + { + id: 'roofMaterialRemoveAll', + name: getMessage('contextmenu.roof.material.remove.all'), + fn: () => removeAllRoofMaterial(), + }, + { + id: 'selectMove', + name: getMessage('contextmenu.select.move'), + fn: (currentMousePos) => { + moveRoofMaterial(currentMousePos) + }, + }, + { + id: 'wallLineRemove', + name: getMessage('contextmenu.wallline.remove'), + fn: (currentMousePos) => { + removeOuterLines(currentMousePos) + }, + }, + ], + ]) + break + } case 'roof': - case 'auxiliaryLine': - case 'hip': - case 'ridge': - case 'eaveHelpLine': if (selectedMenu === 'surface') { setContextMenu([ [ @@ -249,6 +277,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', diff --git a/src/hooks/useLine.js b/src/hooks/useLine.js index 3e1d72e6..df876fb1 100644 --- a/src/hooks/useLine.js +++ b/src/hooks/useLine.js @@ -166,8 +166,12 @@ export const useLine = () => { * @param line * @param direction polygon의 방향 * @param pitch + * @param forceUpdate */ - const setActualSize = (line, direction, pitch = globalPitch) => { + const setActualSize = (line, direction, pitch = globalPitch, forceUpdate = false) => { + if (line.attributes.isCalculated && !forceUpdate) { + return + } const { x1, y1, x2, y2 } = line const isHorizontal = y1 === y2 @@ -209,7 +213,11 @@ export const useLine = () => { } } - line.attributes = { ...line.attributes, actualSize: Number(line.attributes.actualSize.toFixed(0)) } + line.attributes = { + ...line.attributes, + actualSize: Number(line.attributes.actualSize.toFixed(0)), + isCalculated: true, + } } return { diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index e84ff041..9481af67 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -973,7 +973,6 @@ export const usePolygon = () => { }) canvas.renderAll() - /*polygonLines.forEach((line) => { line.set({ strokeWidth: 10 }) canvas.add(line) @@ -1023,7 +1022,7 @@ export const usePolygon = () => { for (let i = divideLines.length - 1; i >= 0; i--) { const line = divideLines[i] const { intersections, startPoint, endPoint } = line - console.log("intersections::::::::::", intersections) + if (intersections.length === 1) { const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y] const newLinePoint2 = [intersections[0].x, intersections[0].y, line.x2, line.y2] @@ -1043,16 +1042,25 @@ export const usePolygon = () => { name: 'newLine', }) + // 두 라인 중 큰 길이로 통일 + const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10 + const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10 + const maxLength = Math.max(length1, length2) + const unifiedPlaneSize = line.attributes.planeSize ?? maxLength + const unifiedActualSize = line.attributes.actualSize ?? maxLength + newLine1.attributes = { ...line.attributes, - planeSize: Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10, - actualSize: Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10, + planeSize: unifiedPlaneSize, + actualSize: unifiedActualSize, } + newLine1.length = maxLength newLine2.attributes = { ...line.attributes, - planeSize: Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10, - actualSize: Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10, + planeSize: unifiedPlaneSize, + actualSize: unifiedActualSize, } + newLine2.length = maxLength newLines.push(newLine1, newLine2) divideLines.splice(i, 1) // 기존 line 제거 @@ -1071,11 +1079,13 @@ export const usePolygon = () => { name: 'newLine', }) + const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 newLine.attributes = { ...line.attributes, - planeSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, - actualSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, + planeSize: line.attributes.planeSize ?? calcLength, + actualSize: line.attributes.actualSize ?? calcLength, } + newLine.length = line.attributes.planeSize ?? calcLength newLines.push(newLine) currentPoint = minDistancePoint @@ -1089,11 +1099,13 @@ export const usePolygon = () => { attributes: line.attributes, name: 'newLine', }) + const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 newLine.attributes = { ...line.attributes, - planeSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, - actualSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, + planeSize: line.attributes.planeSize ?? lastCalcLength, + actualSize: line.attributes.actualSize ?? lastCalcLength, } + newLine.length = line.attributes.planeSize ?? lastCalcLength newLines.push(newLine) divideLines.splice(i, 1) // 기존 line 제거 @@ -1931,39 +1943,57 @@ export const usePolygon = () => { /** * 폴리곤의 라인 속성을 복도치수, 실제치수에 따라 actualSize 설정 * @param polygon + * @param forceUpdate */ - const setPolygonLinesActualSize = (polygon) => { + const setPolygonLinesActualSize = (polygon, forceUpdate = false) => { if (!polygon.lines || polygon.lines.length === 0 || !polygon.roofMaterial) { return } - // createdRoofs들의 모든 lines를 확인해서 length값이 1이하인 차이가 있으면 통일 시킨다. - const allRoofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) - const allRoofLines = allRoofs.flatMap((roof) => roof.lines) - for (let i = 0; i < allRoofLines.length; i++) { - for (let j = i + 1; j < allRoofLines.length; j++) { - const line1 = allRoofLines[i] - const line2 = allRoofLines[j] - const diff = Math.abs(line1.length - line2.length) - if (diff > 0 && diff <= 2) { - const minLength = Math.min(line1.length, line2.length) - line1.setLengthByValue(minLength * 10) - line2.setLengthByValue(minLength * 10) + // 배치면으로 그린 내용은 업데이트를 해준다. + if (polygon.from === 'surface') { + forceUpdate = true + } + + if (polygon.from !== 'surface') { + // createdRoofs들의 모든 lines를 확인해서 length값이 1이하인 차이가 있으면 통일 시킨다. + const allRoofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) + const allRoofLines = allRoofs.flatMap((roof) => roof.lines) + for (let i = 0; i < allRoofLines.length; i++) { + for (let j = i + 1; j < allRoofLines.length; j++) { + const line1 = allRoofLines[i] + const line2 = allRoofLines[j] + const diff = Math.abs(line1.length - line2.length) + if (diff > 0 && diff <= 2) { + const maxLength = Math.max(line1.length, line2.length) + line1.setLengthByValue(maxLength * 10) + line2.setLengthByValue(maxLength * 10) + // attributes도 통일 + const maxPlaneSize = Math.max(line1.attributes.planeSize || 0, line2.attributes.planeSize || 0) + const maxActualSize = Math.max(line1.attributes.actualSize || 0, line2.attributes.actualSize || 0) + line1.attributes.planeSize = maxPlaneSize + line1.attributes.actualSize = maxActualSize + line2.attributes.planeSize = maxPlaneSize + line2.attributes.actualSize = maxActualSize + } } } } polygon.lines.forEach((line, index) => { + if (line.attributes.isCalculated && !forceUpdate) { + return + } //text 와 planSize 및 actualSize가 안맞는 문제 - const nextText = polygon?.texts?.[index]?.text + /*const nextText = polygon?.texts?.[index]?.text const nextPlaneSize = Number(nextText) - if (nextText != null && nextText !== '' && Number.isFinite(nextPlaneSize) ) { - if(line.attributes.actualSize !== nextPlaneSize && line.attributes.planeSize !== nextPlaneSize) { + if (nextText != null && nextText !== '' && Number.isFinite(nextPlaneSize)) { + if (line.attributes.actualSize !== nextPlaneSize && line.attributes.planeSize !== nextPlaneSize) { line.attributes.planeSize = nextPlaneSize } + }*/ - } - setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch) + setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch, forceUpdate) }) addLengthText(polygon) diff --git a/src/locales/ja.json b/src/locales/ja.json index 6f762667..0c589e1e 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -997,7 +997,9 @@ "estimate.detail.itemTableHeader.amount": "数量", "estimate.detail.itemTableHeader.unit": "単位", "estimate.detail.itemTableHeader.salePrice": "単価", + "estimate.detail.itemTableHeader.unitPrice": "定価", "estimate.detail.itemTableHeader.saleTotPrice": "金額(税別)", + "estimate.detail.itemTableHeader.unitTotprice": "金額(税別)", "estimate.detail.docPopup.title": "見積書出力オプションの設定", "estimate.detail.docPopup.explane": "ダウンロードする文書オプションを選択し、[見積書出力]ボタンをクリックします。", "estimate.detail.docPopup.schUnitPriceFlg": "ダウンロードファイル", @@ -1111,6 +1113,7 @@ "module.layout.setup.has.zero.value": "モジュールの列数、段数を入力して下さい。", "modal.placement.initial.setting.plan.drawing.only.number": "(※数字は[半角]入力のみ可能です。)", "wall.line.not.found": "外壁がありません", + "wall.line.not.selected": "選択された外郭線がありません。", "roof.line.not.found": "屋根形状がありません", "roof.material.can.not.delete": "割り当てられた配置面があります。", "chidory.can.not.install": "千鳥配置できない工法です。", diff --git a/src/locales/ko.json b/src/locales/ko.json index 93b1a4f1..6ada9943 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -997,7 +997,9 @@ "estimate.detail.itemTableHeader.amount": "수량", "estimate.detail.itemTableHeader.unit": "단위", "estimate.detail.itemTableHeader.salePrice": "단가", + "estimate.detail.itemTableHeader.unitPrice": "정가", "estimate.detail.itemTableHeader.saleTotPrice": "금액(부가세별도)", + "estimate.detail.itemTableHeader.unitTotprice": "금액(부가세별도)", "estimate.detail.docPopup.title": "문서다운로드 옵션설정", "estimate.detail.docPopup.explane": "다운로드할 문서 옵션을 선택한 후 문서 다운로드 버튼을 클릭합니다.", "estimate.detail.docPopup.schUnitPriceFlg": "다운로드 파일", @@ -1111,6 +1113,7 @@ "module.layout.setup.has.zero.value": "모듈의 열수, 단수를 입력해 주세요.", "modal.placement.initial.setting.plan.drawing.only.number": "(※ 숫자는 [반각]입력만 가능합니다.)", "wall.line.not.found": "외벽선이 없습니다.", + "wall.line.not.selected": "선택된 외곽선이 없습니다.", "roof.line.not.found": "지붕형상이 없습니다.", "roof.material.can.not.delete": "할당된 배치면이 있습니다.", "chidory.can.not.install": "치조 불가 공법입니다.", diff --git a/src/store/fontAtom.js b/src/store/fontAtom.js index 3cc4396c..1e42a6b6 100644 --- a/src/store/fontAtom.js +++ b/src/store/fontAtom.js @@ -1,11 +1,14 @@ import { atom, selectorFamily } from 'recoil' -const defaultFont = { +const makeDefaultFont = (fontSize) => ({ fontFamily: { id: 1, name: 'MS PGothic', value: 'MS PGothic' }, fontWeight: { id: 'normal', name: '보통', value: 'normal' }, - fontSize: { id: 16, name: 16, value: 16 }, + fontSize: { id: fontSize, name: fontSize, value: fontSize }, fontColor: { id: 'black', name: '검정색', value: 'black' }, -} +}) + +const defaultFont = makeDefaultFont(28) +const defaultCircuitNumberFont = makeDefaultFont(36) export const globalFontAtom = atom({ key: 'fontAtom', @@ -14,7 +17,7 @@ export const globalFontAtom = atom({ dimensionLineText: defaultFont, flowText: defaultFont, lengthText: defaultFont, - circuitNumberText: defaultFont, + circuitNumberText: defaultCircuitNumberFont, }, }) diff --git a/src/util/common-utils.js b/src/util/common-utils.js index 0a1265fe..2b3fe37d 100644 --- a/src/util/common-utils.js +++ b/src/util/common-utils.js @@ -94,6 +94,17 @@ export const inputNumberCheck = (e) => { } } +// 영문, 숫자, 특수문자(ASCII)만 입력 체크 +export const inputUserIdCheck = (e) => { + const input = e.target + const allowedRegex = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/g + if (allowedRegex.test(input.value)) { + input.value = input.value + } else { + input.value = input.value.replace(/[^A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/g, '') + } +} + // 값이 숫자인지 확인 export const numberCheck = (value) => { return !isNaN(value) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 47e8f4e9..931362f4 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -5,6 +5,7 @@ import { getAdjacent, getDegreeByChon, isPointOnLine, isPointOnLineNew } from '@ import { QPolygon } from '@/components/fabric/QPolygon' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import * as turf from '@turf/turf' const TWO_PI = Math.PI * 2 const EPSILON = 1e-10 //좌표계산 시 최소 차이값 @@ -248,6 +249,47 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) { return paddingPolygon } +/** + * 자기교차(self-intersection)가 있는 폴리곤을 정리하는 함수 + * turf.js의 unkinkPolygon을 사용하여 꼬인 부분을 제거하고 가장 큰 폴리곤을 반환 + * @param {Array} vertices - [{x, y}, ...] 형태의 점 배열 + * @returns {Array} 정리된 점 배열 + */ +export function cleanSelfIntersectingPolygon(vertices) { + if (!vertices || vertices.length < 3) return vertices + + try { + // vertices를 GeoJSON 폴리곤으로 변환 + const coords = vertices.map((p) => [p.x, p.y]) + coords.push([vertices[0].x, vertices[0].y]) // ring 닫기 + + const turfPoly = turf.polygon([coords]) + + // 자기교차 검사 + const kinked = turf.kinks(turfPoly) + + if (kinked.features.length === 0) { + return vertices // 꼬임 없음 + } + + // 꼬인 폴리곤을 분리 + const unkinked = turf.unkinkPolygon(turfPoly) + + if (unkinked.features.length > 0) { + // 가장 큰 면적의 폴리곤 선택 + const largest = unkinked.features.reduce((max, f) => (turf.area(f) > turf.area(max) ? f : max)) + + // GeoJSON 좌표를 다시 {x, y} 형태로 변환 + const cleanedCoords = largest.geometry.coordinates[0] + return cleanedCoords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + } + } catch (e) { + console.warn('Failed to clean self-intersecting polygon:', e) + } + + return vertices +} + export default function offsetPolygon(vertices, offset) { const polygon = createPolygon(vertices) const arcSegments = 0 @@ -255,25 +297,29 @@ export default function offsetPolygon(vertices, offset) { const originPolygon = new QPolygon(vertices, { fontSize: 0 }) originPolygon.setViewLengthText(false) + let result if (offset > 0) { - let result = createMarginPolygon(polygon, offset, arcSegments).vertices - const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) + let marginResult = createMarginPolygon(polygon, offset, arcSegments).vertices + const allPointsOutside = marginResult.every((point) => !originPolygon.inPolygon(point)) if (allPointsOutside) { - return createMarginPolygon(polygon, offset, arcSegments).vertices + result = createMarginPolygon(polygon, offset, arcSegments).vertices } else { - return createPaddingPolygon(polygon, offset, arcSegments).vertices + result = createPaddingPolygon(polygon, offset, arcSegments).vertices } } else { - let result = createPaddingPolygon(polygon, offset, arcSegments).vertices - const allPointsInside = result.every((point) => originPolygon.inPolygon(point)) + let paddingResult = createPaddingPolygon(polygon, offset, arcSegments).vertices + const allPointsInside = paddingResult.every((point) => originPolygon.inPolygon(point)) if (allPointsInside) { - return createPaddingPolygon(polygon, offset, arcSegments).vertices + result = createPaddingPolygon(polygon, offset, arcSegments).vertices } else { - return createMarginPolygon(polygon, offset, arcSegments).vertices + result = createMarginPolygon(polygon, offset, arcSegments).vertices } } + + // 자기교차(꼬임) 제거 + return cleanSelfIntersectingPolygon(result) } function normalizePoint(point) {