frontend add

This commit is contained in:
shinmj
2021-10-21 09:03:17 +09:00
parent 8caa4bbc5a
commit cb9d50511e
443 changed files with 88282 additions and 0 deletions

View 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

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

View 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(),
],
}
}

View 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

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

View File

@@ -0,0 +1,4 @@
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse) => {
res.status(200)
}

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

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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