Merge branch 'dev' into feature/test-jy

# Conflicts:
#	src/components/fabric/QPolygon.js
This commit is contained in:
Jaeyoung Lee 2024-08-12 13:20:11 +09:00
commit 11e84ed614
17 changed files with 8464 additions and 1084 deletions

4320
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,10 +24,12 @@
"react-datepicker": "^7.3.0", "react-datepicker": "^7.3.0",
"react-dom": "^18", "react-dom": "^18",
"react-responsive-modal": "^6.4.2", "react-responsive-modal": "^6.4.2",
"react-toastify": "^10.0.5",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@turf/turf": "^7.0.0",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^5.18.0", "prisma": "^5.18.0",

View File

@ -1,16 +1,26 @@
'use client' 'use client'
import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react' import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
import { AxiosType, useAxios } from '@/hooks/useAxios'
// import { get } from '@/lib/Axios'
import QSelect from '@/components/ui/QSelect' import QSelect from '@/components/ui/QSelect'
import styles from './changelog.module.css' import styles from './changelog.module.css'
import { get } from '@/lib/Axios'
export default function changelogPage() { export default function changelogPage() {
const { get } = useAxios()
const testVar = process.env.NEXT_PUBLIC_TEST const testVar = process.env.NEXT_PUBLIC_TEST
const handleUsers = async () => { const handleUsers = async () => {
const users = await get('/api/user/find-all') // const users = await get('/api/user/find-all')
console.log(users) const params = {
type: AxiosType.INTERNAL,
url: '/api/user/find-all',
}
const users = await get(params)
console.log('users', users)
} }
const data = [ const data = [

View File

@ -1,10 +1,15 @@
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
import './globals.css'
import '../styles/style.scss'
import Headers from '@/components/Headers'
import RecoilRootWrapper from './RecoilWrapper' import RecoilRootWrapper from './RecoilWrapper'
import UIProvider from './UIProvider' import UIProvider from './UIProvider'
import { headers } from 'next/headers' import { headers } from 'next/headers'
import Headers from '@/components/Headers'
import { ToastContainer } from 'react-toastify'
import QModal from '@/components/common/modal/QModal'
import './globals.css'
import '../styles/style.scss'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
@ -24,6 +29,8 @@ export default function RootLayout({ children }) {
{headerPathname !== '/login' && <Headers />} {headerPathname !== '/login' && <Headers />}
<RecoilRootWrapper> <RecoilRootWrapper>
<UIProvider>{children}</UIProvider> <UIProvider>{children}</UIProvider>
<QModal />
<ToastContainer />
</RecoilRootWrapper> </RecoilRootWrapper>
</body> </body>
</html> </html>

View File

@ -4,15 +4,22 @@ import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useRecoilState } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom'
import { AxiosType, useAxios } from '@/hooks/useAxios'
import { Button } from '@nextui-org/react' import { Button } from '@nextui-org/react'
import SingleDatePicker from './common/datepicker/SingleDatePicker' import SingleDatePicker from './common/datepicker/SingleDatePicker'
import RangeDatePicker from './common/datepicker/RangeDatePicker' import RangeDatePicker from './common/datepicker/RangeDatePicker'
import QGrid from './common/grid/QGrid' import QGrid from './common/grid/QGrid'
import QModal from './common/modal/QModal' import { QToast } from '@/hooks/useToast'
export default function Intro() { export default function Intro() {
const [open, setOpen] = useState(false) const { get } = useAxios()
// const [open, setOpen] = useState(false)
const [startDate, setStartDate] = useState(new Date()) const [startDate, setStartDate] = useState(new Date())
const singleDatePickerProps = { const singleDatePickerProps = {
startDate, startDate,
@ -35,6 +42,9 @@ export default function Intro() {
isPageable: false, isPageable: false,
}) })
const [open, setOpen] = useRecoilState(modalState)
const [contents, setContent] = useRecoilState(modalContent)
const modelProps = { const modelProps = {
open, open,
setOpen, setOpen,
@ -60,8 +70,9 @@ export default function Intro() {
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
const response = await fetch('https://www.ag-grid.com/example-assets/space-mission-data.json') // const response = await fetch('https://www.ag-grid.com/example-assets/space-mission-data.json')
const data = await response.json() // const data = await response.json()
const data = await get({ type: AxiosType.EXTERNAL, url: 'https://www.ag-grid.com/example-assets/space-mission-data.json' })
setGridProps({ ...gridProps, gridData: data }) setGridProps({ ...gridProps, gridData: data })
} }
fetchData() fetchData()
@ -95,10 +106,35 @@ export default function Intro() {
<div className="my-2"> <div className="my-2">
<div className="text-2xl">QModal</div> <div className="text-2xl">QModal</div>
<div> <div>
<Button color="primary" onClick={() => setOpen(true)}> {/* <Button color="primary" onClick={() => setOpen(true)}>
Open Modal Open Modal
</Button> </Button>
<QModal {...modelProps}>{ipsum}</QModal> <QModal {...modelProps}>{ipsum}</QModal> */}
<Button
color="primary"
onClick={() => {
setContent(ipsum)
setOpen(true)
}}
>
Open Modal
</Button>
</div>
</div>
<div className="my-2">
<div className="text-2xl">QToast</div>
<div>
<Button
color="primary"
onClick={() => {
QToast({
message: 'This is a toast message',
type: 'success',
})
}}
>
Open Toast
</Button>
</div> </div>
</div> </div>
</> </>

View File

@ -2,14 +2,15 @@ import { useCanvas } from '@/hooks/useCanvas'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Mode, useMode } from '@/hooks/useMode' import { Mode, useMode } from '@/hooks/useMode'
import { Button } from '@nextui-org/react' import { Button } from '@nextui-org/react'
import RangeSlider from './ui/RangeSlider' import RangeSlider from './ui/RangeSlider'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { canvasSizeState, fontSizeState, roofMaterialState, sortedPolygonArray, templateTypeState } from '@/store/canvasAtom' import { canvasSizeState, fontSizeState, roofMaterialState, sortedPolygonArray, templateTypeState, compassState } from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { getCanvasState, insertCanvasState } from '@/lib/canvas' import { getCanvasState, insertCanvasState } from '@/lib/canvas'
import { calculateIntersection } from '@/util/canvas-util' import { calculateIntersection } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import * as turf from '@turf/turf'
import { toGeoJSON } from '@/util/qpolygon-utils'
export default function Roof2() { export default function Roof2() {
const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas') const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas')
@ -35,6 +36,8 @@ export default function Roof2() {
const [templateType, setTemplateType] = useRecoilState(templateTypeState) const [templateType, setTemplateType] = useRecoilState(templateTypeState)
const [compass, setCompass] = useRecoilState(compassState)
const { const {
mode, mode,
setMode, setMode,
@ -51,6 +54,7 @@ export default function Roof2() {
makeRoofPatternPolygon, makeRoofPatternPolygon,
createRoofRack, createRoofRack,
drawRoofPolygon, drawRoofPolygon,
drawCellInTrestle,
} = useMode() } = useMode()
// const [canvasState, setCanvasState] = useRecoilState(canvasAtom) // const [canvasState, setCanvasState] = useRecoilState(canvasAtom)
@ -492,6 +496,10 @@ export default function Roof2() {
}) })
} }
const setCompassState = (degree) => {
setCompass(degree)
}
return ( return (
<> <>
{canvas && ( {canvas && (
@ -521,12 +529,27 @@ export default function Roof2() {
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => drawRoofPatterns(2)}> <Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => drawRoofPatterns(2)}>
지붕패턴2 지붕패턴2
</Button> </Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(90)}>
방위표
</Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(270)}>
방위표
</Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(180)}>
방위표
</Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(0)}>
방위표
</Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.ROOF_TRESTLE)}> <Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.ROOF_TRESTLE)}>
지붕가대생성 지붕가대생성
</Button> </Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.FILL_CELLS)}> <Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.FILL_CELLS)}>
태양광셀생성 태양광셀생성
</Button> </Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.CELL_POWERCON)}>
파워콘설치
</Button>
<Button className="m-1 p-2" color={`${mode === Mode.TEXTBOX ? 'primary' : 'default'}`} onClick={() => setMode(Mode.TEXTBOX)}> <Button className="m-1 p-2" color={`${mode === Mode.TEXTBOX ? 'primary' : 'default'}`} onClick={() => setMode(Mode.TEXTBOX)}>
텍스트박스 모드 텍스트박스 모드
</Button> </Button>
@ -542,9 +565,10 @@ export default function Roof2() {
{/*<Button className="m-1 p-2" onClick={zoomIn}> {/*<Button className="m-1 p-2" onClick={zoomIn}>
확대 확대
</Button> </Button>
*/}
<Button className="m-1 p-2" onClick={zoomOut}> <Button className="m-1 p-2" onClick={zoomOut}>
축소 축소
</Button>*/} </Button>
현재 : {zoom}% 현재 : {zoom}%
<Button className="m-1 p-2" onClick={makeLine}> <Button className="m-1 p-2" onClick={makeLine}>
추가 추가
@ -596,7 +620,10 @@ export default function Roof2() {
지붕타입 지붕재 지붕타입 지붕재
</Button> </Button>
<Button className="m-1 p-2" onClick={createRoofRack}> <Button className="m-1 p-2" onClick={createRoofRack}>
지붕가대 지붕가대설치
</Button>
<Button className="m-1 p-2" onClick={drawCellInTrestle}>
선택한 가대 셀채우기
</Button> </Button>
</> </>
)} )}

View File

@ -1,8 +1,16 @@
'use client'
import { useRecoilState, useRecoilValue } from 'recoil'
import { Modal } from 'react-responsive-modal' import { Modal } from 'react-responsive-modal'
import { modalContent, modalState } from '@/store/modalAtom'
import 'react-responsive-modal/styles.css' import 'react-responsive-modal/styles.css'
export default function QModal(props) { export default function QModal() {
const { open, setOpen, children } = props const [open, setOpen] = useRecoilState(modalState)
const children = useRecoilValue(modalContent)
return ( return (
<Modal open={open} onClose={() => setOpen(false)} center> <Modal open={open} onClose={() => setOpen(false)} center>

View File

@ -2,7 +2,8 @@ import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util'
import { calculateAngle, drawHippedRoof } from '@/util/qpolygon-utils' import { calculateAngle, drawHippedRoof, inPolygon, lineIntersect, splitPolygonWithLines, toGeoJSON } from '@/util/qpolygon-utils'
import * as turf from '@turf/turf'
export const QPolygon = fabric.util.createClass(fabric.Polygon, { export const QPolygon = fabric.util.createClass(fabric.Polygon, {
type: 'QPolygon', type: 'QPolygon',
@ -238,14 +239,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
const rectPoints = [ const rectPoints = [
{ x: rectLeft, y: rectTop }, { x: rectLeft, y: rectTop },
{ x: rectLeft + rectWidth, y: rectTop },
{ x: rectLeft, y: rectTop + rectHeight }, { x: rectLeft, y: rectTop + rectHeight },
{ x: rectLeft + rectWidth, y: rectTop + rectHeight }, { x: rectLeft + rectWidth, y: rectTop + rectHeight },
{ x: rectLeft + rectWidth, y: rectTop },
] ]
const allPointsInside = rectPoints.every((point) => this.inPolygon(point)) if (inPolygon(this.points, rectPoints)) {
if (allPointsInside) {
const rect = new fabric.Rect({ const rect = new fabric.Rect({
left: rectLeft, left: rectLeft,
top: rectTop, top: rectTop,
@ -275,7 +274,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.cells = drawCellsArray this.cells = drawCellsArray
return drawCellsArray return drawCellsArray
}, },
fillCellABTemplate(cell = { width: 50, height: 100, padding: 5, parallelPoint: undefined, startX: 0, startY: 0 }) { fillCellABType(cell = { width: 50, height: 100, padding: 5, wallDirection: 'left', referenceDirection: 'none', startIndex: -1 }) {
const points = this.points const points = this.points
const minX = Math.min(...points.map((p) => p.x)) //왼쪽 const minX = Math.min(...points.map((p) => p.x)) //왼쪽
@ -293,44 +292,43 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
const rows = Math.floor((boundingBoxHeight + cell.padding) / (rectHeight + cell.padding)) const rows = Math.floor((boundingBoxHeight + cell.padding) / (rectHeight + cell.padding))
//전체 높이에서 패딩을 포함하고 rows를 곱해서 여백길이를 계산 후에 2로 나누면 반높이를 넣어서 중간으로 정렬 //전체 높이에서 패딩을 포함하고 rows를 곱해서 여백길이를 계산 후에 2로 나누면 반높이를 넣어서 중간으로 정렬
const tmpHeight = (boundingBoxHeight - (rectHeight + cell.padding) * rows) / 2
//센터 정렬시에 쓴다 체크박스가 존재함 TODO: if문 추가해서 정렬해야함 let centerHeight = rows > 1 ? (boundingBoxHeight - (rectHeight + cell.padding / 2) * rows) / 2 : (boundingBoxHeight - rectHeight * rows) / 2 //rows 1개 이상이면 cell 을 반 나눠서 중간을 맞춘다
const tmpWidth = (boundingBoxWidth - (rectWidth + cell.padding) * cols) / 2 let centerWidth = cols > 1 ? (boundingBoxWidth - (rectWidth + cell.padding / 2) * cols) / 2 : (boundingBoxWidth - rectWidth * cols) / 2
const drawCellsArray = [] //그려진 셀의 배열 const drawCellsArray = [] //그려진 셀의 배열
let idx = 1 let idx = 1
if (cell.parallelPoint > -1) {
//4각형 이상이면 각 꼭지점에서 위, 아래로 이동하면서 채움
//앞에서 -1로 선언함
if (cell.parallelPoint === 1 || cell.parallelPoint === 2) {
//ㄴ자 역ㄱ자
//밑에서 위로 올라가야하면
cell.startY = cell.startY - (rectHeight + cell.padding) * rows
if (cell.parallelPoint === 2) {
cell.startX = cell.startX - (rectWidth + cell.padding) * cols
}
} else {
if (cell.parallelPoint === 5) {
cell.startX = cell.startX - (rectWidth + cell.padding) * cols
}
}
}
let startXPos, startYPos let startXPos, startYPos
for (let i = 0; i < cols; i++) { for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) { for (let j = 0; j < rows; j++) {
if (cell.parallelPoint > -1) { const rectPoints = []
startXPos = cell.startX + i * (rectWidth + cell.padding)
startYPos = cell.startY + j * (rectHeight + cell.padding) if (cell.referenceDirection !== 'none') {
//4각형은 기준점이 없다
if (cell.referenceDirection === 'top') {
//top, bottom은 A패턴만
if (cell.wallDirection === 'left') {
startXPos = minX + i * rectWidth
startYPos = minY + j * rectHeight
if (i > 0) {
startXPos = startXPos + i * cell.padding //옆으로 패딩
}
} else { } else {
startXPos = minX + i * (rectWidth + cell.padding) startXPos = maxX - (1 + i) * rectWidth - 0.01
startYPos = minY + j * (rectHeight + cell.padding) startYPos = minY + j * rectHeight + 0.01
if (i > 0) {
startXPos = startXPos - i * cell.padding //옆으로 패딩
}
} }
const rectPoints = [] if (j > 0) {
startYPos = startYPos + j * cell.padding
}
rectPoints.push( rectPoints.push(
{ x: startXPos, y: startYPos }, { x: startXPos, y: startYPos },
@ -338,13 +336,182 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
{ x: startXPos, y: startYPos + rectHeight }, { x: startXPos, y: startYPos + rectHeight },
{ x: startXPos + rectWidth, y: startYPos + rectHeight }, { x: startXPos + rectWidth, y: startYPos + rectHeight },
) )
} else if (cell.referenceDirection === 'bottom') {
if (cell.wallDirection === 'left') {
startXPos = minX + i * rectWidth
startYPos = maxY - j * rectHeight - 0.01
const allPointsInside = rectPoints.every((point) => this.inPolygon(point)) if (i > 0) {
startXPos = startXPos + i * cell.padding
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos + rectWidth, y: startYPos },
{ x: startXPos, y: startYPos - rectHeight },
{ x: startXPos + rectWidth, y: startYPos - rectHeight },
)
} else {
startXPos = maxX - i * rectWidth - 0.01
startYPos = maxY - j * rectHeight - 0.01
if (i > 0) {
startXPos = startXPos - i * cell.padding
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos - rectWidth, y: startYPos },
{ x: startXPos, y: startYPos - rectHeight },
{ x: startXPos - rectWidth, y: startYPos - rectHeight },
)
startXPos = startXPos - rectWidth //우 -> 좌 들어가야해서 마이너스 처리
}
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
if (j > 0) {
startYPos = startYPos - j * cell.padding
}
} else if (cell.referenceDirection === 'left') {
//여기서부턴 B패턴임
if (cell.wallDirection === 'top') {
startXPos = minX + i * rectWidth
startYPos = minY + j * rectHeight
if (i > 0) {
startXPos = startXPos + i * cell.padding //밑으로
}
if (j > 0) {
startYPos = startYPos + j * cell.padding //옆으로 패딩
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos + rectWidth, y: startYPos },
{ x: startXPos, y: startYPos + rectHeight },
{ x: startXPos + rectWidth, y: startYPos + rectHeight },
)
} else {
startXPos = minX + i * rectWidth
startYPos = maxY - j * rectHeight - 0.01
if (i > 0) {
startXPos = startXPos + i * cell.padding
}
if (j > 0) {
startYPos = startYPos - j * cell.padding
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos + rectWidth, y: startYPos },
{ x: startXPos, y: startYPos - rectHeight },
{ x: startXPos + rectWidth, y: startYPos - rectHeight },
)
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
}
} else if (cell.referenceDirection === 'right') {
if (cell.wallDirection === 'top') {
startXPos = maxX - i * rectWidth - 0.01
startYPos = minY + j * rectHeight + 0.01
if (j > 0) {
startYPos = startYPos + j * cell.padding //위에서 밑으로라 +
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos - rectWidth, y: startYPos },
{ x: startXPos, y: startYPos + rectHeight },
{ x: startXPos - rectWidth, y: startYPos + rectHeight },
)
} else {
startXPos = maxX - i * rectWidth - 0.01
startYPos = maxY - j * rectHeight - 0.01
if (j > 0) {
startYPos = startYPos - j * cell.padding
}
rectPoints.push(
{ x: startXPos, y: startYPos },
{ x: startXPos - rectWidth, y: startYPos },
{ x: startXPos, y: startYPos - rectHeight },
{ x: startXPos - rectWidth, y: startYPos - rectHeight },
)
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
}
if (i > 0) {
startXPos = startXPos - i * cell.padding //옆으로 패딩
}
startXPos = startXPos - rectWidth // 우측에서 -> 좌측으로 그려짐
}
} else {
// centerWidth = 0 //나중에 중간 정렬 이면 어쩌구 함수 만들어서 넣음
if (['left', 'right'].includes(cell.wallDirection)) {
centerWidth = 0
} else if (['top', 'bottom'].includes(cell.wallDirection)) {
centerHeight = 0
}
if (cell.wallDirection === 'left') {
startXPos = minX + i * rectWidth + centerWidth
startYPos = minY + j * rectHeight + centerHeight
if (i > 0) {
startXPos = startXPos + i * cell.padding
}
if (j > 0 && j < rows) {
startYPos = startYPos + j * cell.padding
}
} else if (cell.wallDirection === 'right') {
startXPos = maxX - (1 + i) * rectWidth - 0.01 - centerWidth
startYPos = minY + j * rectHeight + 0.01 + centerHeight
if (i > 0) {
startXPos = startXPos - i * cell.padding
}
if (j > 0 && j < rows) {
startYPos = startYPos + j * cell.padding
}
} else if (cell.wallDirection === 'top') {
startXPos = minX + i * rectWidth - 0.01 + centerWidth
startYPos = minY + j * rectHeight + 0.01 + centerHeight
if (i > 0) {
startXPos = startXPos + i * cell.padding
}
if (j > 0 && j < rows) {
startYPos = startYPos + j * cell.padding
}
} else if (cell.wallDirection === 'bottom') {
startXPos = minX + i * rectWidth + 0.01 + centerWidth
startYPos = maxY - (j + 1) * rectHeight - 0.01 - centerHeight
if (i > 0) {
startXPos = startXPos + i * cell.padding
}
if (j > 0 && j < rows) {
startYPos = startYPos - j * cell.padding
}
}
}
const allPointsInside = rectPoints.every((point) => this.inPolygonABType(point.x, point.y, points))
if (allPointsInside) { if (allPointsInside) {
//먼저 그룹화를 시켜놓고 뒤에서 글씨를 넣어서 변경한다
const text = new fabric.Text(``, {
fontFamily: 'serif',
fontSize: 30,
fill: 'black',
type: 'cellText',
})
const rect = new fabric.Rect({ const rect = new fabric.Rect({
left: startXPos, // left: startXPos,
top: startYPos, // top: startYPos,
width: rectWidth, width: rectWidth,
height: rectHeight, height: rectHeight,
fill: '#BFFD9F', fill: '#BFFD9F',
@ -358,15 +525,22 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
opacity: 0.8, opacity: 0.8,
name: 'cell', name: 'cell',
idx: idx, idx: idx,
type: 'cellRect',
})
var group = new fabric.Group([rect, text], {
left: startXPos,
top: startYPos,
}) })
idx++ idx++
drawCellsArray.push(rect) //배열에 넣어서 반환한다 drawCellsArray.push(group) //배열에 넣어서 반환한다
this.canvas.add(rect) this.canvas.add(group)
}
}
}
this.canvas?.renderAll() this.canvas?.renderAll()
}
}
}
this.cells = drawCellsArray this.cells = drawCellsArray
return drawCellsArray return drawCellsArray
}, },
@ -402,6 +576,57 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
return intersects % 2 === 1 return intersects % 2 === 1
}, },
inPolygonABType(x, y, polygon) {
let inside = false
let n = polygon.length
for (let i = 0, j = n - 1; i < n; j = i++) {
let xi = polygon[i].x
let yi = polygon[i].y
let xj = polygon[j].x
let yj = polygon[j].y
// console.log('xi : ', xi, 'yi : ', yi, 'xj : ', xj, 'yj : ', yj)
let intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
if (intersect) inside = !inside
}
return inside
},
inPolygon2(rectPoints) {
const polygonCoords = toGeoJSON(this.points)
const rectCoords = toGeoJSON(rectPoints)
const outerPolygon = turf.polygon([polygonCoords])
const innerPolygon = turf.polygon([rectCoords])
// 각 점이 다각형 내부에 있는지 확인
const allPointsInside = rectCoords.every((coord) => {
const point = turf.point(coord)
return turf.booleanPointInPolygon(point, outerPolygon)
})
// 사각형의 변 정의
const rectEdges = [
[rectCoords[0], rectCoords[1]],
[rectCoords[1], rectCoords[2]],
[rectCoords[2], rectCoords[3]],
[rectCoords[3], rectCoords[0]],
]
// 다각형의 변 정의
const outerEdges = turf.lineString(outerPolygon.geometry.coordinates[0])
// 사각형의 변들이 다각형의 변과 교차하는지 확인
const noEdgesIntersect = rectEdges.every((edge) => {
const line = turf.lineString(edge)
const intersects = turf.lineIntersect(line, outerEdges)
return intersects.features.length === 0
})
return allPointsInside && noEdgesIntersect
},
distanceFromEdge(point) { distanceFromEdge(point) {
const vertices = this.getCurrentPoints() const vertices = this.getCurrentPoints()
let minDistance = Infinity let minDistance = Infinity

67
src/hooks/useAxios.js Normal file
View File

@ -0,0 +1,67 @@
import axios from 'axios'
export const AxiosType = {
INTERNAL: 'Internal',
EXTERNAL: 'External',
}
export function useAxios() {
const getInstances = (type) => {
return axios.create({
baseURL: type === AxiosType.INTERNAL ? process.env.NEXT_PUBLIC_API_SERVER_PATH : '',
headers: {
Accept: 'application/json',
},
})
}
axios.interceptors.request.use((config) => {
// config['Authorization'] = localStorage.getItem('token')
//TODO: 인터셉터에서 추가 로직 구현
return config
})
axios.interceptors.request.use(undefined, (error) => {
//TODO: 인터셉터에서 에러 처리 로직 구현
// if (error.isAxiosError && e.response?.status === 401) {
// localStorage.removeItem('token')
// }
})
const get = async ({ type, url }) => {
return await getInstances(type)
.get(url)
.then((res) => res.data)
.catch(console.error)
}
const post = async ({ type, url, data }) => {
return await getInstances(type)
.post(url, data)
.then((res) => res.data)
.catch(console.error)
}
const put = async ({ type, url, data }) => {
return await getInstances(type)
.put(url, data)
.then((res) => res.data)
.catch(console.error)
}
const patch = async ({ type, url, data }) => {
return await getInstances(type)
.patch(url, data)
.then((res) => res.data)
.catch(console.error)
}
const del = async ({ type, url }) => {
return await getInstances(type)
.delete(url)
.then((res) => res.data)
.catch(console.error)
}
return { get, post, put, patch, del }
}

View File

@ -89,8 +89,8 @@ export function useCanvas(id) {
canvas?.off('object:modified') canvas?.off('object:modified')
canvas?.off('object:removed') canvas?.off('object:removed')
canvas?.off('object:added') canvas?.off('object:added')
canvas?.off('mouse:move', drawMouseLines) canvas?.off('mouse:move')
canvas?.off('mouse:down', handleMouseDown) canvas?.off('mouse:down')
} }
const addEventOnObject = (e) => { const addEventOnObject = (e) => {
@ -112,9 +112,11 @@ export function useCanvas(id) {
target.on('mousedown', () => { target.on('mousedown', () => {
if (target.get('selected')) { if (target.get('selected')) {
target.set({ strokeWidth: 1 }) target.set({ strokeWidth: 1 })
target.set({ strokeDashArray: [5, 5] })
target.set({ selected: false }) target.set({ selected: false })
} else { } else {
target.set({ strokeWidth: 5 }) target.set({ strokeWidth: 5 })
target.set({ strokeDashArray: [0, 0] })
target.set({ selected: true }) target.set({ selected: true })
} }
canvas?.renderAll() canvas?.renderAll()

File diff suppressed because it is too large Load Diff

35
src/hooks/useToast.js Normal file
View File

@ -0,0 +1,35 @@
import { toast } from 'react-toastify'
const toastDefaultOptions = {
position: 'top-right',
autoClose: 3000,
draggable: false,
hideProgressBar: false,
rtl: false,
pauseOnFocusLoss: true,
pauseOnHover: true,
theme: 'light',
limit: 2,
closeOnClick: true,
}
const QToast = (props) => {
// type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default'
const { message, type = 'info', options } = props
const customOptions = { ...toastDefaultOptions, ...options }
switch (type) {
case 'info':
return toast.info(message, customOptions)
case 'success':
return toast.success(message, customOptions)
case 'warning':
return toast.warn(message, customOptions)
case 'error':
return toast.error(message, customOptions)
default:
return toast(message, customOptions)
}
}
export { QToast }

View File

@ -67,3 +67,9 @@ export const roofMaterialState = atom({
default: { width: 20, height: 10, rafter: 0, roofStyle: 2 }, default: { width: 20, height: 10, rafter: 0, roofStyle: 2 },
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
export const compassState = atom({
key: 'compass',
default: undefined,
dangerouslyAllowMutability: true,
})

15
src/store/modalAtom.js Normal file
View File

@ -0,0 +1,15 @@
import { atom } from 'recoil'
export const modalState = atom({
key: 'modalState',
default: false,
})
export const modalContent = atom({
key: 'modalContent',
default: (
<>
<div>test</div>
</>
),
})

View File

@ -1 +1,2 @@
@import '_test.scss'; @import '_test.scss';
@import 'react-toastify/dist/ReactToastify.css';

View File

@ -9,6 +9,7 @@ import {
getRoofHypotenuse, getRoofHypotenuse,
} from '@/util/canvas-util' } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import * as turf from '@turf/turf'
const TWO_PI = Math.PI * 2 const TWO_PI = Math.PI * 2
@ -983,6 +984,43 @@ export const splitPolygonWithLines = (polygon) => {
}) })
}) })
/**
* 좌표 테스트용
*/
allLines.forEach((line) => {
const text = new fabric.Text(`(${line.startPoint.x},${line.startPoint.y})`, {
left: line.startPoint.x,
top: line.startPoint.y,
fontSize: 15,
})
polygon.canvas.add(text)
polygon.canvas.renderAll()
const text2 = new fabric.Text(`(${line.endPoint.x},${line.endPoint.y})`, {
left: line.endPoint.x,
top: line.endPoint.y,
fontSize: 15,
})
polygon.canvas.add(text2)
polygon.canvas.renderAll()
})
polygon.points.forEach((point, index) => {
const text = new fabric.Text(`(${point.x},${point.y})`, {
left: point.x,
top: point.y,
fontSize: 15,
})
polygon.canvas.add(text)
polygon.canvas.renderAll()
})
/**
* 좌표 테스트용
*/
polygon.points.forEach((point, index) => { polygon.points.forEach((point, index) => {
allLines.forEach((line) => { allLines.forEach((line) => {
if (line.endPoint.x === point.x && line.endPoint.y === point.y) { if (line.endPoint.x === point.x && line.endPoint.y === point.y) {
@ -1007,22 +1045,39 @@ export const splitPolygonWithLines = (polygon) => {
const arrivalPoint = endLine.endPoint const arrivalPoint = endLine.endPoint
routes.push(startLine.startPoint) routes.push(startLine.startPoint)
routes.push(startLine.endPoint) routes.push(startLine.endPoint)
//hip끼리 만나는 경우는 아무것도 안해도됨 //hip끼리 만나는 경우는 아무것도 안해도됨
let count = 0
if (!isSamePoint(startLine.endPoint, arrivalPoint)) { if (!isSamePoint(startLine.endPoint, arrivalPoint)) {
// polygon line까지 추가 // polygon line까지 추가
const allLinesCopy = [...allLines, ...polygon.lines] const allLinesCopy = [...allLines, ...polygon.lines]
// hip이 만나지 않는 경우 갈 수 있는 길을 다 돌아야함 // hip이 만나지 않는 경우 갈 수 있는 길을 다 돌아야함
let currentPoint = startLine.endPoint let currentPoint = startLine.endPoint
let currentLine = startLine let currentLine = startLine
let movedLines = []
while (!isSamePoint(currentPoint, arrivalPoint) && count <= polygon.points.length) { let subMovedLines = []
count++ while (!isSamePoint(currentPoint, arrivalPoint)) {
// startHip에서 만나는 출발선 두개. 두개의 선을 출발하여 arrivalPoint에 도착할 때 까지 count를 세고, 더 낮은 count를 가진 길을 선택한다. // startHip에서 만나는 출발선 두개. 두개의 선을 출발하여 arrivalPoint에 도착할 때 까지 count를 세고, 더 낮은 count를 가진 길을 선택한다.
let connectedLines = allLinesCopy.filter((line) => isSamePoint(line.startPoint, currentPoint) || isSamePoint(line.endPoint, currentPoint)) let connectedLines = allLinesCopy.filter((line) => isSamePoint(line.startPoint, currentPoint) || isSamePoint(line.endPoint, currentPoint))
connectedLines = connectedLines.filter((line) => line !== currentLine) connectedLines = connectedLines.filter((line) => line !== currentLine)
connectedLines = connectedLines.filter((line) => !subMovedLines.includes(line))
//마지막 선이 endLine의 startPoint와 같은경우 그 전까지 movedLine을 제거한다.
const endLineMeetLineCnt = connectedLines.filter((line) => {
return isSamePoint(line.endPoint, endLine.startPoint) || isSamePoint(line.startPoint, endLine.startPoint)
}).length
if (endLineMeetLineCnt !== 0) {
movedLines.push(subMovedLines)
console.log(movedLines, index)
}
connectedLines = connectedLines.filter((line) => {
return !isSamePoint(line.endPoint, endLine.startPoint) && !isSamePoint(line.startPoint, endLine.startPoint)
})
if (connectedLines.length === 0) { if (connectedLines.length === 0) {
return return
} }
@ -1050,14 +1105,15 @@ export const splitPolygonWithLines = (polygon) => {
currentPoint = tempPoints[minIndex].point currentPoint = tempPoints[minIndex].point
currentLine = tempPoints[minIndex].line currentLine = tempPoints[minIndex].line
if (currentLine !== startLine) {
subMovedLines.push(currentLine)
}
routes.push(currentPoint) routes.push(currentPoint)
} }
} }
if (count <= polygon.points.length - 1) {
routes.push(endLine.startPoint) routes.push(endLine.startPoint)
roofs.push(routes) roofs.push(routes)
}
}) })
// 중복 제거 // 중복 제거
@ -1078,6 +1134,7 @@ export const splitPolygonWithLines = (polygon) => {
fill: 'transparent', fill: 'transparent',
strokeWidth: 3, strokeWidth: 3,
name: 'roof', name: 'roof',
selectable: false,
}) })
polygon.canvas.add(roof) polygon.canvas.add(roof)
@ -2737,3 +2794,35 @@ function arraysHaveSamePoints(array1, array2) {
return true return true
} }
export const toGeoJSON = (pointsArray) => {
// 객체 배열을 GeoJSON 형식의 좌표 배열로 변환
const coordinates = pointsArray.map((point) => [point.x, point.y])
// 닫힌 다각형을 만들기 위해 첫 번째 점을 마지막에 추가
coordinates.push([pointsArray[0].x, pointsArray[0].y])
return coordinates
}
export const inPolygon = (polygonPoints, rectPoints) => {
const polygonCoordinates = toGeoJSON(polygonPoints)
const rectCoordinates = toGeoJSON(rectPoints)
const polygonFeature = turf.polygon([polygonCoordinates])
const rectFeature = turf.polygon([rectCoordinates])
// 사각형의 모든 꼭짓점이 다각형 내부에 있는지 확인
const allPointsInsidePolygon = rectCoordinates.every((coord) => {
const point = turf.point(coord)
return turf.booleanPointInPolygon(point, polygonFeature)
})
// 다각형의 모든 점이 사각형 내부에 있지 않은지 확인
const noPolygonPointsInsideRect = polygonCoordinates.every((coord) => {
const point = turf.point(coord)
return !turf.booleanPointInPolygon(point, rectFeature)
})
return allPointsInsidePolygon && noPolygonPointsInsideRect
}

3588
yarn.lock

File diff suppressed because it is too large Load Diff