diff --git a/client/src/app/(protectedRoute)/edit-post/[pid]/layout.tsx b/client/src/app/(protectedRoute)/edit-post/[pid]/layout.tsx
new file mode 100644
index 0000000..ca6c5a7
--- /dev/null
+++ b/client/src/app/(protectedRoute)/edit-post/[pid]/layout.tsx
@@ -0,0 +1,24 @@
+import CustomAppbar from "@/components/layout/CustomAppbar";
+import { ReactNode, Suspense } from "react";
+import CustomContainer from "@/components/layout/CustomContainer";
+
+type Props = {
+ children: ReactNode;
+};
+
+const layout = ({ children }: Props) => {
+ return (
+
+
+
+ >
+ }
+ >
+ {children}
+
+ );
+};
+
+export default layout;
diff --git a/client/src/app/(protectedRoute)/edit-post/[pid]/page.tsx b/client/src/app/(protectedRoute)/edit-post/[pid]/page.tsx
new file mode 100644
index 0000000..29845a7
--- /dev/null
+++ b/client/src/app/(protectedRoute)/edit-post/[pid]/page.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import HOME from "@/const/clientPath";
+import { useGlobalLoadingStore } from "@/store/useGlobalLoadingStore";
+import { useInvalidatePostList } from "@/queries/post/useGetPostListInfiniteQuery";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+
+import CustomAppbar from "@/components/layout/CustomAppbar";
+import CustomContainer from "@/components/layout/CustomContainer";
+import useSubmitEditPostMutation from "@/queries/newPost/useSubmitEditPostMutation";
+import PostEditor from "@/components/newpost/PostEditor";
+import useGetPostDetailQuery, { useInvalidatePostDetail } from "@/queries/post/useGetPostDetailQuery";
+
+export default function EditPostPage({ params }: { params: { pid: string } }) {
+ const { setLoading } = useGlobalLoadingStore();
+
+ const router = useRouter();
+ const invalidatePreviousPost = useInvalidatePostList();
+ const invalidatePostDetail = useInvalidatePostDetail()
+
+ const { data: initialData } = useGetPostDetailQuery(params.pid);
+ const { alcoholName, alcoholNo, alcoholType, postContent, postAttachUrls,postNo } =
+ initialData;
+
+ const [formData, setFormData] = useState();
+ const [file, setFile] = useState();
+
+ const { mutateAsync: submitHandler, isSuccess } = useSubmitEditPostMutation({
+ onMutate: () => setLoading(true),
+ onSuccess: () => {
+ invalidatePreviousPost();
+ invalidatePostDetail(String(postNo))
+ router.push(HOME);
+ },
+ onSettled: () => setLoading(false),
+ });
+
+ return (
+ <>
+ {/* 최상단 앱바 */}
+
+ formData &&
+ submitHandler({
+ postNo: String(initialData.postNo),
+ formData,
+ file,
+ prevFileNo: initialData.postAttachUrls?.[0]?.attachNo,
+ })
+ }
+ />
+
+ setFormData(formData)}
+ onFileChange={(file) => setFile(file)}
+ initialAlcohol={
+ alcoholName ? { alcoholName, alcoholNo, alcoholType } : undefined
+ }
+ initialContent={postContent}
+ initialImage={(postAttachUrls ?? [])[0]?.attachUrl}
+ />
+
+ >
+ );
+}
diff --git a/client/src/app/(protectedRoute)/new-post/page.tsx b/client/src/app/(protectedRoute)/new-post/page.tsx
index 182667b..373b8f4 100644
--- a/client/src/app/(protectedRoute)/new-post/page.tsx
+++ b/client/src/app/(protectedRoute)/new-post/page.tsx
@@ -1,128 +1,47 @@
"use client";
-import { Box, Container, Paper, Tooltip } from "@mui/material";
-
import { useRouter } from "next/navigation";
-import { useCallback, useState } from "react";
+import { useState } from "react";
import HOME from "@/const/clientPath";
-import PictureIcon from "@/assets/icons/PictureIcon.svg";
-import PinIcon from "@/assets/icons/PinIcon.svg";
import { useGlobalLoadingStore } from "@/store/useGlobalLoadingStore";
-import useNewPostMutation from "@/queries/newPost/useNewPostMutation";
-import useNewAttachMutation from "@/queries/attach/useNewAttachMutation";
import { useInvalidatePostList } from "@/queries/post/useGetPostListInfiniteQuery";
-import { useDeletePostMutation } from "@/queries/post/useDeletePostMutation";
-import {
- NewPostRequestInterface,
- NewPostRequestAlCohol,
-} from "@/types/newPost/NewPostInterface";
-import SearchAlcoholInput from "@/components/newpost/SearchAlcoholInput";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+
import CustomAppbar from "@/components/layout/CustomAppbar";
-import SquareIconButton from "@/components/SquareIconButton";
-import PreviewImageByURL from "@/components/PreviewImageByURL";
-import NewPostTextEditor from "@/components/newpost/NewPostTextEditor";
-import useRenderAsDataUrl from "@/hooks/useRenderAsDataUrl";
-import SingleImageInput from "@/components/SingleImageInput";
-import { POST_IMAGE_SIZE } from "@/const/imageSize";
import CustomContainer from "@/components/layout/CustomContainer";
+import useSubmitPostMutation from "@/queries/newPost/useSubmitPostMutation";
+import PostEditor from "@/components/newpost/PostEditor";
export default function NewpostPage() {
const { setLoading } = useGlobalLoadingStore();
+
const router = useRouter();
const invalidatePreviousPost = useInvalidatePostList();
- const [formValue, setFormValue] = useState({
- postContent: "",
- postType: "BASIC",
- positionInfo: "",
- tagList: [] as string[],
- });
-
- const [alcoholNo, setAlcoholNo] =
- useState();
-
+ const [formValue, setFormValue] = useState();
const [file, setFile] = useState();
- const fileUrl = useRenderAsDataUrl(file);
- const [isSuccess, SetIsSuccess] = useState(false);
-
- const { mutateAsync: newPostHandler } = useNewPostMutation();
- const { mutateAsync: attachFileHandler } = useNewAttachMutation();
- const { mutateAsync: deletePostHandler } = useDeletePostMutation();
-
- const submitHandler = useCallback(
- async (formValue: NewPostRequestInterface, file?: File) => {
- setLoading(true);
- let postNo;
- try {
- const { postNo: res } = await newPostHandler(formValue);
- postNo = res;
- if (file) {
- try {
- await attachFileHandler({
- file,
- url: { pk: postNo, type: "POST" },
- size: POST_IMAGE_SIZE,
- });
- } catch {
- deletePostHandler(postNo);
- return;
- }
- }
- invalidatePreviousPost();
- SetIsSuccess(true);
- router.push(HOME);
- } catch {
- return;
- } finally {
- setLoading(false);
- }
+ const { mutateAsync: submitHandler, isSuccess } = useSubmitPostMutation({
+ onMutate: () => setLoading(true),
+ onSuccess: () => {
+ invalidatePreviousPost();
+ router.push(HOME);
},
- [router]
- );
+ onSettled: () => setLoading(false),
+ });
return (
-
+ <>
{/* 최상단 앱바 */}
submitHandler({ ...formValue, alcoholNo }, file)}
+ onClickAppend={() => formValue && submitHandler({ formValue, file })}
/>
-
- {/* 검색창 */}
-
- {/* 내용 */}
-
- setFormValue((prev) => ({
- ...prev,
- postContent: content,
- tagList,
- }))
- }
- />
- {/* 파일 미리보기 */}
- {fileUrl && }
- {/* 버튼 그룹 */}
-
- {/* 사진 */}
-
- }
- >
- setFile(file)} />
-
-
- {/* 위치 */}
-
- } />
-
-
+
-
+ >
);
}
diff --git a/client/src/components/PreviewImageByURL.tsx b/client/src/components/PreviewImageByURL.tsx
index 9a561d3..1d903b8 100644
--- a/client/src/components/PreviewImageByURL.tsx
+++ b/client/src/components/PreviewImageByURL.tsx
@@ -14,13 +14,13 @@ const PreviewImageByURL = (
sx={{
backgroundImage: `url(${fileUrl})`,
width: "100%",
- height: 142,
borderRadius: 4,
border: "1px solid",
borderColor: "gray.secondary",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition: "center",
+ aspectRatio: 2.36,
...sx,
}}
ref={ref}
diff --git a/client/src/components/newpost/NewPostTextEditor.tsx b/client/src/components/newpost/NewPostTextEditor.tsx
index 2058f1b..fe48693 100644
--- a/client/src/components/newpost/NewPostTextEditor.tsx
+++ b/client/src/components/newpost/NewPostTextEditor.tsx
@@ -9,15 +9,20 @@ import { sanitize } from "isomorphic-dompurify";
interface NewPostTextEditorInterface {
onContentChange: (props: { content: string; tagList: string[] }) => void;
- maxLength?:number
+ initialValue?: string;
+ maxLength?: number;
}
-const NewPostTextEditor = ({ onContentChange,maxLength=200 }: NewPostTextEditorInterface) => {
+const NewPostTextEditor = ({
+ onContentChange,
+ maxLength = 200,
+ initialValue,
+}: NewPostTextEditorInterface) => {
const [_mentioningValue, setMentioningValue] = useState("");
const [tagList, setTagList] = useState([]);
- const [content, setContent] = useState("");
+ const [content, setContent] = useState(initialValue ?? "");
const [textLength, setTextLength] = useState(0);
useEffect(() => {
@@ -60,6 +65,7 @@ const NewPostTextEditor = ({ onContentChange,maxLength=200 }: NewPostTextEditorI
modules={modules}
placeholder="입력해주세요"
onChange={(content, _d, _s, editor) => {
+ const textLength = editor.getLength() - 1;
const parsedTags = editor
.getContents()
.filter((op) => op.insert?.mention?.value)
@@ -68,8 +74,18 @@ const NewPostTextEditor = ({ onContentChange,maxLength=200 }: NewPostTextEditorI
[]
);
setTagList(parsedTags);
- setContent(content);
- setTextLength(editor.getLength() - 1);
+ setTextLength((prev) => {
+ if (textLength < maxLength) {
+ return textLength;
+ }
+ return prev;
+ });
+ setContent((prev) => {
+ if (textLength < maxLength) {
+ return content;
+ }
+ return prev;
+ });
}}
value={content}
/>
diff --git a/client/src/components/newpost/PostEditor.tsx b/client/src/components/newpost/PostEditor.tsx
new file mode 100644
index 0000000..7de2236
--- /dev/null
+++ b/client/src/components/newpost/PostEditor.tsx
@@ -0,0 +1,100 @@
+import { Box, Tooltip } from "@mui/material";
+import SquareIconButton from "@/components/SquareIconButton";
+import PreviewImageByURL from "@/components/PreviewImageByURL";
+import NewPostTextEditor from "@/components/newpost/NewPostTextEditor";
+import SearchAlcoholInput from "@/components/newpost/SearchAlcoholInput";
+import PictureIcon from "@/assets/icons/PictureIcon.svg";
+import PinIcon from "@/assets/icons/PinIcon.svg";
+import SingleImageInput from "@/components/SingleImageInput";
+import { useEffect, useState } from "react";
+import useRenderAsDataUrl from "@/hooks/useRenderAsDataUrl";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface";
+
+interface PostEditorProps {
+ initialContent?: string;
+ initialAlcohol?: Pick<
+ AlcoholDetailInterface,
+ "alcoholName" | "alcoholNo" | "alcoholType"
+ >;
+ initialImage?: string;
+ /**
+ * formvalue와 file 을 인자로 넘겨주는 커스텀이벤트핸들러
+ * @param props {formvalue,file}
+ * @returns
+ */
+ onFormChange: (formValue: NewPostRequestInterface) => void;
+ onFileChange: (file?: File) => void;
+}
+
+const PostEditor = ({
+ initialContent,
+ initialAlcohol,
+ initialImage,
+ onFormChange,
+ onFileChange,
+}: PostEditorProps) => {
+ const [formValue, setFormValue] = useState({
+ postType: "BASIC",
+ positionInfo: "",
+ tagList: [] as string[],
+ });
+
+ useEffect(() => {
+ onFormChange(formValue);
+ }, [formValue]);
+
+ const [file, setFile] = useState();
+
+ useEffect(() => {
+ onFileChange(file);
+ }, [file]);
+
+ const fileUrl = useRenderAsDataUrl(file) ?? initialImage;
+
+ return (
+ <>
+ {/* 검색창 */}
+
+ setFormValue((prev) => ({ ...prev, alcoholNo }))
+ }
+ />
+ {/* 내용 */}
+
+ setFormValue((prev) => ({
+ ...prev,
+ postContent: content,
+ tagList,
+ }))
+ }
+ initialValue={initialContent}
+ />
+ {/* 파일 미리보기 */}
+ {(fileUrl) && (
+
+ )}
+ {/* 버튼 그룹 */}
+
+ {/* 사진 */}
+
+ }>
+ {
+ setFile(file);
+ }}
+ />
+
+
+ {/* 위치 */}
+
+ } />
+
+
+ >
+ );
+};
+
+export default PostEditor;
diff --git a/client/src/components/newpost/SearchAlcoholInput.tsx b/client/src/components/newpost/SearchAlcoholInput.tsx
index 1d2ceb1..f2dc595 100644
--- a/client/src/components/newpost/SearchAlcoholInput.tsx
+++ b/client/src/components/newpost/SearchAlcoholInput.tsx
@@ -10,7 +10,7 @@ import {
Typography,
InputAdornment,
} from "@mui/material";
-import { Dispatch, SetStateAction, memo, useEffect, useState } from "react";
+import { memo, useEffect, useState } from "react";
import AlcholeSearchIcon from "@/assets/icons/AlcholeSearchIcon.svg";
import InputSearchIcon from "@/assets/icons/InputSearchIcon.svg";
import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery";
@@ -20,11 +20,18 @@ import useDebounce from "@/hooks/useDebounce";
import { NewPostRequestAlCohol } from "@/types/newPost/NewPostInterface";
interface SearchAlcoholInputInterface {
- setAlcoholNo: Dispatch>;
+ setAlcoholNo: (alcoholNo: NewPostRequestAlCohol["alcoholNo"]) => void;
+ initialValue?: Pick<
+ AlcoholDetailInterface,
+ "alcoholName" | "alcoholNo" | "alcoholType"
+ >;
}
-const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => {
+const SearchAlcoholInput = ({
+ setAlcoholNo,
+ initialValue,
+}: SearchAlcoholInputInterface) => {
// 유저가 검색한 키워드
- const [searchKeyword, setSearchKeyword] = useState();
+ const [searchKeyword, setSearchKeyword] = useState("");
// 검색한 키워드의 Debounced 값
const debouncedValue = useDebounce(searchKeyword, 300);
const [isSearchingAlcohol, setIsSearchingAlCohol] = useState(false);
@@ -32,11 +39,13 @@ const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => {
// 검색결과
const { data, isLoading, isSuccess } = useGetAlcoholListQuery(debouncedValue);
// 유저가 검색후 최종적으로 선택한 값
- const [selectedAlcohol, setSelectedAlcohol] =
- useState();
+ const [selectedAlcohol, setSelectedAlcohol] = useState<
+ | Pick
+ | undefined
+ >(initialValue);
useEffect(() => {
- setSearchKeyword(selectedAlcohol?.alcoholName);
+ setSearchKeyword(selectedAlcohol?.alcoholName ?? "");
setAlcoholNo(selectedAlcohol?.alcoholNo);
}, [selectedAlcohol]);
diff --git a/client/src/components/post/PostCardOptionDropdown.tsx b/client/src/components/post/PostCardOptionDropdown.tsx
index da4155e..4988fc6 100644
--- a/client/src/components/post/PostCardOptionDropdown.tsx
+++ b/client/src/components/post/PostCardOptionDropdown.tsx
@@ -1,10 +1,7 @@
-import React, { useState } from "react";
-import { MoreVertOutlined } from "@mui/icons-material";
-import { ButtonBase, Menu, MenuItem } from "@mui/material";
import { useDeletePostMutation } from "@/queries/post/useDeletePostMutation";
import useDeleteAttachMutation from "@/queries/attach/useDeleteAttachMutation";
import { useRouter } from "next/navigation";
-import HOME from "@/const/clientPath";
+import HOME, { EDIT_POST } from "@/const/clientPath";
import DeleteEditDropdown from "../DeleteEditDropdown";
type PostCardOptionDropdownProps = {
@@ -17,6 +14,7 @@ const PostCardOptionDropdown = ({
filePk,
}: PostCardOptionDropdownProps) => {
const router = useRouter();
+
const { mutateAsync: deletePost } = useDeletePostMutation();
const { mutateAsync: deleteFile } = useDeleteAttachMutation();
@@ -27,8 +25,11 @@ const PostCardOptionDropdown = ({
router.push(HOME);
}
};
+ const editHandler = () => {
+ router.push(EDIT_POST(String(postId)));
+ };
- return ;
+ return ;
};
export default PostCardOptionDropdown;
diff --git a/client/src/const/clientPath.ts b/client/src/const/clientPath.ts
index f6c329a..b51e157 100644
--- a/client/src/const/clientPath.ts
+++ b/client/src/const/clientPath.ts
@@ -23,12 +23,12 @@ export const USER_PAGE = (pk: string | number) => `${MY_PROFILE}/${pk}`;
/**
* 유저가 팔로잉/팔로워 리스트페이지로 이동하는 라우트
*/
-export const USER_FOLLOW_LIST = `${MY_PROFILE}/follow-list`
+export const USER_FOLLOW_LIST = `${MY_PROFILE}/follow-list`;
/**
* 유저정보 세팅 페이지로 이동하는 라우트
*/
-export const SETTING_PAGE = `${MY_PROFILE}/setting` as const
+export const SETTING_PAGE = `${MY_PROFILE}/setting` as const;
/**
* 술과사전 페이지 라우트
@@ -69,4 +69,9 @@ export const POST_DETAIL = (userId: string, postId: string) => {
*/
export const NEW_POST = "/new-post";
+/**
+ * 포스트ID 를 입력받아 해당 포스트틀 수정하는 링크로 이동
+ */
+export const EDIT_POST = (postId: string) => `/edit-post/${postId}`;
+
export default HOME;
diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts
index b02d8ea..85d9a98 100644
--- a/client/src/const/serverPath.ts
+++ b/client/src/const/serverPath.ts
@@ -52,6 +52,12 @@ export const POST_LIST_V2 = "/posts/v2" as const;
* ID(pk) 를 입력받아 해당 포스트를 지우는 URL
*/
export const REMOVE_POST = (pk: number) => `${POST_LIST}/${pk}` as const;
+
+/**
+ * ID(pk) 를 입력받아 해당 포스트를 수정하는 URL
+ */
+export const EDIT_POST = (pk: string) => `${POST_LIST}/${pk}` as const;
+
/**
* 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요를 요청
* @param id 게시글의 PK
diff --git a/client/src/queries/newPost/useEditPostMutation.ts b/client/src/queries/newPost/useEditPostMutation.ts
new file mode 100644
index 0000000..6ff3593
--- /dev/null
+++ b/client/src/queries/newPost/useEditPostMutation.ts
@@ -0,0 +1,36 @@
+import { useMutation } from "@tanstack/react-query";
+import { EDIT_POST } from "@/const/serverPath";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+import { useErrorHandler } from "@/utils/errorHandler";
+import useAxiosPrivate from "@/hooks/useAxiosPrivate";
+
+const useEditPostMutation = () => {
+ const errorHandler = useErrorHandler();
+ return useMutation({
+ mutationFn: async ({
+ pk,
+ formData,
+ }: {
+ pk: string;
+ formData: NewPostRequestInterface;
+ }) => {
+ const data = await editPostFn(pk, formData);
+ return data;
+ },
+ onError: (err) => errorHandler(err),
+ });
+};
+
+export const editPostFn = async (
+ pk: string,
+ formData: NewPostRequestInterface
+) => {
+ const axiosPrivate = useAxiosPrivate();
+ const { data } = await axiosPrivate.patch(
+ EDIT_POST(pk),
+ formData
+ );
+ return data;
+};
+
+export default useEditPostMutation;
diff --git a/client/src/queries/newPost/useNewPostMutation.tsx b/client/src/queries/newPost/useNewPostMutation.ts
similarity index 100%
rename from client/src/queries/newPost/useNewPostMutation.tsx
rename to client/src/queries/newPost/useNewPostMutation.ts
diff --git a/client/src/queries/newPost/useSubmitEditPostMutation.tsx b/client/src/queries/newPost/useSubmitEditPostMutation.tsx
new file mode 100644
index 0000000..0d61489
--- /dev/null
+++ b/client/src/queries/newPost/useSubmitEditPostMutation.tsx
@@ -0,0 +1,82 @@
+import { POST_IMAGE_SIZE } from "@/const/imageSize";
+import useNewAttachMutation from "@/queries/attach/useNewAttachMutation";
+import { useGlobalSnackbarStore } from "@/store/useGlobalSnackbarStore";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+import { UseMutationOptions, useMutation } from "@tanstack/react-query";
+import useEditPostMutation from "./useEditPostMutation";
+import useDeleteAttachMutation from "../attach/useDeleteAttachMutation";
+
+const useSubmitEditPostMutation = (
+ options?: UseMutationOptions<
+ void,
+ Error,
+ {
+ formData: NewPostRequestInterface;
+ file?: File | undefined;
+ },
+ void
+ >
+) => {
+ const { mutateAsync: editPostHandler } = useEditPostMutation();
+ const { mutateAsync: attachFileHandler } = useNewAttachMutation();
+ const { mutateAsync: deleteFileHandler } = useDeleteAttachMutation();
+
+ const fireToast = useGlobalSnackbarStore(({ fireToast }) => fireToast);
+
+ return useMutation({
+ mutationFn: async ({
+ postNo,
+ formData,
+ file,
+ prevFileNo,
+ }: {
+ postNo: string;
+ formData: NewPostRequestInterface;
+ file?: File;
+ prevFileNo?: string;
+ }) => {
+ if (!formData.postContent?.length) {
+ fireToast("내용을 입력해주세요");
+ return;
+ }
+ // 게시글 수정시도
+ try {
+ await editPostHandler({ pk: postNo, formData });
+ } catch (error) {
+ console.error("게시글수정 실패:", error);
+ return;
+ }
+ // 게시글 수정 성공시 파일 수정시도
+ if (file) {
+ try {
+ await handleFileAttachments(file, postNo, prevFileNo);
+ } catch (error) {
+ console.error("파일처리 실패:", error);
+ }
+ }
+ },
+ ...options,
+ });
+
+ async function handleFileAttachments(
+ file: File,
+ postNo: string,
+ prevFileNo?: string
+ ) {
+ try {
+ await attachFileHandler({
+ file,
+ url: { pk: Number(postNo), type: "POST" },
+ size: POST_IMAGE_SIZE,
+ });
+ } catch (error) {
+ console.log("파일 첨부에 실패:", error);
+ }
+
+ if (prevFileNo) {
+ await deleteFileHandler(prevFileNo);
+ }
+ }
+};
+
+export default useSubmitEditPostMutation;
diff --git a/client/src/queries/newPost/useSubmitPostMutation.ts b/client/src/queries/newPost/useSubmitPostMutation.ts
new file mode 100644
index 0000000..bbbe0fe
--- /dev/null
+++ b/client/src/queries/newPost/useSubmitPostMutation.ts
@@ -0,0 +1,75 @@
+import { POST_IMAGE_SIZE } from "@/const/imageSize";
+import useNewAttachMutation from "@/queries/attach/useNewAttachMutation";
+import useNewPostMutation from "@/queries/newPost/useNewPostMutation";
+import { useDeletePostMutation } from "@/queries/post/useDeletePostMutation";
+import { useGlobalSnackbarStore } from "@/store/useGlobalSnackbarStore";
+import { NewPostRequestInterface } from "@/types/newPost/NewPostInterface";
+import { UseMutationOptions, useMutation } from "@tanstack/react-query";
+
+/**
+ * 파일, 포스트내용을 입력받아 서버에 제출하는 트렌젝션을 수행하는 뮤테이션,
+ * @param Options mutate, success, settle 시 실행될 함수
+ * @returns 파일, 포스트내용을 입력받아 제출트렌젝션을 수행하는 함수
+ */
+const useSubmitPostMutation = (
+ options?: UseMutationOptions<
+ void,
+ Error,
+ {
+ formValue: NewPostRequestInterface;
+ file?: File | undefined;
+ },
+ void
+ >
+) => {
+ const { mutateAsync: newPostHandler } = useNewPostMutation();
+ const { mutateAsync: attachFileHandler } = useNewAttachMutation();
+ const { mutateAsync: deletePostHandler } = useDeletePostMutation();
+ const fireToast = useGlobalSnackbarStore(({ fireToast }) => fireToast);
+
+ return useMutation({
+ mutationFn: async ({
+ formValue,
+ file,
+ }: {
+ formValue: NewPostRequestInterface;
+ file?: File;
+ }) => {
+ if (!formValue.postContent?.length) {
+ fireToast("내용을 입력해주세요");
+ return;
+ }
+ let postNo;
+ try {
+ const { postNo: res } = await newPostHandler(formValue);
+ postNo = res;
+ } catch (error) {
+ console.log("게시글 처리 실패 처리 실패", error);
+ return;
+ }
+ if (file) {
+ try {
+ await fileHandler(file, postNo);
+ } catch (error) {
+ console.log("파일 처리 실패", error);
+ }
+ }
+ },
+ ...options,
+ });
+
+ async function fileHandler(file: File, postNo: number) {
+ try {
+ await attachFileHandler({
+ file,
+ url: { pk: postNo, type: "POST" },
+ size: POST_IMAGE_SIZE,
+ });
+ } catch {
+ deletePostHandler(postNo);
+ return;
+ }
+ }
+};
+
+export default useSubmitPostMutation;
diff --git a/client/src/queries/post/useGetPostDetailQuery.tsx b/client/src/queries/post/useGetPostDetailQuery.tsx
index 6296303..658a9ef 100644
--- a/client/src/queries/post/useGetPostDetailQuery.tsx
+++ b/client/src/queries/post/useGetPostDetailQuery.tsx
@@ -1,4 +1,4 @@
-import { useSuspenseQuery } from "@tanstack/react-query";
+import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import axios from "@/libs/axios";
import { PostInterface } from "@/types/post/PostInterface";
import { AxiosRequestConfig } from "axios";
@@ -35,4 +35,10 @@ export const postDetailQueryKey = {
byId: (id: string) => ["post", id] as const,
};
+export const useInvalidatePostDetail = () => {
+ const queryclient = useQueryClient();
+ return (id: string) =>
+ queryclient.invalidateQueries({ queryKey: postDetailQueryKey.byId(id) });
+};
+
export default useGetPostDetailQuery;
diff --git a/client/src/utils/formatTime.ts b/client/src/utils/formatTime.ts
index c69b962..8cce193 100644
--- a/client/src/utils/formatTime.ts
+++ b/client/src/utils/formatTime.ts
@@ -13,6 +13,13 @@ const formatTime = (timestamp: string, now?: string) => {
const postTime = dayjs(timestamp);
const currentTime = dayjs(now || new Date());
+ if (
+ currentTime.diff(postTime, "day") < 7 &&
+ currentTime.diff(postTime, "day") >= 1
+ ) {
+ return `${currentTime.diff(postTime, "day")}일 전`;
+ }
+
// 1주 ~ 1달 까지는 n주 전 으로 표기
if (
currentTime.diff(postTime, "day") >= 7 &&