Skip to content
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -69,6 +70,7 @@ const App = () => {
<Route path="report" element={<ReportManage />} />
<Route path="badwords" element={<FilteringManage />} />
<Route path="filtered-letter" element={<FilteredLetterManage />} />
<Route path="rolling-paper" element={<AdminRollingPaper />} />
</Route>
</Route>
</Routes>
Expand Down
47 changes: 47 additions & 0 deletions src/apis/rolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RollingPaperList> => {
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

오홍 data를 이런식으로 받아와서 타입을 지정하는 군요!

data: { data },
} = await client.patch(`/api/admin/event-posts/${eventPostId}/status`);
console.log(data);
return data;
} catch (error) {
console.error(error);
throw error;
}
};
40 changes: 37 additions & 3 deletions src/components/NoticeRollingPaper.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router';
import { twMerge } from 'tailwind-merge';

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<HTMLDivElement>(null);
const textRef = useRef<HTMLParagraphElement>(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 (
<Link to={`/board/rolling/${data?.eventPostId}`}>
<article
Expand All @@ -23,8 +52,13 @@ const NoticeRollingPaper = () => {
)}
>
<NoticeIcon className="h-6 w-6 shrink-0 text-gray-50" />
<div className="w-full overflow-hidden">
<p className="body-sb animate-marquee whitespace-nowrap">{noticeText}</p>
<div ref={containerRef} className="w-full overflow-hidden whitespace-nowrap">
<p
ref={textRef}
className={twMerge('body-sb inline-block', activeAnimate && 'animate-marquee')}
>
{noticeText}
</p>
</div>
</article>
</Link>
Expand Down
84 changes: 34 additions & 50 deletions src/pages/Admin/RollingPaper.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
Expand All @@ -26,55 +33,32 @@ export default function AdminRollingPaper() {
롤링페이퍼 생성
</button>
</section>
<table className="mt-5 table-auto">
<thead>
<tr className="bg-primary-3 border-gray-40 h-14 border-b">
<th className="w-14 text-center">ID</th>
<th className="text-left">제목</th>
<th className="w-30 text-center">쌓인 편지 수</th>
<th className="w-30 text-center">상태</th>
<th></th>
</tr>
</thead>
<tbody>
<tr className="border-gray-40 h-14 border-b">
<td className="w-14 text-center">1</td>
<td className="text-left">
침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요!
</td>
<td className="w-30 text-center">12</td>
<td className="text-center">
<span className="rounded-full border border-amber-500 bg-amber-100/70 px-4 py-1.5 whitespace-nowrap text-amber-500">
진행 중
</span>
</td>
<td></td>
</tr>
<tr className="border-gray-40 h-14 border-b">
<td className="w-14 text-center">2</td>
<td className="text-left">
침수 피해를 복구중인 포스코 임직원 분들에게 응원의 메시지를 보내주세요!
</td>
<td className="w-30 text-center">12</td>
<td className="w-30 px-2 text-center">
<button
type="button"
className="hover:bg-gray-10 text-gray-60 rounded-md px-3 py-1 hover:text-black"
>
진행하기
</button>
</td>
<td>
<button
type="button"
className="text-gray-60 flex items-center justify-center p-1 hover:text-black"
>
<DeleteIcon className="h-5 w-5" />
</button>
</td>
</tr>
</tbody>
</table>
{isLoading && <p className="mt-20 text-center">Loading...</p>}
{isSuccess && (
<>
<table className="mt-5 table-auto">
<thead>
<tr className="bg-primary-3 border-gray-40 h-14 border-b">
<th className="w-14 text-center">ID</th>
<th className="text-left">제목</th>
<th className="w-30 text-center">상태</th>
<th className="w-6"></th>
</tr>
</thead>
<tbody>
{data.content.map((rollingPaper) => (
<RollingPaperItem key={rollingPaper.eventPostId} information={rollingPaper} />
))}
</tbody>
</table>
{data.content.length === 0 && (
<span className="my-10 text-center text-gray-50">
아직 생성된 롤링페이퍼가 없어요
</span>
)}
</>
)}
{/* TODO: 페이지네이션 적용 */}
</WrapperFrame>
</>
);
Expand Down
19 changes: 18 additions & 1 deletion src/pages/Admin/components/AddRollingPaperModal.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<HTMLTextAreaElement>) => {
setTitle(e.target.value);
Expand All @@ -21,7 +38,7 @@ export default function AddRollingPaperModal({ onClose }: AddRollingPaperModalPr
return;
}

console.log(title);
mutate();
};

return (
Expand Down
76 changes: 76 additions & 0 deletions src/pages/Admin/components/RollingPaperItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<tr className="border-gray-40 h-14 border-b">
<td className="w-14 text-center">{information.eventPostId}</td>
<td className="text-left">
<div>
{information.used && (
<span className="mr-2 rounded-full border border-amber-500 bg-amber-100/70 px-3 py-0.5 whitespace-nowrap text-amber-500">
진행 중
</span>
)}
{information.title}
</div>
</td>
<td className="text-center">
<button
type="button"
className="hover:bg-gray-10 text-gray-60 rounded-md px-3 py-1 hover:text-black"
onClick={() => toggleStatus()}
>
{information.used ? '중단하기' : '진행하기'}
</button>
</td>
<td className="w-6">
{!information.used && (
<button
type="button"
className="text-gray-60 flex items-center justify-center p-1 hover:text-black"
onClick={() => deleteMutate()}
>
<DeleteIcon className="h-5 w-5" />
</button>
)}
</td>
</tr>
);
}
2 changes: 1 addition & 1 deletion src/pages/RollingPaper/components/CommentDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const CommentDetailModal = ({ comment, isWriter, onClose, onDelete }: CommentDet

<MemoWrapper className="mt-1 flex max-h-1/2 w-78 overflow-y-auto px-5 text-black">
<div className="z-1 flex flex-col gap-3">
<p className="body-r leading-[26px]">{comment.content}</p>
<p className="body-r leading-[26px] whitespace-pre-wrap">{comment.content}</p>
<p className="body-m place-self-end">From. {comment.zipCode}</p>
</div>
</MemoWrapper>
Expand Down
Loading