From 0bcc25cc7ecb40f687043cab2a770f25ff0eef70 Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Tue, 25 Feb 2025 18:13:08 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8B=A0=EA=B3=A0=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=EC=99=84=EB=A3=8C(=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/admin.ts | 39 +++++ src/pages/Admin/Report.tsx | 143 ++++-------------- src/pages/Admin/admin.d.ts | 31 ---- src/pages/Admin/components/DetailFrame.tsx | 24 ++- src/pages/Admin/components/MenuModal.tsx | 24 ++- .../Admin/components/ReportDetailModal.tsx | 24 +++ .../Admin/components/ReportHandlingModal.tsx | 76 ++++++++++ src/pages/Admin/components/ReportListItem.tsx | 78 ++++++++++ src/pages/Admin/components/WrapperTitle.tsx | 13 ++ src/styles/components.css | 9 ++ src/types/admin.d.ts | 55 +++++++ 11 files changed, 368 insertions(+), 148 deletions(-) create mode 100644 src/apis/admin.ts delete mode 100644 src/pages/Admin/admin.d.ts create mode 100644 src/pages/Admin/components/ReportDetailModal.tsx create mode 100644 src/pages/Admin/components/ReportHandlingModal.tsx create mode 100644 src/pages/Admin/components/ReportListItem.tsx create mode 100644 src/pages/Admin/components/WrapperTitle.tsx create mode 100644 src/types/admin.d.ts diff --git a/src/apis/admin.ts b/src/apis/admin.ts new file mode 100644 index 0000000..6164b58 --- /dev/null +++ b/src/apis/admin.ts @@ -0,0 +1,39 @@ +import client from './client'; + +const getReports = async ( + setReports: React.Dispatch>, + queryString: string = '', + callBack?: () => void, +) => { + try { + const res = await client.get(`/api/reports${queryString}`); + setReports(res.data.data); + if (callBack) callBack(); + console.log(res.data.data); + } catch (error) { + console.error(error); + } +}; + +const patchReport = async (reportId: number, reportRequest: ReportRequest) => { + try { + console.log(`/api/reports/${reportId}`, reportRequest); + const res = await client.patch(`/api/reports/${reportId}`, reportRequest); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +// +const getBadWords = async (setBadWords: React.Dispatch>) => { + try { + const res = await client.get('/api/bad-words'); + setBadWords(res.data.data); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +export { getReports, patchReport, getBadWords }; diff --git a/src/pages/Admin/Report.tsx b/src/pages/Admin/Report.tsx index 48f85bb..f59e1ac 100644 --- a/src/pages/Admin/Report.tsx +++ b/src/pages/Admin/Report.tsx @@ -1,138 +1,59 @@ import { useEffect, useState } from 'react'; -import client from '@/apis/client'; +import { getReports } from '@/apis/admin'; import { AlarmIcon } from '@/assets/icons'; -import DetailFrame from './components/DetailFrame'; -import ListItem from './components/ListItem'; +import ReportDetailModal from './components/ReportDetailModal'; +import ReportHandlingModal from './components/ReportHandlingModal'; +import ReportListItem from './components/ReportListItem'; import WrapperFrame from './components/WrapperFrame'; +import WrapperTitle from './components/WrapperTitle'; export default function ReportManage() { const [detailModalOpen, setDetailModalOpen] = useState(false); - // { - // id: '001', - // reporterEmail: 'user1@gmail.com', - // targetEmail: 'user22@gmail.com', - // reportedAt: new Date(2025, 1, 20), - // letterId:2001, - // sharePostId:null, - // eventId:null, - // reportType:'LETTER', - // reason:"ABUSE", - // reasonDetail:null, - // status: 'PENDING', - // }, - const DUMMY = [ - { - id: '001', - reporterEmail: 'user1fawfaws@gmail.com', - targetEmail: 'faw5f1a5w6@gmail.com', - reportedAt: new Date(2020, 12, 4), - reason: '욕설', - }, - { - id: '002', - reporterEmail: 'user1fawfaws@gmail.com', - targetEmail: 'faw5f1a5w6@gmail.com', - reportedAt: new Date(2020, 12, 4), - reason: '욕설', - }, - { - id: '003', - reporterEmail: 'fa5w6f1a5f1w6@gmail.com', - targetEmail: 'afwf@gmail.com', - reportedAt: new Date(2000, 6, 23), - reason: '욕설', - }, - { - id: '004', - reporterEmail: 'a@gmail.com', - targetEmail: 'a1f515wa6@gmail.com', - reportedAt: new Date(1080, 11, 5), - reason: '욕설', - }, - { - id: '005', - reporterEmail: 'aa@gmail.com', - targetEmail: 'w@gmail.com', - reportedAt: new Date(2040, 1, 2), - reason: '욕설', - }, - { - id: '006', - reporterEmail: 'a5w1f65a@gmail.com', - targetEmail: 'aw1f56a1f5aw16@gmail.com', - reportedAt: new Date(2025, 1, 23), - reason: '욕설', - }, - ]; - const modalContents = [ - { - title: '신고 목록 삭제', - onClick: () => { - console.log('삭제'); - }, - }, - { - title: '작성자 활동 정지', - onClick: () => { - console.log('정지'); - }, - }, - ]; + const [handleModalOpen, setHandleModalOpen] = useState(false); + const [reports, setReports] = useState([]); + const [selectedReport, setSelectReport] = useState(null); + const [selectedReportId, setSelectedReportId] = useState(null); + // const [allReports, setAllReports] = useState(); useEffect(() => { - const getAllReports = async () => { - const res = await client.get('/api/reports'); - console.log(res); - }; - getAllReports(); - const getReportDetail = async () => { - const res = await client.get('/api/reports/2'); - console.log(res); - }; - getReportDetail(); - const postReport = async () => { - const res = await client.post('/api/reports', { - letterId: 2010, - reportType: 'POST', - reason: 'HARASSMENT', - reasonDetail: '테스트용', - }); - console.log(res); - }; - postReport(); + getReports(setReports, '?status=PENDING'); }, []); return ( - - 신고 편지 목록 - + +
- ID - 제보자 이메일 - 작성자 이메일 - 제보 일자 - 제보 사유 + ID + 제보자 이메일 + 작성자 이메일 + 제보 일자 + 제보 사유
- {DUMMY.map((data, idx) => ( - ( + ))}
{detailModalOpen && ( - - <> - 제보 편지 상세 조회 - - + + )} + {handleModalOpen && ( + )}
); diff --git a/src/pages/Admin/admin.d.ts b/src/pages/Admin/admin.d.ts deleted file mode 100644 index e50f82d..0000000 --- a/src/pages/Admin/admin.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -interface Report { - id: number; - reporterEmail: string; - targetEmail: string; - reportedAt: Date; - letterId: number | null; - comment: string | null; - sharePostId: number | null; - reportType: 'LETTER' | 'POST' | 'COMMENT'; - reason: 'ABUSE' | 'DEFAMATION' | 'HARASSMENT' | 'THREATS' | 'ETC'; - reasonDetail: string | null; - status: 'PENDING' | 'RESOLVED' | 'REJECTED'; -} - -interface ReportDetail { - id: number; - memberId: number | null; - letterId: number | null; - sharePostId: number | null; - eventId: number | null; - reportType: 'LETTER' | 'POST' | 'COMMENT' | 'EVENT'; - reason: 'ABUSE' | 'DEFAMATION' | 'HARASSMENT' | 'THREATS' | 'ETC'; - reasonDetail: string | null; - status: 'PENDING' | 'RESOLVED' | 'REJECTED'; - reportedAt: Date; - createdAt: Date; - letterDetail: { - title: string; - content: string; - }; -} diff --git a/src/pages/Admin/components/DetailFrame.tsx b/src/pages/Admin/components/DetailFrame.tsx index 505d494..f6bf7e9 100644 --- a/src/pages/Admin/components/DetailFrame.tsx +++ b/src/pages/Admin/components/DetailFrame.tsx @@ -1,4 +1,6 @@ -import { ReactNode, useRef } from 'react'; +import { ReactNode, useEffect, useRef } from 'react'; + +import { getScrollbarWidth } from '@/utils/getScrollbarWidth'; export default function DetailFrame({ children, @@ -9,16 +11,30 @@ export default function DetailFrame({ }) { const contentRef = useRef(null); const handleOutsideClick = (e: React.MouseEvent) => { - if (e.target !== contentRef.current) closeEvent(false); + const target = e.target as HTMLElement; + if (!contentRef.current?.contains(target)) closeEvent(false); }; + + useEffect(() => { + // 대황민하님 스크롤바 넓이 측정기 + const scrollbarWidth = getScrollbarWidth(); + + document.body.style.overflowY = 'hidden'; + document.body.style.marginRight = `${scrollbarWidth}px`; + + return () => { + document.body.style.overflowY = ''; + document.body.style.marginRight = `0px`; + }; + }, []); return (
handleOutsideClick(e)} >
{children}
diff --git a/src/pages/Admin/components/MenuModal.tsx b/src/pages/Admin/components/MenuModal.tsx index ea900eb..924baf9 100644 --- a/src/pages/Admin/components/MenuModal.tsx +++ b/src/pages/Admin/components/MenuModal.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react'; + interface ModalContents { title: string; onClick: () => void; @@ -9,12 +11,30 @@ export default function MenuModal({ modalContents: ModalContents[]; setModalOpen: React.Dispatch>; }) { + const divRef = useRef(null); + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!divRef.current?.contains(target)) { + setModalOpen(false); + } + }; + document.body.addEventListener('click', handleOutsideClick); + return () => { + document.body.removeEventListener('click', handleOutsideClick); + }; + }, [setModalOpen]); + return ( -
+
{modalContents.map((content, idx) => ( + +
+
+ + ); +} diff --git a/src/pages/Admin/components/ReportListItem.tsx b/src/pages/Admin/components/ReportListItem.tsx new file mode 100644 index 0000000..eab3125 --- /dev/null +++ b/src/pages/Admin/components/ReportListItem.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; + +import { KebobMenuIcon } from '@/assets/icons'; + +import MenuModal from './MenuModal'; + +export default function ReportListItem({ + report, + setDetailModalOpen, + setSelectedReportId, + setHandleModalOpen, + setSelectReport, +}: { + report: Report; + setDetailModalOpen: React.Dispatch>; + setSelectedReportId: React.Dispatch>; + setHandleModalOpen: React.Dispatch>; + setSelectReport: React.Dispatch>; +}) { + const [modalOpen, setModalOpen] = useState(false); + + const dateString = report.reportedAt; + const date = new Date(dateString); + const formattedDate = date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + const formattedTime = date.toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + + const modalContents: ModalContents[] = [ + { + title: '신고 처리', + onClick: () => { + setSelectedReportId(report.id); + setHandleModalOpen(true); + }, + }, + ]; + + const reasonList = { + ABUSE: '욕설', + DEFAMATION: '비방', + HARASSMENT: '성희롱', + THREATS: '폭언', + ETC: '기타', + }; + return ( +
{ + setSelectReport(report); + setDetailModalOpen(true); + }} + > +
+ {report.id} + {report.reporterEmail} + {report.targetEmail} + {`${formattedDate} ${formattedTime}`} + {reasonList[report.reason]} +
+ + {modalOpen && } +
+ ); +} diff --git a/src/pages/Admin/components/WrapperTitle.tsx b/src/pages/Admin/components/WrapperTitle.tsx new file mode 100644 index 0000000..6c2eaff --- /dev/null +++ b/src/pages/Admin/components/WrapperTitle.tsx @@ -0,0 +1,13 @@ +export default function WrapperTitle({ + title, + Icon, +}: { + title: string; + Icon: React.FunctionComponent>; +}) { + return ( + + {title} + + ); +} diff --git a/src/styles/components.css b/src/styles/components.css index 74af716..90ea6ce 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -36,4 +36,13 @@ .message-header { @apply text-gray-60 w-fit rounded-full bg-white px-6 py-4; } + + .admin-list-set { + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } } diff --git a/src/types/admin.d.ts b/src/types/admin.d.ts new file mode 100644 index 0000000..b78f7cf --- /dev/null +++ b/src/types/admin.d.ts @@ -0,0 +1,55 @@ +type Status = 'PENDING' | 'RESOLVED' | 'REJECTED'; +type reportType = 'LETTER' | 'POST' | 'COMMENT'; +type Reason = 'ABUSE' | 'DEFAMATION' | 'HARASSMENT' | 'THREATS' | 'ETC'; + +interface Report { + id: number; + reporterEmail: string; + targetEmail: string; + reportedAt: Date; + letterId: number | null; + comment: string | null; + sharePostId: number | null; + reportType: reportType; + reason: Reason; + reasonDetail: string | null; + status: Status; + letterDetail: { + title: string | null; + content: string; + }; +} + +interface ReportRequest { + status: 'RESOLVED' | 'REJECTED'; + adminMemo: string; +} + +// interface ReportDetail { +// id: number; +// memberId: number | null; +// letterId: number | null; +// sharePostId: number | null; +// eventId: number | null; +// reportType: 'LETTER' | 'POST' | 'COMMENT' | 'EVENT'; +// reason: 'ABUSE' | 'DEFAMATION' | 'HARASSMENT' | 'THREATS' | 'ETC'; +// reasonDetail: string | null; +// status: 'PENDING' | 'RESOLVED' | 'REJECTED'; +// reportedAt: Date; +// createdAt: Date; +// letterDetail: { +// title: string; +// content: string; +// }; +// } + +// badWords +interface BadWords { + word: string; +} + +// +interface ModalContents { + title: string; + onClick: () => void; +} From ed6b383aa9fb6e5341b6a47e74729bf802a8c05c Mon Sep 17 00:00:00 2001 From: "wl990@naver.com" Date: Tue, 25 Feb 2025 18:14:07 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B2=80=EC=97=B4=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=A4=91(=EA=B2=80=EC=97=B4=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=EC=A4=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 + src/assets/icons/add.svg | 3 ++ src/assets/icons/cancel.svg | 3 ++ src/assets/icons/index.ts | 4 ++ src/pages/Admin/BadWords.tsx | 53 +++++++++++++++++++ src/pages/Admin/components/AddInputButton.tsx | 52 ++++++++++++++++++ src/pages/Admin/components/ListItem.tsx | 43 --------------- src/pages/Admin/constants/index.ts | 0 8 files changed, 117 insertions(+), 43 deletions(-) create mode 100644 src/assets/icons/add.svg create mode 100644 src/assets/icons/cancel.svg create mode 100644 src/pages/Admin/BadWords.tsx create mode 100644 src/pages/Admin/components/AddInputButton.tsx delete mode 100644 src/pages/Admin/components/ListItem.tsx create mode 100644 src/pages/Admin/constants/index.ts diff --git a/src/App.tsx b/src/App.tsx index ddc86c0..1585b43 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import useViewport from './hooks/useViewport'; import Layout from './layouts/Layout'; import MobileLayout from './layouts/MobileLayout'; import AdminPage from './pages/Admin'; +import BadWordsManage from './pages/Admin/BadWords'; import ReportManage from './pages/Admin/Report'; import Home from './pages/Home'; import Landing from './pages/Landing'; @@ -57,6 +58,7 @@ const App = () => { }> } /> + } /> ); diff --git a/src/assets/icons/add.svg b/src/assets/icons/add.svg new file mode 100644 index 0000000..db9adc1 --- /dev/null +++ b/src/assets/icons/add.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/cancel.svg b/src/assets/icons/cancel.svg new file mode 100644 index 0000000..b103417 --- /dev/null +++ b/src/assets/icons/cancel.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index d0f09f4..2d86c84 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,7 +1,9 @@ +import AddIcon from './add.svg?react'; import AlarmIcon from './alarm.svg?react'; import ArrowDownIcon from './arrow-down.svg?react'; import ArrowLeftIcon from './arrow-left.svg?react'; import BoardIcon from './board.svg?react'; +import CancelIcon from './cancel.svg?react'; import CheckIcon from './check.svg?react'; import CloudIcon from './cloud.svg?react'; import DeleteIcon from './delete.svg?react'; @@ -24,6 +26,7 @@ import ThermostatIcon from './thermostat.svg?react'; import WarmIcon from './warm.svg?react'; export { + AddIcon, NaverIcon, KakaoIcon, GoogleIcon, @@ -48,4 +51,5 @@ export { LikeFilledIcon, LikeOutlinedIcon, DeleteIcon, + CancelIcon, }; diff --git a/src/pages/Admin/BadWords.tsx b/src/pages/Admin/BadWords.tsx new file mode 100644 index 0000000..2874d07 --- /dev/null +++ b/src/pages/Admin/BadWords.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from 'react'; + +import { getBadWords } from '@/apis/admin'; +import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons'; + +import AddInputButton from './components/AddInputButton'; +import WrapperFrame from './components/WrapperFrame'; +import WrapperTitle from './components/WrapperTitle'; + +const BadWordsManage = () => { + const [badWords, setBadWords] = useState([]); + const [addInputShow, setAddInputShow] = useState(false); + + useEffect(() => { + getBadWords(setBadWords); + }, []); + return ( + + +
+ {badWords.map((badWord, idx) => { + return ( + + {badWord.word} + + + ); + })} + {addInputShow ? ( + + ) : ( + + 추가하기 + + + )} +
+
+ ); +}; + +export default BadWordsManage; diff --git a/src/pages/Admin/components/AddInputButton.tsx b/src/pages/Admin/components/AddInputButton.tsx new file mode 100644 index 0000000..301377e --- /dev/null +++ b/src/pages/Admin/components/AddInputButton.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; + +import { AddIcon } from '@/assets/icons'; + +export default function AddInputButton({ + setAddInputShow, +}: { + setAddInputShow: React.Dispatch>; +}) { + const inputRef = useRef(null); + const handleInputWidth = (event: Event) => { + const target = event.target as HTMLInputElement; + target.style.width = '50px'; + target.style.width = `${target.scrollWidth}px`; + }; + + useEffect(() => { + const inputElement = inputRef.current; + if (inputElement) { + inputElement.addEventListener('input', handleInputWidth); + inputElement.focus(); + } + + return () => { + if (inputElement) { + inputElement.removeEventListener('input', handleInputWidth); + } + }; + }, []); + + return ( + + { + if (e.key === 'Enter') { + setAddInputShow(false); + } + }} + /> + + + ); +} diff --git a/src/pages/Admin/components/ListItem.tsx b/src/pages/Admin/components/ListItem.tsx deleted file mode 100644 index a9eca5b..0000000 --- a/src/pages/Admin/components/ListItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useState } from 'react'; - -import { KebobMenuIcon } from '@/assets/icons'; - -import MenuModal from './MenuModal'; - -interface DataProps { - id: string; - reporterEmail: string; - targetEmail: string; - reportedAt: Date; - reason: string; -} -interface ModalContents { - title: string; - onClick: () => void; -} -export default function ListItem({ - data, - modalContents, - setDetailModalOpen, -}: { - data: DataProps; - modalContents: ModalContents[]; - setDetailModalOpen: React.Dispatch>; -}) { - const [modalOpen, setModalOpen] = useState(false); - return ( -
-
setDetailModalOpen(true)}> - {data.id} - {data.reporterEmail} - {data.targetEmail} - {`${data.reportedAt.getFullYear()}.${data.reportedAt.getMonth()}.${data.reportedAt.getDay()}`} - {data.reason} -
- - {modalOpen && } -
- ); -} diff --git a/src/pages/Admin/constants/index.ts b/src/pages/Admin/constants/index.ts new file mode 100644 index 0000000..e69de29 From 61b58f35d3c26809e431f6c8a216991180e50537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Tue, 25 Feb 2025 22:18:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=8B=A8=EC=96=B4=20=EC=B6=94=EA=B0=80=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9api=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C(=EB=8B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=82=AD=EC=A0=9C=EB=8A=94=20=EB=AA=A9api=EA=B0=80?= =?UTF-8?q?=20=EC=95=88=EB=A7=8C=EB=93=A4=EC=96=B4=EC=A7=90)=20+=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20=ED=8E=B8=EC=A7=80=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/admin.ts | 29 ++++++++++- src/pages/Admin/BadWords.tsx | 52 +++---------------- src/pages/Admin/FilteredLetter.tsx | 27 ++++++++++ src/pages/Admin/Filtering.tsx | 51 ++++++++++++++++++ src/pages/Admin/Report.tsx | 17 +++--- src/pages/Admin/components/AddInputButton.tsx | 34 ++++++++---- .../components/FilteredLetterListItem.tsx | 23 ++++++++ .../Admin/components/ListHeaderFrame.tsx | 9 ++++ src/pages/Admin/components/WrapperFrame.tsx | 16 +++++- 9 files changed, 188 insertions(+), 70 deletions(-) create mode 100644 src/pages/Admin/FilteredLetter.tsx create mode 100644 src/pages/Admin/Filtering.tsx create mode 100644 src/pages/Admin/components/FilteredLetterListItem.tsx create mode 100644 src/pages/Admin/components/ListHeaderFrame.tsx diff --git a/src/apis/admin.ts b/src/apis/admin.ts index 6164b58..53ec1f3 100644 --- a/src/apis/admin.ts +++ b/src/apis/admin.ts @@ -25,7 +25,7 @@ const patchReport = async (reportId: number, reportRequest: ReportRequest) => { } }; -// +// badwords const getBadWords = async (setBadWords: React.Dispatch>) => { try { const res = await client.get('/api/bad-words'); @@ -36,4 +36,29 @@ const getBadWords = async (setBadWords: React.Dispatch void) => { + try { + const res = await client.post('/api/bad-words', badWordsRequest); + if (callBack) callBack(); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +// 내 상상대로 만든 필터링 단어 취소 버튼 +const patchBadWords = async ( + badWordId: number, + badWordsRequest: BadWords, + callBack?: () => void, +) => { + try { + const res = await client.patch(`/api/bad-words/${badWordId}/status`, badWordsRequest); + if (callBack) callBack(); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +export { getReports, patchReport, getBadWords, postBadWords, patchBadWords }; diff --git a/src/pages/Admin/BadWords.tsx b/src/pages/Admin/BadWords.tsx index 2874d07..6dcd164 100644 --- a/src/pages/Admin/BadWords.tsx +++ b/src/pages/Admin/BadWords.tsx @@ -1,52 +1,12 @@ -import { useEffect, useState } from 'react'; - -import { getBadWords } from '@/apis/admin'; -import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons'; - -import AddInputButton from './components/AddInputButton'; -import WrapperFrame from './components/WrapperFrame'; -import WrapperTitle from './components/WrapperTitle'; +import FilteredLetter from './FilteredLetter'; +import Filtering from './Filtering'; const BadWordsManage = () => { - const [badWords, setBadWords] = useState([]); - const [addInputShow, setAddInputShow] = useState(false); - - useEffect(() => { - getBadWords(setBadWords); - }, []); return ( - - -
- {badWords.map((badWord, idx) => { - return ( - - {badWord.word} - - - ); - })} - {addInputShow ? ( - - ) : ( - - 추가하기 - - - )} -
-
+ <> + + + ); }; diff --git a/src/pages/Admin/FilteredLetter.tsx b/src/pages/Admin/FilteredLetter.tsx new file mode 100644 index 0000000..8f60ded --- /dev/null +++ b/src/pages/Admin/FilteredLetter.tsx @@ -0,0 +1,27 @@ +import { AlarmIcon } from '@/assets/icons'; + +import FilteredLetterListItem from './components/FilteredLetterListItem'; +import ListHeaderFrame from './components/ListHeaderFrame'; +import WrapperFrame from './components/WrapperFrame'; +import WrapperTitle from './components/WrapperTitle'; + +export default function FilteredLetter() { + const arr = new Array(10).fill(null); + return ( + + +
+ + ID + 제보자 이메일 + 작성자 이메일 + 차단 일자 + 포함된 단어 + + {arr.map((_, idx) => { + return ; + })} +
+
+ ); +} diff --git a/src/pages/Admin/Filtering.tsx b/src/pages/Admin/Filtering.tsx new file mode 100644 index 0000000..02e81ef --- /dev/null +++ b/src/pages/Admin/Filtering.tsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; + +import { getBadWords } from '@/apis/admin'; +import { AddIcon, AlarmIcon, CancelIcon } from '@/assets/icons'; + +import AddInputButton from './components/AddInputButton'; +import WrapperFrame from './components/WrapperFrame'; +import WrapperTitle from './components/WrapperTitle'; + +export default function Filtering() { + const [badWords, setBadWords] = useState([]); + const [addInputShow, setAddInputShow] = useState(false); + + useEffect(() => { + getBadWords(setBadWords); + }, []); + return ( + + +
+ {badWords.map((badWord, idx) => { + return ( + + {badWord.word} + + + ); + })} + {addInputShow ? ( + + ) : ( + + 추가하기 + + + )} +
+
+ ); +} diff --git a/src/pages/Admin/Report.tsx b/src/pages/Admin/Report.tsx index f59e1ac..bbd6f74 100644 --- a/src/pages/Admin/Report.tsx +++ b/src/pages/Admin/Report.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { getReports } from '@/apis/admin'; import { AlarmIcon } from '@/assets/icons'; +import ListHeaderFrame from './components/ListHeaderFrame'; import ReportDetailModal from './components/ReportDetailModal'; import ReportHandlingModal from './components/ReportHandlingModal'; import ReportListItem from './components/ReportListItem'; @@ -25,15 +26,13 @@ export default function ReportManage() {
-
-
- ID - 제보자 이메일 - 작성자 이메일 - 제보 일자 - 제보 사유 -
-
+ + ID + 제보자 이메일 + 작성자 이메일 + 제보 일자 + 제보 사유 + {reports.map((data, idx) => ( >; + setBadWords: React.Dispatch>; }) { + const [inputText, setInputText] = useState({ word: '' }); const inputRef = useRef(null); - const handleInputWidth = (event: Event) => { + + const handleInputWidth = (event: React.FormEvent) => { const target = event.target as HTMLInputElement; target.style.width = '50px'; target.style.width = `${target.scrollWidth}px`; }; + const handlePostBadWords = () => { + if (inputText.word === '') return setAddInputShow(false); + postBadWords(inputText, () => { + setBadWords((cur) => [...cur, inputText]); + setAddInputShow(false); + }); + }; + useEffect(() => { const inputElement = inputRef.current; if (inputElement) { - inputElement.addEventListener('input', handleInputWidth); inputElement.focus(); } - - return () => { - if (inputElement) { - inputElement.removeEventListener('input', handleInputWidth); - } - }; }, []); return ( @@ -34,15 +40,21 @@ export default function AddInputButton({ ref={inputRef} type="text" className="w-10 min-w-10" + onInput={(e) => { + handleInputWidth(e); + }} + onChange={(e) => { + setInputText(() => ({ word: e.target.value })); + }} onKeyDown={(e) => { if (e.key === 'Enter') { - setAddInputShow(false); + handlePostBadWords(); } }} /> + {/* {modalOpen && } */} +
+ ); +} diff --git a/src/pages/Admin/components/ListHeaderFrame.tsx b/src/pages/Admin/components/ListHeaderFrame.tsx new file mode 100644 index 0000000..59e084e --- /dev/null +++ b/src/pages/Admin/components/ListHeaderFrame.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; + +export default function ListHeaderFrame({ children }: { children: ReactNode }) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/pages/Admin/components/WrapperFrame.tsx b/src/pages/Admin/components/WrapperFrame.tsx index 4c87de5..b4a6db3 100644 --- a/src/pages/Admin/components/WrapperFrame.tsx +++ b/src/pages/Admin/components/WrapperFrame.tsx @@ -1,8 +1,20 @@ import { ReactNode } from 'react'; +import { twMerge } from 'tailwind-merge'; -export default function WrapperFrame({ children }: { children: ReactNode }) { +export default function WrapperFrame({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { return ( -
+
{children}
); From 380de17ea13c0d84b7ee78e142770e2ca58414af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=A7=80=EC=9B=90?= Date: Tue, 25 Feb 2025 22:28:26 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:badwords,=20filteredLetter=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EB=B6=84=EB=A6=AC=20+=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=97=90=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 6 ++++-- src/pages/Admin/BadWords.tsx | 13 ------------- src/pages/Admin/FilteredLetter.tsx | 4 ++-- src/pages/Admin/Filtering.tsx | 2 +- src/pages/Admin/index.tsx | 18 ++++++++++++++---- 5 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 src/pages/Admin/BadWords.tsx diff --git a/src/App.tsx b/src/App.tsx index 1585b43..cd564f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,8 @@ import useViewport from './hooks/useViewport'; import Layout from './layouts/Layout'; import MobileLayout from './layouts/MobileLayout'; import AdminPage from './pages/Admin'; -import BadWordsManage from './pages/Admin/BadWords'; +import FilteredLetterManage from './pages/Admin/FilteredLetter'; +import FilteringManage from './pages/Admin/Filtering'; import ReportManage from './pages/Admin/Report'; import Home from './pages/Home'; import Landing from './pages/Landing'; @@ -58,7 +59,8 @@ const App = () => { }> } /> - } /> + } /> + } /> ); diff --git a/src/pages/Admin/BadWords.tsx b/src/pages/Admin/BadWords.tsx deleted file mode 100644 index 6dcd164..0000000 --- a/src/pages/Admin/BadWords.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import FilteredLetter from './FilteredLetter'; -import Filtering from './Filtering'; - -const BadWordsManage = () => { - return ( - <> - - - - ); -}; - -export default BadWordsManage; diff --git a/src/pages/Admin/FilteredLetter.tsx b/src/pages/Admin/FilteredLetter.tsx index 8f60ded..0390d61 100644 --- a/src/pages/Admin/FilteredLetter.tsx +++ b/src/pages/Admin/FilteredLetter.tsx @@ -5,10 +5,10 @@ import ListHeaderFrame from './components/ListHeaderFrame'; import WrapperFrame from './components/WrapperFrame'; import WrapperTitle from './components/WrapperTitle'; -export default function FilteredLetter() { +export default function FilteredLetterManage() { const arr = new Array(10).fill(null); return ( - +
diff --git a/src/pages/Admin/Filtering.tsx b/src/pages/Admin/Filtering.tsx index 02e81ef..4967629 100644 --- a/src/pages/Admin/Filtering.tsx +++ b/src/pages/Admin/Filtering.tsx @@ -7,7 +7,7 @@ import AddInputButton from './components/AddInputButton'; import WrapperFrame from './components/WrapperFrame'; import WrapperTitle from './components/WrapperTitle'; -export default function Filtering() { +export default function FilteringManage() { const [badWords, setBadWords] = useState([]); const [addInputShow, setAddInputShow] = useState(false); diff --git a/src/pages/Admin/index.tsx b/src/pages/Admin/index.tsx index 681164f..08cb69d 100644 --- a/src/pages/Admin/index.tsx +++ b/src/pages/Admin/index.tsx @@ -1,8 +1,9 @@ -import { Outlet } from 'react-router'; +import { Outlet, useNavigate } from 'react-router'; import { AlarmIcon, ArrowDownIcon } from '@/assets/icons'; const AdminPage = () => { + const navigate = useNavigate(); return (
@@ -52,13 +53,22 @@ const AdminPage = () => {
- - -