Merge branch 'dev' into feature/test-jy

This commit is contained in:
Jaeyoung Lee 2024-09-02 14:42:19 +09:00
commit 33a80ede97
59 changed files with 8799 additions and 2599 deletions

View File

@ -1,6 +1,7 @@
NEXT_PUBLIC_TEST="테스트변수입니다. development" NEXT_PUBLIC_TEST="테스트변수입니다. development"
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080" 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" DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true"

View File

@ -1,7 +1,9 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
swcMinify: true,
webpack: (config) => { webpack: (config) => {
config.resolve.fallback = { fs: false }
config.externals.push({ config.externals.push({
'utf-8-validate': 'commonjs utf-8-validate', 'utf-8-validate': 'commonjs utf-8-validate',
bufferutil: 'commonjs bufferutil', bufferutil: 'commonjs bufferutil',

3582
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,24 +15,28 @@
"axios": "^1.7.3", "axios": "^1.7.3",
"fabric": "^5.3.0", "fabric": "^5.3.0",
"framer-motion": "^11.2.13", "framer-motion": "^11.2.13",
"fs": "^0.0.1-security",
"iron-session": "^8.0.2", "iron-session": "^8.0.2",
"mathjs": "^13.0.2", "mathjs": "^13.0.2",
"mssql": "^11.0.1", "mssql": "^11.0.1",
"next": "14.2.3", "next": "14.2.3",
"next-international": "^1.2.4", "next-international": "^1.2.4",
"react": "^18", "react": "^18",
"react-colorful": "^5.6.1",
"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", "react-toastify": "^10.0.5",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"uuid": "^9.0.1" "uuid": "^10.0.0"
}, },
"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",
"react-color-palette": "^7.2.2",
"sass": "^1.77.8", "sass": "^1.77.8",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
} }

View File

@ -0,0 +1,23 @@
<?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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 179 B

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

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Archive from '@/components/community/Archive' import Archive from '@/components/community/Archive'
import { initCheck } from '@/util/session-util'
export default async function CommunityArchivePage() {
await initCheck()
export default function CommunityArchivePage() {
return ( return (
<> <>
<Hero title="자료 다운로드" /> <Hero title="자료 다운로드" />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Faq from '@/components/community/Faq' import Faq from '@/components/community/Faq'
import { initCheck } from '@/util/session-util'
export default async function CommunityFaqPage() {
await initCheck()
export default function CommunityFaqPage() {
return ( return (
<> <>
<Hero title="FAQ" /> <Hero title="FAQ" />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Notice from '@/components/community/Notice' import Notice from '@/components/community/Notice'
import { initCheck } from '@/util/session-util'
export default async function CommunityNoticePage() {
await initCheck()
export default function CommunityNoticePage() {
return ( return (
<> <>
<Hero title="공지사항" /> <Hero title="공지사항" />

View File

@ -1,6 +1,9 @@
import Intro from '@/components/Intro' import Intro from '@/components/Intro'
import { initCheck } from '@/util/session-util'
export default async function IntroPage() {
await initCheck()
export default function IntroPage() {
return ( return (
<> <>
<div className="container mx-auto p-4 m-4 border"> <div className="container mx-auto p-4 m-4 border">

View File

@ -2,14 +2,19 @@
import { useCurrentLocale } from '@/locales/client' import { useCurrentLocale } from '@/locales/client'
import { LocaleProvider } from './LocaleProvider' import { LocaleProvider } from './LocaleProvider'
import ServerError from './error'
import { ErrorBoundary } from 'next/dist/client/components/error-boundary'
export default function LocaleLayout({ children }) { export default function LocaleLayout({ children }) {
const locale = useCurrentLocale() const locale = useCurrentLocale()
return ( return (
<> <>
<LocaleProvider locale={locale} fallback={''}> <ErrorBoundary fallback={<ServerError />}>
<LocaleProvider locale={locale} fallback={<ServerError />}>
{children} {children}
</LocaleProvider> </LocaleProvider>
</ErrorBoundary>
</> </>
) )
} }

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Plan from '@/components/management/Plan' import Plan from '@/components/management/Plan'
import { initCheck } from '@/util/session-util'
export default async function ManagementPlanPage() {
await initCheck()
export default function ManagementPlanPage() {
return ( return (
<> <>
<Hero title="도면관리" /> <Hero title="도면관리" />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Stuff from '@/components/management/Stuff' import Stuff from '@/components/management/Stuff'
import { initCheck } from '@/util/session-util'
export default async function ManagementStuffPage() {
await initCheck()
export default function ManagementStuffPage() {
return ( return (
<> <>
<Hero title="물건관리" /> <Hero title="물건관리" />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Company from '@/components/master/Company' import Company from '@/components/master/Company'
import { initCheck } from '@/util/session-util'
export default async function MasterCompanyPage() {
await initCheck()
export default function MasterCompanyPage() {
return ( return (
<> <>
<Hero title="회사정보 조회" /> <Hero title="회사정보 조회" />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Price from '@/components/master/Price' import Price from '@/components/master/Price'
import { initCheck } from '@/util/session-util'
export default async function MasterPricePage() {
await initCheck()
export default function MasterPricePage() {
return ( return (
<> <>
<Hero title="가격 마스터 조회" /> <Hero title="가격 마스터 조회" />

View File

@ -1,6 +1,6 @@
import MainPage from '@/components/Main'
import { getSession } from '@/lib/authActions' import { getSession } from '@/lib/authActions'
import { getCurrentLocale } from '@/locales/server' import { getCurrentLocale } from '@/locales/server'
import MainPage from '@/components/Main'
export default async function page() { export default async function page() {
const session = await getSession() const session = await getSession()

View File

@ -1,6 +1,14 @@
import Playground from '@/components/Playground' import Playground from '@/components/Playground'
import { initCheck } from '@/util/session-util'
export default async function PlaygroundPage() {
// const { session } = await checkSession()
// if (!session.isLoggedIn) {
// redirect('/login')
// }
await initCheck()
export default function PlaygroundPage() {
return ( return (
<> <>
<Playground /> <Playground />

View File

@ -1,7 +1,10 @@
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Roof from '@/components/Roof' import Roof from '@/components/Roof'
import { initCheck } from '@/util/session-util'
export default async function RoofPage() {
await initCheck()
export default function RoofPage() {
return ( return (
<> <>
<Hero title="Drawing on canvas 2D Roof" /> <Hero title="Drawing on canvas 2D Roof" />

View File

@ -6,25 +6,94 @@ import { useAxios } from '@/hooks/useAxios'
export default function RoofSelect() { export default function RoofSelect() {
const [roofMaterials, setRoofMaterials] = useState([]) const [roofMaterials, setRoofMaterials] = useState([])
const [manufacturers, setManufacturers] = useState([])
const [trestles, setTrestles] = useState([]) const [trestles, setTrestles] = useState([])
const [modules, setModules] = useState([])
const [originTrestles, setOriginTrestles] = useState([])
const [roofMaterialId, setRoofMaterialId] = useState(null)
const [manufacturerId, setManufacturerId] = useState(null)
const [trestleId, setTrestleId] = useState(null)
const { get } = useAxios() const { get } = useAxios()
const handleRoofMaterialOnChange = (e) => {
const id = e.target.value
setTrestles([])
get({ url: `/api/trestle/v1.0/trestles/${id}` }).then((res) => {
setTrestles(res)
})
}
useEffect(() => { useEffect(() => {
get({ url: '/api/roof-material/v1.0/roof-materials' }).then((res) => { get({ url: '/api/roof-material/roof-material-infos' }).then((res) => {
//TODO: error handling
if (!res) return
setRoofMaterials(res) setRoofMaterials(res)
}) })
}, []) }, [])
useEffect(() => {
if (!roofMaterialId) {
return
}
get({ url: `/api/roof-material/roof-material-infos/${roofMaterialId}/trestles` }).then((res) => {
if (res.length === 0) {
return
}
setOriginTrestles(res)
const manufactural = res.map((trestle) => {
return { id: trestle.manufacturerId, name: trestle.manufacturerName }
})
// Remove duplicates
const uniqueManufactural = Array.from(new Set(manufactural.map((a) => a.id))).map((id) => {
return manufactural.find((a) => a.id === id)
})
setManufacturers(uniqueManufactural)
})
}, [roofMaterialId])
useEffect(() => {
if (!manufacturerId) {
return
}
const trestles = originTrestles.filter((trestle) => trestle.manufacturerId === manufacturerId)
setTrestles(trestles)
}, [manufacturerId])
useEffect(() => {
if (!trestleId) {
return
}
get({ url: `/api/module/module-infos?roofMaterialId=${roofMaterialId}&trestleId=${trestleId}` }).then((res) => {
if (res.length === 0) {
return
}
setModules(res)
})
}, [trestleId])
const handleRoofMaterialOnChange = (e) => {
const roofMaterialId = e.target.value
setRoofMaterialId(roofMaterialId)
setManufacturers([])
setManufacturerId(null)
setTrestleId(null)
setTrestles([])
setModules([])
}
const handleManufacturersOnChange = (e) => {
const manufacturerId = Number(e.target.value)
setTrestles([])
setManufacturerId(manufacturerId)
setTrestleId(null)
setModules([])
}
const handleTrestlesOnChange = (e) => {
const trestleId = Number(e.target.value)
setTrestleId(trestleId)
setModules([])
}
return ( return (
<>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
{roofMaterials.length > 0 && ( {roofMaterials.length > 0 && (
<Select label="지붕재" className="max-w-xs" onChange={handleRoofMaterialOnChange}> <Select label="지붕재" className="max-w-xs" onChange={handleRoofMaterialOnChange}>
@ -33,15 +102,27 @@ export default function RoofSelect() {
))} ))}
</Select> </Select>
)} )}
{manufacturers.length > 0 && (
<Select label="제조 회사" className="max-w-xs" onChange={handleManufacturersOnChange}>
{manufacturers.map((manufacturer) => (
<SelectItem key={manufacturer.id}>{manufacturer.name}</SelectItem>
))}
</Select>
)}
{trestles.length > 0 && ( {trestles.length > 0 && (
<Select label="가대" className="max-w-xs"> <Select label="가대" className="max-w-xs" onChange={handleTrestlesOnChange}>
{trestles.map((trestle) => ( {trestles.map((trestle) => (
<SelectItem key={trestle.id}>{trestle.name}</SelectItem> <SelectItem key={trestle.id}>{trestle.name}</SelectItem>
))} ))}
</Select> </Select>
)} )}
{modules.length > 0 && (
<Select label="설치가능 모듈" className="max-w-xs">
{modules.map((module) => (
<SelectItem key={module.id}>{module.name}</SelectItem>
))}
</Select>
)}
</div> </div>
</>
) )
} }

View File

@ -1,17 +1,15 @@
'use client'
import Roof2 from '@/components/Roof2' import Roof2 from '@/components/Roof2'
import { textState } from '@/store/canvasAtom'
import { useEffect } from 'react'
import { useRecoilState } from 'recoil'
import RoofSelect from '@/app/[locale]/roof2/RoofSelect' import RoofSelect from '@/app/[locale]/roof2/RoofSelect'
import { initCheck } from '@/util/session-util'
export default function Roof2Page() { export default async function Roof2Page() {
const [text, setText] = useRecoilState(textState) const session = await initCheck()
const roof2Props = {
useEffect(() => { name: session.name || '',
console.log(text) userId: session.userId || '',
}, []) email: session.email || '',
isLoggedIn: session.isLoggedIn,
}
return ( return (
<> <>
@ -21,7 +19,7 @@ export default function Roof2Page() {
</div> </div>
</div> </div>
<div className="flex flex-col justify-center my-8 pt-20"> <div className="flex flex-col justify-center my-8 pt-20">
<Roof2 /> <Roof2 {...roof2Props} />
</div> </div>
</> </>
) )

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>
</>
)
}

21
src/common/common.js Normal file
View File

@ -0,0 +1,21 @@
export const Mode = {
DRAW_LINE: 'drawLine', // 기준선 긋기모드`
EDIT: 'edit',
TEMPLATE: 'template',
PATTERNA: 'patterna',
PATTERNB: 'patternb',
TEXTBOX: 'textbox',
DRAW_RECT: 'drawRect',
ROOF_PATTERN: 'roofPattern', //지붕패턴 모드
ROOF_TRESTLE: 'roofTrestle', //지붕가대 모드
FILL_CELLS: 'fillCells', //태양광셀 모드
CELL_POWERCON: 'cellPowercon', //파워콘
DRAW_HELP_LINE: 'drawHelpLine', // 보조선 그리기 모드 지붕 존재해야함
ADSORPTION_POINT: 'adsorptionPoint', //흡착점 모드
DEFAULT: 'default',
}
export const LineType = {
EAVES: 'eaves', // 처마
RIDGE: 'ridge', // 용마루....
}

View File

@ -0,0 +1,272 @@
import { useEffect, useRef, useState } from 'react'
import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input } from '@nextui-org/react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom'
import { guideLineState, horiGuideLinesState, vertGuideLinesState } from '@/store/canvasAtom'
import { fabric } from 'fabric'
import { ColorPicker, useColor } from 'react-color-palette'
import 'react-color-palette/css'
export default function GridSettingsModal(props) {
const { canvasProps } = props
const [isCustomGridSetting, setIsCustomGridSetting] = useState(true)
const [gridCheckedValue, setGridCheckValue] = useState([])
const [ratioValue, setRatioValue] = useState('1')
const moduleLength = useRef(null) // mm
const customModuleHoriLength = useRef(null)
const customModuleVertLength = useRef(null)
const [open, setOpen] = useRecoilState(modalState)
const [guideLine, setGuideLine] = useRecoilState(guideLineState)
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
const gridSettingArray = []
const [guideColor, setGuideColor] = useColor('rgb(200, 15, 15)')
const [colorPickerShow, setColorPickerShow] = useState(false)
const boxStyle = {
width: '50px',
height: '30px',
border: '1px solid black',
backgroundColor: guideColor.hex,
}
useEffect(() => {
moduleLength.current.value = 90
customModuleHoriLength.current.value = 90
customModuleVertLength.current.value = 90
}, [])
useEffect(() => {
setIsCustomGridSetting(ratioValue !== 'custom')
}, [ratioValue])
const drawGridSettings = () => {
//
if (!(Object.keys(guideLine).length === 0 && guideLine.constructor === Object)) {
gridSettingArray.push(...guideLine)
}
let moduleHoriLength = moduleLength.current.value //
let moduleVertLength = moduleLength.current.value //
if (ratioValue === 'custom') {
moduleHoriLength = customModuleHoriLength.current.value
moduleVertLength = customModuleVertLength.current.value
} else {
moduleHoriLength = moduleHoriLength / ratioValue
moduleVertLength = moduleVertLength / ratioValue
}
if (gridCheckedValue.includes('line')) {
const horizontalLineArray = []
const verticalLineArray = []
for (let i = 0; i < canvasProps.height / moduleVertLength + 1; i++) {
const horizontalLine = new fabric.Line(
[0, i * moduleVertLength - moduleVertLength / 2, canvasProps.width, i * moduleVertLength - moduleVertLength / 2],
{
stroke: guideColor.hex,
strokeWidth: 1,
selectable: true,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
name: 'guideLine',
strokeDashArray: [5, 2],
opacity: 0.3,
direction: 'horizontal',
},
)
canvasProps.add(horizontalLine)
horizontalLineArray.push(horizontalLine)
}
for (let i = 0; i < canvasProps.width / moduleHoriLength + 1; i++) {
const verticalLine = new fabric.Line(
[i * moduleHoriLength - moduleHoriLength / 2, 0, i * moduleHoriLength - moduleHoriLength / 2, canvasProps.height],
{
stroke: guideColor.hex,
strokeWidth: 1,
selectable: true,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
name: 'guideLine',
strokeDashArray: [5, 2],
opacity: 0.3,
direction: 'vertical',
},
)
canvasProps.add(verticalLine)
verticalLineArray.push(verticalLine)
}
canvasProps.renderAll()
const snapDistance = 10
const recoilObj = {
guideMode: 'guideLine',
horizontalLineArray,
verticalLineArray,
moduleVertLength: moduleVertLength,
moduleHoriLength: moduleHoriLength,
}
gridSettingArray.push(recoilObj)
const newHoriGuideLines = [...horiGuideLines]
horizontalLineArray.forEach((line) => {
newHoriGuideLines.push(line)
})
const newVertGuideLines = [...vertGuideLines]
verticalLineArray.forEach((line) => {
newVertGuideLines.push(line)
})
setHoriGuideLines(newHoriGuideLines)
setVertGuideLines(newVertGuideLines)
}
if (gridCheckedValue.includes('dot')) {
const circle = new fabric.Circle({
radius: 2,
fill: 'white',
stroke: guideColor.hex,
strokeWidth: 0.7,
originX: 'center',
originY: 'center',
selectable: false,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
})
const patternSourceCanvas = new fabric.StaticCanvas(null, {
width: moduleHoriLength,
height: moduleVertLength,
})
patternSourceCanvas.add(circle)
circle.set({
left: patternSourceCanvas.width / 2,
top: patternSourceCanvas.height / 2,
})
patternSourceCanvas.renderAll()
const pattern = new fabric.Pattern({
source: patternSourceCanvas.getElement(),
repeat: 'repeat',
})
const backgroundPolygon = new fabric.Polygon(
[
{ x: 0, y: 0 },
{ x: canvasProps.width, y: 0 },
{ x: canvasProps.width, y: canvasProps.height },
{ x: 0, y: canvasProps.height },
],
{
fill: pattern,
selectable: false,
name: 'guideDot',
},
)
canvasProps.add(backgroundPolygon)
backgroundPolygon.sendToBack()
canvasProps.renderAll()
const recoilObj = {
guideMode: 'guideDot',
moduleVertLength: moduleVertLength,
moduleHoriLength: moduleHoriLength,
}
gridSettingArray.push(recoilObj)
}
canvasProps.renderAll()
setGuideLine(gridSettingArray)
}
const removeGuideLines = () => {
if (!(Object.keys(guideLine).length === 0 && guideLine.constructor === Object)) {
const guideLines = canvasProps._objects.filter((obj) => obj.name === 'guideLine' || obj.name === 'guideDot')
guideLines?.forEach((item) => canvasProps.remove(item))
canvasProps.renderAll()
setGuideLine([])
setHoriGuideLines([])
setVertGuideLines([])
} else {
alert('그리드가 없습니다.')
return
}
}
return (
<>
<div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<label style={{ display: 'block', marginBottom: '5px' }}>
<CheckboxGroup label="그리드 설정" value={gridCheckedValue} defaultChecked={gridCheckedValue} onValueChange={setGridCheckValue}>
<Checkbox value="dot"> 그리드</Checkbox>
<Checkbox value="line">점선 그리드</Checkbox>
</CheckboxGroup>
</label>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="모듈" ref={moduleLength} />
mm
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<RadioGroup label="비율 설정" value={ratioValue} defaultValue={ratioValue} onValueChange={setRatioValue}>
<Radio value="1">원치수</Radio>
<Radio value="2">1/2</Radio>
<Radio value="4">1/4</Radio>
<Radio value="10">1/10</Radio>
<Radio value="custom">임의간격</Radio>
</RadioGroup>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
가이드컬러 <div style={boxStyle} onClick={() => setColorPickerShow(!colorPickerShow)}></div>
</div>
{colorPickerShow && (
<ColorPicker color={guideColor} onChange={setGuideColor} hideInput={['hsv', 'rgb', 'hex']} height={100} hideAlpha={true} />
)}
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Checkbox value="linked" isDisabled={isCustomGridSetting}>
종횡연동
</Checkbox>
</div>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="가로간격" ref={customModuleHoriLength} min={0} isDisabled={isCustomGridSetting} />
mm
</div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="세로간격" ref={customModuleVertLength} min={0} isDisabled={isCustomGridSetting} />
mm
</div>
<div className="flex gap-4 items-center">
<Button size="sm">초기화</Button>
<Button size="sm" color="secondary" onClick={drawGridSettings} isDisabled={gridCheckedValue.length === 0}>
저장
</Button>
<Button size="sm" onClick={() => setOpen(!open)}>
취소
</Button>
<Button size="sm" onClick={() => removeGuideLines()}>
그리드 삭제
</Button>
</div>
</>
)
}

View File

@ -10,6 +10,7 @@ export default function Headers() {
<div className="space-x-4 text-xl"> <div className="space-x-4 text-xl">
<Link href="/intro">Intro</Link> <Link href="/intro">Intro</Link>
<Link href="/playground">Playground</Link> <Link href="/playground">Playground</Link>
<Link href="/settings">Canvas Settings</Link>
<Link href="/roof">Roof</Link> <Link href="/roof">Roof</Link>
<Link href="/roof2">Roof2</Link> <Link href="/roof2">Roof2</Link>
</div> </div>

View File

@ -0,0 +1,189 @@
import { useEffect, useState, memo, useCallback } from 'react'
import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input, Select, SelectItem } from '@nextui-org/react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { modalContent, modalState } from '@/store/modalAtom'
import { canvasSettingState } from '@/store/canvasAtom'
import { useAxios } from '@/hooks/useAxios'
export default function InitSettingsModal(props) {
const [open, setOpen] = useRecoilState(modalState)
const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState)
const [roofMaterials, setRoofMaterials] = useState([])
const [basicSetting, setBasicSettings] = useState({
type: '1',
inputType: '1',
angleType: 'slope',
roofs: [],
})
const { get, post } = useAxios()
useEffect(() => {
get({ url: '/api/roof-material/roof-material-infos' }).then((res) => {
//TODO: error handling
if (!res) return
setRoofMaterials(res)
})
if (!(Object.keys(canvasSetting).length === 0 && canvasSetting.constructor === Object)) {
setBasicSettings({ ...canvasSetting })
}
}, [])
//
const handleBasicSetting = (event) => {
const newBasicSetting = { ...basicSetting, [event.target.name]: event.target.value }
setBasicSettings(newBasicSetting)
}
//
const addRoofSetting = () => {
if (basicSetting.roofs.length === 4) {
alert('지붕재는 최대 4종까지 선택할 수 있습니다.')
return
}
//
const newRoofSettings = {
id: basicSetting.roofs.length + 1,
roofId: '3',
width: '200',
height: '200',
gap: '0',
layout: 'parallel',
}
setBasicSettings((prevState) => ({
...prevState,
roofs: [...prevState.roofs, newRoofSettings],
}))
}
//
const handleRoofSettings = (id, event) => {
const roof = basicSetting.roofs.map((roof, i) => (id === roof.id ? { ...roof, [event.target.name]: event.target.value } : roof))
setBasicSettings((prevState) => ({
...prevState,
roofs: [...roof],
}))
}
const submitCanvasConfig = () => {
post({ url: '/api/canvas-config', data: basicSetting }).then((res) => {
if (!res) {
setCanvasSetting({ ...basicSetting })
}
})
}
return (
<>
<div className="container mx-auto mt-10 p-6 bg-white shadow-lg rounded-lg">
<div className="text-lg font-semibold mb-4">배치면 초기설정</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="도면 작성방법" name="type" orientation="horizontal" value={basicSetting.type} onChange={handleBasicSetting}>
<Radio value="1">치수 입력에 의한 물건작성</Radio>
</RadioGroup>
</div>
</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="치수 입력방법" name="inputType" orientation="horizontal" value={basicSetting.inputType} onChange={handleBasicSetting}>
<Radio value="1">복사도 입력</Radio>
<Radio value="2">실측값 입력</Radio>
<Radio value="3">육지붕</Radio>
</RadioGroup>
</div>
</div>
<div className="mb-6">
<div className="flex space-x-4">
<RadioGroup label="지붕각도 설정" name="angleType" orientation="horizontal" value={basicSetting.angleType} onChange={handleBasicSetting}>
<Radio value="slope">경사</Radio>
<Radio value="angle">각도</Radio>
</RadioGroup>
</div>
</div>
<div className="flex items-center mb-4">
<button className="px-3 py-1 bg-blue-500 text-white rounded mr-3" onClick={addRoofSetting}>
Add
</button>
<span className="text-sm text-gray-500"> 지붕재는 최대 4종까지 선택할 있습니다.</span>
</div>
{basicSetting.roofs &&
basicSetting.roofs.map((roof, index) => {
return <RoofSelectBox roofMaterials={roofMaterials} roof={roof} key={index} onChange={handleRoofSettings} />
})}
<div className="flex gap-4 items-right">
<Button size="sm" color="secondary" onClick={submitCanvasConfig}>
저장
</Button>
<Button size="sm" onClick={() => setOpen(!open)}>
취소
</Button>
</div>
</div>
</>
)
}
const RoofSelectBox = (props) => {
return (
<div className="mb-4 flex flex-wrap items-center space-x-4" style={{ border: '1px solid black' }}>
<Select
aria-label="roofMaterial"
className={'w-52'}
name="roofId"
onChange={(e) => props.onChange(props.roof.id, e)}
items={props.roofMaterials}
defaultSelectedKeys={props.roof.roofId ? props.roof.roofId : ''}
selectedKeys={props.roof.roofId}
value={props.roof.roofId}
>
{(roofMaterial) => (
<SelectItem key={roofMaterial.id} value={roofMaterial.id}>
{roofMaterial.name}
</SelectItem>
)}
</Select>
<Input
type="text"
name="width"
placeholder="너비"
value={props.roof.width}
className="w-24"
onChange={(e) => props.onChange(props.roof.id, e)}
/>
<Input
type="text"
name="height"
placeholder="높이"
value={props.roof.height}
className="w-24"
onChange={(e) => props.onChange(props.roof.id, e)}
/>
mm
<Input type="text" name="gap" placeholder="간격" value={props.roof.gap} className="w-24" onChange={(e) => props.onChange(props.roof.id, e)} />
mm
<div className="flex space-x-4">
<RadioGroup
orientation="horizontal"
name="layout"
value={props.roof.layout}
defaultValue="parallel"
onChange={(e) => props.onChange(props.roof.id, e)}
>
<Radio value="parallel">병렬식</Radio>
<Radio value="cascade">계단식</Radio>
</RadioGroup>
</div>
</div>
)
}

View File

@ -1,8 +1,11 @@
'use client' 'use client'
import { useState } from 'react'
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 ColorPicker from './common/color-picker/ColorPicker'
import { useAxios } from '@/hooks/useAxios' import { useAxios } from '@/hooks/useAxios'
import { useMessage } from '@/hooks/useMessage'
// import { get } from '@/lib/Axios' // import { get } from '@/lib/Axios'
import QSelect from '@/components/ui/QSelect' import QSelect from '@/components/ui/QSelect'
@ -12,6 +15,9 @@ import styles from './playground.module.css'
export default function Playground() { export default function Playground() {
const { get } = useAxios() const { get } = useAxios()
const testVar = process.env.NEXT_PUBLIC_TEST const testVar = process.env.NEXT_PUBLIC_TEST
const { getMessage } = useMessage()
const [color, setColor] = useState('#ff0000')
const handleUsers = async () => { const handleUsers = async () => {
// const users = await get('/api/user/find-all') // const users = await get('/api/user/find-all')
@ -58,6 +64,11 @@ export default function Playground() {
<div className="test"> <div className="test">
<p className="text-white">Sass 테스트입니다.</p> <p className="text-white">Sass 테스트입니다.</p>
</div> </div>
<div>{getMessage('hi')}</div>
<div>
<h1>React ColorPicker</h1>
<ColorPicker color={color} setColor={setColor} />
</div>
</div> </div>
</> </>
) )

View File

@ -1,8 +1,10 @@
'use client'
import { useEffect } from 'react'
import { addDistanceTextToPolygon, getDistance } from '@/util/canvas-util' import { addDistanceTextToPolygon, getDistance } from '@/util/canvas-util'
import { useCanvas } from '@/hooks/useCanvas' import { useCanvas } from '@/hooks/useCanvas'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useEffect } from 'react'
export default function Roof() { export default function Roof() {
const { const {
@ -281,82 +283,43 @@ export default function Roof() {
return ( return (
<> <>
<div className="flex justify-center my-8 w-full"> <div className="flex justify-center my-8 w-full">
<button <button className="w-30 mx-2 p-2 rounded bg-blue-500 text-white" onClick={addRect}>
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addRect}
>
ADD RECTANGLE ADD RECTANGLE
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-blue-500 text-white" onClick={addHorizontalLine}>
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addHorizontalLine}
>
ADD HORIZONTAL LINE ADD HORIZONTAL LINE
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-blue-500 text-white" onClick={addVerticalLine}>
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addVerticalLine}
>
ADD VERTICALITY LINE ADD VERTICALITY LINE
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-blue-500 text-white" onClick={addTriangle}>
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addTriangle}
>
ADD TRIANGLE ADD TRIANGLE
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-blue-500 text-white" onClick={addTrapezoid}>
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addTrapezoid}
>
ADD TRAPEZOID ADD TRAPEZOID
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-black text-white" onClick={handleCopy}>
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handleCopy}
>
COPY shape COPY shape
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-red-500 text-white" onClick={handleDelete}>
className="w-30 mx-2 p-2 rounded bg-red-500 text-white"
onClick={handleDelete}
>
DELETE DELETE
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-red-500 text-white" onClick={handleClear}>
className="w-30 mx-2 p-2 rounded bg-red-500 text-white"
onClick={handleClear}
>
CLEAR CLEAR
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-green-500 text-white" onClick={handleUndo}>
className="w-30 mx-2 p-2 rounded bg-green-500 text-white"
onClick={handleUndo}
>
UNDO UNDO
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-green-300 text-white" onClick={handleRedo}>
className="w-30 mx-2 p-2 rounded bg-green-300 text-white"
onClick={handleRedo}
>
REDO REDO
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-black text-white" onClick={handleSave}>
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handleSave}
>
저장 저장
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-black text-white" onClick={handlePaste}>
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handlePaste}
>
붙여넣기 붙여넣기
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-black text-white" onClick={() => handleRotate()}>
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={() => handleRotate()}
>
45 회전 45 회전
</button> </button>
<button <button
@ -367,10 +330,7 @@ export default function Roof() {
> >
이미지 저장 이미지 저장
</button> </button>
<button <button className="w-30 mx-2 p-2 rounded bg-black text-white" onClick={handleFlip}>
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handleFlip}
>
도형반전 도형반전
</button> </button>
</div> </div>

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

@ -12,7 +12,7 @@ export default function Login() {
</div> </div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form action={login} method="POST" className="space-y-6"> <form action={login} method="POST" encType="application/json" className="space-y-6">
<div> <div>
<label htmlFor="userId" className="block text-sm font-medium leading-6 text-gray-900"> <label htmlFor="userId" className="block text-sm font-medium leading-6 text-gray-900">
User ID User ID

View File

@ -0,0 +1,11 @@
import { HexColorPicker } from 'react-colorful'
export default function ColorPicker(props) {
const { color, setColor } = props
return (
<>
<HexColorPicker color={color} onChange={setColor} />
</>
)
}

View File

@ -0,0 +1,68 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
export default function QContextMenu(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 })
}
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)}>
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>
)}
</>
)
}

View File

@ -0,0 +1,50 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
export default function QEmptyContextMenu(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 })
}
return <></>
}

View File

@ -0,0 +1,71 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
export default function QLineContextMenu(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 })
}
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)}>
line
</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>
)}
</>
)
}

View File

@ -0,0 +1,71 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
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 })
}
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>
)}
</>
)
}

View File

@ -7,7 +7,6 @@ import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-quartz.css' import 'ag-grid-community/styles/ag-theme-quartz.css'
export default function QGrid(props) { export default function QGrid(props) {
console.log('QGrid props:', props)
const { gridData, gridColumns, isPageable = true } = props const { gridData, gridColumns, isPageable = true } = props
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
const [clickedCount, setClickedCount] = useState(0) const [clickedCount, setClickedCount] = useState(0)

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

@ -1,6 +1,6 @@
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getDirection } from '@/util/canvas-util' import { getDirection, getDirectionByPoint } from '@/util/canvas-util'
export const QLine = fabric.util.createClass(fabric.Line, { export const QLine = fabric.util.createClass(fabric.Line, {
type: 'QLine', type: 'QLine',
@ -11,6 +11,7 @@ export const QLine = fabric.util.createClass(fabric.Line, {
direction: null, direction: null,
idx: 0, idx: 0,
area: 0, area: 0,
children: [],
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
this.callSuper('initialize', points, { ...options, selectable: options.selectable ?? false }) this.callSuper('initialize', points, { ...options, selectable: options.selectable ?? false })
if (options.id) { if (options.id) {
@ -28,7 +29,7 @@ export const QLine = fabric.util.createClass(fabric.Line, {
this.setLength() this.setLength()
this.direction = options.direction ?? getDirection({ x: this.x1, y: this.y1 }, { x: this.x2, y: this.y2 }) this.direction = options.direction ?? getDirectionByPoint({ x: this.x1, y: this.y1 }, { x: this.x2, y: this.y2 })
this.startPoint = { x: this.x1, y: this.y1 } this.startPoint = { x: this.x1, y: this.y1 }
this.endPoint = { x: this.x2, y: this.y2 } this.endPoint = { x: this.x2, y: this.y2 }
@ -91,13 +92,38 @@ export const QLine = fabric.util.createClass(fabric.Line, {
this.text = thisText this.text = thisText
return return
} }
let left, top
if (this.direction === 'left' || this.direction === 'right') {
left = (x1 + x2) / 2
top = (y1 + y2) / 2 + 10
} else if (this.direction === 'top' || this.direction === 'bottom') {
left = (x1 + x2) / 2 + 10
top = (y1 + y2) / 2
}
const minX = this.left
const maxX = this.left + this.width
const minY = this.top
const maxY = this.top + this.length
const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
const text = new fabric.Textbox(this.length.toFixed(0).toString(), { const text = new fabric.Textbox(this.length.toFixed(0).toString(), {
left: (x1 + x2) / 2, left: left,
top: (y1 + y2) / 2, top: top,
fontSize: this.fontSize, fontSize: this.fontSize,
selectable: false, minX,
maxX,
minY,
maxY,
parentDirection: this.direction,
parentDegree: degree,
parentId: this.id, parentId: this.id,
editable: false,
selectable: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
parent: this,
name: 'lengthText', name: 'lengthText',
}) })

View File

@ -17,6 +17,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
cells: [], cells: [],
parentId: null, parentId: null,
innerLines: [], innerLines: [],
children: [],
initOptions: null,
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
// 소수점 전부 제거 // 소수점 전부 제거
points.forEach((point) => { points.forEach((point) => {
@ -57,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()
@ -129,6 +133,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
}) })
this.texts = null this.texts = null
}) })
// polygon.fillCell({ width: 50, height: 30, padding: 10 })
}, },
initLines() { initLines() {
@ -156,36 +162,44 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
}, },
addLengthText() { addLengthText() {
if (this.texts.length > 0) {
this.texts.forEach((text) => {
this.canvas.remove(text)
})
}
let points = this.getCurrentPoints() let points = this.getCurrentPoints()
points.forEach((start, i) => { points.forEach((start, i) => {
const thisText = this.canvas.getObjects().find((obj) => obj.name === 'lengthText' && obj.parentId === this.id && obj.idx === i)
const end = points[(i + 1) % points.length] const end = points[(i + 1) % points.length]
const dx = end.x - start.x const dx = end.x - start.x
const dy = end.y - start.y const dy = end.y - start.y
const length = Math.sqrt(dx * dx + dy * dy) const length = Math.sqrt(dx * dx + dy * dy)
if (thisText) {
thisText.set({
left: (start.x + points[(i + 1) % points.length].x) / 2,
top: (start.y + points[(i + 1) % points.length].y) / 2,
text: length.toFixed(0),
})
return
}
const midPoint = new fabric.Point((start.x + end.x) / 2, (start.y + end.y) / 2) const midPoint = new fabric.Point((start.x + end.x) / 2, (start.y + end.y) / 2)
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,
selectable: false,
parentId: this.id, parentId: this.id,
minX: Math.min(start.x, end.x),
maxX: Math.max(start.x, end.x),
minY: Math.min(start.y, end.y),
maxY: Math.max(start.y, end.y),
parentDirection: getDirectionByPoint(start, end),
parentDegree: degree,
dirty: true,
editable: true,
selectable: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
idx: i, idx: i,
name: 'lengthText', name: 'lengthText',
parent: this,
}) })
this.texts.push(text) this.texts.push(text)
@ -262,6 +276,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
name: 'cell', name: 'cell',
idx: idx, idx: idx,
parentId: this.id, parentId: this.id,
parent: this,
}) })
idx++ idx++
@ -274,7 +289,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
this.cells = drawCellsArray this.cells = drawCellsArray
return drawCellsArray return drawCellsArray
}, },
fillCellABType(cell = { width: 50, height: 100, padding: 5, wallDirection: 'left', referenceDirection: 'none', startIndex: -1 }) { fillCellABType(
cell = { width: 50, height: 100, padding: 5, wallDirection: 'left', referenceDirection: 'none', startIndex: -1, isCellCenter: false },
) {
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)) //왼쪽
@ -451,9 +468,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
} else { } else {
// centerWidth = 0 //나중에 중간 정렬 이면 어쩌구 함수 만들어서 넣음 // centerWidth = 0 //나중에 중간 정렬 이면 어쩌구 함수 만들어서 넣음
if (['left', 'right'].includes(cell.wallDirection)) { if (['left', 'right'].includes(cell.wallDirection)) {
centerWidth = 0 centerWidth = cell.isCellCenter ? centerWidth : 0
} else if (['top', 'bottom'].includes(cell.wallDirection)) { } else if (['top', 'bottom'].includes(cell.wallDirection)) {
centerHeight = 0 centerHeight = cell.isCellCenter ? centerHeight : 0
} }
if (cell.wallDirection === 'left') { if (cell.wallDirection === 'left') {
@ -528,7 +545,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
type: 'cellRect', type: 'cellRect',
}) })
var group = new fabric.Group([rect, text], { const group = new fabric.Group([rect, text], {
left: startXPos, left: startXPos,
top: startYPos, top: startYPos,
}) })

View File

@ -1,20 +1,38 @@
'use client'
import { memo } from 'react'
import { Card, Image } from '@nextui-org/react' import { Card, Image } from '@nextui-org/react'
export default function ThumbnailList(props) { function ThumbnailList(props) {
const { thumbnails } = props const { thumbnails, canvas } = props
console.log(props)
console.log(thumbnails) const handleSelectThumb = (canvasStatus) => {
console.log('canvasStatus', canvasStatus.length)
canvas?.clear() // .
canvas?.loadFromJSON(JSON.parse(canvasStatus), function () {
canvas?.renderAll() // .
})
}
return ( return (
<> <>
<div className="flex justify-center m-4 w-full"> <div className="flex justify-center m-4 w-full">
{thumbnails.length > 0 && {thumbnails.length > 0 &&
thumbnails.map((thumbnail, index) => ( thumbnails.map((thumbnail, index) => (
<Card isFooterBlurred radius="lg" className="border-none m-2"> <Card isFooterBlurred radius="lg" key={index} className="border-none m-2">
<Image alt="Woman listing to music" className="object-cover" height={200} src="https://nextui.org/images/hero-card.jpeg" width={200} /> <Image
alt="Woman listing to music"
className="object-cover"
height={200}
src={thumbnail.imageName}
width={200}
onClick={() => handleSelectThumb(thumbnail.canvasStatus)}
/>
</Card> </Card>
))} ))}
</div> </div>
</> </>
) )
} }
export default memo(ThumbnailList)

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { actionHandler, anchorWrapper, calculateIntersection, distanceBetweenPoints, polygonPositionHandler } from '@/util/canvas-util'
import { actionHandler, anchorWrapper, polygonPositionHandler } from '@/util/canvas-util'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { canvasSizeState, fontSizeState } from '@/store/canvasAtom' import { canvasSizeState, fontSizeState } from '@/store/canvasAtom'
@ -8,8 +9,9 @@ import { QLine } from '@/components/fabric/QLine'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import { defineQLine } from '@/util/qline-utils' import { defineQLine } from '@/util/qline-utils'
import { defineQPloygon } from '@/util/qpolygon-utils' import { defineQPloygon } from '@/util/qpolygon-utils'
import { writeImage } from '@/lib/canvas'
import * as turf from '@turf/turf' import { useCanvasEvent } from '@/hooks/useCanvasEvent'
import { post } from '@/lib/Axios'
export function useCanvas(id) { export function useCanvas(id) {
const [canvas, setCanvas] = useState() const [canvas, setCanvas] = useState()
@ -17,7 +19,7 @@ export function useCanvas(id) {
const [history, setHistory] = useState([]) const [history, setHistory] = useState([])
const [canvasSize] = useRecoilState(canvasSizeState) const [canvasSize] = useRecoilState(canvasSizeState)
const [fontSize] = useRecoilState(fontSizeState) const [fontSize] = useRecoilState(fontSizeState)
const points = useRef([]) const { setCanvasForEvent, attachDefaultEventOnCanvas } = useCanvasEvent()
/** /**
* 처음 셋팅 * 처음 셋팅
@ -31,6 +33,8 @@ export function useCanvas(id) {
}) })
setCanvas(c) setCanvas(c)
setCanvasForEvent(c)
return () => { return () => {
c.dispose() c.dispose()
} }
@ -38,8 +42,7 @@ export function useCanvas(id) {
useEffect(() => { useEffect(() => {
// canvas 사이즈가 변경되면 다시 // canvas 사이즈가 변경되면 다시
removeEventOnCanvas() attachDefaultEventOnCanvas()
addEventOnCanvas()
}, [canvasSize]) }, [canvasSize])
useEffect(() => { useEffect(() => {
@ -65,71 +68,9 @@ export function useCanvas(id) {
useEffect(() => { useEffect(() => {
if (canvas) { if (canvas) {
initialize() initialize()
canvas?.on('object:added', onChange) attachDefaultEventOnCanvas()
canvas?.on('object:added', addEventOnObject)
canvas?.on('object:modified', onChange)
canvas?.on('object:removed', onChange)
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:out', removeMouseLines)
} }
}, [canvas]) }, [canvas])
const addEventOnCanvas = () => {
canvas?.on('object:added', onChange)
canvas?.on('object:modified', onChange)
canvas?.on('object:removed', onChange)
canvas?.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown)
})
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:down', handleMouseDown)
canvas?.on('mouse:out', removeMouseLines)
}
const removeEventOnCanvas = () => {
canvas?.off('object:added')
canvas?.off('object:modified')
canvas?.off('object:removed')
canvas?.off('object:added')
canvas?.off('mouse:move')
canvas?.off('mouse:down')
}
const addEventOnObject = (e) => {
const target = e.target
if (target.name === 'cell') {
target.on('mousedown', () => {
if (target.get('selected')) {
target.set({ selected: false })
target.set({ fill: '#BFFD9F' })
} else {
target.set({ selected: true })
target.set({ fill: 'red' })
}
canvas?.renderAll()
})
}
if (target.name === 'trestle') {
target.on('mousedown', () => {
if (target.defense === 'north') {
alert('북쪽은 선택 불가합니다.')
return
}
if (target.get('selected')) {
target.set({ strokeWidth: 1 })
target.set({ strokeDashArray: [5, 5] })
target.set({ selected: false })
} else {
target.set({ strokeWidth: 5 })
target.set({ strokeDashArray: [0, 0] })
target.set({ selected: true })
}
canvas?.renderAll()
})
}
}
/** /**
* 마우스 포인터의 가이드라인을 제거합니다. * 마우스 포인터의 가이드라인을 제거합니다.
@ -179,71 +120,6 @@ export function useCanvas(id) {
setIsLocked(false) setIsLocked(false)
} }
const drawMouseLines = (e) => {
// 현재 마우스 포인터의 위치를 가져옵니다.
const pointer = canvas?.getPointer(e.e)
// 기존에 그려진 가이드라인을 제거합니다.
removeMouseLines()
if (canvas?.getActiveObject()) {
return
}
// 가로선을 그립니다.
const horizontalLine = new fabric.Line([0, pointer.y, canvasSize.horizontal, pointer.y], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 세로선을 그립니다.
const verticalLine = new fabric.Line([pointer.x, 0, pointer.x, canvasSize.vertical], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 선들을 캔버스에 추가합니다.
canvas?.add(horizontalLine, verticalLine)
// 캔버스를 다시 그립니다.
canvas?.renderAll()
}
const handleMouseDown = (e) => {
// 현재 마우스 포인터의 위치를 가져옵니다.
if (canvas?.getActiveObject()) {
points.current = []
return
}
const pointer = canvas?.getPointer(e.e)
// 클릭한 위치를 배열에 추가합니다.
points.current.push(pointer)
// 두 점을 모두 찍었을 때 사각형을 그립니다.
if (points.current.length === 2) {
const rect = new fabric.Rect({
left: points.current[0].x,
top: points.current[0].y,
width: points.current[1].x - points.current[0].x,
height: points.current[1].y - points.current[0].y,
fill: 'transparent',
stroke: 'black',
strokeWidth: 1,
})
// 사각형을 캔버스에 추가합니다.
canvas?.add(rect)
// 배열을 초기화합니다.
points.current = []
}
}
/** /**
* 눈금 모양에 맞게 움직이도록 한다. * 눈금 모양에 맞게 움직이도록 한다.
*/ */
@ -302,25 +178,6 @@ export function useCanvas(id) {
}) })
} }
/**
* 선택한 도형을 삭제한다.
*/
const handleDelete = () => {
const targets = canvas?.getActiveObjects()
if (targets?.length === 0) {
alert('삭제할 대상을 선택해주세요.')
return
}
if (!confirm('정말로 삭제하시겠습니까?')) {
return
}
targets?.forEach((target) => {
canvas?.remove(target)
})
}
/** /**
* 페이지 캔버스 저장 * 페이지 캔버스 저장
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함 * todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
@ -418,50 +275,6 @@ export function useCanvas(id) {
canvas?.renderAll() canvas?.renderAll()
} }
/**
* 각종 키보드 이벤트
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
*/
const handleKeyDown = (e) => {
const key = e.key
switch (key) {
case 'Delete':
case 'Backspace':
handleDelete()
break
case 'Down': // IE/Edge에서 사용되는 값
case 'ArrowDown':
// "아래 화살표" 키가 눌렸을 때의 동작입니다.
moveDown()
break
case 'Up': // IE/Edge에서 사용되는 값
case 'ArrowUp':
// "위 화살표" 키가 눌렸을 때의 동작입니다.
moveUp()
break
case 'Left': // IE/Edge에서 사용되는 값
case 'ArrowLeft':
// "왼쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveLeft()
break
case 'Right': // IE/Edge에서 사용되는 값
case 'ArrowRight':
// "오른쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveRight()
break
case 'Enter':
// "enter" 또는 "return" 키가 눌렸을 때의 동작입니다.
break
case 'Esc': // IE/Edge에서 사용되는 값
case 'Escape':
break
default:
return // 키 이벤트를 처리하지 않는다면 종료합니다.
}
e.preventDefault()
}
const handleRotate = (degree = 45) => { const handleRotate = (degree = 45) => {
const target = canvas?.getActiveObject() const target = canvas?.getActiveObject()
@ -502,16 +315,28 @@ export function useCanvas(id) {
* 이미지로 저장하는 함수 * 이미지로 저장하는 함수
* @param {string} title - 저장할 이미지 이름 * @param {string} title - 저장할 이미지 이름
*/ */
const saveImage = (title = 'canvas') => { const saveImage = async (title = 'canvas', userId, setThumbnails) => {
const dataURL = canvas?.toDataURL('png') removeMouseLines()
await writeImage(title, canvas?.toDataURL('image/png').replace('data:image/png;base64,', ''))
.then((res) => {
console.log('success', res)
})
.catch((err) => {
console.log('err', err)
})
// 이미지 다운로드 링크 생성 const canvasStatus = addCanvas()
const link = document.createElement('a')
link.download = `${title}.png`
link.href = dataURL
// 링크 클릭하여 이미지 다운로드 const patternData = {
link.click() userId: userId,
imageName: title,
objectNo: 'test123240822001',
canvasStatus: JSON.stringify(canvasStatus).replace(/"/g, '##'),
}
await post({ url: '/api/canvas-management/canvas-statuses', data: patternData })
setThumbnails((prev) => [...prev, { imageName: `/canvasState/${title}.png`, userId, canvasStatus: JSON.stringify(canvasStatus) }])
} }
const handleFlip = () => { const handleFlip = () => {
@ -596,20 +421,27 @@ export function useCanvas(id) {
'lockScalingY', 'lockScalingY',
'opacity', 'opacity',
'cells', 'cells',
'maxX',
'maxY',
'minX',
'minY',
'x',
'y',
]) ])
const str = JSON.stringify(objs) const str = JSON.stringify(objs)
canvas?.clear() canvas?.clear()
return str
setTimeout(() => { // setTimeout(() => {
// 역직렬화하여 캔버스에 객체를 다시 추가합니다. // // 역직렬화하여 캔버스에 객체를 다시 추가합니다.
canvas?.loadFromJSON(JSON.parse(str), function () { // canvas?.loadFromJSON(JSON.parse(str), function () {
// 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다. // // 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다.
console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof')) // console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof'))
canvas?.renderAll() // 캔버스를 다시 그립니다. // canvas?.renderAll() // 캔버스를 다시 그립니다.
}) // })
}, 1000) // }, 1000)
} }
return { return {
@ -618,7 +450,6 @@ export function useCanvas(id) {
handleUndo, handleUndo,
handleRedo, handleRedo,
handleCopy, handleCopy,
handleDelete,
handleSave, handleSave,
handlePaste, handlePaste,
handleRotate, handleRotate,
@ -627,5 +458,6 @@ export function useCanvas(id) {
handleFlip, handleFlip,
setCanvasBackgroundWithDots, setCanvasBackgroundWithDots,
addCanvas, addCanvas,
removeMouseLines,
} }
} }

335
src/hooks/useCanvasEvent.js Normal file
View File

@ -0,0 +1,335 @@
import { useEffect, useState } from 'react'
import { fabric } from 'fabric'
import { useRecoilState, useRecoilValue } from 'recoil'
import { canvasSizeState, currentObjectState, modeState } from '@/store/canvasAtom'
import { QPolygon } from '@/components/fabric/QPolygon'
// 캔버스에 필요한 이벤트
export function useCanvasEvent() {
const [canvas, setCanvasForEvent] = useState(null)
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
const canvasSize = useRecoilValue(canvasSizeState)
// 기본적인 이벤트 필요시 추가
const attachDefaultEventOnCanvas = () => {
removeEventOnCanvas()
canvas?.on('object:added', objectEvent.onChange)
canvas?.on('object:added', objectEvent.addEvent)
canvas?.on('object:modified', objectEvent.onChange)
canvas?.on('object:removed', objectEvent.onChange)
canvas?.on('selection:cleared', selectionEvent.cleared)
canvas?.on('selection:created', selectionEvent.created)
canvas?.on('selection:updated', selectionEvent.updated)
/*canvas?.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown)
})*/
canvas?.on('object:removed', objectEvent.removed)
}
const objectEvent = {
onChange: (e) => {
const target = e.target
if (target) {
// settleDown(target)
}
},
addEvent: (e) => {
const target = e.target
if (target.type === 'QPolygon' || target.type === 'QLine') {
const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText')
textObjs.forEach((obj) => {
obj.bringToFront()
})
}
if (target.name === 'cell') {
target.on('mousedown', () => {
if (target.get('selected')) {
target.set({ selected: false })
target.set({ fill: '#BFFD9F' })
} else {
target.set({ selected: true })
target.set({ fill: 'red' })
}
canvas?.renderAll()
})
}
if (target.name === 'trestle') {
target.on('mousedown', () => {
if (target.defense === 'north') {
alert('북쪽은 선택 불가합니다.')
return
}
if (target.get('selected')) {
target.set({ strokeWidth: 1 })
target.set({ strokeDashArray: [5, 5] })
target.set({ selected: false })
} else {
target.set({ strokeWidth: 5 })
target.set({ strokeDashArray: [0, 0] })
target.set({ selected: true })
}
canvas?.renderAll()
})
}
if (target.name === 'lengthText') {
const x = target.left
const y = target.top
// Add a property to store the previous value
const previousValue = target.text
target.on('selected', (e) => {
Object.keys(target.controls).forEach((controlKey) => {
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) => {
if (target.parentDirection === 'left' || target.parentDirection === 'right') {
const minX = target.minX
const maxX = target.maxX
if (target.left <= minX) {
target.set({ left: minX, top: y })
} else if (target.left >= maxX) {
target.set({ left: maxX, top: y })
} else {
target.set({ top: y })
}
} else if (target.parentDirection === 'top' || target.parentDirection === 'bottom') {
const minY = target.minY
const maxY = target.maxY
if (target.top <= minY) {
target.set({ left: x, top: minY })
} else if (target.top >= maxY) {
target.set({ left: x, top: maxY })
} else {
target.set({ left: x })
}
}
canvas?.renderAll()
})
}
},
removed: (e) => {
const whiteList = ['mouseLine', 'guideLine']
if (whiteList.includes(e.target.name)) {
return
}
},
}
const selectionEvent = {
created: (e) => {
const target = e.selected[0]
setCurrentObject(target)
},
cleared: (e) => {
setCurrentObject(null)
},
updated: (e) => {
const target = e.selected[0]
setCurrentObject(target)
},
}
const removeEventOnCanvas = () => {
canvas?.off('object:added')
canvas?.off('object:modified')
canvas?.off('object:removed')
canvas?.off('selection:cleared')
canvas?.off('selection:created')
canvas?.off('selection:updated')
}
/**
* 각종 키보드 이벤트
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
*/
const handleKeyDown = (e) => {
const key = e.key
switch (key) {
case 'Delete':
case 'Backspace':
handleDelete()
break
case 'Down': // IE/Edge에서 사용되는 값
case 'ArrowDown':
// "아래 화살표" 키가 눌렸을 때의 동작입니다.
moveDown()
break
case 'Up': // IE/Edge에서 사용되는 값
case 'ArrowUp':
// "위 화살표" 키가 눌렸을 때의 동작입니다.
moveUp()
break
case 'Left': // IE/Edge에서 사용되는 값
case 'ArrowLeft':
// "왼쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveLeft()
break
case 'Right': // IE/Edge에서 사용되는 값
case 'ArrowRight':
// "오른쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveRight()
break
case 'Enter':
// "enter" 또는 "return" 키가 눌렸을 때의 동작입니다.
break
case 'Esc': // IE/Edge에서 사용되는 값
case 'Escape':
break
case 'z':
if (!e.ctrlKey) {
return
}
console.log('뒤로가기')
break
default:
return // 키 이벤트를 처리하지 않는다면 종료합니다.
}
e.preventDefault()
}
const moveDown = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let top = targetObj.top + 10
if (top > canvasSize.vertical) {
top = canvasSize.vertical
}
targetObj.set({ top: top })
targetObj.fire('modified')
canvas?.renderAll()
}
const moveUp = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let top = targetObj.top - 10
if (top < 0) {
top = 0
}
targetObj.set({ top: top })
targetObj.fire('modified')
canvas?.renderAll()
}
const moveLeft = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let left = targetObj.left - 10
if (left < 0) {
left = 0
}
targetObj.set({ left: left })
targetObj.fire('modified')
canvas?.renderAll()
}
const moveRight = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let left = targetObj.left + 10
if (left > canvasSize.horizontal) {
left = canvasSize.horizontal
}
targetObj.set({ left: left })
targetObj.fire('modified')
canvas?.renderAll()
}
/**
* 선택한 도형을 삭제한다.
*/
const handleDelete = () => {
const targets = canvas?.getActiveObjects()
if (targets?.length === 0) {
alert('삭제할 대상을 선택해주세요.')
return
}
if (!confirm('정말로 삭제하시겠습니까?')) {
return
}
targets?.forEach((target) => {
canvas?.remove(target)
})
}
return {
setCanvasForEvent,
attachDefaultEventOnCanvas,
}
}

38
src/hooks/useMessage.js Normal file
View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil'
import { globalLocaleState } from '@/store/localeAtom'
import KO from '@/locales/ko.json'
import JA from '@/locales/ja.json'
const SESSION_STORAGE_MESSAGE_KEY = 'QCAST_MESSAGE_STORAGE'
export const useMessage = () => {
const globalLocale = useRecoilValue(globalLocaleState)
const getMessage = (key, args = []) => {
if (sessionStorage.getItem(SESSION_STORAGE_MESSAGE_KEY) === null) {
if (globalLocale === 'ko') {
setSessionMessage(JSON.stringify(KO))
} else {
setSessionMessage(JSON.stringify(JA))
}
}
const sessionMessage = getSessionMessage()
const message = sessionMessage[key] || key
return args.reduce((acc, arg, i) => {
return acc.replaceAll(`{${i}}`, arg)
}, message)
}
const setSessionMessage = (sessionMessage) => {
sessionStorage.setItem(SESSION_STORAGE_MESSAGE_KEY, sessionMessage)
}
const getSessionMessage = () => {
return JSON.parse(sessionStorage.getItem(SESSION_STORAGE_MESSAGE_KEY))
}
return { getMessage }
}

File diff suppressed because it is too large Load Diff

View File

@ -24,32 +24,32 @@ axiosInstance.interceptors.request.use(undefined, (error) => {
// } // }
}) })
export const get = (url) => export const get = ({ url }) =>
axiosInstance axiosInstance
.get(url) .get(url)
.then((res) => res.data) .then((res) => res.data)
.catch(console.error) .catch(console.error)
export const post = (url, data) => export const post = ({ url, data }) =>
axiosInstance axiosInstance
.post(url, data) .post(url, data)
.then((res) => res.data) .then((res) => res.data)
.catch(console.error) .catch(console.error)
export const put = (url, data) => export const put = ({ url, data }) =>
axiosInstance axiosInstance
.put(url, data) .put(url, data)
.then((res) => res.data) .then((res) => res.data)
.catch(console.error) .catch(console.error)
export const patch = (url, data) => export const patch = ({ url, data }) =>
axiosInstance axiosInstance
.patch(url, data) .patch(url, data)
.then((res) => res.data) .then((res) => res.data)
.catch(console.error) .catch(console.error)
export const del = (url) => export const del = ({ url }) =>
axiosInstance axiosInstance
.delete(url) .delete(url)
.then((res) => res.data) .then((res) => res.data)

View File

@ -20,7 +20,8 @@ export async function getSession() {
console.log('session:', session) console.log('session:', session)
if (!session.isLoggedIn) { if (!session.isLoggedIn) {
session.isLoggedIn = defaultSession.isLoggedIn // session.isLoggedIn = defaultSession.isLoggedIn
session.isLoggedIn = false
} }
return session return session
@ -35,24 +36,21 @@ export async function login(formData) {
console.log('id:', userId) console.log('id:', userId)
console.log('password:', password) console.log('password:', password)
// const user = { // const loginUser = await getUserByIdAndPassword({ userId, password })
// id: 1, const loginUser = {
// name: 'jinsoo Kim', id: 1,
// email: 'jinsoo.kim@example.com', userId: 'test123',
// } name: 'jinsoo Kim',
const loginUser = await getUserByIdAndPassword({ userId, password }) email: 'jinsoo.kim@example.com',
console.log('loginUser:', loginUser) }
if (!loginUser) { if (!loginUser) {
throw Error('Wrong Credentials!') throw Error('Wrong Credentials!')
} }
// session.id = user.id session.name = loginUser.name
// session.email = user.email session.userId = loginUser.userId
session.userId = loginUser.USER_ID session.email = loginUser.email
session.saleStoreId = loginUser.SALE_STORE_ID
session.name = loginUser.NAME
session.mail = loginUser.MAIL
session.isLoggedIn = true session.isLoggedIn = true
console.log('session:', session) console.log('session:', session)

View File

@ -1,8 +1,10 @@
'use server' 'use server'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import fs from 'fs/promises'
const prisma = new PrismaClient() const prisma = new PrismaClient()
const imagePath = 'public/canvasState'
export const getTests = () => { export const getTests = () => {
return prisma.test.findMany() return prisma.test.findMany()
@ -36,3 +38,14 @@ export const insertCanvasState = (param) => {
data: param, data: param,
}) })
} }
export const writeImage = async (title, data) => {
// 해당 경로에 Directory 가 없다면 생성
try {
await fs.readdir(imagePath)
} catch {
await fs.mkdir(imagePath)
}
return fs.writeFile(`${imagePath}/${title}.png`, data, 'base64')
}

View File

@ -1,10 +1,4 @@
export const defaultSession = { export const defaultSession = {}
userId: null,
saleStoreId: null,
name: null,
mail: null,
isLoggedIn: false,
}
export const sessionOptions = { export const sessionOptions = {
password: process.env.SESSION_SECRET, password: process.env.SESSION_SECRET,

View File

@ -1,5 +1,7 @@
'use server' 'use server'
import { getSession } from './authActions'
const { PrismaClient } = require('@prisma/client') const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -29,3 +31,11 @@ export async function getUsers() {
}, },
}) })
} }
export async function checkSession() {
const session = await getSession()
return {
session,
}
}

3
src/locales/ja.json Normal file
View File

@ -0,0 +1,3 @@
{
"hi": "こんにちは"
}

3
src/locales/ko.json Normal file
View File

@ -0,0 +1,3 @@
{
"hi": "안녕하세요"
}

View File

@ -5,6 +5,16 @@ export const textState = atom({
default: 'test text', default: 'test text',
}) })
export const modeState = atom({
key: 'modeState',
default: 'default',
})
export const guideModeLineState = atom({
key: 'guideLineModeState',
default: false,
})
export const fontSizeState = atom({ export const fontSizeState = atom({
key: 'fontSizeState', key: 'fontSizeState',
default: 16, default: 16,
@ -73,3 +83,32 @@ export const compassState = atom({
default: undefined, default: undefined,
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
export const guideLineState = atom({
key: 'guideLine',
default: {},
dangerouslyAllowMutability: true,
})
export const currentObjectState = atom({
key: 'currentObject',
default: null,
dangerouslyAllowMutability: true,
})
export const horiGuideLinesState = atom({
key: 'horiGuideLines',
default: [],
dangerouslyAllowMutability: true,
})
export const vertGuideLinesState = atom({
key: 'vertGuideLines',
default: [],
})
export const canvasSettingState = atom({
key: 'canvasSetting',
default: {},
dangerouslyAllowMutability: true,
})

6
src/store/localeAtom.js Normal file
View File

@ -0,0 +1,6 @@
import { atom } from 'recoil'
export const globalLocaleState = atom({
key: 'globalLocaleState',
default: 'ko',
})

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

@ -675,3 +675,33 @@ export function findClosestPointWithDifferentXY(targetPoint, points) {
return closestPoint return closestPoint
} }
export const getClosestHorizontalLine = (pointer, horizontalLineArray) => {
let closestLine = null
let minDistance = Infinity
horizontalLineArray.forEach((line) => {
const distance = Math.abs(line.y1 - pointer.y) // Assuming horizontal lines have the same y1 and y2
if (distance < minDistance) {
minDistance = distance
closestLine = line
}
})
return closestLine
}
export const getClosestVerticalLine = (pointer, verticalLineArray) => {
let closestLine = null
let minDistance = Infinity
verticalLineArray.forEach((line) => {
const distance = Math.abs(line.x1 - pointer.x) // Assuming horizontal lines have the same y1 and y2
if (distance < minDistance) {
minDistance = distance
closestLine = line
}
})
return closestLine
}

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('&')
}

12
src/util/session-util.js Normal file
View File

@ -0,0 +1,12 @@
import { checkSession } from '@/lib/user'
import { redirect } from 'next/navigation'
export const initCheck = async () => {
const { session } = await checkSession()
if (!session.isLoggedIn) {
redirect('/login')
}
return session
}

2917
yarn.lock

File diff suppressed because it is too large Load Diff