diff --git a/src/api/axios/article/_type.ts b/src/api/axios/article/_type.ts index 12fb8533..f37e2141 100644 --- a/src/api/axios/article/_type.ts +++ b/src/api/axios/article/_type.ts @@ -59,3 +59,14 @@ export interface PostArticleRequest { } export type PostArticleResponse = ArticleListItem; + +export interface PatchArticleRequest { + articleId: number; + body: { + image?: string | null; + content?: string; + title?: string; + }; +} + +export type PatchArticleResponse = ArticleDetail; diff --git a/src/api/axios/article/patchArticle.ts b/src/api/axios/article/patchArticle.ts new file mode 100644 index 00000000..63c7fed8 --- /dev/null +++ b/src/api/axios/article/patchArticle.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { PatchArticleRequest, PatchArticleResponse } from "./_type"; + +const patchArticle = async ({ articleId, body }: PatchArticleRequest) => { + const { data } = await instance.patch(`/articles/${articleId}`, body); + return data; +}; + +export default patchArticle; diff --git a/src/api/axios/index.ts b/src/api/axios/index.ts index e0994366..1b303843 100644 --- a/src/api/axios/index.ts +++ b/src/api/axios/index.ts @@ -36,3 +36,4 @@ export { default as deleteArticleComment } from "./articleComment/deleteArticleC export { default as patchArticleComment } from "./articleComment/patchArticleComment"; export { default as getInvitation } from "./group/getInvitation"; export { default as deleteMember } from "./group/deleteMember"; +export { default as patchArticle } from "./article/patchArticle"; diff --git a/src/api/hooks/article/useGetArticlesInfinite.ts b/src/api/hooks/article/useGetArticlesInfinite.ts new file mode 100644 index 00000000..8e27b0d1 --- /dev/null +++ b/src/api/hooks/article/useGetArticlesInfinite.ts @@ -0,0 +1,35 @@ +"use client"; + +import { getArticles } from "@/api/axios"; +import { GetArticlesRequest } from "@/api/axios/article/_type"; +import { useInfiniteQuery } from "@tanstack/react-query"; + +type UseGetArticlesInfiniteParams = Omit & { + pageSize?: number; +}; + +const useGetArticlesInfinite = (params: UseGetArticlesInfiniteParams) => { + const { pageSize = 6, ...rest } = params; + + return useInfiniteQuery({ + queryKey: ["articles", rest], + queryFn: ({ pageParam = 1 }) => + getArticles({ + ...rest, + page: pageParam, + pageSize, + }), + initialPageParam: 1, + + getNextPageParam: (lastPage, allPages) => { + const loadedCount = allPages.reduce((acc, page) => acc + page.list.length, 0); + + return loadedCount < lastPage.totalCount ? allPages.length + 1 : undefined; + }, + + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 60 * 24, + }); +}; + +export default useGetArticlesInfinite; diff --git a/src/api/hooks/article/usePatchArticle.ts b/src/api/hooks/article/usePatchArticle.ts new file mode 100644 index 00000000..63983e4b --- /dev/null +++ b/src/api/hooks/article/usePatchArticle.ts @@ -0,0 +1,23 @@ +import { patchArticle } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const usePatchArticle = () => { + const { success, error } = toastKit(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["patchArticle"], + mutationFn: patchArticle, + onSuccess: (data) => { + const articleId = data.id; + success("게시물을 성공적으로 수정하였습니다."); + queryClient.invalidateQueries({ queryKey: ["article", articleId] }); + }, + onError: () => { + error("게시물을 수정하지 못하였습니다."); + }, + }); +}; + +export default usePatchArticle; diff --git a/src/api/hooks/article/usePostArticle.ts b/src/api/hooks/article/usePostArticle.ts index d53c4d47..0452c011 100644 --- a/src/api/hooks/article/usePostArticle.ts +++ b/src/api/hooks/article/usePostArticle.ts @@ -1,16 +1,18 @@ import { postArticle } from "@/api/axios"; import { toastKit } from "@/utils"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; const usePostArticle = () => { const router = useRouter(); + const queryClient = useQueryClient(); const { success, error } = toastKit(); return useMutation({ mutationFn: postArticle, onSuccess: (data) => { success("게시물 등록을 성공했습니다."); - router.push(`/dashboard/${data.id}`); + queryClient.invalidateQueries({ queryKey: ["articles"] }); + router.replace(`/dashboard/${data.id}`); }, onError: () => { error("게시물을 등록하지 못하였습니다."); diff --git a/src/api/hooks/group/usePatchGroup.ts b/src/api/hooks/group/usePatchGroup.ts index ed6113cd..f06e18c9 100644 --- a/src/api/hooks/group/usePatchGroup.ts +++ b/src/api/hooks/group/usePatchGroup.ts @@ -1,11 +1,9 @@ import { patchGroup } from "@/api/axios"; import { toastKit } from "@/utils"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useRouter } from "next/navigation"; const usePatchGroup = () => { const { success, error } = toastKit(); - const router = useRouter(); const queryClient = useQueryClient(); return useMutation({ mutationFn: patchGroup, @@ -13,7 +11,6 @@ const usePatchGroup = () => { success("팀 이름을 성공적으로 변경하였습니다."); queryClient.invalidateQueries({ queryKey: ["groups"] }); queryClient.invalidateQueries({ queryKey: ["user"] }); - router.back(); }, onError: () => { error("팀 이름을 변경하지 못했습니다."); diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index bcbe1531..25b919cd 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -35,3 +35,4 @@ export { default as useDeleteArticleComment } from "./articleComment/useDeleteAr export { default as usePatchArticleComment } from "./articleComment/usePatchArticleComment"; export { default as useGetInvitation } from "./group/useGetInvitation"; export { default as useDeleteMember } from "./group/useDeleteMember"; +export { default as usePatchArticle } from "./article/usePatchArticle"; diff --git a/src/app/(route)/dashboard/[id]/_components/ArticleDetail.tsx b/src/app/(route)/dashboard/[id]/_components/ArticleDetail.tsx index ef3eb80b..d3076906 100644 --- a/src/app/(route)/dashboard/[id]/_components/ArticleDetail.tsx +++ b/src/app/(route)/dashboard/[id]/_components/ArticleDetail.tsx @@ -1,11 +1,17 @@ import ArticleBody from "./_internal/ArticleBody"; import ArticleComments from "./_internal/ArticleComments"; +import { LinkButton } from "@/common"; const ArticleDetail = () => { return (
+ + + 목록으로 + +
); }; diff --git a/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx b/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx new file mode 100644 index 00000000..bb14eb87 --- /dev/null +++ b/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { usePatchArticle } from "@/api/hooks"; +import { BaseButton, Input, InputBox, Modal, Icon, FloatingButton } from "@/common"; +import { ArticleDetail } from "@/types/ArticleType"; +import { ChangeEvent, useState } from "react"; +import Image from "next/image"; +import useImageUpload from "@/hooks/useImageUpload"; + +interface ArticleEditModalProps { + isOpen: boolean; + onClose: () => void; + article: ArticleDetail; +} + +interface FormStateType { + title: string; + content: string; + image: string | null; +} + +const ArticleEditModal = ({ isOpen, onClose, article }: ArticleEditModalProps) => { + const { mutate: patchArticle, isPending } = usePatchArticle(); + + const [formState, setFormState] = useState({ + title: article.title, + content: article.content, + image: article.image, + }); + + const { preview, file, handleImageChange, uploadImage, isUploading, clear } = useImageUpload( + article.image ?? undefined, + ); + + const isDisabledEditButton = isPending || isUploading || !formState.title.trim() || !formState.content.trim(); + + const handleTextChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormState((prev) => ({ ...prev, [name]: value })); + }; + + const handleRemoveImage = () => { + clear(); + setFormState((prev) => ({ + ...prev, + image: null, + })); + }; + + const handleEditClick = async () => { + const { title, content } = formState; + + let imageUrl: string | null = null; + + if (file) { + imageUrl = await uploadImage(); + } + const newForm = { + title, + content, + image: imageUrl !== null ? imageUrl : formState.image === null ? null : formState.image, + }; + patchArticle( + { + articleId: article.id, + body: newForm, + }, + { + onSuccess: () => { + onClose(); + }, + }, + ); + }; + + return ( + + +

게시글 수정하기

+ +
+ + + + + +
+ + { + const selectedFile = e.target.files?.[0]; + if (selectedFile) handleImageChange(selectedFile); + }} + /> + +
+ + + {preview && ( + + )} +
+
+ +
+ + + + 닫기 + + + + {isPending || isUploading ? "수정 중..." : "수정하기"} + + +
+ ); +}; + +export default ArticleEditModal; diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx index bd5e22fd..5d18ee31 100644 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx +++ b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx @@ -8,7 +8,7 @@ import ArticleTitle from "../../../_components/Article/_internal/ArticleTitle"; import ArticleWriter from "../../../_components/Article/_internal/ArticleWriter"; import ArticleContent from "../../../_components/Article/_internal/ArticleContent"; import ArticleLikeButton from "./ArticleLikeButton"; -import ArticleEditModal from "./ArticleEditModal"; +import ArticleEditModal from "../Modal/ArticleEditModal"; import { useState } from "react"; const ArticleBody = () => { @@ -46,12 +46,19 @@ const ArticleBody = () => {
- +
- setIsOpenEditModal(false)} /> + {isOpenEditModal && ( + setIsOpenEditModal(false)} + article={article} + /> + )} ); }; diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleEditModal.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleEditModal.tsx deleted file mode 100644 index 29ef4832..00000000 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleEditModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Modal } from "@/common"; - -interface ArticleEditModalProps { - isOpen: boolean; - onClose: () => void; -} - -const ArticleEditModal = ({ isOpen, onClose }: ArticleEditModalProps) => { - return ( - - -

게시글 수정하기

-
-
- ); -}; - -export default ArticleEditModal; diff --git a/src/app/(route)/dashboard/[id]/page.tsx b/src/app/(route)/dashboard/[id]/page.tsx index 93232390..2f6d81ff 100644 --- a/src/app/(route)/dashboard/[id]/page.tsx +++ b/src/app/(route)/dashboard/[id]/page.tsx @@ -1,5 +1,11 @@ import { PageLayout } from "@/common"; import { ArticleDetail } from "./_components"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Coworkers | 자유게시판", + description: "게시글을 통해 팀원들과 소통해보세요.", +}; const DashBoardDetail = () => { return ( diff --git a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx index 9ceff105..8208028b 100644 --- a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx +++ b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import Image from "next/image"; import { useGetArticle } from "@/api/hooks"; import { Icon } from "@/common"; import ArticleBestBadge from "./_internal/ArticleBestBadge"; @@ -17,14 +16,12 @@ const BestArticleCard = ({ articleId }: { articleId: number }) => { return ( -
+
-
-
- - -
- {/* {article.image && 게시글 이미지} */} + +
+ +