From 769547ef686d231da9577e6ea836c91c8667fe13 Mon Sep 17 00:00:00 2001
From: Jungu Lee <100949102+jobkaeHenry@users.noreply.github.com>
Date: Wed, 29 Nov 2023 22:52:45 +0900
Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C=20(#61)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Minor : Icon 추가
* New : 유저정보 변경 뮤테이션 추가
* New : 파일 삭제 뮤테이션 추가
* Refactor : 쿼리 인벨리데이션과정 추가
* New : 정보 수정 서버 URL 추가
* Refactor : 앱바, 드로워 컴포넌트 분리
* New : 프로필 수정기능 적용
---
.../app/(protectedRoute)/new-post/page.tsx | 10 +-
client/src/app/user/[userId]/layout.tsx | 6 +-
.../icons/{CameraIcon.svg => PictureIcon.svg} | 0
client/src/assets/icons/badge/CameraIcon.svg | 3 +
client/src/components/CustomAppbar.tsx | 59 +++++--
.../src/components/CustomSwipeableDrawer.tsx | 49 ++++++
.../info/drawer/UserInfoEditingDrawer.tsx | 20 +++
.../user/info/drawer/UserInfoEditingForm.tsx | 154 ++++++++++++++++++
client/src/components/wiki/WikiAppbar.tsx | 4 +-
.../wiki/searchDrawer/WikiSearchDrawer.tsx | 32 +---
client/src/const/serverPath.ts | 38 +++--
.../queries/attach/useDeleteAttachMutation.ts | 27 +++
.../queries/attach/useNewAttachMutation.ts | 38 ++++-
.../queries/user/usePatchUserInfoMutation.ts | 33 ++++
14 files changed, 407 insertions(+), 66 deletions(-)
rename client/src/assets/icons/{CameraIcon.svg => PictureIcon.svg} (100%)
create mode 100644 client/src/assets/icons/badge/CameraIcon.svg
create mode 100644 client/src/components/CustomSwipeableDrawer.tsx
create mode 100644 client/src/components/user/info/drawer/UserInfoEditingDrawer.tsx
create mode 100644 client/src/components/user/info/drawer/UserInfoEditingForm.tsx
create mode 100644 client/src/queries/attach/useDeleteAttachMutation.ts
create mode 100644 client/src/queries/user/usePatchUserInfoMutation.ts
diff --git a/client/src/app/(protectedRoute)/new-post/page.tsx b/client/src/app/(protectedRoute)/new-post/page.tsx
index 9bcd9d7..1ed3056 100644
--- a/client/src/app/(protectedRoute)/new-post/page.tsx
+++ b/client/src/app/(protectedRoute)/new-post/page.tsx
@@ -5,7 +5,7 @@ import { Box, Container, Paper, Tooltip } from "@mui/material";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import HOME from "@/const/clientPath";
-import CameraIcon from "@/assets/icons/CameraIcon.svg";
+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";
@@ -90,9 +90,9 @@ export default function NewpostPage() {
{/* 최상단 앱바 */}
@@ -125,7 +125,7 @@ export default function NewpostPage() {
}
+ iconComponent={}
>
{
return (
{
+ appendButton={isMyProfile ? "설정" : undefined}
+ onClickAppend={() => {
if (!isMyProfile) {
return;
}
@@ -42,6 +43,7 @@ const UserInfoPageLayout = ({ children, params }: Props) => {
p: 2,
}}
>
+
{children}
diff --git a/client/src/assets/icons/CameraIcon.svg b/client/src/assets/icons/PictureIcon.svg
similarity index 100%
rename from client/src/assets/icons/CameraIcon.svg
rename to client/src/assets/icons/PictureIcon.svg
diff --git a/client/src/assets/icons/badge/CameraIcon.svg b/client/src/assets/icons/badge/CameraIcon.svg
new file mode 100644
index 0000000..acf9f31
--- /dev/null
+++ b/client/src/assets/icons/badge/CameraIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/src/components/CustomAppbar.tsx b/client/src/components/CustomAppbar.tsx
index 1b3638e..8a1fb5c 100644
--- a/client/src/components/CustomAppbar.tsx
+++ b/client/src/components/CustomAppbar.tsx
@@ -1,42 +1,62 @@
"use client";
-import { AppBar, Button, IconButton, Toolbar, Typography } from "@mui/material";
+import {
+ AppBar,
+ Button,
+ IconButton,
+ Toolbar,
+ Typography,
+ styled,
+} from "@mui/material";
import GoBackIcon from "@/assets/icons/GoBackIcon.svg";
import { MouseEventHandler, ReactNode, memo } from "react";
import { useRouter } from "next/navigation";
interface CustomAppbarInterface {
title?: string;
- buttonComponent?: ReactNode;
- disableButton?: boolean;
- onClickButton?: MouseEventHandler;
+ prependButton?: ReactNode;
+ onClickPrepend?: MouseEventHandler;
+
+ appendButton?: ReactNode;
+ disableAppend?: boolean;
+ onClickAppend?: MouseEventHandler;
}
const CustomAppbar = ({
title,
- buttonComponent,
- disableButton,
- onClickButton,
+ appendButton,
+ prependButton,
+ onClickPrepend,
+ disableAppend,
+ onClickAppend,
}: CustomAppbarInterface) => {
const router = useRouter();
return (
- router.back()}>
-
-
+ {/* 프리팬드 버튼 */}
+ {prependButton ? (
+
+ {prependButton}
+
+ ) : (
+ router.back()}>
+
+
+ )}
+ {/* 타이틀 */}
{title}
- {buttonComponent ? (
-
+ {appendButton}
+
) : (
)}
@@ -44,5 +64,10 @@ const CustomAppbar = ({
);
};
+const AppbarButton = styled(Button)(() => ({
+ minWidth: 40,
+ fontWeight: "medium",
+ fontSize:'18px'
+}));
export default memo(CustomAppbar);
diff --git a/client/src/components/CustomSwipeableDrawer.tsx b/client/src/components/CustomSwipeableDrawer.tsx
new file mode 100644
index 0000000..b4abefa
--- /dev/null
+++ b/client/src/components/CustomSwipeableDrawer.tsx
@@ -0,0 +1,49 @@
+import {
+ Box,
+ SwipeableDrawer,
+ SwipeableDrawerProps,
+ styled,
+} from "@mui/material";
+import React from "react";
+
+interface Props extends SwipeableDrawerProps {}
+
+const CustomSwipeableDrawer = ({ open, onOpen, onClose, children }: Props) => {
+ const pullerBleed = 24;
+
+ return (
+
+
+ {children}
+
+ );
+};
+
+const Puller = styled(Box)(() => ({
+ width: 56,
+ height: 4,
+ backgroundColor: "#F6EAFB",
+ borderRadius: 3,
+ position: "absolute",
+ top: 8,
+ left: "calc(50% - 28px)",
+}));
+
+export default CustomSwipeableDrawer;
diff --git a/client/src/components/user/info/drawer/UserInfoEditingDrawer.tsx b/client/src/components/user/info/drawer/UserInfoEditingDrawer.tsx
new file mode 100644
index 0000000..cead415
--- /dev/null
+++ b/client/src/components/user/info/drawer/UserInfoEditingDrawer.tsx
@@ -0,0 +1,20 @@
+import CustomSwipeableDrawer from "@/components/CustomSwipeableDrawer";
+import UserPageContext from "@/store/user/UserPageContext";
+import { useContext } from "react";
+import UserInfoEditingForm from "./UserInfoEditingForm";
+
+const UserInfoEditingDrawer = () => {
+ const { isEditing, setIsEditing } = useContext(UserPageContext);
+
+ return (
+ setIsEditing(false)}
+ onOpen={() => setIsEditing(true)}
+ >
+
+
+ );
+};
+
+export default UserInfoEditingDrawer;
diff --git a/client/src/components/user/info/drawer/UserInfoEditingForm.tsx b/client/src/components/user/info/drawer/UserInfoEditingForm.tsx
new file mode 100644
index 0000000..e0867b3
--- /dev/null
+++ b/client/src/components/user/info/drawer/UserInfoEditingForm.tsx
@@ -0,0 +1,154 @@
+"use client";
+import { useMyInfoQuery } from "@/queries/auth/useMyInfoQuery";
+import {
+ Stack,
+ Avatar,
+ Typography,
+ TextField,
+ Button,
+ Badge,
+} from "@mui/material";
+import CameraIcon from "@/assets/icons/badge/CameraIcon.svg";
+import { useCallback, useContext, useEffect, useState } from "react";
+import useNewAttachMutation from "@/queries/attach/useNewAttachMutation";
+import useDeleteAttachMutation from "@/queries/attach/useDeleteAttachMutation";
+import UserPageContext from "@/store/user/UserPageContext";
+import CustomAppbar from "@/components/CustomAppbar";
+import { useGlobalLoadingStore } from "./../../../../store/useGlobalLoadingStore";
+import usePatchUserInfoMutation from "@/queries/user/usePatchUserInfoMutation";
+
+const UserInfoEditingForm = () => {
+ const { setIsEditing } = useContext(UserPageContext);
+
+ const { isLoading, setLoading } = useGlobalLoadingStore();
+
+ const { data } = useMyInfoQuery();
+
+ const [introduction, setIntroduction] = useState(data?.introduction);
+ const [file, setFile] = useState();
+ const [fileUrl, setFileUrl] = useState(null);
+
+ const { mutateAsync: attachFile } = useNewAttachMutation();
+ const { mutateAsync: removeFile } = useDeleteAttachMutation();
+ const { mutateAsync: patchUserInfo } = usePatchUserInfoMutation();
+
+ const submitFileHandler = useCallback(
+ async (file: File) => {
+ if (!data) {
+ return;
+ }
+ await attachFile({
+ file: file,
+ url: { type: "PROFILE", pk: Number(data?.userNo) },
+ });
+ },
+ [data]
+ );
+
+ const submitHandler = async () => {
+ setLoading(true);
+ await patchUserInfo({ introduction });
+ if (file) {
+ data?.profileImages.forEach(async (profile) => {
+ await removeFile(String(profile.attachNo));
+ });
+ await submitFileHandler(file);
+ }
+ setIsEditing(false);
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ if (!file) {
+ return;
+ }
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onloadend = () => setFileUrl(reader.result);
+ }, [file]);
+
+ return (
+ <>
+ setIsEditing(false)}
+ onClickAppend={submitHandler}
+ appendButton={"저장"}
+ disableAppend={isLoading}
+ />
+
+
+
+
+
+ {"닉네임"}
+
+
+
+
+
+ {"자기소개"}
+
+ {
+ setIntroduction(target.value);
+ }}
+ helperText={
+ "나에 대해 소개해보세요, 좋아하는 술, 술의 맛, 음식 뭐든 좋아요"
+ }
+ size="small"
+ fullWidth
+ autoComplete="off"
+ multiline
+ rows={3}
+ />
+
+
+
+
+ >
+ );
+};
+
+export default UserInfoEditingForm;
diff --git a/client/src/components/wiki/WikiAppbar.tsx b/client/src/components/wiki/WikiAppbar.tsx
index 2e7b6f2..21d31fe 100644
--- a/client/src/components/wiki/WikiAppbar.tsx
+++ b/client/src/components/wiki/WikiAppbar.tsx
@@ -9,8 +9,8 @@ const WikiAppbar = () => {
return (
}
- onClickButton={() => setIsSearching(true)}
+ appendButton={}
+ onClickAppend={() => setIsSearching(true)}
/>
);
};
diff --git a/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
index 2538faf..a07c2bd 100644
--- a/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
+++ b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
@@ -2,46 +2,24 @@ import { SwipeableDrawer, Stack, styled, Box } from "@mui/material";
import { useContext } from "react";
import WikiPageContext from "@/store/wiki/WikiPageContext";
import WikiSerachArea from "@/components/wiki/searchDrawer/WikiSerachArea";
+import CustomSwipeableDrawer from "@/components/CustomSwipeableDrawer";
const WikiSearchDrawer = () => {
const { isSearching, setIsSearching } = useContext(WikiPageContext);
- const pullerBleed = 24;
+
return (
- setIsSearching(true)}
onClose={() => setIsSearching(false)}
- anchor="bottom"
- disableSwipeToOpen
- PaperProps={{
- sx: {
- p: 2,
- borderTopLeftRadius: pullerBleed,
- borderTopRightRadius: pullerBleed,
- overFlow: "hidden",
- },
- }}
- ModalProps={{
- keepMounted: false,
- }}
>
-
-
+
-
+
);
};
export default WikiSearchDrawer;
-const Puller = styled(Box)(() => ({
- width: 56,
- height: 4,
- backgroundColor: "#F6EAFB",
- borderRadius: 3,
- position: "absolute",
- top: 8,
- left: "calc(50% - 28px)",
-}));
diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts
index 1460064..cb0dc7b 100644
--- a/client/src/const/serverPath.ts
+++ b/client/src/const/serverPath.ts
@@ -12,6 +12,11 @@ export const SIGNUP_API_PATH = "/user/signup" as const;
*/
export const MY_INFO = "/user/me" as const;
+/**
+ * 유저정보를 수정하는 path
+ */
+export const PATCH_USER_INFO = '/user' as const
+
/**
* 쿠키를 심어주는 로그인 BFF
*/
@@ -28,7 +33,7 @@ export const POST_LIST = "/posts" as const;
/**
* ID(pk) 를 입력받아 해당 포스트를 지우는 URL
*/
-export const REMOVE_POST = (pk:number)=>`${POST_LIST}/${pk}` as const
+export const REMOVE_POST = (pk: number) => `${POST_LIST}/${pk}` as const;
/**
*
@@ -37,42 +42,51 @@ export const REMOVE_POST = (pk:number)=>`${POST_LIST}/${pk}` as const
* @returns
*/
export type ATTACH_FILE_ResourceType = "POST" | "PROFILE" | "ALCOHOL";
-export const ATTACH_FILE = (type: ATTACH_FILE_ResourceType, resourcePk: number) =>
- `/attach/resources/${type}/${resourcePk}` as const;
+export const ATTACH_FILE = (
+ type: ATTACH_FILE_ResourceType,
+ resourcePk: number
+) => `/attach/resources/${type}/${resourcePk}` as const;
+
+/**
+ * 파일PK 를 입력받아 해당 파일을 제거하는 URL
+ * @param attachNo 파일 PK
+ */
+export const REMOVE_FILE = (attachNo: string) => `/attach/${attachNo}` as const;
/**
* 알콜리스트를 받아오는 URL
*/
-export const GET_ALCOHOL_LIST = '/alcohols' as const
+export const GET_ALCOHOL_LIST = "/alcohols" as const;
/**
* 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요를 요청
* @param id 게시글의 PK
*/
-export const POST_LIKE_URL = (id:string)=>`/posts/like/${id}` as const
+export const POST_LIKE_URL = (id: string) => `/posts/like/${id}` as const;
/**
* 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요 취소를 요청
* @param id 게시글의 PK
*/
-export const POST_UN_LIKE_URL = (id:string)=>`/posts/like-cancel/${id}` as const
+export const POST_UN_LIKE_URL = (id: string) =>
+ `/posts/like-cancel/${id}` as const;
/**
* 유저 ID 를 입력받아 해당 유저의 정보를 불러오는 URL
* @param id 유저 PK
- * @returns
+ * @returns
*/
-export const USER_SUMMARY = (id:string)=>`/user/${id}/summary` as const
+export const USER_SUMMARY = (id: string) => `/user/${id}/summary` as const;
/**
* 유저 ID 를 입력받아 해당 유저를 팔로우 하는 URL
* @param id 유저 PK
- * @returns
+ * @returns
*/
-export const FOLLOW_USER = (id:string) => `/user/follow/${id}` as const
+export const FOLLOW_USER = (id: string) => `/user/follow/${id}` as const;
/**
* 유저 ID 를 입력받아 해당 유저를 언팔로우 하는 URL
* @param id 유저 PK
- * @returns
+ * @returns
*/
-export const UNFOLLOW_USER = (id:string) => `/user/unfollow/${id}` as const
\ No newline at end of file
+export const UNFOLLOW_USER = (id: string) => `/user/unfollow/${id}` as const;
diff --git a/client/src/queries/attach/useDeleteAttachMutation.ts b/client/src/queries/attach/useDeleteAttachMutation.ts
new file mode 100644
index 0000000..3d49248
--- /dev/null
+++ b/client/src/queries/attach/useDeleteAttachMutation.ts
@@ -0,0 +1,27 @@
+import { REMOVE_FILE } from "@/const/serverPath";
+import { axiosPrivate } from "@/libs/axios";
+import { useErrorHandler } from "@/utils/errorHandler";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+const useDeleteAttachMutation = () => {
+ const errorHandler = useErrorHandler();
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (attachNo: string) =>
+ await deleteAttachMutationFn(attachNo),
+ onError: (error) => {
+ errorHandler(error);
+ },
+ onSuccess: () => {
+ // queryClient.invalidateQueries({ queryKey: [] });
+ },
+ });
+};
+
+export const deleteAttachMutationFn = async (attachNo: string) => {
+ const { data } = await axiosPrivate.delete(REMOVE_FILE(attachNo));
+ return data;
+};
+
+export default useDeleteAttachMutation;
diff --git a/client/src/queries/attach/useNewAttachMutation.ts b/client/src/queries/attach/useNewAttachMutation.ts
index 980d2ac..da3842b 100644
--- a/client/src/queries/attach/useNewAttachMutation.ts
+++ b/client/src/queries/attach/useNewAttachMutation.ts
@@ -1,11 +1,47 @@
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { axiosPrivate } from "@/libs/axios";
import { ATTACH_FILE, ATTACH_FILE_ResourceType } from "@/const/serverPath";
+import { useErrorHandler } from "./../../utils/errorHandler";
+import { getPostListInfiniteQueryKey } from "./../post/useGetPostListInfiniteQuery";
+import { postDetailQueryKey } from "../post/useGetPostDetailQuery";
+import { MyInfoQueryKeys } from "../auth/useMyInfoQuery";
+import { UserInfoQueryKey } from "../user/useUserInfoQuery";
export const useNewAttachMutation = () => {
+ const errorHandler = useErrorHandler();
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: async (param: { file: File; url: NewAttatchRequestUrl }) =>
await postImageFn(param.file, param.url),
+ onMutate: (variables) => {
+ return variables;
+ },
+ onSuccess: (_data, variables, context) => {
+ switch (variables.url.type) {
+ case "POST":
+ queryClient.invalidateQueries({
+ queryKey: [getPostListInfiniteQueryKey.all],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [postDetailQueryKey.byId(String(context?.url.pk))],
+ });
+ return;
+ case "PROFILE":
+ queryClient.invalidateQueries({ queryKey: MyInfoQueryKeys.all });
+ queryClient.invalidateQueries({
+ queryKey: UserInfoQueryKey.byId(String(context?.url.pk)),
+ });
+ queryClient.invalidateQueries({
+ queryKey: getPostListInfiniteQueryKey.byKeyword({
+ userNo: String(context?.url.pk),
+ }),
+ });
+ case "ALCOHOL":
+ }
+ },
+ onError: (error) => {
+ errorHandler(error);
+ },
});
};
diff --git a/client/src/queries/user/usePatchUserInfoMutation.ts b/client/src/queries/user/usePatchUserInfoMutation.ts
new file mode 100644
index 0000000..bda40cf
--- /dev/null
+++ b/client/src/queries/user/usePatchUserInfoMutation.ts
@@ -0,0 +1,33 @@
+import { PATCH_USER_INFO } from "@/const/serverPath";
+import { axiosPrivate } from "@/libs/axios";
+import { UserInfoInterface } from "@/types/user/userInfoInterface";
+import { useErrorHandler } from "@/utils/errorHandler";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { UserInfoQueryKey } from "./useUserInfoQuery";
+import { useMyInfoQuery } from "../auth/useMyInfoQuery";
+
+const usePatchUserInfoMutation = () => {
+ const errorHandler = useErrorHandler();
+ const queryClient = useQueryClient();
+ const { data: MyInfo } = useMyInfoQuery();
+
+ return useMutation({
+ mutationFn: async (info: Partial) =>
+ patchUserInfoMutateFn(info),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: UserInfoQueryKey.byId(String(MyInfo?.userNo)),
+ });
+ },
+ onError: (err) => errorHandler(err),
+ });
+};
+
+export const patchUserInfoMutateFn = async (
+ info: Partial
+) => {
+ const { data } = await axiosPrivate.patch(PATCH_USER_INFO, { ...info });
+ return data;
+};
+
+export default usePatchUserInfoMutation;