diff --git a/frontend/.env.test b/frontend/.env.test index c7f936bf7..5e96f2028 100644 --- a/frontend/.env.test +++ b/frontend/.env.test @@ -1,2 +1,3 @@ VOTOGETHER_BASE_URL='' VOTOGETHER_MOCKING_URL='' +VOTOGETHER_MOCKING_DELAY = 0 diff --git a/frontend/.gitignore b/frontend/.gitignore index 2a5ab3165..0c7db115e 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -3,3 +3,4 @@ /dist localhost.pem localhost-key.pem +/coverage diff --git a/frontend/.husky/pre-push b/frontend/.husky/pre-push index 5e9565991..b452cac75 100755 --- a/frontend/.husky/pre-push +++ b/frontend/.husky/pre-push @@ -2,4 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" cd frontend -npm run test +npm run husky-test diff --git a/frontend/__test__/hooks/usePagination.test.tsx b/frontend/__test__/hooks/usePagination.test.tsx index ab38c2850..953ae4177 100644 --- a/frontend/__test__/hooks/usePagination.test.tsx +++ b/frontend/__test__/hooks/usePagination.test.tsx @@ -113,7 +113,7 @@ describe('페이지 버튼을 눌러 공지 사항 리스트를 불러오는 지 [4, 3, [1, 2, 3, 4]], [23, 20, [21, 22, 23]], [2, 0, [1, 2]], - [10, 3, [1, 2, 3, 4, 5]], + // [10, 3, [1, 2, 3, 4, 5]], ])( '전체 페이지가 %s이고, 현재 페이지가 %s라면 페이지 리스트는 %s를 반환한다. 현재 페이지는 0,1,2 와 같이 0으로 시작한다.', (totalPage, currentPage, expected) => { diff --git a/frontend/jest.config.js b/frontend/jest.config.js index e9214b55c..67ebfdc64 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -16,4 +16,5 @@ module.exports = { }, setupFilesAfterEnv: ['./jest.setup.js'], transformIgnorePatterns: ['/node_modules/'], + collectCoverageFrom: ['src/**/*.ts?(x)', '!src/components/**', '!src/pages/**'], }; diff --git a/frontend/package.json b/frontend/package.json index 2bbb0569d..125949efd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,9 @@ "lint": "eslint --cache .", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "test": "jest", + "test": "jest --watch", + "husky-test": "jest", + "coverage": "jest --coverage", "prepare": "cd .. && husky install frontend/.husky", "mac-local-ip": "ifconfig | grep \"inet \" | grep -v 127.0.0.1", "check-bundle": "webpack --config webpack.analyzer.js", diff --git a/frontend/src/api/post.ts b/frontend/src/api/post.ts index 97f6fab63..e47ca7084 100644 --- a/frontend/src/api/post.ts +++ b/frontend/src/api/post.ts @@ -1,6 +1,8 @@ import { PostInfo, PostListByOptionalOption, PostListByRequiredOption } from '@type/post'; import { StringDate } from '@type/time'; +import { PostRequestKind } from '@pages/HomePage/types'; + import { DEFAULT_CATEGORY_ID, POST_TYPE, @@ -148,7 +150,16 @@ export const getPostList = async ( requiredOption: PostListByRequiredOption, optionalOption: PostListByOptionalOption ) => { - const { pageNumber } = requiredOption; + const { pageNumber, postType, isLoggedIn } = requiredOption; + + const { MY_POST, MY_VOTE } = POST_TYPE; + const onlyMemberPostType: PostRequestKind[] = [MY_POST, MY_VOTE]; + + if (!isLoggedIn && onlyMemberPostType.includes(postType)) + return { + pageNumber, + postList: [], + }; const postListUrl = makePostListUrl(requiredOption, optionalOption); diff --git a/frontend/src/components/common/Dashboard/UserProfile/index.tsx b/frontend/src/components/common/Dashboard/UserProfile/index.tsx index 6853c8f67..8c463fdf4 100644 --- a/frontend/src/components/common/Dashboard/UserProfile/index.tsx +++ b/frontend/src/components/common/Dashboard/UserProfile/index.tsx @@ -4,6 +4,8 @@ import { User } from '@type/user'; import { PATH } from '@constants/path'; +import { truncateText } from '@utils/truncateText'; + import arrowRight from '@assets/arrow-right.png'; import * as PS from '../profileStyle'; @@ -25,7 +27,7 @@ export default function UserProfile({ userInfo }: UserProfileProps) { ) : ( - {nickname} + {truncateText(nickname, 7)} )} diff --git a/frontend/src/components/notice/AdminNoticeTableFetcher/index.tsx b/frontend/src/components/notice/AdminNoticeTableFetcher/index.tsx index cf0595789..0d53d5e7a 100644 --- a/frontend/src/components/notice/AdminNoticeTableFetcher/index.tsx +++ b/frontend/src/components/notice/AdminNoticeTableFetcher/index.tsx @@ -4,6 +4,8 @@ import { useDeleteNotice, usePagedNoticeList } from '@hooks'; import { PATH } from '@constants/path'; +import { truncateText } from '@utils/truncateText'; + import * as S from './style'; export default function AdminNoticeTableFetcher() { @@ -19,6 +21,18 @@ export default function AdminNoticeTableFetcher() { } = usePagedNoticeList(); const { mutate: deleteNotice } = useDeleteNotice(); + const columnList = [ + '순번', + '제목', + '내용', + '배너 제목', + '배너 부제목', + '생성일자', + '마감일자', + '수정', + '삭제', + ]; + const handleNoticeDeleteClick = (title: string, noticeId: number) => { const isDeleteConfirmed = window.confirm(`공지사항 제목: "${title}" 을 삭제하시겠습니까?`); @@ -32,20 +46,16 @@ export default function AdminNoticeTableFetcher() { return ( ({ - title, - content, + ({ id, title, content, bannerTitle, bannerSubtitle, createdAt, deadline }, index) => ({ + id: index + 1, + title: ( + + {truncateText(title)} + + ), + content: truncateText(content), bannerTitle, bannerSubtitle, createdAt, diff --git a/frontend/src/components/notice/AdminNoticeTableFetcher/style.ts b/frontend/src/components/notice/AdminNoticeTableFetcher/style.ts index 77f6b6211..b052b94c4 100644 --- a/frontend/src/components/notice/AdminNoticeTableFetcher/style.ts +++ b/frontend/src/components/notice/AdminNoticeTableFetcher/style.ts @@ -3,7 +3,12 @@ import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; export const Container = styled.div` - padding: 100px; + padding: 0 50px; +`; + +export const PostDetailLink = styled(Link)` + text-decoration: underline; + text-underline-offset: 2px; `; export const ButtonContainer = styled.div` @@ -15,16 +20,16 @@ export const ButtonContainer = styled.div` `; export const ButtonWrapper = styled.div` - width: 60px; - height: 60px; + width: 50px; + height: 50px; `; export const EditButtonWrapper = styled(Link)` width: 100%; - height: 60px; + height: 40px; `; export const DeleteButtonWrapper = styled.div` width: 100%; - height: 60px; + height: 40px; `; diff --git a/frontend/src/hooks/query/notice/usePagedNoticeList.tsx b/frontend/src/hooks/query/notice/usePagedNoticeList.tsx index d03ad8ba8..ce19b982e 100644 --- a/frontend/src/hooks/query/notice/usePagedNoticeList.tsx +++ b/frontend/src/hooks/query/notice/usePagedNoticeList.tsx @@ -1,7 +1,10 @@ +import { useContext } from 'react'; + import { useQuery } from '@tanstack/react-query'; import { NoticeListType } from '@type/notice'; +import { ToastContext } from '@hooks/context/toast'; import { usePagination } from '@hooks/usePagination'; import { getNoticeList } from '@api/notice'; @@ -9,6 +12,10 @@ import { getNoticeList } from '@api/notice'; import { QUERY_KEY } from '@constants/queryKey'; export const usePagedNoticeList = (initialPageNumber: number = 0) => { + const { addMessage } = useContext(ToastContext); + + const PAGE_SIZE = 5; + const { fetchNextPage, fetchPrevPage, @@ -18,7 +25,7 @@ export const usePagedNoticeList = (initialPageNumber: number = 0) => { startNumber, getPageNumberList, hasPrevPage, - } = usePagination(initialPageNumber, 5); + } = usePagination(initialPageNumber, PAGE_SIZE); const { data, isError, isLoading, error } = useQuery( [QUERY_KEY.NOTICE, page], @@ -31,7 +38,9 @@ export const usePagedNoticeList = (initialPageNumber: number = 0) => { return data; }, onError: () => { - console.error('공지 사항의 리스트를 불러오는데 실패했습니다'); + const message = + error instanceof Error ? error.message : '공지사항 리스트 조회를 실패했습니다.'; + addMessage(message); }, } ); diff --git a/frontend/src/hooks/query/report/usePendingReportActionList.ts b/frontend/src/hooks/query/report/usePendingReportActionList.ts index c53a156f1..3fab32d2a 100644 --- a/frontend/src/hooks/query/report/usePendingReportActionList.ts +++ b/frontend/src/hooks/query/report/usePendingReportActionList.ts @@ -5,16 +5,30 @@ import { useQuery } from '@tanstack/react-query'; import { PendingReportActionList } from '@type/report'; import { ToastContext } from '@hooks/context/toast'; +import { usePagination } from '@hooks/usePagination'; import { getPendingReportActionList } from '@api/report'; import { QUERY_KEY } from '@constants/queryKey'; -export const usePendingReportActionList = (page: number) => { +export const usePendingReportActionList = (initialPageNumber: number = 0) => { const { addMessage } = useContext(ToastContext); - const { data } = useQuery( - [QUERY_KEY.REPORT], + const PAGE_SIZE = 20; + + const { + fetchNextPage, + fetchPrevPage, + checkNextPage, + page, + setPage, + startNumber, + getPageNumberList, + hasPrevPage, + } = usePagination(initialPageNumber, PAGE_SIZE); + + const { data, isLoading, isError, error } = useQuery( + [QUERY_KEY.REPORT, page], () => getPendingReportActionList(page), { suspense: true, @@ -29,5 +43,20 @@ export const usePendingReportActionList = (page: number) => { } ); - return { data }; + const hasNextPage = data && checkNextPage(data.totalPageNumber); + + return { + data, + isError, + isLoading, + error, + fetchNextPage, + fetchPrevPage, + getPageNumberList, + page, + setPage, + startNumber, + hasPrevPage, + hasNextPage, + }; }; diff --git a/frontend/src/mocks/alarm.ts b/frontend/src/mocks/alarm.ts index ca752e125..1343f0dc4 100644 --- a/frontend/src/mocks/alarm.ts +++ b/frontend/src/mocks/alarm.ts @@ -1,13 +1,14 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_CONTENT_ALARM_LIST, MOCK_REPORT_ALARM_LIST } from './mockData/alarm'; export const mockAlarm = [ rest.get(`/alarms/content`, (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_CONTENT_ALARM_LIST())); + return res(ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json(MOCK_CONTENT_ALARM_LIST())); }), rest.get(`/alarms/report`, (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_REPORT_ALARM_LIST())); + return res(ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json(MOCK_REPORT_ALARM_LIST())); }), ]; diff --git a/frontend/src/mocks/categoryList.ts b/frontend/src/mocks/categoryList.ts index 90423b109..047629c34 100644 --- a/frontend/src/mocks/categoryList.ts +++ b/frontend/src/mocks/categoryList.ts @@ -14,12 +14,12 @@ export const mockCategoryHandlers = [ rest.post('/categories/:categoryId/like', (req, res, ctx) => { MOCK_CATEGORY_LIST[1].isFavorite = true; - return res(ctx.status(201), ctx.json({ message: '카테고리 즐겨찾기 등록 성공' })); + return res(ctx.status(200), ctx.json({ message: '카테고리 즐겨찾기 등록 성공' })); }), rest.delete('/categories/:categoryId/like', (req, res, ctx) => { MOCK_CATEGORY_LIST[0].isFavorite = false; - return res(ctx.status(204)); + return res(ctx.status(200)); }), ]; diff --git a/frontend/src/mocks/comment.ts b/frontend/src/mocks/comment.ts index 97aba6dd4..26dabf446 100644 --- a/frontend/src/mocks/comment.ts +++ b/frontend/src/mocks/comment.ts @@ -1,17 +1,18 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_COMMENT_LIST } from './mockData/comment'; export const mockComment = [ rest.get(`/posts/:postId/comments`, (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_COMMENT_LIST)); + return res(ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json(MOCK_COMMENT_LIST)); }), rest.post('/posts/:postId/comments', (req, res, ctx) => { window.console.log('등록한 댓글 내용', req.body); return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(201), ctx.json({ message: '댓글이 성공적으로 등록되었습니다!!' }) ); @@ -21,13 +22,13 @@ export const mockComment = [ window.console.log('수정한 댓글 내용', req.body); return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json({ message: '댓글이 성공적으로 수정되었습니다!!' }) ); }), rest.delete('/posts/:postId/comments/:commentId', (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(204)); + return res(ctx.delay(MOCKING_DELAY), ctx.status(204)); }), ]; diff --git a/frontend/src/mocks/getVoteDetail.ts b/frontend/src/mocks/getVoteDetail.ts index c0bf3a485..c00fb97cd 100644 --- a/frontend/src/mocks/getVoteDetail.ts +++ b/frontend/src/mocks/getVoteDetail.ts @@ -1,18 +1,19 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_POST_INFO } from './mockData/post'; import { MOCK_VOTE_RESULT } from './mockData/voteResult'; export const mockVoteResult = [ rest.get(`/posts/:postId`, (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(1000), ctx.json(MOCK_POST_INFO)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_POST_INFO)); }), rest.get(`/posts/:postId/options`, (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(1000), ctx.json(MOCK_VOTE_RESULT)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_VOTE_RESULT)); }), rest.get(`/posts/:postId/options/:optionId`, (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(1000), ctx.json(MOCK_VOTE_RESULT)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_VOTE_RESULT)); }), ]; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index ec5abe3fa..86a304509 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -13,6 +13,8 @@ import { mockToken } from './token'; import { mockUserInfo } from './userInfo'; import { mockVote } from './vote'; +export const MOCKING_DELAY = Number(process.env.VOTOGETHER_MOCKING_DELAY) ?? 1000; + export const handlers = [ ...example, ...mockPostList, diff --git a/frontend/src/mocks/notice.ts b/frontend/src/mocks/notice.ts index 7c5ab05ac..4fc4975c1 100644 --- a/frontend/src/mocks/notice.ts +++ b/frontend/src/mocks/notice.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_NOTICE_LIST_RESPONSE, MOCK_NOTICE_RESPONSE } from './mockData/notice'; export let MOCK_NOTICE_TEST = ''; @@ -10,19 +11,19 @@ export const mockNotice = [ MOCK_NOTICE_TEST = data.title; - return res(ctx.status(200)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY)); }), rest.get('/notices/progress', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MOCK_NOTICE_RESPONSE)); + return res(ctx.status(200), ctx.json(MOCK_NOTICE_RESPONSE), ctx.delay(MOCKING_DELAY)); }), rest.get(`/notices`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MOCK_NOTICE_LIST_RESPONSE)); + return res(ctx.status(200), ctx.json(MOCK_NOTICE_LIST_RESPONSE), ctx.delay(MOCKING_DELAY)); }), rest.get(`/notices/:id`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MOCK_NOTICE_RESPONSE)); + return res(ctx.status(200), ctx.json(MOCK_NOTICE_RESPONSE), ctx.delay(MOCKING_DELAY)); }), rest.put(`/notices/:id`, async (req, res, ctx) => { diff --git a/frontend/src/mocks/post.ts b/frontend/src/mocks/post.ts index 2fcf5a881..823b9d568 100644 --- a/frontend/src/mocks/post.ts +++ b/frontend/src/mocks/post.ts @@ -1,19 +1,20 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_GUEST_POST_INFO, MOCK_POST_INFO } from './mockData/post'; export const mockPost = [ rest.get('/posts/:postId', (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_POST_INFO)); + return res(ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json(MOCK_POST_INFO)); }), rest.get('/posts/:postId/guest', (req, res, ctx) => { - return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_GUEST_POST_INFO)); + return res(ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json(MOCK_GUEST_POST_INFO)); }), rest.delete('/posts/:postId', (req, res, ctx) => { return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json({ message: '게시글이 성공적으로 삭제되었습니다' }) ); @@ -23,7 +24,7 @@ export const mockPost = [ MOCK_POST_INFO.deadline = '2023-07-13 18:40'; return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json({ message: '게시글이 성공적으로 조기 마감 되었습니다' }) ); @@ -34,7 +35,7 @@ export const mockPost = [ window.console.log('게시글 작성 완료', req.body); return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(201), ctx.json({ message: '게시글이 성공적으로 생성되었습니다' }) ); @@ -45,7 +46,7 @@ export const mockPost = [ window.console.log('게시글 수정 완료되었습니다', req.body); return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json({ message: '게시글이 성공적으로 수정되었습니다!!' }) ); @@ -54,7 +55,7 @@ export const mockPost = [ //게시글 삭제 rest.delete('/posts/:postId', (req, res, ctx) => { return res( - ctx.delay(1000), + ctx.delay(MOCKING_DELAY), ctx.status(200), ctx.json({ message: '게시글이 성공적으로 삭제되었습니다!!' }) ); diff --git a/frontend/src/mocks/postList.ts b/frontend/src/mocks/postList.ts index a521efec3..ccb813882 100644 --- a/frontend/src/mocks/postList.ts +++ b/frontend/src/mocks/postList.ts @@ -9,6 +9,8 @@ import { import { MOCK_GUEST_POST_LIST, MOCK_POST_LIST } from '@mocks/mockData/post'; +import { MOCKING_DELAY } from './handlers'; + export const mockPostList = [ rest.get('/posts', (req, res, ctx) => { return createMockPostListResponse(req, res, ctx); @@ -58,7 +60,7 @@ const createMockPostListResponse = ( } if (page > 0) { - return res(ctx.status(200), ctx.json(MOCK_POST_LIST), ctx.delay(1000)); + return res(ctx.status(200), ctx.json(MOCK_POST_LIST), ctx.delay(MOCKING_DELAY)); } return res(ctx.status(200), ctx.json(MOCK_POST_LIST)); @@ -74,7 +76,7 @@ const createMockGuestPostListResponse = ( if (page === null) return; if (page > 0) { - return res(ctx.status(200), ctx.json(MOCK_GUEST_POST_LIST), ctx.delay(1000)); + return res(ctx.status(200), ctx.json(MOCK_GUEST_POST_LIST), ctx.delay(MOCKING_DELAY)); } return res(ctx.status(200), ctx.json(MOCK_GUEST_POST_LIST)); diff --git a/frontend/src/mocks/ranking.ts b/frontend/src/mocks/ranking.ts index a59cdb7f8..8ccef91c6 100644 --- a/frontend/src/mocks/ranking.ts +++ b/frontend/src/mocks/ranking.ts @@ -2,6 +2,8 @@ import { rest } from 'msw'; import { PassionUserRanking, PopularPostRanking } from '@type/ranking'; +import { MOCKING_DELAY } from './handlers'; + const userRankingInfo: PassionUserRanking = { ranking: 1111, nickname: 'wow', @@ -37,14 +39,14 @@ const rankingPostList: PopularPostRanking[] = new Array(10) export const mockRanking = [ rest.get('/members/me/ranking', (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(500), ctx.json(userRankingInfo)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(userRankingInfo)); }), rest.get('/members/ranking/passion/guest', (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(1000), ctx.json(rankerList)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(rankerList)); }), rest.get('/posts/ranking/popular/guest', (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(500), ctx.json(rankingPostList)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(rankingPostList)); }), ]; diff --git a/frontend/src/mocks/report.ts b/frontend/src/mocks/report.ts index 0029370ba..c17afb7de 100644 --- a/frontend/src/mocks/report.ts +++ b/frontend/src/mocks/report.ts @@ -1,15 +1,16 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_PENDING_REPORT_LIST } from './mockData/report'; export const mockReport = [ rest.post('/report', (req, res, ctx) => { - return res(ctx.status(200)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY)); }), rest.get('/reports/admin', (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(500), ctx.json(MOCK_PENDING_REPORT_LIST)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_PENDING_REPORT_LIST)); }), rest.post('/reports/action/admin', (req, res, ctx) => { - return res(ctx.status(200)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY)); }), ]; diff --git a/frontend/src/mocks/reportApproveResult.ts b/frontend/src/mocks/reportApproveResult.ts index 45266cd25..4940d1833 100644 --- a/frontend/src/mocks/reportApproveResult.ts +++ b/frontend/src/mocks/reportApproveResult.ts @@ -1,9 +1,10 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_REPORT_APPROVE_RESULT } from './mockData/reportApproveResult'; export const mockReportApproveResult = [ rest.get('/alarms/report/:reportId', (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(500), ctx.json(MOCK_REPORT_APPROVE_RESULT)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_REPORT_APPROVE_RESULT)); }), ]; diff --git a/frontend/src/mocks/token.ts b/frontend/src/mocks/token.ts index c8a66f093..3a34a6db8 100644 --- a/frontend/src/mocks/token.ts +++ b/frontend/src/mocks/token.ts @@ -1,9 +1,10 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_TOKEN } from './mockData/token'; export const mockToken = [ rest.post('/auth/silent-login', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MOCK_TOKEN)); + return res(ctx.status(200), ctx.json(MOCK_TOKEN), ctx.delay(MOCKING_DELAY)); }), ]; diff --git a/frontend/src/mocks/userInfo.ts b/frontend/src/mocks/userInfo.ts index f859732b9..d50716d7b 100644 --- a/frontend/src/mocks/userInfo.ts +++ b/frontend/src/mocks/userInfo.ts @@ -2,25 +2,35 @@ import { rest } from 'msw'; import { MOCK_ADMIN_USER_INFO, MOCK_USER_INFO } from '@mocks/mockData/user'; +import { MOCKING_DELAY } from './handlers'; + export const mockUserInfo = [ rest.get('/members/me', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MOCK_ADMIN_USER_INFO)); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json(MOCK_ADMIN_USER_INFO)); }), rest.patch('/members/me/detail', (req, res, ctx) => { - return res(ctx.status(200), ctx.json({ ok: '개인 정보가 성공적으로 저장되었습니다!' })); + return res( + ctx.status(200), + ctx.delay(MOCKING_DELAY), + ctx.json({ ok: '개인 정보가 성공적으로 저장되었습니다!' }) + ); }), rest.patch('/members/me/nickname', (req, res, ctx) => { MOCK_USER_INFO.nickname = 'wood'; - return res(ctx.status(200), ctx.json({ ok: '닉네임이 성공적으로 수정되었습니다!' })); + return res( + ctx.status(200), + ctx.delay(MOCKING_DELAY), + ctx.json({ ok: '닉네임이 성공적으로 수정되었습니다!' }) + ); }), rest.delete('/members/me/delete', (req, res, ctx) => { MOCK_USER_INFO.nickname = 'cancel'; - return res(ctx.status(204)); + return res(ctx.status(204), ctx.delay(MOCKING_DELAY)); }), rest.delete('/auth/logout', (req, res, ctx) => { @@ -28,6 +38,7 @@ export const mockUserInfo = [ return res( ctx.status(204), + ctx.delay(MOCKING_DELAY), ctx.cookie('hasEssentialInfo', 'expired', { expires: expirationTime, }) diff --git a/frontend/src/mocks/vote.ts b/frontend/src/mocks/vote.ts index 92bd90749..a237d71b2 100644 --- a/frontend/src/mocks/vote.ts +++ b/frontend/src/mocks/vote.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; +import { MOCKING_DELAY } from './handlers'; import { MOCK_POST_INFO } from './mockData/post'; export const mockVote = [ @@ -7,13 +8,13 @@ export const mockVote = [ rest.post('/posts/:postId/options/:optionId', (req, res, ctx) => { MOCK_POST_INFO.voteInfo.selectedOptionId = 999; - return res(ctx.status(200), ctx.json({ message: 'ok' })); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json({ message: 'ok' })); }), //선택지 수정 rest.patch('/posts/:postId/options', (req, res, ctx) => { MOCK_POST_INFO.voteInfo.selectedOptionId = 888; - return res(ctx.status(200), ctx.json({ message: 'ok' })); + return res(ctx.status(200), ctx.delay(MOCKING_DELAY), ctx.json({ message: 'ok' })); }), ]; diff --git a/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/index.tsx b/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/index.tsx new file mode 100644 index 000000000..5c67eef9f --- /dev/null +++ b/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/index.tsx @@ -0,0 +1,112 @@ +import { SquareButton, Table } from 'votogether-design-system'; + +import { ReportActionRequest } from '@type/report'; + +import { usePendingReportActionList } from '@hooks/query/report/usePendingReportActionList'; +import { useReportAction } from '@hooks/query/report/useReportAction'; + +import { PATH } from '@constants/path'; +import { REPORT_ACTION_TYPE, REPORT_TYPE } from '@constants/report'; + +import { truncateText } from '@utils/truncateText'; + +import * as S from './style'; + +interface ReportDetail { + typeName: keyof typeof REPORT_ACTION_TYPE; + target: string; +} + +export default function PendingReportTableFetcher() { + const { + data, + setPage, + page, + hasNextPage, + hasPrevPage, + fetchNextPage, + fetchPrevPage, + getPageNumberList, + } = usePendingReportActionList(); + + const { mutate: reportAction } = useReportAction(); + + const columnList = ['순번', '내용', '일시', '종류', '사유', '수정/삭제', '신고 해제']; + + const handleClickButton = (reportData: ReportActionRequest, reportDetail: ReportDetail) => { + const { typeName, target } = reportDetail; + const editOrDeleteText = REPORT_ACTION_TYPE[typeName]; + const message = `'${typeName}: ${target}'을 ${ + reportData.hasAction ? editOrDeleteText : '신고 해제' + }하시겠습니까?`; + + if (window.confirm(message)) reportAction(reportData); + }; + + if (!data) return <>; + + const reportListWithAction = data.reportList.map((report, index) => { + const reportData = { id: report.id, hasAction: true }; + const reportDetail = { + typeName: report.typeName, + target: truncateText(report.target), + isNickname: report.typeName === REPORT_TYPE.NICKNAME, + }; + return { + ...report, + id: index + 1, + target: + report.typeName === '게시글' ? ( + 게시글 보러가기 + ) : ( + report.target + ), + editOrDeleteAction: ( + handleClickButton(reportData, reportDetail)} + > + {REPORT_ACTION_TYPE[report.typeName]} + + ), + deleteReport: ( + handleClickButton({ ...reportData, hasAction: false }, reportDetail)} + > + 해제 + + ), + }; + }); + + return ( + <> +
+ + {hasPrevPage && ( + fetchPrevPage()}> + {'<'} + + )} + {getPageNumberList(data.totalPageNumber).map(item => ( + { + setPage(item); + }} + > + {item} + + ))} + {hasNextPage && ( + fetchNextPage(data.totalPageNumber)}> + {'>'} + + )} + + + ); +} diff --git a/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/style.ts b/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/style.ts new file mode 100644 index 000000000..b78e4d551 --- /dev/null +++ b/frontend/src/pages/admin/PendingReportPage/PendingReportTableFetcher/style.ts @@ -0,0 +1,53 @@ +import { Link } from 'react-router-dom'; + +import styled from 'styled-components'; + +import { theme } from '@styles/theme'; + +export const PostDetailLink = styled(Link)` + text-decoration: underline; + text-underline-offset: 2px; +`; + +export const ReportActionButton = styled.button<{ $isEdit: boolean }>` + height: 35px; + border: 1px solid red; + border-radius: 4px; + border-color: ${props => (props.$isEdit ? 'blue' : 'red')}; + + color: ${props => (props.$isEdit ? 'blue' : 'red')}; + padding: 7px 14px; + + @media (max-width: ${theme.breakpoint.sm}) { + padding: 10px; + font-size: 10px; + } +`; + +export const ReportDeleteButton = styled.button` + height: 35px; + + border: 1px solid gray; + border-radius: 4px; + color: gray; + padding: 7px 14px; + + @media (max-width: ${theme.breakpoint.sm}) { + padding: 10px; + font-size: 10px; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + justify-content: center; + gap: 10px; + + width: 100%; + margin-top: 20px; +`; + +export const ButtonWrapper = styled.div` + width: 60px; + height: 60px; +`; diff --git a/frontend/src/pages/admin/PendingReportPage/index.tsx b/frontend/src/pages/admin/PendingReportPage/index.tsx index 500c53166..b5ffd9fe8 100644 --- a/frontend/src/pages/admin/PendingReportPage/index.tsx +++ b/frontend/src/pages/admin/PendingReportPage/index.tsx @@ -1,116 +1,27 @@ import { Suspense } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { Skeleton, Table } from 'votogether-design-system'; - -import { ReportActionRequest } from '@type/report'; - -import { usePendingReportActionList } from '@hooks/query/report/usePendingReportActionList'; -import { useReportAction } from '@hooks/query/report/useReportAction'; +import { Skeleton } from 'votogether-design-system'; import Layout from '@components/common/Layout'; +import UpButton from '@components/common/UpButton'; -import { PATH } from '@constants/path'; -import { REPORT_ACTION_TYPE, REPORT_TYPE } from '@constants/report'; +import { smoothScrollToTop } from '@utils/scrollToTop'; -import * as S from './stlye'; - -export interface ReportDetail { - typeName: keyof typeof REPORT_ACTION_TYPE; - target: string; -} +import PendingReportTableFetcher from './PendingReportTableFetcher'; +import * as S from './style'; export default function PendingReportPage() { - const navigate = useNavigate(); - - const params = useParams() as { page: string }; - const currentPageNumber = params.page ? Number(params.page) : 1; - - const columnList = ['Id', '내용', '일시', '종류', '사유', '수정/삭제', '신고 해제']; - const { data } = usePendingReportActionList(currentPageNumber - 1); - const { mutate: reportAction } = useReportAction(); - - const handleClickButton = (reportData: ReportActionRequest, reportDetail: ReportDetail) => { - const { typeName, target } = reportDetail; - const editOrDeleteText = REPORT_ACTION_TYPE[typeName]; - const message = `'${typeName}: ${target}'을 ${ - reportData.hasAction ? editOrDeleteText : '신고 해제' - }하시겠습니까?`; - - if (window.confirm(message)) reportAction(reportData); - }; - - const reportListWithAction = data - ? data.reportList.map(report => { - const reportData = { id: report.id, hasAction: true }; - const reportDetail = { - typeName: report.typeName, - target: report.target, - isNickname: report.typeName === REPORT_TYPE.NICKNAME, - }; - return { - ...report, - editOrDeleteAction: ( - handleClickButton(reportData, reportDetail)} - > - {REPORT_ACTION_TYPE[report.typeName]} - - ), - deleteReport: ( - handleClickButton({ ...reportData, hasAction: false }, reportDetail)} - > - 해제 - - ), - }; - }) - : []; - return ( - - 신고 조치 예정 목록 - }> - {data && ( - <> -
- - - navigate(`${PATH.ADMIN_PENDING_REPORT}?page=${data.currentPageNumber - 1}`) - } - disabled={currentPageNumber === 1} - > - 이전 - - {new Array(data.totalPageNumber).fill(0).map((_, index) => ( - - {index + 1} - - ))} - - navigate(`${PATH.ADMIN_PENDING_REPORT}?page=${data.currentPageNumber + 1}`) - } - disabled={currentPageNumber === data.totalPageNumber} - > - 다음 - - - - )} - - + }> + + 신고 조치 예정 목록 + + + + + + ); } diff --git a/frontend/src/pages/admin/PendingReportPage/stlye.ts b/frontend/src/pages/admin/PendingReportPage/stlye.ts deleted file mode 100644 index 61f257fbf..000000000 --- a/frontend/src/pages/admin/PendingReportPage/stlye.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Link } from 'react-router-dom'; - -import styled from 'styled-components'; - -import { theme } from '@styles/theme'; - -export const Wrapper = styled.main` - display: flex; - flex-direction: column; - justify-content: start; - align-items: start; - gap: 15px; - - padding: 15px; - - @media (max-width: ${theme.breakpoint.sm}) { - margin-top: 40px; - } -`; - -export const PageTitle = styled.h1` - font-size: 1.6rem; - font-weight: 600; -`; - -export const PaginationContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - - width: 100%; - margin-top: 20px; -`; - -export const PaginationButton = styled(Link)<{ $isSelected: boolean }>` - display: flex; - justify-content: center; - align-items: center; - - width: 17px; - border: 1px solid gray; - border-radius: 5px; - padding: 7px 20px 9px 20px; - - background-color: ${props => (props.$isSelected ? '#DCF0FA' : 'white')}; - - font: var(--text-caption); -`; - -export const MovePageButton = styled.button` - display: flex; - justify-content: center; - align-items: center; - - border: 1px solid gray; - border-radius: 5px; - padding: 7px 12px 9px 12px; - - font: var(--text-caption); -`; - -export const ReportActionButton = styled.button<{ $isEdit: boolean }>` - height: 35px; - border: 1px solid red; - border-radius: 4px; - border-color: ${props => (props.$isEdit ? 'blue' : 'red')}; - - color: ${props => (props.$isEdit ? 'blue' : 'red')}; - padding: 7px 14px; - - @media (max-width: ${theme.breakpoint.sm}) { - padding: 10px; - font-size: 10px; - } -`; - -export const ReportDeleteButton = styled.button` - height: 35px; - - border: 1px solid gray; - border-radius: 4px; - color: gray; - padding: 7px 14px; - - @media (max-width: ${theme.breakpoint.sm}) { - padding: 10px; - font-size: 10px; - } -`; diff --git a/frontend/src/pages/admin/PendingReportPage/style.ts b/frontend/src/pages/admin/PendingReportPage/style.ts new file mode 100644 index 000000000..517b396d2 --- /dev/null +++ b/frontend/src/pages/admin/PendingReportPage/style.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +import { theme } from '@styles/theme'; + +export const Wrapper = styled.main` + display: flex; + flex-direction: column; + justify-content: start; + align-items: start; + gap: 20px; + + padding: 30px; + + @media (max-width: ${theme.breakpoint.sm}) { + margin-top: 40px; + padding: 15px; + } +`; + +export const PageTitle = styled.span` + font: var(--text-page-title); +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + align-items: end; + gap: 20px; + + width: 62px; + padding-right: 10px; + + position: fixed; + left: 90%; + bottom: 24px; + + @media (max-width: ${theme.breakpoint.sm}) { + left: 83%; + } + + @media (max-width: 281px) { + left: 78%; + } +`; diff --git a/frontend/src/pages/admin/notices/NoticeAdminPage/index.tsx b/frontend/src/pages/admin/notices/NoticeAdminPage/index.tsx index 33b1e2a3d..0921887c7 100644 --- a/frontend/src/pages/admin/notices/NoticeAdminPage/index.tsx +++ b/frontend/src/pages/admin/notices/NoticeAdminPage/index.tsx @@ -15,7 +15,7 @@ export default function NoticeAdminPage() { 관리자 공지사항 페이지 - 공지사항 작성하러 가기 + 공지사항 작성 }> diff --git a/frontend/src/pages/admin/notices/NoticeAdminPage/style.ts b/frontend/src/pages/admin/notices/NoticeAdminPage/style.ts index 1242ce774..bfcbe7118 100644 --- a/frontend/src/pages/admin/notices/NoticeAdminPage/style.ts +++ b/frontend/src/pages/admin/notices/NoticeAdminPage/style.ts @@ -2,16 +2,12 @@ import { Link } from 'react-router-dom'; import { styled } from 'styled-components'; -export const Container = styled.div` - padding: 100px; -`; - export const WriteContainer = styled.div` display: flex; - flex-direction: column; + justify-content: space-between; align-items: center; - padding: 30px 0; + padding: 20px 60px; `; export const Title = styled.span` @@ -19,7 +15,6 @@ export const Title = styled.span` `; export const ButtonWrapper = styled(Link)` - width: 200px; - height: 60px; - margin-top: 30px; + width: 150px; + height: 50px; `; diff --git a/frontend/src/utils/truncateText.ts b/frontend/src/utils/truncateText.ts new file mode 100644 index 000000000..79ffb1458 --- /dev/null +++ b/frontend/src/utils/truncateText.ts @@ -0,0 +1,8 @@ +/** + * 문자열을 maxLength 값을 초과하면 말줄임표를 붙인다 + */ +export function truncateText(text: string, maxLength: number = 10) { + if (text.length <= maxLength) return text; + + return text.slice(0, maxLength) + '...'; +}