✨ frontend add
This commit is contained in:
488
frontend/admin/src/components/comment/list.tsx
Normal file
488
frontend/admin/src/components/comment/list.tsx
Normal file
@@ -0,0 +1,488 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
useTheme,
|
||||
} from '@material-ui/core/styles'
|
||||
import Box from '@material-ui/core/Box'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardActions from '@material-ui/core/CardActions'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||
import RefreshIcon from '@material-ui/icons/Refresh'
|
||||
import Link from '@material-ui/core/Link'
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
|
||||
import Button from '@material-ui/core/Button'
|
||||
|
||||
import { detailButtonsSnackAtom, errorStateSelector } from '@stores'
|
||||
import useUser from '@hooks/useUser'
|
||||
import { commentService, IComment } from '@service'
|
||||
import { convertStringToDateFormat } from '@libs/date'
|
||||
import { ConfirmDialog, ConfirmDialogProps } from '@components/Confirm'
|
||||
import { CustomButtons } from '@components/Buttons'
|
||||
import { CommentForm } from './form'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
commentRoot: {
|
||||
marginTop: theme.spacing(1),
|
||||
padding: theme.spacing(0, 2, 1, 2),
|
||||
},
|
||||
commentBox: {
|
||||
padding: theme.spacing(2, 2, 1, 2),
|
||||
},
|
||||
commentTitle: {
|
||||
display: 'flex',
|
||||
padding: theme.spacing(2, 1),
|
||||
},
|
||||
commentView: {
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
commentIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
commentContent: {
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
commentDate: {
|
||||
marginRight: theme.spacing(3),
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
moreBox: {
|
||||
textAlign: 'center',
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
black: {
|
||||
color: 'black',
|
||||
},
|
||||
ml1: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
pd1: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
pdtb1: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
cancel: {
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ICommentProps {
|
||||
boardNo: number
|
||||
postsNo: number
|
||||
commentUseAt: boolean
|
||||
deleteAt: number
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
refreshCommentCount: (count) => void
|
||||
}
|
||||
|
||||
interface ICommentSearchProps {
|
||||
_page: number
|
||||
_mode: 'replace' | 'append' | 'until'
|
||||
}
|
||||
|
||||
const Comment: React.FC<ICommentProps> = ({
|
||||
boardNo,
|
||||
postsNo,
|
||||
commentUseAt,
|
||||
deleteAt,
|
||||
refreshCommentCount,
|
||||
}: ICommentProps) => {
|
||||
const classes = useStyles()
|
||||
const { user } = useUser()
|
||||
const { t } = useTranslation()
|
||||
const theme = useTheme()
|
||||
const pagingSize = 2
|
||||
|
||||
// 현 페이지내 필요한 hook
|
||||
const [page, setPage] = useState<number>(undefined)
|
||||
const [totalPages, setTotalPages] = useState<number>(0)
|
||||
|
||||
// 버튼 component 상태 전이
|
||||
const setSuccessSnackBar = useSetRecoilState(detailButtonsSnackAtom)
|
||||
|
||||
// 상태관리 hook
|
||||
const setErrorState = useSetRecoilState(errorStateSelector)
|
||||
|
||||
const [openConfirm, setOpenConfirm] = useState<boolean>(false)
|
||||
const [confirm, setConfirm] = useState<ConfirmDialogProps>({
|
||||
open: openConfirm,
|
||||
handleConfirm: () => {
|
||||
setOpenConfirm(false)
|
||||
},
|
||||
handleClose: () => {
|
||||
setOpenConfirm(false)
|
||||
},
|
||||
})
|
||||
|
||||
// const [comments, setComments] = useRecoilState(commentState)
|
||||
const [comments, setComments] = useState<any[]>([])
|
||||
|
||||
// 댓글 데이터 복사본 리턴
|
||||
const cloneComments = useCallback(
|
||||
() => comments.slice(0, comments.length),
|
||||
[comments],
|
||||
)
|
||||
|
||||
// 페이지 조회
|
||||
const getComments = useCallback(
|
||||
({ _page, _mode }: ICommentSearchProps) => {
|
||||
let searchPage = _page
|
||||
let searchSize = pagingSize
|
||||
if (_mode === 'until') {
|
||||
searchSize = pagingSize * (_page + 1)
|
||||
searchPage = 0
|
||||
}
|
||||
|
||||
commentService
|
||||
.list(boardNo, postsNo, searchSize, searchPage)
|
||||
.then(result => {
|
||||
setPage(_page)
|
||||
// setTotalPages(result.totalPages)
|
||||
setTotalPages(Math.ceil(result.groupElements / pagingSize))
|
||||
refreshCommentCount(result.totalElements)
|
||||
|
||||
let arr = _mode === 'append' ? cloneComments() : []
|
||||
arr.push(...result.content)
|
||||
|
||||
setComments(arr)
|
||||
})
|
||||
},
|
||||
[boardNo, cloneComments, postsNo, refreshCommentCount],
|
||||
)
|
||||
|
||||
// 전체 조회
|
||||
const getAllComments = () => {
|
||||
commentService.all(boardNo, postsNo).then(result => {
|
||||
setPage(result.number)
|
||||
setTotalPages(result.totalPages)
|
||||
refreshCommentCount(result.totalElements)
|
||||
|
||||
let arr = []
|
||||
arr.push(...result.content)
|
||||
|
||||
setComments(arr)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (page === undefined) {
|
||||
getComments({ _page: 0, _mode: 'replace' })
|
||||
}
|
||||
}, [getComments, page])
|
||||
|
||||
// 댓글 갱신
|
||||
const handleRefresh = useCallback(() => {
|
||||
// getComments({ _page: 0, _mode: 'replace' }) // 첫페이지 재조회
|
||||
getComments({ _page: page, _mode: 'until' }) // 현재 페이지까지 재조회
|
||||
}, [getComments, page])
|
||||
|
||||
// 댓글 상태 초기화
|
||||
const initComments = useCallback(() => {
|
||||
let arr: IComment[] = cloneComments()
|
||||
|
||||
while (true) {
|
||||
const index = arr.findIndex(a => a.mode === 'reply' || a.mode === 'edit')
|
||||
if (index === -1) break
|
||||
|
||||
if (arr[index].mode === 'reply') {
|
||||
arr.splice(index, 1)
|
||||
} else {
|
||||
arr[index].mode = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
return arr
|
||||
}, [cloneComments])
|
||||
|
||||
// 성공 callback
|
||||
const successCallback = useCallback(() => {
|
||||
setSuccessSnackBar('success')
|
||||
|
||||
// handleRefresh()
|
||||
|
||||
let arr: IComment[] = initComments()
|
||||
setComments(arr)
|
||||
}, [initComments, setSuccessSnackBar])
|
||||
|
||||
// 에러 callback
|
||||
const errorCallback = useCallback(
|
||||
(error: AxiosError) => {
|
||||
setErrorState({
|
||||
error,
|
||||
})
|
||||
},
|
||||
[setErrorState],
|
||||
)
|
||||
|
||||
// 댓글 더보기
|
||||
const handleCommentMore = () => {
|
||||
getComments({ _page: page + 1, _mode: 'append' })
|
||||
}
|
||||
|
||||
// 댓글 답글쓰기
|
||||
const handleCommentReply = async (parentCommentNo: number) => {
|
||||
let arr: IComment[] = initComments()
|
||||
|
||||
const parentIndex = arr.findIndex(a => a.commentNo === parentCommentNo)
|
||||
|
||||
const reply: IComment = {
|
||||
boardNo,
|
||||
postsNo,
|
||||
groupNo: arr[parentIndex].groupNo,
|
||||
parentCommentNo,
|
||||
depthSeq: arr[parentIndex].depthSeq + 1,
|
||||
createdBy: user.userId,
|
||||
createdName: user.userName,
|
||||
commentContent: '',
|
||||
mode: 'reply',
|
||||
}
|
||||
|
||||
arr.splice(parentIndex + 1, 0, reply)
|
||||
|
||||
setComments(arr)
|
||||
}
|
||||
|
||||
// 댓글 수정
|
||||
const handleCommentEdit = async (commentNo: number) => {
|
||||
let arr: IComment[] = initComments()
|
||||
|
||||
const index = arr.findIndex(a => a.commentNo === commentNo)
|
||||
|
||||
arr[index].mode = 'edit'
|
||||
setComments(arr)
|
||||
}
|
||||
|
||||
// 댓글 삭제
|
||||
const handleCommentDelete = async (commentNo: number) => {
|
||||
setConfirm({
|
||||
open: openConfirm,
|
||||
contentText: t('msg.confirm.delete'),
|
||||
handleConfirm: () => {
|
||||
setOpenConfirm(false)
|
||||
|
||||
commentService.delete({
|
||||
boardNo,
|
||||
postsNo,
|
||||
commentNo,
|
||||
callback: successCallback,
|
||||
errorCallback,
|
||||
})
|
||||
},
|
||||
handleClose: () => {
|
||||
setOpenConfirm(false)
|
||||
},
|
||||
})
|
||||
setOpenConfirm(true)
|
||||
}
|
||||
|
||||
// handleSubmit 댓글 저장
|
||||
const handleCommentSave = async (comment: IComment) => {
|
||||
if (comment.commentNo > 0) {
|
||||
await commentService.update({
|
||||
callback: () => {
|
||||
successCallback()
|
||||
|
||||
getComments({ _page: page, _mode: 'until' }) // 현재 페이지까지 재조회
|
||||
},
|
||||
errorCallback,
|
||||
data: comment,
|
||||
})
|
||||
} else {
|
||||
await commentService.save({
|
||||
callback: () => {
|
||||
successCallback()
|
||||
|
||||
if (comment.parentCommentNo) {
|
||||
getComments({ _page: page, _mode: 'until' }) // 현재 페이지까지 재조회
|
||||
} else {
|
||||
getAllComments() // 마지막 페이지까지 조회
|
||||
}
|
||||
},
|
||||
errorCallback,
|
||||
data: comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 취소
|
||||
const handleCommentCancel = async () => {
|
||||
let arr: IComment[] = initComments()
|
||||
|
||||
setComments(arr)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box boxShadow={1} className={classes.commentRoot}>
|
||||
<Box className={classes.commentTitle}>
|
||||
<Typography variant="h4" component="h3" className={classes.pdtb1}>
|
||||
{t('comment')}
|
||||
</Typography>
|
||||
<Link
|
||||
href="#"
|
||||
className={classNames({
|
||||
[classes.black]: true,
|
||||
[classes.ml1]: true,
|
||||
[classes.pdtb1]: true,
|
||||
})}
|
||||
onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault()
|
||||
handleRefresh()
|
||||
}}
|
||||
>
|
||||
<RefreshIcon fontSize="small" className={classes.commentIcon} />
|
||||
</Link>
|
||||
</Box>
|
||||
{comments &&
|
||||
comments.map(comment => {
|
||||
if (comment.mode !== 'edit' && comment.mode !== 'reply') {
|
||||
let buttons = []
|
||||
|
||||
if (commentUseAt && deleteAt === 0) {
|
||||
buttons.push({
|
||||
label: t('label.button.reply'),
|
||||
size: 'small',
|
||||
handleButton: () => {
|
||||
handleCommentReply(comment.commentNo)
|
||||
},
|
||||
})
|
||||
|
||||
if (user?.userId === comment.createdBy) {
|
||||
buttons.push({
|
||||
label: t('label.button.edit'),
|
||||
size: 'small',
|
||||
handleButton: () => {
|
||||
handleCommentEdit(comment.commentNo)
|
||||
},
|
||||
})
|
||||
buttons.push({
|
||||
label: t('label.button.delete'),
|
||||
size: 'small',
|
||||
handleButton: () => {
|
||||
handleCommentDelete(comment.commentNo)
|
||||
},
|
||||
completeMessage: t('msg.success.delete'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`comment${comment.commentNo}`}
|
||||
className={classNames({
|
||||
[classes.commentBox]: true,
|
||||
})}
|
||||
style={{
|
||||
paddingLeft: `${
|
||||
theme.spacing(2) + comment.depthSeq * theme.spacing(4)
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<CardContent className={classes.commentView}>
|
||||
<Typography gutterBottom variant="h6" component="h4">
|
||||
{comment.createdName}
|
||||
</Typography>
|
||||
{comment.deleteAt !== 0 && (
|
||||
<>
|
||||
<ErrorOutlineIcon
|
||||
fontSize="small"
|
||||
className={classes.commentIcon}
|
||||
/>
|
||||
{comment.deleteAt === 1 && t('common.delete.creator')}
|
||||
{comment.deleteAt === 2 && t('common.delete.manager')}
|
||||
</>
|
||||
)}
|
||||
<Typography variant="body2" color="textPrimary" component="p">
|
||||
<Box
|
||||
className={classNames({
|
||||
[classes.commentContent]: true,
|
||||
[classes.cancel]: comment.deleteAt !== 0,
|
||||
})}
|
||||
component="span"
|
||||
>
|
||||
{comment.commentContent}
|
||||
</Box>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions className={classes.commentView}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
component="p"
|
||||
className={classes.commentDate}
|
||||
>
|
||||
{comment.createdDate
|
||||
? convertStringToDateFormat(
|
||||
comment.createdDate,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
)
|
||||
: ''}
|
||||
</Typography>
|
||||
{comment.deleteAt === 0 && (
|
||||
<CustomButtons buttons={buttons} className="mg0" />
|
||||
)}
|
||||
</CardActions>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`comment${comment.commentNo}`}
|
||||
boxShadow={1}
|
||||
className={classes.pd1}
|
||||
>
|
||||
<CommentForm
|
||||
boardNo={boardNo}
|
||||
postsNo={postsNo}
|
||||
commentNo={comment.commentNo}
|
||||
commentContent={comment.commentContent}
|
||||
groupNo={comment.groupNo}
|
||||
parentCommentNo={comment.parentCommentNo}
|
||||
depthSeq={comment.depthSeq}
|
||||
handleCommentSave={handleCommentSave}
|
||||
handleCommentCancel={handleCommentCancel}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<Box className={classes.moreBox} hidden={page + 1 >= totalPages}>
|
||||
<Button
|
||||
startIcon={<ExpandMoreIcon />}
|
||||
endIcon={<ExpandMoreIcon />}
|
||||
onClick={handleCommentMore}
|
||||
>
|
||||
{t('common.more')}
|
||||
</Button>
|
||||
</Box>
|
||||
{commentUseAt && deleteAt === 0 && (
|
||||
<Box className={classes.pdtb1}>
|
||||
<CommentForm
|
||||
handleCommentSave={handleCommentSave}
|
||||
boardNo={boardNo}
|
||||
postsNo={postsNo}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
open={openConfirm}
|
||||
contentText={confirm.contentText}
|
||||
handleClose={confirm.handleClose}
|
||||
handleConfirm={confirm.handleConfirm}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export { Comment }
|
||||
Reference in New Issue
Block a user