Merge branch 'dev'

This commit is contained in:
yoosangwook 2024-08-12 09:26:20 +09:00
commit efbf268886
58 changed files with 12248 additions and 1444 deletions

View File

@ -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="

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

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

View File

@ -0,0 +1,11 @@
'use client'
import { I18nProviderClient } from '@/locales/client'
export function LocaleProvider({ locale, children }) {
return (
<I18nProviderClient locale={locale} fallback={''}>
{children}
</I18nProviderClient>
)
}

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

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

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

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

View File

@ -0,0 +1,9 @@
import Login from '@/components/auth/Login'
export default function LoginPage() {
return (
<>
<Login />
</>
)
}

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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 ? '숨기기' : '보이기'}`}

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

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

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

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

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

View File

@ -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

View File

@ -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
View File

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

View File

@ -89,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 })

File diff suppressed because it is too large Load Diff

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

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

61
src/lib/authActions.js Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
console.log('Loaded JA')
export default {
hello: 'こんにちは',
welcome: 'こんにちは {name}!',
locale: '現在のロケールは {locale} です。',
}

7
src/locales/ko.js Normal file
View File

@ -0,0 +1,7 @@
console.log('Loaded KO')
export default {
hello: '안녕',
welcome: '안녕 {name}!',
locale: '현재 로케일은 {locale}입니다.',
}

14
src/locales/server.js Normal file
View 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,
},
)

View File

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

View File

@ -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
View File

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

View File

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

View File

@ -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

4387
yarn.lock

File diff suppressed because it is too large Load Diff