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;