Merge branch 'dev'
This commit is contained in:
commit
efbf268886
@ -1,3 +1,7 @@
|
||||
NEXT_PUBLIC_TEST="테스트변수입니다. development"
|
||||
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
|
||||
DATABASE_URL="sqlserver://mssql.devgrr.kr:1433;database=qcast;user=qcast;password=Qwertqaz12345;trustServerCertificate=true"
|
||||
|
||||
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
|
||||
@ -1,3 +1,7 @@
|
||||
NEXT_PUBLIC_TEST="테스트변수입니다. production"
|
||||
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080"
|
||||
|
||||
DATABASE_URL=""
|
||||
|
||||
SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y="
|
||||
4320
package-lock.json
generated
4320
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -10,22 +10,29 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"@prisma/client": "^5.17.0",
|
||||
"@prisma/client": "^5.18.0",
|
||||
"ag-grid-react": "^32.0.2",
|
||||
"axios": "^1.7.3",
|
||||
"fabric": "^5.3.0",
|
||||
"framer-motion": "^11.2.13",
|
||||
"iron-session": "^8.0.2",
|
||||
"mathjs": "^13.0.2",
|
||||
"mongodb": "^6.8.0",
|
||||
"mssql": "^11.0.1",
|
||||
"next": "14.2.3",
|
||||
"next-international": "^1.2.4",
|
||||
"react": "^18",
|
||||
"react-datepicker": "^7.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-responsive-modal": "^6.4.2",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recoil": "^0.7.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turf/turf": "^7.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^5.17.0",
|
||||
"prisma": "^5.18.0",
|
||||
"sass": "^1.77.8",
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
|
||||
@ -3,17 +3,26 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mongodb"
|
||||
provider = "sqlserver"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model test {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
content String
|
||||
}
|
||||
|
||||
model canvas {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
loginId String
|
||||
canvas Json
|
||||
model M_USER {
|
||||
USER_ID String @id(map: "M_USER_pk") @db.VarChar(50)
|
||||
SALE_STORE_ID String @db.VarChar(10)
|
||||
PASSWORD String @db.VarChar(10)
|
||||
CATEGORY String? @db.NVarChar(20)
|
||||
NAME String @db.NVarChar(20)
|
||||
NAME_KANA String @db.NVarChar(20)
|
||||
TEL String? @db.VarChar(15)
|
||||
FAX String? @db.VarChar(15)
|
||||
MAIL String? @db.NVarChar(100)
|
||||
GROUP_ID String @db.VarChar(5)
|
||||
MODULE_SELECT_GROUP_ID String? @db.VarChar(5)
|
||||
VERSION_MANAGEMENT_ID String? @db.VarChar(20)
|
||||
DISP_COST_PRICE Boolean?
|
||||
DISP_SELLING_PRICE Boolean?
|
||||
REGIST_DATETIME DateTime? @db.DateTime
|
||||
LAST_EDIT_DATETIME DateTime? @db.DateTime
|
||||
LAST_EDIT_USER String? @db.VarChar(50)
|
||||
}
|
||||
11
src/app/[locale]/LocaleProvider.js
Normal file
11
src/app/[locale]/LocaleProvider.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { I18nProviderClient } from '@/locales/client'
|
||||
|
||||
export function LocaleProvider({ locale, children }) {
|
||||
return (
|
||||
<I18nProviderClient locale={locale} fallback={''}>
|
||||
{children}
|
||||
</I18nProviderClient>
|
||||
)
|
||||
}
|
||||
83
src/app/[locale]/changelog/page.jsx
Normal file
83
src/app/[locale]/changelog/page.jsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
|
||||
|
||||
import { AxiosType, useAxios } from '@/hooks/useAxios'
|
||||
// import { get } from '@/lib/Axios'
|
||||
|
||||
import QSelect from '@/components/ui/QSelect'
|
||||
|
||||
import styles from './changelog.module.css'
|
||||
|
||||
export default function changelogPage() {
|
||||
const { get } = useAxios()
|
||||
const testVar = process.env.NEXT_PUBLIC_TEST
|
||||
|
||||
const handleUsers = async () => {
|
||||
// const users = await get('/api/user/find-all')
|
||||
const params = {
|
||||
type: AxiosType.INTERNAL,
|
||||
url: '/api/user/find-all',
|
||||
}
|
||||
const users = await get(params)
|
||||
console.log('users', users)
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
author: 'SWYOO',
|
||||
contents: '버튼 정리(템플릿 적용)',
|
||||
date: '2024.07.16',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: 'SWYOO',
|
||||
contents: 'README.md 파일 이미지 경로 수정',
|
||||
date: '2024.07.17',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: 'SWYOO',
|
||||
contents: '',
|
||||
date: '',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto p-4 m-4 border">
|
||||
<div className={styles.test}>이 영역은 테스트입니다.</div>
|
||||
<div>
|
||||
<Table isStriped>
|
||||
<TableHeader>
|
||||
<TableColumn>DATE</TableColumn>
|
||||
<TableColumn>NAME</TableColumn>
|
||||
<TableColumn>CONTENTS</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.date}</TableCell>
|
||||
<TableCell>{item.author}</TableCell>
|
||||
<TableCell>{item.contents}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="m-2">
|
||||
<QSelect />
|
||||
</div>
|
||||
<div className="w-full bg-orange-300 m-2">{testVar}</div>
|
||||
<div>
|
||||
<div className="m-2">
|
||||
<Button onClick={handleUsers}>Button</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="test">
|
||||
<p className="text-white">Sass 테스트입니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
src/app/[locale]/error.jsx
Normal file
15
src/app/[locale]/error.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
export default function ServerError() {
|
||||
return (
|
||||
<section className="bg-white dark:bg-gray-900">
|
||||
<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div className="mx-auto max-w-screen-sm text-center">
|
||||
<h1 className="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500">500</h1>
|
||||
<p className="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">Internal Server Error.</p>
|
||||
<p className="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">We are already working to solve the problem. </p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
11
src/app/[locale]/intro/page.jsx
Normal file
11
src/app/[locale]/intro/page.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import Intro from '@/components/Intro'
|
||||
|
||||
export default function IntroPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto p-4 m-4 border">
|
||||
<Intro />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
src/app/[locale]/layout.js
Normal file
15
src/app/[locale]/layout.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useCurrentLocale } from '@/locales/client'
|
||||
import { LocaleProvider } from './LocaleProvider'
|
||||
|
||||
export default function LocaleLayout({ children }) {
|
||||
const locale = useCurrentLocale()
|
||||
return (
|
||||
<>
|
||||
<LocaleProvider locale={locale} fallback={''}>
|
||||
{children}
|
||||
</LocaleProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
9
src/app/[locale]/login/page.jsx
Normal file
9
src/app/[locale]/login/page.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
import Login from '@/components/auth/Login'
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<>
|
||||
<Login />
|
||||
</>
|
||||
)
|
||||
}
|
||||
25
src/app/[locale]/not-found.jsx
Normal file
25
src/app/[locale]/not-found.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<section className="bg-white dark:bg-gray-900">
|
||||
<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div className="mx-auto max-w-screen-sm text-center">
|
||||
<h1 className="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500">404</h1>
|
||||
<p className="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">Something's missing.</p>
|
||||
<p className="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">
|
||||
Sorry, we can't find that page. You'll find lots to explore on the home page.{' '}
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900 my-4"
|
||||
>
|
||||
Back to Homepage
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
23
src/app/[locale]/page.js
Normal file
23
src/app/[locale]/page.js
Normal file
@ -0,0 +1,23 @@
|
||||
import MainPage from '@/components/Main'
|
||||
import { getSession } from '@/lib/authActions'
|
||||
import { getCurrentLocale } from '@/locales/server'
|
||||
|
||||
export default async function page() {
|
||||
const session = await getSession()
|
||||
|
||||
const currentLocale = getCurrentLocale()
|
||||
|
||||
const mainPageProps = {
|
||||
currentLocale,
|
||||
isLoggedIn: session?.isLoggedIn,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="m-4">
|
||||
<h3>Main</h3>
|
||||
<MainPage {...mainPageProps} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react'
|
||||
import Hero from '@/components/Hero'
|
||||
import QSelect from '@/components/ui/QSelect'
|
||||
import styles from './changelog.module.css'
|
||||
import { get } from '@/lib/Axios'
|
||||
|
||||
export default function changelogPage() {
|
||||
const testVar = process.env.NEXT_PUBLIC_TEST
|
||||
|
||||
const handleUsers = async () => {
|
||||
const users = await get('/api/user/find-all')
|
||||
console.log(users)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Hero title="Change log" />
|
||||
<div className={styles.test}>이 영역은 테스트입니다.</div>
|
||||
<div>
|
||||
<Table isStriped>
|
||||
<TableHeader>
|
||||
<TableColumn>DATE</TableColumn>
|
||||
<TableColumn>NAME</TableColumn>
|
||||
<TableColumn>CONTENTS</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow key="2">
|
||||
<TableCell>2024.07.17</TableCell>
|
||||
<TableCell>SWYOO</TableCell>
|
||||
<TableCell>` * README.md 파일 이미지 경로 수정 `</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="1">
|
||||
<TableCell>2024.07.16</TableCell>
|
||||
<TableCell>SWYOO</TableCell>
|
||||
<TableCell>` * 버튼 정리(템플릿 적용) `</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="m-2">
|
||||
<QSelect />
|
||||
</div>
|
||||
<div className="w-full bg-orange-300 m-2">{testVar}</div>
|
||||
<div>
|
||||
<div className="m-2">
|
||||
<Button onClick={handleUsers}>Button</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="test">
|
||||
<p className="text-white">Sass 테스트입니다.</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Hero from '@/components/Hero'
|
||||
import Intro from '@/components/Intro'
|
||||
|
||||
export default function IntroPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero title="Drawing on canvas 2D Intro" />
|
||||
<div className="container flex flex-wrap items-center justify-between mx-auto p-4 m-4 border">
|
||||
<Intro />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,10 +1,15 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import '../styles/style.scss'
|
||||
import Headers from '@/components/Headers'
|
||||
|
||||
import RecoilRootWrapper from './RecoilWrapper'
|
||||
import UIProvider from './UIProvider'
|
||||
import { headers } from 'next/headers'
|
||||
import Headers from '@/components/Headers'
|
||||
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
import QModal from '@/components/common/modal/QModal'
|
||||
|
||||
import './globals.css'
|
||||
import '../styles/style.scss'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
@ -24,6 +29,8 @@ export default function RootLayout({ children }) {
|
||||
{headerPathname !== '/login' && <Headers />}
|
||||
<RecoilRootWrapper>
|
||||
<UIProvider>{children}</UIProvider>
|
||||
<QModal />
|
||||
<ToastContainer />
|
||||
</RecoilRootWrapper>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
export default function page() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col align-center h-screen">
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img
|
||||
alt="Your Company"
|
||||
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
|
||||
className="mx-auto h-10 w-auto"
|
||||
/>
|
||||
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form action="#" method="POST" className="space-y-6">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
autoComplete="email"
|
||||
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Not a member?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
Start a 14 day free trial
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import Hero from '@/components/Hero'
|
||||
import Main from '@/components/Main'
|
||||
|
||||
export default function Home() {
|
||||
return <Hero title="Q.CAST III - Prototype" />
|
||||
return <Main />
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
|
||||
export default function Headers() {
|
||||
return (
|
||||
<div className="w-full absolute z-10">
|
||||
<div className="w-full">
|
||||
<nav className="container relative flex flex-wrap items-center justify-between mx-auto p-8">
|
||||
<Link href="/" className="font-bold text-3xl">
|
||||
Home
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default function Hero(props) {
|
||||
return (
|
||||
<div className="pt-48 flex justify-center">
|
||||
<h1 className="text-6xl archivo-black-regular">{props.title}</h1>
|
||||
<h1 className="text-4xl archivo-black-regular">{props.title}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { modalContent, modalState } from '@/store/modalAtom'
|
||||
|
||||
import { AxiosType, useAxios } from '@/hooks/useAxios'
|
||||
|
||||
import { Button } from '@nextui-org/react'
|
||||
|
||||
import SingleDatePicker from './common/datepicker/SingleDatePicker'
|
||||
import RangeDatePicker from './common/datepicker/RangeDatePicker'
|
||||
import QGrid from './common/grid/QGrid'
|
||||
import { QToast } from '@/hooks/useToast'
|
||||
|
||||
export default function Intro() {
|
||||
const { get } = useAxios()
|
||||
|
||||
// const [open, setOpen] = useState(false)
|
||||
const [startDate, setStartDate] = useState(new Date())
|
||||
const singleDatePickerProps = {
|
||||
startDate,
|
||||
setStartDate,
|
||||
}
|
||||
|
||||
const [dateRange, setDateRange] = useState([null, null])
|
||||
const [startRangeDate, endRangeDate] = dateRange
|
||||
const rangeDatePickerProps = {
|
||||
startRangeDate,
|
||||
endRangeDate,
|
||||
setDateRange,
|
||||
}
|
||||
|
||||
// const gridProps = {
|
||||
// isPageable: false,
|
||||
// }
|
||||
const [gridProps, setGridProps] = useState({
|
||||
gridData: [],
|
||||
isPageable: false,
|
||||
})
|
||||
|
||||
const [open, setOpen] = useRecoilState(modalState)
|
||||
const [contents, setContent] = useRecoilState(modalContent)
|
||||
|
||||
const modelProps = {
|
||||
open,
|
||||
setOpen,
|
||||
}
|
||||
|
||||
const ipsum = (
|
||||
<>
|
||||
<p className="text-2xl">title</p>
|
||||
<p>
|
||||
저작자·발명가·과학기술자와 예술가의 권리는 법률로써 보호한다. 이 헌법은 1988년 2월 25일부터 시행한다. 다만, 이 헌법을 시행하기 위하여 필요한
|
||||
법률의 제정·개정과 이 헌법에 의한 대통령 및 국회의원의 선거 기타 이 헌법시행에 관한 준비는 이 헌법시행 전에 할 수 있다.
|
||||
</p>
|
||||
<p>
|
||||
국가는 주택개발정책등을 통하여 모든 국민이 쾌적한 주거생활을 할 수 있도록 노력하여야 한다. 통신·방송의 시설기준과 신문의 기능을 보장하기
|
||||
위하여 필요한 사항은 법률로 정한다.
|
||||
</p>
|
||||
<p>
|
||||
국회에서 의결된 법률안은 정부에 이송되어 15일 이내에 대통령이 공포한다. 선거에 관한 경비는 법률이 정하는 경우를 제외하고는 정당 또는
|
||||
후보자에게 부담시킬 수 없다.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
// const response = await fetch('https://www.ag-grid.com/example-assets/space-mission-data.json')
|
||||
// const data = await response.json()
|
||||
const data = await get({ type: AxiosType.EXTERNAL, url: 'https://www.ag-grid.com/example-assets/space-mission-data.json' })
|
||||
setGridProps({ ...gridProps, gridData: data })
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Intro</h1>
|
||||
<p>Drawing on canvas 2D is a simple way to create graphics on the web.</p>
|
||||
<div className="text-2xl">
|
||||
<Link href={'/login'}>
|
||||
<Button color="primary">로그인 페이지로 이동</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<div className="text-2xl">Single Date Picker</div>
|
||||
<div>
|
||||
<SingleDatePicker {...singleDatePickerProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<div className="text-2xl">Range Date Picker</div>
|
||||
<div>
|
||||
<RangeDatePicker {...rangeDatePickerProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<div className="text-2xl">QGrid</div>
|
||||
<div>
|
||||
<QGrid {...gridProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<div className="text-2xl">QModal</div>
|
||||
<div>
|
||||
{/* <Button color="primary" onClick={() => setOpen(true)}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<QModal {...modelProps}>{ipsum}</QModal> */}
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setContent(ipsum)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
Open Modal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<div className="text-2xl">QToast</div>
|
||||
<div>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
QToast({
|
||||
message: 'This is a toast message',
|
||||
type: 'success',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Open Toast
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
39
src/components/Main.jsx
Normal file
39
src/components/Main.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { logout } from '@/lib/authActions'
|
||||
import { useChangeLocale, useI18n } from '@/locales/client'
|
||||
import { Button, Chip } from '@nextui-org/react'
|
||||
|
||||
export default function MainPage(props) {
|
||||
const { currentLocale, isLoggedIn } = props
|
||||
const t = useI18n()
|
||||
const changeLocale = useChangeLocale()
|
||||
|
||||
const handleChangeLocale = () => {
|
||||
currentLocale === 'ja' ? changeLocale('ko') : changeLocale('ja')
|
||||
}
|
||||
|
||||
// console.log('MainPage', currentLocale)
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>{t('locale', { locale: <Chip color="primary">{currentLocale}</Chip> })}</h3>
|
||||
<div>{t('hello')}</div>
|
||||
<div>{t('welcome', { name: '효준' })}</div>
|
||||
<div>
|
||||
<Button onClick={handleChangeLocale}>Change Locale</Button>
|
||||
</div>
|
||||
{isLoggedIn && (
|
||||
<div className="my-4">
|
||||
<Button color="primary" onClick={handleLogout}>
|
||||
로그아웃
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -2,15 +2,15 @@ import { useCanvas } from '@/hooks/useCanvas'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Mode, useMode } from '@/hooks/useMode'
|
||||
import { Button } from '@nextui-org/react'
|
||||
|
||||
import RangeSlider from './ui/RangeSlider'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { canvasSizeState, fontSizeState, roofMaterialState, sortedPolygonArray } from '@/store/canvasAtom'
|
||||
import { canvasSizeState, fontSizeState, roofMaterialState, sortedPolygonArray, templateTypeState, compassState } from '@/store/canvasAtom'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { getCanvasState, insertCanvasState } from '@/lib/canvas'
|
||||
import { calculateIntersection } from '@/util/canvas-util'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import offsetPolygon from '@/util/qpolygon-utils'
|
||||
import * as turf from '@turf/turf'
|
||||
import { toGeoJSON } from '@/util/qpolygon-utils'
|
||||
|
||||
export default function Roof2() {
|
||||
const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas')
|
||||
@ -34,6 +34,10 @@ export default function Roof2() {
|
||||
//지붕재
|
||||
const roofMaterial = useRecoilValue(roofMaterialState)
|
||||
|
||||
const [templateType, setTemplateType] = useRecoilState(templateTypeState)
|
||||
|
||||
const [compass, setCompass] = useRecoilState(compassState)
|
||||
|
||||
const {
|
||||
mode,
|
||||
setMode,
|
||||
@ -49,6 +53,8 @@ export default function Roof2() {
|
||||
applyTemplateB,
|
||||
makeRoofPatternPolygon,
|
||||
createRoofRack,
|
||||
drawRoofPolygon,
|
||||
drawCellInTrestle,
|
||||
} = useMode()
|
||||
|
||||
// const [canvasState, setCanvasState] = useRecoilState(canvasAtom)
|
||||
@ -202,14 +208,25 @@ export default function Roof2() {
|
||||
]
|
||||
|
||||
const eightPoint4 = [
|
||||
{ x: 228, y: 92 },
|
||||
{ x: 228, y: 592 },
|
||||
{ x: 478, y: 592 },
|
||||
{ x: 478, y: 342 },
|
||||
{ x: 728, y: 342 },
|
||||
{ x: 728, y: 592 },
|
||||
{ x: 1078, y: 592 },
|
||||
{ x: 1078, y: 92 },
|
||||
{ x: 200, y: 200 },
|
||||
{ x: 200, y: 400 },
|
||||
{ x: 500, y: 400 },
|
||||
{ x: 500, y: 700 },
|
||||
{ x: 800, y: 700 },
|
||||
{ x: 800, y: 400 },
|
||||
{ x: 1100, y: 400 },
|
||||
{ x: 1100, y: 200 },
|
||||
]
|
||||
|
||||
const eightPoint5 = [
|
||||
{ x: 140, y: 101 },
|
||||
{ x: 140, y: 601 },
|
||||
{ x: 440, y: 601 },
|
||||
{ x: 440, y: 801 },
|
||||
{ x: 840, y: 801 },
|
||||
{ x: 840, y: 601 },
|
||||
{ x: 1140, y: 601 },
|
||||
{ x: 1140, y: 101 },
|
||||
]
|
||||
|
||||
const twelvePoint = [
|
||||
@ -227,6 +244,21 @@ export default function Roof2() {
|
||||
{ x: 995, y: 166 },
|
||||
]
|
||||
|
||||
const twelvePoint2 = [
|
||||
{ x: 165, y: 81 },
|
||||
{ x: 165, y: 1081 },
|
||||
{ x: 465, y: 1081 },
|
||||
{ x: 465, y: 781 },
|
||||
{ x: 765, y: 781 },
|
||||
{ x: 765, y: 1081 },
|
||||
{ x: 1065, y: 1081 },
|
||||
{ x: 1065, y: 581 },
|
||||
{ x: 765, y: 581 },
|
||||
{ x: 765, y: 281 },
|
||||
{ x: 1065, y: 281 },
|
||||
{ x: 1065, y: 81 },
|
||||
]
|
||||
|
||||
const complicatedType = [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 100, y: 1100 },
|
||||
@ -242,6 +274,29 @@ export default function Roof2() {
|
||||
{ x: 1000, y: 100 },
|
||||
]
|
||||
|
||||
const testType = [
|
||||
{ x: 500, y: 400 },
|
||||
{ x: 650, y: 550 },
|
||||
{ x: 575, y: 625 },
|
||||
{ x: 325, y: 625 },
|
||||
{ x: 100, y: 400 },
|
||||
]
|
||||
|
||||
const testType2 = [
|
||||
{ x: 100, y: 400 },
|
||||
{ x: 325, y: 625 },
|
||||
{ x: 575, y: 625 },
|
||||
{ x: 650, y: 550 },
|
||||
{ x: 500, y: 400 },
|
||||
]
|
||||
|
||||
const triangleType = [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 100, y: 600 },
|
||||
{ x: 600, y: 600 },
|
||||
{ x: 600, y: 100 },
|
||||
]
|
||||
|
||||
const types = [type1, type2, type3, type4, type1A, type1B, eightPoint, eightPoint2, eightPoint3, eightPoint4, twelvePoint]
|
||||
const newP = [
|
||||
{ x: 450, y: 450 },
|
||||
@ -249,9 +304,10 @@ export default function Roof2() {
|
||||
{ x: 675, y: 275 },
|
||||
{ x: 450, y: 850 },
|
||||
]
|
||||
const polygon = new QPolygon(type1, {
|
||||
|
||||
const polygon = new QPolygon(twelvePoint, {
|
||||
fill: 'transparent',
|
||||
stroke: 'black',
|
||||
stroke: 'green',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
fontSize: fontSize,
|
||||
@ -259,9 +315,8 @@ export default function Roof2() {
|
||||
})
|
||||
|
||||
canvas?.add(polygon)
|
||||
handleOuterlinesTest2(polygon)
|
||||
// const lines = togglePolygonLine(polygon)
|
||||
// togglePolygonLine(lines[0])
|
||||
handleOuterlinesTest2(polygon, 50)
|
||||
setTemplateType(1)
|
||||
}
|
||||
|
||||
const rotateShape = () => {
|
||||
@ -433,6 +488,18 @@ export default function Roof2() {
|
||||
makeRoofPatternPolygon(roofStyle)
|
||||
}
|
||||
|
||||
const deleteCell = () => {
|
||||
const selectedCells = canvas?.getObjects().filter((obj) => obj.name === 'cell' && obj.selected)
|
||||
|
||||
selectedCells.forEach((cell) => {
|
||||
canvas?.remove(cell)
|
||||
})
|
||||
}
|
||||
|
||||
const setCompassState = (degree) => {
|
||||
setCompass(degree)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{canvas && (
|
||||
@ -462,27 +529,43 @@ export default function Roof2() {
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => drawRoofPatterns(2)}>
|
||||
지붕패턴2
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(90)}>
|
||||
방위표◀
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(270)}>
|
||||
방위표▶
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(180)}>
|
||||
방위표▲
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setCompassState(0)}>
|
||||
방위표▼
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.ROOF_TRESTLE)}>
|
||||
지붕가대생성
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.FILL_CELLS)}>
|
||||
태양광셀생성
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEMPLATE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.CELL_POWERCON)}>
|
||||
파워콘설치
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${mode === Mode.TEXTBOX ? 'primary' : 'default'}`} onClick={() => setMode(Mode.TEXTBOX)}>
|
||||
텍스트박스 모드
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={handleUndo}>
|
||||
{/*<Button className="m-1 p-2" onClick={handleUndo}>
|
||||
Undo
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={handleRedo}>
|
||||
Redo
|
||||
</Button>
|
||||
</Button>*/}
|
||||
<Button className="m-1 p-2" onClick={handleClear}>
|
||||
clear
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={zoomIn}>
|
||||
{/*<Button className="m-1 p-2" onClick={zoomIn}>
|
||||
확대
|
||||
</Button>
|
||||
*/}
|
||||
<Button className="m-1 p-2" onClick={zoomOut}>
|
||||
축소
|
||||
</Button>
|
||||
@ -493,51 +576,59 @@ export default function Roof2() {
|
||||
<Button className="m-1 p-2" onClick={makePolygon}>
|
||||
다각형 추가
|
||||
</Button>
|
||||
<Button
|
||||
className="m-1 p-2"
|
||||
onClick={() => {
|
||||
setCanvasBackgroundWithDots(canvas, 10)
|
||||
}}
|
||||
>
|
||||
점선 추가
|
||||
</Button>
|
||||
<Button
|
||||
className="m-1 p-2"
|
||||
onClick={() => {
|
||||
setCanvasBackgroundWithDots(canvas, 20)
|
||||
}}
|
||||
>
|
||||
점선 추가
|
||||
</Button>
|
||||
{templateType === 0 && (
|
||||
<>
|
||||
<Button
|
||||
className="m-1 p-2"
|
||||
onClick={() => {
|
||||
setCanvasBackgroundWithDots(canvas, 10)
|
||||
}}
|
||||
>
|
||||
점선 추가
|
||||
</Button>
|
||||
<Button
|
||||
className="m-1 p-2"
|
||||
onClick={() => {
|
||||
setCanvasBackgroundWithDots(canvas, 20)
|
||||
}}
|
||||
>
|
||||
점선 추가
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={makeQPolygon}>
|
||||
QPolygon
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button className="m-1 p-2" onClick={saveImage}>
|
||||
저장
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={makeQPolygon}>
|
||||
QPolygon
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={rotateShape}>
|
||||
{/*<Button className="m-1 p-2" onClick={rotateShape}>
|
||||
회전
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={makeQLine}>
|
||||
</Button>*/}
|
||||
{/*<Button className="m-1 p-2" onClick={makeQLine}>
|
||||
QLine
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={PolygonToLine}>
|
||||
PolygonToLine
|
||||
</Button>
|
||||
{/*<Button className="m-1 p-2" onClick={handleSaveCanvas}>
|
||||
canvas 내용 저장하기
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={handleLoadCanvas}>
|
||||
canvas 내용 불러오기
|
||||
</Button>*/}
|
||||
<Button className="m-1 p-2" onClick={addCanvas}>
|
||||
캔버스 추가
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={drawRoofMaterial}>
|
||||
지붕타입 지붕재
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={createRoofRack}>
|
||||
지붕가대
|
||||
</Button>*/}
|
||||
{templateType === 1 && (
|
||||
<>
|
||||
<Button className="m-1 p-2" onClick={drawRoofMaterial}>
|
||||
지붕타입 지붕재
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={createRoofRack}>
|
||||
지붕가대설치
|
||||
</Button>
|
||||
<Button className="m-1 p-2" onClick={drawCellInTrestle}>
|
||||
선택한 가대 셀채우기
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button className="m-1 p-2" onClick={deleteCell}>
|
||||
선택 셀 지우기
|
||||
</Button>
|
||||
<Button className="m-1 p-2" color={`${showControl ? 'primary' : 'default'}`} onClick={handleShowController}>
|
||||
canvas 컨트롤러 {`${showControl ? '숨기기' : '보이기'}`}
|
||||
|
||||
75
src/components/auth/Login.jsx
Normal file
75
src/components/auth/Login.jsx
Normal file
@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import { login } from '@/lib/authActions'
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<div className="flex flex-col align-center h-screen">
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img alt="Your Company" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" className="mx-auto h-10 w-auto" />
|
||||
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in to your account</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form action={login} method="POST" className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="userId" className="block text-sm font-medium leading-6 text-gray-900">
|
||||
User ID
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="userId"
|
||||
name="id"
|
||||
type="text"
|
||||
required
|
||||
// autoComplete="email"
|
||||
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">
|
||||
Password
|
||||
</label>
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-semibold text-indigo-600 hover:text-indigo-500">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Not a member?{' '}
|
||||
<a href="#" className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">
|
||||
Start a 14 day free trial
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
src/components/common/datepicker/RangeDatePicker.jsx
Normal file
19
src/components/common/datepicker/RangeDatePicker.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import DatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
export default function RangeDatePicker(props) {
|
||||
const { startRangeDate, endRangeDate, setDateRange } = props
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
dateFormat={`yyyy-MM-dd`}
|
||||
selectsRange={true}
|
||||
startDate={startRangeDate}
|
||||
endDate={endRangeDate}
|
||||
onChange={(update) => {
|
||||
setDateRange(update)
|
||||
}}
|
||||
isClearable={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
8
src/components/common/datepicker/SingleDatePicker.jsx
Normal file
8
src/components/common/datepicker/SingleDatePicker.jsx
Normal file
@ -0,0 +1,8 @@
|
||||
import DatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
export default function SingleDatePicker(props) {
|
||||
const { startDate, setStartDate } = props
|
||||
|
||||
return <DatePicker showIcon dateFormat={`yyyy-MM-dd`} selected={startDate} onChange={(date) => setStartDate(date)} />
|
||||
}
|
||||
89
src/components/common/grid/QGrid.jsx
Normal file
89
src/components/common/grid/QGrid.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { AgGridReact } from 'ag-grid-react'
|
||||
|
||||
import 'ag-grid-community/styles/ag-grid.css'
|
||||
import 'ag-grid-community/styles/ag-theme-quartz.css'
|
||||
|
||||
export default function QGrid(props) {
|
||||
console.log('QGrid props:', props)
|
||||
const { gridData, gridColumns, isPageable = true } = props
|
||||
const [count, setCount] = useState(0)
|
||||
const [clickedCount, setClickedCount] = useState(0)
|
||||
/**
|
||||
* 행 데이터를 설정할 때 useState을 사용하여 렌더링 전반에 걸쳐 일관된 배열 참조를 유지하는 것이 좋습니다
|
||||
*/
|
||||
const [rowData, setRowData] = useState(null)
|
||||
|
||||
/**
|
||||
* Column Definitions를 설정할 때는 useMemo 또는 useState를 사용하여 렌더 간에 일관된 참조를 유지하십시오.
|
||||
* 응용 프로그램이 Column Definitions를 동적으로 변경하는 경우에도 렌더링 간에 일관된 참조를 유지하려면 useMemo 또는 useState를 사용하십시오.
|
||||
*/
|
||||
const [colDefs, setColDefs] = useState(
|
||||
gridColumns ?? [
|
||||
{ field: 'mission', filter: true },
|
||||
{ field: 'company' },
|
||||
{ field: 'location' },
|
||||
{ field: 'date' },
|
||||
{ field: 'price', valueFormatter: (params) => `₩ ${params.value.toLocaleString()}` },
|
||||
{ field: 'successful' },
|
||||
{ field: 'rocket' },
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* defaultColDef 속성을 제공할 때 이 인라인 또는 구성 요소의 단순 개체로 정의하지 마십시오. 이렇게 하면 모든 렌더링에서 새 인스턴스가 생성됩니다.
|
||||
* 대신 or useState 를 사용하여 useMemo 렌더 간에 일관된 참조가 유지되도록 합니다.
|
||||
*/
|
||||
const defaultColDef = useMemo(() => {
|
||||
filter: true
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 단순 유형(string, boolean 및 number)의 속성은 렌더링 간에 값으로 비교되므로 후크를 사용할 필요가 없습니다.
|
||||
*/
|
||||
const rowBuffer = 0
|
||||
|
||||
/**
|
||||
* 모든 렌더링에서 그리드 상태를 재설정하지 않도록 useCallback을 사용하는 것이 좋습니다.
|
||||
*/
|
||||
const isRowSelectable = useCallback((node) => node.data.value > count, [count])
|
||||
|
||||
/**
|
||||
* Event Listeners의 경우 이벤트 처리기가 그리드 내에서 업데이트를 트리거하지 않으므로 useCallback을 사용할 필요가 없습니다.
|
||||
* 그러나 콜백과 일관성을 유지하고 항상 useCallback을 사용하는 것이 더 쉬울 수 있습니다.
|
||||
*/
|
||||
const onFilterOpened = useCallback(() => {
|
||||
console.log(`number of clicks is ${clickedCount}`)
|
||||
}, [clickedCount])
|
||||
|
||||
// Fetch data & update rowData state
|
||||
useEffect(() => {
|
||||
// async function fetchData() {
|
||||
// const response = await fetch('https://www.ag-grid.com/example-assets/space-mission-data.json')
|
||||
// const data = await response.json()
|
||||
// setRowData(data)
|
||||
// }
|
||||
|
||||
console.log('use effect')
|
||||
gridData ? setRowData(gridData) : ''
|
||||
}, [gridData])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ag-theme-quartz" // applying the Data Grid theme
|
||||
style={{ height: 500 }} // the Data Grid will fill the size of the parent container
|
||||
>
|
||||
<AgGridReact
|
||||
rowBuffer={rowBuffer}
|
||||
rowData={rowData}
|
||||
columnDefs={colDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
isRowSelectable={isRowSelectable}
|
||||
onFilterOpened={onFilterOpened}
|
||||
pagination={isPageable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
src/components/common/modal/QModal.jsx
Normal file
20
src/components/common/modal/QModal.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
|
||||
import { Modal } from 'react-responsive-modal'
|
||||
|
||||
import { modalContent, modalState } from '@/store/modalAtom'
|
||||
|
||||
import 'react-responsive-modal/styles.css'
|
||||
|
||||
export default function QModal() {
|
||||
const [open, setOpen] = useRecoilState(modalState)
|
||||
const children = useRecoilValue(modalContent)
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={() => setOpen(false)} center>
|
||||
{children}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@ -29,6 +29,8 @@ export const QLine = fabric.util.createClass(fabric.Line, {
|
||||
this.setLength()
|
||||
|
||||
this.direction = options.direction ?? getDirection({ x: this.x1, y: this.y1 }, { x: this.x2, y: this.y2 })
|
||||
this.startPoint = { x: this.x1, y: this.y1 }
|
||||
this.endPoint = { x: this.x2, y: this.y2 }
|
||||
|
||||
if (canvas) {
|
||||
this.canvas = canvas
|
||||
|
||||
@ -2,7 +2,8 @@ import { fabric } from 'fabric'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util'
|
||||
import { calculateAngle, dividePolygon, drawHelpLineInHexagon } from '@/util/qpolygon-utils'
|
||||
import { calculateAngle, drawHippedRoof, inPolygon, lineIntersect, splitPolygonWithLines, toGeoJSON } from '@/util/qpolygon-utils'
|
||||
import * as turf from '@turf/turf'
|
||||
|
||||
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
type: 'QPolygon',
|
||||
@ -14,6 +15,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
ridges: [],
|
||||
connectRidges: [],
|
||||
cells: [],
|
||||
parentId: null,
|
||||
innerLines: [],
|
||||
initialize: function (points, options, canvas) {
|
||||
// 소수점 전부 제거
|
||||
points.forEach((point) => {
|
||||
@ -21,6 +24,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
point.y = Math.round(point.y)
|
||||
})
|
||||
options.sort = options.sort ?? true
|
||||
options.parentId = options.parentId ?? null
|
||||
|
||||
if (!options.sort && points.length <= 8) {
|
||||
points = sortedPointLessEightPoint(points)
|
||||
} else {
|
||||
@ -31,7 +36,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
}
|
||||
const nextPoint = points[(i + 1) % points.length]
|
||||
const angle = calculateAngle(point, nextPoint)
|
||||
if (!(Math.abs(angle) === 0 || Math.abs(angle) === 180)) {
|
||||
if (!(Math.abs(angle) === 0 || Math.abs(angle) === 180 || Math.abs(angle) === 90)) {
|
||||
isDiagonal = true
|
||||
}
|
||||
})
|
||||
@ -146,11 +151,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
|
||||
// 보조선 그리기
|
||||
drawHelpLine(chon = 4) {
|
||||
drawHelpLineInHexagon(this, chon)
|
||||
// drawHelpLineInHexagon(this, chon)
|
||||
drawHippedRoof(this, chon)
|
||||
},
|
||||
|
||||
addLengthText() {
|
||||
let points = this.points
|
||||
let points = this.getCurrentPoints()
|
||||
|
||||
points.forEach((start, i) => {
|
||||
const thisText = this.canvas.getObjects().find((obj) => obj.name === 'lengthText' && obj.parentId === this.id && obj.idx === i)
|
||||
@ -228,18 +234,17 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const rectLeft = minX + i * (rectWidth + cell.padding) + tmpWidth
|
||||
const rectTop = minY + j * (rectHeight + cell.padding) + tmpHeight
|
||||
const rectLeft = minX + i * (rectWidth + cell.padding)
|
||||
const rectTop = minY + j * (rectHeight + cell.padding)
|
||||
|
||||
const rectPoints = [
|
||||
{ x: rectLeft, y: rectTop },
|
||||
{ x: rectLeft + rectWidth, y: rectTop },
|
||||
{ x: rectLeft, y: rectTop + rectHeight },
|
||||
{ x: rectLeft + rectWidth, y: rectTop + rectHeight },
|
||||
{ x: rectLeft + rectWidth, y: rectTop },
|
||||
]
|
||||
const allPointsInside = rectPoints.every((point) => this.inPolygon(point))
|
||||
|
||||
if (allPointsInside) {
|
||||
if (inPolygon(this.points, rectPoints)) {
|
||||
const rect = new fabric.Rect({
|
||||
left: rectLeft,
|
||||
top: rectTop,
|
||||
@ -256,6 +261,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
opacity: 0.8,
|
||||
name: 'cell',
|
||||
idx: idx,
|
||||
parentId: this.id,
|
||||
})
|
||||
|
||||
idx++
|
||||
@ -268,6 +274,277 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.cells = drawCellsArray
|
||||
return drawCellsArray
|
||||
},
|
||||
fillCellABType(cell = { width: 50, height: 100, padding: 5, wallDirection: 'left', referenceDirection: 'none', startIndex: -1 }) {
|
||||
const points = this.points
|
||||
|
||||
const minX = Math.min(...points.map((p) => p.x)) //왼쪽
|
||||
const maxX = Math.max(...points.map((p) => p.x)) //오른쪽
|
||||
const minY = Math.min(...points.map((p) => p.y)) //위
|
||||
const maxY = Math.max(...points.map((p) => p.y)) //아래
|
||||
|
||||
const boundingBoxWidth = maxX - minX
|
||||
const boundingBoxHeight = maxY - minY
|
||||
|
||||
const rectWidth = cell.width
|
||||
const rectHeight = cell.height
|
||||
|
||||
const cols = Math.floor((boundingBoxWidth + cell.padding) / (rectWidth + cell.padding))
|
||||
const rows = Math.floor((boundingBoxHeight + cell.padding) / (rectHeight + cell.padding))
|
||||
|
||||
//전체 높이에서 패딩을 포함하고 rows를 곱해서 여백길이를 계산 후에 2로 나누면 반높이를 넣어서 중간으로 정렬
|
||||
|
||||
let centerHeight = rows > 1 ? (boundingBoxHeight - (rectHeight + cell.padding / 2) * rows) / 2 : (boundingBoxHeight - rectHeight * rows) / 2 //rows 1개 이상이면 cell 을 반 나눠서 중간을 맞춘다
|
||||
let centerWidth = cols > 1 ? (boundingBoxWidth - (rectWidth + cell.padding / 2) * cols) / 2 : (boundingBoxWidth - rectWidth * cols) / 2
|
||||
|
||||
const drawCellsArray = [] //그려진 셀의 배열
|
||||
|
||||
let idx = 1
|
||||
let startXPos, startYPos
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const rectPoints = []
|
||||
|
||||
if (cell.referenceDirection !== 'none') {
|
||||
//4각형은 기준점이 없다
|
||||
|
||||
if (cell.referenceDirection === 'top') {
|
||||
//top, bottom은 A패턴만
|
||||
if (cell.wallDirection === 'left') {
|
||||
startXPos = minX + i * rectWidth
|
||||
startYPos = minY + j * rectHeight
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding //옆으로 패딩
|
||||
}
|
||||
} else {
|
||||
startXPos = maxX - (1 + i) * rectWidth - 0.01
|
||||
startYPos = minY + j * rectHeight + 0.01
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos - i * cell.padding //옆으로 패딩
|
||||
}
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
startYPos = startYPos + j * cell.padding
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos + rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos + rectHeight },
|
||||
{ x: startXPos + rectWidth, y: startYPos + rectHeight },
|
||||
)
|
||||
} else if (cell.referenceDirection === 'bottom') {
|
||||
if (cell.wallDirection === 'left') {
|
||||
startXPos = minX + i * rectWidth
|
||||
startYPos = maxY - j * rectHeight - 0.01
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos + rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos - rectHeight },
|
||||
{ x: startXPos + rectWidth, y: startYPos - rectHeight },
|
||||
)
|
||||
} else {
|
||||
startXPos = maxX - i * rectWidth - 0.01
|
||||
startYPos = maxY - j * rectHeight - 0.01
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos - i * cell.padding
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos - rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos - rectHeight },
|
||||
{ x: startXPos - rectWidth, y: startYPos - rectHeight },
|
||||
)
|
||||
startXPos = startXPos - rectWidth //우 -> 좌 들어가야해서 마이너스 처리
|
||||
}
|
||||
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
|
||||
|
||||
if (j > 0) {
|
||||
startYPos = startYPos - j * cell.padding
|
||||
}
|
||||
} else if (cell.referenceDirection === 'left') {
|
||||
//여기서부턴 B패턴임
|
||||
if (cell.wallDirection === 'top') {
|
||||
startXPos = minX + i * rectWidth
|
||||
startYPos = minY + j * rectHeight
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding //밑으로
|
||||
}
|
||||
if (j > 0) {
|
||||
startYPos = startYPos + j * cell.padding //옆으로 패딩
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos + rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos + rectHeight },
|
||||
{ x: startXPos + rectWidth, y: startYPos + rectHeight },
|
||||
)
|
||||
} else {
|
||||
startXPos = minX + i * rectWidth
|
||||
startYPos = maxY - j * rectHeight - 0.01
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
startYPos = startYPos - j * cell.padding
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos + rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos - rectHeight },
|
||||
{ x: startXPos + rectWidth, y: startYPos - rectHeight },
|
||||
)
|
||||
|
||||
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
|
||||
}
|
||||
} else if (cell.referenceDirection === 'right') {
|
||||
if (cell.wallDirection === 'top') {
|
||||
startXPos = maxX - i * rectWidth - 0.01
|
||||
startYPos = minY + j * rectHeight + 0.01
|
||||
|
||||
if (j > 0) {
|
||||
startYPos = startYPos + j * cell.padding //위에서 밑으로라 +
|
||||
}
|
||||
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos - rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos + rectHeight },
|
||||
{ x: startXPos - rectWidth, y: startYPos + rectHeight },
|
||||
)
|
||||
} else {
|
||||
startXPos = maxX - i * rectWidth - 0.01
|
||||
startYPos = maxY - j * rectHeight - 0.01
|
||||
|
||||
if (j > 0) {
|
||||
startYPos = startYPos - j * cell.padding
|
||||
}
|
||||
rectPoints.push(
|
||||
{ x: startXPos, y: startYPos },
|
||||
{ x: startXPos - rectWidth, y: startYPos },
|
||||
{ x: startXPos, y: startYPos - rectHeight },
|
||||
{ x: startXPos - rectWidth, y: startYPos - rectHeight },
|
||||
)
|
||||
startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos - i * cell.padding //옆으로 패딩
|
||||
}
|
||||
startXPos = startXPos - rectWidth // 우측에서 -> 좌측으로 그려짐
|
||||
}
|
||||
} else {
|
||||
// centerWidth = 0 //나중에 중간 정렬 이면 어쩌구 함수 만들어서 넣음
|
||||
if (['left', 'right'].includes(cell.wallDirection)) {
|
||||
centerWidth = 0
|
||||
} else if (['top', 'bottom'].includes(cell.wallDirection)) {
|
||||
centerHeight = 0
|
||||
}
|
||||
|
||||
if (cell.wallDirection === 'left') {
|
||||
startXPos = minX + i * rectWidth + centerWidth
|
||||
startYPos = minY + j * rectHeight + centerHeight
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding
|
||||
}
|
||||
if (j > 0 && j < rows) {
|
||||
startYPos = startYPos + j * cell.padding
|
||||
}
|
||||
} else if (cell.wallDirection === 'right') {
|
||||
startXPos = maxX - (1 + i) * rectWidth - 0.01 - centerWidth
|
||||
startYPos = minY + j * rectHeight + 0.01 + centerHeight
|
||||
if (i > 0) {
|
||||
startXPos = startXPos - i * cell.padding
|
||||
}
|
||||
if (j > 0 && j < rows) {
|
||||
startYPos = startYPos + j * cell.padding
|
||||
}
|
||||
} else if (cell.wallDirection === 'top') {
|
||||
startXPos = minX + i * rectWidth - 0.01 + centerWidth
|
||||
startYPos = minY + j * rectHeight + 0.01 + centerHeight
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding
|
||||
}
|
||||
if (j > 0 && j < rows) {
|
||||
startYPos = startYPos + j * cell.padding
|
||||
}
|
||||
} else if (cell.wallDirection === 'bottom') {
|
||||
startXPos = minX + i * rectWidth + 0.01 + centerWidth
|
||||
startYPos = maxY - (j + 1) * rectHeight - 0.01 - centerHeight
|
||||
|
||||
if (i > 0) {
|
||||
startXPos = startXPos + i * cell.padding
|
||||
}
|
||||
|
||||
if (j > 0 && j < rows) {
|
||||
startYPos = startYPos - j * cell.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allPointsInside = rectPoints.every((point) => this.inPolygonABType(point.x, point.y, points))
|
||||
|
||||
if (allPointsInside) {
|
||||
//먼저 그룹화를 시켜놓고 뒤에서 글씨를 넣어서 변경한다
|
||||
const text = new fabric.Text(``, {
|
||||
fontFamily: 'serif',
|
||||
fontSize: 30,
|
||||
fill: 'black',
|
||||
type: 'cellText',
|
||||
})
|
||||
|
||||
const rect = new fabric.Rect({
|
||||
// left: startXPos,
|
||||
// top: startYPos,
|
||||
width: rectWidth,
|
||||
height: rectHeight,
|
||||
fill: '#BFFD9F',
|
||||
stroke: 'black',
|
||||
selectable: true, // 선택 가능하게 설정
|
||||
// lockMovementX: true, // X 축 이동 잠금
|
||||
// lockMovementY: true, // Y 축 이동 잠금
|
||||
// lockRotation: true, // 회전 잠금
|
||||
// lockScalingX: true, // X 축 크기 조정 잠금
|
||||
// lockScalingY: true, // Y 축 크기 조정 잠금
|
||||
opacity: 0.8,
|
||||
name: 'cell',
|
||||
idx: idx,
|
||||
type: 'cellRect',
|
||||
})
|
||||
|
||||
var group = new fabric.Group([rect, text], {
|
||||
left: startXPos,
|
||||
top: startYPos,
|
||||
})
|
||||
|
||||
idx++
|
||||
drawCellsArray.push(group) //배열에 넣어서 반환한다
|
||||
this.canvas.add(group)
|
||||
this.canvas?.renderAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cells = drawCellsArray
|
||||
return drawCellsArray
|
||||
},
|
||||
|
||||
inPolygon(point) {
|
||||
const vertices = this.points
|
||||
let intersects = 0
|
||||
@ -291,7 +568,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
}
|
||||
|
||||
let xInt = ((point.y - vertex1.y) * (vertex2.x - vertex1.x)) / (vertex2.y - vertex1.y) + vertex1.x
|
||||
if (xInt < point.x) {
|
||||
if (xInt <= point.x) {
|
||||
intersects++
|
||||
}
|
||||
}
|
||||
@ -299,6 +576,57 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
return intersects % 2 === 1
|
||||
},
|
||||
|
||||
inPolygonABType(x, y, polygon) {
|
||||
let inside = false
|
||||
let n = polygon.length
|
||||
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
let xi = polygon[i].x
|
||||
let yi = polygon[i].y
|
||||
let xj = polygon[j].x
|
||||
let yj = polygon[j].y
|
||||
|
||||
// console.log('xi : ', xi, 'yi : ', yi, 'xj : ', xj, 'yj : ', yj)
|
||||
|
||||
let intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
||||
if (intersect) inside = !inside
|
||||
}
|
||||
|
||||
return inside
|
||||
},
|
||||
|
||||
inPolygon2(rectPoints) {
|
||||
const polygonCoords = toGeoJSON(this.points)
|
||||
const rectCoords = toGeoJSON(rectPoints)
|
||||
|
||||
const outerPolygon = turf.polygon([polygonCoords])
|
||||
const innerPolygon = turf.polygon([rectCoords])
|
||||
// 각 점이 다각형 내부에 있는지 확인
|
||||
const allPointsInside = rectCoords.every((coord) => {
|
||||
const point = turf.point(coord)
|
||||
return turf.booleanPointInPolygon(point, outerPolygon)
|
||||
})
|
||||
|
||||
// 사각형의 변 정의
|
||||
const rectEdges = [
|
||||
[rectCoords[0], rectCoords[1]],
|
||||
[rectCoords[1], rectCoords[2]],
|
||||
[rectCoords[2], rectCoords[3]],
|
||||
[rectCoords[3], rectCoords[0]],
|
||||
]
|
||||
|
||||
// 다각형의 변 정의
|
||||
const outerEdges = turf.lineString(outerPolygon.geometry.coordinates[0])
|
||||
|
||||
// 사각형의 변들이 다각형의 변과 교차하는지 확인
|
||||
const noEdgesIntersect = rectEdges.every((edge) => {
|
||||
const line = turf.lineString(edge)
|
||||
const intersects = turf.lineIntersect(line, outerEdges)
|
||||
return intersects.features.length === 0
|
||||
})
|
||||
|
||||
return allPointsInside && noEdgesIntersect
|
||||
},
|
||||
distanceFromEdge(point) {
|
||||
const vertices = this.getCurrentPoints()
|
||||
let minDistance = Infinity
|
||||
@ -330,24 +658,15 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
return minDistance
|
||||
},
|
||||
getCurrentPoints() {
|
||||
const scaleX = this.scaleX
|
||||
const scaleY = this.scaleY
|
||||
|
||||
const left = this.left
|
||||
const top = this.top
|
||||
|
||||
// 시작점
|
||||
const point = this.points[0]
|
||||
|
||||
const movingX = left - point.x * scaleX
|
||||
const movingY = top - point.y * scaleY
|
||||
|
||||
return this.points.map((point) => {
|
||||
return {
|
||||
x: point.x * scaleX + movingX,
|
||||
y: point.y * scaleY + movingY,
|
||||
}
|
||||
})
|
||||
const pathOffset = this.get('pathOffset')
|
||||
const matrix = this.calcTransformMatrix()
|
||||
return this.get('points')
|
||||
.map(function (p) {
|
||||
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
|
||||
})
|
||||
.map(function (p) {
|
||||
return fabric.util.transformPoint(p, matrix)
|
||||
})
|
||||
},
|
||||
setWall: function (wall) {
|
||||
this.wall = wall
|
||||
@ -358,6 +677,6 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
})
|
||||
},
|
||||
divideLine() {
|
||||
dividePolygon(this)
|
||||
splitPolygonWithLines(this)
|
||||
},
|
||||
})
|
||||
|
||||
67
src/hooks/useAxios.js
Normal file
67
src/hooks/useAxios.js
Normal file
@ -0,0 +1,67 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const AxiosType = {
|
||||
INTERNAL: 'Internal',
|
||||
EXTERNAL: 'External',
|
||||
}
|
||||
|
||||
export function useAxios() {
|
||||
const getInstances = (type) => {
|
||||
return axios.create({
|
||||
baseURL: type === AxiosType.INTERNAL ? process.env.NEXT_PUBLIC_API_SERVER_PATH : '',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
// config['Authorization'] = localStorage.getItem('token')
|
||||
//TODO: 인터셉터에서 추가 로직 구현
|
||||
return config
|
||||
})
|
||||
|
||||
axios.interceptors.request.use(undefined, (error) => {
|
||||
//TODO: 인터셉터에서 에러 처리 로직 구현
|
||||
// if (error.isAxiosError && e.response?.status === 401) {
|
||||
// localStorage.removeItem('token')
|
||||
// }
|
||||
})
|
||||
|
||||
const get = async ({ type, url }) => {
|
||||
return await getInstances(type)
|
||||
.get(url)
|
||||
.then((res) => res.data)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const post = async ({ type, url, data }) => {
|
||||
return await getInstances(type)
|
||||
.post(url, data)
|
||||
.then((res) => res.data)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const put = async ({ type, url, data }) => {
|
||||
return await getInstances(type)
|
||||
.put(url, data)
|
||||
.then((res) => res.data)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const patch = async ({ type, url, data }) => {
|
||||
return await getInstances(type)
|
||||
.patch(url, data)
|
||||
.then((res) => res.data)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const del = async ({ type, url }) => {
|
||||
return await getInstances(type)
|
||||
.delete(url)
|
||||
.then((res) => res.data)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
return { get, post, put, patch, del }
|
||||
}
|
||||
@ -89,21 +89,37 @@ export function useCanvas(id) {
|
||||
canvas?.off('object:modified')
|
||||
canvas?.off('object:removed')
|
||||
canvas?.off('object:added')
|
||||
canvas?.off('mouse:move', drawMouseLines)
|
||||
canvas?.off('mouse:down', handleMouseDown)
|
||||
canvas?.off('mouse:move')
|
||||
canvas?.off('mouse:down')
|
||||
}
|
||||
|
||||
const addEventOnObject = (e) => {
|
||||
const target = e.target
|
||||
if (target.name === 'cell') {
|
||||
target.on('mousedown', () => {
|
||||
target.set({ fill: 'red' })
|
||||
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', () => {
|
||||
target.set({ strokeWidth: 5 })
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -339,8 +355,8 @@ export function useCanvas(id) {
|
||||
|
||||
let top = targetObj.top + 10
|
||||
|
||||
if (top > CANVAS.HEIGHT) {
|
||||
top = CANVAS.HEIGHT
|
||||
if (top > canvasSize.vertical) {
|
||||
top = canvasSize.vertical
|
||||
}
|
||||
|
||||
targetObj.set({ top: top })
|
||||
@ -387,8 +403,8 @@ export function useCanvas(id) {
|
||||
|
||||
let left = targetObj.left + 10
|
||||
|
||||
if (left > CANVAS.WIDTH) {
|
||||
left = CANVAS.WIDTH
|
||||
if (left > canvasSize.horizontal) {
|
||||
left = canvasSize.horizontal
|
||||
}
|
||||
|
||||
targetObj.set({ left: left })
|
||||
|
||||
1428
src/hooks/useMode.js
1428
src/hooks/useMode.js
File diff suppressed because it is too large
Load Diff
35
src/hooks/useToast.js
Normal file
35
src/hooks/useToast.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const toastDefaultOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 3000,
|
||||
draggable: false,
|
||||
hideProgressBar: false,
|
||||
rtl: false,
|
||||
pauseOnFocusLoss: true,
|
||||
pauseOnHover: true,
|
||||
theme: 'light',
|
||||
limit: 2,
|
||||
closeOnClick: true,
|
||||
}
|
||||
|
||||
const QToast = (props) => {
|
||||
// type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default'
|
||||
const { message, type = 'info', options } = props
|
||||
const customOptions = { ...toastDefaultOptions, ...options }
|
||||
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return toast.info(message, customOptions)
|
||||
case 'success':
|
||||
return toast.success(message, customOptions)
|
||||
case 'warning':
|
||||
return toast.warn(message, customOptions)
|
||||
case 'error':
|
||||
return toast.error(message, customOptions)
|
||||
default:
|
||||
return toast(message, customOptions)
|
||||
}
|
||||
}
|
||||
|
||||
export { QToast }
|
||||
61
src/lib/authActions.js
Normal file
61
src/lib/authActions.js
Normal file
@ -0,0 +1,61 @@
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { getIronSession } from 'iron-session'
|
||||
|
||||
import { getUserByIdAndPassword } from './user'
|
||||
import { defaultSession, sessionOptions } from './session'
|
||||
|
||||
export async function logout() {
|
||||
const session = await getSession()
|
||||
session.destroy()
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
export async function getSession() {
|
||||
let session
|
||||
session = await getIronSession(cookies(), sessionOptions)
|
||||
|
||||
console.log('session:', session)
|
||||
if (!session.isLoggedIn) {
|
||||
session.isLoggedIn = defaultSession.isLoggedIn
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export async function login(formData) {
|
||||
const session = await getSession()
|
||||
|
||||
const userId = formData.get('id')
|
||||
const password = formData.get('password')
|
||||
|
||||
console.log('id:', userId)
|
||||
console.log('password:', password)
|
||||
|
||||
// const user = {
|
||||
// id: 1,
|
||||
// name: 'jinsoo Kim',
|
||||
// email: 'jinsoo.kim@example.com',
|
||||
// }
|
||||
const loginUser = await getUserByIdAndPassword({ userId, password })
|
||||
console.log('loginUser:', loginUser)
|
||||
|
||||
if (!loginUser) {
|
||||
throw Error('Wrong Credentials!')
|
||||
}
|
||||
|
||||
// session.id = user.id
|
||||
// session.email = user.email
|
||||
session.userId = loginUser.USER_ID
|
||||
session.saleStoreId = loginUser.SALE_STORE_ID
|
||||
session.name = loginUser.NAME
|
||||
session.mail = loginUser.MAIL
|
||||
session.isLoggedIn = true
|
||||
console.log('session:', session)
|
||||
|
||||
await session.save()
|
||||
redirect('/')
|
||||
}
|
||||
16
src/lib/session.js
Normal file
16
src/lib/session.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const defaultSession = {
|
||||
userId: null,
|
||||
saleStoreId: null,
|
||||
name: null,
|
||||
mail: null,
|
||||
isLoggedIn: false,
|
||||
}
|
||||
|
||||
export const sessionOptions = {
|
||||
password: process.env.SESSION_SECRET,
|
||||
cookieName: 'lama-session',
|
||||
cookieOptions: {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
}
|
||||
31
src/lib/user.js
Normal file
31
src/lib/user.js
Normal file
@ -0,0 +1,31 @@
|
||||
'use server'
|
||||
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export async function getUserByIdAndPassword({ userId, password }) {
|
||||
return prisma.m_USER.findFirst({
|
||||
where: {
|
||||
USER_ID: userId,
|
||||
PASSWORD: password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUser(userId) {
|
||||
return prisma.m_USER.findUnique({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUsers() {
|
||||
return prisma.m_USER.findMany({
|
||||
where: {
|
||||
// USER_ID: 'daiwajoho01',
|
||||
USER_ID: { in: ['daiwajoho01', 'daiwajoho', 'daiwabutsuryu'] },
|
||||
},
|
||||
})
|
||||
}
|
||||
18
src/locales/client.js
Normal file
18
src/locales/client.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { createI18nClient } from 'next-international/client'
|
||||
|
||||
export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defineLocale, useCurrentLocale } = createI18nClient(
|
||||
{
|
||||
ko: () => import('./ko'),
|
||||
ja: () => import('./ja'),
|
||||
},
|
||||
{
|
||||
// Uncomment to set base path
|
||||
// basePath: '/base',
|
||||
// Uncomment to use custom segment name
|
||||
// segmentName: 'locale',
|
||||
// Uncomment to set fallback locale
|
||||
// fallbackLocale: en,
|
||||
},
|
||||
)
|
||||
7
src/locales/ja.js
Normal file
7
src/locales/ja.js
Normal file
@ -0,0 +1,7 @@
|
||||
console.log('Loaded JA')
|
||||
|
||||
export default {
|
||||
hello: 'こんにちは',
|
||||
welcome: 'こんにちは {name}!',
|
||||
locale: '現在のロケールは {locale} です。',
|
||||
}
|
||||
7
src/locales/ko.js
Normal file
7
src/locales/ko.js
Normal file
@ -0,0 +1,7 @@
|
||||
console.log('Loaded KO')
|
||||
|
||||
export default {
|
||||
hello: '안녕',
|
||||
welcome: '안녕 {name}!',
|
||||
locale: '현재 로케일은 {locale}입니다.',
|
||||
}
|
||||
14
src/locales/server.js
Normal file
14
src/locales/server.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { createI18nServer } from 'next-international/server'
|
||||
|
||||
export const { getI18n, getScopedI18n, getCurrentLocale, getStaticParams } = createI18nServer(
|
||||
{
|
||||
ko: () => import('./ko'),
|
||||
ja: () => import('./ja'),
|
||||
},
|
||||
{
|
||||
// Uncomment to use custom segment name
|
||||
// segmentName: 'locale',
|
||||
// Uncomment to set fallback locale
|
||||
// fallbackLocale: en,
|
||||
},
|
||||
)
|
||||
@ -1,12 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { createI18nMiddleware } from 'next-international/middleware'
|
||||
|
||||
const I18nMiddleware = createI18nMiddleware({
|
||||
locales: ['ko', 'ja'],
|
||||
defaultLocale: 'ko',
|
||||
})
|
||||
|
||||
export function middleware(request) {
|
||||
const requestHeaders = new Headers(request.headers)
|
||||
requestHeaders.set('x-pathname', request.nextUrl.pathname)
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
})
|
||||
return I18nMiddleware(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)'],
|
||||
}
|
||||
|
||||
// import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// export function middleware(request) {
|
||||
// const requestHeaders = new Headers(request.headers)
|
||||
// requestHeaders.set('x-pathname', request.nextUrl.pathname)
|
||||
|
||||
// return NextResponse.next({
|
||||
// request: {
|
||||
// headers: requestHeaders,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
@ -50,7 +50,7 @@ export const roofPolygonArrayState = atom({
|
||||
|
||||
export const templateTypeState = atom({
|
||||
key: 'templateType',
|
||||
default: 1, //1:모임지붕, 2:A타입, 3:B타입
|
||||
default: 0, //1:모임지붕, 2:A타입, 3:B타입
|
||||
dangerouslyAllowMutability: true,
|
||||
})
|
||||
|
||||
@ -67,3 +67,9 @@ export const roofMaterialState = atom({
|
||||
default: { width: 20, height: 10, rafter: 0, roofStyle: 2 },
|
||||
dangerouslyAllowMutability: true,
|
||||
})
|
||||
|
||||
export const compassState = atom({
|
||||
key: 'compass',
|
||||
default: undefined,
|
||||
dangerouslyAllowMutability: true,
|
||||
})
|
||||
|
||||
15
src/store/modalAtom.js
Normal file
15
src/store/modalAtom.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { atom } from 'recoil'
|
||||
|
||||
export const modalState = atom({
|
||||
key: 'modalState',
|
||||
default: false,
|
||||
})
|
||||
|
||||
export const modalContent = atom({
|
||||
key: 'modalContent',
|
||||
default: (
|
||||
<>
|
||||
<div>test</div>
|
||||
</>
|
||||
),
|
||||
})
|
||||
@ -1 +1,2 @@
|
||||
@import '_test.scss';
|
||||
@import '_test.scss';
|
||||
@import 'react-toastify/dist/ReactToastify.css';
|
||||
@ -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
|
||||
export function anchorWrapper(anchorIndex, fn) {
|
||||
return function (eventData, transform, x, y) {
|
||||
return function(eventData, transform, x, y) {
|
||||
let fabricObject = transform.target
|
||||
let originX = fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
|
||||
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
|
||||
@ -241,6 +241,14 @@ export const getRoofHypotenuse = (base) => {
|
||||
return Math.sqrt(base * base * 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 빗변의 길이를 입력받아 밑변의 길이를 반환
|
||||
* @param base 빗변
|
||||
*/
|
||||
export const getAdjacent = (base) => {
|
||||
return Math.round(Math.sqrt(Math.pow(base, 2) / 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* 촌을 입력받아 각도를 반환
|
||||
* @param chon
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user