Skip to content

Commit

Permalink
[#523] [모임 상세] 모임 수정 페이지 (#527)
Browse files Browse the repository at this point in the history
* chore: DatePicker, TextArea 컴포넌트 use client 지시문 작성

* refactor: InputLength 컴포넌트 수정

* feat: 독서모임 수정 페이지 마크업

* fix: ChakraUI theme에서 input[type="date"] 속성 제거

* fix: DatePicker 컴포넌트 gap 속성 추가

* feat: 모임 수정 페이지 폼 기능 구현

* refactor: 독서 모임 수정 페이지 개선

* chore: BookGroupTitleForm을 BookGroupEditTitleForm로 수정

* refactor: TextArea 컴포넌트 defaultValue props 타입 정의

* fix: 독서모임 수정 폼 수정(defaultValue 추가)

* feat: useBookGroupEditCurrentInfo 쿼리 작성

* feat: 모임정보 수정 기능 구현

* fix: next.config에 images.unuptimized:true 옵션 추가

* chore: 불필요한 코드 제거

* fix: 빌드에러 수정

* fix: next.config 내부 images.unoptimized 옵션 제거

* feat: 독서 모임 수정 reqBody 타입 작성

* refactor: formContext 타입 정의

* feat: 그룹 정보 수정 mutation 쿼리 훅 작성

* feat: 모임수정 타입명 수정, isOwner 타입 추가

* feat: 페이지 접근 권한 추가 (로그인 & 모임 장)
  • Loading branch information
hanyugeon authored Apr 22, 2024
1 parent a6ed111 commit 7e628e4
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 66 deletions.
125 changes: 92 additions & 33 deletions src/app/group/[groupId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,102 @@
'use client';

import GroupAPI from '@/apis/group';
import { APIGroupDetail } from '@/types/group';
import AuthRequired from '@/ui/AuthRequired';
import TopNavigation from '@/ui/common/TopNavigation';
import EditGroupForm from '@/ui/Group/EditGroupForm';
import { VStack } from '@chakra-ui/react';
import { useCallback, useEffect, useState } from 'react';

const GroupEditPage = ({
import { notFound, useRouter } from 'next/navigation';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';

import {
useBookGroupEditCurrentInfo,
useBookGroupInfoMutation,
} from '@/queries/group/useBookGroupQuery';
import type { APIGroupDetail, APIEditBookGroup } from '@/types/group';

import { SERVICE_ERROR_MESSAGE } from '@/constants';
import {
checkAuthentication,
isAxiosErrorWithCustomCode,
} from '@/utils/helpers';

import useToast from '@/v1/base/Toast/useToast';
import BookGroupEditDateForm from '@/v1/bookGroup/edit/BookGroupEditDateForm';
import BookGroupEditIntroduceForm from '@/v1/bookGroup/edit/BookGroupEditIntroduceForm';
import BookGroupEditTitleForm from '@/v1/bookGroup/edit/BookGroupEditTitleForm';
import BookGroupEditTopNavigation from '@/v1/bookGroup/edit/BookGroupEditTopNavigation';

const BookGroupEditPage = ({
params: { groupId },
}: {
params: { groupId: number };
params: { groupId: APIGroupDetail['bookGroupId'] };
}) => {
const [group, setGroup] = useState<APIGroupDetail>();

const getGroup = useCallback(async () => {
try {
const { data } = await GroupAPI.getGroupDetailInfo({
bookGroupId: groupId,
});
setGroup(data);
} catch (error) {
console.error(error);
}
}, [groupId]);

useEffect(() => {
getGroup();
}, [getGroup]);
const router = useRouter();

const isAuthenticated = checkAuthentication();

const { data: bookGroupData } = useBookGroupEditCurrentInfo(groupId);
const { isOwner, title, description, maxMemberCount, startDate, endDate } =
bookGroupData;

if (!isAuthenticated || !isOwner) {
notFound();
}

const bookGroupEdit = useBookGroupInfoMutation(groupId);

const { show: showToast } = useToast();

const methods = useForm<Omit<APIEditBookGroup, 'isOwner'>>({
mode: 'all',
defaultValues: {
title: title,
introduce: description,
maxMemberCount: maxMemberCount ? maxMemberCount : 9999,
startDate: startDate,
endDate: endDate,
},
});

const handleFormSubmit: SubmitHandler<
Omit<APIEditBookGroup, 'isOwner' | 'startDate'>
> = async ({ title, introduce, maxMemberCount, endDate }) => {
bookGroupEdit.mutate(
{ title, introduce, maxMemberCount, endDate },
{
onSuccess: () => {
router.push(`/group/${groupId}`);

showToast({ type: 'success', message: '모임 정보를 수정했어요! 🎉' });
return;
},
onError: error => {
if (isAxiosErrorWithCustomCode(error)) {
const { code } = error.response.data;
const message = SERVICE_ERROR_MESSAGE[code];

showToast({ type: 'error', message });
return;
}

showToast({
type: 'error',
message: '모임 정보 수정을 실패했어요 🥲',
});
},
}
);
};

return (
<AuthRequired>
<VStack justify="center" align="center">
<TopNavigation pageTitle="모임 수정" />
{group && <EditGroupForm group={group} />}
</VStack>
</AuthRequired>
<FormProvider {...methods}>
<BookGroupEditTopNavigation onSubmit={handleFormSubmit} />

<form
className="mt-[2.5rem] flex flex-col gap-[3.2rem]"
onSubmit={methods.handleSubmit(handleFormSubmit)}
>
<BookGroupEditTitleForm />
<BookGroupEditIntroduceForm />
<BookGroupEditDateForm />
</form>
</FormProvider>
);
};

export default GroupEditPage;
export default BookGroupEditPage;
51 changes: 44 additions & 7 deletions src/queries/group/useBookGroupQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { UseQueryOptions } from '@tanstack/react-query';
import {
useMutation,
useQueryClient,
UseQueryOptions,
} from '@tanstack/react-query';

import { APIGroupDetail, BookGroupDetail } from '@/types/group';
import { isExpired } from '@/utils/date';
import GroupAPI from '@/apis/group';
import type {
APIGroupDetail,
BookGroupDetail,
APIEditBookGroup,
} from '@/types/group';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import groupAPI from '@/apis/group';
import { isExpired } from '@/utils/date';

import bookGroupKeys from './key';

Expand All @@ -26,9 +34,9 @@ export const useBookGroupQuery = <TData = APIGroupDetail>(
useQueryWithSuspense(
bookGroupKeys.detail(groupId),
() =>
GroupAPI.getGroupDetailInfo({ bookGroupId: groupId }).then(
({ data }) => data
),
groupAPI
.getGroupDetailInfo({ bookGroupId: groupId })
.then(({ data }) => data),
options
);

Expand Down Expand Up @@ -56,3 +64,32 @@ export const useBookGroupJoinInfo = (groupId: APIGroupDetail['bookGroupId']) =>
question: data.joinQuestion,
}),
});

export const useBookGroupEditCurrentInfo = (
groupId: APIGroupDetail['bookGroupId']
) =>
useBookGroupQuery(groupId, {
select: data => ({
isOwner: data.isOwner,
title: data.title,
description: data.introduce,
maxMemberCount: data.maxMemberCount,
startDate: data.startDate,
endDate: data.endDate,
}),
});

export const useBookGroupInfoMutation = (
bookGroupId: APIGroupDetail['bookGroupId']
) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (group: Omit<APIEditBookGroup, 'isOwner' | 'startDate'>) =>
groupAPI.updateGroupInfo({ bookGroupId, group }).then(({ data }) => data),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: bookGroupKeys.detail(bookGroupId),
}),
});
};
18 changes: 0 additions & 18 deletions src/styles/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,24 +107,6 @@ const theme: ThemeOverride = extendTheme({
buttonSizes,
colors,
scheme,
styles: {
global: {
'input[type="date"]': {
position: 'relative',
},
'input[type="date"]::-webkit-inner-spin-button, input[type="date"]::-webkit-calendar-picker-indicator':
{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
background: 'transparent',
color: 'transparent',
cursor: 'pointer',
},
},
},
shadows,
});

Expand Down
9 changes: 9 additions & 0 deletions src/types/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,12 @@ export type BookGroupComment = {
createdAt: APIGroupComment['createdAt'];
content: APIGroupComment['contents'];
};

export type APIEditBookGroup = {
isOwner: APIGroupDetail['isOwner'];
title: APIGroupDetail['title'];
introduce: APIGroupDetail['introduce'];
maxMemberCount: APIGroupDetail['maxMemberCount'];
startDate: APIGroupDetail['startDate'];
endDate: APIGroupDetail['endDate'];
};
4 changes: 3 additions & 1 deletion src/v1/base/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import {
ChangeEventHandler,
forwardRef,
Expand Down Expand Up @@ -51,7 +53,7 @@ const DatePicker = (

return (
<label
className={`relative flex h-[3rem] max-w-[14rem] items-center justify-between bg-transparent ${disabledClasses}`}
className={`relative flex h-[3rem] max-w-[14rem] items-center justify-between gap-[0.5rem] bg-transparent ${disabledClasses}`}
htmlFor={name}
>
<div className="flex h-full min-w-0 flex-grow items-center">
Expand Down
8 changes: 4 additions & 4 deletions src/v1/base/InputLength.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
type InputLengthProps = {
currentLength: number;
isError: boolean;
maxLength: number;
currentLength?: number;
isError?: boolean;
maxLength?: number;
};

const InputLength = ({
currentLength,
isError,
isError = false,
maxLength,
}: InputLengthProps) => {
const textColor = isError ? 'text-warning-800 ' : 'text-main-900';
Expand Down
10 changes: 7 additions & 3 deletions src/v1/base/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import {
ChangeEventHandler,
ForwardedRef,
Expand All @@ -16,6 +18,7 @@ import InputLength from './InputLength';
interface BaseTextAreaProps
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: boolean;
defaultValue?: string;
}
interface TextAreaProps extends BaseTextAreaProps {
count?: boolean;
Expand All @@ -24,6 +27,7 @@ interface TextAreaProps extends BaseTextAreaProps {
const _TextArea = (
{
maxLength = 500,
defaultValue,
count = false,
error = false,
onChange,
Expand All @@ -32,7 +36,7 @@ const _TextArea = (
}: PropsWithChildren<TextAreaProps>,
ref: ForwardedRef<HTMLTextAreaElement>
) => {
const [value, setValue] = useState('');
const [value, setValue] = useState(defaultValue || '');

const handleChange: ChangeEventHandler<HTMLTextAreaElement> = e => {
setValue(e.target.value);
Expand Down Expand Up @@ -68,13 +72,13 @@ const TextArea = Object.assign(forwardRef(_TextArea), {
Error: ErrorMessage,
});

const ErrorMeesageType = (<ErrorMessage />).type;
const ErrorMessageType = (<ErrorMessage />).type;

const getErrorChildren = (children: ReactNode) => {
const childrenArray = Children.toArray(children);

return childrenArray.find(
child => isValidElement(child) && child.type === ErrorMeesageType
child => isValidElement(child) && child.type === ErrorMessageType
);
};

Expand Down
50 changes: 50 additions & 0 deletions src/v1/bookGroup/edit/BookGroupEditDateForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useFormContext } from 'react-hook-form';

import type { APIEditBookGroup } from '@/types/group';

import DatePicker from '@/v1/base/DatePicker';
import ErrorMessage from '@/v1/base/ErrorMessage';

type EditDateFormTypes = Pick<APIEditBookGroup, 'startDate' | 'endDate'>;

const BookGroupEditDateForm = () => {
const {
register,
formState: { errors, defaultValues },
} = useFormContext<EditDateFormTypes>();

return (
<>
<section className="flex justify-between">
<div>
<h2 className="text-md text-black-500">모임 시작일</h2>
<p className="text-xs text-placeholder">
모임 시작일은 수정할 수 없어요
</p>
</div>
<DatePicker disabled={true} {...register('startDate')} />
</section>
<section className="flex flex-col gap-[0.5rem]">
<div className="flex justify-between">
<h2 className="text-md text-black-700">모임 종료일</h2>
<DatePicker
{...register('endDate', {
required: { value: true, message: '종료일을 입력해주세요' },
min: {
value: defaultValues?.startDate as string,
message: '종료일은 시작일보다 늦어야 해요',
},
})}
/>
</div>
<div>
{errors.endDate && (
<ErrorMessage>{errors.endDate.message}</ErrorMessage>
)}
</div>
</section>
</>
);
};

export default BookGroupEditDateForm;
Loading

0 comments on commit 7e628e4

Please sign in to comment.