Skip to content

Commit

Permalink
Merge branch 'dev' into Feat/home
Browse files Browse the repository at this point in the history
  • Loading branch information
Nahyun-Kang committed Dec 15, 2024
2 parents 00d7fcc + 15e6086 commit b61b7ca
Show file tree
Hide file tree
Showing 22 changed files with 435 additions and 83 deletions.
13 changes: 13 additions & 0 deletions src/app/_api/notice/updateNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { NoticeCreateType } from '@/lib/types/noticeType';

interface UpdateNoticeRequestType {
noticeData: NoticeCreateType;
noticeId: number;
}

const updateNotice = async ({ noticeData, noticeId }: UpdateNoticeRequestType) => {
await axiosInstance.put<ResponseType>(`/admin/notices/${noticeId}`, noticeData);
};

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

export const nav = style({
minWidth: 200,

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

export const variantLink = styleVariants({
default: [BodyRegular],
selected: [
BodyBold,
{
color: vars.color.blue,
},
],
});
31 changes: 31 additions & 0 deletions src/app/admin/_components/NavLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';

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

interface NavLinksProps {
links: Array<Record<string, string>>;
}

export default function NavLinks({ links }: NavLinksProps) {
const pathname = usePathname();

return (
<nav className={styles.nav}>
{links.map((link) => {
const isActive = pathname && pathname.startsWith(link.path);
return (
<Link
key={link.path}
href={link.path}
className={isActive ? styles.variantLink.selected : styles.variantLink.default}
>
{link.label}
</Link>
);
})}
</nav>
);
}
16 changes: 3 additions & 13 deletions src/app/admin/layout.css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { Header, BodyRegular } from '@/styles/font.css';
import { Header } from '@/styles/font.css';
import { vars } from '@/styles/theme.css';

export const container = style({
Expand All @@ -8,11 +8,11 @@ export const container = style({
});

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

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

borderRight: '2px solid',
borderRightColor: vars.color.bluegray6,
Expand All @@ -25,16 +25,6 @@ export const title = style([
},
]);

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

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

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

interface AdminNoticeLayoutProps {
children: ReactNode;
Expand All @@ -10,13 +10,21 @@ interface AdminNoticeLayoutProps {
export default function AdminNoticeLayout({ children }: AdminNoticeLayoutProps) {
return (
<section className={styles.container}>
<nav className={styles.nav}>
<div className={styles.nav}>
<h1 className={styles.title}>🤍 리스티웨이브 관리</h1>
<ul className={styles.menu}>
<Link href="/admin/topics">요청 주제</Link>
<Link href="/admin/notice">게시물</Link>
</ul>
</nav>
<NavLinks
links={[
{
label: '요청 주제',
path: '/admin/topics',
},
{
label: '게시물',
path: '/admin/notice',
},
]}
/>
</div>
<main className={styles.main}>{children}</main>
</section>
);
Expand Down
145 changes: 145 additions & 0 deletions src/app/admin/notice/[noticeId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
'use client';

import { useRouter } from 'next/navigation';
import { BaseSyntheticEvent, useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FormProvider, useForm } from 'react-hook-form';

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

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

import { NoticeCreateType, NoticeDetailType } from '@/lib/types/noticeType';
import { noticeDescriptionRules, noticeTitleRules } from '@/lib/constants/formInputValidationRules';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { NOTICE_CATEGORY_NAME } from '@/lib/constants/notice';
import { formatImageData, formatNoticeData } from '@/lib/utils/formatDataForNotice';

import CategoryDropdown from '../../create/_components/CategoryDropdown';
import ContentsBody from '../../create/_components/ContentsBody';

type NoticeCategoryNameType = (typeof NOTICE_CATEGORY_NAME)[keyof typeof NOTICE_CATEGORY_NAME];

/** 카테고리 값에 해당하는 카테고리 코드 반환 */
function getCodeByObject(value: NoticeCategoryNameType) {
return Object.values(NOTICE_CATEGORY_NAME).findIndex((field) => field === value) + 1;
}

export default function EditNotice({ params }: { params: { noticeId: number } }) {
const noticeId = params.noticeId;

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

const router = useRouter();
const queryClient = useQueryClient();
const methods = useForm<NoticeCreateType>({
mode: 'onChange',
defaultValues: {
categoryCode: 1,
title: notice?.title,
description: notice?.description,
contents: [
{
order: 0,
type: 'subtitle',
description: '',
},
],
},
});

// prettier-ignore
const { register, handleSubmit, formState: { errors, isValid }, reset } = methods;

const uploadImageMutation = useMutation({
mutationFn: uploadNoticeImages,
onError: () => alert('이미지 업로드를 다시 시도해주세요.'),
});

const editNoticeMutation = useMutation({
mutationFn: updateNotice,
onSuccess: () => {
const originData = methods.getValues();
const { imageExtensionData, imageFileData } = formatImageData(originData);

// 생성된 공지 ID에 이미지 업로드
if (imageExtensionData.length !== 0) {
uploadImageMutation.mutate({
noticeId,
imageExtensionData,
imageFileData,
});
}
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.getAdminAllNotice] });
router.push('/admin/notice');
},
onError: () => alert('게시물 수정을 다시 시도해주세요.'),
});

/** 게시물 수정 */
const onSubmit = (data: NoticeCreateType, e?: BaseSyntheticEvent) => {
e?.preventDefault();

const noticeData = formatNoticeData(data);
editNoticeMutation.mutate({ noticeData, noticeId });
};

useEffect(() => {
if (notice) {
reset({
title: notice.title,
categoryCode: getCodeByObject(notice.category),
description: notice.description,
contents: notice.contents.map((obj) =>
Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null))
),
});
}
}, [notice, reset]);

return (
<FormProvider {...methods}>
<form className={styles.container}>
<h1>게시물 수정</h1>
<CategoryDropdown />
<div className={styles.row}>
<label className={styles.rowLabel}>제목 *</label>
<div className={styles.field}>
<input
className={styles.rowInput}
placeholder="제목 또는 알림 메시지 문구를 입력해 주세요. (최대 30자)"
{...register('title', noticeTitleRules)}
/>
<p className={styles.rowErrorMessage}>{errors.title && errors.title?.message}</p>
</div>
</div>
<div className={styles.row}>
<label className={styles.rowLabel}>소개 *</label>
<div className={styles.field}>
<input
className={styles.rowInput}
placeholder="글 소개하는 짧은 문구를 입력해 주세요. (최대 30자)"
{...register('description', noticeDescriptionRules)}
/>
<p className={styles.rowErrorMessage}>{errors.description && errors.description.message}</p>
</div>
</div>
<ContentsBody />
<button
type="button"
onClick={handleSubmit(onSubmit)}
disabled={!isValid}
className={isValid ? styles.savedButton.active : styles.savedButton.default}
>
수정하기
</button>
</form>
</FormProvider>
);
}
4 changes: 4 additions & 0 deletions src/app/admin/notice/_components/NoticeItem.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const buttons = style({
display: 'flex',
justifyContent: 'center',
gap: '0.5rem',

whiteSpace: 'nowrap',
});

const button = style({
Expand All @@ -43,6 +45,8 @@ const button = style({
backgroundColor: vars.color.blue,
color: vars.color.white,

whiteSpace: 'nowrap',

':hover': {
opacity: 0.7,
},
Expand Down
6 changes: 5 additions & 1 deletion src/app/admin/notice/_components/NoticeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as styles from './NoticeItem.css';

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

import Modal from '@/components/Modal/Modal';
import NoticeDetailInfo from '@/components/NoticeDetail/NoticeDetailInfo';
Expand Down Expand Up @@ -35,6 +36,7 @@ interface NoticeItemProps {
function NoticeItem({ notice }: NoticeItemProps) {
const { deleteNoticeMutation, sendNoticeAlarmMutation, updateNoticePublicMutation } = useNotice();
const { isOn, handleSetOn, handleSetOff } = useBooleanOutput();
const { onClickMoveToPage } = useMoveToPage();

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

Expand Down Expand Up @@ -70,7 +72,9 @@ function NoticeItem({ notice }: NoticeItemProps) {
<span className={styles.rowText}>{description}</span>
</td>
<td className={styles.buttons}>
<button className={styles.variantsButton.default}>수정</button>
<button onClick={onClickMoveToPage(`/admin/notice/${id}/edit`)} className={styles.variantsButton.default}>
수정
</button>
<button className={styles.variantsButton.default} onClick={handleDeleteNotice}>
삭제
</button>
Expand Down
12 changes: 12 additions & 0 deletions src/app/admin/notice/create/_components/BlockContainer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export const wrapper = style({
alignItems: 'center',
});

export const titleWrapper = style({
display: 'flex',
alignItems: 'center',
gap: '1rem',
});

export const drag = style({
padding: '0.5rem',
borderRadius: '0.5rem',
border: `1px solid ${vars.color.lightgray}`,
});

export const title = style([BodyRegular]);

export const deleteButton = style({
Expand Down
12 changes: 10 additions & 2 deletions src/app/admin/notice/create/_components/BlockContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Image from 'next/image';
import { FieldArrayWithId } from 'react-hook-form';
import { DraggableProvided } from '@hello-pangea/dnd';

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

Expand Down Expand Up @@ -34,15 +36,21 @@ interface ContainerProps {
content: FieldArrayWithId<NoticeCreateType, 'contents', 'id'>;
handleDeleteBlock: (order: number) => void;
order: number;
provided: DraggableProvided;
}

export default function ContentsContainer({ content, handleDeleteBlock, order }: ContainerProps) {
export default function BlockContainer({ content, handleDeleteBlock, order, provided }: ContainerProps) {
const { type } = content;

return (
<div className={styles.container}>
<div className={styles.wrapper}>
<h3 className={styles.title}>{NOTICE_CONTENT[type]}</h3>
<div className={styles.titleWrapper}>
<div {...provided.dragHandleProps} className={styles.drag}>
<Image src={'/icons/dnd.svg'} width={22} height={15} alt="drag and drop" />
</div>
<h3 className={styles.title}>{NOTICE_CONTENT[type]}</h3>
</div>
<button type="button" onClick={() => handleDeleteBlock(order)} className={styles.deleteButton}>
삭제
</button>
Expand Down
3 changes: 2 additions & 1 deletion src/app/admin/notice/create/_components/CategoryDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import * as styles from './CategoryDropdown.css';

import getNoticeCategories from '@/app/_api/notice/getNoticeCategories';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { NoticeCategoryType } from '@/lib/types/noticeType';

export default function CategoryDropdown() {
const { register } = useFormContext();

/** 게시물 카테고리 조회 */
const { data: categories } = useQuery({
const { data: categories } = useQuery<NoticeCategoryType[]>({
queryKey: [QUERY_KEYS.getNoticeCategories],
queryFn: getNoticeCategories,
staleTime: Infinity,
Expand Down
Loading

0 comments on commit b61b7ca

Please sign in to comment.