Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions src/api/service/group-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
GetGroupsResponse,
GetMyGroupsPayload,
GetMyGroupsResponse,
GroupIdPayload,
GroupIdParams,
PreUploadGroupImageResponse,
} from '@/types/service/group';

Expand Down Expand Up @@ -43,20 +43,24 @@ export const groupServiceRemote = () => ({
return apiV2.post<CreateGroupResponse>('/groups/create', payload);
},

getGroupDetails: (payload: GroupIdPayload) => {
return apiV2.get<GetGroupDetailsResponse>(`/groups/${payload.groupId}`);
editGroup: (params: GroupIdParams, payload: CreateGroupPayload) => {
return apiV2.patch<CreateGroupResponse>(`/groups/${params.groupId}`, payload);
},

attendGroup: (payload: GroupIdPayload) => {
return apiV2.post<GetGroupDetailsResponse>(`/groups/${payload.groupId}/attend`);
getGroupDetails: (params: GroupIdParams) => {
return apiV2.get<GetGroupDetailsResponse>(`/groups/${params.groupId}`);
},

leaveGroup: (payload: GroupIdPayload) => {
return apiV2.post<GetGroupDetailsResponse>(`/groups/${payload.groupId}/left`);
attendGroup: (params: GroupIdParams) => {
return apiV2.post<GetGroupDetailsResponse>(`/groups/${params.groupId}/attend`);
},

deleteGroup: (payload: GroupIdPayload) => {
return apiV2.delete(`/groups/${payload.groupId}`);
leaveGroup: (params: GroupIdParams) => {
return apiV2.post<GetGroupDetailsResponse>(`/groups/${params.groupId}/left`);
},

deleteGroup: (params: GroupIdParams) => {
return apiV2.delete(`/groups/${params.groupId}`);
},

uploadGroupImages: (payload: FormData) => {
Expand Down
38 changes: 38 additions & 0 deletions src/app/post-meetup/[groupId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 <HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>;
};

export default EditMeetupLayout;
117 changes: 117 additions & 0 deletions src/app/post-meetup/[groupId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +31 to +42
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

data가 undefined일 경우 런타임 에러 발생 가능

useGetGroupDetails가 로딩 중이거나 에러 상태일 때 dataundefined일 수 있습니다. 타입 단언(as GetGroupDetailsResponse)으로 이를 무시하면 구조 분해 할당 시 런타임 에러가 발생합니다.

레이아웃에서 prefetch와 권한 체크를 수행한다고 해도, 클라이언트 컴포넌트에서는 데이터 로딩 상태를 명시적으로 처리하는 것이 안전합니다.

🔎 제안된 수정 사항
  const { data } = useGetGroupDetails({ groupId });
- const { mutateAsync: EditGroup } = useEditGroup({ groupId });

- const {
-   title,
-   address: { location },
-   startTime,
-   tags,
-   description,
-   maxParticipants,
-   images,
- } = data as GetGroupDetailsResponse;
+ const { mutateAsync: EditGroup } = useEditGroup({ groupId });
+
+ if (!data) {
+   return null; // 또는 로딩 스피너
+ }
+
+ const {
+   title,
+   address: { location },
+   startTime,
+   tags,
+   description,
+   maxParticipants,
+   images,
+ } = data;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data } = useGetGroupDetails({ groupId });
const { mutateAsync: EditGroup } = useEditGroup({ groupId });
const {
title,
address: { location },
startTime,
tags,
description,
maxParticipants,
images,
} = data as GetGroupDetailsResponse;
const { data } = useGetGroupDetails({ groupId });
const { mutateAsync: EditGroup } = useEditGroup({ groupId });
if (!data) {
return null; // 또는 로딩 스피너
}
const {
title,
address: { location },
startTime,
tags,
description,
maxParticipants,
images,
} = data;
🤖 Prompt for AI Agents
In src/app/post-meetup/[groupId]/page.tsx around lines 31 to 42, you destructure
fields from data using a type assertion which will throw if data is undefined
during loading or error; guard against undefined by checking data before
destructuring (e.g., return a loading/error UI or early return when !data), or
provide safe defaults/optional chaining when extracting nested fields
(address?.location) and only call useEditGroup when groupId/data is present;
update the code to handle loading and error states explicitly instead of
assuming data exists.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 진지하게 수정 페이지에서 data가 undefined 이면 그냥 터지는게 맞는게 아닐까?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HopeFullee, 좋은 지적이십니다. 타당한 관점입니다.

레이아웃에서 prefetch와 권한 검증을 통해 데이터가 보장된다면, dataundefined인 상황은 정상적인 로딩 상태가 아니라 시스템 오류를 의미합니다. 이 경우 "fail fast" 원칙에 따라 일찍 터져서 문제를 명확히 드러내는 것이 오히려 더 나은 선택일 수 있습니다.

이 접근의 장점:

  • 버그나 아키텍처 문제를 즉시 발견 가능
  • 불가능한 상태를 조용히 처리하면서 실제 문제를 숨기지 않음
  • 레이아웃의 보장이 깨졌을 때 명확한 신호

고려사항:

  • 하이드레이션 불일치나 네트워크 일시적 오류 같은 엣지 케이스에서도 크래시 발생
  • 사용자 경험 측면에서는 로딩 상태가 더 우아할 수 있음

하지만 아키텍처 설계 의도를 고려하면 현재 코드가 맞습니다. 제안을 무시하셔도 됩니다. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그나저나 @claudia99503 님은 왜 자꾸 너를 부르는걸까?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


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 (
<div>
<form>
<section className='px-4'>
<form.Field children={(field) => <MeetupTitleField field={field} />} name='title' />
<form.Field children={(field) => <MeetupLocationField field={field} />} name='location' />
<form.Field children={(field) => <MeetupDateField field={field} />} name='startTime' />
<form.Field
children={(field) => <MeetupCapField field={field} />}
name='maxParticipants'
/>
<form.Field children={(field) => <MeetupImagesField field={field} />} name='images' />
<form.Field
children={(field) => <MeetupDetailField field={field} />}
name='description'
/>
<form.Field children={(field) => <MeetupTagsField field={field} />} name='tags' />
</section>

<form.Subscribe
children={(state) => (
<MeetupSubmitButton state={state} onSubmitClick={() => form.handleSubmit()} />
)}
selector={(state) => state}
/>
</form>
</div>
);
};

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 };
};
3 changes: 2 additions & 1 deletion src/app/post-meetup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const PostMeetupPage = () => {
description: '',
maxParticipants: 0,
images: [],
joinPolicy: 'FREE',
} as CreateGroupFormValues,
validators: {
onChange: createGroupSchema,
Expand All @@ -40,7 +41,7 @@ const PostMeetupPage = () => {
return { ...image, sortOrder: idx };
});

const res = await createGroup({ ...value });
const res = await createGroup(value);

replace(`/meetup/${res.id}`);
},
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/use-group/use-group-attend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('모임 참여 성공.');
},
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/use-group/use-group-delete/index.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/use-group/use-group-edit/index.ts
Original file line number Diff line number Diff line change
@@ -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('모임 생성 실패.');
Comment on lines +15 to +18
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

콘솔 메시지가 편집이 아닌 생성을 참조하고 있습니다.

성공 및 오류 콘솔 로그에 "모임 생성"이 표시되지만 이 훅은 모임을 편집하는 것입니다. 올바른 메시지로 수정해주세요.

🔎 제안된 수정
     onSuccess: async () => {
       await queryClient.invalidateQueries({ queryKey: groupKeys.detail(params.groupId) });
 
-      console.log('모임 생성 성공.');
+      console.log('모임 수정 성공.');
     },
     onError: () => {
-      console.log('모임 생성 실패.');
+      console.log('모임 수정 실패.');
     },
🤖 Prompt for AI Agents
In src/hooks/use-group/use-group-edit/index.ts around lines 15 to 18, the
success and error console logs currently say "모임 생성 성공." and "모임 생성 실패." even
though this hook performs group editing; change both messages to refer to
editing (e.g., "모임 수정 성공." and "모임 수정 실패."). Ensure you update the onSuccess and
onError console.log strings accordingly so logs correctly reflect the edit
operation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

올 ㅋ create hook 복사해와서 수정한건데 이걸 잡네

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HopeFullee 복사-붙여넣기할 때 이런 실수는 흔하죠! 😄 수정해주시면 감사하겠습니다!

    (\(\
    (=^.^=)
    (")_(")

},
});
return query;
};
8 changes: 4 additions & 4 deletions src/hooks/use-group/use-group-get-details/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
8 changes: 4 additions & 4 deletions src/hooks/use-group/use-group-leave/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('모임 탈퇴 성공.');
},
Expand Down
1 change: 1 addition & 0 deletions src/lib/schema/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const createGroupSchema = z.object({
}),
)
.optional(),
joinPolicy: z.union([z.literal('FREE'), z.literal('APPROVE')]),
});

export type CreateGroupFormValues = z.infer<typeof createGroupSchema>;
3 changes: 2 additions & 1 deletion src/types/service/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface CreateGroupPayload {
imageKey: string;
sortOrder: number;
}[];
joinPolicy: 'FREE' | 'APPROVE';
}

export interface CreateGroupResponse {
Expand Down Expand Up @@ -200,6 +201,6 @@ export interface GetGroupDetailsResponse {
}[];
}

export interface GroupIdPayload {
export interface GroupIdParams {
groupId: string;
}