diff --git a/src/App.tsx b/src/App.tsx index e6148da..b039ae8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,9 @@ import { Route, Routes } from 'react-router'; import useViewport from './hooks/useViewport'; import Layout from './layouts/Layout'; +import MobileLayout from './layouts/MobileLayout'; +import AdminPage from './pages/Admin'; +import ReportManage from './pages/Admin/Report'; import Home from './pages/Home'; import Landing from './pages/Landing'; import LetterBoardPage from './pages/LetterBoard'; @@ -22,7 +25,7 @@ const App = () => { return ( - + }> } /> } /> } /> @@ -49,6 +52,9 @@ const App = () => { } /> + }> + } /> + ); }; diff --git a/src/apis/letterDetail.ts b/src/apis/letterDetail.ts new file mode 100644 index 0000000..cefd391 --- /dev/null +++ b/src/apis/letterDetail.ts @@ -0,0 +1,26 @@ +import { client } from './client'; + +const getLetter = async ( + letterId: string, + setLetterState: React.Dispatch>, +) => { + try { + const res = await client.get(`/api/letters/${letterId}`); + setLetterState(res.data.data); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +const deleteLetter = async (letterId: string) => { + try { + console.log(`/api/letters/${letterId}`); + const res = await client.delete(`/api/letters/${letterId}`); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +export { getLetter, deleteLetter }; diff --git a/src/apis/write.ts b/src/apis/write.ts new file mode 100644 index 0000000..b8e950a --- /dev/null +++ b/src/apis/write.ts @@ -0,0 +1,29 @@ +import { client } from './client'; + +const postLetter = async ( + data: LetterRequest, + setState?: React.Dispatch>, +) => { + try { + const res = await client.post('/api/letters', data); + if (setState) setState(true); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +const getPrevLetter = async ( + setPrevLetterState: React.Dispatch>, + letterId: string, +) => { + try { + const res = await client.get(`/api/letters/${letterId}/previous`); + setPrevLetterState(res.data.data); + console.log(res); + } catch (error) { + console.error(error); + } +}; + +export { postLetter, getPrevLetter }; diff --git a/src/assets/icons/arrow-down.svg b/src/assets/icons/arrow-down.svg new file mode 100644 index 0000000..1076c2b --- /dev/null +++ b/src/assets/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 0000000..d7b785a --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 56e7f98..d0f09f4 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,12 +1,15 @@ 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 CheckIcon from './check.svg?react'; import CloudIcon from './cloud.svg?react'; import DeleteIcon from './delete.svg?react'; import EnvelopeIcon from './envelope.svg?react'; import GoogleIcon from './google.svg?react'; import InformationIcon from './information.svg?react'; import KakaoIcon from './kakao.svg?react'; +import KebobMenuIcon from './kebob-menu.svg?react'; import LikeFilledIcon from './like-filled.svg?react'; import LikeOutlinedIcon from './like-outlined.svg?react'; import NaverIcon from './naver.svg?react'; @@ -26,9 +29,12 @@ export { GoogleIcon, StampIcon, AlarmIcon, + CheckIcon, + ArrowDownIcon, PersonIcon, ArrowLeftIcon, InformationIcon, + KebobMenuIcon, EnvelopeIcon, BoardIcon, RestartIcon, diff --git a/src/assets/icons/kebob-menu.svg b/src/assets/icons/kebob-menu.svg new file mode 100644 index 0000000..32857ac --- /dev/null +++ b/src/assets/icons/kebob-menu.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx new file mode 100644 index 0000000..4a9815a --- /dev/null +++ b/src/components/BackButton.tsx @@ -0,0 +1,12 @@ +import { useNavigate } from 'react-router'; + +import { ArrowLeftIcon } from '@/assets/icons'; + +export default function BackButton() { + const navigate = useNavigate(); + return ( + + ); +} diff --git a/src/components/ResultLetter.tsx b/src/components/ResultLetter.tsx index 4fa14ed..635ecd1 100644 --- a/src/components/ResultLetter.tsx +++ b/src/components/ResultLetter.tsx @@ -1,12 +1,12 @@ import letterPink from '@/assets/images/letter-pink.png'; -import { STAMPS } from '../pages/Write/constants'; +import { CATEGORYS } from '../pages/Write/constants'; export default function ResultLetter({ - stampName = '위로와 공감', + categoryName = 'CONSOLATION', title, }: { - stampName: Stamp; + categoryName: Category; title: string; }) { const address = '1A3E2'; @@ -15,7 +15,7 @@ export default function ResultLetter({ return (
@@ -23,7 +23,7 @@ export default function ResultLetter({ 따숨이님께 {title}
- 우표 + 우표
{today} diff --git a/src/layouts/MobileLayout.tsx b/src/layouts/MobileLayout.tsx new file mode 100644 index 0000000..3229467 --- /dev/null +++ b/src/layouts/MobileLayout.tsx @@ -0,0 +1,12 @@ +import { Outlet } from 'react-router'; + +const MobileLayout = () => { + return ( +
+
+ +
+
+ ); +}; +export default MobileLayout; diff --git a/src/pages/Admin/Report.tsx b/src/pages/Admin/Report.tsx new file mode 100644 index 0000000..36b8040 --- /dev/null +++ b/src/pages/Admin/Report.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; + +import { client } from '@/apis/client'; +import { AlarmIcon } from '@/assets/icons'; + +import DetailFrame from './components/DetailFrame'; +import ListItem from './components/ListItem'; +import WrapperFrame from './components/WrapperFrame'; + +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 [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(); + }, []); + return ( + + + 신고 편지 목록 + +
+
+
+ ID + 제보자 이메일 + 작성자 이메일 + 제보 일자 + 제보 사유 +
+
+ {DUMMY.map((data, idx) => ( + + ))} +
+ {detailModalOpen && ( + + <> + 제보 편지 상세 조회 + + + )} +
+ ); +} diff --git a/src/pages/Admin/admin.d.ts b/src/pages/Admin/admin.d.ts new file mode 100644 index 0000000..e50f82d --- /dev/null +++ b/src/pages/Admin/admin.d.ts @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000..505d494 --- /dev/null +++ b/src/pages/Admin/components/DetailFrame.tsx @@ -0,0 +1,27 @@ +import { ReactNode, useRef } from 'react'; + +export default function DetailFrame({ + children, + closeEvent, +}: { + children: ReactNode; + closeEvent: React.Dispatch>; +}) { + const contentRef = useRef(null); + const handleOutsideClick = (e: React.MouseEvent) => { + if (e.target !== contentRef.current) closeEvent(false); + }; + return ( +
handleOutsideClick(e)} + > +
+ {children} +
+
+ ); +} diff --git a/src/pages/Admin/components/ListItem.tsx b/src/pages/Admin/components/ListItem.tsx new file mode 100644 index 0000000..a9eca5b --- /dev/null +++ b/src/pages/Admin/components/ListItem.tsx @@ -0,0 +1,43 @@ +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/components/MenuModal.tsx b/src/pages/Admin/components/MenuModal.tsx new file mode 100644 index 0000000..ea900eb --- /dev/null +++ b/src/pages/Admin/components/MenuModal.tsx @@ -0,0 +1,27 @@ +interface ModalContents { + title: string; + onClick: () => void; +} +export default function MenuModal({ + modalContents, + setModalOpen, +}: { + modalContents: ModalContents[]; + setModalOpen: React.Dispatch>; +}) { + return ( +
+ {modalContents.map((content, idx) => ( + + ))} +
+ ); +} diff --git a/src/pages/Admin/components/WrapperFrame.tsx b/src/pages/Admin/components/WrapperFrame.tsx new file mode 100644 index 0000000..4c87de5 --- /dev/null +++ b/src/pages/Admin/components/WrapperFrame.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; + +export default function WrapperFrame({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/pages/Admin/index.tsx b/src/pages/Admin/index.tsx new file mode 100644 index 0000000..681164f --- /dev/null +++ b/src/pages/Admin/index.tsx @@ -0,0 +1,83 @@ +import { Outlet } from 'react-router'; + +import { AlarmIcon, ArrowDownIcon } from '@/assets/icons'; + +const AdminPage = () => { + return ( +
+
+
+ 로고 +
+
+ 로그인된 계정 + {'admin123@test.com'} +
+
+
+ 사이트 관리 + +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+
+ +
+
+
+
+ 사용자 목록 +
+
+ +
+
+
+ ); +}; +export default AdminPage; diff --git a/src/pages/LetterDetail/index.tsx b/src/pages/LetterDetail/index.tsx index 0dd48a4..c892656 100644 --- a/src/pages/LetterDetail/index.tsx +++ b/src/pages/LetterDetail/index.tsx @@ -1,16 +1,26 @@ import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; import { twMerge } from 'tailwind-merge'; -import { CloudIcon, SirenOutlinedIcon, SnowIcon, ThermostatIcon, WarmIcon } from '@/assets/icons'; +import { deleteLetter, getLetter } from '@/apis/letterDetail'; +import { + CloudIcon, + DeleteIcon, + SirenOutlinedIcon, + SnowIcon, + ThermostatIcon, + WarmIcon, +} from '@/assets/icons'; +import BackButton from '@/components/BackButton'; import ReportModal from '@/components/ReportModal'; +import { FONT_TYPE_OBJ, PAPER_TYPE_OBJ } from '@/pages/Write/constants'; const LetterDetailPage = () => { - const DUMMY = { - title: '나에게 햄버거 햄부기우기우가 햄북스따스 함부르크 햄버거링고를 대령하거라 ', - text: '이 편지는 영국에서 최초로 시작되어 일년에 한바퀴를 돌면서 받는 사람에게 행운을 주었고 지금은 당신에게로 옮겨진 이 편지는 4일 안에 당신 곁을 떠나야 합니다. 이 편지를 포함해서 7통을 행운이 필요한 사람에게 보내 주셔야 합니다. 복사를 해도 좋습니다. 혹 미신이라 하실지 모르지만 사실입니다.영국에서 HGXWCH이라는 사람은 1930년에 이 편지를 받았습니다. 그는 비서에게 복사해서 보내라고 했습니다. 며칠 뒤에 복권이 당첨되어 20억을 받았습니다. 어떤 이는 이 편지를 받았으나 96시간 이내 자신의 손에서 떠나야 한다는 사실을 잊었습니다. 그는 곧 사직되었습니다. 나중에야 이 사실을 알고 7통의 편지를 보냈는데 다시 좋은 직장을 얻었습니다. 미국의 케네디 대통령은 이 편지를 받았지만 그냥 버렸습니다. 결국 9일 후 그는 암살당했습니다. 기억해 주세요. 이 편지를 보내면 7년의 행운이 있을 것이고 그렇지 않으면 3년의 불행이 있을 것입니다. ', - }; - const FONT = 'kobyo'; - const THEME = 'celebrate'; + const params = useParams(); + const navigate = useNavigate(); + // 상대방의 우편번호도 데이터에 포함되어야 할 거 같음!!! + const [letterDetail, setLetterDetail] = useState(null); + const DEGREES = [ { icon: , title: '따뜻해요' }, { icon: , title: '그럭저럭' }, @@ -29,63 +39,92 @@ const LetterDetailPage = () => { }; useEffect(() => { document.body.addEventListener('click', handleOutsideClick); + if (params.id) { + getLetter(params.id, setLetterDetail); + // 편지 삭제 요청 테스트(내일 삭제 버튼 만들어서 여기다 추가하긔) + } return () => { document.body.removeEventListener('click', handleOutsideClick); }; - }, []); + }, [params.id]); return ( <> {reportModalOpen && setReportModalOpen(false)} />} -
-
- - - {degreeModalOpen && ( -
- {DEGREES.map((degree, idx) => { - return ( - - ); - })} -
- )} +
+
+ +
+ + + + {degreeModalOpen && ( +
+ {DEGREES.map((degree, idx) => { + return ( + + ); + })} +
+ )} +
TO. 따숨이 - {DUMMY.title} + {letterDetail?.title}
FROM. {'12E12'} - +
); diff --git a/src/pages/RandomLetters/Matched.tsx b/src/pages/RandomLetters/Matched.tsx index 6d504fc..bc77d3a 100644 --- a/src/pages/RandomLetters/Matched.tsx +++ b/src/pages/RandomLetters/Matched.tsx @@ -9,7 +9,7 @@ export default function Matched() { {'00'} : {'00'} : {'00'}

- +
); diff --git a/src/pages/RandomLetters/MatchingSelectModal.tsx b/src/pages/RandomLetters/MatchingSelectModal.tsx index 2fc083d..6be984e 100644 --- a/src/pages/RandomLetters/MatchingSelectModal.tsx +++ b/src/pages/RandomLetters/MatchingSelectModal.tsx @@ -19,7 +19,7 @@ function MatchingSelectModal({ 수락한 편지는 5분이 지나면 취소할 수 없습니다.
- +