diff --git a/src/api/services/group/group.api.ts b/src/api/services/group/group.api.ts index 5e610d27..615c3b39 100644 --- a/src/api/services/group/group.api.ts +++ b/src/api/services/group/group.api.ts @@ -26,6 +26,13 @@ export const useGroupInfo = (groupId: number) => { }) } +export const useGroupInfoSuspense = (groupId: number) => { + return useSuspenseQuery({ + queryKey: ['group', groupId], + queryFn: () => getGroupInfo(groupId), + }) +} + type GroupResponse = PagingResponse[]> const getGroupPaging = async (params: PagingRequestParams) => { @@ -159,3 +166,25 @@ export const approveGroupQuestion = async ( ) return response.data } + +export type ModifyGroupImgRequestBody = { + groupId: number + image: File +} + +export const modifyGroupImg = async ({ + groupId, + image, +}: ModifyGroupImgRequestBody) => { + const formData = new FormData() + formData.append('image', image) + const response = await authorizationInstance.patch( + `/api/group/modify/image/${groupId}`, + formData, + { + headers: { 'Content-Type': 'multipart/form-data' }, + } + ) + + return response.data +} diff --git a/src/api/services/profile/my-page.api.ts b/src/api/services/profile/my-page.api.ts index f9dfc7ef..85a8ee25 100644 --- a/src/api/services/profile/my-page.api.ts +++ b/src/api/services/profile/my-page.api.ts @@ -1,21 +1,28 @@ -import { useQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { authorizationInstance, fetchInstance } from '@/api/instance' import { MyPageItem, UserRankingItem } from '@/types' -export const getMyPage = async (userId: string) => { +export const getMyPage = async (userId: number) => { const response = await fetchInstance.get(`/api/profile/${userId}`) return response.data } -export const useMyPage = (userId: string) => { +export const useMyPage = (userId: number) => { return useQuery({ queryKey: ['myPage', userId], queryFn: () => getMyPage(userId), }) } +export const useMyPageSuspense = (userId: number) => { + return useSuspenseQuery({ + queryKey: ['myPage', userId], + queryFn: () => getMyPage(userId), + }) +} + type UploadProfileBgRequest = { image: File } diff --git a/src/components/AvatarLabel/index.tsx b/src/components/AvatarLabel/index.tsx new file mode 100644 index 00000000..754abf73 --- /dev/null +++ b/src/components/AvatarLabel/index.tsx @@ -0,0 +1,22 @@ +import { Avatar, HStack, StackProps, Text } from '@chakra-ui/react' + +import { colors } from '@/styles/colors' + +interface AvatarLabelProps extends StackProps { + avatarSrc?: string + label: string +} + +export const AvatarLabel = ({ avatarSrc, label }: AvatarLabelProps) => { + return ( + + + {label} + + ) +} diff --git a/src/components/AvatarLabelWithNavigate/index.tsx b/src/components/AvatarLabelWithNavigate/index.tsx deleted file mode 100644 index 43892501..00000000 --- a/src/components/AvatarLabelWithNavigate/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useNavigate } from 'react-router-dom' - -import { Avatar, HStack, StackProps, Text } from '@chakra-ui/react' - -interface AvatarLabelWithNavigateProps extends StackProps { - avatarSrc?: string - label: string - linkTo?: string -} - -export const AvatarLabelWithNavigate = ({ - avatarSrc, - label, - linkTo, -}: AvatarLabelWithNavigateProps) => { - const navigate = useNavigate() - - if (linkTo) { - return ( - navigate(linkTo)}> - - {label} - - ) - } - - return ( - - - {label} - - ) -} diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx index 18043cc4..397893ad 100644 --- a/src/components/Form/FormContext.tsx +++ b/src/components/Form/FormContext.tsx @@ -38,6 +38,7 @@ export const useFormField = () => { name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, ...fieldState, } } diff --git a/src/components/Form/index.tsx b/src/components/Form/index.tsx index cf827d3d..5ae80b48 100644 --- a/src/components/Form/index.tsx +++ b/src/components/Form/index.tsx @@ -13,9 +13,17 @@ const FormItem = FormItemProvider const FormControl = forwardRef( ({ ...props }, ref) => { - const { error, formItemId } = useFormField() + const { error, formItemId, formMessageId } = useFormField() - return + return ( + + ) } ) FormControl.displayName = 'FormControl' @@ -37,4 +45,28 @@ const FormDescription = forwardRef( ) FormDescription.displayName = 'FormDescription' -export { Form, FormField, FormItem, FormControl, FormDescription } +const FormMessage = forwardRef( + ({ children, ...props }, ref) => { + const { error, formMessageId } = useFormField() + const body = error ? String(error?.message) : children + + if (!body) { + return null + } + + return ( + + {body} + + ) + } +) +FormMessage.displayName = 'FormMessage' + +export { Form, FormField, FormItem, FormControl, FormDescription, FormMessage } diff --git a/src/pages/CreateGroupPage/CreateGroupForm/index.tsx b/src/pages/CreateGroupPage/CreateGroupForm/index.tsx index 39e9ad33..90b468c7 100644 --- a/src/pages/CreateGroupPage/CreateGroupForm/index.tsx +++ b/src/pages/CreateGroupPage/CreateGroupForm/index.tsx @@ -9,7 +9,7 @@ import { FormField, FormItem, } from '@/components/Form' -import { CreateGroupFields } from '@/schema/create-group' +import { CreateGroupFields } from '@/schema/group' interface CreateGroupFormProps { form: UseFormReturn diff --git a/src/pages/CreateGroupPage/index.tsx b/src/pages/CreateGroupPage/index.tsx index 24bebc18..825c100c 100644 --- a/src/pages/CreateGroupPage/index.tsx +++ b/src/pages/CreateGroupPage/index.tsx @@ -14,7 +14,7 @@ import { } from '@/api/services/group/group.api' import cookies from '@/assets/cookies.svg' import { AlertModal } from '@/components/Modal/AlertModal' -import { CreateGroupFields, CreateGroupSchema } from '@/schema/create-group' +import { CreateGroupFields, CreateGroupSchema } from '@/schema/group' import { Group } from '@/types' import { CreateGroupForm } from './CreateGroupForm' diff --git a/src/pages/GroupMembersPage/MembersTable/index.tsx b/src/pages/GroupMembersPage/MembersTable/index.tsx index 8013180b..b74dab96 100644 --- a/src/pages/GroupMembersPage/MembersTable/index.tsx +++ b/src/pages/GroupMembersPage/MembersTable/index.tsx @@ -40,7 +40,7 @@ export default function MembersTable({ data: profile, status: profileStatus, isError: isProfileError, - } = useMyPage(myUserId.toString()) + } = useMyPage(myUserId) const { data: memberList, diff --git a/src/pages/GroupPage/GroupProfile/ImgModify/index.tsx b/src/pages/GroupPage/GroupProfile/ImgModify/index.tsx new file mode 100644 index 00000000..b6bc7f5e --- /dev/null +++ b/src/pages/GroupPage/GroupProfile/ImgModify/index.tsx @@ -0,0 +1,122 @@ +import { ChangeEvent, useState } from 'react' +import { useForm } from 'react-hook-form' +import { BiError } from 'react-icons/bi' + +import { Box, Input, useDisclosure } from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation } from '@tanstack/react-query' + +import { queryClient } from '@/api/instance' +import { + ModifyGroupImgRequestBody, + modifyGroupImg, +} from '@/api/services/group/group.api' +import { Form, FormControl, FormField, FormItem } from '@/components/Form' +import { AlertModal } from '@/components/Modal/AlertModal' +import { ModifyGroupImageFields, ModifyGroupImageSchema } from '@/schema/group' +import { Group, GroupRole } from '@/types' + +type ImgModifyProps = { + gprofile: Group + role: GroupRole +} + +export default function ImgModify({ role, gprofile }: ImgModifyProps) { + const form = useForm({ + resolver: zodResolver(ModifyGroupImageSchema), + mode: 'onSubmit', + defaultValues: { + groupId: gprofile.groupId, + image: new File([], ''), + }, + }) + + const [errorMessage, setErrorMessage] = useState('') + const errorModal = useDisclosure() + + const { mutate: uploadImage } = useMutation({ + mutationFn: (data: ModifyGroupImgRequestBody) => modifyGroupImg(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['group', gprofile.groupId] }) + }, + }) + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : null + + if (file) { + form.setValue('image', file) + form.handleSubmit( + () => { + uploadImage(form.getValues()) + }, + (errors) => { + const errorMessages = + Object.values(errors).flatMap((error) => error.message)[0] || '' + + setErrorMessage(errorMessages) + errorModal.onOpen() + } + )() + } + } + + if (role === 'MEMBER') { + return ( + + ) + } + + return ( + document.getElementById('fileInput')?.click()} + backgroundImage={`url('${gprofile.groupImageUrl}')`} + backgroundSize="cover" + backgroundPosition="center" + borderRadius="100%" + > +
+ + ( + + + + + + )} + /> + + + } + title={errorMessage} + description="" + /> +
+ ) +} diff --git a/src/pages/GroupPage/GroupProfile/ModifySuccessModal/index.tsx b/src/pages/GroupPage/GroupProfile/ModifySuccessModal/index.tsx new file mode 100644 index 00000000..4e0f1217 --- /dev/null +++ b/src/pages/GroupPage/GroupProfile/ModifySuccessModal/index.tsx @@ -0,0 +1,24 @@ +import { BiCheckCircle } from 'react-icons/bi' + +import { AlertModal } from '@/components/Modal/AlertModal' +import { Modal } from '@/types' + +interface ModifySuccessModalProps { + successModal: Modal +} + +export const ModifySuccessModal = ({ + successModal, +}: ModifySuccessModalProps) => { + return ( + { + successModal.onClose() + }} + icon={} + title="그룹 이름과 소개를 수정하였습니다" + description="" + /> + ) +} diff --git a/src/pages/GroupPage/GroupProfile/ProfileFormField/index.tsx b/src/pages/GroupPage/GroupProfile/ProfileFormField/index.tsx new file mode 100644 index 00000000..4b114f12 --- /dev/null +++ b/src/pages/GroupPage/GroupProfile/ProfileFormField/index.tsx @@ -0,0 +1,50 @@ +import { UseFormReturn } from 'react-hook-form' + +import { Input } from '@chakra-ui/react' + +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/Form' +import { ModifyGroupFields } from '@/schema/group' + +interface GroupFormFieldProps { + form: UseFormReturn + name: keyof ModifyGroupFields + size: 'xl' | 'md' + width: string +} + +export const GroupFormField = ({ + form, + name, + size, + width, +}: GroupFormFieldProps) => { + return ( + ( + + + + + + + )} + /> + ) +} diff --git a/src/pages/GroupPage/GroupProfile/index.tsx b/src/pages/GroupPage/GroupProfile/index.tsx new file mode 100644 index 00000000..33ee68c2 --- /dev/null +++ b/src/pages/GroupPage/GroupProfile/index.tsx @@ -0,0 +1,149 @@ +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import { BiCheck, BiEditAlt } from 'react-icons/bi' + +import { Center, Flex, Text, useDisclosure } from '@chakra-ui/react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation } from '@tanstack/react-query' + +import { queryClient } from '@/api/instance' +import { + ModifyGroupRequestBody, + modifyGroup, +} from '@/api/services/group/group.api' +import { Form } from '@/components/Form' +import { ModifyGroupFields, ModifyGroupSchema } from '@/schema/group' +import { colors } from '@/styles/colors' +import { Group, GroupRole } from '@/types' + +import ImgModify from './ImgModify' +import { ModifySuccessModal } from './ModifySuccessModal' +import { GroupFormField } from './ProfileFormField' + +interface GroupProfileProps { + group: Group + role: GroupRole +} + +export const GroupProfile = ({ group, role }: GroupProfileProps) => { + const form = useForm({ + resolver: zodResolver(ModifyGroupSchema), + mode: 'onChange', + defaultValues: { + groupId: group.groupId, + groupName: group.groupName, + description: group.groupDescription, + }, + }) + + const [isEdit, setIsEdit] = useState(false) + const successModal = useDisclosure() + + const { mutate } = useMutation({ + mutationFn: (groupFields: ModifyGroupRequestBody) => + modifyGroup(groupFields), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['group', group.groupId] }) + queryClient.invalidateQueries({ queryKey: ['groups'] }) + successModal.onOpen() + }, + }) + + const handleModifyProfile = form.handleSubmit(() => { + mutate(form.getValues()) + setIsEdit(!isEdit) + }) + + const onClickEditButton = () => { + if (!isEdit) { + setIsEdit(!isEdit) + return + } + + handleModifyProfile() + } + + useEffect(() => { + form.reset({ + groupId: group.groupId, + groupName: group.groupName, + description: group.groupDescription, + }) + setIsEdit(false) + }, [group, form]) + + return ( + + +
+ + + + {isEdit ? ( + + ) : ( + {group.groupName} + )} + + {role === 'LEADER' ? '그룹장' : '그룹원'} + + + + {isEdit ? ( + + ) : ( + + {group.groupDescription} + + )} + {role === 'LEADER' && ( +
+ {isEdit ? : } +
+ )} +
+
+
+ + {successModal.isOpen && ( + + )} +
+ ) +} diff --git a/src/pages/GroupPage/Management/CreateQuestion/CreateQuestionForm/index.tsx b/src/pages/GroupPage/Management/CreateQuestion/CreateQuestionForm/index.tsx new file mode 100644 index 00000000..fdbc27a0 --- /dev/null +++ b/src/pages/GroupPage/Management/CreateQuestion/CreateQuestionForm/index.tsx @@ -0,0 +1,58 @@ +import { UseFormReturn } from 'react-hook-form' + +import { Flex, Text, Textarea } from '@chakra-ui/react' + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from '@/components/Form' +import { CreateQuestionFields } from '@/schema/group' + +interface CreateQuestionFormProps { + form: UseFormReturn +} + +export const CreateQuestionForm = ({ form }: CreateQuestionFormProps) => { + return ( +
+ + ( + + + + 질문 +