diff --git a/components/admin/feedback/FeedbackTable.tsx b/components/admin/feedback/FeedbackTable.tsx new file mode 100644 index 000000000..6e0f2c82a --- /dev/null +++ b/components/admin/feedback/FeedbackTable.tsx @@ -0,0 +1,163 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import instance from 'utils/axios'; +import { modalState } from 'utils/recoil/modal'; +import PageNation from 'components/Pagination'; +import AdminSearchBar from 'components/admin/common/AdminSearchBar'; +import { tableFormat } from 'constants/admin/table'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import style from 'styles/admin/feedback/FeedbackTable.module.scss'; + +export interface IFeedback { + id: number; + intraId: string; + category: number; // 1: bug, 2: suggestion, 3: question + content: string; + createdTime: Date; + isSolved: boolean; +} + +interface IFeedbackTable { + feedbackList: IFeedback[]; + totalPage: number; + currentPage: number; +} + +const MAX_CONTENT_LENGTH = 20; + +export default function FeedbackTable() { + const [feedbackInfo, setFeedbackInfo] = useState({ + feedbackList: [], + totalPage: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + const [intraId, setIntraId] = useState(''); + const [modal, setModal] = useRecoilState(modalState); + + const getUserFeedbacks = useCallback(async () => { + try { + // TODO : api 수정 필요 + const res = await instance.get( + `/pingpong/admin/feedback/users/${intraId}?page=${currentPage}&size=10` + ); + setIntraId(intraId); + setFeedbackInfo({ ...res.data }); + } catch (e) { + console.error('MS04'); + } + }, [intraId, currentPage]); + + const getAllFeedbacks = useCallback(async () => { + try { + const res = await instance.get( + `/pingpong/admin/feedback?page=${currentPage}&size=10` + ); + setFeedbackInfo({ ...res.data }); + } catch (e) { + console.error('MS03'); + } + }, [currentPage]); + + const initSearch = useCallback((intraId?: string) => { + setIntraId(intraId || ''); + setCurrentPage(1); + }, []); + + const solvingFeedback = (feedback: IFeedback) => { + setModal({ + modalName: 'ADMIN-CHECK_FEEDBACK', + feedback, + }); + }; + + const openDetailModal = (feedback: IFeedback) => { + setModal({ + modalName: 'ADMIN-DETAIL_CONTENT', + intraId: feedback.intraId, + detailContent: feedback.content, + }); + }; + + useEffect(() => { + intraId ? getUserFeedbacks() : getAllFeedbacks(); + }, [intraId, getUserFeedbacks, getAllFeedbacks, modal]); + + return ( + <> +
+
+ 피드백 관리 + +
+ + + + + {tableFormat['feedback'].columns.map((columnName) => ( + + {columnName} + + ))} + + + + {feedbackInfo.feedbackList.map((feedback: IFeedback) => ( + + {tableFormat['feedback'].columns.map((columnName: string) => { + const value = feedback[columnName as keyof IFeedback]; + return ( + + {typeof value === 'boolean' ? ( + + ) : value.toString().length > MAX_CONTENT_LENGTH ? ( +
+ {value.toString().slice(0, MAX_CONTENT_LENGTH)} + openDetailModal(feedback)} + > + ...더보기 + +
+ ) : ( + value.toString() + )} +
+ ); + })} +
+ ))} +
+
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ + ); +} diff --git a/components/admin/notification/NotificationTable.tsx b/components/admin/notification/NotificationTable.tsx index 53e48889f..15332b422 100644 --- a/components/admin/notification/NotificationTable.tsx +++ b/components/admin/notification/NotificationTable.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { tableColumnNames } from 'types/admin/tableTypes'; +import { tableFormat } from 'constants/admin/table'; import { Paper, Table, @@ -14,6 +14,7 @@ import PageNation from 'components/Pagination'; import AdminSearchBar from 'components/admin/common/AdminSearchBar'; import CreateNotiButton from 'components/admin/notification/CreateNotiButton'; import style from 'styles/admin/notification/NotificationTable.module.scss'; +import instance from 'utils/axios'; interface INotification { notiId: number; @@ -41,9 +42,8 @@ export default function NotificationTable() { const getUserNotifications = useCallback(async () => { try { - // TODO! : change to real endpoint - const res = await axios.get( - `${process.env.NEXT_PUBLIC_ADMIN_MOCK_ENDPOINT}/notifications/${intraId}?page=${currentPage}` + const res = await instance.get( + `pingpong/admin/notifications?q=${intraId}&page=${currentPage}&size=10` ); setIntraId(intraId); setNotificationInfo({ ...res.data }); @@ -59,9 +59,8 @@ export default function NotificationTable() { const getAllUserNotifications = useCallback(async () => { try { - // TODO! : change to real endpoint - const res = await axios.get( - `${process.env.NEXT_PUBLIC_ADMIN_MOCK_ENDPOINT}/notifications?page=${currentPage}` + const res = await instance.get( + `pingpong/admin/notifications?page=${currentPage}&size=10` ); setIntraId(''); setNotificationInfo({ ...res.data }); @@ -74,8 +73,8 @@ export default function NotificationTable() { intraId ? getUserNotifications() : getAllUserNotifications(); }, [intraId, getUserNotifications, getAllUserNotifications]); - if (notificationInfo === undefined) { - return
loading...
; + if (notificationInfo.notiList.length === 0) { + return
비어있습니다!
; } return ( @@ -90,7 +89,7 @@ export default function NotificationTable() { - {tableColumnNames['notification'].map((columnName) => ( + {tableFormat['notification'].columns.map((columnName) => ( {columnName} @@ -100,15 +99,13 @@ export default function NotificationTable() { {notificationInfo.notiList.map((notification: INotification) => ( - {Object.values(notification).map( - (value: number | string | boolean, index: number) => { + {tableFormat['notification'].columns.map( + (columnName: string, index: number) => { return ( - {typeof value === 'boolean' - ? value - ? 'Checked' - : 'Unchecked' - : value} + {notification[ + columnName as keyof INotification + ]?.toString()} ); } diff --git a/components/modal/ModalProvider.tsx b/components/modal/ModalProvider.tsx index a38cbed0d..7ad566d25 100644 --- a/components/modal/ModalProvider.tsx +++ b/components/modal/ModalProvider.tsx @@ -17,11 +17,23 @@ import AdminPenaltyModal from './admin/AdminPenaltyModal'; import AdminNotiAllModal from './admin/AdminNotiAllModal'; import AdminNotiUserModal from './admin/AdminNotiUserModal'; import AdminCheckFeedback from './admin/AdminFeedbackCheckModal'; +import FeedbackDetailModal from './admin/FeedbackDetailModal'; import styles from 'styles/modal/Modal.module.scss'; export default function ModalProvider() { const [ - { modalName, cancel, enroll, manual, announcement, exp, intraId, userId }, + { + modalName, + cancel, + enroll, + manual, + announcement, + exp, + intraId, + detailContent, + feedback, + userId, + }, setModal, ] = useRecoilState(modalState); const setReloadMatch = useSetRecoilState(reloadMatchState); @@ -42,7 +54,13 @@ export default function ModalProvider() { 'ADMIN-PENALTY': intraId ? : null, 'ADMIN-NOTI_ALL': intraId ? : null, 'ADMIN-NOTI_USER': intraId ? : null, - 'ADMIN-CHECK_FEEDBACK': intraId ? : null, + 'ADMIN-CHECK_FEEDBACK': feedback ? ( + + ) : null, + 'ADMIN-DETAIL_CONTENT': + intraId && detailContent ? ( + + ) : null, }; useEffect(() => { diff --git a/components/modal/admin/AdminFeedbackCheckModal.tsx b/components/modal/admin/AdminFeedbackCheckModal.tsx index c41666b25..0efbc43c3 100644 --- a/components/modal/admin/AdminFeedbackCheckModal.tsx +++ b/components/modal/admin/AdminFeedbackCheckModal.tsx @@ -1,59 +1,54 @@ import { useSetRecoilState } from 'recoil'; -import { useEffect, useState } from 'react'; -import styles from 'styles/admin/modal/AdminFeedbackCheck.module.scss'; import { modalState } from 'utils/recoil/modal'; +import { IFeedback } from 'components/admin/feedback/FeedbackTable'; import instance from 'utils/axios'; -// import { finished } from 'stream'; -import { useRouter } from 'next/router'; -import GameResultSmallItem from 'components/game/small/GameResultSmallItem'; - -interface Result { - status: string; -} +import { IoSend } from 'react-icons/io5'; +import styles from 'styles/admin/modal/AdminFeedbackCheck.module.scss'; -export default function AdminFeedbackCheck(props: any) { - const [result, setResult] = useState({ - status: '', - }); +export default function AdminFeedbackCheck({ + id, + intraId, + isSolved, +}: IFeedback) { const setModal = useSetRecoilState(modalState); - const sendReminder: { [key: string]: string } = { - YES: '성공적으로 전송되었습니다', - NO: '전송이 취소되었습니다', - }; - - const finishSendHandler = async () => { - if (result.status == 'completed') { - try { - await instance.post(`/admin/feedback/is-solved`); - setModal({ modalName: null }); - } catch (e) { - console.log(e); - } - } - }; - - const cancelReminderHandler = () => { - if (result.status == 'completed') { + const sendNotificationHandler = async (isSend: boolean) => { + try { + await instance.put(`pingpong/admin/feedback/is-solved`, { + feedbackId: id, + }); + await instance.post(`pingpong/admin/notifications/${intraId}`, { + intraId, + message: isSolved + ? '피드백을 검토중입니다.' + : '피드백이 반영되었습니다.', + sendMail: !isSend, + }); setModal({ modalName: null }); + } catch (e) { + console.error(e); } }; + return (
-
FEEDBACK REMINDER
-
- Are you sure you want to send the reminder? +
+
+
알림을 보내겠습니까?
-
diff --git a/components/modal/admin/FeedbackDetailModal.tsx b/components/modal/admin/FeedbackDetailModal.tsx new file mode 100644 index 000000000..ea590d373 --- /dev/null +++ b/components/modal/admin/FeedbackDetailModal.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; + +export default function FeedbackDetailModal({ + intraId, + detailContent, +}: { + intraId: string; + detailContent: string; +}) { + const [contentWithNewLine, setContent] = useState(''); + const MAX_CONTENT_LENGTH = 30; + + useEffect(() => { + const contentWithNewLine = detailContent + .split('') + .reduce( + (result: string, char: string, index: number) => + index % MAX_CONTENT_LENGTH === 0 && index > 0 + ? result + '\r\n' + char + : result + char, + '' + ); + setContent(contentWithNewLine); + }, [detailContent]); + return ( + <> +

{intraId}

+ {contentWithNewLine.split('\r\n').map((value: string, index: number) => { + return ( + + {value} + + ); + })} + + ); +} diff --git a/constants/admin/table.ts b/constants/admin/table.ts new file mode 100644 index 000000000..73df4f461 --- /dev/null +++ b/constants/admin/table.ts @@ -0,0 +1,39 @@ +import { TableFormat } from 'types/admin/tableTypes'; + +export const tableFormat: TableFormat = { + notification: { + name: '알림 관리', + columns: [ + 'notiId', + 'intraId', + 'slotId', + 'type', + 'message', + 'createdTime', + 'isChecked', + ], + }, + userInfo: { + name: '사용자 정보', + columns: ['Index', 'role', 'intraId', 'message', 'etc'], + etc: { + type: 'button', + value: ['자세히', '패널티 부여'], + }, + }, + feedback: { + name: '피드백 관리', + columns: [ + 'id', + 'intraId', + 'category', + 'content', + 'createdTime', + 'isSolved', + ], + etc: { + type: 'toggle', + value: ['completed', 'notCompleted'], + }, + }, +}; diff --git a/pages/admin/feedback.tsx b/pages/admin/feedback.tsx index ca8bd8cc9..fe7c091a7 100644 --- a/pages/admin/feedback.tsx +++ b/pages/admin/feedback.tsx @@ -1,19 +1,9 @@ -import { useSetRecoilState } from 'recoil'; -import { modalState } from 'utils/recoil/modal'; +import FeedbackTable from 'components/admin/feedback/FeedbackTable'; export default function Feedback() { - const setModal = useSetRecoilState(modalState); - return (
- feedback page{' '} - +
); } diff --git a/pages/api/pingpong/admin/feedback/index.ts b/pages/api/pingpong/admin/feedback/index.ts new file mode 100644 index 000000000..f7c44ec56 --- /dev/null +++ b/pages/api/pingpong/admin/feedback/index.ts @@ -0,0 +1,75 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +interface IFeedback { + id: number; + intraId: string; + category: number; // 1: bug, 2: suggestion, 3: question + content: string; + createdTime: Date; + isSolved: boolean; +} + +interface IPagedFeedback { + feedbackList: IFeedback[]; + totalPage: number; + currentPage: number; +} + +const PER_PAGE = 10; +const TOTAL_NOTI = 46; + +export const makeFeedbacks = ( + page: string, + intraId?: string +): IPagedFeedback => { + let totalPage = (TOTAL_NOTI / PER_PAGE) as number; + let filteredFeedbacks: IFeedback[] = []; + const feedbacks: IFeedback[] = []; + + for (let i = 0; i < TOTAL_NOTI; i++) { + feedbacks.push({ + id: i, + intraId: `${ + Math.floor(Math.random() * 10) % 2 ? 'mosong' : `mosong${i}` + }`, + category: (Math.floor(Math.random() * 10) % 3) + 1, + content: `Hello, World! ${Math.floor(Math.random() * 10)}`, + createdTime: new Date(), + isSolved: Math.floor(Math.random() * 10) % 3 ? true : false, + }); + } + + if (intraId) { + filteredFeedbacks = feedbacks.filter( + (feedback) => feedback.intraId === intraId + ); + totalPage = (filteredFeedbacks.length / PER_PAGE) as number; + } + + return { + feedbackList: intraId + ? filteredFeedbacks.slice( + (parseInt(page) - 1) * PER_PAGE, + parseInt(page) * PER_PAGE + ) + : feedbacks.slice( + (parseInt(page) - 1) * PER_PAGE, + parseInt(page) * PER_PAGE + ), + totalPage, + currentPage: parseInt(page), + }; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { method, query } = req; + const { page } = query as { page: string }; + if (method === 'GET') { + const feedbacks: IPagedFeedback = makeFeedbacks(page); + console.log(feedbacks); + return res.status(200).json(feedbacks); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${method} Not Allowed`); + } +} diff --git a/pages/api/pingpong/admin/feedback/is-solved.ts b/pages/api/pingpong/admin/feedback/is-solved.ts new file mode 100644 index 000000000..381f21a9e --- /dev/null +++ b/pages/api/pingpong/admin/feedback/is-solved.ts @@ -0,0 +1,21 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { method, body } = req; + const { feedbackId } = body as { feedbackId: number }; + + switch (method) { + case 'PUT': + if (feedbackId === undefined) { + res.status(400).end('Bad Request'); + return; + } + res.status(200).json({ + isSolved: true, + }); + break; + default: + res.setHeader('Allow', ['PUT']); + res.status(405).end(`Method ${method} Not Allowed`); + } +} diff --git a/styles/admin/feedback/FeedbackTable.module.scss b/styles/admin/feedback/FeedbackTable.module.scss new file mode 100644 index 000000000..b478f642a --- /dev/null +++ b/styles/admin/feedback/FeedbackTable.module.scss @@ -0,0 +1,45 @@ +@import 'styles/admin/common/Pagination.module.scss'; + +.feedbackWrap { + width: '100%'; + height: '100%'; + + .header { + width: 700px; + margin: 10px auto; + display: flex; + justify-content: space-between; + align-items: center; + } + .title { + font-weight: 600; + font-size: 18px; + line-height: 150%; + } + .tableContainer { + width: 700px; + margin: auto; + .table { + min-width: 700px; + .tableHeader { + background-color: lightgray; + .tableHeaderItem { + padding: 10px; + text-align: center; + font-weight: 600; + font-size: 14px; + line-height: 150%; + } + } + .tableBody { + .tableBodyItem { + padding: 10px; + text-align: center; + line-height: 150%; + } + } + } + } +} + +@include pagination; diff --git a/styles/admin/modal/AdminFeedbackCheck.module.scss b/styles/admin/modal/AdminFeedbackCheck.module.scss index 2a97501ba..b6d0b636a 100644 --- a/styles/admin/modal/AdminFeedbackCheck.module.scss +++ b/styles/admin/modal/AdminFeedbackCheck.module.scss @@ -1,76 +1,47 @@ .whole { text-align: center; - display: flex; justify-content: center; - display: inline-block; align-items: center; - display: inline-block; - background-color: aliceblue; -} - -.title { - color: cornflowerblue; - font-size: 2.5rem; - font-style: normal; - font-weight: bold; - margin: auto; - margin-bottom: 20px; - margin: auto; + background-color: white; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.25); + border-radius: 10px; + width: 20rem; + height: 15rem; } - .body { - display: flex; - font-size: 1.5rem; - margin: auto; padding: 1rem 1rem; flex-direction: column; - justify-content: center; - text-align: center; -} - -.text { - display: flex; - font-size: 1.5rem; - font-weight: bold; - justify-content: center; text-align: center; - margin-top: 30px; - margin-bottom: 30px; - margin-left: 30px; - margin-right: 30px; - color: cadetblue; + .content { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-size: 1rem; + line-height: 150%; + color: #6b7280; + padding: 1rem 2rem; + } } .btns { - color: aliceblue; text-align: center; flex-direction: column; display: flex; - justify-content: center; - // justify-items: center; -} - -.btn { - background-color: aliceblue; - display: flex; - text-align: center; - justify-content: center; - margin-bottom: 10px; - width: 400px; - height: 60px; - margin-left: auto; - margin-right: auto; - font-weight: bold; -} - -.btntext { - color: cadetblue; - margin: auto; -} - -.hide { - display: none; - visibility: hidden; - margin-left: auto; - margin-right: auto; + color: white; + .btn { + width: 100%; + margin: 0.5rem 0; + padding: 0.5rem 0; + border-radius: 10px; + border: none; + font-size: 1rem; + cursor: pointer; + color: white; + &.first { + background-color: #2678f3; + } + &.second { + background-color: #6c757d; + } + } } diff --git a/types/admin/tableTypes.ts b/types/admin/tableTypes.ts index 578e9e499..b22e4da2c 100644 --- a/types/admin/tableTypes.ts +++ b/types/admin/tableTypes.ts @@ -1,12 +1,13 @@ -export const tableColumnNames = { - notification: [ - 'notiId', - 'intraId', - 'slotId', - 'type', - 'message', - 'createdTime', - 'isChecked', - ], - // TODO: 각 페이지별 column name을 contants object로 분류, 각 페이지에서 indexing해서 사용 +export type TableName = 'notification' | 'userInfo' | 'feedback'; +export type EtcType = 'button' | 'toggle'; + +export type TableFormat = { + [key in TableName]: { + name: string; + columns: string[]; + etc?: { + type: EtcType; + value: string[]; + }; + }; }; diff --git a/types/modalTypes.ts b/types/modalTypes.ts index d34fdffaa..1265df001 100644 --- a/types/modalTypes.ts +++ b/types/modalTypes.ts @@ -1,5 +1,6 @@ import { MatchMode } from './mainType'; import { Value } from 'react-quill'; +import { IFeedback } from 'components/admin/feedback/FeedbackTable'; type EventModal = 'WELCOME' | 'ANNOUNCEMENT'; @@ -16,7 +17,8 @@ type AdminModal = | 'PENALTY' | 'NOTI_ALL' | 'NOTI_USER' - | 'CHECK_FEEDBACK'; + | 'CHECK_FEEDBACK' + | 'DETAIL_CONTENT'; type ModalName = | null @@ -63,5 +65,7 @@ export interface Modal { exp?: Exp; gameId?: number; intraId?: string; + detailContent?: string; + feedback?: IFeedback; userId?: number; }