✨ frontend add
This commit is contained in:
8
frontend/portal/src/pages/404/index.tsx
Normal file
8
frontend/portal/src/pages/404/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import CustomErrorPage from '@components/Errors'
|
||||
|
||||
const Error404: React.FC = () => {
|
||||
return <CustomErrorPage statusCode={404} />
|
||||
}
|
||||
|
||||
export default Error404
|
||||
100
frontend/portal/src/pages/_app/index.tsx
Normal file
100
frontend/portal/src/pages/_app/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import Head from 'next/head'
|
||||
import { AppContext, AppProps } from 'next/app'
|
||||
import { appWithTranslation, useTranslation } from 'next-i18next'
|
||||
import '@libs/i18n'
|
||||
import { RecoilRoot } from 'recoil'
|
||||
import App from '@components/App'
|
||||
import GlobalStyles from '@components/App/GlobalStyles'
|
||||
import {
|
||||
BASE_URL,
|
||||
CUSTOM_HEADER_SITE_ID_KEY,
|
||||
DEFAULT_APP_NAME,
|
||||
} from '@constants'
|
||||
import { useLocalStorage } from '@hooks/useLocalStorage'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
import { ThemeProvider } from '@material-ui/core/styles'
|
||||
import theme from '@styles/theme'
|
||||
import { SITE_ID } from '@constants/env'
|
||||
import { CookiesProvider } from 'react-cookie'
|
||||
|
||||
// axios 기본 설정
|
||||
axios.defaults.headers.common[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
axios.defaults.baseURL = BASE_URL
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
const MyApp = ({ Component, pageProps }: AppProps) => {
|
||||
/**
|
||||
* locales
|
||||
*/
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const [storedValue, setValue] = useLocalStorage('locale', i18n.language)
|
||||
useEffect(() => {
|
||||
if (storedValue !== i18n.language) {
|
||||
i18n.changeLanguage(storedValue)
|
||||
}
|
||||
}, [i18n, storedValue])
|
||||
|
||||
useEffect(() => {
|
||||
// Remove the server-side injected CSS.
|
||||
const jssStyles = document.querySelector('#jss-server-side')
|
||||
if (jssStyles) {
|
||||
jssStyles.parentElement!.removeChild(jssStyles)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles>
|
||||
<Head>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico"></link>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="user-scalable=no, width=device-width, initial-scale=1"
|
||||
/>
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>{DEFAULT_APP_NAME}</title>
|
||||
</Head>
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
iconVariant={{
|
||||
success: '✅ ',
|
||||
error: '✖ ',
|
||||
warning: '⚠ ',
|
||||
info: 'ℹ️ ',
|
||||
}}
|
||||
autoHideDuration={2000}
|
||||
preventDuplicate={true}
|
||||
>
|
||||
<CookiesProvider>
|
||||
<App component={Component} {...pageProps} />
|
||||
</CookiesProvider>
|
||||
</SnackbarProvider>
|
||||
</GlobalStyles>
|
||||
</ThemeProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
}
|
||||
|
||||
MyApp.getInitialProps = async ({ Component, ctx, router }: AppContext) => {
|
||||
let pageProps = {}
|
||||
const locale = router.locale
|
||||
|
||||
axios.defaults.headers.common[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
|
||||
if (Component.getInitialProps) {
|
||||
const componentInitialProps = await Component.getInitialProps(ctx)
|
||||
if (componentInitialProps) {
|
||||
pageProps = componentInitialProps
|
||||
}
|
||||
}
|
||||
|
||||
global.__localeId__ = locale
|
||||
|
||||
return { pageProps }
|
||||
}
|
||||
|
||||
export default appWithTranslation(MyApp)
|
||||
52
frontend/portal/src/pages/_document/index.tsx
Normal file
52
frontend/portal/src/pages/_document/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { ServerStyleSheets } from '@material-ui/core/styles'
|
||||
import Document, {
|
||||
DocumentContext,
|
||||
Head,
|
||||
Html,
|
||||
Main,
|
||||
NextScript,
|
||||
} from 'next/document'
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
loadWindowProperty = locale => (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{ __html: `window.__localeId__= "${locale}"` }}
|
||||
></script>
|
||||
)
|
||||
render() {
|
||||
const { loadWindowProperty } = this
|
||||
const { locale } = this.props
|
||||
return (
|
||||
<Html lang={locale}>
|
||||
<Head />
|
||||
<body>
|
||||
{this.loadWindowProperty(locale)}
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
|
||||
const sheets = new ServerStyleSheets()
|
||||
const originalRenderPage = ctx.renderPage
|
||||
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: App => props => sheets.collect(<App {...props} />),
|
||||
})
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
// Styles fragment is rendered after the app and page rendering finish.
|
||||
styles: [
|
||||
...React.Children.toArray(initialProps.styles),
|
||||
sheets.getStyleElement(),
|
||||
],
|
||||
}
|
||||
}
|
||||
15
frontend/portal/src/pages/_error/index.tsx
Normal file
15
frontend/portal/src/pages/_error/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import { NextPageContext } from 'next'
|
||||
import CustomErrorPage from '@components/Errors'
|
||||
|
||||
const Error = ({ statusCode }) => {
|
||||
return <CustomErrorPage statusCode={statusCode} />
|
||||
}
|
||||
|
||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
|
||||
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
export default Error
|
||||
89
frontend/portal/src/pages/api/editor/index.ts
Normal file
89
frontend/portal/src/pages/api/editor/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { CUSTOM_HEADER_SITE_ID_KEY, EDITOR_LOAD_IMAGE_URL } from '@constants'
|
||||
import { SERVER_API_URL, SITE_ID } from '@constants/env'
|
||||
import axios from 'axios'
|
||||
import multer from 'multer'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
})
|
||||
|
||||
const initMiddleware = (middleware: any) => {
|
||||
return (req: NextApiRequest, res: NextApiResponse) =>
|
||||
new Promise((resolve, reject) => {
|
||||
middleware(req, res, result => {
|
||||
if (result instanceof Error) {
|
||||
return reject(result)
|
||||
}
|
||||
return resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// for parsing multipart/form-data
|
||||
// editor 요청인 경우 무조건 single임
|
||||
const multerAny = initMiddleware(upload.single('upload'))
|
||||
|
||||
type NextApiRequestWithFormData = NextApiRequest & {
|
||||
file: Express.Multer.File
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestWithFormData,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
await multerAny(req, res)
|
||||
|
||||
//첨부파일 base64 endoding -> 서버에서 decoding 필요
|
||||
if (req.file.size > 300000) {
|
||||
res.status(501).json({ message: 'File is too big!! 😵💫' })
|
||||
return
|
||||
}
|
||||
|
||||
const base64Encoding = req.file.buffer.toString('base64')
|
||||
|
||||
const body = {
|
||||
fieldName: req.file.fieldname,
|
||||
originalName: req.file.originalname,
|
||||
fileType: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
fileBase64: base64Encoding,
|
||||
}
|
||||
|
||||
//headers
|
||||
let editorHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
editorHeaders[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
|
||||
// const cookies = new Cookies(req, res)
|
||||
// const authToken = cookies.get(ACCESS_TOKEN)
|
||||
// // header에 authentication 추가
|
||||
// if (authToken) {
|
||||
// editorHeaders[CLAIM_NAME] = authToken
|
||||
// }
|
||||
|
||||
const result = await axios.post(
|
||||
`${SERVER_API_URL}/portal-service/api/v1/upload/editor`,
|
||||
body,
|
||||
{
|
||||
headers: editorHeaders,
|
||||
},
|
||||
)
|
||||
|
||||
let data = {}
|
||||
if (result) {
|
||||
data = {
|
||||
...result.data,
|
||||
url: `${SERVER_API_URL}${EDITOR_LOAD_IMAGE_URL}${result.data.url}`,
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(data)
|
||||
}
|
||||
4
frontend/portal/src/pages/api/health.ts
Normal file
4
frontend/portal/src/pages/api/health.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.status(200)
|
||||
}
|
||||
82
frontend/portal/src/pages/api/login/[...path].ts
Normal file
82
frontend/portal/src/pages/api/login/[...path].ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { CUSTOM_HEADER_SITE_ID_KEY, DEFAULT_ERROR_MESSAGE } from '@constants'
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from '@constants/env'
|
||||
import axios from 'axios'
|
||||
import Cookies from 'cookies'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import url from 'url'
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const pathname = url.pathToFileURL(req.url).pathname
|
||||
let isLogin = pathname === '/api/login/user-service/login'
|
||||
|
||||
req.url = req.url.replace(/^\/api\/login/, '')
|
||||
|
||||
if (pathname.indexOf('undefined') > -1) {
|
||||
res.status(500).json({ message: DEFAULT_ERROR_MESSAGE })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
headers[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
|
||||
//silent refresh
|
||||
if (pathname.indexOf('/refresh') > -1) {
|
||||
isLogin = true
|
||||
const cookies = new Cookies(req, res)
|
||||
headers[CLAIM_NAME] = cookies.get(REFRESH_TOKEN)
|
||||
|
||||
if (!headers[CLAIM_NAME] || headers[CLAIM_NAME] === '') {
|
||||
console.warn(`can't refresh`)
|
||||
res.status(401).json({ message: 'Invalid Credentials 🥺' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetch(`${SERVER_API_URL}${req.url}`, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.body,
|
||||
})
|
||||
|
||||
if (result) {
|
||||
const refreshToken = result.headers.get(REFRESH_TOKEN)
|
||||
const accessToken = result.headers.get(ACCESS_TOKEN)
|
||||
const userId = result.headers.get(AUTH_USER_ID)
|
||||
|
||||
const cookies = new Cookies(req, res)
|
||||
|
||||
cookies.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax', //CSRF protection
|
||||
})
|
||||
|
||||
if (accessToken) {
|
||||
let payload = {}
|
||||
payload[ACCESS_TOKEN] = accessToken
|
||||
payload[AUTH_USER_ID] = userId
|
||||
axios.defaults.headers.common[CLAIM_NAME] = accessToken
|
||||
axios.defaults.headers.common[AUTH_USER_ID] = userId
|
||||
|
||||
res.status(200).json(payload)
|
||||
} else {
|
||||
res.status(401).json({ message: 'Invalid Credentials 🥺' })
|
||||
}
|
||||
} else {
|
||||
res.status(401).json({ message: 'Invalid Credentials 🥺' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
res.status(500).json(error)
|
||||
}
|
||||
}
|
||||
53
frontend/portal/src/pages/api/v1/messages.ts
Normal file
53
frontend/portal/src/pages/api/v1/messages.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { CUSTOM_HEADER_SITE_ID_KEY } from '@constants'
|
||||
import { SERVER_API_URL, SITE_ID } from '@constants/env'
|
||||
import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
}
|
||||
|
||||
const MESSAGE_URL = `${SERVER_API_URL}/portal-service/api/v1/messages/`
|
||||
const locales = ['ko', 'en']
|
||||
const FILE_PATH = `public/locales/`
|
||||
|
||||
/**
|
||||
* messages reload
|
||||
*/
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let noResultLocales: string[] = []
|
||||
|
||||
req.headers[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
|
||||
for (const locale of locales) {
|
||||
try {
|
||||
const result = await axios.get(`${MESSAGE_URL}${locale}`, {
|
||||
headers: {
|
||||
...req.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (result) {
|
||||
const jsonstring = JSON.stringify(result.data)
|
||||
|
||||
await fs.writeFileSync(`${FILE_PATH}${locale}/common.json`, jsonstring)
|
||||
} else {
|
||||
noResultLocales.push(locale)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('catch error', error.message)
|
||||
noResultLocales.push(locale)
|
||||
}
|
||||
}
|
||||
|
||||
if (noResultLocales.length > 0) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: `Not Found Messages for ${noResultLocales.join(', ')}` })
|
||||
} else {
|
||||
res.status(200).json({ message: 'Success!!' })
|
||||
}
|
||||
}
|
||||
15
frontend/portal/src/pages/api/v1/token.ts
Normal file
15
frontend/portal/src/pages/api/v1/token.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { REFRESH_TOKEN } from '@constants/env'
|
||||
import Cookies from 'cookies'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
/**
|
||||
* refresh token 만료 시 쿠키 삭제
|
||||
*/
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const cookies = new Cookies(req, res)
|
||||
|
||||
// Delete the cookie by not setting a value
|
||||
cookies.set(REFRESH_TOKEN)
|
||||
|
||||
res.status(200).json({ message: 'success' })
|
||||
}
|
||||
244
frontend/portal/src/pages/auth/find/password/change.tsx
Normal file
244
frontend/portal/src/pages/auth/find/password/change.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { DLWrapper } from '@components/WriteDLFields'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { userService } from '@service'
|
||||
import { format, isValidPassword } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useState } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
alert: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}))
|
||||
|
||||
interface IUserPasswordForm {
|
||||
tokenValue: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
interface ChangePasswordProps {
|
||||
tokenValue: string | null
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
const ChangePassword = ({ tokenValue, valid }: ChangePasswordProps) => {
|
||||
const router = useRouter()
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const passwordRef = createRef<HTMLInputElement>()
|
||||
|
||||
const [changed, setChanged] = useState<boolean>(false)
|
||||
const [errorState, setErrorState] = useState<string | null>(null)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserPasswordForm>({
|
||||
defaultValues: {
|
||||
tokenValue,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
},
|
||||
})
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = methods
|
||||
|
||||
// 비밀번호 변경
|
||||
const handleChangePassword = async (data: IUserPasswordForm) => {
|
||||
userService
|
||||
.changePassword(data)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
setChanged(true)
|
||||
} else {
|
||||
setErrorState(t('err.internal.server'))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState(error.response.data.message || t('err.internal.server'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogin = event => {
|
||||
event.preventDefault()
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
const handleMain = event => {
|
||||
event.preventDefault()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="member">
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.change_password')}</h2>
|
||||
</article>
|
||||
{valid === true && changed === false && (
|
||||
<article className="pass">
|
||||
<form>
|
||||
<div className="table_write01">
|
||||
<span>{t('common.required_fields')}</span>
|
||||
<div className="write">
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('label.title.new_password')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
ref={passwordRef}
|
||||
type="password"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('label.title.new_password')}
|
||||
/>
|
||||
<span>{t('label.text.password_format')}</span>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value => {
|
||||
return (
|
||||
isValidPassword(value) ||
|
||||
(t('valid.password') as string)
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passwordConfirm"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('label.title.new_password_confirm')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('label.title.new_password_confirm')}
|
||||
/>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value => {
|
||||
return (
|
||||
(isValidPassword(value) &&
|
||||
passwordRef.current?.value === value) ||
|
||||
(t('valid.password.confirm') as string)
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errorState && (
|
||||
<Alert className={classes.alert} severity="warning">
|
||||
{errorState}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="btn_center">
|
||||
<button
|
||||
type="submit"
|
||||
className="blue"
|
||||
onClick={handleSubmit(handleChangePassword)}
|
||||
>
|
||||
{t('label.button.change')}
|
||||
</button>
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
)}
|
||||
{valid === true && changed === true && (
|
||||
<article>
|
||||
<div className="complete">
|
||||
<span className="reset">{t('label.text.changed_password')}</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" className="blue" onClick={handleLogin}>
|
||||
{t('common.login')}
|
||||
</a>
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
{valid === false && (
|
||||
<article>
|
||||
<div className="complete">
|
||||
<span className="reset">{t('err.user.change.password')}</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" className="blue" onClick={handleLogin}>
|
||||
{t('common.login')}
|
||||
</a>
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let tokenValue = context.query.token as string
|
||||
|
||||
let valid = false
|
||||
|
||||
try {
|
||||
if (tokenValue) {
|
||||
const result = await userService.getFindPassword(tokenValue)
|
||||
if (result && result.data) {
|
||||
valid = (await result.data) as boolean
|
||||
}
|
||||
} else {
|
||||
tokenValue = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`find-password item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
tokenValue,
|
||||
valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangePassword
|
||||
219
frontend/portal/src/pages/auth/find/password/index.tsx
Normal file
219
frontend/portal/src/pages/auth/find/password/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import ValidationAlert from '@components/ValidationAlert'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { userService } from '@service'
|
||||
import { format } from '@utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
alert: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
progressWrap: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
progress: {
|
||||
// display: 'flex',
|
||||
'& > * + *': {
|
||||
marginLeft: theme.spacing(2),
|
||||
},
|
||||
textAlign: 'center',
|
||||
marginTop: '320px',
|
||||
},
|
||||
}))
|
||||
|
||||
interface IUserPasswordForm {
|
||||
emailAddr: string
|
||||
userName: string
|
||||
mainUrl?: string
|
||||
changePasswordUrl?: string
|
||||
}
|
||||
|
||||
const FindPassword = () => {
|
||||
const router = useRouter()
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [requestChange, setRequestChange] = useState<boolean>(false)
|
||||
const [emailAddr, setEmailAddr] = useState<string>(null)
|
||||
const [errorState, setErrorState] = useState<string | null>(null)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserPasswordForm>({
|
||||
defaultValues: {
|
||||
emailAddr: '',
|
||||
userName: '',
|
||||
},
|
||||
})
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = methods
|
||||
|
||||
// 비밀번호 찾기
|
||||
const handleFind = async (data: IUserPasswordForm) => {
|
||||
if (requestChange === true) return // 메일 발송 완료까지 시간이 소요되어 비활성화 처리
|
||||
|
||||
data.mainUrl = window.location.origin
|
||||
data.changePasswordUrl =
|
||||
window.location.origin + '/auth/find/password/change'
|
||||
|
||||
setRequestChange(true)
|
||||
|
||||
userService
|
||||
.findPassword(data)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
setEmailAddr(data.emailAddr)
|
||||
} else {
|
||||
setErrorState(t('err.internal.server'))
|
||||
}
|
||||
setRequestChange(false)
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState(error.response.data.message)
|
||||
setRequestChange(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogin = event => {
|
||||
event.preventDefault()
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
const handleMain = event => {
|
||||
event.preventDefault()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{emailAddr === null && (
|
||||
<section className="login">
|
||||
<h2>{t('label.title.find_password')}</h2>
|
||||
<form>
|
||||
<fieldset>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userName"
|
||||
render={({ field, fieldState }) => (
|
||||
<input
|
||||
type="text"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('label.title.name')}
|
||||
/>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: format(t('valid.minlength.format'), [2]),
|
||||
},
|
||||
maxLength: {
|
||||
value: 25,
|
||||
message: format(t('valid.maxlength.format'), [25]),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{errors.userName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.userName}
|
||||
target={[25]}
|
||||
label={t('label.title.name')}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="emailAddr"
|
||||
render={({ field, fieldState }) => (
|
||||
<input
|
||||
type="text"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('user.email')}
|
||||
inputMode="email"
|
||||
maxLength={50}
|
||||
/>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: format(t('valid.maxlength.format'), [50]),
|
||||
},
|
||||
pattern: {
|
||||
value:
|
||||
/^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,
|
||||
message: t('valid.email.pattern'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{errors.emailAddr && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.emailAddr}
|
||||
target={[50]}
|
||||
label={t('user.emailAddr')}
|
||||
/>
|
||||
)}
|
||||
{errorState && (
|
||||
<Alert className={classes.alert} severity="warning">
|
||||
{errorState}
|
||||
</Alert>
|
||||
)}
|
||||
<button type="submit" onClick={handleSubmit(handleFind)}>
|
||||
{t('label.title.find_password')}
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
{emailAddr !== null && (
|
||||
<section className="member">
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.find_password')}</h2>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div className="complete">
|
||||
<span className="pass">
|
||||
{format(t('msg.user.find.password'), [emailAddr])}
|
||||
</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" className="blue" onClick={handleLogin}>
|
||||
{t('common.login')}
|
||||
</a>
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
)}
|
||||
{errorState && (
|
||||
<Alert className={classes.alert} severity="warning">
|
||||
{errorState}
|
||||
</Alert>
|
||||
)}
|
||||
{requestChange === true && (
|
||||
<div className={classes.progressWrap}>
|
||||
<div className={classes.progress}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FindPassword
|
||||
46
frontend/portal/src/pages/auth/join/complete.tsx
Normal file
46
frontend/portal/src/pages/auth/join/complete.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Complete = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleLogin = event => {
|
||||
event.preventDefault()
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
const handleMain = event => {
|
||||
event.preventDefault()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="member">
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.join')}</h2>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<div className="complete">
|
||||
<span>
|
||||
{t('label.text.join.complete1')}
|
||||
<br />
|
||||
{t('label.text.join.complete2')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" className="blue" onClick={handleLogin}>
|
||||
{t('common.login')}
|
||||
</a>
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Complete
|
||||
313
frontend/portal/src/pages/auth/join/form.tsx
Normal file
313
frontend/portal/src/pages/auth/join/form.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import CustomAlert, { CustomAlertPrpps } from '@components/CustomAlert'
|
||||
import { DLWrapper } from '@components/WriteDLFields'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { userService } from '@service'
|
||||
import { format, isValidPassword } from '@utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useState } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
alert: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}))
|
||||
|
||||
interface IUserForm {
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
userName: string
|
||||
}
|
||||
|
||||
const Form = () => {
|
||||
const router = useRouter()
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [errorState, setErrorState] = useState<string | null>(null)
|
||||
|
||||
const [checkedEmail, setCheckedEmail] = useState<boolean>(false)
|
||||
const emailRef = createRef<HTMLInputElement>()
|
||||
const passwordRef = createRef<HTMLInputElement>()
|
||||
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
} as CustomAlertPrpps)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserForm>({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
userName: '',
|
||||
},
|
||||
})
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = methods
|
||||
|
||||
const showMessage = (message: string, callback?: () => void) => {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message,
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
if (callback) callback()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 이메일중복확인
|
||||
const handleCheckEmail = event => {
|
||||
event.preventDefault()
|
||||
|
||||
const emailElement = emailRef.current
|
||||
const email = emailElement?.value
|
||||
|
||||
if (
|
||||
/^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i.test(
|
||||
email,
|
||||
) === false
|
||||
) {
|
||||
showMessage(t('valid.email.pattern'), () => {
|
||||
emailElement?.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userService
|
||||
.existsEmail(emailElement?.value)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
setCheckedEmail(false)
|
||||
showMessage(t('msg.user.email.exists'), () => {
|
||||
emailElement?.focus()
|
||||
})
|
||||
} else {
|
||||
setCheckedEmail(true)
|
||||
showMessage(t('msg.user.email.notexists'))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState(error.response.data.message || t('err.internal.server'))
|
||||
})
|
||||
}
|
||||
|
||||
// 가입
|
||||
const handleJoin = async (data: IUserForm) => {
|
||||
if (!checkedEmail) {
|
||||
showMessage(t('msg.user.email.check'), () => {
|
||||
emailRef.current?.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userService
|
||||
.join(data)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
router.push('/auth/join/complete')
|
||||
} else {
|
||||
setErrorState(t('err.internal.server'))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState(error.response.data.message || t('err.internal.server'))
|
||||
})
|
||||
}
|
||||
|
||||
// 취소
|
||||
const handleCancel = event => {
|
||||
event.preventDefault()
|
||||
router.back()
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="member">
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.join')}</h2>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<form>
|
||||
<div className="table_write01">
|
||||
<span>{t('label.title.required')}</span>
|
||||
<div className="write">
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('user.email')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="text"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('user.email')}
|
||||
inputMode="email"
|
||||
maxLength={50}
|
||||
onInput={event => {
|
||||
setCheckedEmail(false)
|
||||
}}
|
||||
/>
|
||||
<button type="button" onClick={handleCheckEmail}>
|
||||
{t('label.button.check_email')}
|
||||
</button>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: format(t('valid.maxlength.format'), [50]),
|
||||
},
|
||||
pattern: {
|
||||
value:
|
||||
/^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,
|
||||
message: t('valid.email.pattern'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('user.password')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
ref={passwordRef}
|
||||
type="password"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('user.password')}
|
||||
/>
|
||||
<span>{t('label.text.password_format')}</span>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value => {
|
||||
return (
|
||||
isValidPassword(value) || (t('valid.password') as string)
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passwordConfirm"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('label.title.password_confirm')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('label.title.password_confirm')}
|
||||
/>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value => {
|
||||
return (
|
||||
(isValidPassword(value) &&
|
||||
passwordRef.current?.value === value) ||
|
||||
(t('valid.password.confirm') as string)
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userName"
|
||||
render={({ field, fieldState }) => (
|
||||
<DLWrapper
|
||||
title={t('label.title.name')}
|
||||
className="inputTitle"
|
||||
required
|
||||
error={fieldState.error}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder={t('label.title.name')}
|
||||
/>
|
||||
</DLWrapper>
|
||||
)}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: format(t('valid.minlength.format'), [2]),
|
||||
},
|
||||
maxLength: {
|
||||
value: 25,
|
||||
message: format(t('valid.maxlength.format'), [25]),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errorState && (
|
||||
<Alert className={classes.alert} severity="warning">
|
||||
{errorState}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="btn_center">
|
||||
<a href="#" className="blue" onClick={handleSubmit(handleJoin)}>
|
||||
{t('label.button.join')}
|
||||
</a>
|
||||
<a href="#" onClick={handleCancel}>
|
||||
{t('label.button.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={customAlert.handleAlert}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
143
frontend/portal/src/pages/auth/join/index.tsx
Normal file
143
frontend/portal/src/pages/auth/join/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import CustomAlert, { CustomAlertPrpps } from '@components/CustomAlert'
|
||||
import { IPolicy, policyService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface IJoinProps {
|
||||
policyTOS?: IPolicy
|
||||
policyPP?: IPolicy
|
||||
}
|
||||
|
||||
const Join = ({ policyPP, policyTOS }: IJoinProps) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const agree1Ref = createRef<HTMLInputElement>()
|
||||
const agree2Ref = createRef<HTMLInputElement>()
|
||||
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
} as CustomAlertPrpps)
|
||||
|
||||
const handleCancel = event => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const handleNext = (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
const agree1 = agree1Ref.current
|
||||
if (agree1?.checked !== true) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: t('msg.join.agree1'),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
agree1?.focus() // visibility: hidden; 스타일 속성으로 인해 포커스 되지 않음
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const agree2 = agree1Ref.current
|
||||
if (agree2Ref.current?.checked !== true) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: t('msg.join.agree2'),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
agree2?.focus()
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/auth/join/form')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="member">
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.join')}</h2>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3>{t('label.title.agree1')}</h3>
|
||||
<div className="join01">
|
||||
<div>
|
||||
<p dangerouslySetInnerHTML={{ __html: policyTOS.contents }} />
|
||||
</div>
|
||||
<div className="check">
|
||||
<input ref={agree1Ref} type="radio" id="termsOK" name="terms" />
|
||||
<label htmlFor="termsOK">{t('common.agree.y')}</label>
|
||||
<input type="radio" id="termsNO" name="terms" />
|
||||
<label htmlFor="termsNO">{t('common.agree.n')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{t('label.title.agree2')}</h3>
|
||||
<div className="join01">
|
||||
<div>
|
||||
<p dangerouslySetInnerHTML={{ __html: policyPP.contents }} />
|
||||
</div>
|
||||
<div className="check">
|
||||
<input ref={agree2Ref} type="radio" id="infoOK" name="info" />
|
||||
<label htmlFor="infoOK">{t('common.agree.y')}</label>
|
||||
<input type="radio" id="infoNO" name="info" />
|
||||
<label htmlFor="infoNO">{t('common.agree.n')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" onClick={handleCancel}>
|
||||
{t('label.button.cancel')}
|
||||
</a>
|
||||
<a href="#" className="blue" onClick={handleNext}>
|
||||
{t('label.button.next')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={customAlert.handleAlert}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let policyTOS = {}
|
||||
let policyPP = {}
|
||||
|
||||
try {
|
||||
const resultTOS = await policyService.getLatest('TOS')
|
||||
if (resultTOS && resultTOS.data) {
|
||||
policyTOS = (await resultTOS.data) as IPolicy
|
||||
}
|
||||
const resultPP = await policyService.getLatest('PP')
|
||||
if (resultPP && resultPP.data) {
|
||||
policyPP = (await resultPP.data) as IPolicy
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
policyTOS,
|
||||
policyPP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Join
|
||||
117
frontend/portal/src/pages/auth/login/index.tsx
Normal file
117
frontend/portal/src/pages/auth/login/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import ActiveLink from '@components/ActiveLink'
|
||||
import { LoginForm, loginFormType } from '@components/Auth'
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import Loader from '@components/Loader'
|
||||
import useUser from '@hooks/useUser'
|
||||
import { ILogin, loginSerivce } from '@service'
|
||||
import { userAtom } from '@stores'
|
||||
import Router from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { isLogin, mutate } = useUser()
|
||||
const user = useRecoilValue(userAtom)
|
||||
const [errorState, setErrorState] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLogin && user) {
|
||||
Router.replace('/')
|
||||
}
|
||||
}, [isLogin, user])
|
||||
|
||||
if (isLogin) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
// 로그인 처리
|
||||
const login = async (data: ILogin) => {
|
||||
try {
|
||||
const result = await loginSerivce.login(data)
|
||||
|
||||
if (result === 'success') {
|
||||
mutate()
|
||||
} else {
|
||||
setErrorState(result)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState(t('err.user.login'))
|
||||
}
|
||||
}
|
||||
|
||||
// 이메일 로그인
|
||||
const handleLoginSubmit = async (form: loginFormType) => {
|
||||
await login({ provider: 'email', ...form })
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = async response => {
|
||||
if (response.response?.access_token) {
|
||||
setErrorState(null)
|
||||
await login({
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
})
|
||||
} else {
|
||||
setErrorState('noAuth')
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.accessToken) {
|
||||
setErrorState(null)
|
||||
await login({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
})
|
||||
} else {
|
||||
setErrorState('noAuth')
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = async response => {
|
||||
if (response.tokenId) {
|
||||
setErrorState(null)
|
||||
await login({
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
})
|
||||
} else {
|
||||
setErrorState('noAuth')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="login">
|
||||
<h2>{t('common.login')}</h2>
|
||||
<LoginForm handleLogin={handleLoginSubmit} errorMessage={errorState} />
|
||||
<div>
|
||||
<ActiveLink href="/auth/join">{t('common.join')}</ActiveLink>
|
||||
<ActiveLink href="/auth/find/password">
|
||||
{t('login.password_find')}
|
||||
</ActiveLink>
|
||||
</div>
|
||||
<article>
|
||||
<h3>
|
||||
<span>{t('label.title.login.oauth')}</span>
|
||||
</h3>
|
||||
<div>
|
||||
<KakaoLoginButton handleClick={handleKakaoLogin} />
|
||||
<NaverLoginButton handleClick={handleNaverLogin} />
|
||||
<GoogleLoginButton handleClick={handleGoogleLogin} />
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
20
frontend/portal/src/pages/auth/login/naver.tsx
Normal file
20
frontend/portal/src/pages/auth/login/naver.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { NaverLoginButton } from '@components/Buttons'
|
||||
|
||||
const LoginNaver = () => {
|
||||
/* const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (router.asPath) {
|
||||
const token = router.asPath.split('=')[1].split('&')[0]
|
||||
window.opener.naver.successCallback(token)
|
||||
window.close()
|
||||
}
|
||||
}, [router.asPath])
|
||||
|
||||
return <></> */
|
||||
|
||||
return <NaverLoginButton handleClick={() => {}} />
|
||||
}
|
||||
|
||||
export default LoginNaver
|
||||
42
frontend/portal/src/pages/auth/logout/index.tsx
Normal file
42
frontend/portal/src/pages/auth/logout/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from '@constants/env'
|
||||
import axios from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
function Logout() {
|
||||
useEffect(() => {
|
||||
axios.defaults.headers.common[CLAIM_NAME] = ''
|
||||
axios.defaults.headers.common[AUTH_USER_ID] = ''
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a href="/auth/logout">Logout</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
if (!process.browser) {
|
||||
const Cookies = require('cookies')
|
||||
const cookies = new Cookies(req, res)
|
||||
|
||||
// Delete the cookie by not setting a value
|
||||
cookies.set(REFRESH_TOKEN)
|
||||
axios.defaults.headers.common[CLAIM_NAME] = ''
|
||||
axios.defaults.headers.common[AUTH_USER_ID] = ''
|
||||
|
||||
const { redirect } = req['query']
|
||||
|
||||
res.writeHead(307, {
|
||||
Location: typeof redirect !== 'undefined' ? redirect : '/',
|
||||
})
|
||||
res.end()
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
export default Logout
|
||||
195
frontend/portal/src/pages/board/[skin]/[board]/edit/[id].tsx
Normal file
195
frontend/portal/src/pages/board/[skin]/[board]/edit/[id].tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { NormalEditForm } from '@components/EditForm'
|
||||
import {
|
||||
boardService,
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
IBoard,
|
||||
IPosts,
|
||||
IPostsForm,
|
||||
PostsReqPayload,
|
||||
SKINT_TYPE_CODE_NORMAL,
|
||||
SKINT_TYPE_CODE_QNA,
|
||||
} from '@service'
|
||||
import { errorStateSelector } from '@stores'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
interface BoardEditProps {
|
||||
post: IPosts
|
||||
board: IBoard
|
||||
}
|
||||
|
||||
export const BoardFormContext = createContext<{
|
||||
post: IPostsForm
|
||||
board: IBoard
|
||||
attachList: IAttachmentResponse[]
|
||||
setPostDataHandler: (data: IPostsForm) => void
|
||||
setAttachListHandler: (data: IAttachmentResponse[]) => void
|
||||
}>({
|
||||
post: undefined,
|
||||
board: undefined,
|
||||
attachList: undefined,
|
||||
setPostDataHandler: () => {},
|
||||
setAttachListHandler: () => {},
|
||||
})
|
||||
|
||||
const BoardEdit = (props: BoardEditProps) => {
|
||||
const { post, board } = props
|
||||
const router = useRouter()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [postData, setPostData] = useState<IPostsForm>(undefined)
|
||||
const setPostDataHandler = (data: IPostsForm) => {
|
||||
setPostData(data)
|
||||
}
|
||||
const [attachList, setAttachList] = useState<IAttachmentResponse[]>(undefined)
|
||||
const setAttachListHandler = (data: IAttachmentResponse[]) => {
|
||||
setAttachList(data)
|
||||
}
|
||||
|
||||
// callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
const successCallback = useCallback(() => {
|
||||
router.back()
|
||||
}, [])
|
||||
|
||||
const save = useCallback(() => {
|
||||
const data: IPosts = {
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
...postData,
|
||||
}
|
||||
|
||||
if (post.postsNo === -1) {
|
||||
boardService.savePost({
|
||||
boardNo: post.boardNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
boardService.updatePost({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data,
|
||||
})
|
||||
}
|
||||
// boardService.
|
||||
}, [postData, post, successCallback, errorCallback])
|
||||
|
||||
useEffect(() => {
|
||||
if (postData) {
|
||||
save()
|
||||
}
|
||||
}, [postData, attachList])
|
||||
|
||||
useEffect(() => {
|
||||
if (post.attachmentCode) {
|
||||
const getAttachments = async () => {
|
||||
try {
|
||||
const result = await fileService.getAttachmentList(
|
||||
post.attachmentCode,
|
||||
)
|
||||
if (result?.data) {
|
||||
setAttachList(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
getAttachments()
|
||||
}
|
||||
|
||||
return () => setAttachList(null)
|
||||
}, [post])
|
||||
|
||||
return (
|
||||
<div className="qnaWrite">
|
||||
<div className="table_write01">
|
||||
<span>{t('common.required_fields')}</span>
|
||||
<BoardFormContext.Provider
|
||||
value={{
|
||||
post,
|
||||
board,
|
||||
attachList,
|
||||
setPostDataHandler,
|
||||
setAttachListHandler,
|
||||
}}
|
||||
>
|
||||
{board.skinTypeCode === SKINT_TYPE_CODE_NORMAL && (
|
||||
<NormalEditForm post={post} />
|
||||
)}
|
||||
{board.skinTypeCode === SKINT_TYPE_CODE_QNA && (
|
||||
<NormalEditForm post={post} />
|
||||
)}
|
||||
{/* <QnAEditForm /> */}
|
||||
</BoardFormContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const boardNo = Number(context.query.board)
|
||||
const postsNo = Number(context.query.id)
|
||||
const { keywordType, keyword } = context.query
|
||||
|
||||
let board = {}
|
||||
let post = {}
|
||||
|
||||
try {
|
||||
if (postsNo !== -1) {
|
||||
const result = await boardService.getPostById({
|
||||
boardNo,
|
||||
postsNo,
|
||||
keywordType,
|
||||
keyword,
|
||||
} as PostsReqPayload)
|
||||
if (result && result.data && result.data.board) {
|
||||
board = (await result.data.board) as IBoard
|
||||
post = (await result.data) as IPosts
|
||||
}
|
||||
} else {
|
||||
const result = await boardService.getBoardById(boardNo)
|
||||
if (result && result.data) {
|
||||
board = (await result.data) as IBoard
|
||||
post = {
|
||||
boardNo,
|
||||
postsNo,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
board,
|
||||
post,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default BoardEdit
|
||||
149
frontend/portal/src/pages/board/[skin]/[board]/index.tsx
Normal file
149
frontend/portal/src/pages/board/[skin]/[board]/index.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { FAQBaordList, NormalBoardList } from '@components/BoardList'
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import usePage from '@hooks/usePage'
|
||||
import { boardService, IBoard } from '@service'
|
||||
import { conditionAtom, userAtom } from '@stores'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
|
||||
interface BoardProps {
|
||||
board: IBoard
|
||||
}
|
||||
|
||||
const Board = ({ board }: BoardProps) => {
|
||||
const router = useRouter()
|
||||
const { query } = router
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
|
||||
const conditionKey = useMemo(() => {
|
||||
if (query) {
|
||||
return `board-${query.board}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, [query])
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
const [pageSize, setPageSize] = useState<number>(board?.postDisplayCount)
|
||||
|
||||
const { data, mutate } = boardService.search(
|
||||
parseInt(query?.board as string, 10),
|
||||
{
|
||||
keywordType: keywordState?.keywordType || 'postsTitle',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: pageSize,
|
||||
page,
|
||||
},
|
||||
)
|
||||
|
||||
const handlePageSize = (size: number) => {
|
||||
setPageSize(size)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtons = useMemo(
|
||||
(): IButtons[] => [
|
||||
{
|
||||
id: 'regist',
|
||||
title: t('label.button.reg'),
|
||||
href: `${router.asPath}/edit/-1`,
|
||||
className: 'blue',
|
||||
},
|
||||
],
|
||||
[t, router.asPath],
|
||||
)
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
const handleChangePage = (
|
||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||
_page: number,
|
||||
) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={query?.skin === 'faq' ? 'qna_list' : 'table_list01'}>
|
||||
{query?.skin === 'normal' && (
|
||||
<NormalBoardList
|
||||
data={data}
|
||||
pageSize={pageSize}
|
||||
handlePageSize={handlePageSize}
|
||||
handleSearch={handleSearch}
|
||||
conditionKey={conditionKey}
|
||||
page={page}
|
||||
handlePageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
{query?.skin === 'faq' && (
|
||||
<FAQBaordList
|
||||
data={data}
|
||||
pageSize={pageSize}
|
||||
page={page}
|
||||
handleChangePage={handleChangePage}
|
||||
/>
|
||||
)}
|
||||
{query?.skin === 'qna' && (
|
||||
/* <QnABaordList
|
||||
data={data}
|
||||
pageSize={pageSize}
|
||||
page={page}
|
||||
handleChangePage={handleChangePage}
|
||||
/> */
|
||||
<NormalBoardList
|
||||
data={data}
|
||||
pageSize={pageSize}
|
||||
handlePageSize={handlePageSize}
|
||||
handleSearch={handleSearch}
|
||||
conditionKey={conditionKey}
|
||||
page={page}
|
||||
handlePageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
{user && board.userWriteAt === true && (
|
||||
<BottomButtons handleButtons={handleButtons} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const boardNo = Number(context.query.board)
|
||||
|
||||
let board = {}
|
||||
|
||||
try {
|
||||
const result = await boardService.getBoardById(boardNo)
|
||||
if (result) {
|
||||
board = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`board query error : ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
board,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Board
|
||||
411
frontend/portal/src/pages/board/[skin]/[board]/view/[id].tsx
Normal file
411
frontend/portal/src/pages/board/[skin]/[board]/view/[id].tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import AttachList from '@components/AttachList'
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import {
|
||||
CommentsList,
|
||||
EditComments,
|
||||
EditCommentsType,
|
||||
} from '@components/Comments'
|
||||
import { COMMENTS_PAGE_SIZE } from '@constants'
|
||||
import { format as dateFormat } from '@libs/date'
|
||||
import {
|
||||
boardService,
|
||||
CommentSavePayload,
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
IBoard,
|
||||
ICommentSearchProps,
|
||||
IPosts,
|
||||
PostsReqPayload,
|
||||
SKINT_TYPE_CODE_NORMAL,
|
||||
SKINT_TYPE_CODE_QNA,
|
||||
} from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, {
|
||||
createRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
interface BaordViewProps {
|
||||
post: IPosts
|
||||
board: IBoard
|
||||
}
|
||||
|
||||
const BoardView = (props: BaordViewProps) => {
|
||||
const { post, board } = props
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
|
||||
const replyRef = createRef<EditCommentsType>()
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 첨부파일
|
||||
const [attachList, setAttachList] = useState<IAttachmentResponse[]>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (post.attachmentCode) {
|
||||
const getAttachments = async () => {
|
||||
try {
|
||||
const result = await fileService.getAttachmentList(
|
||||
post.attachmentCode,
|
||||
)
|
||||
if (result?.data) {
|
||||
setAttachList(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
getAttachments()
|
||||
}
|
||||
|
||||
return () => setAttachList(null)
|
||||
}, [post, setErrorState])
|
||||
|
||||
const [page, setPage] = useState<number>(undefined)
|
||||
const [totalPages, setTotalPages] = useState<number>(0)
|
||||
const [commentCount, setCommentCount] = useState<number>(undefined)
|
||||
|
||||
const [comments, setComments] = useState<CommentSavePayload[]>(undefined)
|
||||
|
||||
// 댓글 데이터 복사본 리턴
|
||||
const cloneComments = useCallback(
|
||||
() => comments.slice(0, comments.length),
|
||||
[comments],
|
||||
)
|
||||
|
||||
// 페이지 조회
|
||||
const getComments = useCallback(
|
||||
({ boardNo, postsNo, _page, _mode }: ICommentSearchProps) => {
|
||||
let searchPage = _page
|
||||
let searchSize = COMMENTS_PAGE_SIZE
|
||||
if (_mode === 'until') {
|
||||
searchSize = COMMENTS_PAGE_SIZE * (_page + 1)
|
||||
searchPage = 0
|
||||
}
|
||||
|
||||
boardService
|
||||
.getComments(boardNo, postsNo, searchSize, searchPage)
|
||||
.then(result => {
|
||||
setPage(_page)
|
||||
// setTotalPages(result.totalPages)
|
||||
setTotalPages(Math.ceil(result.groupElements / COMMENTS_PAGE_SIZE))
|
||||
setCommentCount(result.totalElements)
|
||||
|
||||
let arr = _mode === 'append' ? cloneComments() : []
|
||||
arr.push(...result.content)
|
||||
|
||||
setComments(arr)
|
||||
})
|
||||
},
|
||||
[cloneComments],
|
||||
)
|
||||
|
||||
// 전체 조회
|
||||
const getAllComments = () => {
|
||||
boardService.getAllComments(post.boardNo, post.postsNo).then(result => {
|
||||
setPage(result.number)
|
||||
setTotalPages(result.totalPages)
|
||||
setCommentCount(result.totalElements)
|
||||
|
||||
let arr = []
|
||||
arr.push(...result.content)
|
||||
|
||||
setComments(arr)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
getComments({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
_page: 0,
|
||||
_mode: 'replace',
|
||||
})
|
||||
}
|
||||
}, [post])
|
||||
|
||||
// 댓글 등록
|
||||
const handleCommentRegist = (comment: CommentSavePayload) => {
|
||||
if (comment.commentNo > 0) {
|
||||
boardService.updateComment(comment).then(() => {
|
||||
getComments({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
_page: page,
|
||||
_mode: 'until',
|
||||
}) // 현재 페이지까지 재조회
|
||||
})
|
||||
} else {
|
||||
boardService.saveComment(comment).then(() => {
|
||||
if (comment.parentCommentNo) {
|
||||
getComments({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
_page: page,
|
||||
_mode: 'until',
|
||||
}) // 현재 페이지까지 재조회
|
||||
} else {
|
||||
getAllComments() // 마지막 페이지까지 조회
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 댓글 삭제
|
||||
const handleCommentDelete = (comment: CommentSavePayload) => {
|
||||
boardService.deleteComment(comment).then(() => {
|
||||
getComments({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
_page: page,
|
||||
_mode: 'until',
|
||||
}) // 현재 페이지까지 재조회
|
||||
})
|
||||
}
|
||||
|
||||
const handleCommentCancel = () => {
|
||||
replyRef.current?.clear()
|
||||
}
|
||||
|
||||
const handleCommentMore = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault()
|
||||
getComments({
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
_page: page + 1,
|
||||
_mode: 'append',
|
||||
})
|
||||
}
|
||||
|
||||
// 이전글, 다음글 클릭
|
||||
const handleNearPostClick = nearPost => {
|
||||
router.push(
|
||||
`/board/${router.query.skin}/${router.query.board}/view/${nearPost.postsNo}?size=${router.query.size}&page=${router.query.page}&keywordType=${router.query.keywordType}&keyword=${router.query.keyword}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo((): IButtons[] => {
|
||||
const buttons: IButtons[] = [
|
||||
{
|
||||
id: 'board-list-button',
|
||||
title: t('label.button.list'),
|
||||
href: `/board/${router.query.skin}/${router.query.board}`,
|
||||
},
|
||||
]
|
||||
|
||||
if (user && board.userWriteAt && post.createdBy === user.userId) {
|
||||
buttons.push({
|
||||
id: 'board-edit-button',
|
||||
title: t('label.button.edit'),
|
||||
href: router.asPath.replace('view', 'edit'),
|
||||
className: 'blue',
|
||||
})
|
||||
return buttons.reverse()
|
||||
}
|
||||
|
||||
return buttons
|
||||
}, [
|
||||
t,
|
||||
router.query.skin,
|
||||
router.query.board,
|
||||
router.asPath,
|
||||
user,
|
||||
board.userWriteAt,
|
||||
post.createdBy,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="table_view01">
|
||||
<h4>
|
||||
{post.noticeAt ? `[${t('common.notice')}] ` : ''}
|
||||
{post.postsTitle}
|
||||
</h4>
|
||||
<div className="view">
|
||||
<div className="top">
|
||||
<dl>
|
||||
<dt>{t('common.written_by')}</dt>
|
||||
<dd>{post.createdName}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{t('common.written_date')}</dt>
|
||||
<dd>{dateFormat(new Date(post.createdDate), 'yyyy-MM-dd')}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{t('common.read_count')}</dt>
|
||||
<dd>{post.readCount}</dd>
|
||||
</dl>
|
||||
{board.uploadUseAt && (
|
||||
<dl className="file">
|
||||
<dt>{t('common.attachment')}</dt>
|
||||
<dd>
|
||||
<AttachList
|
||||
data={attachList}
|
||||
setData={setAttachList}
|
||||
readonly
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
{board.skinTypeCode === SKINT_TYPE_CODE_QNA && (
|
||||
<div className="qna-box">
|
||||
<div className="qna-question">
|
||||
<p
|
||||
className="qna-content"
|
||||
dangerouslySetInnerHTML={{ __html: post.postsContent }}
|
||||
/>
|
||||
</div>
|
||||
<div className="qna-answer">
|
||||
<p
|
||||
className="qna-content"
|
||||
dangerouslySetInnerHTML={{ __html: post.postsAnswerContent }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{board.skinTypeCode !== SKINT_TYPE_CODE_QNA && (
|
||||
<div
|
||||
className="content"
|
||||
dangerouslySetInnerHTML={{ __html: post.postsContent }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{board.commentUseAt && (
|
||||
<div className="commentWrap">
|
||||
<dl>
|
||||
<dt>{t('comment')}</dt>
|
||||
<dd>{commentCount}</dd>
|
||||
</dl>
|
||||
{user && (
|
||||
<EditComments
|
||||
ref={replyRef}
|
||||
handleCancel={handleCommentCancel}
|
||||
handleRegist={handleCommentRegist}
|
||||
comment={
|
||||
{
|
||||
boardNo: post.boardNo,
|
||||
postsNo: post.postsNo,
|
||||
depthSeq: 0,
|
||||
} as CommentSavePayload
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{comments?.length > 0 ? (
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
handleRegist={handleCommentRegist}
|
||||
handleDelete={handleCommentDelete}
|
||||
/>
|
||||
) : null}
|
||||
{page + 1 >= totalPages ? null : (
|
||||
<a href="#" onClick={handleCommentMore}>
|
||||
{t('posts.see_more')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{board.skinTypeCode === SKINT_TYPE_CODE_NORMAL && (
|
||||
<div className="skip">
|
||||
<dl>
|
||||
<dt>{t('posts.prev_post')}</dt>
|
||||
<dd>
|
||||
{(!post.prevPosts[0] || post.prevPosts.length === 0) && (
|
||||
<span>{t('posts.notexists.prev')}</span>
|
||||
)}
|
||||
{post.prevPosts[0] && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
handleNearPostClick(post.prevPosts[0])
|
||||
}}
|
||||
>
|
||||
{post.prevPosts[0].postsTitle}
|
||||
</a>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl className="next">
|
||||
<dt>{t('posts.next_post')}</dt>
|
||||
<dd>
|
||||
{(!post.nextPosts[0] || post.nextPosts.length === 0) && (
|
||||
<span>{t('posts.notexists.next')}</span>
|
||||
)}
|
||||
{post.nextPosts[0] && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
handleNearPostClick(post.nextPosts[0])
|
||||
}}
|
||||
>
|
||||
{post.nextPosts[0].postsTitle}
|
||||
</a>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
<BottomButtons handleButtons={bottomButtons} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const boardNo = Number(context.query.board)
|
||||
const postsNo = Number(context.query.id)
|
||||
const { keywordType, keyword } = context.query
|
||||
|
||||
let board = {}
|
||||
let post = {}
|
||||
|
||||
try {
|
||||
if (postsNo !== -1) {
|
||||
const result = await boardService.getPostById({
|
||||
boardNo,
|
||||
postsNo,
|
||||
keywordType,
|
||||
keyword,
|
||||
} as PostsReqPayload)
|
||||
if (result && result.data && result.data.board) {
|
||||
board = (await result.data.board) as IBoard
|
||||
post = (await result.data) as IPosts
|
||||
}
|
||||
} else {
|
||||
const result = await boardService.getBoardById(boardNo)
|
||||
if (result && result.data) {
|
||||
board = (await result.data) as IBoard
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
board,
|
||||
post,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default BoardView
|
||||
55
frontend/portal/src/pages/content/[id].tsx
Normal file
55
frontend/portal/src/pages/content/[id].tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import Editor from '@components/Editor'
|
||||
import { contentService, IContent } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export interface IContentItemsProps {
|
||||
content: IContent | null
|
||||
}
|
||||
|
||||
const Content = ({ content }: IContentItemsProps) => {
|
||||
const [contents, setContents] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
setContents(content.contentValue)
|
||||
}
|
||||
}, [content])
|
||||
return (
|
||||
<Editor contents={contents} setContents={setContents} readonly={true} />
|
||||
)
|
||||
// return (
|
||||
// <article className="intro">
|
||||
// <div dangerouslySetInnerHTML={{ __html: content.contentValue }} />
|
||||
// </article>
|
||||
// )
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const { query } = context
|
||||
const contentNo = Number(query.id)
|
||||
|
||||
let content = {}
|
||||
|
||||
try {
|
||||
const result = await contentService.get(contentNo)
|
||||
if (result) {
|
||||
content = (await result.data) as IContent
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`content item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Content
|
||||
71
frontend/portal/src/pages/index.tsx
Normal file
71
frontend/portal/src/pages/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
bannerTypeCodes,
|
||||
boardNos,
|
||||
MainLG,
|
||||
MainSM,
|
||||
slideCount,
|
||||
} from '@components/Main'
|
||||
import { MODE } from '@constants/env'
|
||||
import {
|
||||
bannerService,
|
||||
boardService,
|
||||
IMainBanner,
|
||||
IMainBoard,
|
||||
IMainItem,
|
||||
reserveService,
|
||||
} from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
export interface IHomeItemsProps {
|
||||
banners: IMainBanner | null
|
||||
boards: IMainBoard | null
|
||||
reserveItems: IMainItem | null
|
||||
}
|
||||
|
||||
const Home = ({ banners, boards, reserveItems }: IHomeItemsProps) => {
|
||||
return (
|
||||
<>
|
||||
{MODE === 'sm' ? (
|
||||
<MainSM banners={banners} boards={boards} />
|
||||
) : (
|
||||
<MainLG banners={banners} boards={boards} reserveItems={reserveItems} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
let banners = {}
|
||||
let boards = {}
|
||||
let reserveItems = {}
|
||||
try {
|
||||
const banner = await bannerService.getBanners(bannerTypeCodes, slideCount)
|
||||
if (banner) {
|
||||
banners = (await banner.data) as IMainBanner
|
||||
}
|
||||
const result = await boardService.getMainPosts(boardNos, slideCount)
|
||||
if (result) {
|
||||
boards = (await result.data) as IMainBoard
|
||||
}
|
||||
|
||||
if (MODE === 'lg') {
|
||||
const items = await reserveService.getMainItems(slideCount)
|
||||
if (items) {
|
||||
reserveItems = items.data as IMainItem
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
banners,
|
||||
boards,
|
||||
reserveItems,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Home
|
||||
71
frontend/portal/src/pages/privacy/index.tsx
Normal file
71
frontend/portal/src/pages/privacy/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IPrivacy, privacyService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface IPrivacyItemsProps {
|
||||
privacies: IPrivacy[] | null
|
||||
}
|
||||
|
||||
const Privacy = ({ privacies }: IPrivacyItemsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [privacyIndex, setPrivacyIndex] = useState<number>(0)
|
||||
const privacySelectRef = useRef<any>(null)
|
||||
|
||||
const handleSearch = () => {
|
||||
setPrivacyIndex(privacySelectRef.current?.selectedIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<article className="rocation">
|
||||
<h2>{t('privacy')}</h2>
|
||||
</article>
|
||||
<article className="privacy">
|
||||
<fieldset>
|
||||
<select title={t('common.select')} ref={privacySelectRef}>
|
||||
{privacies &&
|
||||
privacies.map((p, i) => (
|
||||
<option key={`privacy-${p.privacyNo}`} value={i}>
|
||||
{p.privacyTitle}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={handleSearch}>{t('common.search')}</button>
|
||||
</fieldset>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: privacies[privacyIndex]?.privacyContent,
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
let privacies = {}
|
||||
|
||||
try {
|
||||
const result = await privacyService.alluse()
|
||||
if (result) {
|
||||
privacies = (await result.data) as IPrivacy[]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`privacy item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
privacies,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Privacy
|
||||
49
frontend/portal/src/pages/reload/index.tsx
Normal file
49
frontend/portal/src/pages/reload/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
initialLoginStatus: string
|
||||
}
|
||||
|
||||
function Reload(props: Props) {
|
||||
const [reloadState, setReloadSteate] = useState<{
|
||||
message: string
|
||||
severity: 'success' | 'info' | 'error' | 'warning'
|
||||
}>({
|
||||
message: 'reload message!!',
|
||||
severity: 'info',
|
||||
})
|
||||
|
||||
const onClickReload = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
fetch('/api/v1/messages')
|
||||
.then(async response => {
|
||||
const result = await response.json()
|
||||
if (response.ok) {
|
||||
setReloadSteate({
|
||||
message: result.message,
|
||||
severity: 'success',
|
||||
})
|
||||
} else {
|
||||
setReloadSteate({
|
||||
message: result.message,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setReloadSteate({
|
||||
message: error.message,
|
||||
severity: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{reloadState.message}</span>
|
||||
<button onClick={onClickReload}>Reload</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Reload
|
||||
103
frontend/portal/src/pages/reserve/[category]/[id].tsx
Normal file
103
frontend/portal/src/pages/reserve/[category]/[id].tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import {
|
||||
ReserveComplete,
|
||||
ReserveEdit,
|
||||
ReserveItemAdditional,
|
||||
ReserveItemInfo,
|
||||
} from '@components/Reserve'
|
||||
import { IReserveItem, reserveService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface ReserveDetailProps {
|
||||
initData: IReserveItem
|
||||
}
|
||||
|
||||
export interface IReserveComplete {
|
||||
done: boolean
|
||||
reserveId?: string
|
||||
}
|
||||
|
||||
const ReserveDetail = ({ initData }: ReserveDetailProps) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [edit, setEdit] = useState<boolean>(false)
|
||||
const [complete, setComplete] = useState<IReserveComplete>({ done: false })
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo((): IButtons[] => {
|
||||
const buttons: IButtons[] = [
|
||||
{
|
||||
id: 'item-list-button',
|
||||
title: t('label.button.list'),
|
||||
href: `/reserve/${router.query.category}`,
|
||||
},
|
||||
]
|
||||
|
||||
if (initData?.isPossible) {
|
||||
buttons.push({
|
||||
id: 'item-edit-button',
|
||||
title: t('reserve_item.request'),
|
||||
href: '',
|
||||
className: 'blue',
|
||||
handleClick: () => {
|
||||
if (!edit) {
|
||||
setEdit(true)
|
||||
}
|
||||
},
|
||||
})
|
||||
return buttons.reverse()
|
||||
}
|
||||
|
||||
return buttons
|
||||
}, [t, router.query, initData, edit])
|
||||
|
||||
return (
|
||||
<>
|
||||
{complete.done ? (
|
||||
<ReserveComplete reserveId={complete.reserveId} />
|
||||
) : (
|
||||
<div className="table_view02">
|
||||
{initData && <ReserveItemInfo data={initData} />}
|
||||
{initData && !edit && <ReserveItemAdditional data={initData} />}
|
||||
{!edit && <BottomButtons handleButtons={bottomButtons} />}
|
||||
{edit && (
|
||||
<ReserveEdit reserveItem={initData} setComplete={setComplete} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const categoryId = String(context.query.category)
|
||||
const id = Number(context.query.id)
|
||||
|
||||
let initData: IReserveItem
|
||||
|
||||
try {
|
||||
const result = await reserveService.getItem(id)
|
||||
if (result) {
|
||||
initData = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
initData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ReserveDetail
|
||||
328
frontend/portal/src/pages/reserve/[category]/index.tsx
Normal file
328
frontend/portal/src/pages/reserve/[category]/index.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { OptionsType, SelectBox, SelectType } from '@components/Inputs'
|
||||
import Search from '@components/Search'
|
||||
import DataGridTable from '@components/TableList/DataGridTable'
|
||||
import { DEFUALT_GRID_PAGE_SIZE, GRID_ROWS_PER_PAGE_OPTION } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchTypes'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueGetterParams,
|
||||
MuiEvent,
|
||||
} from '@material-ui/data-grid'
|
||||
import { ICode, ILocation, Page, reserveService } from '@service'
|
||||
import { conditionAtom, conditionValue } from '@stores'
|
||||
import { rownum } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useMemo, useState } from 'react'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
|
||||
type ColumnsType = (data: Page, t?: TFunction) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (data, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'desc'),
|
||||
},
|
||||
{
|
||||
field: 'locationName',
|
||||
headerName: t('location'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
headerName: t('reserve_item.type'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
cellClassName: 'title',
|
||||
},
|
||||
{
|
||||
field: 'isPossible',
|
||||
headerName: t('reserve_item.is_possible'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>
|
||||
{Boolean(params.value) === true ? (
|
||||
<Typography color="error">{t('reserve_item.possible')}</Typography>
|
||||
) : (
|
||||
<Typography>{t('reserve_item.impossible')}</Typography>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getXsColumns: ColumnsType = (data, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
sortable: false,
|
||||
renderCell,
|
||||
},
|
||||
]
|
||||
|
||||
function renderCell(params: GridCellParams) {
|
||||
return (
|
||||
<div>
|
||||
<div className="title">{params.value}</div>
|
||||
<div className="sub">
|
||||
<p>{params.row.locationName}</p>
|
||||
<p>{params.row.categoryName}</p>
|
||||
|
||||
<p>
|
||||
{Boolean(params.row.isPossible) ? (
|
||||
<Typography component="span" color="error">
|
||||
{t('reserve_item.possible')}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography component="span">
|
||||
{t('reserve_item.impossible')}
|
||||
</Typography>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const conditionKey = 'reserve'
|
||||
|
||||
interface ReserveProps {
|
||||
locations: OptionsType[]
|
||||
categories: OptionsType[]
|
||||
}
|
||||
|
||||
const Reserve = ({ locations, categories }: ReserveProps) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const pageSizeRef = createRef<SelectType>()
|
||||
|
||||
const locationRef = createRef<SelectType>()
|
||||
const categoryRef = createRef<SelectType>()
|
||||
|
||||
const { page, setPageValue } = usePage(conditionKey, 0)
|
||||
const [pageSize, setPageSize] = useState<number>(DEFUALT_GRID_PAGE_SIZE)
|
||||
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue | null>({
|
||||
locationId: keywordState?.locationId || '0',
|
||||
categoryId:
|
||||
router.query?.category === 'all'
|
||||
? keywordState?.categoryId || 'all'
|
||||
: String(router.query?.category),
|
||||
})
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
value: 'item',
|
||||
label: t('reserve_item.name'),
|
||||
},
|
||||
])
|
||||
|
||||
const { data, mutate } = reserveService.search({
|
||||
keywordType: keywordState?.keywordType || 'item',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: pageSize,
|
||||
page,
|
||||
locationId:
|
||||
keywordState?.locationId !== '0' ? keywordState?.locationId : null,
|
||||
categoryId:
|
||||
router.query?.category !== 'all'
|
||||
? String(router.query?.category)
|
||||
: keywordState?.categoryId !== 'all'
|
||||
? keywordState?.categoryId
|
||||
: null,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}
|
||||
|
||||
const handleCellClick = (
|
||||
params: GridCellParams,
|
||||
event: MuiEvent<React.MouseEvent>,
|
||||
details?: any,
|
||||
) => {
|
||||
if (params.field === 'reserveItemName') {
|
||||
router.push(`${router.asPath}/${params.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
locationId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
categoryId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(() => getColumns(data, t), [data, t])
|
||||
|
||||
const xsColumns = useMemo(() => getXsColumns(data, t), [data, t])
|
||||
|
||||
const rowsPerPageSizeOptinos = GRID_ROWS_PER_PAGE_OPTION.map(item => {
|
||||
return {
|
||||
value: item,
|
||||
label: `${item} 개`,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="table_list01">
|
||||
<fieldset>
|
||||
<div>
|
||||
<SelectBox
|
||||
ref={pageSizeRef}
|
||||
options={rowsPerPageSizeOptinos}
|
||||
onChange={handlePageSizeChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Search
|
||||
options={searchTypes}
|
||||
conditionKey={conditionKey}
|
||||
handleSearch={handleSearch}
|
||||
customKeyword={customKeyword}
|
||||
className={router.query?.category === 'all' ? '' : 'wide'}
|
||||
conditionNodes={
|
||||
<>
|
||||
{locations && (
|
||||
<SelectBox
|
||||
ref={locationRef}
|
||||
options={locations}
|
||||
value={customKeyword.locationId}
|
||||
onChange={handleLocationChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
{router.query?.category === 'all' && categories && (
|
||||
<SelectBox
|
||||
ref={categoryRef}
|
||||
options={categories}
|
||||
value={customKeyword.categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<DataGridTable
|
||||
columns={columns}
|
||||
rows={data?.content}
|
||||
xsColumns={xsColumns}
|
||||
getRowId={r => r.reserveItemId}
|
||||
pageSize={pageSize}
|
||||
rowCount={data?.totalElements}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
paginationMode="server"
|
||||
onCellClick={handleCellClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const categoryId = String(context.query.category)
|
||||
|
||||
let locations: OptionsType[]
|
||||
let categories: OptionsType[]
|
||||
|
||||
try {
|
||||
const location = (await (
|
||||
await reserveService.getLocation()
|
||||
).data) as ILocation[]
|
||||
if (location) {
|
||||
locations = location.map(item => {
|
||||
return {
|
||||
value: item.locationId,
|
||||
label: item.locationName,
|
||||
}
|
||||
})
|
||||
locations.unshift({
|
||||
value: '0',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
|
||||
const category = (await (
|
||||
await reserveService.getCode('reserve-category')
|
||||
).data) as ICode[]
|
||||
if (category) {
|
||||
categories = category.map(item => {
|
||||
return {
|
||||
value: item.codeId,
|
||||
label: item.codeName,
|
||||
}
|
||||
})
|
||||
categories.unshift({
|
||||
value: 'all',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
locations,
|
||||
categories,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Reserve
|
||||
268
frontend/portal/src/pages/user/info/index.tsx
Normal file
268
frontend/portal/src/pages/user/info/index.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import CustomAlert, { CustomAlertPrpps } from '@components/CustomAlert'
|
||||
import { PasswordConfirm } from '@components/Password'
|
||||
import { IUserForm, UserInfoDone, UserInfoModified } from '@components/UserInfo'
|
||||
import { userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import produce from 'immer'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
interface AlertProps extends CustomAlertPrpps {
|
||||
message: string
|
||||
}
|
||||
|
||||
const UserInfo = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const setUser = useSetRecoilState(userAtom)
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [checkedEmail, setCheckedEmail] = useState<boolean>(false)
|
||||
const [modified, setModified] = useState<boolean>(false)
|
||||
|
||||
const [customAlert, setCustomAlert] = useState<AlertProps | undefined>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => {},
|
||||
})
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserForm>({
|
||||
defaultValues: {
|
||||
password: '',
|
||||
email: user?.email || '',
|
||||
userName: user?.userName || '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState, getValues, setFocus } = methods
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.verification) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 비밀번호 확인
|
||||
const handleCheckPassword = async (data: IUserForm) => {
|
||||
data = produce(data, draft => {
|
||||
draft.password = data.currentPassword
|
||||
})
|
||||
try {
|
||||
const result = await userService.matchPassword(data.currentPassword)
|
||||
if (result === true) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'password',
|
||||
password: data.currentPassword,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const showMessage = (message: string, callback?: () => void) => {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message,
|
||||
handleAlert: () => {
|
||||
setCustomAlert({
|
||||
...customAlert,
|
||||
open: false,
|
||||
})
|
||||
if (callback) callback()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 변경
|
||||
const handleUpdate = async (data: IUserForm) => {
|
||||
if (user.email === data.email && user.userName === data.userName) {
|
||||
showMessage(t('msg.notmodified'))
|
||||
return
|
||||
}
|
||||
|
||||
if (user.email !== data.email && !checkedEmail) {
|
||||
showMessage(t('msg.user.email.check'), () => {
|
||||
setFocus('email')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await userService.updateInfo(user.userId, {
|
||||
...data,
|
||||
...user.verification,
|
||||
})
|
||||
if (result) {
|
||||
setUser({
|
||||
...user,
|
||||
...data,
|
||||
})
|
||||
|
||||
setModified(true)
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 이동
|
||||
const handleFirst = () => {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = async response => {
|
||||
if (response.profile?.id?.toString() === user.kakaoId) {
|
||||
// setVerification({
|
||||
// provider: 'kakao',
|
||||
// token: response.response.access_token,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.user?.id === user.naverId) {
|
||||
// Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
|
||||
// 로그인 페이지에서는 잘되는데 이유를 모르겠다. 구글/카카오는 잘된다. 편법으로 일단 진행
|
||||
/* setVerification({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
}) */
|
||||
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = async response => {
|
||||
if (response.googleId === user.googleId) {
|
||||
// setVerification({
|
||||
// provider: 'google',
|
||||
// token: response.tokenId,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
},
|
||||
})
|
||||
setErrorState(null)
|
||||
} else {
|
||||
setErrorState(t('err.user.login.social'))
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 랜더링
|
||||
const renderPasswordForm = () => {
|
||||
return (
|
||||
<article className="mypage">
|
||||
<div className="message small">
|
||||
<span className="">{t('label.text.required.login')}</span>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleCheckPassword)}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton handleClick={handleKakaoLogin} />
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton handleClick={handleNaverLogin} />
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton handleClick={handleGoogleLogin} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modified === false && (
|
||||
<form>
|
||||
{!user?.verification && renderPasswordForm()}
|
||||
{user?.verification && (
|
||||
<UserInfoModified
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleUpdate={handleSubmit(handleUpdate)}
|
||||
handleList={handleFirst}
|
||||
getValues={getValues}
|
||||
setFocus={setFocus}
|
||||
showMessage={showMessage}
|
||||
setCheckedEmail={setCheckedEmail}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{modified === true && <UserInfoDone handleList={handleFirst} />}
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={customAlert.handleAlert}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserInfo
|
||||
50
frontend/portal/src/pages/user/leave/bye.tsx
Normal file
50
frontend/portal/src/pages/user/leave/bye.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
mg0: {
|
||||
margin: '0 auto',
|
||||
},
|
||||
}))
|
||||
|
||||
const Bye = () => {
|
||||
const router = useRouter()
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleMain = event => {
|
||||
event.preventDefault()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classes.mg0}>
|
||||
<article className="rocation">
|
||||
<h2>{t('label.title.mypage')}</h2>
|
||||
<ul>
|
||||
<li>{t('label.title.home')}</li>
|
||||
<li>{t('label.title.mypage')}</li>
|
||||
<li>{t('label.title.leave')}</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article className="mypage">
|
||||
<div className="message">
|
||||
<span className="end">
|
||||
{t('label.text.leave.complete1')}
|
||||
<br />
|
||||
{t('label.text.leave.complete2')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="btn_center">
|
||||
<a href="#" onClick={handleMain}>
|
||||
{t('label.button.first')}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Bye
|
||||
202
frontend/portal/src/pages/user/leave/index.tsx
Normal file
202
frontend/portal/src/pages/user/leave/index.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import CustomConfirm, { CustomConfirmPrpps } from '@components/CustomConfirm'
|
||||
import { PasswordConfirm } from '@components/Password'
|
||||
import { IVerification, userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
interface IUserForm {
|
||||
currentPassword: string
|
||||
}
|
||||
|
||||
interface AlertProps extends CustomConfirmPrpps {
|
||||
message: string
|
||||
}
|
||||
|
||||
const UserLeave = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
|
||||
const [customConfirm, setCustomConfirm] = useState<AlertProps | null>(null)
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserForm>({
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState } = methods
|
||||
|
||||
// 탈퇴 처리
|
||||
const leave = async (data: IVerification) => {
|
||||
console.log('leave', data)
|
||||
try {
|
||||
const result = await userService.leave(data)
|
||||
if (result === true) {
|
||||
router.push('/auth/logout?redirect=/user/leave/bye')
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const leaveConfirm = (data: IVerification) => {
|
||||
setCustomConfirm({
|
||||
open: true,
|
||||
message: t('msg.confirm.leave'),
|
||||
handleConfirm: () => {
|
||||
setCustomConfirm({
|
||||
...customConfirm,
|
||||
open: false,
|
||||
})
|
||||
|
||||
leave(data)
|
||||
},
|
||||
handleCancel: () => {
|
||||
setCustomConfirm({ ...customConfirm, open: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 탈퇴 클릭
|
||||
const handleLeave = async (data: IUserForm) => {
|
||||
const { currentPassword } = data
|
||||
|
||||
try {
|
||||
const result = await userService.matchPassword(currentPassword)
|
||||
if (result === true) {
|
||||
leaveConfirm({
|
||||
provider: 'password',
|
||||
password: currentPassword,
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = async response => {
|
||||
if (response.response?.access_token) {
|
||||
await leave({
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.accessToken) {
|
||||
await leave({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = async response => {
|
||||
if (response.tokenId) {
|
||||
await leave({
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.login.social') })
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<article className="mypage">
|
||||
<p>
|
||||
{t('label.text.user.leave1')}
|
||||
<br />
|
||||
{t('label.text.user.leave2')}
|
||||
</p>
|
||||
<div className="guide">
|
||||
<h4>{t('label.title.guide')}</h4>
|
||||
<ul>
|
||||
<li>{t('label.text.user.leave.guide1')}</li>
|
||||
<li>{t('label.text.user.leave.guide2')}</li>
|
||||
<li>{t('label.text.user.leave.guide3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<>
|
||||
<p>{t('label.text.user.leave.password')}</p>
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleLeave)}
|
||||
handleList={handleList}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton
|
||||
handleClick={handleKakaoLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton
|
||||
handleClick={handleNaverLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton
|
||||
handleClick={handleGoogleLogin}
|
||||
confirmMessage={t('msg.confirm.leave')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
{customConfirm && (
|
||||
<CustomConfirm
|
||||
handleConfirm={customConfirm.handleConfirm}
|
||||
handleCancel={customConfirm.handleCancel}
|
||||
contentText={customConfirm.message}
|
||||
open={customConfirm.open}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserLeave
|
||||
216
frontend/portal/src/pages/user/password/index.tsx
Normal file
216
frontend/portal/src/pages/user/password/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
GoogleLoginButton,
|
||||
KakaoLoginButton,
|
||||
NaverLoginButton,
|
||||
} from '@components/Buttons'
|
||||
import {
|
||||
IUserPasswordForm,
|
||||
PasswordChange,
|
||||
PasswordConfirm,
|
||||
PasswordDone,
|
||||
} from '@components/Password'
|
||||
import { userService } from '@service'
|
||||
import { errorStateSelector, userAtom } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
const EditPassword = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const setUser = useSetRecoilState(userAtom)
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [modified, setModified] = useState<boolean>(false)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserPasswordForm>({
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
newPasswordConfirm: '',
|
||||
},
|
||||
})
|
||||
const { control, handleSubmit, formState, setFocus } = methods
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.verification) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 메인 이동
|
||||
const handleFirst = () => {
|
||||
setUser({
|
||||
...user,
|
||||
verification: null,
|
||||
})
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 비밀번호 확인
|
||||
const handleCheckPassword = async (data: IUserPasswordForm) => {
|
||||
try {
|
||||
const result = await userService.matchPassword(data.currentPassword)
|
||||
if (result === true) {
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'password',
|
||||
password: data.currentPassword,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('err.user.password.notmatch'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 변경
|
||||
const handleChangePassword = async (data: IUserPasswordForm) => {
|
||||
try {
|
||||
const result = await userService.updatePassword({
|
||||
...user.verification,
|
||||
newPassword: data.newPassword,
|
||||
})
|
||||
|
||||
if (result === true) {
|
||||
setModified(true)
|
||||
} else {
|
||||
throw new Error(t('err.internal.server'))
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 카카오 로그인
|
||||
const handleKakaoLogin = response => {
|
||||
if (response.profile?.id?.toString() === user.kakaoId) {
|
||||
// setVerification({
|
||||
// provider: 'kakao',
|
||||
// token: response.response.access_token,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'kakao',
|
||||
token: response.response.access_token,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 네이버 로그인
|
||||
const handleNaverLogin = async response => {
|
||||
if (response.user?.id === user.naverId) {
|
||||
// Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
|
||||
// 로그인 페이지에서는 잘되는데 이유를 모르겠다. 구글/카카오는 잘된다. 편법으로 일단 진행
|
||||
/* setVerification({
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
}) */
|
||||
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'naver',
|
||||
token: response.accessToken,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 구글 로그인
|
||||
const handleGoogleLogin = response => {
|
||||
if (response.googleId === user.googleId) {
|
||||
// setVerification({
|
||||
// provider: 'google',
|
||||
// token: response.tokenId,
|
||||
// })
|
||||
setUser({
|
||||
...user,
|
||||
verification: {
|
||||
provider: 'google',
|
||||
token: response.tokenId,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setErrorState({ message: t('err.user.social.notmatch') })
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 랜더링
|
||||
const renderPasswordForm = () => {
|
||||
return (
|
||||
<article className="mypage">
|
||||
<div className="message small">
|
||||
<span className="">{t('label.text.required.login')}</span>
|
||||
</div>
|
||||
{user?.hasPassword === true && (
|
||||
<PasswordConfirm
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleCheckPassword={handleSubmit(handleCheckPassword)}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
{user?.isSocialUser === true && (
|
||||
<>
|
||||
<h3>
|
||||
<span>{t('label.title.oauth')}</span>
|
||||
</h3>
|
||||
<div className="btn_social">
|
||||
{user?.kakaoId && (
|
||||
<KakaoLoginButton handleClick={handleKakaoLogin} />
|
||||
)}
|
||||
{user?.naverId && (
|
||||
<NaverLoginButton handleClick={handleNaverLogin} />
|
||||
)}
|
||||
{user?.googleId && (
|
||||
<GoogleLoginButton handleClick={handleGoogleLogin} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modified === false && (
|
||||
<form>
|
||||
{!user?.verification && renderPasswordForm()}
|
||||
{user?.verification && (
|
||||
<PasswordChange
|
||||
control={control}
|
||||
formState={formState}
|
||||
handleChangePassword={handleSubmit(handleChangePassword)}
|
||||
setFocus={setFocus}
|
||||
currentPassword={user.verification.password}
|
||||
handleList={handleFirst}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{modified === true && <PasswordDone handleList={handleFirst} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditPassword
|
||||
98
frontend/portal/src/pages/user/reserve/[id].tsx
Normal file
98
frontend/portal/src/pages/user/reserve/[id].tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import { ReserveInfo, ReserveItemInfo } from '@components/Reserve'
|
||||
import { ICode, IReserve, reserveService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface UserReserveDetailProps {
|
||||
initData: IReserve
|
||||
status?: ICode
|
||||
}
|
||||
|
||||
const UserReserveDetail = ({ initData, status }: UserReserveDetailProps) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo((): IButtons[] => {
|
||||
const buttons: IButtons[] = [
|
||||
{
|
||||
id: 'item-list-button',
|
||||
title: t('label.button.list'),
|
||||
href: `/user/reserve`,
|
||||
},
|
||||
]
|
||||
|
||||
if (
|
||||
initData?.reserveStatusId === 'approve' ||
|
||||
initData?.reserveStatusId === 'request'
|
||||
) {
|
||||
buttons.push({
|
||||
id: 'item-cancel-button',
|
||||
title: `${t('reserve')} ${t('label.button.cancel')}`,
|
||||
href: `/user/reserve/cancel/${initData.reserveId}`,
|
||||
className: 'blue',
|
||||
})
|
||||
return buttons.reverse()
|
||||
}
|
||||
|
||||
return buttons
|
||||
}, [t, router.query, initData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table_view02">
|
||||
{initData && (
|
||||
<>
|
||||
<ReserveItemInfo
|
||||
data={initData.reserveItem}
|
||||
reserveStatus={status}
|
||||
/>
|
||||
<ReserveInfo data={initData} />
|
||||
</>
|
||||
)}
|
||||
<BottomButtons handleButtons={bottomButtons} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const id = String(context.query.id)
|
||||
|
||||
let initData: IReserve = null
|
||||
let status: ICode = null
|
||||
|
||||
try {
|
||||
const result = await reserveService.getReserve(id)
|
||||
if (result) {
|
||||
initData = result.data
|
||||
}
|
||||
|
||||
const codeResult = await reserveService.getCode('reserve-status')
|
||||
|
||||
if (codeResult) {
|
||||
status = codeResult.data.find(
|
||||
item => item.codeId === initData?.reserveStatusId,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
initData,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default UserReserveDetail
|
||||
102
frontend/portal/src/pages/user/reserve/cancel/[id].tsx
Normal file
102
frontend/portal/src/pages/user/reserve/cancel/[id].tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { BottomButtons, IButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/ValidationAlert'
|
||||
import useInputs from '@hooks/useInputs'
|
||||
import { reserveService } from '@service'
|
||||
import { errorStateSelector } from '@stores'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const ReserveCancel = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const searchText = useInputs('')
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText.value !== '') {
|
||||
setError(false)
|
||||
}
|
||||
}, [searchText])
|
||||
|
||||
const handleCancelClick = async () => {
|
||||
if (searchText.value === '') {
|
||||
setError(true)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await reserveService.cancel(
|
||||
String(router.query?.id),
|
||||
searchText.value,
|
||||
)
|
||||
if (result) {
|
||||
enqueueSnackbar(
|
||||
`${t('reserve')} ${t('common.cancel')}${t('common.msg.done.format')}`,
|
||||
{
|
||||
variant: 'success',
|
||||
},
|
||||
)
|
||||
router.push('/user/reserve')
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼
|
||||
const bottomButtons = useMemo(
|
||||
(): IButtons[] => [
|
||||
{
|
||||
id: 'item-confirm-button',
|
||||
title: t('label.button.confirm'),
|
||||
href: ``,
|
||||
handleClick: handleCancelClick,
|
||||
className: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'item-list-button',
|
||||
title: t('label.button.cancel'),
|
||||
href: ``,
|
||||
handleClick: () => {
|
||||
router.back()
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[t, router, searchText],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mypage">
|
||||
<p>{t('reserve.msg.calcel_reason')}</p>
|
||||
<div className="table_write01">
|
||||
<span>{t('common.required_fields')}</span>
|
||||
<div className="write">
|
||||
<dl>
|
||||
<dt className="import">{t('reserve.cancel_reason')}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('reserve.cancel_reason')}
|
||||
{...searchText}
|
||||
/>
|
||||
{error && (
|
||||
<ValidationAlert message={t('reserve.msg.calcel_reason')} />
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<BottomButtons handleButtons={bottomButtons} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReserveCancel
|
||||
419
frontend/portal/src/pages/user/reserve/index.tsx
Normal file
419
frontend/portal/src/pages/user/reserve/index.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
import {
|
||||
OptionsType,
|
||||
SelectBox,
|
||||
SelectType,
|
||||
} from '@components/Inputs/SelectBox'
|
||||
import Search from '@components/Search'
|
||||
import DataGridTable from '@components/TableList/DataGridTable'
|
||||
import { DEFUALT_GRID_PAGE_SIZE, GRID_ROWS_PER_PAGE_OPTION } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchTypes'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
MuiEvent,
|
||||
} from '@material-ui/data-grid'
|
||||
import { ICode, ILocation, Page, reserveService } from '@service'
|
||||
import { conditionAtom, conditionValue, userAtom } from '@stores'
|
||||
import { rownum } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { createRef, useEffect, useMemo, useState } from 'react'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
|
||||
type ColorType =
|
||||
| 'inherit'
|
||||
| 'initial'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'textPrimary'
|
||||
| 'textSecondary'
|
||||
| 'error'
|
||||
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
locations: OptionsType[],
|
||||
categories: OptionsType[],
|
||||
status: ICode[],
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (data, locations, categories, status, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'desc'),
|
||||
},
|
||||
{
|
||||
field: 'locationId',
|
||||
headerName: t('location'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{locations.find(item => item.value === params.value)?.label}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'categoryId',
|
||||
headerName: t('reserve_item.type'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{categories.find(item => item.value === params.value)?.label}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
cellClassName: 'title',
|
||||
},
|
||||
{
|
||||
field: 'reserveQty',
|
||||
headerName: `${t('reserve.count')}/${t('reserve.number_of_people')} (${t(
|
||||
'reserve_item.inventory',
|
||||
)})`,
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => {
|
||||
const category = params.row.categoryId
|
||||
if (category === 'education') {
|
||||
return `${params.value}(${params.row.totalQty})`
|
||||
} else {
|
||||
return `${params.row.totalQty}`
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'reserveStatusId',
|
||||
headerName: t('reserve.status'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => {
|
||||
let color: ColorType = 'inherit'
|
||||
if (params.value === 'request' || params.value === 'cancel') {
|
||||
color = 'error'
|
||||
}
|
||||
return (
|
||||
<Typography color={color}>
|
||||
{status.find(item => item.codeId === params.value)?.codeName}
|
||||
</Typography>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_date'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
params.value
|
||||
? convertStringToDateFormat(String(params.value), 'yyyy-MM-dd HH:mm')
|
||||
: null,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getXsColumns: ColumnsType = (data, locations, categories, status, t) => {
|
||||
return [
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
sortable: false,
|
||||
renderCell,
|
||||
},
|
||||
]
|
||||
|
||||
function renderCell(params: GridCellParams) {
|
||||
return (
|
||||
<div>
|
||||
<div className="title">{params.value}</div>
|
||||
<div className="sub">
|
||||
<p>
|
||||
{
|
||||
locations.find(item => item.value === params.row.locationId)
|
||||
?.label
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
categories.find(item => item.value === params.row.categoryId)
|
||||
?.label
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{params.row.reserveStatusId === 'request' ||
|
||||
params.row.reserveStatusId === 'cancel' ? (
|
||||
<Typography component="span" color="error">
|
||||
{
|
||||
status.find(
|
||||
item => item.codeId === params.row.reserveStatusId,
|
||||
).codeName
|
||||
}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography component="span">
|
||||
{
|
||||
status.find(
|
||||
item => item.codeId === params.row.reserveStatusId,
|
||||
).codeName
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const conditionKey = 'user-reserve'
|
||||
|
||||
interface UserReserveProps {
|
||||
locations: OptionsType[]
|
||||
categories: OptionsType[]
|
||||
status: ICode[]
|
||||
}
|
||||
|
||||
const UserReserve = (props: UserReserveProps) => {
|
||||
const { locations, categories, status } = props
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const pageSizeRef = createRef<SelectType>()
|
||||
|
||||
const user = useRecoilValue(userAtom)
|
||||
const { page, setPageValue } = usePage(conditionKey, 0)
|
||||
const [pageSize, setPageSize] = useState<number>(DEFUALT_GRID_PAGE_SIZE)
|
||||
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue | null>({
|
||||
locationId: keywordState?.locationId || '0',
|
||||
categoryId: keywordState?.categoryId || 'all',
|
||||
})
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
value: 'item',
|
||||
label: t('reserve_item.name'),
|
||||
},
|
||||
])
|
||||
|
||||
const { data, mutate } = reserveService.searchUserReserve({
|
||||
userId: user?.userId,
|
||||
keywordType: keywordState?.keywordType || 'item',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: pageSize,
|
||||
page,
|
||||
locationId:
|
||||
keywordState?.locationId !== '0' ? keywordState?.locationId : null,
|
||||
categoryId:
|
||||
keywordState?.categoryId !== 'all' ? keywordState?.categoryId : null,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
console.log(data)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}
|
||||
|
||||
const handleCellClick = (
|
||||
params: GridCellParams,
|
||||
event: MuiEvent<React.MouseEvent>,
|
||||
details?: any,
|
||||
) => {
|
||||
if (params.field === 'reserveItemName') {
|
||||
router.push(`${router.asPath}/${params.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
locationId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
categoryId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, locations, categories, status, t),
|
||||
[data, t],
|
||||
)
|
||||
|
||||
const xsColumns = useMemo(
|
||||
() => getXsColumns(data, locations, categories, status, t),
|
||||
[data, t],
|
||||
)
|
||||
|
||||
const rowsPerPageSizeOptinos = GRID_ROWS_PER_PAGE_OPTION.map(item => {
|
||||
return {
|
||||
value: item,
|
||||
label: `${item} 개`,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mypage">
|
||||
<div className="table_list01">
|
||||
<fieldset>
|
||||
<div>
|
||||
<SelectBox
|
||||
ref={pageSizeRef}
|
||||
options={rowsPerPageSizeOptinos}
|
||||
onChange={handlePageSizeChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Search
|
||||
options={searchTypes}
|
||||
conditionKey={conditionKey}
|
||||
handleSearch={handleSearch}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<>
|
||||
{locations && (
|
||||
<SelectBox
|
||||
options={locations}
|
||||
value={customKeyword.locationId}
|
||||
onChange={handleLocationChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
{categories && (
|
||||
<SelectBox
|
||||
options={categories}
|
||||
value={customKeyword.categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
style={{ marginRight: '2px' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<DataGridTable
|
||||
columns={columns}
|
||||
rows={data?.content}
|
||||
xsColumns={xsColumns}
|
||||
getRowId={r => r.reserveId}
|
||||
pageSize={pageSize}
|
||||
rowCount={data?.totalElements}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
paginationMode="server"
|
||||
onCellClick={handleCellClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const categoryId = String(context.query.category)
|
||||
|
||||
let locations: OptionsType[] = []
|
||||
let categories: OptionsType[] = []
|
||||
let status: ICode[] = []
|
||||
|
||||
try {
|
||||
const location = (await (
|
||||
await reserveService.getLocation()
|
||||
).data) as ILocation[]
|
||||
if (location) {
|
||||
locations = location.map(item => {
|
||||
return {
|
||||
value: item.locationId,
|
||||
label: item.locationName,
|
||||
}
|
||||
})
|
||||
locations.unshift({
|
||||
value: '0',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
|
||||
const category = (await (
|
||||
await reserveService.getCode('reserve-category')
|
||||
).data) as ICode[]
|
||||
if (category) {
|
||||
categories = category.map(item => {
|
||||
return {
|
||||
value: item.codeId,
|
||||
label: item.codeName,
|
||||
}
|
||||
})
|
||||
categories.unshift({
|
||||
value: 'all',
|
||||
label: '전체',
|
||||
})
|
||||
}
|
||||
|
||||
const codeResult = await reserveService.getCode('reserve-status')
|
||||
if (codeResult) {
|
||||
status = codeResult.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve detail item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
locations,
|
||||
categories,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default UserReserve
|
||||
Reference in New Issue
Block a user