diff --git a/package.json b/package.json index 628550a1..c11ef8dc 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@turf/turf": "^7.0.0", + "dayjs": "^1.11.13", "postcss": "^8", "prettier": "^3.3.3", "prisma": "^5.18.0", diff --git a/src/components/SettingsModal.jsx b/src/components/SettingsModal.jsx index 3cf9997b..dc7dc5ba 100644 --- a/src/components/SettingsModal.jsx +++ b/src/components/SettingsModal.jsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input } from '@nextui-org/react' import { useRecoilState, useRecoilValue } from 'recoil' import { modalContent, modalState } from '@/store/modalAtom' -import { guideLineState } from '@/store/canvasAtom' +import { guideLineState, horiGuideLinesState, vertGuideLinesState } from '@/store/canvasAtom' import { fabric } from 'fabric' export default function SettingsModal(props) { @@ -16,6 +16,8 @@ export default function SettingsModal(props) { const [open, setOpen] = useRecoilState(modalState) const [guideLine, setGuideLine] = useRecoilState(guideLineState) + const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState) + const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState) const gridSettingArray = [] @@ -63,6 +65,7 @@ export default function SettingsModal(props) { lockScalingX: true, lockScalingY: true, name: 'guideLine', + direction: 'horizontal', }, ) canvasProps.add(horizontalLine) @@ -82,6 +85,7 @@ export default function SettingsModal(props) { lockScalingX: true, lockScalingY: true, name: 'guideLine', + direction: 'vertical', }, ) canvasProps.add(verticalLine) @@ -99,6 +103,16 @@ export default function SettingsModal(props) { moduleHoriLength: moduleHoriLength, } gridSettingArray.push(recoilObj) + const newHoriGuideLines = [...horiGuideLines] + horizontalLineArray.forEach((line) => { + newHoriGuideLines.push(line) + }) + const newVertGuideLines = [...vertGuideLines] + verticalLineArray.forEach((line) => { + newVertGuideLines.push(line) + }) + setHoriGuideLines(newHoriGuideLines) + setVertGuideLines(newVertGuideLines) } if (gridCheckedValue.includes('dot')) { @@ -170,6 +184,8 @@ export default function SettingsModal(props) { guideLines?.forEach((item) => canvasProps.remove(item)) canvasProps.renderAll() setGuideLine([]) + setHoriGuideLines([]) + setVertGuideLines([]) } else { alert('그리드가 없습니다.') return diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index bcc8b7d7..0dc724f3 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -425,6 +425,8 @@ export function useCanvas(id) { 'maxY', 'minX', 'minY', + 'x', + 'y', ]) const str = JSON.stringify(objs) diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index 89a15b3a..ee2f2fdd 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -19,9 +19,9 @@ export function useCanvasEvent() { canvas?.on('selection:cleared', selectionEvent.cleared) canvas?.on('selection:created', selectionEvent.created) canvas?.on('selection:updated', selectionEvent.updated) - canvas?.on('object:added', () => { + /*canvas?.on('object:added', () => { document.addEventListener('keydown', handleKeyDown) - }) + })*/ canvas?.on('object:removed', objectEvent.removed) } diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index ae970461..4d6dc238 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { calculateIntersection, distanceBetweenPoints, @@ -25,6 +25,8 @@ import { templateTypeState, wallState, guideLineState, + horiGuideLinesState, + vertGuideLinesState, } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' @@ -64,23 +66,22 @@ export function useMode() { const compass = useRecoilValue(compassState) const [isCellCenter, setIsCellCenter] = useState(false) - const guideLineInfo = useRecoilValue(guideLineState) + const [guideLineInfo, setGuideLineInfo] = useRecoilState(guideLineState) const [guideLineMode, setGuideLineMode] = useState(false) const [guideDotMode, setGuideDotMode] = useState(false) + const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState) + const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState) + useEffect(() => { - // 이벤트 리스너 추가 // if (!canvas) { // canvas?.setZoom(0.8) // return // } - document.addEventListener('keydown', handleKeyDown) + if (!canvas) return + setCanvas(canvas) canvas?.on('mouse:move', drawMouseLines) - // 컴포넌트가 언마운트될 때 이벤트 리스너 제거 - return () => { - document.removeEventListener('keydown', handleKeyDown) - } }, [canvas]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함 useEffect(() => { @@ -100,15 +101,10 @@ export function useMode() { }, [endPoint]) useEffect(() => { + changeMode(canvas, mode) canvas?.off('mouse:move') canvas?.on('mouse:move', drawMouseLines) - changeMode(canvas, mode) - /* - if (mode === Mode.EDIT) { - canvas?.off('mouse:down') - canvas?.on('mouse:down', mouseEvent.editMode) - }*/ - }, [mode]) + }, [mode, horiGuideLines, vertGuideLines]) useEffect(() => { setGuideLineMode(false) @@ -142,8 +138,8 @@ export function useMode() { } if (isGuideLineMode) { - horizontalLineArray = [...guideLineState[0].horizontalLineArray] - verticalLineArray = [...guideLineState[0].verticalLineArray] + horizontalLineArray = [...horiGuideLines] + verticalLineArray = [...vertGuideLines] guideLineLengthHori = Number(guideLineState[0].moduleHoriLength) guideLineLengthVert = Number(guideLineState[0].moduleVertLength) } @@ -163,13 +159,8 @@ export function useMode() { if (mode === Mode.EDIT || mode === Mode.ADSORPTION_POINT) { let adsorptionPoint = adsorptionPointList.length > 0 ? findClosestPoint(pointer, adsorptionPointList) : null - - if (isGuideLineMode && isGuideDotMode) { - const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) - const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) - const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) - const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) - + if ((horiGuideLines.length > 0 || vertGuideLines.length > 0) && guideDotMode) { + } else if (guideDotMode) { const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) @@ -181,35 +172,28 @@ export function useMode() { if (isAttachX && isAttachY) { newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 - } else { - if (Math.min(xDiff, yDiff) <= 20) { - if (xDiff < yDiff) { - newX = closetVerticalLine.x1 - newY = pointer.y - } else { - newX = pointer.x - newY = closestHorizontalLine.y1 - } + } + } else if (horiGuideLines.length > 0 || vertGuideLines.length > 0) { + const closestHorizontalLine = getClosestHorizontalLine(pointer, horiGuideLines) + const closetVerticalLine = getClosestVerticalLine(pointer, vertGuideLines) + let intersection = null + let intersectionDistance = Infinity + + if (closestHorizontalLine && closetVerticalLine) { + intersection = calculateIntersection(closestHorizontalLine, closetVerticalLine) + if (intersection) { + intersectionDistance = distanceBetweenPoints(pointer, intersection) } } - } else if (isGuideDotMode) { - const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) - const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) - const xRate = x / guideLineLengthHori - const yRate = y / guideLineLengthVert - const isAttachX = xRate >= 0.4 && xRate <= 0.7 - const isAttachY = yRate >= 0.4 && yRate <= 0.7 + let xDiff, yDiff - if (isAttachX && isAttachY) { - newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 - newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 + if (closetVerticalLine) { + xDiff = Math.abs(pointer.x - closetVerticalLine.x1) + } + if (closestHorizontalLine) { + yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) } - } else if (isGuideLineMode) { - const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) - const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) - const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) - const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) @@ -218,23 +202,26 @@ export function useMode() { const yRate = y / guideLineLengthVert const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7 - if (isAttachX && isAttachY) { newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 } else { - if (Math.min(xDiff, yDiff) <= 20) { - if (xDiff < yDiff) { - newX = closetVerticalLine.x1 - newY = pointer.y - } else { - newX = pointer.x - newY = closestHorizontalLine.y1 + if (intersection && intersectionDistance < 20) { + newX = intersection.x + newY = intersection.y + } else { + if (Math.min(xDiff, yDiff) <= 20) { + if (xDiff < yDiff) { + newX = closetVerticalLine.x1 + newY = pointer.y + } else { + newX = pointer.x + newY = closestHorizontalLine.y1 + } } } } } - if (adsorptionPoint && distanceBetweenPoints(pointer, adsorptionPoint) < 20) { newX = adsorptionPoint.left newY = adsorptionPoint.top @@ -362,16 +349,13 @@ export function useMode() { // 모드에 따른 마우스 이벤트 변경 const changeMouseEvent = (mode) => { - canvas?.off('mouse:down') switch (mode) { case 'drawLine': canvas?.on('mouse:down', mouseEvent.drawLineModeLeftClick) - window.document.removeEventListener('contextmenu', mouseEvent.drawLineModeRightClick) - window.document.addEventListener('contextmenu', mouseEvent.drawLineModeRightClick) + document.addEventListener('contextmenu', mouseEvent.drawLineModeRightClick) break case 'edit': canvas?.on('mouse:down', mouseEvent.editMode) - break case 'textbox': canvas?.on('mouse:down', mouseEvent.textboxMode) @@ -394,9 +378,6 @@ export function useMode() { } } - // 모드에 따른 키보드 이벤트 변경 - const changeKeyboardEvent = (mode) => {} - const keyValid = () => { if (points.current.length === 0) { alert('시작점을 선택해주세요') @@ -552,77 +533,98 @@ export function useMode() { } } - const handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowDown': { - if (!keyValid()) { - return - } - const verticalLength = Number(prompt('길이를 입력하세요:')) - const horizontalLength = 0 + const mouseAndkeyboardEventClear = () => { + canvas?.off('mouse:down') + Object.keys(mouseEvent).forEach((key) => { + canvas?.off('mouse:down', mouseEvent[key]) + document.removeEventListener('contextmenu', mouseEvent[key]) + }) - drawCircleAndLine(verticalLength, horizontalLength) + Object.keys(keyboardEvent).forEach((key) => { + document.removeEventListener('keydown', keyboardEvent[key]) + }) + } - break - } - case 'ArrowUp': { - if (!keyValid()) { - return - } - const verticalLength = -Number(prompt('길이를 입력하세요:')) - const horizontalLength = 0 + const keyboardEvent = { + // rerendering을 막기 위해 useCallback 사용 + editMode: useCallback( + (e) => { + e.preventDefault() + switch (e.key) { + case 'ArrowDown': { + if (!keyValid()) { + return + } + const verticalLength = Number(prompt('길이를 입력하세요:')) + const horizontalLength = 0 - drawCircleAndLine(verticalLength, horizontalLength) + drawCircleAndLine(verticalLength, horizontalLength) - break - } - case 'ArrowLeft': { - if (!keyValid()) { - return - } - const verticalLength = 0 - const horizontalLength = -Number(prompt('길이를 입력하세요:')) - - drawCircleAndLine(verticalLength, horizontalLength) - - break - } - case 'ArrowRight': { - if (!keyValid()) { - return - } - - const verticalLength = 0 - const horizontalLength = Number(prompt('길이를 입력하세요:')) - - drawCircleAndLine(verticalLength, horizontalLength) - - break - } - - case 'Enter': { - const result = prompt('입력하세요 (a(A패턴),b(B패턴),t(지붕))') - - switch (result) { - case 'a': - applyTemplateA() break - case 'b': - applyTemplateB() + } + case 'ArrowUp': { + if (!keyValid()) { + return + } + const verticalLength = -Number(prompt('길이를 입력하세요:')) + const horizontalLength = 0 + + drawCircleAndLine(verticalLength, horizontalLength) + break - case 't': - templateMode() + } + case 'ArrowLeft': { + if (!keyValid()) { + return + } + const verticalLength = 0 + const horizontalLength = -Number(prompt('길이를 입력하세요:')) + + drawCircleAndLine(verticalLength, horizontalLength) + break + } + case 'ArrowRight': { + if (!keyValid()) { + return + } + + const verticalLength = 0 + const horizontalLength = Number(prompt('길이를 입력하세요:')) + + drawCircleAndLine(verticalLength, horizontalLength) + + break + } + + case 'Enter': { + const result = prompt('입력하세요 (a(A패턴),b(B패턴),t(지붕))') + + switch (result) { + case 'a': + applyTemplateA() + break + case 'b': + applyTemplateB() + break + case 't': + templateMode() + break + } + } } - } - } + }, + [canvas], + ), } const changeMode = (canvas, mode) => { + mouseAndkeyboardEventClear() setMode(mode) setCanvas(canvas) // mode별 이벤트 변경 + changeMouseEvent(mode) changeKeyboardEvent(mode) @@ -657,40 +659,82 @@ export function useMode() { } } + const changeKeyboardEvent = (mode) => { + if (mode === Mode.EDIT) { + switch (mode) { + case 'edit': + document.addEventListener('keydown', keyboardEvent.editMode) + break + } + } + } + const mouseEvent = { drawLineModeLeftClick: (options) => { + if (mode !== Mode.DRAW_LINE) { + return + } const pointer = canvas?.getPointer(options.e) const line = new QLine( - [pointer.x, 0, pointer.x, canvas.height], // y축에 1자 선을 그립니다. + [pointer.x, 0, pointer.x, canvasSize.vertical], // y축에 1자 선을 그립니다. { - stroke: 'black', - strokeWidth: 2, - viewLengthText: true, - selectable: false, - fontSize: fontSize, + stroke: 'gray', + strokeWidth: 1, + selectable: true, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'guideLine', + direction: 'vertical', }, ) canvas?.add(line) canvas?.renderAll() - }, - drawLineModeRightClick: (options) => { - const line = new fabric.Line( - [0, options.offsetY, canvas.width, options.offsetY], // y축에 1자 선을 그립니다. - { - stroke: 'black', - strokeWidth: 2, - viewLengthText: true, - selectable: false, - fontSize: fontSize, - }, - ) - canvas?.add(line) - canvas?.renderAll() + const newVerticalLineArray = [...vertGuideLines] + newVerticalLineArray.push(line) + + setVertGuideLines(newVerticalLineArray) }, + drawLineModeRightClick: useCallback( + (options) => { + document.removeEventListener('contextmenu', mouseEvent.drawLineModeRightClick) + if (mode !== Mode.DRAW_LINE) { + return + } + const line = new fabric.Line( + [0, options.offsetY, canvasSize.horizontal, options.offsetY], // y축에 1자 선을 그립니다. + { + stroke: 'gray', + strokeWidth: 1, + selectable: true, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'guideLine', + direction: 'horizontal', + }, + ) + + canvas?.add(line) + canvas?.renderAll() + + const newHorizontalLineArray = [...horiGuideLines] + newHorizontalLineArray.push(line) + setHoriGuideLines(newHorizontalLineArray) + }, + [canvas, mode, horiGuideLines], + ), editMode: (options) => { + if (mode !== Mode.EDIT) { + return + } let pointer = canvas?.getPointer(options.e) if (getInterSectPointByMouseLine()) { @@ -801,7 +845,9 @@ export function useMode() { canvas?.renderAll() }, + textboxMode: (options) => { + if (mode !== Mode.TEXTBOX) return if (canvas?.getActiveObject()?.type === 'textbox') return const pointer = canvas?.getPointer(options.e) @@ -821,6 +867,7 @@ export function useMode() { }) }, drawRectMode: (o) => { + if (mode !== Mode.DRAW_RECT) return let rect, isDown, origX, origY isDown = true const pointer = canvas.getPointer(o.e) @@ -862,7 +909,8 @@ export function useMode() { }) }, // 흡착점 추가 - adsorptionPoint(o) { + adsorptionPoint: (o) => { + if (mode !== Mode.ADSORPTION_POINT) return const pointer = canvas.getPointer(o.e) let newX = pointer.x let newY = pointer.y @@ -4484,7 +4532,6 @@ export function useMode() { canvas?.off('mouse:move') canvas?.off('mouse:out') - document.removeEventListener('keydown', handleKeyDown) const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof') roofs.forEach((roof, index) => { const offsetPolygonPoint = offsetPolygon(roof.points, -20) diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index c2b245d7..8d16b98f 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -95,3 +95,15 @@ export const currentObjectState = atom({ default: null, dangerouslyAllowMutability: true, }) + +export const horiGuideLinesState = atom({ + key: 'horiGuideLines', + default: [], + dangerouslyAllowMutability: true, +}) + +export const vertGuideLinesState = atom({ + key: 'vertGuideLines', + default: [], + dangerouslyAllowMutability: true, +}) diff --git a/src/util/common-utils.js b/src/util/common-utils.js index 9f53fede..04f3dccf 100644 --- a/src/util/common-utils.js +++ b/src/util/common-utils.js @@ -9,3 +9,45 @@ export const isObjectNotEmpty = (obj) => { } return Object.keys(obj).length > 0 } + +/** + * ex) const params = {page:10, searchDvsnCd: 20} + * @param {*} params + * @returns page=10&searchDvsnCd=20 + */ +export const queryStringFormatter = (params = {}) => { + const queries = [] + Object.keys(params).forEach((parameterKey) => { + const parameterValue = params[parameterKey] + + if (parameterValue === undefined || parameterValue === null) { + return + } + + // string trim + if (typeof parameterValue === 'string' && !parameterValue.trim()) { + return + } + + // array to query string + if (Array.isArray(parameterValue)) { + // primitive type + if (parameterValue.every((v) => typeof v === 'number' || typeof v === 'string')) { + queries.push(`${encodeURIComponent(parameterKey)}=${parameterValue.map((v) => encodeURIComponent(v)).join(',')}`) + return + } + // reference type + if (parameterValue.every((v) => typeof v === 'object' && v !== null)) { + parameterValue.map((pv, i) => { + return Object.keys(pv).forEach((valueKey) => { + queries.push(`${encodeURIComponent(`${parameterKey}[${i}].${valueKey}`)}=${encodeURIComponent(pv[valueKey])}`) + }) + }) + return + } + } + // 나머지 + queries.push(`${encodeURIComponent(parameterKey)}=${encodeURIComponent(parameterValue)}`) + }) + return queries.join('&') +} diff --git a/yarn.lock b/yarn.lock index d0d3b65e..77b7ed38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4319,6 +4319,11 @@ date-fns@^3.3.1: resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@4, debug@^4.3.3, debug@^4.3.4: version "4.3.5" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz"