Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/apis/admin.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import client from './client';

const getReports = async (
setReports: React.Dispatch<React.SetStateAction<Report[]>>,
queryString: string = '',
callBack?: () => void,
) => {
const postReports = async (postReportRequest: PostReportRequest) => {
try {
const res = await client.get(`/api/reports${queryString}`);
setReports(res.data.data);
if (callBack) callBack();
console.log(res.data.data);
const res = await client.post(`/api/reports`, postReportRequest);
if (res.status === 200) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기로는 axios가 status 가200번대를 제외하고는 무조건 error를 반환하도록 하는 걸로 알고 있는데, 200만 특정해서 리턴한 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 큰 뜻은 없는데 좀 더 true임을 명확하게 표현해주는거 같아서..? 썻습니다 ㅎ

return res;
}
} catch (error) {
console.error(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw error로 에러를 던져주면 좋을 것 같습니다!

}
};

const getReports = async (reportQueryString: ReportQueryString) => {
try {
const queryParams = new URLSearchParams();
if (reportQueryString.reportType !== null)
queryParams.append('reportType', reportQueryString.reportType);
if (reportQueryString.status !== null) queryParams.append('status', reportQueryString.status);
if (reportQueryString.page !== null) queryParams.append('page', reportQueryString.page);
if (reportQueryString.size !== null) queryParams.append('size', reportQueryString.size);

const queryStrings = queryParams.toString();
const res = await client.get(`/api/reports?${queryStrings}`);
if (!res) throw new Error('신고 목록 데이터 조회 도중 에러가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.error(error);
}
};

const patchReport = async (reportId: number, reportRequest: ReportRequest) => {
const patchReport = async (reportId: number, patchReportRequest: PatchReportRequest) => {
try {
console.log(`/api/reports/${reportId}`, reportRequest);
const res = await client.patch(`/api/reports/${reportId}`, reportRequest);
console.log(`/api/reports/${reportId}`, patchReportRequest);
const res = await client.patch(`/api/reports/${reportId}`, patchReportRequest);
console.log(res);
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -61,4 +76,4 @@ const patchBadWords = async (
}
};

export { getReports, patchReport, getBadWords, postBadWords, patchBadWords };
export { postReports, getReports, patchReport, getBadWords, postBadWords, patchBadWords };
4 changes: 2 additions & 2 deletions src/assets/icons/arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import LikeFilledIcon from './like-filled.svg?react';
import LikeOutlinedIcon from './like-outlined.svg?react';
import NaverIcon from './naver.svg?react';
import NoticeIcon from './notice.svg?react';
import PencilIcon from './pencil.svg?react';
import PersonIcon from './person.svg?react';
import RestartIcon from './restart.svg';
import SirenFilledIcon from './siren-filled.svg?react';
Expand Down Expand Up @@ -52,4 +53,5 @@ export {
LikeOutlinedIcon,
DeleteIcon,
CancelIcon,
PencilIcon,
};
3 changes: 3 additions & 0 deletions src/assets/icons/pencil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 51 additions & 17 deletions src/components/ReportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,91 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import { postReports } from '@/apis/admin';

import ConfirmModal from './ConfirmModal';
import TextareaField from './TextareaField';

interface ReportModalProps {
reportType: ReportType;
letterId: number | null;
onClose: () => void;
}

const REPORT_REASON = ['욕설', '비방', '폭언', '성희롱', '기타'];
interface ReportReason {
name: string;
type: Reason;
}
const REPORT_REASON: ReportReason[] = [
{ name: '욕설', type: 'ABUSE' },
{ name: '비방', type: 'DEFAMATION' },
{ name: '폭언', type: 'THREATS' },
{ name: '성희롱', type: 'HARASSMENT' },
{ name: '기타', type: 'ETC' },
];

const ReportModal = ({ onClose }: ReportModalProps) => {
const [selected, setSelected] = useState('');
const [additionalReason, setAdditionalReason] = useState('');
const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => {
const [postReportRequest, setPostReportRequest] = useState<PostReportRequest>({
reportType: reportType,
reasonType: '',
reason: '',
letterId: letterId,
});

const handleReasonClick = (reason: string) => {
if (selected === reason) setSelected('');
else setSelected(reason);
const handleReasonClick = (reason: Reason) => {
if (postReportRequest.reasonType === reason)
setPostReportRequest((cur) => ({ ...cur, reasonType: '' }));
else setPostReportRequest((cur) => ({ ...cur, reasonType: reason }));
};

const handleSubmit = () => {
onClose();
const handleSubmit = async () => {
const res = await postReports(postReportRequest);
if (res?.status === 200) {
alert('신고 처리되었습니다.');
onClose();
} else if (res?.status === 409) {
alert('신고한 이력이 있습니다.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 alert 창으로 사용자에게 안내 메시지를 보내야 할 경우가 꽤 있어서 모달 컴포넌트를 따로 만들어서 사용하는 게 좋을 것 같네요!!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넹 토스트UI를 만들어보려 합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alert를 toastUI로 대체한다는 말씀이신거죠?
저도 alert를 좀 만들어둬야겠네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넹 토스트UI 만들고나면 대부분 alert들을 교체할 거 같습니다!

onClose();
}
};

useEffect(() => {
if (!postReportRequest.letterId) {
alert('신고 모달을 여는 과정에서 오류가 발생했습니다. 새로고침을 눌러주세요');
onClose();
}
});

return (
<ConfirmModal
title="신고 사유를 선택해주세요"
description="신고한 게시물은 관리자 검토 후 처리됩니다."
cancelText="취소하기"
confirmText="제출하기"
confirmDisabled={selected === ''}
confirmDisabled={postReportRequest.reasonType === ''}
onCancel={onClose}
onConfirm={handleSubmit}
>
<section className="my-6 flex flex-wrap gap-x-2.5 gap-y-2">
{REPORT_REASON.map((reason) => (
{REPORT_REASON.map((reason, idx) => (
<button
key={idx}
type="button"
className={twMerge(
'body-m rounded-full bg-white px-5 py-1.5 text-black',
selected === reason && 'bg-primary-2',
postReportRequest.reasonType === reason.type && 'bg-primary-2',
)}
onClick={() => handleReasonClick(reason)}
onClick={() => handleReasonClick(reason.type)}
>
{reason}
{reason.name}
</button>
))}
</section>
<TextareaField
rows={3}
placeholder="이곳을 눌러 추가 사유를 작성해주세요"
value={additionalReason}
onChange={(e) => setAdditionalReason(e.target.value)}
value={postReportRequest.reason}
onChange={(e) => setPostReportRequest((cur) => ({ ...cur, reason: e.target.value }))}
/>
</ConfirmModal>
);
Expand Down
34 changes: 19 additions & 15 deletions src/pages/Admin/FilteredLetter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AlarmIcon } from '@/assets/icons';

import AdminPageTitle from './components/AdminPageTitle';
import FilteredLetterListItem from './components/FilteredLetterListItem';
import ListHeaderFrame from './components/ListHeaderFrame';
import WrapperFrame from './components/WrapperFrame';
Expand All @@ -8,20 +9,23 @@ import WrapperTitle from './components/WrapperTitle';
export default function FilteredLetterManage() {
const arr = new Array(10).fill(null);
return (
<WrapperFrame>
<WrapperTitle title="필터링 단어로 차단된 편지 목록" Icon={AlarmIcon} />
<section className="mt-5 flex flex-col">
<ListHeaderFrame>
<span className="admin-list-set basis-1/10">ID</span>
<span className="admin-list-set basis-2/10">제보자 이메일</span>
<span className="admin-list-set basis-2/10">작성자 이메일</span>
<span className="admin-list-set basis-2/10">차단 일자</span>
<span className="admin-list-set basis-2/10">포함된 단어</span>
</ListHeaderFrame>
{arr.map((_, idx) => {
return <FilteredLetterListItem key={idx} />;
})}
</section>
</WrapperFrame>
<>
<AdminPageTitle>검열 관리 / 차단된 편지 목록</AdminPageTitle>
<WrapperFrame>
<WrapperTitle title="필터링 단어로 차단된 편지 목록" Icon={AlarmIcon} />
<section className="mt-5 flex flex-col">
<ListHeaderFrame>
<span className="admin-list-set basis-1/10">ID</span>
<span className="admin-list-set basis-2/10">제보자 이메일</span>
<span className="admin-list-set basis-2/10">작성자 이메일</span>
<span className="admin-list-set basis-2/10">차단 일자</span>
<span className="admin-list-set basis-2/10">포함된 단어</span>
</ListHeaderFrame>
{arr.map((_, idx) => {
return <FilteredLetterListItem key={idx} />;
})}
</section>
</WrapperFrame>
</>
);
}
64 changes: 34 additions & 30 deletions src/pages/Admin/Filtering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getBadWords } from '@/apis/admin';
import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons';

import AddInputButton from './components/AddInputButton';
import AdminPageTitle from './components/AdminPageTitle';
import WrapperFrame from './components/WrapperFrame';
import WrapperTitle from './components/WrapperTitle';

Expand All @@ -15,37 +16,40 @@ export default function FilteringManage() {
getBadWords(setBadWords);
}, []);
return (
<WrapperFrame>
<WrapperTitle title="필터링 단어 설정" Icon={AlarmIcon} />
<div className="mt-5 flex w-full flex-wrap gap-4">
{badWords.map((badWord, idx) => {
return (
<span
key={idx}
className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5"
>
{badWord.word}
<button>
<CancelIcon className="h-4 w-4" />
<>
<AdminPageTitle>검열 관리 / 필터링 단어 설정</AdminPageTitle>
<WrapperFrame>
<WrapperTitle title="필터링 단어" Icon={AlarmIcon} />
<div className="mt-5 flex w-full flex-wrap gap-4">
{badWords.map((badWord, idx) => {
return (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소한 거지만 {} 대신 JSX만 반환하도록 하고 () 를 사용하면 더 가독성이 좋을 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 맞다 그 방법이 더 좋은거 같네요!! 다음에 바꾸겠습니당🥕🥕

<span
key={idx}
className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5"
>
{badWord.word}
<button>
<CancelIcon className="h-4 w-4" />
</button>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클릭했을 때의 동작이 필요할 것 같습니다! 추후 개발 예정이실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넹 필터링 파트는 아직 api 준비중인 것도 있어서 잠시 미뤄뒀습니다!

</span>
);
})}
{addInputShow ? (
<AddInputButton setAddInputShow={setAddInputShow} setBadWords={setBadWords} />
) : (
<span className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5">
추가하기
<button
onClick={() => {
setAddInputShow(true);
}}
>
<AddIcon className="h-4 w-4" />
</button>
</span>
);
})}
{addInputShow ? (
<AddInputButton setAddInputShow={setAddInputShow} setBadWords={setBadWords} />
) : (
<span className="flex items-center gap-1.5 rounded-2xl bg-[#C1C1C1] px-4 py-1.5">
추가하기
<button
onClick={() => {
setAddInputShow(true);
}}
>
<AddIcon className="h-4 w-4" />
</button>
</span>
)}
</div>
</WrapperFrame>
)}
</div>
</WrapperFrame>
</>
);
}
Loading