Merge branch 'dev' into feature/test-jy
This commit is contained in:
commit
33a80ede97
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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
3582
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
23
public/drawTemplates/153302.svg
Normal file
23
public/drawTemplates/153302.svg
Normal 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 |
4
public/drawTemplates/shape21.svg
Normal file
4
public/drawTemplates/shape21.svg
Normal 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
10
qcast-front.iml
Normal 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>
|
||||||
@ -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="자료 다운로드" />
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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="공지사항" />
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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="도면관리" />
|
||||||
|
|||||||
@ -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="물건관리" />
|
||||||
|
|||||||
@ -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="회사정보 조회" />
|
||||||
|
|||||||
@ -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="가격 마스터 조회" />
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
16
src/app/[locale]/settings/page.jsx
Normal file
16
src/app/[locale]/settings/page.jsx
Normal 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
21
src/common/common.js
Normal 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', // 용마루....
|
||||||
|
}
|
||||||
272
src/components/GridSettingsModal.jsx
Normal file
272
src/components/GridSettingsModal.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
189
src/components/InitSettingsModal.jsx
Normal file
189
src/components/InitSettingsModal.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
197
src/components/Settings.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
11
src/components/common/color-picker/ColorPicker.jsx
Normal file
11
src/components/common/color-picker/ColorPicker.jsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
src/components/common/context-menu/QContextMenu.jsx
Normal file
68
src/components/common/context-menu/QContextMenu.jsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
50
src/components/common/context-menu/QEmptyContextMenu.jsx
Normal file
50
src/components/common/context-menu/QEmptyContextMenu.jsx
Normal 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 <></>
|
||||||
|
}
|
||||||
71
src/components/common/context-menu/QLineContextMenu.jsx
Normal file
71
src/components/common/context-menu/QLineContextMenu.jsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
71
src/components/common/context-menu/QPolygonContextMenu.jsx
Normal file
71
src/components/common/context-menu/QPolygonContextMenu.jsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
335
src/hooks/useCanvasEvent.js
Normal 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
38
src/hooks/useMessage.js
Normal 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
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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')
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
3
src/locales/ja.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"hi": "こんにちは"
|
||||||
|
}
|
||||||
3
src/locales/ko.json
Normal file
3
src/locales/ko.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"hi": "안녕하세요"
|
||||||
|
}
|
||||||
@ -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
6
src/store/localeAtom.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil'
|
||||||
|
|
||||||
|
export const globalLocaleState = atom({
|
||||||
|
key: 'globalLocaleState',
|
||||||
|
default: 'ko',
|
||||||
|
})
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function actionHandler(eventData, transform, x, y) {
|
|||||||
|
|
||||||
// define a function that can keep the polygon in the same position when we change its width/height/top/left
|
// define a function that can keep the polygon in the same position when we change its width/height/top/left
|
||||||
export function anchorWrapper(anchorIndex, fn) {
|
export function anchorWrapper(anchorIndex, fn) {
|
||||||
return function(eventData, transform, x, y) {
|
return function (eventData, transform, x, y) {
|
||||||
let fabricObject = transform.target
|
let fabricObject = transform.target
|
||||||
let originX = fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
|
let originX = fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
|
||||||
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
|
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
12
src/util/session-util.js
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user