Skip to content

Commit

Permalink
Merge branch 'dev' into feature/page-style
Browse files Browse the repository at this point in the history
  • Loading branch information
ParkSohyunee authored Dec 5, 2024
2 parents fb33d4e + dd44db8 commit e2d0ed7
Show file tree
Hide file tree
Showing 48 changed files with 2,712 additions and 57 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@tanstack/react-query": "^5.17.12",
"@tanstack/react-query-devtools": "^5.17.12",
"@types/react-lottie": "^1.2.10",
"@uiw/react-md-editor": "^4.0.4",
"@vanilla-extract/dynamic": "^2.1.0",
"@vanilla-extract/integration": "^6.2.4",
"@vanilla-extract/next-plugin": "^2.3.2",
Expand Down
14 changes: 14 additions & 0 deletions src/app/_api/notice/createNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { NoticeCreateType } from '@/lib/types/noticeType';

interface ResponseType {
id: number;
}

const createNotice = async (data: NoticeCreateType) => {
const response = await axiosInstance.post<ResponseType>('/admin/notices', data);

return response.data;
};

export default createNotice;
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/getNoticeCategories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { NoticeCategoryType } from '@/lib/types/noticeType';

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

return result.data;
};

export default getNoticeCategories;
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/_api/notice/uploadNoticeImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import axios from 'axios';
import axiosInstance from '@/lib/axios/axiosInstance';

interface UploadImageType {
order: number;
extension: string;
}

interface UploadNoticeImagesProps {
noticeId: number;
imageExtensionData: UploadImageType[];
imageFileData: File[];
}

interface PresignedResponseType {
order: number;
presignedUrl: string;
}

const uploadNoticeImages = async ({ noticeId, imageFileData, imageExtensionData }: UploadNoticeImagesProps) => {
// 1. Presigned url 발급 요청
const presignedResponse = await axiosInstance.post<PresignedResponseType[]>(
`/admin/notices/${noticeId}/presigned-url`,
imageExtensionData
);

// 2. 발급 받은 Presigned url로 이미지 업로드
presignedResponse.data.forEach(async (value, index) => {
await axios.put(value.presignedUrl, imageFileData[index], {
headers: {
'Content-Type': imageFileData[index].type,
},
});
});

// 3. 이미지 업로드 완료 서버에 알림
await axiosInstance.post(`/admin/notices/${noticeId}/upload-complete`, imageExtensionData);
};

export default uploadNoticeImages;
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;
29 changes: 29 additions & 0 deletions src/app/admin/notice/create/_components/BlockContainer.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BodyRegular } from '@/styles/font.css';
import { vars } from '@/styles/theme.css';
import { style } from '@vanilla-extract/css';

export const container = style({
padding: '1rem 1rem',
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
});

export const wrapper = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});

export const title = style([BodyRegular]);

export const deleteButton = style({
color: vars.color.red,
});

export const content = style({
height: '100%',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
});
Loading

0 comments on commit e2d0ed7

Please sign in to comment.