-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/refactor/i participated meeting/DEVING-79 내가 참여하고있는 모임 리펙토링 #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/refactor/i participated meeting/DEVING-79 내가 참여하고있는 모임 리펙토링 #68
Conversation
…t/markup/my-likes-meeting/DEVING-74
…ub.com/MoimService/Moim-FE into feat/refactor/i-participated-meeting/DEVING-79
…m/MoimService/Moim-FE into feat/refactor/i-participated-meeting/DEVING-79
…m/MoimService/Moim-FE into feat/refactor/i-participated-meeting/DEVING-79
Walkthrough이번 변경사항은 미팅 관련 기능 전반에 걸쳐 여러 개선 및 리팩토링을 포함합니다.
Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant LMButton as LeaveMeetingButton
participant QuitHook as useQuitMeetingMutation
participant API as API 서버
participant Toast as 알림 토스트
User->>LMButton: 미팅 탈퇴 버튼 클릭
LMButton->>QuitHook: handleClick() 호출 (meetingId 전달)
QuitHook->>API: deleteQuit(meetingId) 호출
API-->>QuitHook: 성공 응답 반환
QuitHook->>Toast: 성공 토스트 메시지 표시
QuitHook->>LMButton: 로딩 상태 업데이트 (false)
sequenceDiagram
participant User as 사용자
participant LikesComp as Likes Component
participant QueryHook as useInfiniteMyMeetingLikesQueries
participant API as API 서버
User->>LikesComp: 스크롤 액션 발생 (무한 스크롤 트리거)
LikesComp->>QueryHook: 다음 페이지 데이터 요청
QueryHook->>API: getMyMeetingLikes(lastMeetingId) 호출
API-->>QueryHook: 페이징된 좋아요 미팅 데이터 반환
QueryHook->>LikesComp: 새로운 데이터 업데이트 및 렌더링
Suggested labels
Suggested reviewers
Poem
Tip ⚡🧪 Multi-step agentic review comment chat (experimental)
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
🔇 Additional comments (5)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
✅ Deploy Preview for deving-together ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |
Codecov ReportAll modified and coverable lines are covered by tests ✅ 🚀 New features to boost your workflow:
|
|
내가 주최한 모임의 경우에도 내가 참여하고 있는 모임에 보여지고 있습니다. 이 부분 백엔드쪽에 요청하거나 조건문 처리하여 보여지지 않도록 하는 작업이 필요해 보입니다. |
|
모임 취소, 탈퇴의 경우 누르자마자 바로 api가 호출이 되어 모달 등의 확인 처리가 필요할 것 같습니다. 머지가 된다면 이 부분 제가 처리해두겠습니다. |
|
내가 참여하고 있는 모임에서 찜하기 버튼 숨김처리 부탁드립니다. |
|
찜한 모임에서 좋아요(찜 취소)를 눌러도 모임 리스트에는 보이지만 하트가 변경되지 않고 있습니다. 확인 부탁드립니다! |
dbswl701
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<PublicSelect isPublic={isPublic ?? false} meetingId={meetingId} />이렇게 ?? 연산자를 넣는 것 보다
const CardRightSection = ({
memberList,
isPublic,
className,
meetingId,
showPublicSelect = false,
}이 부분에서 isPublic=false 기본값을 지정하는 방안은 어떨까요? 더 자연스러운 흐름일 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오호 좋습니당 반영하겠습니다
| meetingId, | ||
| className = '', | ||
| }: LeaveMeetingButtonProps) => { | ||
| const { mutate: quitMeeting, isPending: isLoading } = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isLoading을 사용하지 않으시고 isPending을 isLoading으로 바꿔사용하시는 이유가 있으실까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 프로젝트에선 전부 isLoading을 사용하고 있더라구요. 어제 문득 isPending과 isLoading중 어떤 거 사용해야할지 좀 고민이 있었다가 TanStack/query#6297 요고보구 v5에선 isPending을 사용한다기에, 통일성을 위해서 일단은 isPending 쓰고 isLoading으로 맞춰넣었슴니다. isLoading으로 일단 통일하겠슴니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
윤지님 해당 부분 리엑트쿼리 v5에서 useMutation의 반환값에서 isLoading이 삭제되고 isPending으로 대체되엇다고합니다. 따라 isPending이 isLoading 대신 사용된다고 합니다. 그래서 기존 코드 보았을 때 isLoading을 사용하는 부분이 인피니티 쿼리와 useQuery부분임니당 해당 부분에서의 isLoading은 첫 페이지 데이터를 로딩 중일때만 true가 되는 상태라고 합니다!
| <button | ||
| type="button" | ||
| onClick={handleClick} | ||
| className={`absolute bottom-2 right-2 z-10 flex items-center rounded-md px-3 py-1.5 ${ | ||
| isLoading ? 'opacity-70' : '' | ||
| } ${className}`} | ||
| aria-label="모임 탈퇴하기" | ||
| disabled={isLoading} | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
className이 여기에만 적용이 되어있는데, 상세 조회에서는 이 컴포넌트 커스텀을 어떻게 하면 될까요? 아이콘이나 글씨 크기 등도 고려가 필요할 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요부분 prop추가하겠슴니다
| {/* 데이터가 없는 경우 표시 */} | ||
| {meetingData.pages[0].content.length === 0 && ( | ||
| <div className="text-gray-500 py-8 text-center text-white"> | ||
| 찜한 모임이 없습니다. | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| {/* 더 이상 데이터가 없음을 표시 */} | ||
| {!hasNextPage && meetingData.pages[0].content.length > 0 && ( | ||
| <div className="text-gray-500 py-4 text-center text-white"> | ||
| 모든 모임을 불러왔습니다. | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 푸터가 들어가면 나타나지 않아도 될 것 같은데 어떠신가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 좋습니다
| const res = await axiosInstance.get( | ||
| `${myMeetingURL.likes}?lastMeetingId=${lastMeetingId}&size=${6}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제작하신 url 호출 함수 사용해도 좋을 것 같아요
| const res = await axiosInstance.delete(`${myMeetingURL.quit(meetingId)}`); | ||
| return res.data.data; | ||
| }; | ||
|
|
||
| // 승인 대기중인 모임 취소하기 | ||
| const DeleteCancel = async (meetingId: number) => { | ||
| const res = await axiosInstance.delete(`${myMeetingURL.cancel(meetingId)}`); | ||
| return res.data.data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마찬가지입니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
타입은 겹치는 부분이 많아서 extends를 활용하는 방향이 좋아보여요. 저도 이 부분 수정할 예정이라 제가 수정해두겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다. 저도 저번작업부터 타입폴더 정리하고싶어서 말씀드렸엇는데 요고 작업하실때 말씀해주시면 같이하겠슴니당
src/util/CategoryNameToEng.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 이미 util/searchFilter.ts에 위치해 있습니다. 제거 부탁드립니다.
| animation: { | ||
| 'delay-100': 'bounce 1s infinite 100ms', | ||
| 'delay-200': 'bounce 1s infinite 200ms', | ||
| 'delay-300': 'bounce 1s infinite 300ms', | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 어디에 사용되는 애니메이션인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요부분 승인대기중에 사용되비다!
로드시점에 주최여부를 판단할 수 있는 부분이없어서 추후 백엔드분과 다시 얘기해봐야겠네용 |
넵 감사합니다 윤지님 체고 |
찜한모임 기능동작은 해당 브랜치 머지되면 한나님께서 작업해주시기로 어제 전달받았슴니다! |
3b5d166 to
6b9542d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (9)
src/types/myMeeting.ts (2)
1-1: 새로운 인터페이스 추가에 대한 제안.
IMyMeetingParticipated인터페이스를 추가하고CategoryTitle타입을 가져오는 것은 좋은 시도입니다. 하지만 이전 리뷰에서 언급된 것처럼 여러 인터페이스 간에 중복되는 속성이 많습니다.-interface IMyMeetingParticipated { - categoryTitle: CategoryTitle; - meetingId: number; - title: string; - thumbnail: string; - location: string; - memberCount: number; - maxMember: number; - likesCount: number; - myMemberStatus: 'APPROVED' | 'REJECTED' | 'PENDING' | 'EXPEL'; - memberList: Member[]; - isMeetingManager: boolean; -} +// 기본 인터페이스 정의 +interface IBaseMeeting { + meetingId: number; + title: string; + thumbnail: string; + location: string; + memberCount: number; + maxMember: number; + likesCount: number; +} + +// 참여 모임 인터페이스 +interface IMyMeetingParticipated extends IBaseMeeting { + categoryTitle: CategoryTitle; + myMemberStatus: 'APPROVED' | 'REJECTED' | 'PENDING' | 'EXPEL'; + memberList: Member[]; + isMeetingManager: boolean; +}이런 방식으로 타입을 확장하여 중복을 줄이는 것을 고려해보세요.
Also applies to: 34-46, 104-105
48-57: 타입 확장을 통한 코드 개선 제안.
IMyMeetingLikes인터페이스도 마찬가지로 상당 부분 다른 인터페이스와 중복됩니다. 앞서 제안된IBaseMeeting을 확장하여 사용할 수 있습니다:-interface IMyMeetingLikes { - meetingId: number; - categoryTitle: CategoryTitle; - title: string; - thumbnail: string; - location: string; - memberCount: number; - maxMember: number; - likesCount: number; -} +interface IMyMeetingLikes extends IBaseMeeting { + categoryTitle: CategoryTitle; +}이러한 접근 방식은 타입 정의를 보다 유지보수하기 쉽게 만들고, 확장성을 향상시킵니다.
src/app/(user-page)/my-meeting/_features/PendingStatusChip.tsx (1)
24-46: UI 구현 및 사용자 피드백이 우수합니다.로딩 상태에 따른 시각적 피드백과 애니메이션이 잘 구현되어 있습니다. 특히 애니메이션 딜레이를 통한 점 표시는 사용자에게 대기 중임을 효과적으로 전달합니다.
하지만 모달 확인 대화상자를 추가하는 것을 고려해 보세요:
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); if (window.confirm('정말로 승인 대기를 취소하시겠습니까?')) { cancelPending(meetingId); } };PR 코멘트에서 언급된 것처럼 즉각적인 API 호출 대신 확인 과정을 추가하는 것이 좋을 것 같습니다.
src/app/(user-page)/my-meeting/_features/LeaveMeetingButton.tsx (1)
27-32: 접근성 및 UI 개선
aria-label을 적절하게 사용한 것은 좋습니다. 또한classNameprop을 받아서 컴포넌트가 재사용 가능하도록 구현한 점도 좋습니다. 하지만 버튼의 스타일링이 더 명확하게 구분될 수 있습니다.다음과 같이 스타일을 개선해 보세요:
-className={`absolute bottom-2 right-2 z-10 flex items-center rounded-md px-3 py-1.5 ${ - isLoading ? 'opacity-70' : '' -} ${className}`} +className={`absolute bottom-2 right-2 z-10 flex items-center rounded-md px-3 py-1.5 text-red-500 bg-white border border-red-500 hover:bg-red-50 transition-colors ${ + isLoading ? 'opacity-70 cursor-not-allowed' : '' +} ${className}`}src/app/(user-page)/my-meeting/_features/Likes.tsx (1)
126-133: 빈 상태 UI 개선빈 상태(empty state) UI가 기본적으로 구현되어 있지만, 사용자 경험을 더 향상시킬 수 있습니다.
빈 상태에 아이콘이나 이미지를 추가하고, 사용자에게 모임 페이지로 이동할 수 있는 버튼을 제공하는 것이 좋습니다:
- {meetingData.pages[0].content.length === 0 && ( - <div className="flex h-[60vh] w-full items-center justify-center text-center text-Cgray500"> - <div> - <p className="mb-2">찜한 모임이 없어요.</p> - <p>원하는 모임을 찜해보세요!</p> - </div> - </div> - )} + {meetingData.pages[0].content.length === 0 && ( + <div className="flex h-[60vh] w-full items-center justify-center text-center"> + <div className="flex flex-col items-center"> + <div className="mb-4 text-Cgray400"> + {/* 아이콘 또는 이미지 추가 */} + <svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="currentColor"/> + </svg> + </div> + <p className="mb-2 text-Cgray500">찜한 모임이 없어요.</p> + <p className="mb-6 text-Cgray500">원하는 모임을 찜해보세요!</p> + <button + onClick={() => router.push('/meeting')} + className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors" + > + 모임 둘러보기 + </button> + </div> + </div> + )}src/app/(user-page)/my-meeting/likes/page.tsx (1)
23-28: HTML 구조 최적화추가적인 div 요소가 HydrationBoundary를 감싸고 있지만, 이 div는 특별한 스타일이나 속성이 없어 불필요합니다.
불필요한 div 요소를 제거하여 DOM 구조를 더 간결하게 만드세요:
- return ( - <div> - <HydrationBoundary state={dehydrate(queryClient)}> - <Likes /> - </HydrationBoundary> - </div> - ); + return ( + <HydrationBoundary state={dehydrate(queryClient)}> + <Likes /> + </HydrationBoundary> + );src/hooks/mutations/useMyMeetingMutation.ts (1)
45-67: 승인 대기 취소 기능 추가승인 대기 취소를 위한 뮤테이션 훅이 잘 구현되었습니다. 에러 처리 부분에서 조건문(
if (error.response?.status))은 항상 true로 평가될 가능성이 있습니다. 에러 코드에 따른 더 구체적인 조건을 제공하는 것이 좋을 수 있습니다.- onError: (error: AxiosError) => { - if (error.response?.status) { - showToast('승인 대기 취소에 실패했습니다. 다시 시도해주세요.', 'error'); - } - }, + onError: (error: AxiosError) => { + showToast('승인 대기 취소에 실패했습니다. 다시 시도해주세요.', 'error'); + },src/service/api/mymeeting.ts (1)
30-39: 찜한 모임 로드 기능 추가찜한 모임을 불러오는 API 함수가 잘 구현되었습니다. URL 문자열 구성 방식이 파일 내 다른 함수들과 일관되어 있습니다.
이 부분에서 템플릿 문자열 대신
meeting.ts파일에서 사용한 방식처럼myMeetingURL.likes상수를 활용한다면 코드베이스의 일관성이 더 향상될 수 있습니다.- const res = await axiosInstance.get( - `${myMeetingURL.likes}?lastMeetingId=${lastMeetingId}&size=${6}`, - ); + const res = await axiosInstance.get(myMeetingURL.likes, { + params: { lastMeetingId, size: 6 }, + });src/app/(user-page)/my-meeting/_features/Participated.tsx (1)
135-171: 태블릿 및 모바일 뷰의 반응형 구현태블릿과 모바일 뷰에서도 동일한 조건부 렌더링 로직이 적용되어 있습니다. 다만, 세 가지 뷰(데스크탑, 태블릿, 모바일)에서 상태 관련 컴포넌트 로직이 중복되고 있습니다.
좀 더 DRY한 접근법을 위해, 상태 관련 컴포넌트(PendingStatusChip, LeaveMeetingButton, StatusOverlay)를 렌더링하는 로직을 별도의 함수로 추출하는 것을 고려해 볼 수 있습니다.
예를 들어, 다음과 같은 함수를 만들어 사용할 수 있습니다:
const renderStatusComponents = (meeting: IMyMeetingParticipated, className?: string) => ( <> {meeting.myMemberStatus === 'PENDING' && ( <PendingStatusChip meetingId={meeting.meetingId} /> )} {meeting.myMemberStatus === 'APPROVED' && !meeting.isMeetingManager && ( <LeaveMeetingButton meetingId={meeting.meetingId} className={className || "bg-warning text-white"} /> )} {isDisabledStatus(meeting.myMemberStatus) && ( <StatusOverlay meeting={meeting} /> )} </> );그리고 각 뷰에서는 다음과 같이 사용할 수 있습니다:
{/* 데스크탑 */} <div className="relative w-full"> <HorizonCard {...props} /> {renderStatusComponents(meeting)} </div> {/* 태블릿 */} <div className="relative hidden flex-col border-b border-Cgray300 py-[42px] md:flex lg:hidden"> <HorizonCard {...props} /> <CardRightSection {...props} /> {renderStatusComponents(meeting)} </div> {/* 모바일 */} <div className="relative flex flex-col border-b border-Cgray300 py-[42px] md:hidden"> <HorizonCard {...props} /> <CardRightSection {...props} /> {renderStatusComponents(meeting, "bg-warning px-2 py-1 text-xs text-white")} </div>Also applies to: 175-211
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/app/(user-page)/my-meeting/_features/CardRightSection.tsx(2 hunks)src/app/(user-page)/my-meeting/_features/Joined.tsx(0 hunks)src/app/(user-page)/my-meeting/_features/LeaveMeetingButton.tsx(1 hunks)src/app/(user-page)/my-meeting/_features/Likes.tsx(1 hunks)src/app/(user-page)/my-meeting/_features/Participated.tsx(5 hunks)src/app/(user-page)/my-meeting/_features/PendingStatusChip.tsx(1 hunks)src/app/(user-page)/my-meeting/_features/Tab.tsx(1 hunks)src/app/(user-page)/my-meeting/likes/page.tsx(1 hunks)src/app/(user-page)/my-meeting/my/page.tsx(2 hunks)src/hooks/mutations/useMyMeetingMutation.ts(2 hunks)src/hooks/queries/useMyMeetingQueries.ts(2 hunks)src/hooks/useCard.tsx(1 hunks)src/service/api/meeting.ts(4 hunks)src/service/api/mymeeting.ts(3 hunks)src/types/myMeeting.ts(3 hunks)src/util/CategoryNameToEng.ts(1 hunks)tailwind.config.ts(1 hunks)
💤 Files with no reviewable changes (1)
- src/app/(user-page)/my-meeting/_features/Joined.tsx
🔇 Additional comments (40)
src/app/(user-page)/my-meeting/_features/Tab.tsx (1)
8-8: 이름 변경이 일관성 있게 적용되었습니다.'joined'에서 'participated'로의 변경이 PR 목적과 일치하며, 라우팅 및 관련 컴포넌트에서 일관성 있게 적용되었습니다.
src/util/CategoryNameToEng.ts (1)
1-14: 코드 중복이 발생했습니다.이 기능은 이미 util/searchFilter.ts에 위치해 있습니다. 코드 중복을 방지하기 위해 이 파일을 제거하고 기존 유틸리티를 사용하는 것이 좋겠습니다.
tailwind.config.ts (1)
13-17: 애니메이션 설정이 적절히 추가되었습니다.승인 대기 상태에 사용되는 지연 애니메이션이 명확하게 정의되었습니다. 각 애니메이션마다 다른 지연 시간을 적용하여 시각적으로 구분되는 효과를 제공합니다.
src/app/(user-page)/my-meeting/my/page.tsx (2)
15-15: 타입 가져오기가 적절히 추가되었습니다.
IMyMeetingParticipated타입 import를 추가하여 코드의 타입 안전성을 향상시켰습니다.
50-50: 타입 명시가 정확하게 수정되었습니다.
getNextPageParam함수의 매개변수 타입을Paginated<IMyMeetingParticipated>로 명시하여 참여 중인 모임 데이터 구조와 일치시켰습니다. 이전에 잠재적으로 잘못된 타입이 사용될 가능성을 제거하여 타입 안전성을 높였습니다.src/hooks/useCard.tsx (2)
124-124: 라우팅 경로 업데이트가 적절히 이루어졌습니다.'joined'에서 'participated'로의 용어 변경이 일관되게 적용되었습니다. 이는 모임 참여 상태를 더 명확하게 표현하는 좋은 개선입니다.
128-128: 라우팅 경로 업데이트가 적절히 이루어졌습니다.'joined'에서 'participated'로의 용어 변경이 여기서도 일관되게 적용되었습니다.
src/app/(user-page)/my-meeting/_features/CardRightSection.tsx (2)
17-17: isPublic 속성에 기본값 설정이 잘 적용되었습니다.이전 리뷰 피드백에 따라
isPublic속성에 기본값을 설정하고 타입을 선택적으로 변경한 것은 좋은 개선입니다. 이제 속성을 제공하지 않아도 기본적으로false로 처리됩니다.Also applies to: 23-23
64-64: 라우팅 경로의 일관성 있는 업데이트.'joined'에서 'participated'로의 변경이 라우팅 로직에서도 일관되게 적용되었습니다. 추가로
showPublicSelect를 통한 조건부 처리도 적절하게 구현되었습니다.src/app/(user-page)/my-meeting/_features/PendingStatusChip.tsx (3)
6-9: Props 인터페이스 정의가 명확합니다.
PendingStatusChipProps인터페이스의 속성 정의가 명확하고 간결합니다.text속성을 선택적으로 만든 것은 컴포넌트의 유연성을 높이는 좋은 방법입니다.
15-16: 비동기 상태 관리가 잘 되어 있습니다.
useCancelPendingMutation훅을 사용하여 비동기 상태(isPending)를 관리하고 있어 사용자 경험이 향상됩니다.
18-21: 이벤트 처리가 적절히 구현되었습니다.
stopPropagation()을 사용하여 버블링을 방지하고, 클릭 이벤트가 상위 컴포넌트에 영향을 주지 않도록 처리한 것은 좋은 방법입니다.src/app/(user-page)/my-meeting/_features/LeaveMeetingButton.tsx (2)
15-16: React Query의 속성명 변경과 일관성 유지
isPending을isLoading으로 변경하신 것이 확인됩니다. 이전 리뷰 코멘트에서 언급되었듯이 React Query v5에서는isPending이isLoading을 대체하지만, 프로젝트 전체의 일관성을 위해isLoading으로 사용하는 것이 좋겠습니다.
18-21: 이벤트 처리 및 모달 확인 추가 필요이벤트 전파 방지는 적절하게 구현되었습니다. 그러나 PR 코멘트에서 언급된 것처럼 모임 탈퇴 시 사용자에게 확인을 요청하는 모달이 필요합니다. 현재는 버튼 클릭 시 바로 API가 호출됩니다.
사용자가 실수로 버튼을 클릭했을 때 의도치 않게 모임에서 탈퇴되는 것을 방지하기 위해 확인 모달을 추가해주세요.
src/app/(user-page)/my-meeting/_features/Likes.tsx (1)
56-56: isLike prop 하드코딩 확인 필요
isLike속성이 하드코딩되어 있습니다. 이는 좋아요 목록이므로 항상true일 수 있지만, 데이터 소스에서 실제 상태를 가져오는 것이 더 정확합니다.실제 API 응답에 좋아요 상태가 포함되어 있는지 확인하고, 포함되어 있다면, 하드코딩 대신 해당 값을 사용하세요.
src/app/(user-page)/my-meeting/likes/page.tsx (1)
16-22: 서버 컴포넌트에서 데이터 프리페칭 구현서버 컴포넌트에서 React Query를 사용하여 데이터를 프리페칭하는 구현이 잘 되어 있습니다. 이는 클라이언트에서의 초기 로딩 상태를 방지하여 사용자 경험을 개선합니다.
src/hooks/queries/useMyMeetingQueries.ts (3)
21-21: 키 구조 일관성 유지myMeetingKeys 객체에 likes 키를 추가하여 일관된 구조를 유지하고 있습니다. 이는 좋은 패턴입니다.
41-51: 타입 업데이트 확인
useInfiniteMyMeetingParticipatedQueries함수에서lastPage타입이IMyMeetingManage에서IMyMeetingParticipated로 변경되었습니다. 이는 더 정확한 타입 정의를 제공합니다.다만, API 응답 구조가 실제로 변경되었는지 확인하는 것이 좋습니다.
API 응답 구조가 IMyMeetingParticipated 타입에 맞는지 확인하세요.
53-63: 새로운 쿼리 함수 구현
useInfiniteMyMeetingLikesQueries함수가 기존 패턴을 따라 잘 구현되어 있습니다. 무한 스크롤을 위한 설정과 페이징 처리가 적절합니다.src/service/api/meeting.ts (5)
11-11: 상수 임포트를 통한 코드 개선엔드포인트 URL을 상수로 관리하여 코드 가독성과 유지보수성을 향상시켰습니다. 이는 하드코딩된 URL 문자열 대신 명명된 상수를 사용함으로써 오타 가능성을 줄이고 유지보수를 쉽게 만드는 좋은 접근법입니다.
18-18: URL 상수화로 코드 품질 개선하드코딩된 URL 대신
meetingURL.top상수를 사용하여 코드의 가독성과 유지보수성이 향상되었습니다.
34-34: URL 상수 사용으로 일관성 확보템플릿 문자열 내에
meetingURL.search상수를 사용하여 API 엔드포인트 참조 방식의 일관성을 유지했습니다.
42-42: 좋아요 관련 엔드포인트 상수화
likesURL.create와likesURL.delete함수를 통해 좋아요 관련 엔드포인트를 상수화하여 코드 일관성과 유지보수성을 향상시켰습니다.Also applies to: 48-48
78-78: 모든 엔드포인트의 일관된 상수화모임 상세정보, 매니저 정보, 모임 등록 및 탈퇴 엔드포인트가 모두 일관되게 상수화되었습니다. 이는 코드베이스 전체의 일관성을 유지하고 향후 URL 구조 변경 시 유지보수를 용이하게 합니다.
Also applies to: 83-83, 95-95, 103-103
src/hooks/mutations/useMyMeetingMutation.ts (3)
2-2: 필요한 의존성을 올바르게 가져옴필요한 모듈들을 명확히 가져와서 코드의 가독성과 의존성 관리가 향상되었습니다.
Also applies to: 6-6, 8-9
11-43: 모임 탈퇴 기능 추가모임 탈퇴를 위한 뮤테이션 훅이 잘 구현되었습니다. 특히 주최자가 모임을 탈퇴할 수 없는 경우에 대한 명확한 에러 처리가 잘 되어 있습니다.
성공 및 실패 케이스에 대한 토스트 메시지, 관련 쿼리 무효화 등이 적절하게 구현되어 있습니다.
159-165: 내보내기 문 업데이트새로운 훅들을 내보내기 문에 추가하여 외부에서 사용할 수 있도록 적절히 구성했습니다.
src/service/api/mymeeting.ts (5)
4-4: 필요한 타입 임포트 추가필요한 타입들을 올바르게 가져와서 코드의 타입 안전성이 향상되었습니다.
19-22: 내가 참여한 모임 API 함수의 리턴 타입 변경
getMyMeetingParticipated함수의 반환 타입이IMyMeetingManage에서IMyMeetingParticipated로 변경되었습니다. 이는 다른 파일들의 변경사항과 일치하며 타입의 일관성을 유지합니다.
100-104: 모임 탈퇴 기능 추가모임 탈퇴 API 함수가 잘 구현되었습니다.
myMeetingURL.quit(meetingId)상수를 사용하여 일관된 방식으로 엔드포인트를 참조하고 있습니다.
106-110: 승인 대기 취소 기능 추가승인 대기 취소 API 함수가 잘 구현되었습니다.
myMeetingURL.cancel(meetingId)상수를 사용하여 일관된 방식으로 엔드포인트를 참조하고 있습니다.
112-122: 내보내기 문 업데이트새로운 함수들을 내보내기 문에 추가하여 외부에서 사용할 수 있도록 적절히 구성했습니다.
src/app/(user-page)/my-meeting/_features/Participated.tsx (8)
6-9: 필요한 의존성 및 타입 가져오기카테고리 이름 변환 유틸리티, Next.js Link 컴포넌트, 그리고 필요한 타입을 적절히 가져왔습니다.
12-13: 새로운 컴포넌트 임포트모임 탈퇴 버튼과 대기 상태 칩 컴포넌트를 가져와 UI 기능을 확장했습니다.
16-18: 상태 오버레이를 위한 인터페이스 정의
IStatusOverlay인터페이스를 통해StatusOverlay컴포넌트의 props 타입을 명확하게 정의했습니다.
45-49: 비활성화된 상태 확인 함수 구현모임 상태가 'REJECTED'나 'EXPEL'인 경우를 확인하는 함수가 잘 구현되었습니다. 이는 오버레이 표시 여부를 결정하는 데 사용됩니다.
51-57: 모임 상세 페이지 URL 생성 및 클릭 핸들러모임 상세 페이지 URL을 생성하는 함수와 카드 클릭 핸들러 함수가 잘 구현되었습니다. 카테고리 제목을 영어로 변환하는 유틸리티 함수를 활용하여 URL을 동적으로 생성합니다.
60-82: 상태 오버레이 컴포넌트 구현모임 참여 상태가 'REJECTED'나 'EXPEL'인 경우에 대한 시각적 피드백을 제공하는 오버레이 컴포넌트가 잘 구현되었습니다. 사용자가 모임 상세 페이지로 이동할 수 있는 링크도 포함되어 있습니다.
92-131: 데스크탑 뷰의 상태 기반 UI 구현데스크탑 뷰에서 모임 카드, 상태 칩, 탈퇴 버튼, 그리고 오버레이가 모임 상태에 따라 조건부로 렌더링되도록 잘 구현되었습니다.
모임 카드에
showLikeButton={false}를 추가한 것은 PR 코멘트에서 언급된 "찜하기 버튼을 숨겨달라"는 요청을 반영한 것으로 보입니다.
219-230: 로딩 및 빈 상태 처리 개선무한 스크롤 트리거, 추가 데이터 로딩 표시, 그리고 데이터가 없는 경우의 메시지가 잘 구현되었습니다. 특히 빈 상태에 대한 메시지가 사용자에게 더 명확한 정보를 제공하도록 개선되었습니다.
| <div key={meeting.meetingId}> | ||
| {/* 데스크탑 */} | ||
| <div className="hidden border-b border-Cgray300 py-[42px] lg:flex"> | ||
| <HorizonCard | ||
| onClick={() => | ||
| router.push( | ||
| `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`, | ||
| ) | ||
| } | ||
| key={meeting.meetingId} | ||
| title={meeting.title} | ||
| thumbnailUrl={meeting.thumbnail} | ||
| location={meeting.location} | ||
| total={meeting.maxMember} | ||
| value={meeting.memberCount} | ||
| className="flex-row" | ||
| meetingId={meeting.meetingId} | ||
| category={translateCategoryNameToEng(meeting.categoryTitle)} | ||
| isLike={true} | ||
| likesCount={meeting.likesCount} | ||
| ></HorizonCard> | ||
| </div> | ||
|
|
||
| {/* 태블릿 */} | ||
| <div className="hidden flex-col border-b border-Cgray300 py-[42px] md:flex lg:hidden"> | ||
| <HorizonCard | ||
| onClick={() => | ||
| router.push( | ||
| `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`, | ||
| ) | ||
| } | ||
| key={meeting.meetingId} | ||
| title={meeting.title} | ||
| thumbnailUrl={meeting.thumbnail} | ||
| location={meeting.location} | ||
| total={meeting.maxMember} | ||
| value={meeting.memberCount} | ||
| thumbnailHeight={160} | ||
| thumbnailWidth={160} | ||
| className="" | ||
| meetingId={meeting.meetingId} | ||
| category={translateCategoryNameToEng(meeting.categoryTitle)} | ||
| isLike={true} | ||
| likesCount={meeting.likesCount} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* 모바일 */} | ||
| <div className="flex flex-col border-b border-Cgray300 py-[42px] md:hidden"> | ||
| <HorizonCard | ||
| onClick={() => | ||
| router.push( | ||
| `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`, | ||
| ) | ||
| } | ||
| key={meeting.meetingId} | ||
| title={meeting.title} | ||
| thumbnailUrl={meeting.thumbnail} | ||
| location={meeting.location} | ||
| total={meeting.maxMember} | ||
| value={meeting.memberCount} | ||
| thumbnailHeight={80} | ||
| thumbnailWidth={80} | ||
| className="" | ||
| meetingId={meeting.meetingId} | ||
| category={translateCategoryNameToEng(meeting.categoryTitle)} | ||
| isLike={true} | ||
| likesCount={meeting.likesCount} | ||
| /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
코드 중복 및 최적화 제안
데스크탑, 태블릿, 모바일 레이아웃 코드가 중복되어 있습니다. 반응형 UI를 구현하는 더 효율적인 방법이 있습니다.
반복되는 HorizonCard 컴포넌트를 하나의 함수로 추출하고 화면 크기에 따라 속성값만 변경하는 방식으로 리팩토링하세요:
- {/* 데스크탑 */}
- <div className="hidden border-b border-Cgray300 py-[42px] lg:flex">
- <HorizonCard
- onClick={() =>
- router.push(
- `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
- )
- }
- key={meeting.meetingId}
- title={meeting.title}
- thumbnailUrl={meeting.thumbnail}
- location={meeting.location}
- total={meeting.maxMember}
- value={meeting.memberCount}
- className="flex-row"
- meetingId={meeting.meetingId}
- category={translateCategoryNameToEng(meeting.categoryTitle)}
- isLike={true}
- likesCount={meeting.likesCount}
- ></HorizonCard>
- </div>
-
- {/* 태블릿 */}
- <div className="hidden flex-col border-b border-Cgray300 py-[42px] md:flex lg:hidden">
- <HorizonCard
- onClick={() =>
- router.push(
- `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
- )
- }
- key={meeting.meetingId}
- title={meeting.title}
- thumbnailUrl={meeting.thumbnail}
- location={meeting.location}
- total={meeting.maxMember}
- value={meeting.memberCount}
- thumbnailHeight={160}
- thumbnailWidth={160}
- className=""
- meetingId={meeting.meetingId}
- category={translateCategoryNameToEng(meeting.categoryTitle)}
- isLike={true}
- likesCount={meeting.likesCount}
- />
- </div>
-
- {/* 모바일 */}
- <div className="flex flex-col border-b border-Cgray300 py-[42px] md:hidden">
- <HorizonCard
- onClick={() =>
- router.push(
- `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
- )
- }
- key={meeting.meetingId}
- title={meeting.title}
- thumbnailUrl={meeting.thumbnail}
- location={meeting.location}
- total={meeting.maxMember}
- value={meeting.memberCount}
- thumbnailHeight={80}
- thumbnailWidth={80}
- className=""
- meetingId={meeting.meetingId}
- category={translateCategoryNameToEng(meeting.categoryTitle)}
- isLike={true}
- likesCount={meeting.likesCount}
- />
- </div>
+ <div className="border-b border-Cgray300 py-[42px]">
+ {/* 데스크탑 */}
+ <div className="hidden lg:flex">
+ <HorizonCard
+ onClick={() =>
+ router.push(
+ `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
+ )
+ }
+ key={meeting.meetingId}
+ title={meeting.title}
+ thumbnailUrl={meeting.thumbnail}
+ location={meeting.location}
+ total={meeting.maxMember}
+ value={meeting.memberCount}
+ className="flex-row"
+ meetingId={meeting.meetingId}
+ category={translateCategoryNameToEng(meeting.categoryTitle)}
+ isLike={true}
+ likesCount={meeting.likesCount}
+ />
+ </div>
+
+ {/* 태블릿 */}
+ <div className="hidden md:flex lg:hidden flex-col">
+ <HorizonCard
+ onClick={() =>
+ router.push(
+ `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
+ )
+ }
+ key={meeting.meetingId}
+ title={meeting.title}
+ thumbnailUrl={meeting.thumbnail}
+ location={meeting.location}
+ total={meeting.maxMember}
+ value={meeting.memberCount}
+ thumbnailHeight={160}
+ thumbnailWidth={160}
+ meetingId={meeting.meetingId}
+ category={translateCategoryNameToEng(meeting.categoryTitle)}
+ isLike={true}
+ likesCount={meeting.likesCount}
+ />
+ </div>
+
+ {/* 모바일 */}
+ <div className="flex flex-col md:hidden">
+ <HorizonCard
+ onClick={() =>
+ router.push(
+ `/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
+ )
+ }
+ key={meeting.meetingId}
+ title={meeting.title}
+ thumbnailUrl={meeting.thumbnail}
+ location={meeting.location}
+ total={meeting.maxMember}
+ value={meeting.memberCount}
+ thumbnailHeight={80}
+ thumbnailWidth={80}
+ meetingId={meeting.meetingId}
+ category={translateCategoryNameToEng(meeting.categoryTitle)}
+ isLike={true}
+ likesCount={meeting.likesCount}
+ />
+ </div>
+ </div>더 나아가, renderMeetingCard 함수를 만들어 중복을 더 줄일 수 있습니다:
const renderMeetingCard = (meeting, props = {}) => (
<HorizonCard
onClick={() =>
router.push(
`/meeting/${translateCategoryNameToEng(meeting.categoryTitle)}/${meeting.meetingId}`,
)
}
key={meeting.meetingId}
title={meeting.title}
thumbnailUrl={meeting.thumbnail}
location={meeting.location}
total={meeting.maxMember}
value={meeting.memberCount}
meetingId={meeting.meetingId}
category={translateCategoryNameToEng(meeting.categoryTitle)}
isLike={true}
likesCount={meeting.likesCount}
{...props}
/>
);…t/refactor/i-participated-meeting/DEVING-79
CI Status Report검사 결과
❌ 일부 검사가 실패했습니다. |
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |


📝 주요 작업 내용
커밋순으로 확인해주시면 감사합니다.!
@dbswl701 내가참여하고있는 모임 명칭변경 확인해주시면 감사하겠습니다!
📺 스크린샷
🔗 참고 사항
ex) 의논할 점, 리뷰어가 알 필요가 있는 추가 정보나 문서, 참고 링크를 포함.
💬 리뷰 요구사항
ex) 중점적으로 리뷰해줬으면 하는 부분
📃 관련 이슈
ex) #이슈 번호
Summary by CodeRabbit