Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 어드민 게시물 조회 페이지 UI 및 관련 기능 구현 #279

Merged
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
7 changes: 7 additions & 0 deletions src/app/_api/notice/deleteNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axiosInstance from '@/lib/axios/axiosInstance';

const deleteNotice = async (noticeId: number) => {
await axiosInstance.delete(`/admin/notices/${noticeId}`);
};

export default deleteNotice;
10 changes: 10 additions & 0 deletions src/app/_api/notice/getAdminNotices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { AdminNoticeType } from '@/lib/types/noticeType';

const getAdminNotices = async () => {
const result = await axiosInstance.get<AdminNoticeType[]>('/admin/notices');

return result.data;
};

export default getAdminNotices;
10 changes: 10 additions & 0 deletions src/app/_api/notice/getNoticeDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { NoticeDetailType } from '@/lib/types/noticeType';

const getNoticeDetail = async (noticeId: number) => {
const result = await axiosInstance.get<NoticeDetailType>(`/notices/${noticeId}`);

return result.data;
};

export default getNoticeDetail;
7 changes: 7 additions & 0 deletions src/app/_api/notice/sendNoticeAlarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axiosInstance from '@/lib/axios/axiosInstance';

const sendNoticeAlarm = async (noticeId: number) => {
await axiosInstance.post(`/admin/notices/${noticeId}/alarm`);
};

export default sendNoticeAlarm;
7 changes: 7 additions & 0 deletions src/app/_api/notice/updateNoticePublic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axiosInstance from '@/lib/axios/axiosInstance';

const updateNoticePublic = async (noticeId: number) => {
await axiosInstance.patch(`/admin/notices/${noticeId}`);
};

export default updateNoticePublic;
40 changes: 40 additions & 0 deletions src/app/admin/layout.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { style } from '@vanilla-extract/css';
import { Header, BodyRegular } from '@/styles/font.css';
import { vars } from '@/styles/theme.css';

export const container = style({
minHeight: '100vh',
display: 'flex',
});

export const nav = style({
padding: '1.5rem',

display: 'flex',
flexDirection: 'column',
gap: '3rem',

borderRight: '2px solid',
borderRightColor: vars.color.bluegray6,
});

export const title = style([
Header,
{
color: vars.color.bluegray8,
},
]);

export const menu = style([
BodyRegular,
{
width: 200,
display: 'flex',
flexDirection: 'column',
gap: '2rem',
},
]);

export const main = style({
flexGrow: 1,
});
23 changes: 23 additions & 0 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactNode } from 'react';

import * as styles from './layout.css';
import Link from 'next/link';

interface AdminNoticeLayoutProps {
children: ReactNode;
}

export default function AdminNoticeLayout({ children }: AdminNoticeLayoutProps) {
return (
<section className={styles.container}>
<nav className={styles.nav}>
<h1 className={styles.title}>🤍 리스티웨이브 관리</h1>
<ul className={styles.menu}>
<Link href="/admin/topics">요청 주제</Link>
<Link href="/admin/notice">게시물</Link>
</ul>
</nav>
<main className={styles.main}>{children}</main>
</section>
);
}
66 changes: 66 additions & 0 deletions src/app/admin/notice/_components/NoticeItem.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { style, styleVariants } from '@vanilla-extract/css';
import { vars } from '@/styles/theme.css';
import { Label } from '@/styles/font.css';

export const bodyRow = style([
Label,
{
padding: '1rem 0.5rem',
marginBottom: '1rem',
borderBottom: `1px solid ${vars.color.bluegray6}`,

display: 'grid',
gridTemplateColumns: 'repeat(8, 1fr)',
alignItems: 'center',

textAlign: 'center',
},
]);

export const rowItem = style({
gridColumn: 'span 2',

display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
});

export const rowText = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});

export const buttons = style({
display: 'flex',
justifyContent: 'center',
gap: '0.5rem',
});

const button = style({
padding: '0.5rem 1rem',
borderRadius: '4px',
backgroundColor: vars.color.blue,
color: vars.color.white,

':hover': {
opacity: 0.7,
},
});

export const variantsButton = styleVariants({
default: [button],
disabled: [
button,
{
opacity: 0.7,
cursor: 'default',
},
],
});

export const modal = style({
width: '100%',
height: '100vh',
overflow: 'scroll',
});
111 changes: 111 additions & 0 deletions src/app/admin/notice/_components/NoticeItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useQuery } from '@tanstack/react-query';

import * as styles from './NoticeItem.css';

import useNotice from '@/hooks/queries/useNotice';
import useBooleanOutput from '@/hooks/useBooleanOutput';

import Modal from '@/components/Modal/Modal';
import NoticeDetailInfo from '@/components/NoticeDetail/NoticeDetailInfo';

import { AdminNoticeType, NoticeDetailType } from '@/lib/types/noticeType';
import formatDate from '@/lib/utils/dateFormat';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';

import getNoticeDetail from '@/app/_api/notice/getNoticeDetail';

interface NoticeDetailModalProps {
noticeId: number;
}

function NoticeDetailModal({ noticeId }: NoticeDetailModalProps) {
const { data: notices } = useQuery<NoticeDetailType>({
queryKey: [QUERY_KEYS.getNoticeDetail],
queryFn: () => getNoticeDetail(noticeId),
enabled: !!noticeId,
});

return <>{notices && <NoticeDetailInfo noticeData={notices} />}</>;
}

interface NoticeItemProps {
notice: AdminNoticeType;
}

function NoticeItem({ notice }: NoticeItemProps) {
const { deleteNoticeMutation, sendNoticeAlarmMutation, updateNoticePublicMutation } = useNotice();
const { isOn, handleSetOn, handleSetOff } = useBooleanOutput();

const { id, title, description, didSendAlarm, isExposed, category, createdDate } = notice;

const handleDeleteNotice = () => {
deleteNoticeMutation.mutate(id);
};

const handleSendAlarm = () => {
if (!isExposed) {
alert('공개 게시물만 알림을 보낼 수 있어요.');
return;
}
if (didSendAlarm) {
alert('이미 알림을 보낸 게시물입니다.');
return;
}
sendNoticeAlarmMutation.mutate(id, {
onSuccess: () => alert('알림을 보냈어요.'),
});
};

const handleTogglePublic = () => {
updateNoticePublicMutation.mutate(id);
};

return (
<>
<tr className={styles.bodyRow}>
<td>{formatDate(createdDate)}</td>
<td>{category}</td>
<td className={styles.rowItem}>
<span className={styles.rowText}>{title}</span>
<span className={styles.rowText}>{description}</span>
</td>
<td className={styles.buttons}>
<button className={styles.variantsButton.default}>수정</button>
<button className={styles.variantsButton.default} onClick={handleDeleteNotice}>
삭제
</button>
</td>
<td>
<button className={styles.variantsButton.default} onClick={handleSetOn}>
미리보기
</button>
</td>
<td>
<button
className={didSendAlarm ? styles.variantsButton.disabled : styles.variantsButton.default}
onClick={handleSendAlarm}
disabled={didSendAlarm}
>
{`알림 ${didSendAlarm ? '완료' : '보내기'}`}
</button>
</td>
<td>
<select onChange={handleTogglePublic} value={isExposed ? '공개' : '비공개'}>
<option>공개</option>
<option>비공개</option>
</select>
</td>
</tr>

{isOn && (
<Modal size="large" handleModalClose={handleSetOff}>
<section className={styles.modal}>
<NoticeDetailModal noticeId={id} />
</section>
</Modal>
)}
</>
);
}

export default NoticeItem;
41 changes: 41 additions & 0 deletions src/app/admin/notice/page.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { style } from '@vanilla-extract/css';
import { vars } from '@/styles/theme.css';
import { BodyRegular } from '@/styles/font.css';

export const page = style({
padding: '1.5rem',
height: '100%',
});

export const table = style({
maxWidth: '850px',
padding: '1rem',

display: 'flex',
flexDirection: 'column',
gap: '1rem',

backgroundColor: vars.color.white,
borderRadius: '8px',
});

export const headRow = style([
BodyRegular,
{
padding: '1rem 0.5rem',

display: 'grid',
gridTemplateColumns: 'repeat(8, 1fr)',
alignItems: 'center',

textAlign: 'center',
},
]);

export const rowItem = style({
gridColumn: 'span 2',

display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
});
38 changes: 38 additions & 0 deletions src/app/admin/notice/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { useQuery } from '@tanstack/react-query';

import * as styles from './page.css';

import getAdminNotices from '@/app/_api/notice/getAdminNotices';

import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { AdminNoticeType } from '@/lib/types/noticeType';
import NoticeItem from './_components/NoticeItem';

const TABLE_ROW = ['일시', '카테고리', '제목&소개', '편집', '미리보기', '알림', '공개'];

export default function AdminNoticesPage() {
const { data: notices } = useQuery<AdminNoticeType[]>({
queryKey: [QUERY_KEYS.getAdminAllNotice],
queryFn: getAdminNotices,
staleTime: 1000 * 60 * 30,
});

return (
<section className={styles.page}>
<table className={styles.table}>
<thead>
<tr className={styles.headRow}>
{TABLE_ROW.map((item, index) => (
<th key={index} className={item === '제목&소개' ? styles.rowItem : ''}>
{item}
</th>
))}
</tr>
</thead>
<tbody>{notices?.map((notice) => <NoticeItem key={notice.id} notice={notice} />)}</tbody>
</table>
</section>
);
}
2 changes: 1 addition & 1 deletion src/app/notices/[noticeId]/NoticeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function NoticeDetailComponent() {
</section>
<article className={styles.articleWrapper}>
<ul>
{data.content?.map((item: NoticeContentType, idx) => (
{data.contents?.map((item: NoticeContentType, idx) => (
<li key={idx.toString()}>
<NoticeContent item={item} />
</li>
Expand Down
2 changes: 1 addition & 1 deletion src/app/notices/mockdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const NOTICE_DETAIL_MOCKDATA: NoticeDetailType = {
category: '소식',
title: '서비스 점검 안내',
description: '서비스 점검이 10월 20일에 진행될 예정입니다.',
content: [
contents: [
{
type: 'subtitle',
description: '점검 일정',
Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/Modal.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const sizeVariants = styleVariants<SizeVariantsType>({
container,
{
minWidth: '327px',
maxWidth: '420px',
width: '100%',
margin: '0px 24px',
padding: '6rem 2.5rem',
Expand Down
Loading