Merge branch 'dev' into dev-yj

# Conflicts:
#	src/components/GridSettingsModal.jsx
This commit is contained in:
yjnoh 2024-09-02 13:50:42 +09:00
commit fd118d01b2
16 changed files with 1885 additions and 247 deletions

View File

@ -1,6 +1,6 @@
NEXT_PUBLIC_TEST="테스트변수입니다. development" NEXT_PUBLIC_TEST="테스트변수입니다. development"
NEXT_PUBLIC_API_SERVER_PATH="http://1.248.227.176:38080" NEXT_PUBLIC_API_SERVER_PATH="http://1.248.227.176:38080"
# NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080" # NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true" DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true"

View File

@ -31,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@turf/turf": "^7.0.0", "@turf/turf": "^7.0.0",
"dayjs": "^1.11.13",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^5.18.0", "prisma": "^5.18.0",

10
qcast-front.iml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/qcast-front">
<excludeFolder url="file://$MODULE_DIR$/qcast-front/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,16 @@
import Hero from '@/components/Hero'
import Settings from '@/components/Settings'
import { initCheck } from '@/util/session-util'
export default async function SettingsPage() {
await initCheck()
return (
<>
<Hero title="Canvas Setting" />
<div className="flex flex-col justify-center my-8">
<Settings />
</div>
</>
)
}

View File

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input } from '@nextui-org/react' import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input } from '@nextui-org/react'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom' import { modalContent, modalState } from '@/store/modalAtom'
import { guideLineState } from '@/store/canvasAtom' import { guideLineState, horiGuideLinesState, vertGuideLinesState } from '@/store/canvasAtom'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { ColorPicker, useColor } from 'react-color-palette' import { ColorPicker, useColor } from 'react-color-palette'
import 'react-color-palette/css' import 'react-color-palette/css'
@ -18,6 +18,8 @@ export default function SettingsModal(props) {
const [open, setOpen] = useRecoilState(modalState) const [open, setOpen] = useRecoilState(modalState)
const [guideLine, setGuideLine] = useRecoilState(guideLineState) const [guideLine, setGuideLine] = useRecoilState(guideLineState)
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
const gridSettingArray = [] const gridSettingArray = []
@ -76,6 +78,7 @@ export default function SettingsModal(props) {
name: 'guideLine', name: 'guideLine',
strokeDashArray: [5, 2], strokeDashArray: [5, 2],
opacity: 0.3, opacity: 0.3,
direction: 'horizontal',
}, },
) )
canvasProps.add(horizontalLine) canvasProps.add(horizontalLine)
@ -97,6 +100,7 @@ export default function SettingsModal(props) {
name: 'guideLine', name: 'guideLine',
strokeDashArray: [5, 2], strokeDashArray: [5, 2],
opacity: 0.3, opacity: 0.3,
direction: 'vertical',
}, },
) )
canvasProps.add(verticalLine) canvasProps.add(verticalLine)
@ -114,6 +118,16 @@ export default function SettingsModal(props) {
moduleHoriLength: moduleHoriLength, moduleHoriLength: moduleHoriLength,
} }
gridSettingArray.push(recoilObj) 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')) { if (gridCheckedValue.includes('dot')) {
@ -187,6 +201,8 @@ export default function SettingsModal(props) {
guideLines?.forEach((item) => canvasProps.remove(item)) guideLines?.forEach((item) => canvasProps.remove(item))
canvasProps.renderAll() canvasProps.renderAll()
setGuideLine([]) setGuideLine([])
setHoriGuideLines([])
setVertGuideLines([])
} else { } else {
alert('그리드가 없습니다.') alert('그리드가 없습니다.')
return return

File diff suppressed because it is too large Load Diff

197
src/components/Settings.jsx Normal file
View File

@ -0,0 +1,197 @@
'use client'
import React, { useEffect, useState } from 'react';
import { Button } from '@nextui-org/react';
import { get, post } from '@/lib/Axios';
export default function Settings() {
const [objectNo, setObjectNo] = useState('test123240829010');
const [error, setError] = useState(null);
//
const [settings, setSettings] = useState({
display1: Array(11).fill('N'), // 1
display2: Array(3).fill('N'), // 2
rangeSetting: 0, //
gridSettings: [] //
});
const gridItems = {
display1: [
'할당 표시', '도면 표시', '그리드 표시', '문자 표시', '흐름방향 표시',
'복도치수 표시', '실제치수 표시', '치수 표시 없음', '가대 표시',
'좌표 표시', '도면전환 표시'
],
display2: ['테두리만', '라인해치', 'All Painted'],
rangeSetting: ['극소', '소', '중', '대'],
gridSettings: ['임의 그리드', '실선 그리드', '점 그리드', '그리드 색 설정', '흡착점 추가']
};
//
const handleToggle = (type, index) => {
setSettings((prevSettings) => {
// prevSettings[type] ,
let updated = Array.isArray(prevSettings[type]) ? [...prevSettings[type]] : [];
if (type === 'rangeSetting') {
return { ...prevSettings, [type]: index };
}
updated[index] = updated[index] === 'N' ? 'Y' : 'N';
return { ...prevSettings, [type]: updated };
});
};
// Canvas Setting
const handleSelect = async () => {
try {
const res = await get({ url: `/api/canvas-management/canvas-settings/by-object/${objectNo}` });
//
if (!res || res.length === 0) {
console.warn('조회 결과가 없습니다.');
//
setSettings({
display1: Array(11).fill('N'), // 1
display2: Array(3).fill('N'), // 2
rangeSetting: 0, //
gridSettings: [] //
});
alert('조회된 데이터가 없습니다. 기본 설정이 적용됩니다.');
return; //
}
const data = res.map((item) => ({
display1: [
item.assignDisplay, item.drawDisplay, item.gridDisplay, item.charDisplay, item.flowDisplay,
item.hallwayDimenDisplay, item.actualDimenDisplay, item.noDimenDisplay, item.trestleDisplay,
item.coordiDisplay, item.drawConverDisplay
],
display2: [item.onlyBorder, item.lineHatch, item.allPainted],
rangeSetting: Number(item.adsorpRangeSetting)
}));
setSettings({
display1: data[0].display1,
display2: data[0].display2,
rangeSetting: data[0].rangeSetting,
gridSettings: [] //
});
} catch (error) {
console.error('Data fetching error:', error);
}
};
// Canvas Setting
const handleSubmit = async (e) => {
e.preventDefault();
if (!objectNo) {
alert('object_no를 입력하세요.');
return;
}
const patternData = {
objectNo,
assignDisplay: settings.display1[0],
drawDisplay: settings.display1[1],
gridDisplay: settings.display1[2],
charDisplay: settings.display1[3],
flowDisplay: settings.display1[4],
hallwayDimenDisplay: settings.display1[5],
actualDimenDisplay: settings.display1[6],
noDimenDisplay: settings.display1[7],
trestleDisplay: settings.display1[8],
coordiDisplay: settings.display1[9],
drawConverDisplay: settings.display1[10],
onlyBorder: settings.display2[0],
lineHatch: settings.display2[1],
allPainted: settings.display2[2],
adsorpRangeSetting: String(settings.rangeSetting)
};
await post({ url: `/api/canvas-management/canvas-settings`, data: patternData });
//
handleSelect();
};
//
useEffect(() => {
if (objectNo) {
handleSelect(objectNo);
} else {
alert('object_no를 입력하세요.');
}
}, []);
return (
<>
<div className="container mx-auto p-4 m-4 border">
<div align="right">
<input type="text" placeholder="Object No 입력" value={objectNo} onChange={(e) => setObjectNo(e.target.value)} />
<Button onClick={handleSelect}>조회</Button>
<Button onClick={handleSubmit}>저장</Button>
</div>
<div className="container mx-auto p-4 m-4 border">
<h1>[디스플레이 설정]</h1>
<h1>* 도면에 표시할 항목을 클릭하면 적용 됩니다.</h1>
<div className="grid-container2">
{gridItems.display1.map((item, index) => (
<div
key={index}
className={`grid-item ${settings.display1[index] === 'Y' ? 'selected' : 'unselected'}`}
onClick={() => handleToggle('display1', index)}
>
{settings.display1[index]} {item}
</div>
))}
</div>
<br />
<h1>* 화면 표시</h1>
<div className="grid-container3">
{gridItems.display2.map((item, index) => (
<div
key={index}
className={`grid-item ${settings.display2[index] === 'Y' ? 'selected' : 'unselected'}`}
onClick={() => handleToggle('display2', index)}
>
{settings.display2[index]} {item}
</div>
))}
</div>
<h1>[글꼴/도면크기 설정]</h1>
<h1>* 흡착 범위 설정</h1>
<div className="grid-container4">
{gridItems.rangeSetting.map((item, index) => (
<div
key={index}
className={`grid-item ${settings.rangeSetting === index ? 'selected' : 'unselected'}`}
onClick={() => handleToggle('rangeSetting', index)}
>
{settings.rangeSetting === index ? 'Y' : 'N'} {item}
</div>
))}
</div>
<h1>[그리드 설정]</h1>
<div className="grid-container2 border">
{gridItems.gridSettings.map((item, index) => (
<div
key={index}
className={`grid-item ${settings.gridSettings.includes(index) ? 'selected' : 'unselected'}`}
onClick={() => handleToggle('gridSettings', index)}
>
{settings.gridSettings.includes(index) ? 'Y' : 'N'} {item}
</div>
))}
</div>
</div>
</div>
</>
);
}

View File

@ -1,108 +0,0 @@
import { fabric } from 'fabric'
export class QLine extends fabric.Group {
line
text
fontSize
length = 0
x1
y1
x2
y2
direction
type = 'HelpLine'
parent
#lengthTxt = 0
constructor(points, option, lengthTxt) {
const [x1, y1, x2, y2] = points
if (!option.fontSize) {
throw new Error('Font size is required.')
}
const line = new fabric.Line(points, { ...option, strokeWidth: 1 })
super([line], {})
this.x1 = x1
this.y1 = y1
this.x2 = x2
this.y2 = y2
this.line = line
this.fontSize = option.fontSize
this.direction = option.direction
this.parent = option.parent
if (lengthTxt > 0) {
this.#lengthTxt = Number(lengthTxt)
}
this.#init()
this.#addControl()
}
#init() {
this.#addLengthText(true)
}
#addControl() {
this.on('moving', () => {
this.#addLengthText(false)
})
this.on('modified', (e) => {
this.#addLengthText(false)
})
this.on('selected', () => {
Object.keys(this.controls).forEach((controlKey) => {
if (controlKey !== 'ml' && controlKey !== 'mr') {
this.setControlVisible(controlKey, false)
}
})
})
}
#addLengthText(isFirst) {
if (this.text) {
this.removeWithUpdate(this.text)
this.text = null
}
if (isFirst && this.#lengthTxt > 0) {
const text = new fabric.Textbox(this.#lengthTxt.toFixed(0).toString(), {
left: (this.x1 + this.x2) / 2,
top: (this.y1 + this.y2) / 2,
fontSize: this.fontSize,
})
this.length = this.#lengthTxt
this.text = text
this.addWithUpdate(text)
return
}
const scaleX = this.scaleX
const scaleY = this.scaleY
const x1 = this.left
const y1 = this.top
const x2 = this.left + this.width * scaleX
const y2 = this.top + this.height * scaleY
const dx = x2 - x1
const dy = y2 - y1
this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0))
const text = new fabric.Textbox(this.length.toFixed(0).toString(), {
left: (x1 + x2) / 2,
top: (y1 + y2) / 2,
fontSize: this.fontSize,
})
this.text = text
this.addWithUpdate(text)
}
setFontSize(fontSize) {
this.fontSize = fontSize
this.text.set({ fontSize })
this.addWithUpdate()
}
}

View File

@ -18,6 +18,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
parentId: null, parentId: null,
innerLines: [], innerLines: [],
children: [], children: [],
initOptions: null,
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
// 소수점 전부 제거 // 소수점 전부 제거
points.forEach((point) => { points.forEach((point) => {
@ -58,6 +59,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.canvas = canvas this.canvas = canvas
} }
this.initOptions = options
this.init() this.init()
this.initLines() this.initLines()
this.setShape() this.setShape()
@ -177,7 +180,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
const degree = (Math.atan2(dy, dx) * 180) / Math.PI const degree = (Math.atan2(dy, dx) * 180) / Math.PI
// Create new text object if it doesn't exist // Create new text object if it doesn't exist
const text = new fabric.Text(length.toFixed(0), { const text = new fabric.IText(length.toFixed(0), {
left: midPoint.x, left: midPoint.x,
top: midPoint.y, top: midPoint.y,
fontSize: this.fontSize, fontSize: this.fontSize,
@ -189,7 +192,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
parentDirection: getDirectionByPoint(start, end), parentDirection: getDirectionByPoint(start, end),
parentDegree: degree, parentDegree: degree,
dirty: true, dirty: true,
editable: false, editable: true,
selectable: true, selectable: true,
lockRotation: true, lockRotation: true,
lockScalingX: true, lockScalingX: true,

View File

@ -425,6 +425,8 @@ export function useCanvas(id) {
'maxY', 'maxY',
'minX', 'minX',
'minY', 'minY',
'x',
'y',
]) ])
const str = JSON.stringify(objs) const str = JSON.stringify(objs)

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { canvasSizeState, currentObjectState, modeState } from '@/store/canvasAtom' import { canvasSizeState, currentObjectState, modeState } from '@/store/canvasAtom'
import { QPolygon } from '@/components/fabric/QPolygon'
// 캔버스에 필요한 이벤트 // 캔버스에 필요한 이벤트
export function useCanvasEvent() { export function useCanvasEvent() {
@ -19,9 +20,9 @@ export function useCanvasEvent() {
canvas?.on('selection:cleared', selectionEvent.cleared) canvas?.on('selection:cleared', selectionEvent.cleared)
canvas?.on('selection:created', selectionEvent.created) canvas?.on('selection:created', selectionEvent.created)
canvas?.on('selection:updated', selectionEvent.updated) canvas?.on('selection:updated', selectionEvent.updated)
canvas?.on('object:added', () => { /*canvas?.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown) document.addEventListener('keydown', handleKeyDown)
}) })*/
canvas?.on('object:removed', objectEvent.removed) canvas?.on('object:removed', objectEvent.removed)
} }
@ -77,11 +78,55 @@ export function useCanvasEvent() {
if (target.name === 'lengthText') { if (target.name === 'lengthText') {
const x = target.left const x = target.left
const y = target.top const y = target.top
// Add a property to store the previous value
const previousValue = target.text
target.on('selected', (e) => { target.on('selected', (e) => {
Object.keys(target.controls).forEach((controlKey) => { Object.keys(target.controls).forEach((controlKey) => {
target.setControlVisible(controlKey, false) target.setControlVisible(controlKey, false)
}) })
}) })
target.on('editing:exited', () => {
if (isNaN(target.text.trim())) {
target.set({ text: previousValue })
canvas?.renderAll()
return
}
const updatedValue = parseFloat(target.text.trim())
const targetParent = target.parent
const points = targetParent.getCurrentPoints()
const i = target.idx // Assuming target.index gives the index of the point
const startPoint = points[i]
const endPoint = points[(i + 1) % points.length]
const dx = endPoint.x - startPoint.x
const dy = endPoint.y - startPoint.y
const currentLength = Math.sqrt(dx * dx + dy * dy)
const scaleFactor = updatedValue / currentLength
const newEndPoint = {
x: startPoint.x + dx * scaleFactor,
y: startPoint.y + dy * scaleFactor,
}
const newPoints = [...points]
newPoints[(i + 1) % points.length] = newEndPoint
for (let idx = i + 1; idx < points.length; idx++) {
if (newPoints[idx].x === endPoint.x) {
newPoints[idx].x = newEndPoint.x
} else if (newPoints[idx].y === endPoint.y) {
newPoints[idx].y = newEndPoint.y
}
}
const newPolygon = new QPolygon(newPoints, targetParent.initOptions)
canvas?.add(newPolygon)
canvas?.remove(targetParent)
canvas?.renderAll()
})
target.on('moving', (e) => { target.on('moving', (e) => {
if (target.parentDirection === 'left' || target.parentDirection === 'right') { if (target.parentDirection === 'left' || target.parentDirection === 'right') {
const minX = target.minX const minX = target.minX
@ -116,7 +161,6 @@ export function useCanvasEvent() {
if (whiteList.includes(e.target.name)) { if (whiteList.includes(e.target.name)) {
return return
} }
console.log('removed', e)
}, },
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { import {
calculateIntersection, calculateIntersection,
distanceBetweenPoints, distanceBetweenPoints,
@ -25,6 +25,8 @@ import {
templateTypeState, templateTypeState,
wallState, wallState,
guideLineState, guideLineState,
horiGuideLinesState,
vertGuideLinesState,
} from '@/store/canvasAtom' } from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { fabric } from 'fabric' import { fabric } from 'fabric'
@ -64,23 +66,22 @@ export function useMode() {
const compass = useRecoilValue(compassState) const compass = useRecoilValue(compassState)
const [isCellCenter, setIsCellCenter] = useState(false) const [isCellCenter, setIsCellCenter] = useState(false)
const guideLineInfo = useRecoilValue(guideLineState) const [guideLineInfo, setGuideLineInfo] = useRecoilState(guideLineState)
const [guideLineMode, setGuideLineMode] = useState(false) const [guideLineMode, setGuideLineMode] = useState(false)
const [guideDotMode, setGuideDotMode] = useState(false) const [guideDotMode, setGuideDotMode] = useState(false)
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
useEffect(() => { useEffect(() => {
// 이벤트 리스너 추가
// if (!canvas) { // if (!canvas) {
// canvas?.setZoom(0.8) // canvas?.setZoom(0.8)
// return // return
// } // }
document.addEventListener('keydown', handleKeyDown) if (!canvas) return
setCanvas(canvas)
canvas?.on('mouse:move', drawMouseLines) canvas?.on('mouse:move', drawMouseLines)
// 컴포넌트가 언마운트될 때 이벤트 리스너 제거
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [canvas]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함 }, [canvas]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함
useEffect(() => { useEffect(() => {
@ -100,15 +101,12 @@ export function useMode() {
}, [endPoint]) }, [endPoint])
useEffect(() => { useEffect(() => {
canvas?.off('mouse:out', removeMouseLines)
canvas?.on('mouse:out', removeMouseLines)
changeMode(canvas, mode)
canvas?.off('mouse:move') canvas?.off('mouse:move')
canvas?.on('mouse:move', drawMouseLines) canvas?.on('mouse:move', drawMouseLines)
changeMode(canvas, mode) }, [mode, horiGuideLines, vertGuideLines])
/*
if (mode === Mode.EDIT) {
canvas?.off('mouse:down')
canvas?.on('mouse:down', mouseEvent.editMode)
}*/
}, [mode])
useEffect(() => { useEffect(() => {
setGuideLineMode(false) setGuideLineMode(false)
@ -142,8 +140,8 @@ export function useMode() {
} }
if (isGuideLineMode) { if (isGuideLineMode) {
horizontalLineArray = [...guideLineState[0].horizontalLineArray] horizontalLineArray = [...horiGuideLines]
verticalLineArray = [...guideLineState[0].verticalLineArray] verticalLineArray = [...vertGuideLines]
guideLineLengthHori = Number(guideLineState[0].moduleHoriLength) guideLineLengthHori = Number(guideLineState[0].moduleHoriLength)
guideLineLengthVert = Number(guideLineState[0].moduleVertLength) guideLineLengthVert = Number(guideLineState[0].moduleVertLength)
} }
@ -163,12 +161,27 @@ export function useMode() {
if (mode === Mode.EDIT || mode === Mode.ADSORPTION_POINT) { if (mode === Mode.EDIT || mode === Mode.ADSORPTION_POINT) {
let adsorptionPoint = adsorptionPointList.length > 0 ? findClosestPoint(pointer, adsorptionPointList) : null let adsorptionPoint = adsorptionPointList.length > 0 ? findClosestPoint(pointer, adsorptionPointList) : null
if ((horiGuideLines.length > 0 || vertGuideLines.length > 0) && guideDotMode) {
const closestHorizontalLine = getClosestHorizontalLine(pointer, horiGuideLines)
const closetVerticalLine = getClosestVerticalLine(pointer, vertGuideLines)
let intersection = null
let intersectionDistance = Infinity
if (isGuideLineMode && isGuideDotMode) { if (closestHorizontalLine && closetVerticalLine) {
const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) intersection = calculateIntersection(closestHorizontalLine, closetVerticalLine)
const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) if (intersection) {
const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) intersectionDistance = distanceBetweenPoints(pointer, intersection)
const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) }
}
let xDiff, yDiff
if (closetVerticalLine) {
xDiff = Math.abs(pointer.x - closetVerticalLine.x1)
}
if (closestHorizontalLine) {
yDiff = Math.abs(pointer.y - closestHorizontalLine.y1)
}
const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
@ -177,22 +190,26 @@ export function useMode() {
const yRate = y / guideLineLengthVert const yRate = y / guideLineLengthVert
const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachX = xRate >= 0.4 && xRate <= 0.7
const isAttachY = yRate >= 0.4 && yRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7
if (isAttachX && isAttachY) { if (isAttachX && isAttachY) {
newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} else { } else {
if (Math.min(xDiff, yDiff) <= 20) { if (intersection && intersectionDistance < 20) {
if (xDiff < yDiff) { newX = intersection.x
newX = closetVerticalLine.x1 newY = intersection.y
newY = pointer.y } else {
} else { if (Math.min(xDiff, yDiff) <= 20) {
newX = pointer.x if (xDiff < yDiff) {
newY = closestHorizontalLine.y1 newX = closetVerticalLine.x1
newY = pointer.y
} else {
newX = pointer.x
newY = closestHorizontalLine.y1
}
} }
} }
} }
} else if (isGuideDotMode) { } else if (guideDotMode) {
const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
@ -205,11 +222,27 @@ export function useMode() {
newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} }
} else if (isGuideLineMode) { } else if (horiGuideLines.length > 0 || vertGuideLines.length > 0) {
const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) const closestHorizontalLine = getClosestHorizontalLine(pointer, horiGuideLines)
const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) const closetVerticalLine = getClosestVerticalLine(pointer, vertGuideLines)
const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) let intersection = null
const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) let intersectionDistance = Infinity
if (closestHorizontalLine && closetVerticalLine) {
intersection = calculateIntersection(closestHorizontalLine, closetVerticalLine)
if (intersection) {
intersectionDistance = distanceBetweenPoints(pointer, intersection)
}
}
let xDiff, yDiff
if (closetVerticalLine) {
xDiff = Math.abs(pointer.x - closetVerticalLine.x1)
}
if (closestHorizontalLine) {
yDiff = Math.abs(pointer.y - closestHorizontalLine.y1)
}
const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
@ -218,23 +251,26 @@ export function useMode() {
const yRate = y / guideLineLengthVert const yRate = y / guideLineLengthVert
const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachX = xRate >= 0.4 && xRate <= 0.7
const isAttachY = yRate >= 0.4 && yRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7
if (isAttachX && isAttachY) { if (isAttachX && isAttachY) {
newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} else { } else {
if (Math.min(xDiff, yDiff) <= 20) { if (intersection && intersectionDistance < 20) {
if (xDiff < yDiff) { newX = intersection.x
newX = closetVerticalLine.x1 newY = intersection.y
newY = pointer.y } else {
} else { if (Math.min(xDiff, yDiff) <= 20) {
newX = pointer.x if (xDiff < yDiff) {
newY = closestHorizontalLine.y1 newX = closetVerticalLine.x1
newY = pointer.y
} else {
newX = pointer.x
newY = closestHorizontalLine.y1
}
} }
} }
} }
} }
if (adsorptionPoint && distanceBetweenPoints(pointer, adsorptionPoint) < 20) { if (adsorptionPoint && distanceBetweenPoints(pointer, adsorptionPoint) < 20) {
newX = adsorptionPoint.left newX = adsorptionPoint.left
newY = adsorptionPoint.top newY = adsorptionPoint.top
@ -362,16 +398,13 @@ export function useMode() {
// 모드에 따른 마우스 이벤트 변경 // 모드에 따른 마우스 이벤트 변경
const changeMouseEvent = (mode) => { const changeMouseEvent = (mode) => {
canvas?.off('mouse:down')
switch (mode) { switch (mode) {
case 'drawLine': case 'drawLine':
canvas?.on('mouse:down', mouseEvent.drawLineModeLeftClick) canvas?.on('mouse:down', mouseEvent.drawLineModeLeftClick)
window.document.removeEventListener('contextmenu', mouseEvent.drawLineModeRightClick) document.addEventListener('contextmenu', mouseEvent.drawLineModeRightClick)
window.document.addEventListener('contextmenu', mouseEvent.drawLineModeRightClick)
break break
case 'edit': case 'edit':
canvas?.on('mouse:down', mouseEvent.editMode) canvas?.on('mouse:down', mouseEvent.editMode)
break break
case 'textbox': case 'textbox':
canvas?.on('mouse:down', mouseEvent.textboxMode) canvas?.on('mouse:down', mouseEvent.textboxMode)
@ -394,9 +427,6 @@ export function useMode() {
} }
} }
// 모드에 따른 키보드 이벤트 변경
const changeKeyboardEvent = (mode) => {}
const keyValid = () => { const keyValid = () => {
if (points.current.length === 0) { if (points.current.length === 0) {
alert('시작점을 선택해주세요') alert('시작점을 선택해주세요')
@ -552,77 +582,98 @@ export function useMode() {
} }
} }
const handleKeyDown = (e) => { const mouseAndkeyboardEventClear = () => {
switch (e.key) { canvas?.off('mouse:down')
case 'ArrowDown': { Object.keys(mouseEvent).forEach((key) => {
if (!keyValid()) { canvas?.off('mouse:down', mouseEvent[key])
return document.removeEventListener('contextmenu', mouseEvent[key])
} })
const verticalLength = Number(prompt('길이를 입력하세요:'))
const horizontalLength = 0
drawCircleAndLine(verticalLength, horizontalLength) Object.keys(keyboardEvent).forEach((key) => {
document.removeEventListener('keydown', keyboardEvent[key])
})
}
break const keyboardEvent = {
} // rerendering을 막기 위해 useCallback 사용
case 'ArrowUp': { editMode: useCallback(
if (!keyValid()) { (e) => {
return e.preventDefault()
} switch (e.key) {
const verticalLength = -Number(prompt('길이를 입력하세요:')) case 'ArrowDown': {
const horizontalLength = 0 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 break
case 'b': }
applyTemplateB() case 'ArrowUp': {
if (!keyValid()) {
return
}
const verticalLength = -Number(prompt('길이를 입력하세요:'))
const horizontalLength = 0
drawCircleAndLine(verticalLength, horizontalLength)
break break
case 't': }
templateMode() case 'ArrowLeft': {
if (!keyValid()) {
return
}
const verticalLength = 0
const horizontalLength = -Number(prompt('길이를 입력하세요:'))
drawCircleAndLine(verticalLength, horizontalLength)
break 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) => { const changeMode = (canvas, mode) => {
mouseAndkeyboardEventClear()
setMode(mode) setMode(mode)
setCanvas(canvas) setCanvas(canvas)
// mode별 이벤트 변경 // mode별 이벤트 변경
changeMouseEvent(mode) changeMouseEvent(mode)
changeKeyboardEvent(mode) changeKeyboardEvent(mode)
@ -657,40 +708,82 @@ export function useMode() {
} }
} }
const changeKeyboardEvent = (mode) => {
if (mode === Mode.EDIT) {
switch (mode) {
case 'edit':
document.addEventListener('keydown', keyboardEvent.editMode)
break
}
}
}
const mouseEvent = { const mouseEvent = {
drawLineModeLeftClick: (options) => { drawLineModeLeftClick: (options) => {
if (mode !== Mode.DRAW_LINE) {
return
}
const pointer = canvas?.getPointer(options.e) const pointer = canvas?.getPointer(options.e)
const line = new QLine( const line = new QLine(
[pointer.x, 0, pointer.x, canvas.height], // y축에 1자 선을 그립니다. [pointer.x, 0, pointer.x, canvasSize.vertical], // y축에 1자 선을 그립니다.
{ {
stroke: 'black', stroke: 'gray',
strokeWidth: 2, strokeWidth: 1,
viewLengthText: true, selectable: true,
selectable: false, lockMovementX: true,
fontSize: fontSize, lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
name: 'guideLine',
direction: 'vertical',
}, },
) )
canvas?.add(line) canvas?.add(line)
canvas?.renderAll() 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) const newVerticalLineArray = [...vertGuideLines]
canvas?.renderAll() 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) => { editMode: (options) => {
if (mode !== Mode.EDIT) {
return
}
let pointer = canvas?.getPointer(options.e) let pointer = canvas?.getPointer(options.e)
if (getInterSectPointByMouseLine()) { if (getInterSectPointByMouseLine()) {
@ -801,7 +894,9 @@ export function useMode() {
canvas?.renderAll() canvas?.renderAll()
}, },
textboxMode: (options) => { textboxMode: (options) => {
if (mode !== Mode.TEXTBOX) return
if (canvas?.getActiveObject()?.type === 'textbox') return if (canvas?.getActiveObject()?.type === 'textbox') return
const pointer = canvas?.getPointer(options.e) const pointer = canvas?.getPointer(options.e)
@ -821,6 +916,7 @@ export function useMode() {
}) })
}, },
drawRectMode: (o) => { drawRectMode: (o) => {
if (mode !== Mode.DRAW_RECT) return
let rect, isDown, origX, origY let rect, isDown, origX, origY
isDown = true isDown = true
const pointer = canvas.getPointer(o.e) const pointer = canvas.getPointer(o.e)
@ -862,7 +958,8 @@ export function useMode() {
}) })
}, },
// 흡착점 추가 // 흡착점 추가
adsorptionPoint(o) { adsorptionPoint: (o) => {
if (mode !== Mode.ADSORPTION_POINT) return
const pointer = canvas.getPointer(o.e) const pointer = canvas.getPointer(o.e)
let newX = pointer.x let newX = pointer.x
let newY = pointer.y let newY = pointer.y
@ -4484,7 +4581,6 @@ export function useMode() {
canvas?.off('mouse:move') canvas?.off('mouse:move')
canvas?.off('mouse:out') canvas?.off('mouse:out')
document.removeEventListener('keydown', handleKeyDown)
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof') const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
roofs.forEach((roof, index) => { roofs.forEach((roof, index) => {
const offsetPolygonPoint = offsetPolygon(roof.points, -20) const offsetPolygonPoint = offsetPolygon(roof.points, -20)

View File

@ -95,3 +95,15 @@ export const currentObjectState = atom({
default: null, default: null,
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
export const horiGuideLinesState = atom({
key: 'horiGuideLines',
default: [],
dangerouslyAllowMutability: true,
})
export const vertGuideLinesState = atom({
key: 'vertGuideLines',
default: [],
dangerouslyAllowMutability: true,
})

View File

@ -1,3 +1,81 @@
.test { .test {
background-color: #121212; background-color: #121212;
} }
.grid-container2 {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 2개의 열 */
grid-template-rows: repeat(6, 30px); /* 6개의 행 */
justify-items: center; /* 각 그리드 아이템을 수평 가운데 정렬 */
align-items: center; /* 각 그리드 아이템을 수직 가운데 정렬 */
gap: 5px; /* 그리드 아이템 사이의 간격 */
}
.grid-container3 {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3개의 열 */
grid-template-rows: repeat(6, 30px); /* 6개의 행 */
justify-items: center; /* 각 그리드 아이템을 수평 가운데 정렬 */
align-items: center; /* 각 그리드 아이템을 수직 가운데 정렬 */
gap: 5px; /* 그리드 아이템 사이의 간격 */
}
.grid-container4 {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4개의 열 */
grid-template-rows: repeat(6, 30px); /* 6개의 행 */
justify-items: center; /* 각 그리드 아이템을 수평 가운데 정렬 */
align-items: center; /* 각 그리드 아이템을 수직 가운데 정렬 */
gap: 5px; /* 그리드 아이템 사이의 간격 */
}
.grid-container5 {
display: grid;
grid-template-columns: repeat(5, 1fr); /* 5개의 열 */
grid-template-rows: repeat(5, 30px); /* 5개의 행 */
justify-items: center; /* 각 그리드 아이템을 수평 가운데 정렬 */
align-items: center; /* 각 그리드 아이템을 수직 가운데 정렬 */
gap: 0px; /* 그리드 아이템 사이의 간격 */
}
.grid-item {
width: 100%;
height: 100%;
border: 1px solid black; /* 그리드 외각선 */
text-align: center; /* 그리드 내 가운데 정렬 */
}
.grid-item2 {
padding: 20px;
text-align: center;
cursor: pointer;
border: 1px solid #000;
}
.grid-item3 {
padding: 20px;
text-align: center;
cursor: pointer;
border: 1px solid #000;
transition: background-color 0.3s ease;
}
.grid-item.Y {
background-color: #d3d0d0;
color: black;
}
.grid-item.N {
background-color: white;
color: black;
}
.grid-item.selected {
background-color: #d3d0d0;
color: black;
}
.grid-item.unselected {
background-color: white;
color: black;
}

View File

@ -9,3 +9,45 @@ export const isObjectNotEmpty = (obj) => {
} }
return Object.keys(obj).length > 0 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('&')
}

View File

@ -4319,6 +4319,11 @@ date-fns@^3.3.1:
resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== 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: debug@4, debug@^4.3.3, debug@^4.3.4:
version "4.3.5" version "4.3.5"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz"