From c8ac979946d83a4fbaffd328f848912aa287e0be Mon Sep 17 00:00:00 2001 From: HopeFullee Date: Tue, 23 Dec 2025 18:07:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20hook=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/service/group-service/index.ts | 22 ++++++++++++--------- src/hooks/use-group/use-group-edit/index.ts | 22 +++++++++++++++++++++ src/types/service/group.ts | 3 ++- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/hooks/use-group/use-group-edit/index.ts diff --git a/src/api/service/group-service/index.ts b/src/api/service/group-service/index.ts index ee833a80..1dd79a48 100644 --- a/src/api/service/group-service/index.ts +++ b/src/api/service/group-service/index.ts @@ -7,7 +7,7 @@ import { GetGroupsResponse, GetMyGroupsPayload, GetMyGroupsResponse, - GroupIdPayload, + GroupIdParams, PreUploadGroupImageResponse, } from '@/types/service/group'; @@ -43,20 +43,24 @@ export const groupServiceRemote = () => ({ return apiV2.post('/groups/create', payload); }, - getGroupDetails: (payload: GroupIdPayload) => { - return apiV2.get(`/groups/${payload.groupId}`); + editGroup: (params: GroupIdParams, payload: CreateGroupPayload) => { + return apiV2.patch(`/groups/${params.groupId}`, payload); }, - attendGroup: (payload: GroupIdPayload) => { - return apiV2.post(`/groups/${payload.groupId}/attend`); + getGroupDetails: (params: GroupIdParams) => { + return apiV2.get(`/groups/${params.groupId}`); }, - leaveGroup: (payload: GroupIdPayload) => { - return apiV2.post(`/groups/${payload.groupId}/left`); + attendGroup: (params: GroupIdParams) => { + return apiV2.post(`/groups/${params.groupId}/attend`); }, - deleteGroup: (payload: GroupIdPayload) => { - return apiV2.delete(`/groups/${payload.groupId}`); + leaveGroup: (params: GroupIdParams) => { + return apiV2.post(`/groups/${params.groupId}/left`); + }, + + deleteGroup: (params: GroupIdParams) => { + return apiV2.delete(`/groups/${params.groupId}`); }, uploadGroupImages: (payload: FormData) => { diff --git a/src/hooks/use-group/use-group-edit/index.ts b/src/hooks/use-group/use-group-edit/index.ts new file mode 100644 index 00000000..62a5219b --- /dev/null +++ b/src/hooks/use-group/use-group-edit/index.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { groupKeys } from '@/lib/query-key/query-key-group'; +import { CreateGroupPayload, GroupIdParams } from '@/types/service/group'; + +export const useEditGroup = (params: GroupIdParams) => { + const queryClient = useQueryClient(); + + const query = useMutation({ + mutationFn: (payload: CreateGroupPayload) => API.groupService.editGroup(params, payload), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: groupKeys.detail(params.groupId) }); + + console.log('모임 생성 성공.'); + }, + onError: () => { + console.log('모임 생성 실패.'); + }, + }); + return query; +}; diff --git a/src/types/service/group.ts b/src/types/service/group.ts index 3a1aab39..331c11cf 100644 --- a/src/types/service/group.ts +++ b/src/types/service/group.ts @@ -101,6 +101,7 @@ export interface CreateGroupPayload { imageKey: string; sortOrder: number; }[]; + joinPolicy: 'FREE' | 'APPROVE'; } export interface CreateGroupResponse { @@ -200,6 +201,6 @@ export interface GetGroupDetailsResponse { }[]; } -export interface GroupIdPayload { +export interface GroupIdParams { groupId: string; } From 75f88c96b9d218c39b0284f0e3b45b85c8225c6e Mon Sep 17 00:00:00 2001 From: HopeFullee Date: Tue, 23 Dec 2025 18:15:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20nested=20layout=20?= =?UTF-8?q?=EC=9E=91=EC=97=85,=20prefetching=20=EB=B0=8F=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20redirect=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/post-meetup/[groupId]/layout.tsx | 38 ++++++++ src/app/post-meetup/[groupId]/page.tsx | 117 +++++++++++++++++++++++ src/lib/schema/group.ts | 1 + 3 files changed, 156 insertions(+) create mode 100644 src/app/post-meetup/[groupId]/layout.tsx create mode 100644 src/app/post-meetup/[groupId]/page.tsx diff --git a/src/app/post-meetup/[groupId]/layout.tsx b/src/app/post-meetup/[groupId]/layout.tsx new file mode 100644 index 00000000..50bce543 --- /dev/null +++ b/src/app/post-meetup/[groupId]/layout.tsx @@ -0,0 +1,38 @@ +import { redirect } from 'next/navigation'; + +import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/react-query'; + +import { API } from '@/api'; +import { groupKeys } from '@/lib/query-key/query-key-group'; + +interface Props { + children: React.ReactNode; + params: Promise<{ groupId: string }>; +} + +const EditMeetupLayout = async ({ children, params }: Props) => { + const queryClient = new QueryClient(); + + const { groupId } = await params; + + const { userId: sessionUserId } = await API.userService.getMe(); + + const { createdBy, status } = await queryClient.fetchQuery({ + queryKey: groupKeys.detail(groupId), + queryFn: async () => API.groupService.getGroupDetails({ groupId }), + }); + + const isHost = sessionUserId === createdBy.userId; + const isEditable = status !== 'FINISHED'; + + if (!isHost && !isEditable) { + redirect('/post-meetup'); + } + + const dehydratedState = dehydrate(queryClient); + + return {children}; +}; + +export default EditMeetupLayout; diff --git a/src/app/post-meetup/[groupId]/page.tsx b/src/app/post-meetup/[groupId]/page.tsx new file mode 100644 index 00000000..dd77d8d2 --- /dev/null +++ b/src/app/post-meetup/[groupId]/page.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { use } from 'react'; + +import { useForm } from '@tanstack/react-form'; + +import { + MeetupCapField, + MeetupDateField, + MeetupDetailField, + MeetupImagesField, + MeetupLocationField, + MeetupSubmitButton, + MeetupTagsField, + MeetupTitleField, +} from '@/components/pages/post-meetup'; +import { useEditGroup } from '@/hooks/use-group/use-group-edit'; +import { useGetGroupDetails } from '@/hooks/use-group/use-group-get-details'; +import { CreateGroupFormValues, createGroupSchema } from '@/lib/schema/group'; +import { GetGroupDetailsResponse, PreUploadGroupImageResponse } from '@/types/service/group'; + +interface Props { + params: Promise<{ groupId: string }>; +} + +const EditMeetupPage = ({ params }: Props) => { + const { groupId } = use(params); + const { replace } = useRouter(); + const { data } = useGetGroupDetails({ groupId }); + const { mutateAsync: EditGroup } = useEditGroup({ groupId }); + + const { + title, + address: { location }, + startTime, + tags, + description, + maxParticipants, + images, + } = data as GetGroupDetailsResponse; + + const { defaultImages } = convertToDefaultImages(images); + + const form = useForm({ + defaultValues: { + title, + location, + startTime, + tags, + description, + maxParticipants, + images: defaultImages, + joinPolicy: 'FREE', + } as CreateGroupFormValues, + validators: { + onChange: createGroupSchema, + onSubmit: createGroupSchema, + }, + onSubmit: async ({ value }) => { + value.images = value.images?.map((image, idx) => { + return { ...image, sortOrder: idx }; + }); + + const res = await EditGroup(value); + + replace(`/meetup/${res.id}`); + }, + }); + + return ( +
+
+
+ } name='title' /> + } name='location' /> + } name='startTime' /> + } + name='maxParticipants' + /> + } name='images' /> + } + name='description' + /> + } name='tags' /> +
+ + ( + form.handleSubmit()} /> + )} + selector={(state) => state} + /> + +
+ ); +}; + +export default EditMeetupPage; + +const convertToDefaultImages = (images: GetGroupDetailsResponse['images']) => { + const defaultImages: PreUploadGroupImageResponse['images'] = []; + + images.forEach(({ imageKey, sortOrder, variants }) => { + defaultImages.push({ + imageKey, + sortOrder, + imageUrl100x100: variants[0].imageUrl, + imageUrl440x240: variants[1].imageUrl, + }); + }); + + return { defaultImages }; +}; diff --git a/src/lib/schema/group.ts b/src/lib/schema/group.ts index a0cd3acf..afe778dd 100644 --- a/src/lib/schema/group.ts +++ b/src/lib/schema/group.ts @@ -25,6 +25,7 @@ export const createGroupSchema = z.object({ }), ) .optional(), + joinPolicy: z.union([z.literal('FREE'), z.literal('APPROVE')]), }); export type CreateGroupFormValues = z.infer; From f638fb858039a9d4af7cfc1942028578a8acc5bd Mon Sep 17 00:00:00 2001 From: HopeFullee Date: Tue, 23 Dec 2025 18:18:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20joinPolicy=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/post-meetup/page.tsx | 3 ++- src/hooks/use-group/use-group-attend/index.ts | 8 ++++---- src/hooks/use-group/use-group-delete/index.ts | 6 +++--- src/hooks/use-group/use-group-get-details/index.ts | 8 ++++---- src/hooks/use-group/use-group-leave/index.ts | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/post-meetup/page.tsx b/src/app/post-meetup/page.tsx index 6a16e467..e771ae90 100644 --- a/src/app/post-meetup/page.tsx +++ b/src/app/post-meetup/page.tsx @@ -30,6 +30,7 @@ const PostMeetupPage = () => { description: '', maxParticipants: 0, images: [], + joinPolicy: 'FREE', } as CreateGroupFormValues, validators: { onChange: createGroupSchema, @@ -40,7 +41,7 @@ const PostMeetupPage = () => { return { ...image, sortOrder: idx }; }); - const res = await createGroup({ ...value }); + const res = await createGroup(value); replace(`/meetup/${res.id}`); }, diff --git a/src/hooks/use-group/use-group-attend/index.ts b/src/hooks/use-group/use-group-attend/index.ts index 112729e9..3962ba48 100644 --- a/src/hooks/use-group/use-group-attend/index.ts +++ b/src/hooks/use-group/use-group-attend/index.ts @@ -2,15 +2,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { API } from '@/api'; import { groupKeys } from '@/lib/query-key/query-key-group'; -import { GroupIdPayload } from '@/types/service/group'; +import { GroupIdParams } from '@/types/service/group'; -export const useAttendGroup = (payload: GroupIdPayload, callback: () => void) => { +export const useAttendGroup = (params: GroupIdParams, callback: () => void) => { const queryClient = useQueryClient(); const query = useMutation({ - mutationFn: () => API.groupService.attendGroup(payload), + mutationFn: () => API.groupService.attendGroup(params), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupKeys.detail(payload.groupId) }); + await queryClient.invalidateQueries({ queryKey: groupKeys.detail(params.groupId) }); callback(); console.log('모임 참여 성공.'); }, diff --git a/src/hooks/use-group/use-group-delete/index.ts b/src/hooks/use-group/use-group-delete/index.ts index 224b5d9a..c8f4efb2 100644 --- a/src/hooks/use-group/use-group-delete/index.ts +++ b/src/hooks/use-group/use-group-delete/index.ts @@ -1,11 +1,11 @@ import { useMutation } from '@tanstack/react-query'; import { API } from '@/api'; -import { GroupIdPayload } from '@/types/service/group'; +import { GroupIdParams } from '@/types/service/group'; -export const useDeleteGroup = (payload: GroupIdPayload, callback: () => void) => { +export const useDeleteGroup = (params: GroupIdParams, callback: () => void) => { const query = useMutation({ - mutationFn: () => API.groupService.deleteGroup(payload), + mutationFn: () => API.groupService.deleteGroup(params), onSuccess: async () => { console.log('모임 삭제 성공.'); callback(); diff --git a/src/hooks/use-group/use-group-get-details/index.ts b/src/hooks/use-group/use-group-get-details/index.ts index 6499b965..e48b33c6 100644 --- a/src/hooks/use-group/use-group-get-details/index.ts +++ b/src/hooks/use-group/use-group-get-details/index.ts @@ -2,12 +2,12 @@ import { useQuery } from '@tanstack/react-query'; import { API } from '@/api'; import { groupKeys } from '@/lib/query-key/query-key-group'; -import { GroupIdPayload } from '@/types/service/group'; +import { GroupIdParams } from '@/types/service/group'; -export const useGetGroupDetails = (payload: GroupIdPayload) => { +export const useGetGroupDetails = (params: GroupIdParams) => { const query = useQuery({ - queryKey: groupKeys.detail(payload.groupId), - queryFn: () => API.groupService.getGroupDetails(payload), + queryKey: groupKeys.detail(params.groupId), + queryFn: () => API.groupService.getGroupDetails(params), }); return query; }; diff --git a/src/hooks/use-group/use-group-leave/index.ts b/src/hooks/use-group/use-group-leave/index.ts index 1d1bb2ed..0bb88fcd 100644 --- a/src/hooks/use-group/use-group-leave/index.ts +++ b/src/hooks/use-group/use-group-leave/index.ts @@ -2,15 +2,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { API } from '@/api'; import { groupKeys } from '@/lib/query-key/query-key-group'; -import { GroupIdPayload } from '@/types/service/group'; +import { GroupIdParams } from '@/types/service/group'; -export const useLeaveGroup = (payload: GroupIdPayload, callback: () => void) => { +export const useLeaveGroup = (params: GroupIdParams, callback: () => void) => { const queryClient = useQueryClient(); const query = useMutation({ - mutationFn: () => API.groupService.leaveGroup(payload), + mutationFn: () => API.groupService.leaveGroup(params), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupKeys.detail(payload.groupId) }); + await queryClient.invalidateQueries({ queryKey: groupKeys.detail(params.groupId) }); callback(); console.log('모임 탈퇴 성공.'); }, From 93767cb60421562f61aa3a6e85e77925c17aff9a Mon Sep 17 00:00:00 2001 From: Hope <96109009+HopeFullee@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:11:48 +0900 Subject: [PATCH 4/4] Update src/app/post-meetup/[groupId]/layout.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 레빗이 처음으로 도움이된 건 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/app/post-meetup/[groupId]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/post-meetup/[groupId]/layout.tsx b/src/app/post-meetup/[groupId]/layout.tsx index 50bce543..89257d12 100644 --- a/src/app/post-meetup/[groupId]/layout.tsx +++ b/src/app/post-meetup/[groupId]/layout.tsx @@ -26,7 +26,7 @@ const EditMeetupLayout = async ({ children, params }: Props) => { const isHost = sessionUserId === createdBy.userId; const isEditable = status !== 'FINISHED'; - if (!isHost && !isEditable) { + if (!isHost || !isEditable) { redirect('/post-meetup'); }