Skip to content

Commit 177f8cf

Browse files
wldnjs990AAminhanirii00
authored
feat : 관리자 & 신고 2차 구현 (#77)
* design: 관리자 페이지 디자인 수정 * feat: 페이지에 따라 제목이 다르게 보이도록 AdminPageTitle 컴포넌트 구현 * design: 롤링페이퍼 관리 페이지 퍼블리싱 * feat:신고 페이지 api 수정, 데이터바인딩 수정, 페이지네이션 연결 작업(임시) * 빈 커밋 * feat : 신고기능 구현 + 신고상세 데이터바인딩 오류 수정 --------- Co-authored-by: AAminha <[email protected]> Co-authored-by: Sebin Kim <[email protected]>
1 parent cbe92b0 commit 177f8cf

22 files changed

+551
-216
lines changed

src/apis/admin.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
import client from './client';
22

3-
const getReports = async (
4-
setReports: React.Dispatch<React.SetStateAction<Report[]>>,
5-
queryString: string = '',
6-
callBack?: () => void,
7-
) => {
3+
const postReports = async (postReportRequest: PostReportRequest) => {
84
try {
9-
const res = await client.get(`/api/reports${queryString}`);
10-
setReports(res.data.data);
11-
if (callBack) callBack();
12-
console.log(res.data.data);
5+
const res = await client.post(`/api/reports`, postReportRequest);
6+
if (res.status === 200) {
7+
return res;
8+
}
9+
} catch (error) {
10+
console.error(error);
11+
}
12+
};
13+
14+
const getReports = async (reportQueryString: ReportQueryString) => {
15+
try {
16+
const queryParams = new URLSearchParams();
17+
if (reportQueryString.reportType !== null)
18+
queryParams.append('reportType', reportQueryString.reportType);
19+
if (reportQueryString.status !== null) queryParams.append('status', reportQueryString.status);
20+
if (reportQueryString.page !== null) queryParams.append('page', reportQueryString.page);
21+
if (reportQueryString.size !== null) queryParams.append('size', reportQueryString.size);
22+
23+
const queryStrings = queryParams.toString();
24+
const res = await client.get(`/api/reports?${queryStrings}`);
25+
if (!res) throw new Error('신고 목록 데이터 조회 도중 에러가 발생했습니다.');
26+
console.log(res);
27+
return res;
1328
} catch (error) {
1429
console.error(error);
1530
}
1631
};
1732

18-
const patchReport = async (reportId: number, reportRequest: ReportRequest) => {
33+
const patchReport = async (reportId: number, patchReportRequest: PatchReportRequest) => {
1934
try {
20-
console.log(`/api/reports/${reportId}`, reportRequest);
21-
const res = await client.patch(`/api/reports/${reportId}`, reportRequest);
35+
console.log(`/api/reports/${reportId}`, patchReportRequest);
36+
const res = await client.patch(`/api/reports/${reportId}`, patchReportRequest);
2237
console.log(res);
2338
} catch (error) {
2439
console.error(error);
@@ -61,4 +76,4 @@ const patchBadWords = async (
6176
}
6277
};
6378

64-
export { getReports, patchReport, getBadWords, postBadWords, patchBadWords };
79+
export { postReports, getReports, patchReport, getBadWords, postBadWords, patchBadWords };

src/assets/icons/arrow-down.svg

Lines changed: 2 additions & 2 deletions
Loading

src/assets/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import LikeFilledIcon from './like-filled.svg?react';
1616
import LikeOutlinedIcon from './like-outlined.svg?react';
1717
import NaverIcon from './naver.svg?react';
1818
import NoticeIcon from './notice.svg?react';
19+
import PencilIcon from './pencil.svg?react';
1920
import PersonIcon from './person.svg?react';
2021
import RestartIcon from './restart.svg';
2122
import SirenFilledIcon from './siren-filled.svg?react';
@@ -52,4 +53,5 @@ export {
5253
LikeOutlinedIcon,
5354
DeleteIcon,
5455
CancelIcon,
56+
PencilIcon,
5557
};

src/assets/icons/pencil.svg

Lines changed: 3 additions & 0 deletions
Loading

src/components/ReportModal.tsx

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,91 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { twMerge } from 'tailwind-merge';
33

4+
import { postReports } from '@/apis/admin';
5+
46
import ConfirmModal from './ConfirmModal';
57
import TextareaField from './TextareaField';
68

79
interface ReportModalProps {
10+
reportType: ReportType;
11+
letterId: number | null;
812
onClose: () => void;
913
}
1014

11-
const REPORT_REASON = ['욕설', '비방', '폭언', '성희롱', '기타'];
15+
interface ReportReason {
16+
name: string;
17+
type: Reason;
18+
}
19+
const REPORT_REASON: ReportReason[] = [
20+
{ name: '욕설', type: 'ABUSE' },
21+
{ name: '비방', type: 'DEFAMATION' },
22+
{ name: '폭언', type: 'THREATS' },
23+
{ name: '성희롱', type: 'HARASSMENT' },
24+
{ name: '기타', type: 'ETC' },
25+
];
1226

13-
const ReportModal = ({ onClose }: ReportModalProps) => {
14-
const [selected, setSelected] = useState('');
15-
const [additionalReason, setAdditionalReason] = useState('');
27+
const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => {
28+
const [postReportRequest, setPostReportRequest] = useState<PostReportRequest>({
29+
reportType: reportType,
30+
reasonType: '',
31+
reason: '',
32+
letterId: letterId,
33+
});
1634

17-
const handleReasonClick = (reason: string) => {
18-
if (selected === reason) setSelected('');
19-
else setSelected(reason);
35+
const handleReasonClick = (reason: Reason) => {
36+
if (postReportRequest.reasonType === reason)
37+
setPostReportRequest((cur) => ({ ...cur, reasonType: '' }));
38+
else setPostReportRequest((cur) => ({ ...cur, reasonType: reason }));
2039
};
2140

22-
const handleSubmit = () => {
23-
onClose();
41+
const handleSubmit = async () => {
42+
const res = await postReports(postReportRequest);
43+
if (res?.status === 200) {
44+
alert('신고 처리되었습니다.');
45+
onClose();
46+
} else if (res?.status === 409) {
47+
alert('신고한 이력이 있습니다.');
48+
onClose();
49+
}
2450
};
2551

52+
useEffect(() => {
53+
if (!postReportRequest.letterId) {
54+
alert('신고 모달을 여는 과정에서 오류가 발생했습니다. 새로고침을 눌러주세요');
55+
onClose();
56+
}
57+
});
58+
2659
return (
2760
<ConfirmModal
2861
title="신고 사유를 선택해주세요"
2962
description="신고한 게시물은 관리자 검토 후 처리됩니다."
3063
cancelText="취소하기"
3164
confirmText="제출하기"
32-
confirmDisabled={selected === ''}
65+
confirmDisabled={postReportRequest.reasonType === ''}
3366
onCancel={onClose}
3467
onConfirm={handleSubmit}
3568
>
3669
<section className="my-6 flex flex-wrap gap-x-2.5 gap-y-2">
37-
{REPORT_REASON.map((reason) => (
70+
{REPORT_REASON.map((reason, idx) => (
3871
<button
72+
key={idx}
3973
type="button"
4074
className={twMerge(
4175
'body-m rounded-full bg-white px-5 py-1.5 text-black',
42-
selected === reason && 'bg-primary-2',
76+
postReportRequest.reasonType === reason.type && 'bg-primary-2',
4377
)}
44-
onClick={() => handleReasonClick(reason)}
78+
onClick={() => handleReasonClick(reason.type)}
4579
>
46-
{reason}
80+
{reason.name}
4781
</button>
4882
))}
4983
</section>
5084
<TextareaField
5185
rows={3}
5286
placeholder="이곳을 눌러 추가 사유를 작성해주세요"
53-
value={additionalReason}
54-
onChange={(e) => setAdditionalReason(e.target.value)}
87+
value={postReportRequest.reason}
88+
onChange={(e) => setPostReportRequest((cur) => ({ ...cur, reason: e.target.value }))}
5589
/>
5690
</ConfirmModal>
5791
);

src/pages/Admin/FilteredLetter.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AlarmIcon } from '@/assets/icons';
22

3+
import AdminPageTitle from './components/AdminPageTitle';
34
import FilteredLetterListItem from './components/FilteredLetterListItem';
45
import ListHeaderFrame from './components/ListHeaderFrame';
56
import WrapperFrame from './components/WrapperFrame';
@@ -8,20 +9,23 @@ import WrapperTitle from './components/WrapperTitle';
89
export default function FilteredLetterManage() {
910
const arr = new Array(10).fill(null);
1011
return (
11-
<WrapperFrame>
12-
<WrapperTitle title="필터링 단어로 차단된 편지 목록" Icon={AlarmIcon} />
13-
<section className="mt-5 flex flex-col">
14-
<ListHeaderFrame>
15-
<span className="admin-list-set basis-1/10">ID</span>
16-
<span className="admin-list-set basis-2/10">제보자 이메일</span>
17-
<span className="admin-list-set basis-2/10">작성자 이메일</span>
18-
<span className="admin-list-set basis-2/10">차단 일자</span>
19-
<span className="admin-list-set basis-2/10">포함된 단어</span>
20-
</ListHeaderFrame>
21-
{arr.map((_, idx) => {
22-
return <FilteredLetterListItem key={idx} />;
23-
})}
24-
</section>
25-
</WrapperFrame>
12+
<>
13+
<AdminPageTitle>검열 관리 / 차단된 편지 목록</AdminPageTitle>
14+
<WrapperFrame>
15+
<WrapperTitle title="필터링 단어로 차단된 편지 목록" Icon={AlarmIcon} />
16+
<section className="mt-5 flex flex-col">
17+
<ListHeaderFrame>
18+
<span className="admin-list-set basis-1/10">ID</span>
19+
<span className="admin-list-set basis-2/10">제보자 이메일</span>
20+
<span className="admin-list-set basis-2/10">작성자 이메일</span>
21+
<span className="admin-list-set basis-2/10">차단 일자</span>
22+
<span className="admin-list-set basis-2/10">포함된 단어</span>
23+
</ListHeaderFrame>
24+
{arr.map((_, idx) => {
25+
return <FilteredLetterListItem key={idx} />;
26+
})}
27+
</section>
28+
</WrapperFrame>
29+
</>
2630
);
2731
}

src/pages/Admin/Filtering.tsx

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getBadWords } from '@/apis/admin';
44
import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons';
55

66
import AddInputButton from './components/AddInputButton';
7+
import AdminPageTitle from './components/AdminPageTitle';
78
import WrapperFrame from './components/WrapperFrame';
89
import WrapperTitle from './components/WrapperTitle';
910

@@ -15,37 +16,40 @@ export default function FilteringManage() {
1516
getBadWords(setBadWords);
1617
}, []);
1718
return (
18-
<WrapperFrame>
19-
<WrapperTitle title="필터링 단어 설정" Icon={AlarmIcon} />
20-
<div className="mt-5 flex w-full flex-wrap gap-4">
21-
{badWords.map((badWord, idx) => {
22-
return (
23-
<span
24-
key={idx}
25-
className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5"
26-
>
27-
{badWord.word}
28-
<button>
29-
<CancelIcon className="h-4 w-4" />
19+
<>
20+
<AdminPageTitle>검열 관리 / 필터링 단어 설정</AdminPageTitle>
21+
<WrapperFrame>
22+
<WrapperTitle title="필터링 단어" Icon={AlarmIcon} />
23+
<div className="mt-5 flex w-full flex-wrap gap-4">
24+
{badWords.map((badWord, idx) => {
25+
return (
26+
<span
27+
key={idx}
28+
className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5"
29+
>
30+
{badWord.word}
31+
<button>
32+
<CancelIcon className="h-4 w-4" />
33+
</button>
34+
</span>
35+
);
36+
})}
37+
{addInputShow ? (
38+
<AddInputButton setAddInputShow={setAddInputShow} setBadWords={setBadWords} />
39+
) : (
40+
<span className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5">
41+
추가하기
42+
<button
43+
onClick={() => {
44+
setAddInputShow(true);
45+
}}
46+
>
47+
<AddIcon className="h-4 w-4" />
3048
</button>
3149
</span>
32-
);
33-
})}
34-
{addInputShow ? (
35-
<AddInputButton setAddInputShow={setAddInputShow} setBadWords={setBadWords} />
36-
) : (
37-
<span className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5">
38-
추가하기
39-
<button
40-
onClick={() => {
41-
setAddInputShow(true);
42-
}}
43-
>
44-
<AddIcon className="h-4 w-4" />
45-
</button>
46-
</span>
47-
)}
48-
</div>
49-
</WrapperFrame>
50+
)}
51+
</div>
52+
</WrapperFrame>
53+
</>
5054
);
5155
}

0 commit comments

Comments
 (0)