✨ frontend add
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user