✨ frontend add
This commit is contained in:
48
frontend/admin/src/pages/404/index.tsx
Normal file
48
frontend/admin/src/pages/404/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
|
||||
const useStyles = makeStyles((_: Theme) =>
|
||||
createStyles({
|
||||
content: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
pos: {
|
||||
marginBottom: '3rem',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const Error404 = props => {
|
||||
const classes = useStyles()
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="h5" component="h2">
|
||||
404 Not Found
|
||||
</Typography>
|
||||
<Typography className={classes.pos} color="textSecondary">
|
||||
The page you were looking for doesn't exist
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Back to Home
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Error404
|
||||
120
frontend/admin/src/pages/_app/index.tsx
Normal file
120
frontend/admin/src/pages/_app/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { NextPageContext } from 'next'
|
||||
import Head from 'next/head'
|
||||
import { AppContext, AppProps } from 'next/app'
|
||||
import { ThemeProvider } from '@material-ui/core/styles'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import { Theme } from '@material-ui/core/styles'
|
||||
import { RecoilRoot } from 'recoil'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
|
||||
import theme from '@styles/theme'
|
||||
import darkTheme from '@styles/darkTheme'
|
||||
import App from '@components/App/App'
|
||||
import axios from 'axios'
|
||||
import '@libs/i18n'
|
||||
import { appWithTranslation, useTranslation } from 'next-i18next'
|
||||
import { useLocalStorage } from '@hooks/useLocalStorage'
|
||||
import { SITE_ID } from '@constants/env'
|
||||
import { BASE_URL, CUSTOM_HEADER_SITE_ID_KEY } from '@constants'
|
||||
import { CookiesProvider } from 'react-cookie'
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
export type PageProps = {
|
||||
pathname?: string
|
||||
query?: NextPageContext['query']
|
||||
req?: NextPageContext['req']
|
||||
}
|
||||
|
||||
// axios 기본 설정
|
||||
axios.defaults.headers.common[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
axios.defaults.baseURL = BASE_URL
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
const MyApp = (props: AppProps) => {
|
||||
const { Component, pageProps } = props
|
||||
|
||||
/**
|
||||
* locales
|
||||
*/
|
||||
const { i18n } = useTranslation()
|
||||
const [storedValue, setValue] = useLocalStorage('locale', i18n.language)
|
||||
useEffect(() => {
|
||||
if (storedValue !== i18n.language) {
|
||||
i18n.changeLanguage(storedValue)
|
||||
}
|
||||
}, [i18n, storedValue])
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
* 테마 선택시 사용 (언제??)
|
||||
*/
|
||||
const [selectTheme, setSelectTheme] = useState<Theme>(theme)
|
||||
|
||||
useEffect(() => {
|
||||
// Remove the server-side injected CSS.
|
||||
const jssStyles = document.querySelector('#jss-server-side')
|
||||
if (jssStyles) {
|
||||
jssStyles.parentElement!.removeChild(jssStyles)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<ThemeProvider theme={selectTheme}>
|
||||
<Head>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico"></link>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
/>
|
||||
</Head>
|
||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||
<CssBaseline />
|
||||
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
iconVariant={{
|
||||
success: '✅ ',
|
||||
error: '✖ ',
|
||||
warning: '⚠ ',
|
||||
info: 'ℹ️ ',
|
||||
}}
|
||||
autoHideDuration={2000}
|
||||
preventDuplicate={true}
|
||||
>
|
||||
<CookiesProvider>
|
||||
<App component={Component} {...pageProps} />
|
||||
</CookiesProvider>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
}
|
||||
|
||||
MyApp.getInitialProps = async (context: AppContext) => {
|
||||
const { Component, ctx, router } = context
|
||||
let pageProps: 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
|
||||
pageProps.pathname = ctx.pathname
|
||||
|
||||
return { pageProps }
|
||||
}
|
||||
|
||||
export default appWithTranslation(MyApp)
|
||||
86
frontend/admin/src/pages/_document/index.tsx
Normal file
86
frontend/admin/src/pages/_document/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import Document, {
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
} from 'next/document'
|
||||
import { ServerStyleSheets } from '@material-ui/core/styles'
|
||||
import theme from '@styles/theme'
|
||||
|
||||
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>
|
||||
{/* PWA primary color */}
|
||||
<meta name="theme-color" content={theme.palette.primary.main} />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
{this.loadWindowProperty(locale)}
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
||||
// it's compatible with server-side generation (SSG).
|
||||
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
|
||||
// Resolution order
|
||||
//
|
||||
// On the server:
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. document.getInitialProps
|
||||
// 4. app.render
|
||||
// 5. page.render
|
||||
// 6. document.render
|
||||
//
|
||||
// On the server with error:
|
||||
// 1. document.getInitialProps
|
||||
// 2. app.render
|
||||
// 3. page.render
|
||||
// 4. document.render
|
||||
//
|
||||
// On the client
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. app.render
|
||||
// 4. page.render
|
||||
|
||||
// Render app and page and get the context of the page with collected side effects.
|
||||
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(),
|
||||
],
|
||||
}
|
||||
}
|
||||
19
frontend/admin/src/pages/_error/index.tsx
Normal file
19
frontend/admin/src/pages/_error/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { NextPageContext } from 'next'
|
||||
|
||||
const Error = ({ statusCode }) => {
|
||||
return (
|
||||
<p>
|
||||
{statusCode
|
||||
? `An error ${statusCode} occurred on server`
|
||||
: 'An error occurred on client'}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
export default Error
|
||||
89
frontend/admin/src/pages/api/editor/index.ts
Normal file
89
frontend/admin/src/pages/api/editor/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import Cookies from 'cookies'
|
||||
import multer from 'multer'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import axios from 'axios'
|
||||
import { ACCESS_TOKEN, CLAIM_NAME, SERVER_API_URL } from '@constants/env'
|
||||
import { EDITOR_LOAD_IMAGE_URL } from '@constants'
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
90
frontend/admin/src/pages/api/proxy/[...path].ts
Normal file
90
frontend/admin/src/pages/api/proxy/[...path].ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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/proxy/user-service/login'
|
||||
|
||||
req.url = req.url.replace(/^\/api\/proxy/, '')
|
||||
|
||||
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)
|
||||
|
||||
req.url = '/user-service/api/v1/users/token/refresh'
|
||||
|
||||
if (!headers[CLAIM_NAME] || headers[CLAIM_NAME] === '') {
|
||||
console.warn(`can't refresh`)
|
||||
res.status(401).json({ message: 'Invalid Credentials 🥺' })
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// server API 에 쿠키를 전달하지 않음.
|
||||
req.headers.cookie = ''
|
||||
req.headers[CUSTOM_HEADER_SITE_ID_KEY] = SITE_ID
|
||||
console.info(`req.url : ${req.url}`)
|
||||
|
||||
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 🥺' })
|
||||
}
|
||||
res.end()
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: DEFAULT_ERROR_MESSAGE, error })
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
61
frontend/admin/src/pages/api/v1/messages.ts
Normal file
61
frontend/admin/src/pages/api/v1/messages.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ACCESS_TOKEN, CLAIM_NAME, SERVER_API_URL } from '@constants/env'
|
||||
import axios from 'axios'
|
||||
import Cookies from 'cookies'
|
||||
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) => {
|
||||
const cookies = new Cookies(req, res)
|
||||
const authToken = cookies.get(ACCESS_TOKEN)
|
||||
|
||||
// server 에 cookie 전달하지 않음
|
||||
req.headers.cookie = ''
|
||||
// header에 authentication 추가
|
||||
if (authToken) {
|
||||
req.headers[CLAIM_NAME] = authToken
|
||||
}
|
||||
|
||||
let noResultLocales: string[] = []
|
||||
|
||||
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/admin/src/pages/api/v1/token.ts
Normal file
15
frontend/admin/src/pages/api/v1/token.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { REFRESH_TOKEN } from '@constants/env'
|
||||
import Cookies from 'cookies'
|
||||
|
||||
/**
|
||||
* 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' })
|
||||
}
|
||||
274
frontend/admin/src/pages/attachment/index.tsx
Normal file
274
frontend/admin/src/pages/attachment/index.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Link from '@material-ui/core/Link'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import { attachmentService, fileService } from '@service'
|
||||
import { conditionAtom, errorStateSelector } from '@stores'
|
||||
import { formatBytes, Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { TFunction } from 'next-i18next'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
type ColumnType = (
|
||||
data: Page,
|
||||
handleDelete: (id: string) => void,
|
||||
toggoleIsDelete: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
id: string,
|
||||
) => void,
|
||||
t: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
//그리드 컬럼 정의
|
||||
const getColumns: ColumnType = (
|
||||
data: Page,
|
||||
handleDelete: (id: string) => void,
|
||||
toggoleIsDelete: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
id: string,
|
||||
) => void,
|
||||
t,
|
||||
) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) => {
|
||||
return rownum(
|
||||
data,
|
||||
data?.content.findIndex(v => v.id === params.id),
|
||||
'desc',
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
headerName: t('attachment.file_id'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'seq',
|
||||
headerName: t('attachment.file_no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'originalFileName',
|
||||
headerName: t('attachment.file_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Typography>
|
||||
<Link
|
||||
href={`${fileService.downloadUrl}/${params.row.id}`}
|
||||
download={params.value}
|
||||
variant="body2"
|
||||
>
|
||||
{params.value}
|
||||
</Link>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'size',
|
||||
headerName: t('attachment.file_size'),
|
||||
headerAlign: 'center',
|
||||
align: 'right',
|
||||
width: 100,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) => {
|
||||
return formatBytes(params.value as number)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'downloadCnt',
|
||||
headerName: t('attachment.download_count'),
|
||||
headerAlign: 'center',
|
||||
align: 'right',
|
||||
width: 100,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) => {
|
||||
return convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm',
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'isDelete',
|
||||
headerName: t('common.delete_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggoleIsDelete(event, params.row.id)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<GridButtons
|
||||
id={params.value as string}
|
||||
handleDelete={(id: string) => handleDelete(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const conditionKey = 'attachment'
|
||||
|
||||
const Attachment = () => {
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
// 에러 상태관리
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
// pagination 상태관리
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'id',
|
||||
label: t('attachment.file_id'),
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: t('attachment.file_name'),
|
||||
},
|
||||
])
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = attachmentService.search({
|
||||
keywordType: keywordState?.keywordType || 'id',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
//에러 callback
|
||||
const errorCallback = useCallback((error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//삭제여부 toggle 시 바로 update
|
||||
const toggoleIsDelete = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: string) => {
|
||||
attachmentService.updateToggle({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
isDelete: event.target.checked,
|
||||
})
|
||||
},
|
||||
[page],
|
||||
)
|
||||
|
||||
const handleDelete = useCallback((id: string) => {
|
||||
attachmentService.delete({
|
||||
id,
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(data, handleDelete, toggoleIsDelete, t)
|
||||
}, [data])
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
40
frontend/admin/src/pages/auth/login.tsx
Normal file
40
frontend/admin/src/pages/auth/login.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import LoginForm, { loginFormType } from '@components/Auth/LoginForm'
|
||||
import Loader from '@components/Loader'
|
||||
import { DEFAULT_ERROR_MESSAGE } from '@constants'
|
||||
import useUser from '@hooks/useUser'
|
||||
import Router from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { loginSerivce } from 'src/service/Login'
|
||||
|
||||
const Login = () => {
|
||||
const { isLogin, loggedOut, mutate } = useUser()
|
||||
const [loginError, setLoginError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLogin && !loggedOut) {
|
||||
Router.replace('/')
|
||||
}
|
||||
}, [isLogin, loggedOut])
|
||||
|
||||
if (isLogin) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
const onLoginSubmit = async (form: loginFormType) => {
|
||||
try {
|
||||
const result = await loginSerivce.login(form)
|
||||
if (result === 'success') {
|
||||
mutate()
|
||||
} else {
|
||||
setLoginError(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('login error ', error)
|
||||
setLoginError(error.response?.data.message || DEFAULT_ERROR_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
return <LoginForm handleLogin={onLoginSubmit} errorMessage={loginError} />
|
||||
}
|
||||
|
||||
export default Login
|
||||
30
frontend/admin/src/pages/auth/logout.tsx
Normal file
30
frontend/admin/src/pages/auth/logout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, REFRESH_TOKEN } from '@constants/env'
|
||||
import axios from 'axios'
|
||||
|
||||
function Logout() {
|
||||
axios.defaults.headers.common[ACCESS_TOKEN] = ''
|
||||
axios.defaults.headers.common[AUTH_USER_ID] = ''
|
||||
return (
|
||||
<div>
|
||||
<a href="/auth/logout">Logout</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Logout.getInitialProps = ({ 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)
|
||||
cookies.set(ACCESS_TOKEN)
|
||||
|
||||
res.writeHead(307, { Location: '/' })
|
||||
res.end()
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default Logout
|
||||
327
frontend/admin/src/pages/authorization/[id].tsx
Normal file
327
frontend/admin/src/pages/authorization/[id].tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import { FormControl, InputLabel } from '@material-ui/core'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
AuthorizationSavePayload,
|
||||
authorizationService,
|
||||
codeService,
|
||||
ICode,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
formControl: {
|
||||
width: '100%',
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IAuthorizationFormInput {
|
||||
authorizationName: string
|
||||
urlPatternValue: string
|
||||
httpMethodCode: string
|
||||
sortSeq: number
|
||||
}
|
||||
|
||||
export interface IAuthorizationItemsProps {
|
||||
authorizationNo: string
|
||||
initData: AuthorizationSavePayload | null
|
||||
httpMethodCodeList: ICode[]
|
||||
}
|
||||
|
||||
const AuthorizationItem = ({
|
||||
authorizationNo,
|
||||
initData,
|
||||
httpMethodCodeList,
|
||||
}: IAuthorizationItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IAuthorizationFormInput>({
|
||||
defaultValues: {
|
||||
authorizationName: initData?.authorizationName || '',
|
||||
urlPatternValue: initData?.urlPatternValue || '',
|
||||
httpMethodCode: initData?.httpMethodCode || 'GET',
|
||||
sortSeq: initData?.sortSeq || 0,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IAuthorizationFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const saved: AuthorizationSavePayload = {
|
||||
authorizationName: formData.authorizationName,
|
||||
urlPatternValue: formData.urlPatternValue,
|
||||
httpMethodCode: formData.httpMethodCode,
|
||||
sortSeq: formData.sortSeq,
|
||||
}
|
||||
|
||||
if (authorizationNo === '-1') {
|
||||
await authorizationService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
await authorizationService.update({
|
||||
authorizationNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="authorizationName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 50 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('authorization.authorization_name')}
|
||||
name="authorizationName"
|
||||
required
|
||||
inputProps={{ maxLength: 50 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('authorization.authorization_name'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.authorizationName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.authorizationName}
|
||||
target={[50]}
|
||||
label={t('authorization.authorization_name')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="urlPatternValue"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 200 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('authorization.url_pattern_value')}
|
||||
name="urlPatternValue"
|
||||
required
|
||||
inputProps={{ maxLength: 200 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('authorization.url_pattern_value'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.urlPatternValue && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.urlPatternValue}
|
||||
target={[200]}
|
||||
label={t('authorization.url_pattern_value')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="httpMethodCode-label" required>
|
||||
{t('authorization.http_method_code')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="httpMethodCode"
|
||||
control={control}
|
||||
defaultValue={initData?.httpMethodCode || 'GET'}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="httpMethodCode"
|
||||
required
|
||||
labelId="httpMethodCode-label"
|
||||
label={t('authorization.http_method_code')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{httpMethodCodeList.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="sortSeq"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('common.sort_seq')}
|
||||
name="sortSeq"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('common.sort_seq'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.sortSeq && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.sortSeq}
|
||||
target={[1, 99999]}
|
||||
label={t('common.sort_seq')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const authorizationNo = query.id
|
||||
|
||||
let data = {}
|
||||
let httpMethodCodeList = []
|
||||
|
||||
try {
|
||||
const codeList = await codeService.getCodeDetailList('http_method_code')
|
||||
if (codeList) {
|
||||
httpMethodCodeList = (await codeList.data) as ICode[]
|
||||
}
|
||||
|
||||
if (authorizationNo === '-1') {
|
||||
const result = await authorizationService.getNextSortSeq()
|
||||
if (result) {
|
||||
const nextSortSeq = (await result.data) as number
|
||||
data = { sortSeq: nextSortSeq }
|
||||
}
|
||||
} else {
|
||||
const result = await authorizationService.get(authorizationNo as string)
|
||||
if (result) {
|
||||
data = (await result.data) as AuthorizationSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`authorization item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
authorizationNo,
|
||||
initData: data,
|
||||
httpMethodCodeList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthorizationItem
|
||||
251
frontend/admin/src/pages/authorization/index.tsx
Normal file
251
frontend/admin/src/pages/authorization/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import { authorizationService } from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
deleteAuthorization: (authorizationNo: string) => void,
|
||||
updateAuthorization: (authorizationNo: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
deleteAuthorization,
|
||||
updateAuthorization,
|
||||
t,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'authorizationName',
|
||||
headerName: t('authorization.authorization_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 250,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'urlPatternValue',
|
||||
headerName: t('authorization.url_pattern_value'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'httpMethodCode',
|
||||
headerName: t('authorization.url_pattern_value'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'sortSeq',
|
||||
headerName: t('common.sort_seq'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return (
|
||||
<GridButtons
|
||||
id={params.row.authorizationNo as string}
|
||||
handleDelete={deleteAuthorization}
|
||||
handleUpdate={updateAuthorization}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'authorization'
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Authorization: NextPage<any> = () => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'authorizationName',
|
||||
label: t('authorization.authorization_name'),
|
||||
},
|
||||
{
|
||||
key: 'urlPatternValue',
|
||||
label: t('authorization.url_pattern_value'),
|
||||
},
|
||||
{
|
||||
key: 'httpMethodCode',
|
||||
label: t('authorization.http_method_code'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = authorizationService.search({
|
||||
keywordType: keywordState?.keywordType || 'authorizationName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const deleteAuthorization = useCallback(
|
||||
(authorizationNo: string) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
authorizationService.delete({
|
||||
authorizationNo,
|
||||
callback: () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
mutate()
|
||||
},
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 수정 시 상세 화면 이동
|
||||
const updateAuthorization = useCallback(
|
||||
(authorizationNo: string) => {
|
||||
route.push(`/authorization/${authorizationNo}`)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, deleteAuthorization, updateAuthorization, t),
|
||||
[data, deleteAuthorization, updateAuthorization, t],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('authorization/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.authorizationNo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Authorization
|
||||
568
frontend/admin/src/pages/banner/[id].tsx
Normal file
568
frontend/admin/src/pages/banner/[id].tsx
Normal file
@@ -0,0 +1,568 @@
|
||||
import AttachList from '@components/AttachList'
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import { Upload, UploadType } from '@components/Upload'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import FormControl from '@material-ui/core/FormControl'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import InputLabel from '@material-ui/core/InputLabel'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
BannerSavePayload,
|
||||
bannerService,
|
||||
codeService,
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
ICode,
|
||||
ISite,
|
||||
UploadInfoReqeust,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
formControl: {
|
||||
width: '100%',
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
switchBox: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
textFieldMultiline: {
|
||||
padding: '0 !important',
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyBanner: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IBannerFormInput {
|
||||
siteId: number
|
||||
bannerTypeCode: string
|
||||
bannerTitle: string
|
||||
attachmentCode: string
|
||||
urlAddr: string
|
||||
newWindowAt: boolean
|
||||
bannerContent: string
|
||||
sortSeq: number
|
||||
}
|
||||
|
||||
export interface IBannerItemsProps {
|
||||
bannerNo: string
|
||||
initData: BannerSavePayload | null
|
||||
bannerTypeCodeList: ICode[]
|
||||
sites: ISite[]
|
||||
}
|
||||
|
||||
const BannerItem = ({
|
||||
bannerNo,
|
||||
initData,
|
||||
bannerTypeCodeList,
|
||||
sites,
|
||||
}: IBannerItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const uploadRef = useRef<UploadType>()
|
||||
|
||||
const [attachData, setAttachData] = useState<
|
||||
IAttachmentResponse[] | undefined
|
||||
>(undefined)
|
||||
|
||||
// alert
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => setCustomAlert({ open: false }),
|
||||
})
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IBannerFormInput>({
|
||||
defaultValues: {
|
||||
bannerTypeCode: initData?.bannerTypeCode || '0001',
|
||||
bannerTitle: initData?.bannerTitle || '',
|
||||
urlAddr: initData?.urlAddr || '',
|
||||
newWindowAt:
|
||||
typeof initData?.newWindowAt !== 'undefined'
|
||||
? initData?.newWindowAt
|
||||
: false,
|
||||
bannerContent: initData?.bannerContent || '',
|
||||
sortSeq: initData?.sortSeq || 0,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
} = methods
|
||||
|
||||
const watchSite = useWatch({
|
||||
control,
|
||||
name: 'siteId',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (watchSite) {
|
||||
bannerService
|
||||
.getNextSortSeq(watchSite)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setValue('sortSeq', result.data, {
|
||||
shouldValidate: false,
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [watchSite])
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
const getAttachments = useCallback(
|
||||
async (code: string) => {
|
||||
try {
|
||||
const result = await fileService.getAttachmentList(code)
|
||||
|
||||
if (result) {
|
||||
setAttachData(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initData.attachmentCode) {
|
||||
getAttachments(initData.attachmentCode)
|
||||
}
|
||||
}, [getAttachments, initData.attachmentCode])
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IBannerFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
let { attachmentCode } = initData
|
||||
|
||||
const attachCount = await uploadRef.current.count(attachData)
|
||||
if (attachCount === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.required.format'), [
|
||||
t('banner.attachment_code'),
|
||||
]),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
})
|
||||
setSuccessSnackBar('none')
|
||||
return
|
||||
}
|
||||
|
||||
const isUpload = await uploadRef.current.isModified(attachData)
|
||||
if (isUpload) {
|
||||
const info: UploadInfoReqeust = {
|
||||
entityName: 'banner',
|
||||
entityId: bannerNo,
|
||||
}
|
||||
|
||||
// 업로드 및 저장
|
||||
const result = await uploadRef.current.upload(info, attachData)
|
||||
if (result) {
|
||||
if (result !== 'no attachments' && result !== 'no update list') {
|
||||
attachmentCode = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saved: BannerSavePayload = {
|
||||
siteId: formData.siteId,
|
||||
bannerTypeCode: formData.bannerTypeCode,
|
||||
bannerTitle: formData.bannerTitle,
|
||||
attachmentCode,
|
||||
urlAddr: formData.urlAddr,
|
||||
newWindowAt: formData.newWindowAt,
|
||||
bannerContent: formData.bannerContent,
|
||||
sortSeq: formData.sortSeq,
|
||||
}
|
||||
|
||||
try {
|
||||
let result
|
||||
if (bannerNo === '-1') {
|
||||
result = await bannerService.save({
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
result = await bannerService.update({
|
||||
bannerNo,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
if (result) {
|
||||
successCallback()
|
||||
}
|
||||
} catch (error) {
|
||||
errorCallback(error)
|
||||
if (bannerNo === '-1') {
|
||||
uploadRef.current?.rollback(attachmentCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="banner-site-label" required>
|
||||
{t('menu.site')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="siteId"
|
||||
control={control}
|
||||
defaultValue={initData?.siteId || sites[0]?.id}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="siteId"
|
||||
required
|
||||
labelId="site-label"
|
||||
label={t('menu.site')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{sites?.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={12}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="bannerTypeCode-label" required>
|
||||
{t('banner.banner_type_code')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="bannerTypeCode"
|
||||
control={control}
|
||||
defaultValue={initData?.bannerTypeCode || '0001'}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="bannerTypeCode"
|
||||
required
|
||||
labelId="bannerTypeCode-label"
|
||||
label={t('banner.banner_type_code')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{bannerTypeCodeList.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="bannerTitle"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 / 2 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('banner.banner_title')}
|
||||
name="bannerTitle"
|
||||
required
|
||||
inputProps={{ maxLength: 100 / 2 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('banner.banner_title'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.bannerTitle && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.bannerTitle}
|
||||
target={[100 / 2]}
|
||||
label={t('banner.banner_title')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Upload
|
||||
accept={'image/*'}
|
||||
ref={uploadRef}
|
||||
uploadLimitCount={1}
|
||||
attachmentCode={initData.attachmentCode}
|
||||
attachData={attachData}
|
||||
/>
|
||||
{attachData && (
|
||||
<AttachList data={attachData} setData={setAttachData} />
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={9}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="urlAddr"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 500 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('common.url')}
|
||||
name="urlAddr"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('common.url'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.urlAddr && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.urlAddr}
|
||||
target={[500]}
|
||||
label={t('common.url')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={3}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('banner.new_window_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="newWindowAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="bannerContent"
|
||||
control={control}
|
||||
rules={{ maxLength: 2000 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('banner.banner_content')}
|
||||
name="bannerContent"
|
||||
inputProps={{
|
||||
maxLength: 2000,
|
||||
className: classes.textFieldMultiline,
|
||||
}}
|
||||
multiline
|
||||
minRows={10}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('banner.banner_content'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.bannerContent && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.bannerContent}
|
||||
target={[2000]}
|
||||
label={t('banner.banner_content')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="sortSeq"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('common.sort_seq')}
|
||||
name="sortSeq"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('common.sort_seq'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.sortSeq && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.sortSeq}
|
||||
target={[1, 99999]}
|
||||
label={t('common.sort_seq')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const bannerNo = query.id as string
|
||||
|
||||
let bannerTypeCodeList = []
|
||||
let data = {}
|
||||
let sites: ISite[] = []
|
||||
|
||||
try {
|
||||
sites = await bannerService.getSites()
|
||||
|
||||
const codeList = await codeService.getCodeDetailList('banner_type_code')
|
||||
if (codeList) {
|
||||
bannerTypeCodeList = (await codeList.data) as ICode[]
|
||||
}
|
||||
|
||||
if (bannerNo === '-1') {
|
||||
const result = await bannerService.getNextSortSeq(sites[0].id)
|
||||
if (result) {
|
||||
const nextSortSeq = (await result.data) as number
|
||||
data = { sortSeq: nextSortSeq }
|
||||
}
|
||||
} else {
|
||||
const result = await bannerService.get(bannerNo)
|
||||
if (result) {
|
||||
data = (await result.data) as BannerSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`banner item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
bannerNo,
|
||||
initData: data,
|
||||
bannerTypeCodeList,
|
||||
sites,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default BannerItem
|
||||
361
frontend/admin/src/pages/banner/index.tsx
Normal file
361
frontend/admin/src/pages/banner/index.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import { bannerService, ISite } from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
conditionValue,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps, NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
search: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
width: '18vw',
|
||||
minWidth: 80,
|
||||
maxWidth: 200,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
toggleUseAt,
|
||||
deleteBanner: (bannerNo: string) => void,
|
||||
updateBanner: (bannerNo: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
toggleUseAt,
|
||||
deleteBanner,
|
||||
updateBanner,
|
||||
t,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'siteName',
|
||||
headerName: t('menu.site'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'bannerTypeCodeName',
|
||||
headerName: t('banner.banner_type_code'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'bannerTitle',
|
||||
headerName: t('banner.banner_title'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'useAt',
|
||||
headerName: t('common.use_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellCreatedAt(params: GridCellParams) {
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleUseAt(event, params.row.bannerNo)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return (
|
||||
<GridButtons
|
||||
id={params.row.bannerNo as string}
|
||||
handleDelete={deleteBanner}
|
||||
handleUpdate={updateBanner}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'banner'
|
||||
|
||||
interface BannerProps {
|
||||
sites: ISite[]
|
||||
}
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Banner: NextPage<BannerProps> = ({ sites }) => {
|
||||
// props 및 전역변수
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'bannerTitle',
|
||||
label: t('banner.banner_title'),
|
||||
},
|
||||
{
|
||||
key: 'bannerContent',
|
||||
label: t('banner.banner_content'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue>({
|
||||
siteId: keywordState?.siteId || '-',
|
||||
})
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = bannerService.search({
|
||||
keywordType: keywordState?.keywordType || 'bannerName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
siteId: keywordState?.siteId === '-' ? '' : keywordState?.siteId,
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
mutate()
|
||||
}, [mutate, setSuccessSnackBar])
|
||||
|
||||
// 사용 여부 toggle 시 save
|
||||
const toggleUseAt = useCallback(
|
||||
async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
paramBannerNo: string,
|
||||
) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
await bannerService.updateUseAt({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
bannerNo: paramBannerNo,
|
||||
useAt: event.target.checked,
|
||||
})
|
||||
},
|
||||
[errorCallback, setSuccessSnackBar, successCallback],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const deleteBanner = useCallback(
|
||||
(bannerNo: string) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
bannerService.delete({
|
||||
bannerNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, setSuccessSnackBar, successCallback],
|
||||
)
|
||||
|
||||
// 수정 시 상세 화면 이동
|
||||
const updateBanner = useCallback(
|
||||
(bannerNo: string) => {
|
||||
route.push(`/banner/${bannerNo}`)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, toggleUseAt, deleteBanner, updateBanner, t),
|
||||
[data, toggleUseAt, deleteBanner, updateBanner, t],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
// 조회조건 select onchange
|
||||
const handleSiteIdChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault()
|
||||
setCustomKeyword({
|
||||
siteId: event.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('banner/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-parentCodeId"
|
||||
select
|
||||
value={customKeyword?.siteId}
|
||||
onChange={handleSiteIdChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="-" value="-">
|
||||
{t('common.all')}
|
||||
</MenuItem>
|
||||
{sites.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.bannerNo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let sites: ISite[] = []
|
||||
|
||||
try {
|
||||
const result = await bannerService.getSites()
|
||||
|
||||
if (sites) {
|
||||
sites = result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`banner site getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
sites,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Banner
|
||||
585
frontend/admin/src/pages/board/[id].tsx
Normal file
585
frontend/admin/src/pages/board/[id].tsx
Normal file
@@ -0,0 +1,585 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import FormControl from '@material-ui/core/FormControl'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import InputLabel from '@material-ui/core/InputLabel'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
BoardSavePayload,
|
||||
boardService,
|
||||
codeService,
|
||||
ICode,
|
||||
SKINT_TYPE_CODE_NORMAL,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
formControl: {
|
||||
width: '100%',
|
||||
},
|
||||
switchBox: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyBoard: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IBoardFormInput {
|
||||
boardName: string
|
||||
skinTypeCode: string
|
||||
titleDisplayLength: number
|
||||
postDisplayCount: number
|
||||
pageDisplayCount: number
|
||||
newDisplayDayCount: number
|
||||
editorUseAt: boolean
|
||||
userWriteAt: boolean
|
||||
commentUseAt: boolean
|
||||
uploadUseAt: boolean
|
||||
uploadLimitCount: number
|
||||
uploadLimitSize: number
|
||||
}
|
||||
|
||||
export interface IBoardItemsProps {
|
||||
boardNo: number
|
||||
initData: BoardSavePayload | null
|
||||
skinTypeCodeList?: ICode[]
|
||||
}
|
||||
|
||||
const BoardItem = ({
|
||||
boardNo,
|
||||
initData,
|
||||
skinTypeCodeList,
|
||||
}: IBoardItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const [uploadUseAt, setUploadUseAt] = useState<boolean>(
|
||||
initData?.uploadUseAt !== undefined ? initData?.uploadUseAt : false,
|
||||
)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IBoardFormInput>({
|
||||
defaultValues: {
|
||||
boardName: initData?.boardName || '',
|
||||
skinTypeCode: initData?.skinTypeCode || SKINT_TYPE_CODE_NORMAL,
|
||||
titleDisplayLength: initData?.titleDisplayLength,
|
||||
postDisplayCount: initData?.postDisplayCount,
|
||||
pageDisplayCount: initData?.pageDisplayCount,
|
||||
newDisplayDayCount: initData?.newDisplayDayCount,
|
||||
editorUseAt:
|
||||
typeof initData?.editorUseAt !== 'undefined'
|
||||
? initData?.editorUseAt
|
||||
: false,
|
||||
userWriteAt:
|
||||
typeof initData?.userWriteAt !== 'undefined'
|
||||
? initData?.userWriteAt
|
||||
: false,
|
||||
commentUseAt:
|
||||
typeof initData?.commentUseAt !== 'undefined'
|
||||
? initData?.commentUseAt
|
||||
: false,
|
||||
uploadUseAt:
|
||||
typeof initData?.uploadUseAt !== 'undefined'
|
||||
? initData?.uploadUseAt
|
||||
: false,
|
||||
uploadLimitCount: initData?.uploadLimitCount,
|
||||
uploadLimitSize: initData?.uploadLimitSize,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IBoardFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const saved: BoardSavePayload = {
|
||||
boardName: formData.boardName,
|
||||
skinTypeCode: formData.skinTypeCode,
|
||||
titleDisplayLength: formData.titleDisplayLength,
|
||||
postDisplayCount: formData.postDisplayCount,
|
||||
pageDisplayCount: formData.pageDisplayCount,
|
||||
newDisplayDayCount: formData.newDisplayDayCount,
|
||||
editorUseAt: formData.editorUseAt,
|
||||
userWriteAt: formData.userWriteAt,
|
||||
commentUseAt: formData.commentUseAt,
|
||||
uploadUseAt: formData.uploadUseAt,
|
||||
uploadLimitCount: formData.uploadUseAt ? formData.uploadLimitCount : null,
|
||||
uploadLimitSize: formData.uploadUseAt ? formData.uploadLimitSize : null,
|
||||
}
|
||||
|
||||
if (boardNo === -1) {
|
||||
await boardService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
await boardService.update({
|
||||
boardNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangeUploadUseAt = event => {
|
||||
setUploadUseAt(event.target.checked)
|
||||
}
|
||||
|
||||
const getSwitch = (onChange, ref, value) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="boardName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('board.board_name')}
|
||||
name="boardName"
|
||||
required
|
||||
inputProps={{ maxLength: 100 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.board_name'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.boardName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.boardName}
|
||||
target={[100]}
|
||||
label={t('board.board_name')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="skinTypeCode-label" required>
|
||||
{t('board.skin_type_code')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="skinTypeCode"
|
||||
control={control}
|
||||
defaultValue={initData?.skinTypeCode}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="skinTypeCode"
|
||||
required
|
||||
labelId="skinTypeCode-label"
|
||||
label={t('board.skin_type_code')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{skinTypeCodeList.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="titleDisplayLength"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.title_display_length')}
|
||||
name="titleDisplayLength"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.title_display_length'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.titleDisplayLength && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.titleDisplayLength}
|
||||
target={[1, 99999]}
|
||||
label={t('board.title_display_length')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="postDisplayCount"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.post_display_count')}
|
||||
name="postDisplayCount"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.post_display_count'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.postDisplayCount && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.postDisplayCount}
|
||||
target={[1, 99999]}
|
||||
label={t('board.post_display_count')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="pageDisplayCount"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.page_display_count')}
|
||||
name="pageDisplayCount"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.page_display_count'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.pageDisplayCount && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.pageDisplayCount}
|
||||
target={[1, 99999]}
|
||||
label={t('board.page_display_count')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="newDisplayDayCount"
|
||||
control={control}
|
||||
rules={{ required: true, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.new_display_day_count')}
|
||||
name="newDisplayDayCount"
|
||||
required
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.new_display_day_count'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.newDisplayDayCount && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.newDisplayDayCount}
|
||||
target={[1, 99999]}
|
||||
label={t('board.new_display_day_count')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('board.editor_use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="editorUseAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) =>
|
||||
getSwitch(onChange, ref, value)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('board.user_write_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="userWriteAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) =>
|
||||
getSwitch(onChange, ref, value)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('board.upload_use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="uploadUseAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onClick={handleChangeUploadUseAt}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('board.comment_use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="commentUseAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) =>
|
||||
getSwitch(onChange, ref, value)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} hidden={!uploadUseAt}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="uploadLimitCount"
|
||||
control={control}
|
||||
rules={{ required: uploadUseAt, min: 1, max: 99999 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.upload_limit_count')}
|
||||
name="uploadLimitCount"
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.upload_limit_count'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.uploadLimitCount && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.uploadLimitCount}
|
||||
target={[1, 99999]}
|
||||
label={t('board.upload_limit_count')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} hidden={!uploadUseAt}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="uploadLimitSize"
|
||||
control={control}
|
||||
rules={{
|
||||
required: uploadUseAt,
|
||||
min: 1,
|
||||
max: 99999999999999999999,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('board.upload_limit_size')}
|
||||
name="uploadLimitSize"
|
||||
type="number"
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('board.upload_limit_size'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.uploadLimitSize && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.uploadLimitSize}
|
||||
target={[1, 99999]}
|
||||
label={t('board.upload_limit_size')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const boardNo = Number(query.id)
|
||||
|
||||
let data = {}
|
||||
let skinTypeCodeList = []
|
||||
|
||||
try {
|
||||
const codeList = await codeService.getCodeDetailList('skin_type_code')
|
||||
if (codeList) {
|
||||
skinTypeCodeList = (await codeList.data) as ICode[]
|
||||
}
|
||||
|
||||
if (boardNo !== -1) {
|
||||
const result = await boardService.get(boardNo)
|
||||
if (result) {
|
||||
data = (await result.data) as BoardSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`board item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
boardNo,
|
||||
initData: data,
|
||||
skinTypeCodeList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default BoardItem
|
||||
277
frontend/admin/src/pages/board/index.tsx
Normal file
277
frontend/admin/src/pages/board/index.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { CustomButtons, IButtonProps } from '@components/Buttons'
|
||||
import { Page, rownum } from '@utils'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
|
||||
// api
|
||||
import { boardService } from '@service'
|
||||
import { PopupProps } from '@components/DialogPopup'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import usePage from '@hooks/usePage'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
buttons: IButtonProps[],
|
||||
t?: TFunction,
|
||||
handlePopup?: (row: any) => void,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (data, buttons, t, handlePopup) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'boardName',
|
||||
headerName: t('board.board_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'skinTypeCodeName',
|
||||
headerName: t('board.skin_type_code'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 250,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return handlePopup ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopup(params.row)
|
||||
}}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
{t('common.select')}
|
||||
</Button>
|
||||
) : (
|
||||
<CustomButtons buttons={buttons} row={params.row} />
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'board'
|
||||
export type BoardProps = PopupProps
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Board: NextPage<BoardProps> = props => {
|
||||
// props 및 전역변수
|
||||
const { handlePopup } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'boardName',
|
||||
label: t('board.board_name'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = boardService.search({
|
||||
keywordType: keywordState?.keywordType || 'boardName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const handleDelete = useCallback(
|
||||
(row: any) => {
|
||||
const { boardNo } = row
|
||||
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
boardService.delete({
|
||||
boardNo,
|
||||
callback: () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
mutate()
|
||||
},
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getColumns(
|
||||
data,
|
||||
[
|
||||
{
|
||||
label: `${t('posts')} ${t('common.manage')}`,
|
||||
variant: 'outlined',
|
||||
size: 'small',
|
||||
handleButton: (row: any) => {
|
||||
route.push(`/posts/${row.boardNo}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('label.button.edit'),
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
handleButton: (row: any) => {
|
||||
route.push(`/board/${row.boardNo}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('label.button.delete'),
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.delete'),
|
||||
handleButton: handleDelete,
|
||||
completeMessage: t('msg.success.delete'),
|
||||
},
|
||||
],
|
||||
t,
|
||||
handlePopup,
|
||||
),
|
||||
[data, t, handleDelete, handlePopup, route],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
route.push('board/-1')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={handlePopup ? undefined : handleRegister}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.boardNo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Board
|
||||
293
frontend/admin/src/pages/code/[id].tsx
Normal file
293
frontend/admin/src/pages/code/[id].tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import CardHeader from '@material-ui/core/CardHeader'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { CodeSavePayload, codeService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
content: {
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ICodeFormInput {
|
||||
codeId: string
|
||||
codeName: string
|
||||
codeDescription: string
|
||||
sortSeq: number
|
||||
useAt: boolean
|
||||
}
|
||||
|
||||
export interface ICodeItemsProps {
|
||||
id: string
|
||||
initData: CodeSavePayload | null
|
||||
}
|
||||
|
||||
const CodeItem = ({ id, initData }: ICodeItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//form hook
|
||||
const methods = useForm<ICodeFormInput>({
|
||||
defaultValues: {
|
||||
codeId: initData?.codeId || '',
|
||||
codeName: initData?.codeName || '',
|
||||
codeDescription: initData?.codeDescription || '',
|
||||
sortSeq: initData?.sortSeq || 0,
|
||||
useAt: typeof initData?.useAt !== 'undefined' ? initData?.useAt : true,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
// 코드ID disabled
|
||||
const disabled = id !== '-1'
|
||||
|
||||
// <목록, 저장> 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
//onsubmit 저장
|
||||
const onSubmit = async (formData: ICodeFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
const saved: CodeSavePayload = {
|
||||
codeId: formData.codeId,
|
||||
codeName: formData.codeName,
|
||||
codeDescription: formData.codeDescription,
|
||||
useAt: formData.useAt,
|
||||
sortSeq: formData.sortSeq,
|
||||
}
|
||||
|
||||
if (id === '-1') {
|
||||
codeService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
codeService.update({
|
||||
id,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card className={classes.root}>
|
||||
<CardHeader title={t('code.title')} />
|
||||
<Divider />
|
||||
<CardContent className={classes.content}>
|
||||
<Controller
|
||||
name="codeId"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 20 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code_id')}
|
||||
name="codeId"
|
||||
required
|
||||
variant="outlined"
|
||||
disabled={disabled}
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeId && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeId}
|
||||
target={[20]}
|
||||
label={t('code.code_id')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="codeName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 500 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code_name')}
|
||||
name="codeName"
|
||||
required
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeName}
|
||||
target={[500]}
|
||||
label={t('code.code_name')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="codeDescription"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 500 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code_description')}
|
||||
name="codeDescription"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeDescription && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeDescription}
|
||||
target={[500]}
|
||||
label={t('code.code_description')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="sortSeq"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 3 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
label={t('common.sort_seq')}
|
||||
name="sortSeq"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.sortSeq && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.sortSeq}
|
||||
target={[3]}
|
||||
label={t('common.sort_seq')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
label={t('common.use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="useAt"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 3 }}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</FormProvider>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.push('/code')
|
||||
}}
|
||||
handleSave={handleSubmit(onSubmit)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (id !== '-1') {
|
||||
const result = await codeService.getOne(id as string)
|
||||
if (result) {
|
||||
data = (await result.data) as CodeSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`code item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
id,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeItem
|
||||
362
frontend/admin/src/pages/code/detail/[id].tsx
Normal file
362
frontend/admin/src/pages/code/detail/[id].tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import CardHeader from '@material-ui/core/CardHeader'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import FormControl from '@material-ui/core/FormControl'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import InputLabel from '@material-ui/core/InputLabel'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { CodeSavePayload, codeService, ICode } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
content: {
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
|
||||
},
|
||||
formControl: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
minWidth: 120,
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ICodeFormInput {
|
||||
parentCodeId: string
|
||||
codeId: string
|
||||
codeName: string
|
||||
codeDescription: string
|
||||
sortSeq: number
|
||||
useAt: boolean
|
||||
}
|
||||
|
||||
export interface ICodeItemsProps {
|
||||
id: string
|
||||
parentCodes: ICode[]
|
||||
initData: CodeSavePayload | null
|
||||
}
|
||||
|
||||
const CodeItem = ({ id, parentCodes, initData }: ICodeItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//form hook
|
||||
const methods = useForm<ICodeFormInput>({
|
||||
defaultValues: {
|
||||
parentCodeId: initData?.parentCodeId || '',
|
||||
codeId: initData?.codeId || '',
|
||||
codeName: initData?.codeName || '',
|
||||
codeDescription: initData?.codeDescription || '',
|
||||
sortSeq: initData?.sortSeq || 0,
|
||||
useAt: typeof initData?.useAt !== 'undefined' ? initData?.useAt : true,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
// 코드ID disabled
|
||||
const disabled = Object.keys(initData).length > 0
|
||||
|
||||
// <목록, 저장> 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
//onsubmit 저장
|
||||
const onSubmit = async (formData: ICodeFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
const saved: CodeSavePayload = {
|
||||
parentCodeId: formData.parentCodeId,
|
||||
codeId: formData.codeId,
|
||||
codeName: formData.codeName,
|
||||
codeDescription: formData.codeDescription,
|
||||
useAt: formData.useAt,
|
||||
sortSeq: formData.sortSeq,
|
||||
}
|
||||
|
||||
if (id === '-1') {
|
||||
codeService.saveDetail({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
codeService.updateDetail({
|
||||
id,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card className={classes.root}>
|
||||
<CardHeader title={t('code.title')} />
|
||||
<Divider />
|
||||
<CardContent className={classes.content}>
|
||||
<FormControl
|
||||
variant="outlined"
|
||||
className={classes.formControl}
|
||||
>
|
||||
<InputLabel id="parentCodeId-label" required>
|
||||
{t('code.code_id')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="parentCodeId"
|
||||
control={control}
|
||||
defaultValue={initData?.parentCodeId || ''}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="parentCodeId"
|
||||
required
|
||||
labelId="parentCodeId-label"
|
||||
label={t('code.code_id')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
disabled={disabled}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>{t('code.code_id')}</em>
|
||||
</MenuItem>
|
||||
{parentCodes.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Controller
|
||||
name="codeId"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 20 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code')}
|
||||
name="codeId"
|
||||
required
|
||||
variant="outlined"
|
||||
disabled={disabled}
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeId && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeId}
|
||||
target={[20]}
|
||||
label={t('code.code')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="codeName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 500 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code_name')}
|
||||
name="codeName"
|
||||
required
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeName}
|
||||
target={[500]}
|
||||
label={t('code.code_name')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="codeDescription"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 500 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('code.code_description')}
|
||||
name="codeDescription"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.codeDescription && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.codeDescription}
|
||||
target={[500]}
|
||||
label={t('code.code_description')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="sortSeq"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 3 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
label={t('common.sort_seq')}
|
||||
name="sortSeq"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.sortSeq && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.sortSeq}
|
||||
target={[3]}
|
||||
label={t('common.sort_seq')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
label={t('common.use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="useAt"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 3 }}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.push('/code/detail')
|
||||
}}
|
||||
handleSave={handleSubmit(onSubmit)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({
|
||||
req,
|
||||
res,
|
||||
query,
|
||||
}) => {
|
||||
const { id } = query
|
||||
|
||||
let data = {}
|
||||
let parentCodes = []
|
||||
|
||||
try {
|
||||
// 신규시에는 사용여부 true인 상위공통코드를 가져오고, 수정시에는 현재 상위공통코드 하나만 가져온다
|
||||
if (id === '-1') {
|
||||
const codeList = await codeService.getParentCodeList()
|
||||
if (codeList) {
|
||||
parentCodes = (await codeList.data) as ICode[]
|
||||
}
|
||||
} else {
|
||||
const parentCode = await codeService.getParentCode(id as string)
|
||||
if (parentCode) {
|
||||
parentCodes.push((await parentCode.data) as ICode[])
|
||||
}
|
||||
|
||||
const result = await codeService.getOneDetail(id as string)
|
||||
if (result) {
|
||||
data = (await result.data) as CodeSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`codes query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
id,
|
||||
parentCodes,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeItem
|
||||
318
frontend/admin/src/pages/code/detail/index.tsx
Normal file
318
frontend/admin/src/pages/code/detail/index.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search from '@components/Search'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
//api
|
||||
import { codeService, ICode } from '@service'
|
||||
import { conditionAtom, conditionValue, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps, NextPage } from 'next'
|
||||
import { TFunction } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
search: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
width: '20vw',
|
||||
minWidth: 80,
|
||||
maxWidth: 200,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
//그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
deleteCode: (id: string) => void,
|
||||
updateCode: (id: string) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
deleteCode,
|
||||
updateCode,
|
||||
toggleIsUse,
|
||||
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: 'parentCodeId',
|
||||
headerName: t('code.code_id'), // 코드ID
|
||||
headerAlign: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'codeId',
|
||||
headerName: t('code.code'), // 코드
|
||||
headerAlign: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'codeName',
|
||||
headerName: t('code.code_name'), // 코드명
|
||||
headerAlign: 'center',
|
||||
width: 300,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'useAt',
|
||||
headerName: t('common.use_at'), // 사용여부
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleIsUse(event, params.row.codeId)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
headerName: t('common.manage'), // 관리
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<GridButtons
|
||||
id={params.row.codeId as string}
|
||||
handleUpdate={updateCode}
|
||||
handleDelete={deleteCode}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
interface IParentCodeProps {
|
||||
parentCodes: ICode[]
|
||||
}
|
||||
|
||||
const conditionKey = 'code-detail'
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const CodeDetail: NextPage<IParentCodeProps> = ({ parentCodes }) => {
|
||||
// props 및 전역변수
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'codeId',
|
||||
label: t('code.code_id'),
|
||||
},
|
||||
{
|
||||
key: 'codeName',
|
||||
label: t('code.code_name'),
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
//조회조건 상태관리
|
||||
const [keywordState, setKeywordState] = useRecoilState(
|
||||
conditionAtom(conditionKey),
|
||||
)
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 공통코드 관리 기능에서 넘어오는 경우 parameter
|
||||
const queryParentCodeId = route.query.parentCodeId as string
|
||||
|
||||
//현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue>({
|
||||
parentCodeId: keywordState?.parentCodeId || queryParentCodeId || '-',
|
||||
})
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = codeService.searchDetail({
|
||||
parentCodeId: keywordState?.parentCodeId || queryParentCodeId || '',
|
||||
keywordType: keywordState?.keywordType || 'codeId',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
//에러 callback
|
||||
const errorCallback = useCallback((error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//삭제
|
||||
const deleteCode = useCallback((id: string) => {
|
||||
codeService.deleteDetail({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//수정 시 상세 화면 이동
|
||||
const updateCode = useCallback((id: string) => {
|
||||
route.push(`/code/detail/${id}`)
|
||||
}, [])
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: string) => {
|
||||
codeService.updateUseDetail({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
useAt: event.target.checked,
|
||||
})
|
||||
},
|
||||
[page, customKeyword],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(data, deleteCode, updateCode, toggleIsUse, t)
|
||||
}, [data])
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// 조회조건 select onchange
|
||||
const handleParentCodeIdChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
event.preventDefault()
|
||||
setCustomKeyword({
|
||||
parentCodeId: event.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('/code/detail/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
isNotWrapper={true}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-parentCodeId"
|
||||
select
|
||||
value={customKeyword.parentCodeId}
|
||||
onChange={handleParentCodeIdChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="-" value="-">
|
||||
<em>{t('code.code_id')}</em>
|
||||
</MenuItem>
|
||||
{parentCodes.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.codeId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let parentCodes = []
|
||||
|
||||
try {
|
||||
const codeList = await codeService.getParentCodeList()
|
||||
if (codeList) {
|
||||
parentCodes = (await codeList.data) as ICode[]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`codes query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
parentCodes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeDetail
|
||||
281
frontend/admin/src/pages/code/index.tsx
Normal file
281
frontend/admin/src/pages/code/index.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search from '@components/Search'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Button from '@material-ui/core/Button'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
//api
|
||||
import { codeService } from '@service'
|
||||
import { conditionAtom, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
//그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
routeCodeDetail: (id: string) => void,
|
||||
deleteCode: (id: string) => void,
|
||||
updateCode: (id: string) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
routeCodeDetail,
|
||||
deleteCode,
|
||||
updateCode,
|
||||
toggleIsUse,
|
||||
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: 'codeId',
|
||||
headerName: t('code.code_id'), // 코드ID
|
||||
headerAlign: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'codeName',
|
||||
headerName: t('code.code_name'), // 코드명
|
||||
headerAlign: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'useAt',
|
||||
headerName: t('common.use_at'), // 사용여부
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleIsUse(event, params.row.codeId)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'codeDetailCount',
|
||||
headerName: t('code.detail_count'), // 코드상세수
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
headerName: t('common.manage'), // 관리
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 300,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>
|
||||
<Box mr={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="default"
|
||||
size="small"
|
||||
onClick={() => routeCodeDetail(params.row.codeId)}
|
||||
>
|
||||
{t('code.detail.list')}
|
||||
</Button>
|
||||
</Box>
|
||||
<GridButtons
|
||||
id={params.row.codeId as string}
|
||||
handleUpdate={updateCode}
|
||||
handleDelete={deleteCode}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const conditionKey = 'code'
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Code: NextPage = () => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'codeId',
|
||||
label: t('code.code_id'),
|
||||
},
|
||||
{
|
||||
key: 'codeName',
|
||||
label: t('code.code_name'),
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = codeService.search({
|
||||
keywordType: keywordState?.keywordType || 'codeId',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
//에러 callback
|
||||
const errorCallback = useCallback((error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 코드상세목록
|
||||
const routeCodeDetail = useCallback((id: string) => {
|
||||
route.push(
|
||||
{
|
||||
pathname: '/code/detail',
|
||||
query: {
|
||||
parentCodeId: id,
|
||||
},
|
||||
},
|
||||
'/code/detail',
|
||||
)
|
||||
}, [])
|
||||
|
||||
//삭제
|
||||
const deleteCode = useCallback((id: string) => {
|
||||
codeService.delete({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//수정 시 상세 화면 이동
|
||||
const updateCode = useCallback((id: string) => {
|
||||
route.push(`/code/${id}`)
|
||||
}, [])
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: string) => {
|
||||
codeService.updateUse({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
useAt: event.target.checked,
|
||||
})
|
||||
},
|
||||
[page],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(
|
||||
data,
|
||||
routeCodeDetail,
|
||||
deleteCode,
|
||||
updateCode,
|
||||
toggleIsUse,
|
||||
t,
|
||||
)
|
||||
}, [data])
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('code/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.codeId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Code
|
||||
265
frontend/admin/src/pages/content/[id].tsx
Normal file
265
frontend/admin/src/pages/content/[id].tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Editor from '@components/Editor'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { ContentSavePayload, contentService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IContentFormInput {
|
||||
contentName: string
|
||||
contentRemark: string
|
||||
contentValue: string
|
||||
}
|
||||
|
||||
export interface IContentItemsProps {
|
||||
contentNo: string
|
||||
initData: ContentSavePayload | null
|
||||
}
|
||||
|
||||
const ContentItem = ({ contentNo, initData }: IContentItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// alert
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => setCustomAlert({ open: false }),
|
||||
})
|
||||
|
||||
// Editor
|
||||
const [contentValue, setContentValue] = useState<string>(
|
||||
initData?.contentValue || '',
|
||||
)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IContentFormInput>({
|
||||
defaultValues: {
|
||||
contentName: initData?.contentName || '',
|
||||
contentRemark: initData?.contentRemark || '',
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IContentFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const saved: ContentSavePayload = {
|
||||
contentName: formData.contentName,
|
||||
contentRemark: formData.contentRemark,
|
||||
contentValue,
|
||||
}
|
||||
|
||||
if (!contentValue) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.required.format'), [
|
||||
t('content.content_value'),
|
||||
]),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (contentNo === '-1') {
|
||||
await contentService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
await contentService.update({
|
||||
contentNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="contentName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('content.content_name')}
|
||||
name="contentName"
|
||||
required
|
||||
inputProps={{ maxLength: 100 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('content.content_name'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.contentName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.contentName}
|
||||
target={[100]}
|
||||
label={t('content.content_name')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="contentRemark"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 200 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('content.content_remark')}
|
||||
name="contentRemark"
|
||||
inputProps={{ maxLength: 200 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('content.content_remark'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.contentRemark && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.contentRemark}
|
||||
target={[200]}
|
||||
label={t('content.content_remark')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Editor contents={contentValue} setContents={setContentValue} />
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const contentNo = query.id as string
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (contentNo !== '-1') {
|
||||
const result = await contentService.get(contentNo)
|
||||
if (result) {
|
||||
data = (await result.data) as ContentSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`content item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
contentNo,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentItem
|
||||
242
frontend/admin/src/pages/content/index.tsx
Normal file
242
frontend/admin/src/pages/content/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import { PopupProps } from '@components/DialogPopup'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Button from '@material-ui/core/Button'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import { contentService } from '@service'
|
||||
import { conditionAtom, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
deleteContent: (contentNo: string) => void,
|
||||
updateContent: (contentNo: string) => void,
|
||||
t?: TFunction,
|
||||
handlePopup?: (row: any) => void,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
deleteContent,
|
||||
updateContent,
|
||||
t,
|
||||
handlePopup,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'contentName',
|
||||
headerName: t('content.content_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'lastModifiedBy',
|
||||
headerName: t('common.last_modified_by'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'modifiedDate',
|
||||
headerName: t('common.modified_date'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: handlePopup ? t('common.select') : t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return handlePopup ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopup(params.row)
|
||||
}}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
{t('common.select')}
|
||||
</Button>
|
||||
) : (
|
||||
<GridButtons
|
||||
id={params.row.contentNo as string}
|
||||
handleDelete={deleteContent}
|
||||
handleUpdate={updateContent}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'content'
|
||||
export interface ContentProps extends PopupProps {}
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Content: NextPage<ContentProps> = props => {
|
||||
// props 및 전역변수
|
||||
const { handlePopup } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'contentName',
|
||||
label: t('content.content_name'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = contentService.search({
|
||||
keywordType: keywordState?.keywordType || 'contentName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const deleteContent = useCallback(
|
||||
(contentNo: string) => {
|
||||
contentService.delete({
|
||||
contentNo,
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate],
|
||||
)
|
||||
|
||||
// 수정 시 상세 화면 이동
|
||||
const updateContent = useCallback(
|
||||
(contentNo: string) => {
|
||||
route.push(`/content/${contentNo}`)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, deleteContent, updateContent, t, handlePopup),
|
||||
[data, deleteContent, updateContent, t, handlePopup],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
route.push('content/-1')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={handlePopup ? undefined : handleRegister}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.contentNo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Content
|
||||
25
frontend/admin/src/pages/index.tsx
Normal file
25
frontend/admin/src/pages/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Loader from '@components/Loader'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 통계 페이지로 redirect
|
||||
*/
|
||||
const Home = () => {
|
||||
return (
|
||||
<>
|
||||
<Loader />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: '/statistics',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Home
|
||||
234
frontend/admin/src/pages/location/[id].tsx
Normal file
234
frontend/admin/src/pages/location/[id].tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import { Card, CardActions, CardContent } from '@material-ui/core'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import CardHeader from '@material-ui/core/CardHeader'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { ILocation, locationService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
},
|
||||
cardActions: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
switch: {
|
||||
width: '100%',
|
||||
justifyContent: 'start',
|
||||
border: '1px solid rgba(0, 0, 0, 0.23)',
|
||||
borderRadius: theme.spacing(0.5),
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface LocationDetailProps {
|
||||
locationId: string
|
||||
initData?: ILocation
|
||||
}
|
||||
|
||||
const LocationDetail = ({ locationId, initData }: LocationDetailProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//form hook
|
||||
const methods = useForm<ILocation>({
|
||||
defaultValues: initData,
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
//상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
// <목록, 저장> 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const handleSave = async (formData: ILocation) => {
|
||||
setSuccessSnackBar('loading')
|
||||
try {
|
||||
let result
|
||||
if (locationId === '-1') {
|
||||
result = await locationService.save(formData)
|
||||
} else {
|
||||
result = await locationService.update(parseInt(locationId), formData)
|
||||
}
|
||||
|
||||
if (result) {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
} catch (error) {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = () => {
|
||||
route.back()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Card className={classes.card}>
|
||||
<CardHeader title={t('location')} />
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Controller
|
||||
name="locationName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 200 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('location.name')}
|
||||
name="locationName"
|
||||
required
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
defaultValue={''}
|
||||
/>
|
||||
{errors.locationName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.locationName}
|
||||
target={[200]}
|
||||
label={t('location.name')}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="sortSeq"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: 3,
|
||||
pattern: {
|
||||
value: /^[0-9]*$/,
|
||||
message: t('valid.valueAsNumber'),
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('common.sort_seq')}
|
||||
name="sortSeq"
|
||||
required
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
defaultValue={null}
|
||||
/>
|
||||
{errors.sortSeq && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.sortSeq}
|
||||
target={[3]}
|
||||
label={t('common.sort_seq')}
|
||||
/>
|
||||
)}
|
||||
<Box className={classes.switch}>
|
||||
<FormControlLabel
|
||||
label={t('common.use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="isUse"
|
||||
control={control}
|
||||
rules={{ required: false, maxLength: 3 }}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions className={classes.cardActions}>
|
||||
<DetailButtons
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
handleList={handleList}
|
||||
/>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const locationId = query.id as string
|
||||
|
||||
if (locationId === '-1') {
|
||||
return {
|
||||
props: {
|
||||
locationId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
const result = await locationService.get(parseInt(locationId))
|
||||
|
||||
if (result) {
|
||||
data = (await result.data) as ILocation
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`content item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
locationId,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default LocationDetail
|
||||
221
frontend/admin/src/pages/location/index.tsx
Normal file
221
frontend/admin/src/pages/location/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import { locationService } from '@service'
|
||||
import { conditionAtom, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
const conditionKey = 'location'
|
||||
|
||||
type ColumnType = (
|
||||
data: Page,
|
||||
handleDelete: (id: number) => void,
|
||||
handleUpdate: (id: number) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void,
|
||||
t: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
//그리드 컬럼 정의
|
||||
const getColumns: ColumnType = (
|
||||
data: Page,
|
||||
handleDelete: (id: number) => void,
|
||||
handleUpdate: (id: number) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void,
|
||||
t,
|
||||
) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'locationName',
|
||||
headerName: t('location.name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'isUse',
|
||||
headerName: t('common.use_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleIsUse(event, params.row.locationId)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'locationId',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<GridButtons
|
||||
id={params.value as string}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const Location = () => {
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
// 에러 상태관리
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
// pagination 상태관리
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'locationName',
|
||||
label: t('location.name'),
|
||||
},
|
||||
])
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = locationService.search({
|
||||
keywordType: keywordState?.keywordType || 'locationName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
router.push('location/-1')
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
const result = await locationService.delete(id)
|
||||
if (result?.status === 204) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
router.push(`location/${id}`)
|
||||
}
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: number) => {
|
||||
try {
|
||||
const result = await locationService.updateUse(id, event.target.checked)
|
||||
if (result?.status === 204) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
},
|
||||
[page],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(data, handleDelete, handleUpdate, toggleIsUse, t)
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={handleRegister}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.locationId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Location
|
||||
412
frontend/admin/src/pages/menu/index.tsx
Normal file
412
frontend/admin/src/pages/menu/index.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import { ConfirmDialog } from '@components/Confirm'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import DraggableTreeMenu from '@components/DraggableTreeMenu'
|
||||
import TreeSubButtons from '@components/DraggableTreeMenu/TreeSubButtons'
|
||||
import { findTreeItem } from '@components/DraggableTreeMenu/TreeUtils'
|
||||
import { MenuEditForm } from '@components/EditForm'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import AddIcon from '@material-ui/icons/Add'
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import SettingsIcon from '@material-ui/icons/Settings'
|
||||
import {
|
||||
codeService,
|
||||
ICode,
|
||||
IMenuInfoForm,
|
||||
IMenuSavePayload,
|
||||
IMenuTree,
|
||||
ISite,
|
||||
menuService,
|
||||
} from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
draggableTreeExpandedAtom,
|
||||
draggableTreeSelectedAtom,
|
||||
errorStateSelector,
|
||||
treeChangeNameAtom,
|
||||
} from '@stores'
|
||||
import produce from 'immer'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
background: theme.palette.background.default,
|
||||
},
|
||||
buttons: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
info: {
|
||||
minHeight: 350,
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export interface MenuProps {
|
||||
sites: ISite[]
|
||||
menuTypes?: ICode[]
|
||||
}
|
||||
|
||||
const conditionKey = 'menu'
|
||||
|
||||
const defaultMenu: IMenuSavePayload = {
|
||||
name: 'newMenu',
|
||||
parentId: null,
|
||||
siteId: null,
|
||||
sortSeq: 1,
|
||||
level: 1,
|
||||
isShow: true,
|
||||
isUse: true,
|
||||
}
|
||||
|
||||
interface ICustomAlertState {
|
||||
open: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export const MenuFormContext = createContext<{
|
||||
menuFormData: IMenuInfoForm
|
||||
setMenuFormDataHandler: (data: IMenuInfoForm) => void
|
||||
}>({
|
||||
menuFormData: undefined,
|
||||
setMenuFormDataHandler: () => {},
|
||||
})
|
||||
|
||||
const Menu = ({ sites, menuTypes }: MenuProps) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [menuFormData, setMenuFormData] = useState<IMenuInfoForm>(undefined)
|
||||
const setMenuFormDataHandler = (data: IMenuInfoForm) => {
|
||||
setMenuFormData(data)
|
||||
}
|
||||
|
||||
const [siteState, setSiteState] = useState<number>(
|
||||
+keywordState?.siteId || sites[0]?.id,
|
||||
)
|
||||
const setExpanded = useSetRecoilState(draggableTreeExpandedAtom)
|
||||
const [treeSelected, setTreeSelected] = useRecoilState(
|
||||
draggableTreeSelectedAtom,
|
||||
)
|
||||
const [treeChangeName, setTreeChangeName] = useRecoilState(treeChangeNameAtom)
|
||||
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const [customAlertState, setCustomAlertState] = useState<ICustomAlertState>({
|
||||
open: false,
|
||||
message: '',
|
||||
})
|
||||
const [deleteConfirmState, setDeleteConfirmState] =
|
||||
useState<ICustomAlertState>({
|
||||
open: false,
|
||||
message: t('msg.confirm.delete'),
|
||||
})
|
||||
|
||||
const { data, mutate, error } = menuService.getTreeMenus(siteState)
|
||||
|
||||
useEffect(() => {
|
||||
if (treeSelected) {
|
||||
menuService
|
||||
.getMenu(treeSelected.menuId)
|
||||
.then(result => {
|
||||
setMenuFormDataHandler(result)
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState({ error })
|
||||
})
|
||||
}
|
||||
}, [treeSelected])
|
||||
|
||||
useEffect(() => {
|
||||
if (treeChangeName.state === 'complete') {
|
||||
menuService
|
||||
.updateName(treeChangeName.id, treeChangeName.name)
|
||||
.then(result => {
|
||||
setTreeChangeName({
|
||||
state: 'none',
|
||||
})
|
||||
mutate().then(result => {
|
||||
const selected = findTreeItem(result, treeSelected.menuId, 'menuId')
|
||||
setTreeSelected(selected.item)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
setErrorState({ error })
|
||||
})
|
||||
}
|
||||
}, [treeChangeName])
|
||||
|
||||
const handleSiteChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setSiteState(event.target.value as number)
|
||||
}
|
||||
|
||||
const handleSave = async (formData: IMenuInfoForm) => {
|
||||
console.log(formData)
|
||||
setSuccessSnackBar('loading')
|
||||
try {
|
||||
const result = await menuService.update(treeSelected.menuId, formData)
|
||||
setSuccessSnackBar('success')
|
||||
if (result) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
setSuccessSnackBar('none')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
let addMenu: IMenuSavePayload = produce(defaultMenu, draft => {
|
||||
draft.siteId = siteState
|
||||
draft.sortSeq = data?.length + 1
|
||||
draft.name = t('menu.new_menu')
|
||||
})
|
||||
if (treeSelected) {
|
||||
addMenu = produce(addMenu, draft => {
|
||||
draft.parentId = treeSelected.menuId
|
||||
draft.level = treeSelected.level + 1
|
||||
draft.sortSeq =
|
||||
treeSelected.children.length > 0
|
||||
? treeSelected.children[treeSelected.children.length - 1].sortSeq +
|
||||
1
|
||||
: 1
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await menuService.save(addMenu)
|
||||
if (result) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
if (!treeSelected) {
|
||||
setCustomAlertState({
|
||||
open: true,
|
||||
message: t('menu.valid.delete'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setDeleteConfirmState({
|
||||
...deleteConfirmState,
|
||||
open: true,
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeNameClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!treeSelected) {
|
||||
setCustomAlertState({
|
||||
open: true,
|
||||
message: t('menu.valid.change_name'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setTreeChangeName({
|
||||
state: 'change',
|
||||
id: null,
|
||||
name: null,
|
||||
})
|
||||
}
|
||||
|
||||
const handleExpand = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
setExpanded('expand')
|
||||
}
|
||||
|
||||
const handleCollapse = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
setExpanded('collapse')
|
||||
}
|
||||
|
||||
const handleDeselect = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
setTreeSelected(undefined)
|
||||
}
|
||||
|
||||
const handleAlert = () => {
|
||||
setCustomAlertState({
|
||||
...customAlertState,
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirmClose = () => {
|
||||
setDeleteConfirmState({
|
||||
...deleteConfirmState,
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
handleConfirmClose()
|
||||
|
||||
try {
|
||||
await menuService.delete(treeSelected.menuId)
|
||||
|
||||
enqueueSnackbar(t('msg.success.delete'), {
|
||||
variant: 'success',
|
||||
})
|
||||
mutate()
|
||||
setTreeSelected(undefined)
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const handleTreeDnD = async (tree: IMenuTree[]) => {
|
||||
try {
|
||||
const result = await menuService.updateDnD(siteState, tree)
|
||||
|
||||
mutate()
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item sm={12} md={4}>
|
||||
<Paper className={classes.paper}>
|
||||
<Select fullWidth value={siteState} onChange={handleSiteChange}>
|
||||
{sites?.map(item => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<ButtonGroup
|
||||
className={classes.buttons}
|
||||
size="small"
|
||||
aria-label="menu tree buttons"
|
||||
>
|
||||
<Button color="primary" onClick={handleAddClick}>
|
||||
<AddIcon fontSize="small" />
|
||||
{t('label.button.add')}
|
||||
</Button>
|
||||
<Button onClick={handleChangeNameClick}>
|
||||
<SettingsIcon fontSize="small" />
|
||||
{t('menu.update_name')}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={handleDeleteClick}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
{t('label.button.delete')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{data && (
|
||||
<DraggableTreeMenu handleTreeDnD={handleTreeDnD} data={data} />
|
||||
)}
|
||||
<TreeSubButtons
|
||||
handleExpand={handleExpand}
|
||||
handleCollapse={handleCollapse}
|
||||
handleDeselect={handleDeselect}
|
||||
/>
|
||||
</Paper>
|
||||
<CustomAlert
|
||||
contentText={customAlertState.message}
|
||||
open={customAlertState.open}
|
||||
handleAlert={handleAlert}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={deleteConfirmState.open}
|
||||
contentText={deleteConfirmState.message}
|
||||
handleClose={handleConfirmClose}
|
||||
handleConfirm={handleConfirm}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item sm={12} md={8}>
|
||||
<MenuFormContext.Provider
|
||||
value={{ menuFormData, setMenuFormDataHandler }}
|
||||
>
|
||||
<Paper className={classes.paper}>
|
||||
{treeSelected ? (
|
||||
<MenuEditForm handleSave={handleSave} menuTypes={menuTypes} />
|
||||
) : (
|
||||
<Card className={classes.info}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h2">
|
||||
Tip. 메뉴 관리
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p">
|
||||
1. 왼쪽트리메뉴에서 메뉴를 선택 하시면 해당메뉴의 정보를
|
||||
조회/관리할 수 있습니다.
|
||||
<br />
|
||||
2. 드래그앤드랍 으로 선택된 메뉴를 이동시킬 수 있습니다.
|
||||
<br />
|
||||
3. 메뉴정보를 편집하시려면 메뉴를 선택하세요.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Paper>
|
||||
</MenuFormContext.Provider>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let sites: ISite[] = []
|
||||
let menuTypes: ICode[] = []
|
||||
|
||||
try {
|
||||
const result = await menuService.getSites()
|
||||
|
||||
if (sites) {
|
||||
sites = result
|
||||
}
|
||||
|
||||
const codeDetails = await codeService.getCodeDetailList('menutype')
|
||||
if (codeDetails) {
|
||||
menuTypes = codeDetails.data as ICode[]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`menu getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
sites,
|
||||
menuTypes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Menu
|
||||
215
frontend/admin/src/pages/menu/role/index.tsx
Normal file
215
frontend/admin/src/pages/menu/role/index.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { DetailButtons } from '@components/Buttons/DetailButtons'
|
||||
import CustomTreeView, { CustomTreeViewType } from '@components/CustomTreeView'
|
||||
import TreeSubButtons from '@components/DraggableTreeMenu/TreeSubButtons'
|
||||
import { HorizontalTabs } from '@components/Tabs'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Tab from '@material-ui/core/Tab'
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox'
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
|
||||
import { IRole, ISite, menuService, roleService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React, { createRef, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
import { menuRoleService } from 'src/service/MenuRole'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiButtonGroup-contained': {
|
||||
boxShadow: theme.shadows[0],
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(2),
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
select: {
|
||||
minWidth: 150,
|
||||
maxWidth: 300,
|
||||
},
|
||||
buttons: {
|
||||
padding: theme.spacing(1, 0.5),
|
||||
},
|
||||
buttonGroup: {
|
||||
'& .MuiButton-containedSizeSmall': {
|
||||
padding: '4px 6px',
|
||||
fontSize: '0.8rem',
|
||||
},
|
||||
whiteSpace: 'nowrap',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export interface MenuRoleProps {
|
||||
sites: ISite[]
|
||||
roles: IRole[]
|
||||
}
|
||||
|
||||
const MenuRole = (props: MenuRoleProps) => {
|
||||
const { sites, roles } = props
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const treeViewRef = createRef<CustomTreeViewType>() //treeview Ref
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const [tabs, setTabs] = useState<React.ReactNode>(undefined)
|
||||
const [siteState, setSiteState] = useState<number>(sites ? sites[0].id : null)
|
||||
const [roleState, setRoleState] = useState<string>(
|
||||
roles ? roles[0].roleId : '',
|
||||
)
|
||||
|
||||
const [expanded, setExpanded] = useState<boolean>(null)
|
||||
|
||||
const { data, mutate, error } = menuRoleService.search(roleState, siteState)
|
||||
|
||||
useEffect(() => {
|
||||
if (roles) {
|
||||
const createTabs = roles.map(role => {
|
||||
return (
|
||||
<Tab
|
||||
label={role.roleName}
|
||||
value={role.roleId}
|
||||
key={`tab-${role.roleId}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
setTabs(createTabs)
|
||||
}
|
||||
}, [roles])
|
||||
|
||||
const handleTab = (roleId: string) => {
|
||||
setRoleState(roleId)
|
||||
mutate(data, false)
|
||||
}
|
||||
|
||||
const handleSiteChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setSiteState(event.target.value as number)
|
||||
}
|
||||
|
||||
const handleExpand = () => {
|
||||
setExpanded(true)
|
||||
}
|
||||
|
||||
const handleCollapse = () => {
|
||||
setExpanded(false)
|
||||
}
|
||||
|
||||
const handleAllChecked = () => {
|
||||
treeViewRef.current?.handleAllChecked(true)
|
||||
}
|
||||
|
||||
const handleAllUnchecked = () => {
|
||||
treeViewRef.current?.handleAllChecked(false)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSuccessSnackBar('loading')
|
||||
if (treeViewRef.current) {
|
||||
const tree = treeViewRef.current.getTreeData()
|
||||
try {
|
||||
const result = await menuRoleService.save(tree)
|
||||
setSuccessSnackBar('success')
|
||||
if (result) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
setSuccessSnackBar('none')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{tabs && (
|
||||
<HorizontalTabs tabs={tabs} init={roleState} handleTab={handleTab} />
|
||||
)}
|
||||
<Paper className={classes.paper}>
|
||||
<Select
|
||||
className={classes.select}
|
||||
value={siteState}
|
||||
onChange={handleSiteChange}
|
||||
>
|
||||
{sites?.map(item => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Box className={classes.buttons}>
|
||||
<ButtonGroup
|
||||
className={classes.buttonGroup}
|
||||
size="small"
|
||||
aria-label="menu tree buttons"
|
||||
variant="contained"
|
||||
>
|
||||
<Button onClick={handleAllChecked}>
|
||||
<CheckBoxIcon fontSize="small" />
|
||||
{t('label.button.all_checked')}
|
||||
</Button>
|
||||
<Button onClick={handleAllUnchecked}>
|
||||
<CheckBoxOutlineBlankIcon fontSize="small" />
|
||||
{t('label.button.all_unchecked')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<TreeSubButtons
|
||||
handleExpand={handleExpand}
|
||||
handleCollapse={handleCollapse}
|
||||
/>
|
||||
</Box>
|
||||
{data && (
|
||||
<CustomTreeView
|
||||
ref={treeViewRef}
|
||||
data={data}
|
||||
isChecked={true}
|
||||
isAllExpanded={expanded}
|
||||
/>
|
||||
)}
|
||||
<DetailButtons handleSave={handleSave} />
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let sites: ISite[] = []
|
||||
let roles: IRole[] = []
|
||||
|
||||
try {
|
||||
const siteResult = await menuService.getSites()
|
||||
|
||||
if (siteResult) {
|
||||
sites = siteResult
|
||||
}
|
||||
|
||||
const roleResult = await roleService.searchAll()
|
||||
if (roleResult) {
|
||||
roles = roleResult.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`menu role getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
sites,
|
||||
roles,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default MenuRole
|
||||
270
frontend/admin/src/pages/policy/[id].tsx
Normal file
270
frontend/admin/src/pages/policy/[id].tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Editor from '@components/Editor'
|
||||
import { getCurrentDate } from '@libs/date'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { PolicySavePayload, policyService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
import { IPolicyType } from '.'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
label: {
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IPolicyFormInput {
|
||||
policyType: string
|
||||
isUse: boolean
|
||||
title: string
|
||||
contents: string
|
||||
}
|
||||
|
||||
export interface IPolicyItemsProps {
|
||||
id: string
|
||||
initData: PolicySavePayload | null
|
||||
typeList: IPolicyType[]
|
||||
}
|
||||
|
||||
const PolicyItem = ({ id, initData, typeList }: IPolicyItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//Editor contents
|
||||
const [policyContents, setPolicyContents] = useState<string>(
|
||||
initData?.contents || '',
|
||||
)
|
||||
|
||||
//form hook
|
||||
const methods = useForm<IPolicyFormInput>({
|
||||
defaultValues: {
|
||||
policyType: initData?.type || 'TOS',
|
||||
isUse: typeof initData?.isUse !== 'undefined' ? initData?.isUse : true,
|
||||
title: initData?.title,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
// <목록, 저장> 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
//onsubmit 저장
|
||||
const onSubmit = async (formData: IPolicyFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
const saved: PolicySavePayload = {
|
||||
title: formData.title,
|
||||
isUse: formData.isUse,
|
||||
type: formData.policyType,
|
||||
regDate: id === '-1' ? getCurrentDate() : initData.regDate,
|
||||
contents: policyContents,
|
||||
}
|
||||
|
||||
if (id === '-1') {
|
||||
policyService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
policyService.update({
|
||||
id,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={2}>
|
||||
<Paper className={classes.label}>{t('common.type')}</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Controller
|
||||
name="policyType"
|
||||
render={({ field }) => (
|
||||
<Select variant="outlined" fullWidth {...field}>
|
||||
{typeList?.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
control={control}
|
||||
defaultValue={initData?.type || 'TOS'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={2}>
|
||||
<Paper className={classes.label}>{t('common.use_at')}</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Paper className={classes.switch}>
|
||||
<Controller
|
||||
name="isUse"
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={2}>
|
||||
<Paper className={classes.label}>{t('policy.title')}</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={10}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="title"
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
id="outlined-full-width"
|
||||
placeholder={`${t('policy.title')} ${t(
|
||||
'msg.placeholder',
|
||||
)}`}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={!!fieldState.error}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
{errors.title && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.title}
|
||||
label={t('policy.title')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Editor contents={policyContents} setContents={setPolicyContents} />
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.push('/policy')
|
||||
}}
|
||||
handleSave={handleSubmit(onSubmit)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({
|
||||
req,
|
||||
res,
|
||||
query,
|
||||
}) => {
|
||||
const { id } = query
|
||||
|
||||
let data = {}
|
||||
let typeList = []
|
||||
|
||||
try {
|
||||
const typeResult = await policyService.getTypeList()
|
||||
|
||||
if (typeResult) {
|
||||
typeList = (await typeResult.data) as IPolicyType[]
|
||||
}
|
||||
|
||||
if (id !== '-1') {
|
||||
const result = await policyService.getOne(id as string)
|
||||
if (result) {
|
||||
data = (await result.data) as PolicySavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`policy item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
id,
|
||||
initData: data,
|
||||
typeList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default PolicyItem
|
||||
294
frontend/admin/src/pages/policy/index.tsx
Normal file
294
frontend/admin/src/pages/policy/index.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
//api
|
||||
import { policyService } from '@service'
|
||||
import { conditionAtom, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps, NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
//그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
typeList: IPolicyType[],
|
||||
deletePolicy: (id: string) => void,
|
||||
updatePolicy: (id: string) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
typeList,
|
||||
deletePolicy,
|
||||
updatePolicy,
|
||||
toggleIsUse,
|
||||
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: 'type',
|
||||
headerName: t('common.type'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) => {
|
||||
const type = typeList?.find(item => item.codeId === params.value)
|
||||
return type?.codeName || ''
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
headerName: t('policy.title'),
|
||||
headerAlign: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'isUse',
|
||||
headerName: t('common.use_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleIsUse(event, params.row.id)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'regDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) => {
|
||||
return convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<GridButtons
|
||||
id={params.value as string}
|
||||
handleDelete={deletePolicy}
|
||||
handleUpdate={updatePolicy}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const conditionKey = 'policy'
|
||||
|
||||
export interface IPolicyType {
|
||||
codeId: string
|
||||
codeName: string
|
||||
sortSeq: number
|
||||
}
|
||||
|
||||
export interface IPolicyProps {
|
||||
typeList: IPolicyType[]
|
||||
}
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Policy: NextPage<IPolicyProps> = ({ typeList }) => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
|
||||
const { t } = useTranslation()
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = policyService.search({
|
||||
keywordType: keywordState?.keywordType || 'title',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'title',
|
||||
label: t('policy.title'),
|
||||
},
|
||||
{
|
||||
key: 'contents',
|
||||
label: t('comment.comment_content'),
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
//에러 callback
|
||||
const errorCallback = useCallback((error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//삭제
|
||||
const deletePolicy = useCallback((id: string) => {
|
||||
policyService.delete({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
})
|
||||
}, [])
|
||||
|
||||
//수정 시 상세 화면 이동
|
||||
const updatePolicy = useCallback((id: string) => {
|
||||
route.push(`/policy/${id}`)
|
||||
}, [])
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: string) => {
|
||||
policyService.updateUse({
|
||||
callback: mutate,
|
||||
errorCallback,
|
||||
id,
|
||||
isUse: event.target.checked,
|
||||
})
|
||||
},
|
||||
[page],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(
|
||||
data,
|
||||
typeList,
|
||||
deletePolicy,
|
||||
updatePolicy,
|
||||
toggleIsUse,
|
||||
t,
|
||||
)
|
||||
}, [data])
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('policy/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let typeList: IPolicyType[] = []
|
||||
try {
|
||||
const result = await policyService.getTypeList()
|
||||
if (result) {
|
||||
typeList = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`policy list getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
typeList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Policy
|
||||
499
frontend/admin/src/pages/posts/[board]/edit/[id].tsx
Normal file
499
frontend/admin/src/pages/posts/[board]/edit/[id].tsx
Normal file
@@ -0,0 +1,499 @@
|
||||
import AttachList from '@components/AttachList'
|
||||
import { CustomButtons, IButtonProps } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Editor from '@components/Editor'
|
||||
import { Upload, UploadType } from '@components/Upload'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
BoardSavePayload,
|
||||
boardService,
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
PostsSavePayload,
|
||||
postsService,
|
||||
SKINT_TYPE_CODE_FAQ,
|
||||
SKINT_TYPE_CODE_QNA,
|
||||
UploadInfoReqeust,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
switchBox: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
labelMultiline: {
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
upload: {
|
||||
padding: theme.spacing(2, 2, 0, 2),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IPostsFormInput {
|
||||
postsTitle: string
|
||||
noticeAt: boolean
|
||||
postsContent: string
|
||||
postsAnswerContent: string
|
||||
}
|
||||
|
||||
export interface IPostsItemsProps {
|
||||
boardNo: number
|
||||
postsNo: number
|
||||
board: BoardSavePayload | null
|
||||
initData: PostsSavePayload | null
|
||||
}
|
||||
|
||||
const PostsItem = ({ boardNo, postsNo, board, initData }: IPostsItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const uploadRef = useRef<UploadType>()
|
||||
|
||||
const [attachData, setAttachData] = useState<
|
||||
IAttachmentResponse[] | undefined
|
||||
>(undefined)
|
||||
|
||||
// alert
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => setCustomAlert({ open: false }),
|
||||
})
|
||||
|
||||
// Editor
|
||||
const [postsContent, setPostsContent] = useState<string>(
|
||||
initData?.postsContent || '',
|
||||
)
|
||||
const [postsAnswerContent, setPostsAnswerContent] = useState<string>(
|
||||
initData?.postsAnswerContent || '',
|
||||
)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IPostsFormInput>({
|
||||
defaultValues: {
|
||||
postsTitle: initData?.postsTitle || '',
|
||||
noticeAt:
|
||||
typeof initData?.noticeAt !== 'undefined' ? initData?.noticeAt : false,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
const getAttachments = useCallback(
|
||||
async (code: string) => {
|
||||
try {
|
||||
const result = await fileService.getAttachmentList(code)
|
||||
|
||||
if (result) {
|
||||
setAttachData(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initData.attachmentCode) {
|
||||
getAttachments(initData.attachmentCode)
|
||||
}
|
||||
}, [getAttachments, initData.attachmentCode])
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IPostsFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
let { attachmentCode } = initData
|
||||
try {
|
||||
const postsContentValue = board.editorUseAt
|
||||
? postsContent
|
||||
: formData.postsContent
|
||||
|
||||
if (!postsContentValue) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.required.format'), [
|
||||
t('posts.posts_content'),
|
||||
]),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (board.uploadUseAt) {
|
||||
const isUpload = await uploadRef.current.isModified(attachData)
|
||||
|
||||
if (isUpload) {
|
||||
const info: UploadInfoReqeust = {
|
||||
entityName: 'posts',
|
||||
entityId: board.boardNo?.toString(),
|
||||
}
|
||||
|
||||
// 업로드 및 저장
|
||||
const result = await uploadRef.current.upload(info, attachData)
|
||||
if (result) {
|
||||
if (result !== 'no attachments' && result !== 'no update list') {
|
||||
attachmentCode = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data: PostsSavePayload = {
|
||||
boardNo,
|
||||
postsTitle: formData.postsTitle,
|
||||
noticeAt: formData.noticeAt,
|
||||
postsContent: postsContentValue,
|
||||
postsAnswerContent: board.editorUseAt
|
||||
? postsAnswerContent
|
||||
: formData.postsAnswerContent,
|
||||
attachmentCode,
|
||||
}
|
||||
|
||||
if (postsNo === -1) {
|
||||
await postsService.save({
|
||||
boardNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
await postsService.update({
|
||||
boardNo,
|
||||
postsNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
|
||||
if (postsNo === -1) {
|
||||
uploadRef.current?.rollback(attachmentCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 저장 버튼
|
||||
const saveButton: IButtonProps = {
|
||||
label: t('label.button.save'),
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
confirmMessage: t('msg.confirm.save'),
|
||||
handleButton: handleSubmit(handleSave),
|
||||
}
|
||||
|
||||
// 이전 화면으로 이동
|
||||
const handlePrev = useCallback(() => {
|
||||
/* if (postsNo === -1) {
|
||||
route.push(
|
||||
{
|
||||
pathname: `/posts/${boardNo}`,
|
||||
query: {
|
||||
size: route.query.size,
|
||||
page: route.query.page,
|
||||
keywordType: route.query.keywordType,
|
||||
keyword: route.query.keyword,
|
||||
},
|
||||
},
|
||||
// `/posts/${boardNo}`,
|
||||
)
|
||||
} else {
|
||||
route.push(
|
||||
{
|
||||
pathname: `/posts/${boardNo}/view/${postsNo}`,
|
||||
query: {
|
||||
size: route.query.size,
|
||||
page: route.query.page,
|
||||
keywordType: route.query.keywordType,
|
||||
keyword: route.query.keyword,
|
||||
},
|
||||
},
|
||||
// `/posts/${boardNo}`,
|
||||
)
|
||||
} */
|
||||
route.back()
|
||||
}, [route])
|
||||
|
||||
// 이전 버튼
|
||||
const prevButton: IButtonProps = {
|
||||
label: t('label.button.prev'),
|
||||
variant: 'contained',
|
||||
handleButton: handlePrev,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="postsTitle"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('posts.posts_title')}
|
||||
name="postsTitle"
|
||||
required
|
||||
inputProps={{ maxLength: 100 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('posts.posts_title'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 }}
|
||||
/>
|
||||
{errors.postsTitle && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.postsTitle}
|
||||
target={[100]}
|
||||
label={t('posts.posts_title')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('posts.notice_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="noticeAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
{board.editorUseAt && (
|
||||
<Editor contents={postsContent} setContents={setPostsContent} />
|
||||
)}
|
||||
{!board.editorUseAt && (
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="postsContent"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('posts.posts_content')}
|
||||
name="postsContent"
|
||||
multiline
|
||||
minRows={9.2}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('posts.posts_content'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.postsContent && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.postsContent}
|
||||
label={t('posts.posts_content')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
{(board.skinTypeCode === SKINT_TYPE_CODE_FAQ ||
|
||||
board.skinTypeCode === SKINT_TYPE_CODE_QNA) && (
|
||||
<Grid item xs={12} sm={12}>
|
||||
{board.editorUseAt && (
|
||||
<Editor
|
||||
contents={postsAnswerContent}
|
||||
setContents={setPostsAnswerContent}
|
||||
/>
|
||||
)}
|
||||
{!board.editorUseAt && (
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="postsAnswerContent"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('posts.posts_answer_content')}
|
||||
name="postsAnswerContent"
|
||||
multiline
|
||||
minRows={9.2}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('posts.posts_answer_content'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
{board.uploadUseAt && (
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Upload
|
||||
ref={uploadRef}
|
||||
multi
|
||||
uploadLimitCount={board.uploadLimitCount}
|
||||
uploadLimitSize={board.uploadLimitSize}
|
||||
attachmentCode={initData.attachmentCode}
|
||||
attachData={attachData}
|
||||
/>
|
||||
{attachData && (
|
||||
<AttachList data={attachData} setData={setAttachData} />
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<CustomButtons buttons={[saveButton, prevButton]} />
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const boardNo = Number(query.board)
|
||||
const postsNo = Number(query.id)
|
||||
|
||||
let board = {}
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (postsNo !== -1) {
|
||||
const result = await postsService.get(boardNo, postsNo)
|
||||
if (result) {
|
||||
board = (await result.data.board) as BoardSavePayload
|
||||
data = (await result.data) as PostsSavePayload
|
||||
}
|
||||
} else {
|
||||
const result = await boardService.get(boardNo)
|
||||
if (result) {
|
||||
board = (await result.data) as BoardSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
boardNo,
|
||||
postsNo,
|
||||
board,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default PostsItem
|
||||
552
frontend/admin/src/pages/posts/[board]/index.tsx
Normal file
552
frontend/admin/src/pages/posts/[board]/index.tsx
Normal file
@@ -0,0 +1,552 @@
|
||||
import { CustomButtons, IButtonProps } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import { Box } from '@material-ui/core'
|
||||
import Link from '@material-ui/core/Link'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import FiberNewIcon from '@material-ui/icons/FiberNew'
|
||||
import { ClassNameMap } from '@material-ui/styles'
|
||||
// api
|
||||
import {
|
||||
BoardSavePayload,
|
||||
boardService,
|
||||
CommentDeletePayload,
|
||||
postsService,
|
||||
} from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { format, Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import classNames from 'classnames'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
vMiddle: {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
mgl: {
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
cancel: {
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
handleDetail: (postsNo: number) => void,
|
||||
gridApiRef: React.MutableRefObject<any>,
|
||||
t?: TFunction,
|
||||
classes?: ClassNameMap<string>,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
handleDetail,
|
||||
gridApiRef,
|
||||
t,
|
||||
classes,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'postsTitle',
|
||||
headerName: t('posts.posts_title'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: function renderCellPostsTitle(params: GridValueGetterParams) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
gridApiRef.current = params.api // api
|
||||
return (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault()
|
||||
handleDetail(params.row.postsNo)
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
color="text.primary"
|
||||
component="span"
|
||||
className={classNames({
|
||||
[classes.cancel]: params.row.deleteAt,
|
||||
})}
|
||||
>
|
||||
{(params.row.noticeAt ? `[${t('common.notice')}] ` : '') +
|
||||
params.row.postsTitle}
|
||||
{params.row.commentCount && params.row.commentCount !== 0 ? (
|
||||
<Box
|
||||
color="red"
|
||||
component="span"
|
||||
>{` [${params.row.commentCount}]`}</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{params.row.isNew && (
|
||||
<FiberNewIcon
|
||||
color="secondary"
|
||||
className={classNames({
|
||||
[classes.mgl]: true,
|
||||
[classes.vMiddle]: true,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
params.value
|
||||
? convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
field: 'createdName',
|
||||
headerName: t('common.created_by'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'readCount',
|
||||
headerName: t('common.read_count'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'deleteAt',
|
||||
headerName: t('label.button.delete'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) => {
|
||||
if (params.value === 1) return '작성자'
|
||||
if (params.value === 2) return '관리자'
|
||||
return ''
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'posts'
|
||||
|
||||
export interface IBoardProps {
|
||||
board: BoardSavePayload | null
|
||||
}
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Posts = ({ board }: IBoardProps) => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const gridApiRef = useRef<any>(null)
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'postsData',
|
||||
label: `${t('posts.posts_title')}+${t('posts.posts_content')}`,
|
||||
},
|
||||
{
|
||||
key: 'postsName',
|
||||
label: t('posts.posts_title'),
|
||||
},
|
||||
{
|
||||
key: 'postsContent',
|
||||
label: t('posts.posts_content'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const [boardNo] = useState<number>(Number(route.query.board) || null)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const [page, setPage] = useState<number>(
|
||||
parseInt(route.query.page as string, 10) || 0,
|
||||
)
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
})
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = postsService.search(boardNo, {
|
||||
keywordType: keywordState?.keywordType || 'postsName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: board.postDisplayCount,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPage(0)
|
||||
}
|
||||
}
|
||||
|
||||
// 상세 화면 이동
|
||||
const handleDetail = useCallback(
|
||||
(postsNo: number) => {
|
||||
route.push({
|
||||
pathname: `/posts/${boardNo}/view/${postsNo}`,
|
||||
/* query: {
|
||||
size: board.postDisplayCount,
|
||||
page,
|
||||
keywordType: keywordState?.keywordType,
|
||||
keyword: keywordState?.keyword,
|
||||
}, */
|
||||
})
|
||||
},
|
||||
[boardNo, route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, handleDetail, gridApiRef, t, classes),
|
||||
[data, handleDetail, t, classes, gridApiRef],
|
||||
)
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPage(_page)
|
||||
}
|
||||
|
||||
// 그리드 체크 해제
|
||||
const uncheckedGrid = useCallback(() => {
|
||||
const selectedRowKeys = gridApiRef.current?.getSelectedRows().keys()
|
||||
|
||||
let cnt = 0
|
||||
while (cnt < data.numberOfElements) {
|
||||
const gridRowId = selectedRowKeys.next()
|
||||
if (gridRowId.done === true) break
|
||||
gridApiRef.current.selectRow(gridRowId.value, false, false)
|
||||
cnt += 1
|
||||
}
|
||||
}, [data?.numberOfElements])
|
||||
|
||||
// 선택된 행 수 반환
|
||||
const getSelectedRowCount = (deleteAt: boolean) => {
|
||||
let count = 0
|
||||
|
||||
const selectedRows = gridApiRef.current.getSelectedRows()
|
||||
selectedRows.forEach(m => {
|
||||
if (deleteAt === null || deleteAt ? m.deleteAt !== 0 : m.deleteAt === 0) {
|
||||
count += 1
|
||||
}
|
||||
})
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// 선택된 행 반환
|
||||
const getSelectedRows = (deleteAt: boolean) => {
|
||||
let list: CommentDeletePayload[] = []
|
||||
|
||||
const selectedRows = gridApiRef.current.getSelectedRows()
|
||||
selectedRows.forEach(m => {
|
||||
if (
|
||||
deleteAt === null ||
|
||||
(deleteAt ? m.deleteAt !== 0 : m.deleteAt === 0)
|
||||
) {
|
||||
const saved: CommentDeletePayload = {
|
||||
boardNo: m.boardNo,
|
||||
postsNo: m.postsNo,
|
||||
}
|
||||
list.push(saved)
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
uncheckedGrid()
|
||||
|
||||
mutate()
|
||||
}, [mutate, setSuccessSnackBar, uncheckedGrid])
|
||||
|
||||
// 삭제
|
||||
const handleRemove = useCallback(() => {
|
||||
const selectedRows = getSelectedRows(false)
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
successCallback()
|
||||
return
|
||||
}
|
||||
|
||||
postsService.remove({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: selectedRows,
|
||||
})
|
||||
}, [errorCallback, successCallback])
|
||||
|
||||
// 복원
|
||||
const handleRestore = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const selectedRows = getSelectedRows(true)
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
successCallback()
|
||||
return
|
||||
}
|
||||
|
||||
postsService.restore({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: selectedRows,
|
||||
})
|
||||
}, [setSuccessSnackBar, errorCallback, successCallback])
|
||||
|
||||
// 완전 삭제
|
||||
const handleDelete = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const selectedRows = getSelectedRows(null)
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
successCallback()
|
||||
return
|
||||
}
|
||||
|
||||
postsService.delete({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: selectedRows,
|
||||
})
|
||||
}, [setSuccessSnackBar, errorCallback, successCallback])
|
||||
|
||||
// 삭제 버튼
|
||||
const removeButton: IButtonProps = {
|
||||
label: t('label.button.selection_delete'),
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.delete'),
|
||||
handleButton: handleRemove,
|
||||
validate: () => {
|
||||
if (gridApiRef.current.getSelectedRows().size === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.format'), [
|
||||
`${t('label.button.delete')} ${t('common.target')}`,
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const count = getSelectedRowCount(false) // 미삭제만
|
||||
if (count === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.already_deleted.format'), [
|
||||
t('authorization'),
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
completeMessage: t('msg.success.delete'),
|
||||
}
|
||||
|
||||
// 복원 버튼
|
||||
const restoreButton: IButtonProps = {
|
||||
label: t('label.button.selection_restore'),
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.restore'),
|
||||
handleButton: handleRestore,
|
||||
validate: () => {
|
||||
if (gridApiRef.current.getSelectedRows().size === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.format'), [
|
||||
`${t('label.button.restore')} ${t('common.target')}`,
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const count = getSelectedRowCount(true) // 삭제만
|
||||
if (count === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.already_restored.format'), [
|
||||
t('authorization'),
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
completeMessage: t('msg.success.restore'),
|
||||
}
|
||||
|
||||
// 완전 삭제 버튼
|
||||
const deleteButton: IButtonProps = {
|
||||
label: t('label.button.selection_permanent_delete'),
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.permanent_delete'),
|
||||
handleButton: handleDelete,
|
||||
validate: () => {
|
||||
if (gridApiRef.current.getSelectedRows().size === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.format'), [
|
||||
`${t('label.button.permanent_delete')} ${t('common.target')}`,
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
completeMessage: t('msg.success.permanent_delete'),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push(`${boardNo}/edit/-1`)
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={board.postDisplayCount}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.postsNo}
|
||||
checkboxSelection
|
||||
disableSelectionOnClick
|
||||
/>
|
||||
<CustomButtons
|
||||
buttons={[removeButton, restoreButton, deleteButton]}
|
||||
className="containerLeft"
|
||||
/>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const boardNo = Number(query.board)
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (boardNo !== -1) {
|
||||
const result = await boardService.get(boardNo)
|
||||
if (result) {
|
||||
data = (await result.data) as BoardSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`board item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
board: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Posts
|
||||
458
frontend/admin/src/pages/posts/[board]/view/[id].tsx
Normal file
458
frontend/admin/src/pages/posts/[board]/view/[id].tsx
Normal file
@@ -0,0 +1,458 @@
|
||||
import AttachList from '@components/AttachList'
|
||||
import { CustomButtons, IButtonProps } from '@components/Buttons'
|
||||
import { Comment } from '@components/comment'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import CommentIcon from '@material-ui/icons/Comment'
|
||||
import {
|
||||
BoardSavePayload,
|
||||
boardService,
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
IBoardProps,
|
||||
PostsSavePayload,
|
||||
postsService,
|
||||
SKINT_TYPE_CODE_FAQ,
|
||||
SKINT_TYPE_CODE_NORMAL,
|
||||
SKINT_TYPE_CODE_QNA,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { AxiosError } from 'axios'
|
||||
import classNames from 'classnames'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
content: {
|
||||
position: 'relative',
|
||||
padding: theme.spacing(2),
|
||||
minHeight: '120px',
|
||||
},
|
||||
contentTitle: {
|
||||
marginTop: theme.spacing(0),
|
||||
},
|
||||
contentCreator: {
|
||||
marginTop: theme.spacing(1),
|
||||
display: 'flex',
|
||||
},
|
||||
contentCreatorLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
commentIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
contentLabel: {
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: '30px',
|
||||
top: '40px',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
fontSize: '20px',
|
||||
fontWeight: 700,
|
||||
textAlign: 'center',
|
||||
lineHeight: '40px',
|
||||
borderRadius: '50%',
|
||||
color: '#fff',
|
||||
backgroundColor: '#1a4890',
|
||||
},
|
||||
contentLabelQ: {
|
||||
backgroundColor: '#1a4890',
|
||||
},
|
||||
contentLabelA: {
|
||||
backgroundColor: '#5aab34',
|
||||
},
|
||||
contentEditor: {
|
||||
padding: theme.spacing(2, 2, 2, 10),
|
||||
},
|
||||
label: {
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
number: {
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'right',
|
||||
},
|
||||
mgt1: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
mgl3: {
|
||||
marginLeft: theme.spacing(3),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export interface IPostsItemsProps {
|
||||
boardNo: number
|
||||
postsNo: number
|
||||
board: BoardSavePayload | null
|
||||
initData: PostsSavePayload | null
|
||||
}
|
||||
|
||||
const PostsItem = ({ boardNo, postsNo, board, initData }: IPostsItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [deleteAt, setDeleteAt] = useState<number>(initData.deleteAt)
|
||||
|
||||
const [commentCount, setCommentCount] = useState<number>(0)
|
||||
const refreshCommentCount = count => {
|
||||
setCommentCount(count)
|
||||
}
|
||||
|
||||
const [attachData, setAttachData] = useState<
|
||||
IAttachmentResponse[] | undefined
|
||||
>(undefined)
|
||||
|
||||
const getAttachments = useCallback(
|
||||
async (code: string) => {
|
||||
try {
|
||||
const result = await fileService.getAttachmentList(code)
|
||||
|
||||
if (result) {
|
||||
setAttachData(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initData.attachmentCode) {
|
||||
getAttachments(initData.attachmentCode)
|
||||
}
|
||||
}, [getAttachments, initData.attachmentCode])
|
||||
|
||||
// 목록 화면으로 이동
|
||||
const handleList = useCallback(() => {
|
||||
/* route.push(
|
||||
{
|
||||
pathname: `/posts/${boardNo}`,
|
||||
query: {
|
||||
size: route.query.size,
|
||||
page: route.query.page,
|
||||
keywordType: route.query.keywordType,
|
||||
keyword: route.query.keyword,
|
||||
},
|
||||
},
|
||||
// `/posts/${boardNo}`,
|
||||
) */
|
||||
route.back()
|
||||
}, [route])
|
||||
|
||||
// 수정 화면으로 이동
|
||||
const handleEdit = useCallback(() => {
|
||||
route.push({
|
||||
pathname: `/posts/${boardNo}/edit/${postsNo}`,
|
||||
query: {
|
||||
size: route.query.size,
|
||||
page: route.query.page,
|
||||
keywordType: route.query.keywordType,
|
||||
keyword: route.query.keyword,
|
||||
},
|
||||
})
|
||||
}, [boardNo, postsNo, route])
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const handleRemove = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
postsService.remove({
|
||||
callback: () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
setDeleteAt(2) // 삭제 여부 - 1:작성자, 2:관리자
|
||||
},
|
||||
errorCallback,
|
||||
data: [
|
||||
{
|
||||
boardNo,
|
||||
postsNo,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, [setSuccessSnackBar, errorCallback, boardNo, postsNo])
|
||||
|
||||
// 완전 삭제
|
||||
const handleDelete = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
postsService.delete({
|
||||
callback: () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
handleList() // 목록 화면으로 이동
|
||||
},
|
||||
errorCallback,
|
||||
data: [
|
||||
{
|
||||
boardNo,
|
||||
postsNo,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, [setSuccessSnackBar, errorCallback, boardNo, postsNo, handleList])
|
||||
|
||||
// 복원
|
||||
const handleRestore = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
postsService.restore({
|
||||
callback: () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
setDeleteAt(0)
|
||||
},
|
||||
errorCallback,
|
||||
data: [
|
||||
{
|
||||
boardNo,
|
||||
postsNo,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, [setSuccessSnackBar, errorCallback, boardNo, postsNo])
|
||||
|
||||
// 삭제 버튼
|
||||
const removeButton: IButtonProps = {
|
||||
label: t('label.button.delete'),
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.delete'),
|
||||
handleButton: handleRemove,
|
||||
}
|
||||
|
||||
// 복원 버튼
|
||||
const restoreButton: IButtonProps = {
|
||||
label: t('label.button.restore'),
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.restore'),
|
||||
handleButton: handleRestore,
|
||||
}
|
||||
|
||||
// 완전 삭제 버튼
|
||||
const deleteButton: IButtonProps = {
|
||||
label: t('label.button.permanent_delete'),
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.permanent_delete'),
|
||||
handleButton: handleDelete,
|
||||
}
|
||||
|
||||
// 수정 버튼
|
||||
const editButton: IButtonProps = {
|
||||
label: t('label.button.edit'),
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
handleButton: handleEdit,
|
||||
}
|
||||
|
||||
// 목록 버튼
|
||||
const listButton: IButtonProps = {
|
||||
label: t('label.button.list'),
|
||||
variant: 'outlined',
|
||||
size: 'small',
|
||||
handleButton: handleList,
|
||||
}
|
||||
|
||||
// 하단 버튼
|
||||
let leftButtons = []
|
||||
|
||||
// 삭제/복원 버튼 추가
|
||||
if (deleteAt === 0) {
|
||||
leftButtons.push(removeButton)
|
||||
} else {
|
||||
leftButtons.push(restoreButton)
|
||||
}
|
||||
leftButtons.push(deleteButton)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1} className={classes.content}>
|
||||
<Typography variant="h3">
|
||||
{(initData.noticeAt ? '[공지] ' : '') + initData.postsTitle}
|
||||
</Typography>
|
||||
<Box className={classes.contentCreator}>
|
||||
<Box className={classes.contentCreatorLeft}>
|
||||
<Typography variant="h6" component="h4">
|
||||
{initData.createdName}
|
||||
</Typography>
|
||||
<Box component="span">
|
||||
{convertStringToDateFormat(
|
||||
initData.createdDate,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
)}
|
||||
</Box>
|
||||
<Box component="span" className={classes.mgl3}>
|
||||
{`${t('common.read')} ${initData.readCount}`}
|
||||
</Box>
|
||||
</Box>
|
||||
{board?.commentUseAt && (
|
||||
<Box>
|
||||
<CommentIcon
|
||||
fontSize="small"
|
||||
className={classes.commentIcon}
|
||||
/>
|
||||
{`${t('comment')} ${commentCount}`}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
{board.uploadUseAt && attachData && (
|
||||
<AttachList data={attachData} setData={setAttachData} readonly />
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
{(board.skinTypeCode === SKINT_TYPE_CODE_FAQ ||
|
||||
board.skinTypeCode === SKINT_TYPE_CODE_QNA) && (
|
||||
<Box boxShadow={1} className={classes.content}>
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.contentLabel]: true,
|
||||
[classes.contentLabelQ]: true,
|
||||
})}
|
||||
>
|
||||
Q
|
||||
</div>
|
||||
<div
|
||||
className={classes.contentEditor}
|
||||
dangerouslySetInnerHTML={{ __html: initData.postsContent }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{board.skinTypeCode === SKINT_TYPE_CODE_NORMAL && (
|
||||
<Box boxShadow={1} className={classes.content}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: initData.postsContent }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
{(board.skinTypeCode === SKINT_TYPE_CODE_FAQ ||
|
||||
board.skinTypeCode === SKINT_TYPE_CODE_QNA) && (
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1} className={classes.content}>
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.contentLabel]: true,
|
||||
[classes.contentLabelA]: true,
|
||||
})}
|
||||
>
|
||||
A
|
||||
</div>
|
||||
<div
|
||||
className={classes.contentEditor}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: initData.postsAnswerContent,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
{board?.commentUseAt && (
|
||||
<Comment
|
||||
boardNo={boardNo}
|
||||
postsNo={postsNo}
|
||||
commentUseAt={board.commentUseAt}
|
||||
deleteAt={deleteAt}
|
||||
refreshCommentCount={refreshCommentCount}
|
||||
/>
|
||||
)}
|
||||
<CustomButtons buttons={leftButtons} className="containerLeft" />
|
||||
<CustomButtons
|
||||
buttons={[editButton, listButton]}
|
||||
className="containerRight"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const boardNo = Number(query.board)
|
||||
const postsNo = Number(query.id)
|
||||
|
||||
let board: IBoardProps
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (postsNo !== -1) {
|
||||
const result = await postsService.get(boardNo, postsNo)
|
||||
if (result) {
|
||||
board = (await result.data?.board) as IBoardProps
|
||||
data = (await result.data) as PostsSavePayload
|
||||
}
|
||||
} else {
|
||||
const result = await boardService.get(boardNo)
|
||||
if (result) {
|
||||
board = (await result.data) as IBoardProps
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`posts item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
boardNo,
|
||||
postsNo,
|
||||
board,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default PostsItem
|
||||
262
frontend/admin/src/pages/privacy/[id].tsx
Normal file
262
frontend/admin/src/pages/privacy/[id].tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Editor from '@components/Editor'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { PrivacySavePayload, privacyService } from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
switchBox: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyPrivacy: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IPrivacyFormInput {
|
||||
privacyTitle: string
|
||||
privacyContent: string
|
||||
useAt: boolean
|
||||
}
|
||||
|
||||
export interface IPrivacyItemsProps {
|
||||
privacyNo: string
|
||||
initData: PrivacySavePayload | null
|
||||
}
|
||||
|
||||
const PrivacyItem = ({ privacyNo, initData }: IPrivacyItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// alert
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
handleAlert: () => setCustomAlert({ open: false }),
|
||||
})
|
||||
|
||||
// Editor
|
||||
const [privacyContent, setPrivacyContent] = useState<string>(
|
||||
initData?.privacyContent || '',
|
||||
)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IPrivacyFormInput>({
|
||||
defaultValues: {
|
||||
privacyTitle: initData?.privacyTitle || '',
|
||||
useAt: typeof initData?.useAt !== 'undefined' ? initData?.useAt : true,
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IPrivacyFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const saved: PrivacySavePayload = {
|
||||
privacyTitle: formData.privacyTitle,
|
||||
privacyContent,
|
||||
useAt: formData.useAt,
|
||||
}
|
||||
|
||||
if (!privacyContent) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.required.format'), [
|
||||
t('privacy.privacy_content'),
|
||||
]),
|
||||
handleAlert: () => {
|
||||
setCustomAlert({ open: false })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (privacyNo === '-1') {
|
||||
await privacyService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
await privacyService.update({
|
||||
privacyNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="privacyTitle"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('privacy.privacy_title')}
|
||||
name="privacyTitle"
|
||||
required
|
||||
autoFocus
|
||||
inputProps={{ maxLength: 100 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('privacy.privacy_title'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.privacyTitle && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.privacyTitle}
|
||||
target={[100]}
|
||||
label={t('privacy.privacy_title')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1} className={classes.switchBox}>
|
||||
<FormControlLabel
|
||||
label={t('common.use_at')}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Controller
|
||||
name="useAt"
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, value } }) => (
|
||||
<Switch
|
||||
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||
onChange={onChange}
|
||||
inputRef={ref}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Editor contents={privacyContent} setContents={setPrivacyContent} />
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const privacyNo = query.id
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
if (privacyNo !== '-1') {
|
||||
const result = await privacyService.get(privacyNo as string)
|
||||
if (result) {
|
||||
data = (await result.data) as PrivacySavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`privacy item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
privacyNo,
|
||||
initData: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default PrivacyItem
|
||||
273
frontend/admin/src/pages/privacy/index.tsx
Normal file
273
frontend/admin/src/pages/privacy/index.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import { privacyService } from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
toggleUseAt,
|
||||
deletePrivacy: (privacyNo: string) => void,
|
||||
updatePrivacy: (privacyNo: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
toggleUseAt,
|
||||
deletePrivacy,
|
||||
updatePrivacy,
|
||||
t,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'privacyTitle',
|
||||
headerName: t('privacy.privacy_title'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'useAt',
|
||||
headerName: t('common.use_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellCreatedAt(params: GridCellParams) {
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleUseAt(event, params.row.privacyNo as number)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return (
|
||||
<GridButtons
|
||||
id={params.row.privacyNo as string}
|
||||
handleDelete={deletePrivacy}
|
||||
handleUpdate={updatePrivacy}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'privacy'
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const Privacy: NextPage<any> = () => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'privacyTitle',
|
||||
label: t('privacy.privacy_title'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = privacyService.search({
|
||||
keywordType: keywordState?.keywordType || 'privacyTitle',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
mutate()
|
||||
}, [mutate, setSuccessSnackBar])
|
||||
|
||||
// 사용 여부 toggle 시 save
|
||||
const toggleUseAt = useCallback(
|
||||
async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
paramPrivacyNo: string,
|
||||
) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
await privacyService.updateUseAt({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
privacyNo: paramPrivacyNo,
|
||||
useAt: event.target.checked,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate],
|
||||
)
|
||||
|
||||
// 삭제
|
||||
const deletePrivacy = useCallback(
|
||||
(privacyNo: string) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
privacyService.delete({
|
||||
privacyNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate],
|
||||
)
|
||||
|
||||
// 수정 시 상세 화면 이동
|
||||
const updatePrivacy = useCallback(
|
||||
(privacyNo: string) => {
|
||||
route.push(`/privacy/${privacyNo}`)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, toggleUseAt, deletePrivacy, updatePrivacy, t),
|
||||
[data, toggleUseAt, deletePrivacy, updatePrivacy, t],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={() => {
|
||||
route.push('privacy/-1')
|
||||
}}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.privacyNo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Privacy
|
||||
80
frontend/admin/src/pages/reload/index.tsx
Normal file
80
frontend/admin/src/pages/reload/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Button } from '@material-ui/core'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Alert, { Color } from '@material-ui/lab/Alert'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const useStyles = makeStyles((_: Theme) =>
|
||||
createStyles({
|
||||
alert: {
|
||||
margin: _.spacing(1),
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
type Props = {
|
||||
initialLoginStatus: string
|
||||
}
|
||||
|
||||
function Home(props: Props) {
|
||||
const classes = useStyles(props)
|
||||
const [reloadState, setReloadSteate] = useState<{
|
||||
message: string
|
||||
severity: Color
|
||||
}>({
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="h5" component="h2">
|
||||
Reload Messages
|
||||
</Typography>
|
||||
<Alert className={classes.alert} severity={reloadState.severity}>
|
||||
{reloadState.message}
|
||||
</Alert>
|
||||
<Button variant="outlined" color="primary" onClick={onClickReload}>
|
||||
Reload
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
228
frontend/admin/src/pages/reserve-item/[id].tsx
Normal file
228
frontend/admin/src/pages/reserve-item/[id].tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import {
|
||||
ReserveItemAdditional,
|
||||
ReserveItemBasic,
|
||||
ReserveItemManager,
|
||||
} from '@components/ReserveItem'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
ICode,
|
||||
ILocation,
|
||||
IReserveItem,
|
||||
locationService,
|
||||
reserveItemService,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import produce from 'immer'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
},
|
||||
cardActions: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
switch: {
|
||||
width: '100%',
|
||||
justifyContent: 'start',
|
||||
border: '1px solid rgba(0, 0, 0, 0.23)',
|
||||
borderRadius: theme.spacing(0.5),
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ReserveItemDetailProps {
|
||||
reserveItemId: string
|
||||
initData?: IReserveItem
|
||||
locations: ILocation[]
|
||||
categories: ICode[]
|
||||
reserveMethods: ICode[]
|
||||
reserveMeans: ICode[]
|
||||
selectionMeans: ICode[]
|
||||
targets: ICode[]
|
||||
}
|
||||
|
||||
const ReserveItemDetail = (props: ReserveItemDetailProps) => {
|
||||
const { reserveItemId, initData, targets, ...rest } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
//form hook
|
||||
const methods = useForm<IReserveItem>({
|
||||
defaultValues: initData,
|
||||
})
|
||||
|
||||
const { register, formState, control, handleSubmit, setFocus, getValues } =
|
||||
methods
|
||||
|
||||
//상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
// <목록, 저장> 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.errors) {
|
||||
setFocus('reserveItemName')
|
||||
}
|
||||
}, [formState.errors])
|
||||
|
||||
const handleSave = async (formData: IReserveItem) => {
|
||||
setSuccessSnackBar('loading')
|
||||
try {
|
||||
formData = produce(formData, draft => {
|
||||
draft.isPaid = Boolean(draft.isPaid)
|
||||
draft.isPeriod = Boolean(draft.isPeriod)
|
||||
draft.isUse = Boolean(draft.isUse)
|
||||
})
|
||||
|
||||
let result
|
||||
if (reserveItemId === '-1') {
|
||||
formData = produce(formData, draft => {
|
||||
draft.inventoryQty = draft.totalQty
|
||||
})
|
||||
result = await reserveItemService.save(formData)
|
||||
} else {
|
||||
formData = produce(formData, draft => {
|
||||
draft.inventoryQty =
|
||||
draft.totalQty - draft.prevTotalQty + draft.inventoryQty
|
||||
})
|
||||
result = await reserveItemService.update(
|
||||
parseInt(reserveItemId),
|
||||
formData,
|
||||
)
|
||||
}
|
||||
|
||||
if (result) {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
handleList()
|
||||
}
|
||||
} catch (error) {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = () => {
|
||||
route.push('/reserve-item')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<ReserveItemBasic
|
||||
control={control}
|
||||
formState={formState}
|
||||
register={register}
|
||||
getValues={getValues}
|
||||
data={initData}
|
||||
{...rest}
|
||||
/>
|
||||
<ReserveItemAdditional
|
||||
control={control}
|
||||
formState={formState}
|
||||
targets={targets}
|
||||
/>
|
||||
<ReserveItemManager control={control} formState={formState} />
|
||||
|
||||
<DetailButtons
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
handleList={handleList}
|
||||
/>
|
||||
</FormProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const reserveItemId = query.id as string
|
||||
|
||||
let locations: ILocation[] = []
|
||||
let categories: ICode[] = []
|
||||
let reserveMethods: ICode[] = []
|
||||
let reserveMeans: ICode[] = []
|
||||
let selectionMeans: ICode[] = []
|
||||
let targets: ICode[] = []
|
||||
|
||||
try {
|
||||
locations = await (await locationService.getList()).data
|
||||
categories = await (
|
||||
await reserveItemService.getCode('reserve-category')
|
||||
).data
|
||||
reserveMethods = await (
|
||||
await reserveItemService.getCode('reserve-method')
|
||||
).data
|
||||
reserveMeans = await (
|
||||
await reserveItemService.getCode('reserve-means')
|
||||
).data
|
||||
selectionMeans = await (
|
||||
await reserveItemService.getCode('reserve-selection')
|
||||
).data
|
||||
targets = await (await reserveItemService.getCode('reserve-target')).data
|
||||
} catch (error) {
|
||||
console.error(`reserve item query error ${error.message}`)
|
||||
}
|
||||
|
||||
if (reserveItemId === '-1') {
|
||||
return {
|
||||
props: {
|
||||
reserveItemId,
|
||||
categories,
|
||||
locations,
|
||||
reserveMethods,
|
||||
reserveMeans,
|
||||
selectionMeans,
|
||||
targets,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
const result = await reserveItemService.get(parseInt(reserveItemId))
|
||||
|
||||
if (result) {
|
||||
data = (await result.data) as IReserveItem
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve item query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
reserveItemId,
|
||||
initData: data,
|
||||
categories,
|
||||
locations,
|
||||
reserveMethods,
|
||||
reserveMeans,
|
||||
selectionMeans,
|
||||
targets,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ReserveItemDetail
|
||||
360
frontend/admin/src/pages/reserve-item/index.tsx
Normal file
360
frontend/admin/src/pages/reserve-item/index.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import { PopupProps } from '@components/DialogPopup'
|
||||
import Search from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import { ICode, ILocation, locationService, reserveItemService } from '@service'
|
||||
import { conditionAtom, conditionValue, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
search: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
width: '10vw',
|
||||
maxWidth: 100,
|
||||
minWidth: 80,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const conditionKey = 'reserve-item'
|
||||
|
||||
type ColumnType = (
|
||||
data: Page,
|
||||
handleUpdate: (id: number) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void,
|
||||
t: TFunction,
|
||||
handlePopup?: (row: any) => void,
|
||||
) => GridColDef[]
|
||||
|
||||
//그리드 컬럼 정의
|
||||
const getColumns: ColumnType = (
|
||||
data: Page,
|
||||
handleUpdate: (id: number) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void,
|
||||
t,
|
||||
handlePopup?: (row: any) => void,
|
||||
) => {
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 0.5,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
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.5,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'totalQty',
|
||||
headerName: `${t('reserve.count')}/${t('reserve.number_of_people')}`,
|
||||
headerAlign: 'center',
|
||||
align: 'right',
|
||||
flex: 0.8,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'isUse',
|
||||
headerName: t('common.use_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
hide: handlePopup ? true : false,
|
||||
sortable: false,
|
||||
flex: 1,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleIsUse(event, params.row.reserveItemId)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
flex: 1,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'reserveItemId',
|
||||
headerName: handlePopup ? t('common.select') : t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => {
|
||||
return handlePopup ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopup(params.row)
|
||||
}}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
{t('common.select')}
|
||||
</Button>
|
||||
) : (
|
||||
<GridButtons
|
||||
id={params.value as string}
|
||||
handleUpdate={handleUpdate}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export type ReserveItemProps = PopupProps & {
|
||||
locations?: ILocation[]
|
||||
categories?: ICode[]
|
||||
}
|
||||
|
||||
const ReserveItem = (props: ReserveItemProps) => {
|
||||
const { handlePopup, locations, categories } = props
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
// 에러 상태관리
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue | null>({
|
||||
locationId: keywordState?.locationId || '0',
|
||||
categoryId: keywordState?.categoryId || 'all',
|
||||
})
|
||||
const { page, setPageValue } = usePage(conditionKey, 0)
|
||||
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'item',
|
||||
label: t('reserve_item.name'),
|
||||
},
|
||||
])
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = reserveItemService.search({
|
||||
keywordType: keywordState?.keywordType || 'item',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
locationId:
|
||||
keywordState?.locationId !== '0' ? keywordState?.locationId : null,
|
||||
categoryId:
|
||||
keywordState?.categoryId !== 'all' ? keywordState?.categoryId : null,
|
||||
isUse: Boolean(handlePopup),
|
||||
})
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
router.push('/reserve-item/-1')
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
router.push(`/reserve-item/${id}`)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
categoryId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
locationId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: number) => {
|
||||
try {
|
||||
const result = await reserveItemService.updateUse(
|
||||
id,
|
||||
event.target.checked,
|
||||
)
|
||||
if (result?.status === 204) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
},
|
||||
[customKeyword, page],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(data, handleUpdate, toggleIsUse, t, handlePopup)
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={handlePopup ? undefined : handleRegister}
|
||||
conditionKey={conditionKey}
|
||||
isNotWrapper={true}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<>
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-location"
|
||||
select
|
||||
value={customKeyword.locationId}
|
||||
onChange={handleLocationChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="location-all" value="0">
|
||||
<em>{t('common.all')}</em>
|
||||
</MenuItem>
|
||||
{locations &&
|
||||
locations.map(option => (
|
||||
<MenuItem key={option.locationId} value={option.locationId}>
|
||||
{option.locationName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-category"
|
||||
select
|
||||
value={customKeyword.categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="category-all" value="all">
|
||||
<em>{t('common.all')}</em>
|
||||
</MenuItem>
|
||||
{categories &&
|
||||
categories.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.reserveItemId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
let locations: ILocation[] = []
|
||||
let categories: ICode[] = []
|
||||
|
||||
try {
|
||||
locations = await (await locationService.getList()).data
|
||||
categories = await (
|
||||
await reserveItemService.getCode('reserve-category')
|
||||
).data
|
||||
} catch (error) {
|
||||
console.error(`reserve item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories,
|
||||
locations,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ReserveItem
|
||||
305
frontend/admin/src/pages/reserve/[id].tsx
Normal file
305
frontend/admin/src/pages/reserve/[id].tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import DialogPopup from '@components/DialogPopup'
|
||||
import {
|
||||
ReserveClientInfo,
|
||||
ReserveInfo,
|
||||
ReserveInfoView,
|
||||
ReserveItemInfo,
|
||||
} from '@components/Reserve'
|
||||
import { UploadType } from '@components/Upload'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import ReserveItem from '@pages/reserve-item'
|
||||
import {
|
||||
fileService,
|
||||
IAttachmentResponse,
|
||||
ICode,
|
||||
IReserve,
|
||||
IReserveItem,
|
||||
IReserveItemRelation,
|
||||
reserveItemService,
|
||||
ReserveSavePayload,
|
||||
reserveService,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ReserveDetailProps {
|
||||
reserveId?: string
|
||||
initData?: IReserve
|
||||
reserveItem?: IReserveItemRelation
|
||||
status?: ICode[]
|
||||
}
|
||||
|
||||
const ReserveDetail = (props: ReserveDetailProps) => {
|
||||
const { reserveId, reserveItem, initData, status } = props
|
||||
const classes = useStyles()
|
||||
const router = useRouter()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const uploadRef = useRef<UploadType>()
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
//form hook
|
||||
const methods = useForm<IReserve>({
|
||||
defaultValues: initData,
|
||||
})
|
||||
const {
|
||||
register,
|
||||
formState,
|
||||
control,
|
||||
handleSubmit,
|
||||
clearErrors,
|
||||
getValues,
|
||||
setValue,
|
||||
setError,
|
||||
} = methods
|
||||
|
||||
const [item, setItem] = useState<IReserveItemRelation>(undefined)
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false)
|
||||
const [attachData, setAttachData] = useState<
|
||||
IAttachmentResponse[] | undefined
|
||||
>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (initData?.attachmentCode) {
|
||||
fileService
|
||||
.getAttachmentList(initData.attachmentCode)
|
||||
.then(result => {
|
||||
if (result?.data) {
|
||||
setAttachData(result.data)
|
||||
}
|
||||
})
|
||||
.catch(error => setErrorState({ error }))
|
||||
}
|
||||
}, [initData])
|
||||
|
||||
useEffect(() => {
|
||||
if (reserveItem) {
|
||||
setItem(reserveItem)
|
||||
}
|
||||
}, [reserveItem])
|
||||
|
||||
const handlePopup = async (data: IReserveItem) => {
|
||||
if (data) {
|
||||
try {
|
||||
const result = await reserveItemService.getWithRelation(
|
||||
data.reserveItemId,
|
||||
)
|
||||
if (result) {
|
||||
setItem(result.data)
|
||||
clearErrors()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
handleDialogClose()
|
||||
}
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleSave = async (formData: IReserve) => {
|
||||
setSuccessSnackBar('loading')
|
||||
let attachCode = initData?.attachmentCode
|
||||
try {
|
||||
attachCode = await uploadRef.current?.upload(
|
||||
{
|
||||
entityName: 'reserve',
|
||||
entityId: null,
|
||||
},
|
||||
attachData,
|
||||
)
|
||||
|
||||
// 관리자가 예약하는 경우 심사/실시간 할 것없이 무조건 예약확정(status=approve)
|
||||
const saveData: ReserveSavePayload = {
|
||||
...formData,
|
||||
reserveItemId: item.reserveItemId,
|
||||
reserveStatusId: 'approve',
|
||||
locationId: item.locationId,
|
||||
categoryId: item.categoryId,
|
||||
attachmentCode: attachCode === 'no attachments' ? null : attachCode,
|
||||
}
|
||||
|
||||
let result
|
||||
if (reserveId === '-1') {
|
||||
result = await reserveService.save(saveData)
|
||||
} else {
|
||||
result = await reserveService.update(reserveId, saveData)
|
||||
}
|
||||
|
||||
if (result) {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
handleList()
|
||||
}
|
||||
} catch (error) {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({ error })
|
||||
if (reserveId === '-1') {
|
||||
// 저장 실패한 경우 첨부파일 rollback
|
||||
uploadRef.current?.rollback(attachCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = () => {
|
||||
router.push('/reserve')
|
||||
}
|
||||
|
||||
const handleButtonStatus = async (status: string, reason?: string) => {
|
||||
setSuccessSnackBar('loading')
|
||||
try {
|
||||
let result
|
||||
if (status === 'cancel') {
|
||||
result = await reserveService.cancel(reserveId, reason)
|
||||
} else {
|
||||
result = await reserveService.approve(reserveId)
|
||||
}
|
||||
if (result) {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
handleList()
|
||||
}
|
||||
} catch (error) {
|
||||
setSuccessSnackBar('none')
|
||||
setErrorState({ error })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{item && (
|
||||
<ReserveItemInfo
|
||||
data={item}
|
||||
handleSearchItem={handleDialogOpen}
|
||||
reserveStatus={status.find(
|
||||
code => code.codeId === initData?.reserveStatusId,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<DialogPopup
|
||||
id="find-dialog"
|
||||
handleClose={handleDialogClose}
|
||||
open={dialogOpen}
|
||||
title={`${t('reserve_item')} ${t('label.button.find')}`}
|
||||
>
|
||||
<ReserveItem handlePopup={handlePopup} />
|
||||
</DialogPopup>
|
||||
{initData?.reserveStatusId ? (
|
||||
<>
|
||||
<ReserveInfoView
|
||||
data={initData}
|
||||
handleList={handleList}
|
||||
handleButtons={handleButtonStatus}
|
||||
attachData={attachData}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<FormProvider {...methods}>
|
||||
{item && (
|
||||
<ReserveInfo
|
||||
control={control}
|
||||
formState={formState}
|
||||
register={register}
|
||||
getValues={getValues}
|
||||
data={initData}
|
||||
item={item}
|
||||
setError={setError}
|
||||
clearErrors={clearErrors}
|
||||
fileProps={{
|
||||
uploadRef,
|
||||
attachData,
|
||||
setAttachData,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ReserveClientInfo
|
||||
control={control}
|
||||
formState={formState}
|
||||
register={register}
|
||||
getValues={getValues}
|
||||
data={initData}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<DetailButtons
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
handleList={handleList}
|
||||
/>
|
||||
</FormProvider>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const { id, reserveItemId } = query
|
||||
let initData: IReserve = null
|
||||
let reserveItem: IReserveItemRelation = null
|
||||
let status: ICode = null
|
||||
|
||||
try {
|
||||
status = await (await reserveItemService.getCode('reserve-status')).data
|
||||
|
||||
if (id === '-1') {
|
||||
const result = await reserveItemService.getWithRelation(
|
||||
parseInt(reserveItemId as string),
|
||||
)
|
||||
if (result) {
|
||||
reserveItem = result.data
|
||||
}
|
||||
} else {
|
||||
const result = await reserveService.get(id as string)
|
||||
if (result) {
|
||||
initData = result.data
|
||||
reserveItem = initData.reserveItem
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`reserve detail server side props error occur : ${error.message}`,
|
||||
)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
reserveId: id,
|
||||
initData,
|
||||
reserveItem,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ReserveDetail
|
||||
363
frontend/admin/src/pages/reserve/index.tsx
Normal file
363
frontend/admin/src/pages/reserve/index.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import Search from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
import useSearchTypes from '@hooks/useSearchType'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Link from '@material-ui/core/Link'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import { ICode, ILocation, locationService, reserveItemService } from '@service'
|
||||
import { conditionAtom, conditionValue, errorStateSelector } from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import { reserveService } from 'src/service/Reserve'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
search: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
width: '10vw',
|
||||
maxWidth: 100,
|
||||
minWidth: 80,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const conditionKey = 'reserve'
|
||||
|
||||
type ColumnType = (
|
||||
props: ReserveListProps,
|
||||
data: Page,
|
||||
handleUpdate: (id: number) => void,
|
||||
toggleIsUse: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void,
|
||||
t: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
//그리드 컬럼 정의
|
||||
const getColumns: ColumnType = (props, data, handleUpdate, toggleIsUse, t) => {
|
||||
const { locations, categories, status } = props
|
||||
return [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'locationId',
|
||||
headerName: t('location'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>
|
||||
{
|
||||
locations.find(item => item.locationId === params.value)
|
||||
.locationName
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'categoryId',
|
||||
headerName: t('reserve_item.type'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{categories.find(item => item.codeId === params.value).codeName}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'reserveItemName',
|
||||
headerName: t('reserve_item.name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<Typography>
|
||||
<Link href={`/reserve/${params.row.reserveId}`} variant="body2">
|
||||
{params.value}
|
||||
</Link>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'totalQty',
|
||||
headerName: `${t('reserve.count')}/${t('reserve.number_of_people')}`,
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
headerName: t('reserve.user'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'reserveStatusId',
|
||||
headerName: t('reserve.status'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
renderCell: (params: GridCellParams) => (
|
||||
<>{status.find(item => item.codeId === params.value).codeName}</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'createDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
flex: 1,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
interface ReserveListProps {
|
||||
locations?: ILocation[]
|
||||
categories?: ICode[]
|
||||
status?: ICode[]
|
||||
}
|
||||
|
||||
const Reserve = (props: ReserveListProps) => {
|
||||
const { locations, categories } = props
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
//조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const [customKeyword, setCustomKeyword] = useState<conditionValue | null>({
|
||||
locationId: keywordState?.locationId || '0',
|
||||
categoryId: keywordState?.categoryId || 'all',
|
||||
})
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
// 에러 상태관리
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
//조회조건 select items
|
||||
const searchTypes = useSearchTypes([
|
||||
{
|
||||
key: 'item',
|
||||
label: t('reserve_item.name'),
|
||||
},
|
||||
])
|
||||
|
||||
//목록 데이터 조회 및 관리
|
||||
const { data, mutate } = reserveService.search({
|
||||
keywordType: keywordState?.keywordType || 'item',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
locationId:
|
||||
keywordState?.locationId !== '0' ? keywordState?.locationId : null,
|
||||
categoryId:
|
||||
keywordState?.categoryId !== 'all' ? keywordState?.categoryId : null,
|
||||
})
|
||||
|
||||
//목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate(data, false)
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
router.push('/reserve/item')
|
||||
}
|
||||
|
||||
//datagrid page change event
|
||||
const handlePageChange = (page: number, details?: any) => {
|
||||
setPageValue(page)
|
||||
}
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
router.push(`/reserve-item/${id}`)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
categoryId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
setCustomKeyword({
|
||||
...customKeyword,
|
||||
locationId: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
//사용여부 toggle 시 바로 update
|
||||
const toggleIsUse = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>, id: number) => {
|
||||
try {
|
||||
const result = await reserveItemService.updateUse(
|
||||
id,
|
||||
event.target.checked,
|
||||
)
|
||||
if (result?.status === 204) {
|
||||
mutate()
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorState({ error })
|
||||
}
|
||||
},
|
||||
[customKeyword],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(props, data, handleUpdate, toggleIsUse, t)
|
||||
}, [props, data, t])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={handleRegister}
|
||||
conditionKey={conditionKey}
|
||||
isNotWrapper={true}
|
||||
customKeyword={customKeyword}
|
||||
conditionNodes={
|
||||
<>
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-location"
|
||||
select
|
||||
value={customKeyword.locationId}
|
||||
onChange={handleLocationChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="location-all" value="0">
|
||||
<em>{t('common.all')}</em>
|
||||
</MenuItem>
|
||||
{locations &&
|
||||
locations.map(option => (
|
||||
<MenuItem key={option.locationId} value={option.locationId}>
|
||||
{option.locationName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
<Box className={classes.search}>
|
||||
<TextField
|
||||
id="select-category"
|
||||
select
|
||||
value={customKeyword.categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key="category-all" value="all">
|
||||
<em>{t('common.all')}</em>
|
||||
</MenuItem>
|
||||
{categories &&
|
||||
categories.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<CustomDataGrid
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.reserveId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
let locations: ILocation[] = []
|
||||
let categories: ICode[] = []
|
||||
let status: ICode[] = []
|
||||
|
||||
try {
|
||||
let result = await locationService.getList()
|
||||
if (result) {
|
||||
locations = result.data
|
||||
}
|
||||
|
||||
result = await reserveItemService.getCode('reserve-category')
|
||||
|
||||
if (result) {
|
||||
categories = result.data
|
||||
}
|
||||
|
||||
result = await reserveItemService.getCode('reserve-status')
|
||||
|
||||
if (result) {
|
||||
status = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`reserve item query error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories,
|
||||
locations,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Reserve
|
||||
79
frontend/admin/src/pages/reserve/item.tsx
Normal file
79
frontend/admin/src/pages/reserve/item.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import DialogPopup from '@components/DialogPopup'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import ReserveItem from '@pages/reserve-item'
|
||||
import { IReserveItem } from '@service'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
marginTop: theme.spacing(5),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
pos: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: '3rem',
|
||||
},
|
||||
}),
|
||||
)
|
||||
interface SearchItemProps {}
|
||||
|
||||
const SearchItem = (props: SearchItemProps) => {
|
||||
const classes = useStyles()
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false)
|
||||
|
||||
const handlePopup = (data: IReserveItem) => {
|
||||
if (data) {
|
||||
router.push(`/reserve/-1?reserveItemId=${data.reserveItemId}`)
|
||||
}
|
||||
|
||||
handleDialogClose()
|
||||
}
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography className={classes.pos} variant="h5" color="textSecondary">
|
||||
{t('reserve.msg.find_item')}
|
||||
</Typography>
|
||||
<Button variant="contained" color="primary" onClick={handleDialogOpen}>
|
||||
{`${t('reserve_item')} ${t('common.search')}`}
|
||||
</Button>
|
||||
<DialogPopup
|
||||
id="find-dialog"
|
||||
handleClose={handleDialogClose}
|
||||
open={dialogOpen}
|
||||
title={`${t('reserve_item')} ${t('label.button.find')}`}
|
||||
>
|
||||
<ReserveItem handlePopup={handlePopup} />
|
||||
</DialogPopup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchItem
|
||||
586
frontend/admin/src/pages/role-authorization/index.tsx
Normal file
586
frontend/admin/src/pages/role-authorization/index.tsx
Normal file
@@ -0,0 +1,586 @@
|
||||
import { CustomButtons, IButtonProps } from '@components/Buttons'
|
||||
import CustomAlert from '@components/CustomAlert'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE, GRID_ROW_HEIGHT } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import { Button } from '@material-ui/core'
|
||||
import Box from '@material-ui/core/Box'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import {
|
||||
DataGrid,
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import {
|
||||
IRole,
|
||||
RoleAuthorizationSavePayload,
|
||||
roleAuthorizationService,
|
||||
roleService,
|
||||
} from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { format, Page } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 권한 그리드 컬럼 정의
|
||||
type RoleColumnsType = (
|
||||
data: any[] | [],
|
||||
handleManageRole: (roleId: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getRoleColumns: RoleColumnsType = (data, handleManageRole, t) => [
|
||||
{
|
||||
field: 'roleId',
|
||||
headerName: t('role.role_id'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
headerName: t('role.role_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'roleContent',
|
||||
headerName: t('role.role_content'),
|
||||
headerAlign: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellRoleId(params: GridCellParams) {
|
||||
return (
|
||||
<div>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
onClick={() => {
|
||||
handleManageRole(params.row.roleId)
|
||||
}}
|
||||
>
|
||||
{t('role.manage_authorization')}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 인가 그리드 컬럼 정의
|
||||
type AuthorizationColumnsType = (
|
||||
data: Page,
|
||||
toggleCreatedAt: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
roleId: string,
|
||||
authorizationNo: number,
|
||||
) => void,
|
||||
roleAuthorizationApiRef: React.MutableRefObject<any>,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getAuthorizationColumns: AuthorizationColumnsType = (
|
||||
data,
|
||||
toggleCreatedAt,
|
||||
roleAuthorizationApiRef,
|
||||
t,
|
||||
) => [
|
||||
{
|
||||
field: 'authorizationName',
|
||||
headerName: t('authorization.authorization_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 250,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'urlPatternValue',
|
||||
headerName: t('authorization.url_pattern_value'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'httpMethodCode',
|
||||
headerName: t('authorization.http_method_code'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'sortSeq',
|
||||
headerName: t('common.sort_seq'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
headerName: t('common.created_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
sortable: false,
|
||||
renderCell: function renderCellCreatedAt(params: GridCellParams) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
roleAuthorizationApiRef.current = params.api // api
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(params.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
toggleCreatedAt(
|
||||
event,
|
||||
params.row.roleId as string,
|
||||
params.row.authorizationNo as number,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'authorization'
|
||||
|
||||
export interface IRoleAuthorizationProps {
|
||||
roles: IRole[]
|
||||
initRoleId: string
|
||||
}
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const RoleAuthorization = ({ roles, initRoleId }: IRoleAuthorizationProps) => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const roleAuthorizationApiRef = useRef<any>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'authorizationName',
|
||||
label: t('authorization.authorization_name'),
|
||||
},
|
||||
{
|
||||
key: 'urlPatternValue',
|
||||
label: t('authorization.url_pattern_value'),
|
||||
},
|
||||
{
|
||||
key: 'httpMethodCode',
|
||||
label: t('authorization.http_method_code'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const [customAlert, setCustomAlert] = useState<any>({
|
||||
open: false,
|
||||
message: '',
|
||||
})
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
const [roleId, setRoleId] = useState<string>(initRoleId)
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 권한 정보 초기화
|
||||
if (roles) {
|
||||
let role
|
||||
if (roleId) {
|
||||
role = roles.find(m => m.roleId === roleId)
|
||||
}
|
||||
if (role === undefined) {
|
||||
role = roles.find(m => m)
|
||||
}
|
||||
if (role !== undefined) {
|
||||
if (roleId !== role.roleId) setRoleId(role.roleId)
|
||||
}
|
||||
}
|
||||
|
||||
// 인가 목록 조회
|
||||
const { data, mutate } = roleAuthorizationService.search(roleId, {
|
||||
keywordType: keywordState?.keywordType || 'authorizationName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
// 그리드 체크 해제
|
||||
const uncheckedGrid = useCallback(() => {
|
||||
const selectedRowKeys = roleAuthorizationApiRef.current
|
||||
?.getSelectedRows()
|
||||
.keys()
|
||||
|
||||
let cnt = 0
|
||||
while (cnt < data.numberOfElements) {
|
||||
const gridRowId = selectedRowKeys.next()
|
||||
if (gridRowId.done === true) break
|
||||
roleAuthorizationApiRef.current.selectRow(gridRowId.value, false, false)
|
||||
cnt += 1
|
||||
}
|
||||
}, [data?.numberOfElements])
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
uncheckedGrid()
|
||||
|
||||
mutate()
|
||||
}, [mutate, setSuccessSnackBar, uncheckedGrid])
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 인가 toggle 시 save
|
||||
const toggleCreatedAt = useCallback(
|
||||
async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
paramRoleId: string,
|
||||
paramAuthorizationNo: number,
|
||||
) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const selectedRow: RoleAuthorizationSavePayload = {
|
||||
roleId: paramRoleId,
|
||||
authorizationNo: paramAuthorizationNo,
|
||||
}
|
||||
|
||||
if (event.target.checked) {
|
||||
await roleAuthorizationService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: [selectedRow],
|
||||
})
|
||||
} else {
|
||||
await roleAuthorizationService.delete({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: [selectedRow],
|
||||
})
|
||||
}
|
||||
},
|
||||
[errorCallback, setSuccessSnackBar, successCallback],
|
||||
)
|
||||
|
||||
// 권한매핑관리
|
||||
const handleManageRole = useCallback((_roleId: string) => {
|
||||
setRoleId(_roleId)
|
||||
setPageValue(0)
|
||||
}, [])
|
||||
|
||||
// 권한 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const roleColumns = useMemo(
|
||||
() => getRoleColumns(roles, handleManageRole, t),
|
||||
[handleManageRole, roles, t],
|
||||
)
|
||||
|
||||
// 인가 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const authorizationColumns = useMemo(
|
||||
() =>
|
||||
getAuthorizationColumns(
|
||||
data,
|
||||
toggleCreatedAt,
|
||||
roleAuthorizationApiRef,
|
||||
t,
|
||||
),
|
||||
[data, toggleCreatedAt, roleAuthorizationApiRef, t],
|
||||
)
|
||||
|
||||
// 선택된 행 수 반환
|
||||
const getSelectedRowCount = (checked: boolean) => {
|
||||
let count = 0
|
||||
|
||||
const selectedRows = roleAuthorizationApiRef.current.getSelectedRows()
|
||||
selectedRows.forEach(m => {
|
||||
if (m.createdAt === checked) {
|
||||
count += 1
|
||||
}
|
||||
})
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// 선택된 행 반환
|
||||
const getSelectedRows = (checked: boolean) => {
|
||||
let list: RoleAuthorizationSavePayload[] = []
|
||||
|
||||
const selectedRows = roleAuthorizationApiRef.current.getSelectedRows()
|
||||
selectedRows.forEach(m => {
|
||||
if (m.createdAt === checked) {
|
||||
const saved: RoleAuthorizationSavePayload = {
|
||||
roleId: m.roleId,
|
||||
authorizationNo: m.authorizationNo,
|
||||
}
|
||||
list.push(saved)
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// 선택 저장
|
||||
const handleSave = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const selectedRows = getSelectedRows(false)
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
successCallback()
|
||||
return
|
||||
}
|
||||
|
||||
roleAuthorizationService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: selectedRows,
|
||||
})
|
||||
}, [setSuccessSnackBar, successCallback, errorCallback])
|
||||
|
||||
// 선택 삭제
|
||||
const handleDelete = useCallback(() => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const selectedRows = getSelectedRows(true)
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
successCallback()
|
||||
return
|
||||
}
|
||||
|
||||
roleAuthorizationService.delete({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: selectedRows,
|
||||
})
|
||||
}, [setSuccessSnackBar, successCallback, errorCallback])
|
||||
|
||||
// 선택 등록, 선택 삭제 버튼
|
||||
const saveButton: IButtonProps = {
|
||||
label: t('label.button.selection_registration'),
|
||||
variant: 'outlined',
|
||||
color: 'default',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.registration'),
|
||||
handleButton: handleSave,
|
||||
validate: () => {
|
||||
if (roleAuthorizationApiRef.current.getSelectedRows().size === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.format'), [
|
||||
`${t('label.button.reg')} ${t('common.target')}`,
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const count = getSelectedRowCount(false) // 미등록만
|
||||
if (count === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.already_saved.format'), [
|
||||
t('authorization'),
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
completeMessage: t('msg.success.save'),
|
||||
}
|
||||
const deleteButton: IButtonProps = {
|
||||
label: t('label.button.selection_delete'),
|
||||
variant: 'outlined',
|
||||
color: 'default',
|
||||
size: 'small',
|
||||
confirmMessage: t('msg.confirm.delete'),
|
||||
handleButton: handleDelete,
|
||||
validate: () => {
|
||||
if (roleAuthorizationApiRef.current.getSelectedRows().size === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.format'), [
|
||||
`${t('label.button.delete')} ${t('common.target')}`,
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const count = getSelectedRowCount(true) // 등록만
|
||||
if (count === 0) {
|
||||
setCustomAlert({
|
||||
open: true,
|
||||
message: format(t('valid.selection.already_deleted.format'), [
|
||||
t('authorization'),
|
||||
]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
completeMessage: t('msg.success.delete'),
|
||||
}
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<DataGrid
|
||||
rows={roles || []}
|
||||
columns={roleColumns}
|
||||
rowHeight={GRID_ROW_HEIGHT}
|
||||
autoHeight
|
||||
getRowId={r => r.roleId}
|
||||
hideFooter
|
||||
selectionModel={(roles || [])
|
||||
.filter(r => r.roleId === roleId)
|
||||
.map(r => r.roleId)}
|
||||
onSelectionModelChange={newSelection => {
|
||||
setRoleId(newSelection[0]?.toString())
|
||||
}}
|
||||
/>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={authorizationColumns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.authorizationNo}
|
||||
checkboxSelection
|
||||
disableSelectionOnClick
|
||||
/>
|
||||
<CustomButtons buttons={[saveButton, deleteButton]} />
|
||||
<CustomAlert
|
||||
contentText={customAlert.message}
|
||||
open={customAlert.open}
|
||||
handleAlert={() => setCustomAlert({ open: false })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const roleId = query.roleId as string
|
||||
|
||||
let roles: IRole[] = []
|
||||
let initRoleId = ''
|
||||
|
||||
try {
|
||||
const result = await roleService.searchAll()
|
||||
if (result) {
|
||||
roles = result.data
|
||||
|
||||
if (roles) {
|
||||
if (roleId) {
|
||||
initRoleId = roles.find(m => m.roleId === roleId).roleId
|
||||
}
|
||||
if (!initRoleId) {
|
||||
initRoleId = roles.find(m => m).roleId
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`role list getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
roles,
|
||||
initRoleId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default RoleAuthorization
|
||||
235
frontend/admin/src/pages/role/index.tsx
Normal file
235
frontend/admin/src/pages/role/index.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import { Button } from '@material-ui/core'
|
||||
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import { Page, rownum } from '@utils'
|
||||
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { conditionAtom } from '@stores'
|
||||
|
||||
// api
|
||||
import { roleService } from '@service'
|
||||
import usePage from '@hooks/usePage'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
search: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
},
|
||||
iconButton: {
|
||||
padding: theme.spacing(1),
|
||||
marginLeft: theme.spacing(1),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
fab: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
handleManageRole: (roleId: string) => void,
|
||||
t?: TFunction,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (data, handleManageRole, t) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'roleId',
|
||||
headerName: t('role.role_id'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
headerName: t('role.role_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'roleContent',
|
||||
headerName: t('role.role_content'),
|
||||
headerAlign: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
headerName: t('common.created_datetime'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
convertStringToDateFormat(params.value as string, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return (
|
||||
<div>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
onClick={() => {
|
||||
handleManageRole(params.row.roleId)
|
||||
}}
|
||||
>
|
||||
{t('role.manage_authorization')}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'role'
|
||||
|
||||
// 실제 render 컴포넌트
|
||||
const Role: NextPage<any> = () => {
|
||||
// props 및 전역변수
|
||||
// const { id } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'roleName',
|
||||
label: t('role.role_name'),
|
||||
},
|
||||
{
|
||||
key: 'roleContent',
|
||||
label: t('role.role_content'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = roleService.search({
|
||||
keywordType: keywordState?.keywordType || 'roleName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 권한 인가 매핑 관리 화면 이동
|
||||
const handleManageRole = useCallback(
|
||||
(roleId: string) => {
|
||||
route.push(
|
||||
{
|
||||
pathname: `/role-authorization`,
|
||||
query: { roleId },
|
||||
},
|
||||
'/role-authorization',
|
||||
)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, handleManageRole, t),
|
||||
[data, handleManageRole, t],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.roleId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Role
|
||||
137
frontend/admin/src/pages/statistics/index.tsx
Normal file
137
frontend/admin/src/pages/statistics/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import CustomBarChart from '@components/CustomBarChart'
|
||||
import { format as dateFormat, getCurrentDate } from '@libs/date'
|
||||
import { Card, CardContent, Typography } from '@material-ui/core'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { DailyPayload, ISite, statisticsService } from '@service'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const initDailyPayload: DailyPayload = {
|
||||
year: parseInt(dateFormat(getCurrentDate(), 'yyyy')),
|
||||
month: parseInt(dateFormat(getCurrentDate(), 'MM')),
|
||||
}
|
||||
|
||||
const tooltipContent = tooltip => (
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h5">{tooltip}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
interface StatisticsProps {
|
||||
sites: ISite[]
|
||||
}
|
||||
|
||||
function Statistics(props: StatisticsProps) {
|
||||
const { sites } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [siteState, setSiteState] = useState<number>(sites[0]?.id)
|
||||
const [dailyPayload, setDailyPayload] =
|
||||
useState<DailyPayload>(initDailyPayload)
|
||||
|
||||
const { monthly } = statisticsService.getMonthly(siteState)
|
||||
const { daily } = statisticsService.getDaily(siteState, dailyPayload)
|
||||
|
||||
const handleSiteChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setSiteState(event.target.value as number)
|
||||
setDailyPayload(initDailyPayload)
|
||||
}
|
||||
|
||||
const handleMonthlyClick = (data, index) => {
|
||||
if (data) {
|
||||
setDailyPayload({
|
||||
year: data.year,
|
||||
month: data.month,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const monthlyTooltipContent = ({ active, payload, label }) => {
|
||||
if (!active || !payload || !label) return null
|
||||
|
||||
return tooltipContent(
|
||||
`${label} ${t('statistics.month')} : ${payload[0].value}`,
|
||||
)
|
||||
}
|
||||
|
||||
const dailyTooltipContent = ({ active, payload, label }) => {
|
||||
if (!active || !payload || !label) return null
|
||||
|
||||
return tooltipContent(
|
||||
`${label} ${t('statistics.day')} : ${payload[0].value}`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={siteState}
|
||||
onChange={handleSiteChange}
|
||||
>
|
||||
{sites?.map(item => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{monthly && (
|
||||
<CustomBarChart
|
||||
id="monthlyChart"
|
||||
data={monthly}
|
||||
tooltipContent={monthlyTooltipContent}
|
||||
title={`${dailyPayload.year}${t('statistics.year')} ${t(
|
||||
'statistics.monthly',
|
||||
)} ${t('statistics.access')}`}
|
||||
handleCellClick={handleMonthlyClick}
|
||||
customxAxisTick={(value: any, index: number) => {
|
||||
return `${value} ${t('statistics.month')}`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{daily && (
|
||||
<CustomBarChart
|
||||
id="dailyChart"
|
||||
data={daily}
|
||||
tooltipContent={dailyTooltipContent}
|
||||
title={`${dailyPayload.year}${t('statistics.year')} ${
|
||||
dailyPayload.month
|
||||
}${t('statistics.month')} ${t('statistics.daily')} ${t(
|
||||
'statistics.access',
|
||||
)}`}
|
||||
customxAxisTick={(value: any, index: number) => {
|
||||
return `${value} ${t('statistics.day')}`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
let sites: ISite[] = []
|
||||
|
||||
try {
|
||||
const result = await statisticsService.getSites()
|
||||
|
||||
if (result) {
|
||||
sites = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`statistics getServerSideProps error ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
sites,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Statistics
|
||||
431
frontend/admin/src/pages/user/[id].tsx
Normal file
431
frontend/admin/src/pages/user/[id].tsx
Normal file
@@ -0,0 +1,431 @@
|
||||
import { DetailButtons } from '@components/Buttons'
|
||||
import ValidationAlert from '@components/EditForm/ValidationAlert'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import FormControl from '@material-ui/core/FormControl'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import InputLabel from '@material-ui/core/InputLabel'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Select from '@material-ui/core/Select'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import {
|
||||
codeService,
|
||||
ICode,
|
||||
IRole,
|
||||
roleService,
|
||||
UserSavePayload,
|
||||
userService,
|
||||
} from '@service'
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import { format } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
formControl: {
|
||||
width: '100%',
|
||||
},
|
||||
switch: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
margin: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
'& .MuiButton-root': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface IUserFormInput {
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
userName: string
|
||||
roleId: string
|
||||
userStateCode: string
|
||||
}
|
||||
|
||||
export interface IUserItemsProps {
|
||||
userId: string
|
||||
initData: UserSavePayload | null
|
||||
roles: IRole[]
|
||||
userStateCodeList: ICode[]
|
||||
}
|
||||
|
||||
const UserItem = ({
|
||||
userId,
|
||||
initData,
|
||||
roles,
|
||||
userStateCodeList,
|
||||
}: IUserItemsProps) => {
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// form hook
|
||||
const methods = useForm<IUserFormInput>({
|
||||
defaultValues: {
|
||||
email: initData?.email || '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
userName: initData?.userName || '',
|
||||
roleId: initData?.roleId || 'ROLE_ANONYMOUS',
|
||||
userStateCode: initData?.userStateCode || '00',
|
||||
},
|
||||
})
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
handleSubmit,
|
||||
} = methods
|
||||
|
||||
const successCallback = () => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
route.back()
|
||||
}
|
||||
|
||||
const errorCallback = (error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSubmit 저장
|
||||
const handleSave = async (formData: IUserFormInput) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
const saved: UserSavePayload = {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
userName: formData.userName,
|
||||
roleId: formData.roleId,
|
||||
userStateCode: formData.userStateCode,
|
||||
}
|
||||
|
||||
if (userId === '-1') {
|
||||
await userService.save({
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
} else {
|
||||
await userService.update({
|
||||
userId,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
data: saved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 형식 확인
|
||||
const checkPasswordPattern = value =>
|
||||
/^(?=.*?[a-zA-Z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,20}$/.test(value)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 100 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('user.email')}
|
||||
name="email"
|
||||
required
|
||||
inputProps={{ maxLength: 100 }}
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('user.email'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.email && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.email}
|
||||
target={[100]}
|
||||
label={t('user.email')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
rules={{
|
||||
required: userId === '-1',
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value =>
|
||||
!value ||
|
||||
checkPasswordPattern(value) ||
|
||||
(t('valid.password') as string),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label={t('user.password')}
|
||||
name="password"
|
||||
required={userId === '-1'}
|
||||
inputProps={{ maxLength: 20 }}
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('user.password'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.password && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.password}
|
||||
target={[20]}
|
||||
label={t('user.password')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="passwordConfirm"
|
||||
control={control}
|
||||
rules={{
|
||||
required: userId === '-1',
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: format(t('valid.maxlength.format'), [20]),
|
||||
},
|
||||
validate: value =>
|
||||
(!methods.getValues().password && !value) ||
|
||||
(checkPasswordPattern(value) &&
|
||||
methods.getValues().password === value) ||
|
||||
(t('valid.password.confirm') as string),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label={t('label.title.password_confirm')}
|
||||
name="passwordConfirm"
|
||||
required={userId === '-1'}
|
||||
inputProps={{ maxLength: 20 }}
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('label.title.password_confirm'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.passwordConfirm && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.passwordConfirm}
|
||||
target={[20]}
|
||||
label={t('label.title.password_confirm')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Box boxShadow={1}>
|
||||
<Controller
|
||||
name="userName"
|
||||
control={control}
|
||||
rules={{ required: true, maxLength: 25 }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label={t('user.user_name')}
|
||||
name="userName"
|
||||
required
|
||||
inputProps={{ maxLength: 25 }}
|
||||
id="outlined-full-width"
|
||||
placeholder={format(t('msg.placeholder.format'), [
|
||||
t('user.user_name'),
|
||||
])}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.userName && (
|
||||
<ValidationAlert
|
||||
fieldError={errors.userName}
|
||||
target={[25]}
|
||||
label={t('user.user_name')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="roleId-label" required>
|
||||
{t('role')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="roleId"
|
||||
control={control}
|
||||
defaultValue={initData?.roleId || 'ROLE_ANONYMOUS'}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="roleId"
|
||||
required
|
||||
labelId="roleId-label"
|
||||
label={t('role')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{roles.map(option => (
|
||||
<MenuItem key={option.roleId} value={option.roleId}>
|
||||
{option.roleName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<InputLabel id="userStateCode-label" required>
|
||||
{t('user.user_state_code')}
|
||||
</InputLabel>
|
||||
<Controller
|
||||
name="userStateCode"
|
||||
control={control}
|
||||
defaultValue={initData?.userStateCode || '00'}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
variant="outlined"
|
||||
name="userStateCode"
|
||||
required
|
||||
labelId="roleId-label"
|
||||
label={t('user.user_state_code')}
|
||||
margin="dense"
|
||||
{...field}
|
||||
>
|
||||
{userStateCodeList.map(option => (
|
||||
<MenuItem key={option.codeId} value={option.codeId}>
|
||||
{option.codeName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<DetailButtons
|
||||
handleList={() => {
|
||||
route.back()
|
||||
}}
|
||||
handleSave={handleSubmit(handleSave)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
|
||||
const userId = query.id
|
||||
|
||||
let data = {}
|
||||
let roles: any[] = []
|
||||
let userStateCodeList = []
|
||||
|
||||
try {
|
||||
const result = await roleService.searchAll()
|
||||
if (result) {
|
||||
roles = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`role query error ${error.message}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const codeList = await codeService.getCodeDetailList('user_state_code')
|
||||
if (codeList) {
|
||||
userStateCodeList = (await codeList.data) as ICode[]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`codes query error ${error.message}`)
|
||||
}
|
||||
|
||||
try {
|
||||
if (userId !== '-1') {
|
||||
const result = await userService.get(userId as string)
|
||||
if (result) {
|
||||
data = (await result.data) as UserSavePayload
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`user info query error ${error.message}`)
|
||||
if (error.response?.data?.code === 'E003') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
userId,
|
||||
initData: data,
|
||||
roles,
|
||||
userStateCodeList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default UserItem
|
||||
299
frontend/admin/src/pages/user/index.tsx
Normal file
299
frontend/admin/src/pages/user/index.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { GridButtons } from '@components/Buttons'
|
||||
import { PopupProps } from '@components/DialogPopup'
|
||||
import Search, { IKeywordType } from '@components/Search'
|
||||
import CustomDataGrid from '@components/Table/CustomDataGrid'
|
||||
import { GRID_PAGE_SIZE } from '@constants'
|
||||
import usePage from '@hooks/usePage'
|
||||
// 내부 컴포넌트 및 custom hook, etc...
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import Button from '@material-ui/core/Button'
|
||||
// material-ui deps
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import {
|
||||
GridCellParams,
|
||||
GridColDef,
|
||||
GridValueFormatterParams,
|
||||
GridValueGetterParams,
|
||||
} from '@material-ui/data-grid'
|
||||
// api
|
||||
import { userService } from '@service'
|
||||
import {
|
||||
conditionAtom,
|
||||
detailButtonsSnackAtom,
|
||||
errorStateSelector,
|
||||
} from '@stores'
|
||||
import { Page, rownum } from '@utils'
|
||||
import { AxiosError } from 'axios'
|
||||
import { NextPage } from 'next'
|
||||
import { TFunction, useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// 상태관리 recoil
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
|
||||
// material-ui style
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 그리드 컬럼 정의
|
||||
type ColumnsType = (
|
||||
data: Page,
|
||||
deleteUser: (userId: string) => void,
|
||||
updateUser: (userId: string) => void,
|
||||
t?: TFunction,
|
||||
handlePopup?: (data: any) => void,
|
||||
) => GridColDef[]
|
||||
|
||||
const getColumns: ColumnsType = (
|
||||
data,
|
||||
deleteUser,
|
||||
updateUser,
|
||||
t,
|
||||
handlePopup,
|
||||
) => [
|
||||
{
|
||||
field: 'rownum',
|
||||
headerName: t('common.no'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
rownum(data, params.api.getRowIndex(params.id), 'asc'),
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
headerName: t('user.email'),
|
||||
headerAlign: 'center',
|
||||
align: 'left',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
headerName: t('user.user_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
headerName: t('role.role_name'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'loginFailCount',
|
||||
headerName: t('user.login_lock_at'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
renderCell: function renderCellLoginFailCount(params: GridCellParams) {
|
||||
return params.row.loginFailCount >= 5 ? '잠김' : '해당없음'
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userStateCodeName',
|
||||
headerName: t('user.user_state_code'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'lastLoginDate',
|
||||
headerName: t('user.last_login_date'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
sortable: false,
|
||||
valueFormatter: (params: GridValueFormatterParams) =>
|
||||
params.value === null
|
||||
? ''
|
||||
: convertStringToDateFormat(
|
||||
params.value as string,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
headerName: t('common.manage'),
|
||||
headerAlign: 'center',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
sortable: false,
|
||||
renderCell: function renderCellButtons(params: GridCellParams) {
|
||||
return handlePopup ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopup(params.row)
|
||||
}}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
{t('common.select')}
|
||||
</Button>
|
||||
) : (
|
||||
<GridButtons
|
||||
id={params.row.userId as string}
|
||||
handleDelete={deleteUser}
|
||||
handleUpdate={updateUser}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const conditionKey = 'user'
|
||||
|
||||
export type UserProps = PopupProps
|
||||
|
||||
// 실제 render되는 컴포넌트
|
||||
const User: NextPage<UserProps> = props => {
|
||||
// props 및 전역변수
|
||||
const { handlePopup } = props
|
||||
const classes = useStyles()
|
||||
const route = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 조회조건 select items
|
||||
const searchTypes: IKeywordType[] = [
|
||||
{
|
||||
key: 'userName',
|
||||
label: t('user.user_name'),
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: t('user.email'),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 상태관리 필요한 훅
|
||||
*/
|
||||
// 조회조건 상태관리
|
||||
const keywordState = useRecoilValue(conditionAtom(conditionKey))
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const { page, setPageValue } = usePage(conditionKey)
|
||||
|
||||
// 목록 데이터 조회 및 관리
|
||||
const { data, mutate } = userService.search({
|
||||
keywordType: keywordState?.keywordType || 'userName',
|
||||
keyword: keywordState?.keyword || '',
|
||||
size: GRID_PAGE_SIZE,
|
||||
page,
|
||||
})
|
||||
|
||||
/**
|
||||
* 비지니스 로직
|
||||
*/
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setSuccessSnackBar('none')
|
||||
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
mutate()
|
||||
}, [mutate, setSuccessSnackBar])
|
||||
|
||||
// 삭제
|
||||
const deleteUser = useCallback(
|
||||
(userId: string) => {
|
||||
setSuccessSnackBar('loading')
|
||||
|
||||
userService.delete({
|
||||
userId,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
[errorCallback, mutate, setSuccessSnackBar],
|
||||
)
|
||||
|
||||
// 수정 시 상세 화면 이동
|
||||
const updateUser = useCallback(
|
||||
(userId: string) => {
|
||||
route.push(`/user/${userId}`)
|
||||
},
|
||||
[route],
|
||||
)
|
||||
|
||||
// 목록컬럼 재정의 > 컬럼에 비지니스 로직이 필요한 경우
|
||||
const columns = useMemo(
|
||||
() => getColumns(data, deleteUser, updateUser, t, handlePopup),
|
||||
[data, deleteUser, updateUser, t, handlePopup],
|
||||
)
|
||||
|
||||
// 목록 조회
|
||||
const handleSearch = () => {
|
||||
if (page === 0) {
|
||||
mutate()
|
||||
} else {
|
||||
setPageValue(0)
|
||||
}
|
||||
}
|
||||
|
||||
// datagrid page change event
|
||||
const handlePageChange = (_page: number, details?: any) => {
|
||||
setPageValue(_page)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Search
|
||||
keywordTypeItems={searchTypes}
|
||||
handleSearch={handleSearch}
|
||||
handleRegister={
|
||||
handlePopup
|
||||
? null
|
||||
: () => {
|
||||
route.push('user/-1')
|
||||
}
|
||||
}
|
||||
conditionKey={conditionKey}
|
||||
/>
|
||||
<CustomDataGrid
|
||||
page={page}
|
||||
classes={classes}
|
||||
rows={data?.content}
|
||||
columns={columns}
|
||||
rowCount={data?.totalElements}
|
||||
paginationMode="server"
|
||||
pageSize={GRID_PAGE_SIZE}
|
||||
onPageChange={handlePageChange}
|
||||
getRowId={r => r.userId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default User
|
||||
Reference in New Issue
Block a user