This commit is contained in:
changkyu choi 2024-09-09 14:51:46 +09:00
commit 2e48112873
28 changed files with 2146 additions and 1409 deletions

View File

@ -1,8 +1,10 @@
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"
DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true"
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_bV5zuYMyyIYFlOb3"

View File

@ -5,3 +5,5 @@ NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
DATABASE_URL=""
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_bV5zuYMyyIYFlOb3"

View File

@ -25,6 +25,7 @@
"react-colorful": "^5.6.1",
"react-datepicker": "^7.3.0",
"react-dom": "^18",
"react-icons": "^5.3.0",
"react-responsive-modal": "^6.4.2",
"react-toastify": "^10.0.5",
"recoil": "^0.7.7",
@ -32,6 +33,7 @@
},
"devDependencies": {
"@turf/turf": "^7.0.0",
"convertapi": "^1.14.0",
"dayjs": "^1.11.13",
"postcss": "^8",
"prettier": "^3.3.3",

View File

@ -1,23 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="600" height="300" viewBox="0 0 1280 875"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,875.000000) scale(0.100000,-0.100000)" fill="purple" stroke="#000000" stroke-width="10">
<path d="M3403 7423 c-257 -762 -623 -1817 -636 -1829 -11 -11 -1264 -358
-1817 -504 -405 -106 -540 -144 -539 -150 0 -3 30 -28 67 -55 117 -88 1826
-1407 1841 -1421 12 -11 11 -104 -8 -646 -24 -697 -58 -1634 -64 -1770 -3 -49
-3 -88 -1 -88 5 0 949 674 1528 1092 l479 346 61 -23 c34 -13 279 -104 546
-203 267 -100 754 -282 1083 -405 328 -123 597 -219 597 -215 0 5 -54 189
-119 411 -66 221 -192 652 -280 957 -88 305 -187 643 -219 751 l-58 195 304
385 c168 211 503 636 745 944 242 308 447 568 455 578 8 9 12 20 8 24 -4 4
-220 11 -479 15 -413 6 -1036 22 -1745 44 l-213 7 -427 636 c-235 350 -541
808 -682 1018 -140 210 -258 383 -261 383 -3 0 -78 -215 -166 -477z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +0,0 @@
<svg height="280" width="600" xmlns="http://www.w3.org/2000/svg">
<polygon points="120,15 42,202 400,202 400,99 150,99 " style="fill:lime;stroke:purple;stroke-width:3" />
</svg>

Before

Width:  |  Height:  |  Size: 179 B

View File

@ -0,0 +1,5 @@
import FloorPlan from '@/components/floor-plan/FloorPlan'
export default function floorPlanPage() {
return <FloorPlan />
}

View File

@ -0,0 +1,29 @@
'use server'
import fs from 'fs/promises'
import { NextResponse } from 'next/server'
export async function GET(req) {
const path = 'public/mapImages'
const q = req.nextUrl.searchParams.get('q')
const fileNm = req.nextUrl.searchParams.get('fileNm')
const zoom = req.nextUrl.searchParams.get('zoom')
const targetUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${q}&zoom=${zoom}&maptype=satellite&size=640x640&scale=1&key=AIzaSyDO7nVR1N_D2tKy60hgGFavpLaXkHpiHpc`
const decodeUrl = decodeURIComponent(targetUrl)
const response = await fetch(decodeUrl)
const data = await response.arrayBuffer()
const buffer = Buffer.from(data)
try {
await fs.readdir(path)
} catch {
await fs.mkdir(path)
} finally {
await fs.writeFile(`${path}/${fileNm}.png`, buffer)
}
return NextResponse.json({ fileNm: `${fileNm}.png` })
}

View File

@ -12,6 +12,8 @@ export const Mode = {
CELL_POWERCON: 'cellPowercon', //파워콘
DRAW_HELP_LINE: 'drawHelpLine', // 보조선 그리기 모드 지붕 존재해야함
ADSORPTION_POINT: 'adsorptionPoint', //흡착점 모드
OPENING: 'opening', //개구 모드
SHADOW: 'shadow', //그림자 생성 모드
DEFAULT: 'default',
}

View File

@ -14,7 +14,7 @@ import { Button } from '@nextui-org/react'
import SingleDatePicker from './common/datepicker/SingleDatePicker'
import RangeDatePicker from './common/datepicker/RangeDatePicker'
import QGrid from './common/grid/QGrid'
import { QToast } from '@/hooks/useToast'
import { toastUp } from '@/hooks/useToast'
export default function Intro() {
const { get } = useAxios()
@ -127,7 +127,7 @@ export default function Intro() {
<Button
color="primary"
onClick={() => {
QToast({
toastUp({
message: 'This is a toast message',
type: 'success',
})

View File

@ -1,20 +1,35 @@
'use client'
import { useState } from 'react'
import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
import ColorPicker from './common/color-picker/ColorPicker'
import { useRef, useState } from 'react'
import { useRecoilState } from 'recoil'
import { v4 as uuidv4 } from 'uuid'
import { FaAnglesUp } from 'react-icons/fa6'
import { FaAnglesDown } from 'react-icons/fa6'
import { useAxios } from '@/hooks/useAxios'
import { useMessage } from '@/hooks/useMessage'
// import { get } from '@/lib/Axios'
import { convertDwgToPng } from '@/lib/cadAction'
import { cadFileNameState, googleMapFileNameState, useCadFileState, useGoogleMapFileState } from '@/store/canvasAtom'
import QSelect from '@/components/ui/QSelect'
import { Button } from '@nextui-org/react'
import ColorPicker from './common/color-picker/ColorPicker'
import { toastUp } from '@/hooks/useToast'
import styles from './playground.module.css'
import Image from 'next/image'
export default function Playground() {
const { get } = useAxios()
const [useCadFile, setUseCadFile] = useRecoilState(useCadFileState)
const [cadFileName, setCadFileName] = useRecoilState(cadFileNameState)
const [useGoogleMapFile, setUseGoogleMapFile] = useRecoilState(useGoogleMapFileState)
const [googleMapFileName, setGoogleMapFileName] = useRecoilState(googleMapFileNameState)
const fileRef = useRef(null)
const queryRef = useRef(null)
const [zoom, setZoom] = useState(20)
const { get, promisePost } = useAxios()
const testVar = process.env.NEXT_PUBLIC_TEST
const converterUrl = process.env.NEXT_PUBLIC_CONVERTER_API_URL
const { getMessage } = useMessage()
const [color, setColor] = useState('#ff0000')
@ -28,6 +43,46 @@ export default function Playground() {
console.log('users', users)
}
const handleConvert = async () => {
console.log('file', fileRef.current.files[0])
const formData = new FormData()
formData.append('file', fileRef.current.files[0])
await promisePost({ url: converterUrl, data: formData })
.then((res) => {
console.log('response: ', res)
convertDwgToPng(res.data.Files[0].FileName, res.data.Files[0].FileData)
setUseCadFile(true)
setCadFileName(res.data.Files[0].FileName)
toastUp({ message: '파일 변환 완료', type: 'success' })
})
.catch((err) => {
console.error(err)
toastUp({ message: '파일 변환 실패', type: 'error' })
})
}
const handleDownImage = async (fileName = '') => {
const fileNm = fileName === '' ? uuidv4() : fileName
const queryString = queryRef.current.value === '' ? '서울시 서대문구 연세로5다길 22-3 발리빌라 3층' : queryRef.current.value
const res = await get({ url: `http://localhost:3000/api/html2canvas?q=${queryString}&fileNm=${fileNm}&zoom=${zoom}` })
console.log('res', res)
setGoogleMapFileName(res.fileNm)
toastUp({ message: '이미지 저장 완료', type: 'success' })
setUseGoogleMapFile(true)
}
const handleZoom = async (type) => {
if (type === 'up') {
setZoom((prevState) => prevState + 1)
} else {
setZoom((prevState) => prevState - 1)
}
await handleDownImage()
}
const data = [
{
id: 1,
@ -70,6 +125,32 @@ export default function Playground() {
<ColorPicker color={color} setColor={setColor} />
<div className="p-4">{color}</div>
</div>
<div>
<h1 className="text-2xl">캐드 파일 이미지 사용</h1>
<input type="file" name="file" ref={fileRef} />
<div>
<Button onClick={handleConvert}>Convert</Button>
</div>
</div>
<div>
<h1 className="text-2xl">구글 이미지 사용</h1>
<input type="text" ref={queryRef} className="w-80 border-medium my-2" />
<div>
<Button onClick={handleDownImage}>Google map Download to Image</Button>
</div>
{useGoogleMapFile && (
<>
<div className="my-2">
<p className="text-lg">Zoom Controller : {zoom}</p>
<Button startContent={<FaAnglesUp />} className="mx-2" onClick={() => handleZoom('up')}></Button>
<Button startContent={<FaAnglesDown />} className="mx-2" onClick={() => handleZoom('down')}></Button>
</div>
<div className="my-2">
<Image src={`/mapImages/${googleMapFileName}`} width={640} height={640} />
</div>
</>
)}
</div>
</div>
</>
)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { Children, useEffect, useState } from 'react'
export default function QContextMenu(props) {
const { contextRef, canvasProps } = props
@ -8,6 +7,19 @@ export default function QContextMenu(props) {
// const children = useRecoilValue(modalContent)
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
const activeObject = canvasProps.getActiveObject() //
let contextType = ''
if (activeObject) {
if (activeObject.initOptions) {
//
if (activeObject.initOptions.name.indexOf('guide') > -1) {
contextType = 'surface' //
}
}
}
useEffect(() => {
if (!contextRef.current) return
@ -29,21 +41,49 @@ export default function QContextMenu(props) {
}
}
// Prevent the default context menu from appearing on the canvas
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
document.addEventListener('click', handleClick)
document.addEventListener('click', handleOutsideClick)
return () => {
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
document.removeEventListener('click', handleClick)
document.removeEventListener('click', handleOutsideClick)
}
}, [contextRef, contextMenu])
const handleMenuClick = (option) => {
alert(`option ${option} clicked`)
setContextMenu({ ...contextMenu, visible: false })
const handleObjectMove = () => {
activeObject.set({
lockMovementX: false, // X
lockMovementY: false, // Y
})
canvasProps.on('object:modified', function (e) {
activeObject.set({
lockMovementX: true, // X
lockMovementY: true, // Y
})
})
}
const handleObjectDelete = () => {
if (confirm('삭제하실거?')) {
canvasProps.remove(activeObject)
}
}
const handleObjectCopy = () => {
activeObject.clone((cloned) => {
cloned.set({
left: activeObject.left + activeObject.width + 20,
initOptions: { ...activeObject.initOptions },
lockMovementX: true, // X
lockMovementY: true, // Y
lockRotation: true, //
lockScalingX: true, // X
lockScalingY: true, // Y
})
canvasProps?.add(cloned)
})
}
return (
@ -51,15 +91,16 @@ export default function QContextMenu(props) {
{contextMenu.visible && (
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
Option 1
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectMove()}>
이동
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
Option 2
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectDelete()}>
삭제
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
Option 3
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleObjectCopy()}>
복사
</li>
{props.children}
</ul>
</div>
)}

View File

@ -1,71 +1,20 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import QContextMenu from './QContextMenu'
export default function QPolygonContextMenu(props) {
const { contextRef, canvasProps } = props
// const children = useRecoilValue(modalContent)
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
useEffect(() => {
if (!contextRef.current) return
const handleContextMenu = (e) => {
e.preventDefault() // contextmenu
setContextMenu({ visible: true, x: e.pageX, y: e.pageY })
canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu) //
}
const handleClick = (e) => {
e.preventDefault()
setContextMenu({ ...contextMenu, visible: false })
}
const handleOutsideClick = (e) => {
e.preventDefault()
if (contextMenu.visible && !ref.current.contains(e.target)) {
setContextMenu({ ...contextMenu, visible: false })
}
}
// Prevent the default context menu from appearing on the canvas
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
document.addEventListener('click', handleClick)
document.addEventListener('click', handleOutsideClick)
return () => {
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
document.removeEventListener('click', handleClick)
document.removeEventListener('click', handleOutsideClick)
}
}, [contextRef, contextMenu])
const handleMenuClick = (option) => {
alert(`option ${option} clicked`)
setContextMenu({ ...contextMenu, visible: false })
function handleMenuClick(index) {
alert('test')
}
return (
<>
{contextMenu.visible && (
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
polygon
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
Option 1
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
Option 2
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
Option 3
</li>
</ul>
</div>
)}
</>
<QContextMenu contextRef={contextRef} canvasProps={canvasProps}>
<>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(4)}>
모듈 채우기
</li>
</>
</QContextMenu>
)
}

View File

@ -2,7 +2,15 @@ import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid'
import { QLine } from '@/components/fabric/QLine'
import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util'
import { calculateAngle, drawHippedRoof, inPolygon, splitPolygonWithLines, toGeoJSON } from '@/util/qpolygon-utils'
import {
calculateAngle,
drawDirectionArrow,
drawHippedRoof,
drawPolygonArrow,
inPolygon,
splitPolygonWithLines,
toGeoJSON,
} from '@/util/qpolygon-utils'
import * as turf from '@turf/turf'
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
@ -19,6 +27,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
innerLines: [],
children: [],
initOptions: null,
direction: null,
arrow: null,
initialize: function (points, options, canvas) {
// 소수점 전부 제거
points.forEach((point) => {
@ -100,6 +110,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
toObject: function (propertiesToInclude) {
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
id: this.id,
type: this.type,
text: this.text,
hips: this.hips,
@ -116,9 +127,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.on('modified', (e) => {
this.addLengthText()
if (this.arrow) {
drawDirectionArrow(this)
}
})
this.on('selected', () => {
drawDirectionArrow(this)
Object.keys(this.controls).forEach((controlKey) => {
if (controlKey !== 'ml' && controlKey !== 'mr') {
this.setControlVisible(controlKey, false)
@ -132,6 +147,17 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.canvas.remove(text)
})
this.texts = null
if (this.arrow) {
this.canvas.remove(this.arrow)
this.canvas
.getObjects()
.filter((obj) => obj.name === 'directionText' && obj.parent === this.arrow)
.forEach((text) => {
this.canvas.remove(text)
})
this.arrow = null
}
})
// polygon.fillCell({ width: 50, height: 30, padding: 10 })
@ -162,11 +188,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
},
addLengthText() {
if (this.texts.length > 0) {
this.texts.forEach((text) => {
this.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.forEach((text) => {
this.canvas.remove(text)
})
}
let points = this.getCurrentPoints()
points.forEach((start, i) => {
@ -209,7 +237,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
},
setFontSize(fontSize) {
this.fontSize = fontSize
this.text.set({ fontSize })
this.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.forEach((text) => {
text.set({ fontSize: fontSize })
})
},
_render: function (ctx) {
this.callSuper('_render', ctx)
@ -689,9 +722,20 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.wall = wall
},
setViewLengthText(isView) {
this.texts.forEach((text) => {
text.set({ visible: isView })
})
this.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.forEach((text) => {
text.set({ visible: isView })
})
},
setScaleX(scale) {
this.scaleX = scale
this.addLengthText()
},
setScaleY(scale) {
this.scaleY = scale
this.addLengthText()
},
divideLine() {
splitPolygonWithLines(this)

View File

@ -0,0 +1,7 @@
export default function FloorPlan() {
return (
<>
<h1>도면 작성 페이지</h1>
</>
)
}

View File

@ -0,0 +1,155 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Button, Input } from '@nextui-org/react'
import { useRecoilState, useSetRecoilState } from 'recoil'
import { modalState } from '@/store/modalAtom'
import { fabric } from 'fabric'
import { QPolygon } from '@/components/fabric/QPolygon'
import { modeState, objectPlacementModeState } from '@/store/canvasAtom'
const BATCH_TYPE = {
OPENING: 'opening',
SHADOW: 'shadow',
}
const INPUT_TYPE = {
FREE: 'free',
DIMENSION: 'dimension',
}
const ObjectPlacement = ({ canvas }) => {
const [open, setOpen] = useRecoilState(modalState)
const [mode, setMode] = useRecoilState(modeState)
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const [areaBoundary, setAreaBoundary] = useState(true)
// opening or shadow /
const [batchType, setBatchType] = useState(BATCH_TYPE.OPENING)
// free or dimension /
const [inputType, setInputType] = useState(INPUT_TYPE.FREE)
const handleSave = () => {
setMode(batchType)
setOpen(false)
}
return (
<div className="p-4 w-full max-w-xs border border-gray-300">
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">오브젝트 배치</label>
</div>
<div className="border-b mb-4">
<h2 className="font-semibold">개구 · 그림자 배치</h2>
</div>
<div className="mb-4">
<div className="flex">
<Button
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.OPENING ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
onClick={() => {
setBatchType(BATCH_TYPE.OPENING)
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.OPENING })
}}
>
개구 배치
</Button>
<Button
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.SHADOW ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
onClick={() => {
setBatchType(BATCH_TYPE.SHADOW)
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.SHADOW })
}}
>
그림자 배치
</Button>
</div>
</div>
<div className="mb-4">
<div className="mb-2 text-gray-700 font-semibold">설정</div>
<div className="mb-2">
<label className="inline-flex items-center">
<Input
type="radio"
name="inputType"
checked={objectPlacementMode.inputType === INPUT_TYPE.FREE}
onClick={() => {
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.FREE })
}}
className="form-radio text-blue-500"
/>
<span className="ml-2">프리입력</span>
</label>
</div>
<div className="mb-2">
<label className="inline-flex items-center">
<Input
type="radio"
name="inputType"
checked={objectPlacementMode.inputType === INPUT_TYPE.DIMENSION}
onClick={() => {
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.DIMENSION })
}}
className="form-radio text-blue-500"
/>
<span className="ml-2">치수입력</span>
</label>
</div>
<div className="flex mb-2">
<div className="mr-2">
<label className="block text-gray-700 text-sm mb-1">가로길이</label>
<Input
type="text"
className="w-full px-3 py-2 border rounded"
placeholder="mm"
value={objectPlacementMode.width}
onChange={(e) => {
setObjectPlacementModeState({ ...objectPlacementMode, width: e.target.value })
}}
/>
</div>
<div>
<label className="block text-gray-700 text-sm mb-1">세로길이</label>
<Input
type="text"
className="w-full px-3 py-2 border rounded"
placeholder="mm"
value={objectPlacementMode.height}
onChange={(e) => {
setObjectPlacementModeState({ ...objectPlacementMode, height: e.target.value })
}}
/>
</div>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<Input
type="checkbox"
name={`areaBoundary`}
checked={objectPlacementMode.areaBoundary}
onClick={() => setObjectPlacementModeState({ ...objectPlacementMode, areaBoundary: !objectPlacementMode.areaBoundary })}
className="form-checkbox text-blue-500"
/>
<span className="ml-2">영역교차</span>
</label>
</div>
<div className="text-center">
<Button onClick={handleSave} className="bg-gray-500 text-white py-2 px-4 rounded">
저장
</Button>
</div>
</div>
</div>
)
}
export default ObjectPlacement

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,10 @@ export function useAxios() {
.catch(console.error)
}
const promisePost = async ({ url, data }) => {
return await getInstances(url).post(url, data)
}
const put = async ({ url, data }) => {
return await getInstances(url)
.put(url, data)
@ -66,5 +70,5 @@ export function useAxios() {
.catch(console.error)
}
return { get, post, put, patch, del }
return { get, post, promisePost, put, patch, del }
}

View File

@ -17,6 +17,7 @@ export function useCanvas(id) {
const [canvas, setCanvas] = useState()
const [isLocked, setIsLocked] = useState(false)
const [history, setHistory] = useState([])
const [backImg, setBackImg] = useState()
const [canvasSize] = useRecoilState(canvasSizeState)
const [fontSize] = useRecoilState(fontSizeState)
const { setCanvasForEvent, attachDefaultEventOnCanvas } = useCanvasEvent()
@ -427,6 +428,7 @@ export function useCanvas(id) {
'minY',
'x',
'y',
'stickeyPoint',
])
const str = JSON.stringify(objs)
@ -444,6 +446,30 @@ export function useCanvas(id) {
// }, 1000)
}
/**
* cad 파일 사용시 이미지 로딩 함수
*/
const handleBackImageLoadToCanvas = (url) => {
console.log('image load url: ', url)
fabric.Image.fromURL(url, function (img) {
img.set({
left: 0,
top: 0,
width: 1500,
height: 1500,
selectable: true,
})
canvas.add(img)
canvas.renderAll()
setBackImg(img)
})
}
const handleCadImageInit = () => {
canvas.clear()
}
return {
canvas,
addShape,
@ -459,5 +485,9 @@ export function useCanvas(id) {
setCanvasBackgroundWithDots,
addCanvas,
removeMouseLines,
handleBackImageLoadToCanvas,
handleCadImageInit,
backImg,
setBackImg,
}
}

View File

@ -27,11 +27,12 @@ import {
guideLineState,
horiGuideLinesState,
vertGuideLinesState,
objectPlacementModeState,
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
import { fabric } from 'fabric'
import { QPolygon } from '@/components/fabric/QPolygon'
import offsetPolygon from '@/util/qpolygon-utils'
import offsetPolygon, { inPolygon } from '@/util/qpolygon-utils'
import { isObjectNotEmpty } from '@/util/common-utils'
import * as turf from '@turf/turf'
import { Mode } from '@/common/common'
@ -74,6 +75,8 @@ export function useMode() {
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
useEffect(() => {
// if (!canvas) {
// canvas?.setZoom(0.8)
@ -81,8 +84,11 @@ export function useMode() {
// }
if (!canvas) return
setCanvas(canvas)
canvas?.off('mouse:out', removeMouseLines)
canvas?.on('mouse:out', removeMouseLines)
canvas?.off('mouse:move')
canvas?.on('mouse:move', drawMouseLines)
}, [canvas]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함
}, [canvas, zoom]) // 빈 배열을 전달하여 컴포넌트가 마운트될 때만 실행되도록 함
useEffect(() => {
if (canvas?.getObjects().find((obj) => obj.name === 'connectLine')) {
@ -101,11 +107,7 @@ export function useMode() {
}, [endPoint])
useEffect(() => {
canvas?.off('mouse:out', removeMouseLines)
canvas?.on('mouse:out', removeMouseLines)
changeMode(canvas, mode)
canvas?.off('mouse:move')
canvas?.on('mouse:move', drawMouseLines)
}, [mode, horiGuideLines, vertGuideLines])
useEffect(() => {
@ -278,7 +280,7 @@ export function useMode() {
}
// 가로선을 그립니다.
const horizontalLine = new fabric.Line([0, newY, canvasSize.horizontal, newY], {
const horizontalLine = new fabric.Line([0, newY, 2 * canvas.width, newY], {
stroke: 'red',
strokeWidth: 1,
selectable: false,
@ -286,7 +288,7 @@ export function useMode() {
})
// 세로선을 그립니다.
const verticalLine = new fabric.Line([newX, 0, newX, canvasSize.vertical], {
const verticalLine = new fabric.Line([newX, 0, newX, 2 * canvas.height], {
stroke: 'red',
strokeWidth: 1,
selectable: false,
@ -420,6 +422,17 @@ export function useMode() {
break
case 'adsorptionPoint':
canvas?.on('mouse:down', mouseEvent.adsorptionPoint)
break
case 'shadow':
canvas?.on('mouse:down', mouseEvent.shadowMode.down)
canvas?.on('mouse:move', mouseEvent.shadowMode.move)
canvas?.on('mouse:up', mouseEvent.shadowMode.up)
break
case 'opening':
canvas?.on('mouse:down', mouseEvent.openingMode.down)
canvas?.on('mouse:move', mouseEvent.openingMode.move)
canvas?.on('mouse:up', mouseEvent.openingMode.up)
break
case 'default':
canvas?.off('mouse:down')
@ -584,10 +597,9 @@ export function useMode() {
const mouseAndkeyboardEventClear = () => {
canvas?.off('mouse:down')
Object.keys(mouseEvent).forEach((key) => {
canvas?.off('mouse:down', mouseEvent[key])
document.removeEventListener('contextmenu', mouseEvent[key])
})
canvas?.off('mouse:move')
canvas?.off('mouse:up')
canvas?.off('mouse:out')
Object.keys(keyboardEvent).forEach((key) => {
document.removeEventListener('keydown', keyboardEvent[key])
@ -647,7 +659,7 @@ export function useMode() {
}
case 'Enter': {
const result = prompt('입력하세요 (a(A패턴),b(B패턴),t(지붕))')
const result = prompt('입력하세요 (a(A패턴), b(B패턴), t(지붕), e(변별))')
switch (result) {
case 'a':
@ -659,6 +671,8 @@ export function useMode() {
case 't':
templateMode()
break
case 'e':
templateSideMode()
}
}
}
@ -669,6 +683,7 @@ export function useMode() {
const changeMode = (canvas, mode) => {
mouseAndkeyboardEventClear()
addCommonMouseEvent()
setMode(mode)
setCanvas(canvas)
@ -708,6 +723,12 @@ export function useMode() {
}
}
// 모든 모드에서 사용되는 공통 이벤트 추가
const addCommonMouseEvent = () => {
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:out', removeMouseLines)
}
const changeKeyboardEvent = (mode) => {
if (mode === Mode.EDIT) {
switch (mode) {
@ -930,7 +951,7 @@ export function useMode() {
width: pointer.x - origX,
height: pointer.y - origY,
angle: 0,
fill: 'transparent',
fill: 'white',
stroke: 'black',
transparentCorners: false,
})
@ -987,6 +1008,168 @@ export function useMode() {
canvas.add(circle)
canvas.renderAll()
},
//면 형상 배치 모드
surfaceShapeMode: (o) => {},
// 그림자 모드
shadowMode: {
rect: null,
isDown: false,
origX: 0,
origY: 0,
down: (o) => {
if (mode !== Mode.SHADOW) return
mouseEvent.shadowMode.isDown = true
const pointer = canvas.getPointer(o.e)
mouseEvent.shadowMode.origX = pointer.x
mouseEvent.shadowMode.origY = pointer.y
mouseEvent.shadowMode.rect = new fabric.Rect({
fill: 'grey',
left: mouseEvent.shadowMode.origX,
top: mouseEvent.shadowMode.origY,
originX: 'left',
originY: 'top',
opacity: 0.3,
width: 0,
height: 0,
angle: 0,
transparentCorners: false,
})
canvas.add(mouseEvent.shadowMode.rect)
},
move: (e) => {
if (!mouseEvent.shadowMode.isDown) return
const pointer = canvas.getPointer(e.e)
if (mouseEvent.shadowMode.origX > pointer.x) {
mouseEvent.shadowMode.rect.set({ left: Math.abs(pointer.x) })
}
if (mouseEvent.shadowMode.origY > pointer.y) {
mouseEvent.shadowMode.rect.set({ top: Math.abs(pointer.y) })
}
mouseEvent.shadowMode.rect.set({ width: Math.abs(mouseEvent.shadowMode.origX - pointer.x) })
mouseEvent.shadowMode.rect.set({ height: Math.abs(mouseEvent.shadowMode.origY - pointer.y) })
},
up: (o) => {
mouseEvent.shadowMode.isDown = false
setMode(Mode.DEFAULT)
},
},
openingMode: {
rect: null,
isDown: false,
origX: 0,
origY: 0,
down: (o) => {
if (mode !== Mode.OPENING) return
const roofs = canvas?._objects.filter((obj) => obj.name === 'roof')
if (roofs.length === 0) {
alert('지붕을 먼저 그려주세요')
setMode(Mode.DEFAULT)
return
}
const pointer = canvas.getPointer(o.e)
let selectRoof = null
roofs.forEach((roof) => {
if (roof.inPolygon({ x: pointer.x, y: pointer.y })) {
selectRoof = roof
}
})
if (!selectRoof) {
alert('지붕 내부에만 생성 가능합니다.')
return
}
mouseEvent.openingMode.isDown = true
mouseEvent.openingMode.origX = pointer.x
mouseEvent.openingMode.origY = pointer.y
mouseEvent.openingMode.rect = new fabric.Rect({
fill: 'white',
stroke: 'black',
strokeWidth: 1,
left: mouseEvent.openingMode.origX,
top: mouseEvent.openingMode.origY,
originX: 'left',
originY: 'top',
width: pointer.x - mouseEvent.openingMode.origX,
height: pointer.y - mouseEvent.openingMode.origY,
angle: 0,
transparentCorners: false,
})
canvas.add(mouseEvent.openingMode.rect)
},
move: (e) => {
if (!mouseEvent.openingMode.isDown) return
const pointer = canvas.getPointer(e.e)
if (mouseEvent.openingMode.origX > pointer.x) {
mouseEvent.openingMode.rect.set({ left: Math.abs(pointer.x) })
}
if (mouseEvent.openingMode.origY > pointer.y) {
mouseEvent.openingMode.rect.set({ top: Math.abs(pointer.y) })
}
mouseEvent.openingMode.rect.set({ width: Math.abs(mouseEvent.openingMode.origX - pointer.x) })
mouseEvent.openingMode.rect.set({ height: Math.abs(mouseEvent.openingMode.origY - pointer.y) })
},
up: (o) => {
mouseEvent.openingMode.isDown = false
const { areaBoundary } = objectPlacementMode
//roof의 내부에 있는지 확인
if (!checkInsideRoof(mouseEvent.openingMode.rect)) {
setMode(Mode.DEFAULT)
}
// 영역 교차인지 확인
if (!areaBoundary) {
const isCross = checkCrossAreaBoundary(mouseEvent.openingMode.rect)
if (isCross) {
alert('영역이 교차되었습니다.')
canvas.remove(mouseEvent.openingMode.rect)
}
}
mouseEvent.openingMode.rect.set({ name: 'opening' })
},
},
}
const checkCrossAreaBoundary = (rect) => {
const openings = canvas?._objects.filter((obj) => obj.name === 'opening')
if (openings.length === 0) {
return false
}
const rectPoints = [
{ x: rect.left, y: rect.top },
{ x: rect.left, y: rect.top + rect.height },
{ x: rect.left + rect.width, y: rect.top + rect.height },
{ x: rect.left + rect.width, y: rect.top },
]
for (let i = 0; i < openings.length; i++) {
const rect2 = openings[i]
const rect2Points = [
{ x: rect2.left, y: rect2.top },
{ x: rect2.left, y: rect2.top + rect2.height },
{ x: rect2.left + rect2.width, y: rect2.top + rect2.height },
{ x: rect2.left + rect2.width, y: rect2.top },
]
}
return false
}
const checkInsideRoof = (rect) => {
let result = true
const roofs = canvas?._objects.filter((obj) => obj.name === 'roof')
if (roofs.length === 0) {
alert('지붕을 먼저 그려주세요')
canvas?.remove(rect)
return false
}
return result
}
const getInterSectPointByMouseLine = () => {
@ -1050,6 +1233,18 @@ export function useMode() {
setTemplateType(1)
}
}
const templateSideMode = () => {
changeMode(canvas, Mode.EDIT)
if (historyPoints.current.length >= 4) {
const wall = drawWallPolygon()
setWall(wall)
console.log('sideWall', wall)
}
}
/**
* 점을 연결하는 선과 길이를 그립니다.
* a : 시작점, b : 끝점
@ -1138,11 +1333,17 @@ export function useMode() {
}
const zoomIn = () => {
if (canvas.getZoom() + 0.1 > 1.6) {
return
}
canvas?.setZoom(canvas.getZoom() + 0.1)
setZoom(Math.round(zoom + 10))
}
const zoomOut = () => {
if (canvas.getZoom() - 0.1 < 0.5) {
return
}
canvas?.setZoom(canvas.getZoom() - 0.1)
setZoom(Math.ceil(zoom - 10))
}
@ -4572,18 +4773,14 @@ export function useMode() {
}
const createRoofRack = () => {
roof.divideLine()
const trestlePolygons = canvas?.getObjects().filter((obj) => obj.name === 'trestle')
// 이미 만들어진 가대가 있을 경우 return
if (trestlePolygons.length !== 0) {
return
}
removeMouseLines()
canvas?.off('mouse:move')
canvas?.off('mouse:out')
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof')
roofs.forEach((roof, index) => {
const offsetPolygonPoint = offsetPolygon(roof.points, -20)
// const offsetPolygonPoint = offsetPolygon(roof.points(), -20) //이동되서 찍을라고 바꿈
const offsetPolygonPoint = offsetPolygon(roof.getCurrentPoints(), -20)
const trestlePoly = new QPolygon(offsetPolygonPoint, {
fill: 'transparent',
@ -4604,6 +4801,7 @@ export function useMode() {
})
canvas?.add(trestlePoly)
trestlePoly.setViewLengthText(false)
})
removeHelpPointAndHelpLine()
}
@ -4637,10 +4835,10 @@ export function useMode() {
let drawRoofCells
if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') {
drawRoofCells = trestle.fillCell({ width: 50, height: 100, padding: 10 })
drawRoofCells = trestle.fillCell({ width: 113.4, height: 172.2, padding: 0 })
trestle.direction = 'south'
} else {
drawRoofCells = trestle.fillCell({ width: 100, height: 50, padding: 10 })
drawRoofCells = trestle.fillCell({ width: 172.2, height: 113.4, padding: 0 })
trestle.direction = 'east'
}

View File

@ -13,7 +13,7 @@ const toastDefaultOptions = {
closeOnClick: true,
}
const QToast = (props) => {
const toastUp = (props) => {
// type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default'
const { message, type = 'info', options } = props
const customOptions = { ...toastDefaultOptions, ...options }
@ -32,4 +32,4 @@ const QToast = (props) => {
}
}
export { QToast }
export { toastUp }

18
src/lib/cadAction.js Normal file
View File

@ -0,0 +1,18 @@
'use server'
import fs from 'fs/promises'
const imageSavePath = 'public/cadImages'
const convertDwgToPng = async (fileName, data) => {
console.log('fileName', fileName)
try {
await fs.readdir(imageSavePath)
} catch {
await fs.mkdir(imageSavePath)
}
return await fs.writeFile(`${imageSavePath}/${fileName}`, data, 'base64')
}
export { convertDwgToPng }

View File

@ -1,3 +1,90 @@
{
"hi": "こんにちは"
"hi": "こんにちは",
"common.message.no.data": "No data",
"common.message.no.dataDown": "ダウンロードするデータがありません",
"common.message.noData": "表示するデータがありません",
"common.message.search": "search success",
"common.message.insert": "insert success",
"common.message.update": "update success",
"common.message.delete": "削除",
"common.message.restoration": "復元",
"common.message.cancel": "キャンセル",
"common.message.send": "メールを送信しました.",
"common.message.no.delete": "削除するデータがありません",
"common.message.save": "保存",
"common.message.transfer": "転送",
"common.message.batch.exec": "batch success",
"common.message.not.mov": "移動できません.",
"common.message.required.data": "{0} は入力必須項目となります。",
"common.message.save.error": "データの保存中にエラーが発生しました。 サイト管理者にお問い合わせください。",
"common.message.transfer.error": "データの転送中にエラーが発生しました。 サイト管理者にお問い合わせください。",
"common.message.delete.error": "データの削除中にエラーが発生しました。 サイト管理者にお問い合わせください。",
"common.message.batch.error": "バッチの実行中にエラーが発生しました。 サイト管理者に連絡してください。",
"common.message.send.error": "データの送信中にエラーが発生しました。サイト管理者にお問い合わせください",
"common.message.communication.error": "ネットワークエラーが発生しました。サイト管理者に連絡してください。",
"common.message.data.error": "{0} はデータ形式が無効です。",
"common.message.data.setting.error": "{0} は削除されたか、すでに構成されているデータです。",
"common.message.parameter.error": "パラメータエラー",
"common.message.product.parameter.error": "存在しない製品があります。",
"common.message.customer.parameter.error": "存在しない顧客があります。",
"common.message.file.exists.error": "ファイルが正常にアップロードされないためにエラーが発生しました",
"common.message.file.download.exists": "ファイルが存在しません。",
"common.message.file.download.error": "ァイルのダウンロードエラー",
"common.message.file.template.validation01": "フォルダをアップロードできません",
"common.message.file.template.validation02": "アップロードできるのはExcelファイルのみです。",
"common.message.file.template.validation03": "登録できない拡張子です",
"common.message.file.template.validation04": "容量を超えています アップロード可能な容量:{0} MB",
"common.message.file.template.validation05": "アップロードファイルを選択して下さい",
"common.message.multi.insert": "合計 {0} 件数 ({1}成功、 {2} 失敗 {3})",
"common.message.error": "エラーが発生しました。サイト管理者に連絡してください。",
"common.message.data.save": "保存しますか?",
"common.message.data.delete": " 削除しますか?",
"common.message.data.exists": "{0} はすでに存在するデータです。",
"common.message.data.no.exists": "{0} は存在しないデータです。",
"common.message.all": "All",
"common.message.tab.close.all": "すべてのタブを閉じますか?",
"common.message.transfer.save": "{0}件転送しますか?",
"common.message.confirm.save": "保存しますか?",
"common.message.confirm.confirm": "承認しますか?",
"common.message.confirm.request": "承認リクエストしますか?",
"common.message.confirm.delete": "削除しますか?",
"common.message.confirm.close": "閉じますか?",
"common.message.confirm.unclose": "クローズ中止しますか?",
"common.message.confirm.cancel": "キャンセルしますか?",
"common.message.confirm.uncancel": "キャンセル中止しますか?",
"common.message.confirm.copy": "コピーしますか?",
"common.message.confirm.createSo": "S/O作成しますか",
"common.message.confirm.mark": "保存完了",
"common.message.confirm.mail": "メールを送信しますか?",
"common.message.confirm.printPriceItem": "価格を印刷しますか?",
"common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?",
"common.message.confirm.deliveryFee": "送料を登録しますか?",
"common.message.success.delete": "削除完了",
"common.message.success.close": "閉じる",
"common.message.success.unclose": "キャンセルしました",
"common.message.validation.date": "終了日を開始日より前にすることはできません。 もう一度入力してください。",
"common.message.no.editfield": "フィールドを編集できません",
"common.message.success.rmmail": "リスク管理チームにメールを送信しました。",
"common.message.password.validation01": "パスワードの変更が一致しません。",
"common.message.password.validation02": "英語、数字、特殊文字を組み合わせた8桁以上を入力してください。",
"common.message.password.validation03": "パスワードをIDと同じにすることはできません。",
"common.message.menu.validation01": "注文を保存するメニューはありません.",
"common.message.menu.validation02": "The same sort order exists.",
"common.message.menuCode.check01": "登録可能",
"common.message.menuCode.check02": "登録できません",
"common.message.pleaseSelect": "{0}を選択してください",
"common.message.pleaseInput": "{0}を入力してください。",
"common.message.pleaseInputOr": "{0}または{1}を入力してください。",
"common.message.approved ": "承認済み",
"common.message.errorFieldExist": "エラー項目が存在します",
"common.message.storeIdExist ": "既に利用されている販売店IDです",
"common.message.userIdExist ": "すでに使用しているユーザーID。",
"common.message.noExists ": "削除された掲示物です",
"common.message.emailReqTo": "メール宛先が必要です",
"common.message.downloadPeriod": "ダウンロード検索期間を{0}日以内に選択してください。",
"common.message.backToSubmit": "販売店ブロック解除実行しますか?",
"common.message.backToG3": "Back to G3処理実行しますか",
"common.message.writeToConfirm": "作成解除を実行しますか?",
"common.message.password.init.success": "パスワード [{0}] に初期化されました。",
"common.message.no.edit.save": "この文書は変更できません。"
}

View File

@ -1,3 +1,90 @@
{
"hi": "안녕하세요"
"hi": "안녕하세요",
"common.message.no.data": "No data",
"common.message.no.dataDown": "No data to download",
"common.message.noData": "No data to display",
"common.message.search": "search success",
"common.message.insert": "insert success",
"common.message.update": "update success",
"common.message.delete": "Deleted",
"common.message.restoration": "Restored",
"common.message.cancel": "Canceled",
"common.message.send": "The mail has been sent.",
"common.message.no.delete": "There is no data to delete.",
"common.message.save": "Saved.",
"common.message.transfer": "Transfered",
"common.message.batch.exec": "batch success",
"common.message.not.mov": "Its impossible to move.",
"common.message.required.data": "{0} is required input value.",
"common.message.save.error": "An error occurred while saving the data. Please contact site administrator.",
"common.message.transfer.error": "An error occurred while transfer the data. Please contact site administrator.",
"common.message.delete.error": "An error occurred while deleting data. Please contact site administrator.",
"common.message.batch.error": "An error occurred while executing the batch. Please contact site administrator.",
"common.message.send.error": "Error sending data, please contact your administrator.",
"common.message.communication.error": "Network error occurred. \n Please contact site administrator.",
"common.message.data.error": "{0} The data format is not valid.",
"common.message.data.setting.error": "{0} is data that has been deleted or already configured.",
"common.message.parameter.error": "Parameter Error",
"common.message.product.parameter.error": "존재하지 않는 제품이 있습니다.",
"common.message.customer.parameter.error": "존재하지 않는 고객이 있습니다.",
"common.message.file.exists.error": "Error due to file not uploading normally",
"common.message.file.download.exists": "File does not exist.",
"common.message.file.download.error": "File download error",
"common.message.file.template.validation01": "Unable to upload folder",
"common.message.file.template.validation02": "Only Excel files can be uploaded.",
"common.message.file.template.validation03": "Non-registerable extension",
"common.message.file.template.validation04": "Exceed capacity \n Uploadable capacity : {0} MB",
"common.message.file.template.validation05": "업로드 파일을 선택해주세요.",
"common.message.multi.insert": "Total {0} cases ({1} successes, {2} failures {3})",
"common.message.error": "Error occurred, please contact site administrator.",
"common.message.data.save": "Do you want to save it?",
"common.message.data.delete": "Do you want to delete it?",
"common.message.data.exists": "{0} is data that already exists.",
"common.message.data.no.exists": "{0} is data that does not exist.",
"common.message.all": "All",
"common.message.tab.close.all": "Close all tabs?",
"common.message.transfer.save": "Want to {0} transfer it?",
"common.message.confirm.save": "Want to save it?",
"common.message.confirm.confirm": "Want to approve?",
"common.message.confirm.request": "Would you like to request a Approval?",
"common.message.confirm.delete": "Do you want to delete it?",
"common.message.confirm.close": "Want to close?",
"common.message.confirm.unclose": "Do you want to cancel the close?",
"common.message.confirm.cancel": "Want to cancellation?",
"common.message.confirm.uncancel": "Do you want to cancel the cancellation?",
"common.message.confirm.copy": "Do you want to copy?",
"common.message.confirm.createSo": "Create Sales Order?",
"common.message.confirm.mark": "Saved.",
"common.message.confirm.mail": "Do you want to send mail?",
"common.message.confirm.printPriceItem": "Would you like to print item price?",
"common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?",
"common.message.confirm.deliveryFee": "Do you want to register shipping fee?",
"common.message.success.delete": "Deleted.",
"common.message.success.close": "Closed.",
"common.message.success.unclose": "Cancel Closed.",
"common.message.validation.date": "The end date cannot be earlier than the start date. Please enter it again.",
"common.message.no.editfield": "Can not edit field",
"common.message.success.rmmail": "You have successfully sent mail to the Risk Management team.",
"common.message.password.validation01": "Change passwords do not match.",
"common.message.password.validation02": "Please enter at least 8 digits combining English, numbers, and special characters.",
"common.message.password.validation03": "Password cannot be the same as ID.",
"common.message.menu.validation01": "There is no menu to save the order.",
"common.message.menu.validation02": "The same sort order exists.",
"common.message.menuCode.check01": "Registerable",
"common.message.menuCode.check02": "Unable to register",
"common.message.pleaseSelect": "Please Select {0}",
"common.message.pleaseInput": "Please Input a {0}.",
"common.message.pleaseInputOr": "Please Input a {0} or {1}.",
"common.message.approved ": "Approved.",
"common.message.errorFieldExist": "Error Field Exist",
"common.message.storeIdExist ": "이미 사용하고 있는 판매점 ID 입니다.",
"common.message.userIdExist ": "이미 사용하고 있는 사용자 ID 입니다.",
"common.message.noExists ": "삭제된 게시물 입니다.",
"common.message.emailReqTo": "Email To is required",
"common.message.downloadPeriod": "Please select the download search period within {0} days.",
"common.message.backToSubmit": "판매점 블록 해제를 실행하시겠습니까?",
"common.message.backToG3": "Back to G3 처리를 실행하시겠습니까?",
"common.message.writeToConfirm": "작성 해제를 실행하시겠습니까?",
"common.message.password.init.success": "비밀번호 [{0}]로 초기화 되었습니다.",
"common.message.no.edit.save": "This document cannot be changed."
}

View File

@ -126,3 +126,50 @@ export const customSettingsState = atom({
default: {},
dangerouslyAllowMutability: true,
})
// cad 도면 파일 사용 여부
export const useCadFileState = atom({
key: 'useCadFile',
default: false,
})
// cad 도면 파일 이름
export const cadFileNameState = atom({
key: 'cadFileName',
default: '',
})
// cad 도면 파일 조정 완료
export const cadFileCompleteState = atom({
key: 'cadFileComplete',
default: false,
})
export const useGoogleMapFileState = atom({
key: 'useGoogleMapFile',
default: false,
})
// 구글맵 저장 이미지 파일 이름
export const googleMapFileNameState = atom({
key: 'googleMapFileName',
default: '',
})
export const globalCompassState = atom({
key: 'globalCompass',
default: 0,
dangerouslyAllowMutability: true,
})
// 면형상 배치 모드
export const surfacePlacementModeState = atom({
key: 'surfacePlacementMode',
default: { width: 0, height: 0, areaBoundary: true, inputType: 'free' },
})
// 오브젝트 배치 모드
export const objectPlacementModeState = atom({
key: 'objectPlacementMode',
default: { width: 0, height: 0, areaBoundary: false, inputType: 'free', batchType: 'opening' },
})

View File

@ -705,3 +705,23 @@ export const getClosestVerticalLine = (pointer, verticalLineArray) => {
return closestLine
}
/**
* 빗변과 높이를 가지고 빗변의 x값을 구함
* @param {} p1
* @param {*} p2
* @param {*} y
* @returns
*/
export const getIntersectionPoint = (p1, p2, y) => {
const { x: x1, y: y1 } = p1
const { x: x2, y: y2 } = p2
// 기울기와 y 절편을 이용해 선의 방정식을 구합니다.
const slope = (y2 - y1) / (x2 - x1)
const intercept = y1 - slope * x1
// y = 150일 때의 x 좌표를 구합니다.
const x = (y - intercept) / slope
return { x, y }
}

View File

@ -2704,3 +2704,314 @@ export const inPolygon = (polygonPoints, rectPoints) => {
return allPointsInsidePolygon && noPolygonPointsInsideRect
}
/**
* poolygon의 방향에 따라 화살표를 추가한다.
* @param polygon
*/
export const drawDirectionArrow = (polygon) => {
const direction = polygon.direction
if (!direction) {
return
}
polygon.canvas
.getObjects()
.filter((obj) => obj.name === 'directionText' && obj.parent === polygon.arrow)
.forEach((obj) => polygon.canvas.remove(obj))
let arrow = null
let points = []
if (polygon.arrow) {
polygon.canvas.remove(polygon.arrow)
}
let centerPoint = { x: polygon.width / 2 + polygon.left, y: polygon.height / 2 + polygon.top }
let stickeyPoint
const polygonMaxX = Math.max(...polygon.getCurrentPoints().map((point) => point.x))
const polygonMinX = Math.min(...polygon.getCurrentPoints().map((point) => point.x))
const polygonMaxY = Math.max(...polygon.getCurrentPoints().map((point) => point.y))
const polygonMinY = Math.min(...polygon.getCurrentPoints().map((point) => point.y))
switch (direction) {
case 'north':
points = [
{ x: centerPoint.x, y: polygonMinY - 20 },
{ x: centerPoint.x + 20, y: polygonMinY - 20 },
{ x: centerPoint.x + 20, y: polygonMinY - 50 },
{ x: centerPoint.x + 50, y: polygonMinY - 50 },
{ x: centerPoint.x, y: polygonMinY - 80 },
{ x: centerPoint.x - 50, y: polygonMinY - 50 },
{ x: centerPoint.x - 20, y: polygonMinY - 50 },
{ x: centerPoint.x - 20, y: polygonMinY - 20 },
]
stickeyPoint = { x: centerPoint.x, y: polygonMinY - 80 }
break
case 'south':
points = [
{ x: centerPoint.x, y: polygonMaxY + 20 },
{ x: centerPoint.x + 20, y: polygonMaxY + 20 },
{ x: centerPoint.x + 20, y: polygonMaxY + 50 },
{ x: centerPoint.x + 50, y: polygonMaxY + 50 },
{ x: centerPoint.x, y: polygonMaxY + 80 },
{ x: centerPoint.x - 50, y: polygonMaxY + 50 },
{ x: centerPoint.x - 20, y: polygonMaxY + 50 },
{ x: centerPoint.x - 20, y: polygonMaxY + 20 },
]
stickeyPoint = { x: centerPoint.x, y: polygonMaxY + 80 }
break
case 'west':
points = [
{ x: polygonMinX - 20, y: centerPoint.y },
{ x: polygonMinX - 20, y: centerPoint.y + 20 },
{ x: polygonMinX - 50, y: centerPoint.y + 20 },
{ x: polygonMinX - 50, y: centerPoint.y + 50 },
{ x: polygonMinX - 80, y: centerPoint.y },
{ x: polygonMinX - 50, y: centerPoint.y - 50 },
{ x: polygonMinX - 50, y: centerPoint.y - 20 },
{ x: polygonMinX - 20, y: centerPoint.y - 20 },
]
stickeyPoint = { x: polygonMinX - 80, y: centerPoint.y }
break
case 'east':
points = [
{ x: polygonMaxX + 20, y: centerPoint.y },
{ x: polygonMaxX + 20, y: centerPoint.y + 20 },
{ x: polygonMaxX + 50, y: centerPoint.y + 20 },
{ x: polygonMaxX + 50, y: centerPoint.y + 50 },
{ x: polygonMaxX + 80, y: centerPoint.y },
{ x: polygonMaxX + 50, y: centerPoint.y - 50 },
{ x: polygonMaxX + 50, y: centerPoint.y - 20 },
{ x: polygonMaxX + 20, y: centerPoint.y - 20 },
]
stickeyPoint = { x: polygonMaxX + 80, y: centerPoint.y }
break
}
arrow = new fabric.Polygon(points, {
selectable: false,
name: 'arrow',
fill: 'transparent',
stroke: 'black',
direction: direction,
parent: polygon,
stickeyPoint: stickeyPoint,
})
polygon.arrow = arrow
polygon.canvas.add(arrow)
polygon.canvas.renderAll()
drawDirectionStringToArrow(polygon.canvas, 0)
}
/**
* 방향을 나타낸 화살표에 각도에 따라 글씨 추가
* @param canvas
* @param compass
*/
export const drawDirectionStringToArrow = (canvas, compass = 0) => {
const arrows = canvas?.getObjects().filter((obj) => obj.name === 'arrow')
if (arrows.length === 0) {
return
}
const eastArrows = arrows.filter((arrow) => arrow.direction === 'east')
const westArrows = arrows.filter((arrow) => arrow.direction === 'west')
const northArrows = arrows.filter((arrow) => arrow.direction === 'north')
const southArrows = arrows.filter((arrow) => arrow.direction === 'south')
let southText = '南'
let eastText = '東'
let westText = '西'
let northText = '北'
if (compass === 0 || compass === 360) {
// 남,동,서 가능
// 그대로
} else if (compass < 45) {
//남(남남동),동(동북동),서(서남서) 가능
//북(북북서)
southText = '南南東'
eastText = '東北東'
westText = '西南西'
northText = '北北西'
} else if (compass === 45) {
// 남, 서 가능
// 남(남동)
// 서(남서)
// 북(북서)
// 동(북동)
southText = '南東'
westText = '南西'
northText = '北西'
eastText = '北東'
} else if (compass < 90) {
// 북(서북서)
// 동 (북북동)
// 남(동남동)
// 서(남남서)
northText = '北西北'
eastText = '北北東'
southText = '東南東'
westText = '南南西'
} else if (compass === 90) {
// 동(북)
// 서(남)
// 남(동)
// 북(서)
eastText = '北'
westText = '南'
southText = '東'
northText = '西'
} else if (compass < 135) {
// 남,서,북 가능
// 동(북북서)
// 서(남남동)
// 남(동북동)
// 북(서남서)
eastText = '北北西'
westText = '南南東'
southText = '東北東'
northText = '西南西'
} else if (compass === 135) {
// 서,북 가능
// 서(남동)
// 북(남서)
// 남(북동)
// 동(북서)
westText = '南東'
northText = '南西'
southText = '北東'
eastText = '北西'
} else if (compass < 180) {
// 북,동,서 가능
// 북(남남서)
// 동(서북서)
// 남(북북동)
// 서(동남동)
northText = '南南西'
eastText = '西北西'
southText = '北北東'
westText = '東南東'
} else if (compass === 180) {
// 북,동,서 가능
// 북(남)
// 동(서)
// 남(북)
// 서(동)
northText = '南'
eastText = '西'
southText = '北'
westText = '東'
} else if (compass < 225) {
// 서,북,동 가능
// 북(남남동)
// 동(서남서)
// 남(북북서)
// 서(동남동)
northText = '南南東'
eastText = '西南西'
southText = '北北西'
westText = '東南東'
} else if (compass === 225) {
// 북,동 가능
// 북(남동)
// 동(남서)
// 남(북서)
// 서(북동)
northText = '南東'
eastText = '南西'
southText = '北西'
westText = '北東'
} else if (compass < 270) {
// 북동남 가능
// 북(동남동)
// 동(남남서)
// 남(서북서)
// 서(북북동)
northText = '東南東'
eastText = '南南西'
southText = '西北西'
westText = '北北東'
} else if (compass === 270) {
// 북동남 가능
// 북(동)
// 동(남)
// 남(서)
// 서(북)
northText = '東'
eastText = '南'
southText = '西'
westText = '北'
} else if (compass < 315) {
// 북,동,남 가능
// 북(동북동)
// 동(남남동)
// 남(서남서)
// 서(북북서)
northText = '東北東'
eastText = '南南東'
southText = '西南西'
westText = '北北西'
} else if (compass === 315) {
// 동,남 가능
// 북(북동)
// 동(남동)
// 남(남서)
// 서(북서)
northText = '北東'
eastText = '南東'
southText = '南西'
westText = '北西'
} else if (compass < 360) {
// 남,동,서 가능
// 북(북북동)
// 동(동남동)
// 남(남남서)
// 서(서북서)
northText = '北北東'
eastText = '東南東'
southText = '南南西'
westText = '西北西'
}
clearDirectionText(canvas)
addTextByArrows(eastArrows, eastText, canvas)
addTextByArrows(westArrows, westText, canvas)
addTextByArrows(northArrows, northText, canvas)
addTextByArrows(southArrows, southText, canvas)
}
const clearDirectionText = (canvas) => {
const texts = canvas.getObjects().filter((obj) => obj.name === 'directionText')
texts.forEach((text) => {
canvas.remove(text)
})
}
const addTextByArrows = (arrows, txt, canvas) => {
arrows.forEach((arrow, index) => {
const text = new fabric.Text(`${txt}${index + 1}`, {
fontSize: arrow.parent.fontSize,
fill: 'black',
originX: 'center',
originY: 'center',
name: 'directionText',
selectable: false,
left: arrow.stickeyPoint.x,
top: arrow.stickeyPoint.y,
parent: arrow,
})
canvas.add(text)
})
}

View File

@ -4017,6 +4017,15 @@ asynckit@^0.4.0:
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.6.2:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.7.3:
version "1.7.3"
resolved "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz"
@ -4252,6 +4261,13 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
convertapi@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/convertapi/-/convertapi-1.14.0.tgz#a291a98cb986ae1e0f2340a130adbe17f65c8c76"
integrity sha512-9Rzkn+Mjs4jVLQ5pRUC8KpIjnT9WFUkuJZ5yjCJaDJsDM7Na2lWPKtDJdkfKcYCNDuo1h+OYZedne5SLp60EkQ==
dependencies:
axios "^1.6.2"
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
@ -5499,6 +5515,11 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-icons@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"
integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"