diff --git a/eslint.config.js b/eslint.config.js index 3cc2bdc..567dc87 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], '@tanstack/query/exhaustive-deps': 'error', + '@typescript-eslint/no-empty-object-type': off, 'import/order': [ 'error', { diff --git a/src/App.tsx b/src/App.tsx index 29e7dfa..6e334f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import AdminPage from './pages/Admin'; import FilteredLetterManage from './pages/Admin/FilteredLetter'; import FilteringManage from './pages/Admin/Filtering'; import ReportManage from './pages/Admin/Report'; +import AdminRollingPaper from './pages/Admin/RollingPaper'; import AuthCallbackPage from './pages/Auth'; import Home from './pages/Home'; import Landing from './pages/Landing'; @@ -69,6 +70,7 @@ const App = () => { } /> } /> } /> + } /> diff --git a/src/apis/rolling.ts b/src/apis/rolling.ts index 8930c0d..a8cb25d 100644 --- a/src/apis/rolling.ts +++ b/src/apis/rolling.ts @@ -36,3 +36,50 @@ export const deleteRollingPaperComment = async (commentId: string | number) => { throw error; } }; + +export const postNewRollingPaper = async (title: string) => { + try { + const { + data: { data }, + } = await client.post('/api/admin/event-posts', { title }); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const getRollingPaperList = async (): Promise => { + try { + const { + data: { data }, + } = await client.get('/api/admin/event-posts'); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const deleteRollingPaper = async (eventPostId: number | string) => { + try { + const { data } = await client.delete(`/api/admin/event-posts/${eventPostId}`); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const patchRollingPaper = async (eventPostId: number | string) => { + try { + const { + data: { data }, + } = await client.patch(`/api/admin/event-posts/${eventPostId}/status`); + console.log(data); + return data; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/src/components/NoticeRollingPaper.tsx b/src/components/NoticeRollingPaper.tsx index b495b0e..7c80fe3 100644 --- a/src/components/NoticeRollingPaper.tsx +++ b/src/components/NoticeRollingPaper.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router'; import { twMerge } from 'tailwind-merge'; @@ -6,13 +7,41 @@ import { getCurrentRollingPaper } from '@/apis/rolling'; import { NoticeIcon } from '@/assets/icons'; const NoticeRollingPaper = () => { - const { data } = useQuery({ + const { data, error } = useQuery({ queryKey: ['notice-rolling-paper'], queryFn: () => getCurrentRollingPaper(), }); + const [activeAnimate, setActiveAnimate] = useState(false); + const containerRef = useRef(null); + const textRef = useRef(null); + + useEffect(() => { + if (data?.title) { + const containerElement = containerRef.current; + const element = textRef.current; + + if (containerElement && element) { + const textWidth = element.scrollWidth; + const containerWidth = containerElement.offsetWidth; + + if (textWidth > containerWidth) { + const animationDuration = (textWidth / 10) * 0.3; + const totalDuration = Math.max(animationDuration, 10); + document.documentElement.style.setProperty('--marquee-duration', `${totalDuration}s`); + + setActiveAnimate(true); + } else { + setActiveAnimate(false); + } + } + } + }, [data?.title]); + const noticeText = data?.title; + if (error || !noticeText) return null; + return (
{ )} > -
-

{noticeText}

+
+

+ {noticeText} +

diff --git a/src/pages/Admin/RollingPaper.tsx b/src/pages/Admin/RollingPaper.tsx index a855c2b..6feb8ff 100644 --- a/src/pages/Admin/RollingPaper.tsx +++ b/src/pages/Admin/RollingPaper.tsx @@ -1,14 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; -import { AddIcon, AlarmIcon, DeleteIcon } from '@/assets/icons'; +import { getRollingPaperList } from '@/apis/rolling'; +import { AddIcon, AlarmIcon } from '@/assets/icons'; import AddRollingPaperModal from './components/AddRollingPaperModal'; import PageTitle from './components/AdminPageTitle'; +import RollingPaperItem from './components/RollingPaperItem'; import WrapperFrame from './components/WrapperFrame'; import WrapperTitle from './components/WrapperTitle'; export default function AdminRollingPaper() { const [activeModal, setActiveModal] = useState(false); + const { data, isLoading, isSuccess } = useQuery({ + queryKey: ['admin-rolling-paper'], + queryFn: getRollingPaperList, + }); return ( <> @@ -26,55 +33,32 @@ export default function AdminRollingPaper() { 롤링페이퍼 생성 - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID제목쌓인 편지 수상태
1 - 침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요! - 12 - - 진행 중 - -
2 - 침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요! - 12 - - - -
+ {isLoading &&

Loading...

} + {isSuccess && ( + <> + + + + + + + + + + + {data.content.map((rollingPaper) => ( + + ))} + +
ID제목상태
+ {data.content.length === 0 && ( + + 아직 생성된 롤링페이퍼가 없어요 + + )} + + )} + {/* TODO: 페이지네이션 적용 */} ); diff --git a/src/pages/Admin/components/AddRollingPaperModal.tsx b/src/pages/Admin/components/AddRollingPaperModal.tsx index 2df17c3..0e336be 100644 --- a/src/pages/Admin/components/AddRollingPaperModal.tsx +++ b/src/pages/Admin/components/AddRollingPaperModal.tsx @@ -1,5 +1,7 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ChangeEvent, FormEvent, useState } from 'react'; +import { postNewRollingPaper } from '@/apis/rolling'; import ModalOverlay from '@/components/ModalOverlay'; interface AddRollingPaperModalProps { @@ -9,6 +11,21 @@ interface AddRollingPaperModalProps { export default function AddRollingPaperModal({ onClose }: AddRollingPaperModalProps) { const [title, setTitle] = useState(''); const [error, setError] = useState(''); + const queryClient = useQueryClient(); + + const { mutate } = useMutation({ + mutationFn: () => postNewRollingPaper(title), + onSuccess: () => { + setTitle(''); + setError(''); + onClose(); + // TODO: 페이지네이션 적용 후, 현재 page에 대한 캐싱 날리는 방식으로 변경 + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper'] }); + }, + onError: () => { + setError('편지 작성에 실패했어요. 다시 시도해주세요.'); + }, + }); const handleChange = (e: ChangeEvent) => { setTitle(e.target.value); @@ -21,7 +38,7 @@ export default function AddRollingPaperModal({ onClose }: AddRollingPaperModalPr return; } - console.log(title); + mutate(); }; return ( diff --git a/src/pages/Admin/components/RollingPaperItem.tsx b/src/pages/Admin/components/RollingPaperItem.tsx new file mode 100644 index 0000000..c4e0a18 --- /dev/null +++ b/src/pages/Admin/components/RollingPaperItem.tsx @@ -0,0 +1,76 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { deleteRollingPaper, patchRollingPaper } from '@/apis/rolling'; +import { DeleteIcon } from '@/assets/icons'; + +interface RollingPaperItemProps { + information: AdminRollingPaperInformation; +} + +export default function RollingPaperItem({ information }: RollingPaperItemProps) { + const queryClient = useQueryClient(); + + const { mutate: deleteMutate } = useMutation({ + mutationFn: () => deleteRollingPaper(information.eventPostId), + onSuccess: () => { + // TODO: 페이지네이션 적용 후, 현재 page에 대한 캐싱 날리는 방식으로 변경 + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper'] }); + }, + onError: (err) => { + console.error(err); + }, + }); + + const { mutate: toggleStatus } = useMutation({ + mutationFn: () => patchRollingPaper(information.eventPostId), + onSuccess: () => { + // TODO: 기존 데이터 수정하는 방식으로 ㄱㄱㄱㄱㄱㄱㄱ + // 일단 임시로 캐싱 날리기 + queryClient.invalidateQueries({ queryKey: ['admin-rolling-paper'] }); + }, + onError: (err: AxiosError<{ code: string; message: string }>) => { + if (err.response?.data.code === 'EVENT-004') { + alert(err.response.data.message); + } + console.error(err); + }, + }); + + // TODO: 진짜 삭제하겠냐고 물어보기 + return ( + + {information.eventPostId} + +
+ {information.used && ( + + 진행 중 + + )} + {information.title} +
+ + + + + + {!information.used && ( + + )} + + + ); +} diff --git a/src/pages/RollingPaper/components/CommentDetailModal.tsx b/src/pages/RollingPaper/components/CommentDetailModal.tsx index 4769892..cbabb52 100644 --- a/src/pages/RollingPaper/components/CommentDetailModal.tsx +++ b/src/pages/RollingPaper/components/CommentDetailModal.tsx @@ -20,7 +20,7 @@ const CommentDetailModal = ({ comment, isWriter, onClose, onDelete }: CommentDet
-

{comment.content}

+

{comment.content}

From. {comment.zipCode}

diff --git a/src/pages/RollingPaper/index.tsx b/src/pages/RollingPaper/index.tsx index 6bcc9e6..71b4d70 100644 --- a/src/pages/RollingPaper/index.tsx +++ b/src/pages/RollingPaper/index.tsx @@ -12,19 +12,17 @@ import Header from '@/layouts/Header'; import Comment from './components/Comment'; import CommentDetailModal from './components/CommentDetailModal'; import WriteCommentButton from './components/WriteCommentButton'; - -// TODO: 로그인 구현 완료 시, 더미 완전히 제거 -const DUMMY_USER_ZIP_CODE = '1DR41'; -const DUMMY_MESSAGE_COUNT = 20; +import useAuthStore from '@/stores/authStore'; const RollingPaperPage = () => { const id = useParams().id ?? ''; const [activeComment, setActiveComment] = useState(null); const [activeDetailModal, setActiveDetailModal] = useState(false); const [activeDeleteModal, setActiveDeleteModal] = useState(false); + const zipCode = useAuthStore((props) => props.zipCode); const queryClient = useQueryClient(); - const { data } = useQuery({ + const { data, isSuccess } = useQuery({ queryKey: ['rolling-paper', id], queryFn: () => getRollingPaperDetail(id), }); @@ -37,9 +35,11 @@ const RollingPaperPage = () => { return { ...oldData, - eventPostComments: oldData.eventPostComments.filter( - (comment: RollingPaperComment) => comment.commentId !== data.commentId, - ), + eventPostComments: { + content: oldData.eventPostComments.content.filter( + (comment: RollingPaperComment) => comment.commentId !== data.commentId, + ), + }, }; }); @@ -56,7 +56,7 @@ const RollingPaperPage = () => { {activeDetailModal && activeComment && ( { setActiveDetailModal(false); setActiveComment(null); @@ -85,11 +85,13 @@ const RollingPaperPage = () => {
{data?.title} -

등록된 편지 {DUMMY_MESSAGE_COUNT}

+

+ 등록된 편지 {data ? data.eventPostComments.content.length : 0} +

- {data && - data.eventPostComments.map((comment) => ( + {isSuccess && + data.eventPostComments.content.map((comment) => ( { /> ))} + {isSuccess && data.eventPostComments.content.length === 0 && ( +

+ 아직 등록된 편지가 없어요. +
+ 첫번째로 편지를 남겨볼까요? +

+ )}
diff --git a/src/styles/animations.css b/src/styles/animations.css index 7217bbd..702eca7 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -6,7 +6,7 @@ --animate-rotate-show: rotate-show 1s ease-in-out forwards; --animate-blink: showing 0.3s forwards; --animate-login-move-up-down: move-up-down 3s ease-in-out infinite; - --animate-marquee: marquee 10s linear infinite; + --animate-marquee: marquee var(--marquee-duration) linear infinite; @keyframes down { 0% { @@ -70,7 +70,7 @@ /* SpecialLetterBanner 애니메이션 */ @keyframes marquee { 0% { - transform: translateX(10%); + transform: translateX(100%); } 100% { transform: translateX(-100%); diff --git a/src/styles/preflight.css b/src/styles/preflight.css index c74f27a..c23d658 100644 --- a/src/styles/preflight.css +++ b/src/styles/preflight.css @@ -4,6 +4,7 @@ :root { --vh: 1vh; --vw: 1vw; + --marquee-duration: 10s; } * { diff --git a/src/types/rolling.d.ts b/src/types/rolling.d.ts index af75fe6..d72c810 100644 --- a/src/types/rolling.d.ts +++ b/src/types/rolling.d.ts @@ -3,12 +3,26 @@ interface RollingPaperInformation { title: string; } +interface AdminRollingPaperInformation extends RollingPaperInformation { + used: boolean; +} + interface RollingPaperComment { commentId: number; zipCode: string; content: string; } +interface PaginationData { + content: T[]; + currentPage: number; + size: number; + totalElements: number; + totalPages: number; +} + +interface RollingPaperList extends PaginationData {} + interface RollingPaper extends RollingPaperInformation { - eventPostComments: RollingPaperComment[]; + eventPostComments: PaginationData; }