-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/159/crew review #166
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/159/crew review #166
Conversation
Walkthrough이 PR에서는 새로운 비동기 함수 Changes
Suggested reviewers
Warning There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure. 🔧 eslint (1.23.1)
src/app/(crew)/my-page/_components/profile-card/container.tsxOops! Something went wrong! :( ESLint: 8.57.1 ESLint couldn't determine the plugin "react-hooks" uniquely.
Please remove the "plugins" setting from either config or remove either plugin installation. If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🔇 Additional comments (3)src/app/(crew)/my-page/_components/profile-card/container.tsx (3)
불필요한 상태 변수를 제거하고 user 객체에서 직접 profileImageUrl을 도출하도록 변경한 것이 좋은 개선입니다.
로딩 상태와 사용자 데이터 부재 시의 처리가 명확하게 구분되어 있어 좋습니다. 주석도 이해하기 쉽게 작성되었습니다.
메모리 누수 방지를 위한 URL 정리가 필요합니다. 임시 URL을 생성하는 경우 메모리 누수를 방지하기 위해 정리가 필요합니다. 다음과 같이 수정을 제안합니다: const file = (event.target as HTMLInputElement)?.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
toast.error('5MB 이하의 파일만 업로드 가능합니다.');
return;
}
try {
+ const tempUrl = URL.createObjectURL(file);
await updateUserProfile(file);
toast.success('프로필 이미지가 업데이트되었습니다.');
await refetchUser();
+ URL.revokeObjectURL(tempUrl);
} catch (error) {
toast.error('파일 업로드에 실패했습니다.');
}
}Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 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 (
|
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: 9
🧹 Outside diff range and nitpick comments (25)
src/components/common/skeleton/my-review-skeleton-list/index.tsx (1)
6-6: Array.from 사용에 대한 제안현재 구현은 작동하지만, 더 간단한 방법을 제안드립니다.
다음과 같이 변경하는 것을 고려해보세요:
-{Array.from({ length: 3 }).map((_, idx) => ( +{[...Array(3)].map((_, idx) => (src/_apis/crew/crew-review-apis.ts (1)
4-16: API 구현에 대한 개선 제안다음 사항들을 고려해 주시기 바랍니다:
- 페이지 크기(
size=5)가 하드코딩되어 있습니다. 이를 매개변수로 받거나 설정 파일로 관리하는 것이 더 유연할 것 같습니다.- API 응답 실패에 대한 구체적인 에러 처리가 필요해 보입니다.
다음과 같이 개선해 보시는 것은 어떨까요?:
-export async function getCrewReviews(crewId: number, page: number): Promise<CrewReviewResponse> { - const url = `/api/review/${crewId}?page=${page}&size=5`; +export async function getCrewReviews( + crewId: number, + page: number, + pageSize: number = 5 +): Promise<CrewReviewResponse> { + const url = `/api/review/${crewId}?page=${page}&size=${pageSize}`; const response = await fetchApi<{ data: CrewReviewResponse }>(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); + if (!response.data) { + throw new Error('크루 리뷰 데이터를 불러오는데 실패했습니다.'); + } return response.data; }src/components/common/skeleton/my-review-skeleton/index.tsx (1)
6-6: 스켈레톤 이미지에 로딩 애니메이션 추가를 고려해보세요.이미지 스켈레톤의 반응형 구현은 잘 되어있지만, 사용자 경험 향상을 위해 펄스 애니메이션을 추가하면 좋을 것 같습니다.
-<Skeleton className="relative h-28 w-28 flex-shrink-0 rounded-[12px] md:h-[166px] md:w-[294px]" /> +<Skeleton className="relative h-28 w-28 flex-shrink-0 rounded-[12px] md:h-[166px] md:w-[294px] animate-pulse" />src/components/common/skeleton/review-skeleton/index.tsx (3)
12-17: 배열 인덱스를 key로 사용하는 것에 대한 대안을 고려해보세요.현재 구현에서는 ESLint 규칙을 비활성화하고 배열 인덱스를 key로 사용하고 있습니다. 스켈레톤 컴포넌트의 특성상 큰 문제는 없지만, 더 나은 방법을 제안드립니다.
다음과 같이 수정하는 것을 고려해보세요:
- {Array.from({ length: 3 }).map((_, idx) => ( - // eslint-disable-next-line react/no-array-index-key - <li key={`mine-skeleton-${idx}`} className="h-auto min-h-[112px]"> + {['skeleton1', 'skeleton2', 'skeleton3'].map((id) => ( + <li key={id} className="h-auto min-h-[112px]">
21-26: 그리드 레이아웃의 반응형 처리가 필요할 수 있습니다.현재 그리드 레이아웃이 고정된 갭으로 구현되어 있습니다. 다양한 화면 크기에 대응하기 위해 반응형 처리를 추가하는 것이 좋을 것 같습니다.
다음과 같이 수정하는 것을 고려해보세요:
- <div className="mb-6 grid flex-grow gap-4"> + <div className="mb-6 grid flex-grow gap-4 sm:gap-6 md:gap-8">
7-30: 컴포넌트 구조가 잘 구성되어 있습니다.전반적으로 컴포넌트의 구조가 깔끔하고, 조건부 렌더링을 통해 타입별로 적절한 스켈레톤을 표시하고 있습니다. 재사용성과 유지보수성이 좋아 보입니다.
추후 스켈레톤의 개수를 props로 받아 더 유연하게 처리하는 것을 고려해보세요.
src/app/(crew)/my-page/my-review/page.tsx (1)
19-22: 로딩 상태 UI가 개선되었으나, 구조를 더 단순화할 수 있습니다.스켈레톤 UI를 사용하여 사용자 경험이 향상되었습니다. 하지만 중첩된 div 구조를 단순화할 수 있습니다.
다음과 같이 단일 div로 구조를 단순화하는 것을 제안드립니다:
- <div className="mt-8"> - <div className="w-full"> - <ReviewListSkeleton type="mine" /> - </div> - </div> + <div className="mt-8 w-full"> + <ReviewListSkeleton type="mine" /> + </div>src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx (1)
41-45: 참가자 프로필 이미지 테스트 데이터 보완이 필요합니다.현재 모든 참가자의
profileImageUrl이 비어있어 컴포넌트의 이미지 처리 기능을 제대로 테스트할 수 없습니다.최소한 한 명의 참가자에게는 실제 이미지 URL을 설정하여 이미지 렌더링을 테스트하는 것을 추천드립니다:
participants: [ - { id: 1, nickname: '유저1', profileImageUrl: '' }, + { id: 1, nickname: '유저1', profileImageUrl: 'https://example.com/profile1.jpg' }, { id: 2, nickname: '유저2', profileImageUrl: '' }, { id: 3, nickname: '유저3', profileImageUrl: '' }, ],src/components/common/skeleton/review-skeleton/review-card.tsx (2)
7-16: 조건부 스타일링 로직 개선이 필요합니다.템플릿 리터럴 내의 조건부 클래스네임이 복잡해 보입니다. 가독성 향상을 위해 별도의 변수로 추출하는 것이 좋겠습니다.
다음과 같이 개선해보세요:
+ const containerClassName = isMine + ? 'rounded-[12px] p-6 shadow-bg' + : 'border-b-[2px] border-[#F3F4F6] py-4'; + <div role="presentation" - className={`flex h-full items-end gap-[15px] ${ - isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4' - } bg-white lg:gap-[40px]`} + className={`flex h-full items-end gap-[15px] ${containerClassName} bg-white lg:gap-[40px]`} >
26-34: 하단 영역의 조건부 렌더링 최적화가 필요합니다.현재 구조에서는 불필요한 중첩된 삼항 연산자와 조건부 렌더링이 있습니다. 이를 더 명확하게 분리하면 좋겠습니다.
다음과 같이 개선해보세요:
- <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}> - {!isMine && ( - <> - <Skeleton className="h-8 w-8 rounded-full" /> - <Skeleton className="h-4 w-24" /> - </> - )} - <Skeleton className="h-4 w-12" /> - </div> + <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}> + {isMine ? ( + <Skeleton className="h-4 w-12" /> + ) : ( + <div className="flex items-center gap-2"> + <Skeleton className="h-8 w-8 rounded-full" /> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-4 w-12" /> + </div> + )} + </div>src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx (1)
Line range hint
25-34: 리뷰 목록이 비어있을 때의 상태 처리가 필요합니다.현재 구현에서는 리뷰 목록이 비어있을 때의 UI 처리가 누락되어 있습니다. 사용자 경험 향상을 위해 빈 상태 처리를 추가하는 것이 좋겠습니다.
다음과 같은 방식으로 개선해보세요:
<div className="mb-6 grid flex-grow gap-4"> + {reviews.length === 0 ? ( + <div className="text-center text-gray-500"> + 아직 작성된 리뷰가 없습니다. + </div> + ) : ( {reviews.map((review) => ( <ReviewCard key={review.id} rate={review.rate} comment={review.comment} createdAt={review.createdAt} crewId={review.crewId} reviewer={review.reviewer} /> ))} + )} </div>src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx (1)
Line range hint
9-13: useInfiniteScroll 훅 사용이 개선되었습니다.isLoading 상태값이 적절히 추가되었습니다. 다만, 타입 안정성을 위해 다음과 같은 개선을 제안드립니다.
- const { data, ref, isFetchingNextPage, isLoading } = useInfiniteScroll( + const { + data, + ref, + isFetchingNextPage, + isLoading + }: { + data: ReviewableGatheringResponse | undefined; + ref: RefCallback<HTMLDivElement>; + isFetchingNextPage: boolean; + isLoading: boolean; + } = useInfiniteScroll(src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx (1)
42-46: 반응형 디자인 개선이 잘 이루어졌습니다!모바일과 데스크톱 환경 모두에서 최적화된 레이아웃을 제공하도록 구현되었습니다.
접근성 개선을 위한 제안
스크린 리더 사용자를 위해 약속 개수 정보에 aria-label을 추가하면 좋을 것 같습니다.
다음과 같이 수정해보세요:
- <span className="text-sm font-semibold text-blue-500 md:text-base"> + <span + className="text-sm font-semibold text-blue-500 md:text-base" + aria-label={`현재 개설된 약속 수: ${totalGatheringCount}개`} + >src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx (2)
43-53: 이미지 최적화를 위한 제안사항이 있습니다.이미지 처리가 전반적으로 잘 구현되어 있으나, 성능 최적화를 위해 다음과 같은 개선을 고려해보세요:
<Image src={imageUrl} alt={gatheringName} fill sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw" + loading="lazy" + quality={75} className="h-full w-full object-cover" />
Line range hint
22-102: 에러 처리 개선을 위한 제안사항입니다.컴포넌트 구조는 잘 되어있으나, 에러 처리를 위한 ErrorBoundary 추가를 고려해보세요:
// ErrorBoundary 컴포넌트 예시 class ReviewableGatheringCardErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } render() { if (this.state.hasError) { return <div>리뷰 카드를 불러오는 중 문제가 발생했습니다.</div>; } return this.props.children; } } // 사용 예시: <ReviewableGatheringCardErrorBoundary> <ReviewableGatheringCard {...props} /> </ReviewableGatheringCardErrorBoundary>src/components/common/crew-list/crew-card.tsx (2)
37-41: 성능 최적화를 위한 Set 업데이트 방식 개선이 필요합니다.현재 구현은 매번 새로운 Set을 생성하고 있습니다. 다음과 같이 기존 Set을 직접 수정하는 방식으로 개선할 수 있습니다.
const handleMouseEnter = () => { if (!prefetchedPages.has(CREWPAGE)) { router.prefetch(CREWPAGE); - setPrefetchedPages(new Set(prefetchedPages).add(CREWPAGE)); + setPrefetchedPages(prev => { + prev.add(CREWPAGE); + return new Set(prev); + }); } };
37-41: 프리패칭 전략에 대한 제안현재 마우스 진입 시에만 프리패칭을 수행하고 있습니다. 다음과 같은 개선을 고려해보세요:
- Intersection Observer를 사용하여 카드가 뷰포트에 진입할 때 프리패칭
- 사용자의 스크롤 속도와 방향을 고려한 예측적 프리패칭
- 네트워크 상태에 따른 조건부 프리패칭
이러한 전략은 사용자 경험을 더욱 개선할 수 있습니다.
src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx (1)
54-64: 데이터 처리 로직이 잘 구현되었습니다기본 점수 데이터 구조와 병합 로직이 깔끔하게 구현되었습니다. 다만, 성능 최적화를 위해 다음과 같은 개선사항을 고려해보시면 좋을 것 같습니다.
- const defaultRatingsData = Array.from({ length: 5 }, (_, index) => ({ - score: 5 - index, - count: 0, - })); + // 컴포넌트 외부 또는 useMemo로 캐싱하여 재렌더링 시 불필요한 재생성 방지 + const defaultRatingsData = useMemo( + () => + Array.from({ length: 5 }, (_, index) => ({ + score: 5 - index, + count: 0, + })), + [] + );src/components/common/review-list/review-card.tsx (2)
90-94: 스타일링 개선사항이 있습니다.
cn유틸리티를 사용하여 조건부 클래스 적용이 더 명확해졌습니다. 다만, 매직 넘버(12px,40px등)를 사용하고 있어 개선이 필요합니다.- 'rounded-[12px] p-6 shadow-bg' + 'rounded-xl p-6 shadow-bg' - 'lg:gap-[40px]' + 'lg:gap-10'
121-121: 버튼 스타일링이 개선되었습니다.반응형 디자인을 고려한 패딩 조정이 적절히 이루어졌습니다. 다만, 매직 넘버 사용을 피하고 Tailwind의 기본 스페이싱을 활용하는 것이 좋습니다.
- className="btn-outlined absolute bottom-4 right-4 flex-shrink-0 p-[4px_10px] text-base font-semibold md:bottom-6 md:right-6 md:px-4" + className="btn-outlined absolute bottom-4 right-4 flex-shrink-0 px-2.5 py-1 text-base font-semibold md:bottom-6 md:right-6 md:px-4"src/mock/review-data.ts (5)
Line range hint
69-71: 중복된 ID 값이 발견되었습니다여러 리뷰 데이터에서 ID 값 5가 중복되어 사용되고 있습니다. 고유한 식별자로서 ID는 중복되지 않아야 합니다.
다음과 같이 수정해주세요:
- id: 5, + id: 6, rate: 1, // ... 그리고 - id: 5, + id: 7, rate: 4,Also applies to: 82-84, 95-97
Line range hint
254-254: 날짜 형식이 일관되지 않습니다
createdAt필드의 날짜 형식이 일관되지 않습니다. 대부분 ISO 8601 형식(YYYY-MM-DDThh:mm:ss.sss+HH:MM)을 사용하고 있으나, 일부는 다른 형식(YYYY-MM-DDThh:mm)을 사용하고 있습니다.모든 날짜를 ISO 8601 형식으로 통일해주세요:
- createdAt: '2024-10-31T00:56', + createdAt: '2024-10-31T00:56:00.000+09:00',Also applies to: 276-276
18-18: 프로필 이미지 URL이 모두 동일합니다모든 리뷰어의
profileImageUrl이 동일한 값을 사용하고 있습니다. 목업 데이터의 현실성을 높이기 위해 다양한 이미지 URL을 사용하는 것이 좋습니다.파일 상단에 주석 처리된 이미지 URL들을 활용하여 다양한 프로필 이미지를 설정해주세요.
Also applies to: 30-30, 42-42, 54-54, 66-66, 78-78, 90-90, 102-102, 114-114, 126-126, 138-138, 150-150, 162-162, 174-174, 186-186, 198-198, 210-210
Line range hint
1-3: 주석 처리된 이미지 URL 정리가 필요합니다현재 사용되지 않는 이미지 URL들이 주석으로 남아있습니다. 코드의 가독성을 위해 사용하지 않는 주석은 제거하거나, 실제 데이터에서 활용하는 것이 좋습니다.
Line range hint
219-297: 페이지네이션 테스트를 위한 데이터 보완이 필요합니다
MyReviewData의 페이지네이션 테스트 데이터에서 다음과 같은 부분을 보완하면 좋겠습니다:
pageParams배열이 비어있습니다- 각 페이지의 데이터 수가 일정합니다
테스트의 완성도를 높이기 위해 다음 사항을 고려해주세요:
- 실제 페이지네이션 파라미터 예시 추가
- 페이지별로 다양한 수의 데이터 포함
- 경계값 테스트를 위한 데이터 추가 (빈 페이지, 최대 개수 등)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
src/_apis/crew/crew-review-apis.ts(1 hunks)src/_queries/crew/crew-review-queries.ts(1 hunks)src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx(1 hunks)src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx(3 hunks)src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx(4 hunks)src/app/(crew)/crew/detail/[id]/_components/review-section.tsx(1 hunks)src/app/(crew)/crew/detail/[id]/page.tsx(2 hunks)src/app/(crew)/my-page/my-review/page.tsx(2 hunks)src/components/common/crew-list/crew-card.tsx(1 hunks)src/components/common/review-list/review-card.tsx(5 hunks)src/components/common/skeleton/my-review-skeleton-list/index.tsx(1 hunks)src/components/common/skeleton/my-review-skeleton/index.tsx(1 hunks)src/components/common/skeleton/review-skeleton/index.tsx(1 hunks)src/components/common/skeleton/review-skeleton/review-card.tsx(1 hunks)src/components/gathering-list/liked-list-presenter.tsx(1 hunks)src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx(3 hunks)src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx(1 hunks)src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx(3 hunks)src/mock/review-data.ts(17 hunks)src/types/review.d.ts(1 hunks)
🔇 Additional comments (42)
src/_queries/crew/crew-review-queries.ts (2)
1-2: LGTM! 필요한 의존성들이 올바르게 임포트되었습니다.
필수 라이브러리와 API 함수가 적절하게 임포트되었습니다.
4-9: API 호출 검증이 필요합니다.
크루 리뷰 API가 올바르게 호출되는지 확인이 필요합니다.
다음 스크립트로 API 호출 패턴을 검증해보겠습니다:
✅ Verification successful
API 호출이 올바르게 구현되어 있습니다.
크루 리뷰 API 구현을 검증한 결과:
getCrewReviews함수가src/_apis/crew/crew-review-apis.ts에 올바르게 구현되어 있습니다- API 엔드포인트, HTTP 메서드, 헤더 설정이 적절합니다
useGetCrewReviewsQuery에서 페이지네이션 처리를 위해page - 1로 변환하여 호출하는 것이 확인되었습니다
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# API 구현 확인
echo "크루 리뷰 API 구현 확인:"
ast-grep --pattern 'export async function getCrewReviews($_: number, $_: number)'
# API 응답 타입 정의 확인
echo "응답 타입 정의 확인:"
rg -A 5 "type CrewReviewResponse"
Length of output: 197
Script:
#!/bin/bash
# API 구현 파일 찾기
echo "API 구현 파일 찾기:"
fd "crew.*api" --type f
# getCrewReviews 함수 검색
echo -e "\ngetCrewReviews 함수 검색:"
rg "getCrewReviews" -A 5
# 크루 리뷰 관련 타입 정의 검색
echo -e "\n크루 리뷰 타입 정의 검색:"
rg "Review.*type|type.*Review" -A 3
Length of output: 9736
Script:
#!/bin/bash
# 리뷰 타입 정의 확인
echo "리뷰 타입 정의 확인:"
rg -p "type CrewReview|type CrewReviewResponse" src/types/review.ts -A 5
# API 구현 상세 확인
echo -e "\nAPI 구현 상세 확인:"
cat src/_apis/crew/crew-review-apis.ts
Length of output: 712
src/components/common/skeleton/my-review-skeleton-list/index.tsx (3)
5-5: 시맨틱 마크업으로의 개선을 승인합니다
div에서 ul/li 구조로 변경한 것은 웹 접근성과 시맨틱 마크업 측면에서 적절한 개선입니다. 또한 flex layout과 gap을 활용한 스타일링도 잘 구현되었습니다.
Also applies to: 8-10
8-8: ESLint 규칙 비활성화에 대한 의견
index를 key로 사용하는 것에 대한 ESLint 경고를 비활성화하셨는데, 이 경우는 고정된 개수의 정적 리스트이므로 문제가 되지 않습니다. 주석은 유지하는 것이 좋겠습니다.
3-3: props 제거로 인한 컴포넌트 재사용성 검토 필요
동적인 skeleton 개수 설정이 불가능해졌습니다. 다른 페이지나 컴포넌트에서 다른 개수의 skeleton이 필요한 경우를 고려하여, props를 유지하는 것이 더 좋을 수 있습니다.
다음 스크립트로 다른 페이지에서의 사용 사례를 확인해보겠습니다:
✅ Verification successful
고정된 스켈레톤 개수는 현재 사용 사례에 적합합니다
현재 MyReviewSkeletonList는 로딩 상태를 표시하는 용도로만 사용되고 있으며, 다른 페이지나 컴포넌트에서 재사용되지 않습니다. 3개의 고정된 스켈레톤 아이템은 현재 구현된 사용 사례에 충분합니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check usage of MyReviewSkeletonList in other components
rg -l "MyReviewSkeletonList"
Length of output: 173
Script:
#!/bin/bash
# Check how MyReviewSkeletonList is used in reviewable-gathering-card-list.tsx
rg -A 5 -B 5 "MyReviewSkeletonList" src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx
Length of output: 909
src/_apis/crew/crew-review-apis.ts (1)
1-2: 타입과 유틸리티 임포트가 적절히 구성되어 있습니다.
필요한 유틸리티와 타입이 명확하게 임포트되어 있으며, 타입 정의가 잘 되어 있습니다.
src/components/common/skeleton/my-review-skeleton/index.tsx (1)
5-5: 컨테이너 레이아웃이 적절히 구현되었습니다.
반응형 레이아웃을 위한 flex-wrap과 최대 너비 제한이 잘 설정되어 있습니다. 경계선과 패딩도 적절히 적용되었습니다.
src/components/common/skeleton/review-skeleton/index.tsx (1)
3-5: 타입 정의가 명확하고 적절합니다.
인터페이스가 간단하고 목적에 맞게 잘 정의되어 있습니다.
src/app/(crew)/my-page/my-review/page.tsx (1)
8-8: 스켈레톤 컴포넌트 임포트가 적절합니다!
컴포넌트 이름이 명확하고 절대 경로 임포트 패턴을 잘 따르고 있습니다.
src/types/review.d.ts (3)
32-39: 리뷰 평점 정보 인터페이스가 잘 구성되었습니다
평점의 총합, 평균, 그리고 점수별 분포를 명확하게 표현하고 있습니다.
49-51: 응답 타입이 깔끔하게 구성되었습니다
리뷰 평점 정보와 리뷰 목록을 하나의 응답으로 잘 구조화했습니다.
29-30: 프로퍼티 이름 변경에 따른 사용처 확인 필요
imageUrl에서 profileImageUrl로의 변경이 다른 컴포넌트에도 영향을 미칠 수 있습니다.
✅ Verification successful
Based on the search results, I can generate the final response as the verification is complete. The search shows that:
- The property
imageUrlis used in different contexts (crew images, gathering images) and is not related to profile images - The property
profileImageUrlis consistently used across the codebase for user profile images in relevant interfaces and components - The change from
imageUrltoprofileImageUrlinsrc/types/review.d.tsaligns with the existing usage pattern for profile images
프로퍼티 이름 변경이 올바르게 적용되어 있습니다
imageUrl에서 profileImageUrl로의 변경이 코드베이스 전체에서 일관되게 적용되어 있습니다. 사용자 프로필 이미지는 profileImageUrl을, 다른 이미지(모임, 크루 등)는 imageUrl을 사용하여 명확하게 구분되어 있습니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for any remaining usage of old property name
# Search for imageUrl usage in TypeScript/JavaScript files
rg "imageUrl" --type ts --type js
# Search for profileImageUrl usage to confirm the change
rg "profileImageUrl" --type ts --type js
Length of output: 24414
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.stories.tsx (1)
1-28: 스토리북 설정이 잘 구성되어 있습니다!
컴포넌트 중앙 정렬을 위한 데코레이터와 레이아웃 파라미터가 적절하게 설정되어 있습니다.
src/components/common/skeleton/review-skeleton/review-card.tsx (2)
1-5: Props 인터페이스가 잘 정의되어 있습니다.
isMine prop의 선택적 특성이 잘 표현되어 있으며, 타입 정의가 명확합니다.
17-25: 컨텐츠 영역의 구조가 잘 구성되어 있습니다.
스켈레톤 UI의 각 요소들이 실제 데이터를 표현하기에 적절한 크기와 간격으로 설정되어 있습니다.
src/app/(crew)/crew/detail/[id]/page.tsx (3)
40-40: 마진 조정이 적절해 보입니다
반응형 디자인을 유지하면서 상단 여백을 조정한 것이 레이아웃 개선에 도움이 될 것 같습니다.
4-9: SSR 비활성화에 대한 검토가 필요합니다
세 컴포넌트 모두 SSR을 비활성화했는데, 이는 초기 렌더링과 SEO에 영향을 미칠 수 있습니다. 각 컴포넌트가 정말로 클라이언트 사이드 렌더링만 필요한지 확인해주세요.
✅ Verification successful
클라이언트 사이드 렌더링이 필요한 것으로 확인되었습니다
검증 결과, 다음과 같은 클라이언트 종속성이 발견되어 SSR 비활성화가 적절한 것으로 판단됩니다:
window.locationAPI 사용 (gathering-list-section.tsx,detail-crew-container.tsx)- React의 상태 관리 훅 (
useState) 및 사이드 이펙트 (useEffect) 광범위하게 사용 - 다수의 사용자 상호작용 이벤트 핸들러 존재
이러한 기능들은 브라우저 환경에서만 정상적으로 동작하므로, 현재의 SSR 비활성화 설정이 타당합니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if components use browser-specific APIs that require client-side rendering
# Search for browser-specific API usage in components
echo "Checking for browser API usage in components..."
rg -t tsx "window\.|document\.|localStorage|sessionStorage" ./_components/create-gathering.tsx ./_components/gathering-list-section.tsx ./_components/review-section.tsx
Length of output: 303
Script:
#!/bin/bash
# Let's try searching in the correct directory and without file type restriction
echo "Checking for browser API usage in components..."
rg "window\.|document\.|localStorage|sessionStorage" "src/app/(crew)/crew/detail/[id]/_components/"
# Also check for React hooks that might require client-side rendering
echo -e "\nChecking for React hooks usage..."
rg "use(State|Effect|Context|Ref|Callback|Memo|Reducer)" "src/app/(crew)/crew/detail/[id]/_components/"
# Check for event handlers that might require client-side rendering
echo -e "\nChecking for event handlers..."
rg "on(Click|Change|Submit|Input)" "src/app/(crew)/crew/detail/[id]/_components/"
Length of output: 7381
43-43: CrewReviewSection 컴포넌트의 타입 안전성 확인이 필요합니다
crewId prop이 추가되었습니다. 리뷰 API 연동을 위한 적절한 변경이지만, 타입 정의가 제대로 되어있는지 확인해주세요.
src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx (1)
5-5: 클래스명 관리를 위한 cn 유틸리티 추가가 적절합니다!
클래스명을 조건부로 관리하기 위한 cn 유틸리티의 도입은 코드의 가독성과 유지보수성을 향상시킵니다.
src/components/my-page/reviewable-gatherings/reviewable-gathering-card-list.tsx (3)
5-5: 스켈레톤 컴포넌트 import가 적절히 추가되었습니다.
로딩 상태를 시각적으로 표시하기 위한 스켈레톤 컴포넌트 import가 잘 구현되었습니다.
20-24: 로딩 상태 처리가 깔끔하게 구현되었습니다.
초기 로딩 상태를 스켈레톤 UI로 처리하여 사용자 경험이 향상되었습니다. 한글 주석 처리도 잘 되어있습니다.
39-39: location prop 타입 검증이 필요합니다.
ReviewableGatheringCard 컴포넌트에 location prop이 추가되었습니다. 관련 타입이 올바르게 정의되어 있는지 확인이 필요합니다.
✅ Verification successful
location prop이 올바르게 정의되어 있음을 확인했습니다.
ReviewableGatheringCardProps 인터페이스에 location prop이 string 타입으로 정확하게 정의되어 있습니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# ReviewableGatheringCard의 props 타입 정의 확인
ast-grep --pattern 'interface ReviewableGatheringCardProps {
$$$
location: $_
$$$
}'
Length of output: 1095
src/components/gathering-list/liked-list-presenter.tsx (2)
Line range hint 24-61: UI 구현이 적절해 보입니다.
- 반응형 그리드 레이아웃이 잘 구현되어 있습니다
- 페이지네이션 스타일링이 깔끔하게 처리되어 있습니다
- GatheringCard 컴포넌트와의 연동이 잘 되어 있습니다
21-21: pageSize 제거가 적절한지 확인이 필요합니다.
pageSize prop이 제거되었는데, 이 변경이 의도적인 것인지, 그리고 페이지네이션 로직에 영향을 미치지 않는지 확인해주시기 바랍니다.
✅ Verification successful
pageSize 제거는 안전합니다
liked-list-presenter.tsx 컴포넌트에서 pageSize를 사용하지 않고 있으며, 페이지네이션 로직에도 영향을 미치지 않습니다. pageSize는 GatheringResponseType 인터페이스에 정의되어 있지만, 실제 컴포넌트에서는 content와 totalPages만 필요로 하므로 제거가 적절합니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: pageSize 사용 여부 확인
# pageSize가 다른 컴포넌트에서 사용되는지 확인
rg -A 3 "pageSize.*GatheringResponseType"
# 페이지네이션 관련 로직에서 pageSize 사용 확인
ast-grep --pattern 'interface GatheringResponseType {
$$$
pageSize
$$$
}'
Length of output: 523
Script:
#!/bin/bash
# Check if pageSize is used in the component or related files
rg -A 5 "pageSize" src/components/gathering-list/
# Check pagination related logic in the component
ast-grep --pattern 'const { $$$, pageSize, $$$ } = $_'
# Check if there are any pagination components that might use pageSize
fd -e tsx -e ts pagination src/components/
Length of output: 1939
src/components/my-page/reviewable-gatherings/reviewable-gathering-card.tsx (1)
15-15: 위치 정보 추가가 잘 되었습니다!
인터페이스에 location 속성이 명확하게 정의되어 있습니다.
src/components/common/crew-list/crew-card.tsx (2)
29-29: 상태 변수 이름 변경이 적절합니다!
prefetched에서 prefetchedPages로의 이름 변경은 상태의 목적을 더 명확하게 표현하며, Set 자료구조의 사용은 중복 없이 프리패치된 페이지를 관리하는데 적합합니다.
47-48: 이벤트 핸들러 바인딩이 깔끔합니다!
onClick과 onMouseEnter 이벤트 핸들러가 적절히 분리되어 있고, 각각의 역할이 명확합니다. 코드의 가독성과 유지보수성이 향상되었습니다.
src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx (1)
70-70: UI 렌더링 변경사항이 적절합니다
평점 표시와 프로그레스 바의 계산 로직이 일관성 있게 구현되었습니다. 특히 다음 사항들이 잘 처리되었습니다:
- 총 평가 개수 표시
- 프로그레스 바의 비율 계산
- 평점 데이터 표시 방식
Also applies to: 80-80, 84-84, 88-88
src/components/common/review-list/review-card.tsx (4)
50-54: 에러 처리 로직이 개선되었습니다.
리뷰 ID가 없는 경우에 대한 early return 처리가 추가되어 안정성이 향상되었습니다.
59-59: 선택적 체이닝 연산자 사용이 적절합니다.
refetch?.() 구문을 사용하여 refetch 함수가 없는 경우도 안전하게 처리되도록 개선되었습니다.
109-109: 프로필 이미지 속성명 변경을 확인해주세요.
imageUrl에서 profileImageUrl로 변경된 부분이 API 응답 구조와 일치하는지 확인이 필요합니다.
✅ Verification successful
Let me analyze the search results to verify the profile image property change. I can see that:
- In the types files:
src/types/user.d.tsandsrc/types/review.d.tsuseprofileImageUrl- The reviewer-related types consistently use
profileImageUrl
- In the mock data:
src/mock/review-data.tsusesprofileImageUrlfor reviewer data- Other mock data consistently uses
profileImageUrlfor user profiles
- In the components:
- The Profile component (
src/components/common/profile/index.tsx) acceptsimageUrlas a prop - The review card component is using
profileImageUrlfrom the reviewer object
Based on this analysis, I can now provide the final response:
프로필 이미지 속성명이 올바르게 변경되었습니다.
Profile 컴포넌트는 imageUrl prop을 받도록 설계되어 있으며, reviewer 객체에서 profileImageUrl을 사용하는 것이 API 응답 구조와 일치합니다. 타입 정의와 실제 구현이 모두 일관성 있게 적용되어 있습니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# ReviewerType 인터페이스와 API 응답 구조 확인
rg "type ReviewerType" -A 5
rg "profileImageUrl|imageUrl" --type ts
Length of output: 23439
20-20: 선택적 프로퍼티 변경에 대한 검증이 필요합니다.
id와 refetch 프로퍼티를 선택적으로 변경한 것이 다른 컴포넌트에 영향을 미치지 않는지 확인이 필요합니다.
Also applies to: 26-26
✅ Verification successful
Let me analyze the usage of ReviewCard to verify the impact of optional properties. I need to check the actual props being passed in these locations.
Let me check one more thing to verify the type definitions and usage patterns.
선택적 프로퍼티 변경이 안전하게 이루어졌습니다.
id와 refetch 프로퍼티를 선택적으로 변경한 것이 적절합니다. 코드베이스 분석 결과:
review-card-list.tsx에서는 모든 필수 props를 올바르게 전달하고 있습니다.- 선택적 프로퍼티들은 컴포넌트의 조건부 기능을 위해 사용되며, 필수 기능에는 영향을 주지 않습니다.
- 인터페이스 정의가 명확하고 일관성 있게 구현되어 있습니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# ReviewCard 컴포넌트의 사용처를 검색하여 영향도 분석
rg -A 2 "<ReviewCard"
Length of output: 1587
Script:
#!/bin/bash
# Check implementation of ReviewCard in crew-review-list.tsx
rg -A 10 "<ReviewCard" "src/app/(crew)/crew/detail/\[id\]/_components/crew-review-list.tsx"
# Check implementation in review-card-list.tsx
rg -A 10 "<ReviewCard" "src/components/common/review-list/review-card-list.tsx"
Length of output: 632
Script:
#!/bin/bash
# Check ReviewCard props interface/type definition
ast-grep --pattern 'interface ReviewCardProps {
$$$
}'
# Also check for type definition
ast-grep --pattern 'type ReviewCardProps = {
$$$
}'
Length of output: 1064
src/app/(crew)/crew/detail/[id]/_components/review-section.tsx (10)
3-5: 필요한 모듈들이 정확하게 임포트되었습니다.
useState, useGetCrewReviewsQuery, ReviewListSkeleton 모듈이 올바르게 임포트되었습니다.
9-11: 컴포넌트 프로퍼티 인터페이스 정의가 정확합니다.
CrewReviewSectionProps 인터페이스가 crewId를 포함하도록 올바르게 정의되었습니다.
13-13: 컴포넌트에 프로퍼티가 올바르게 전달되었습니다.
CrewReviewSection 컴포넌트가 { crewId }를 받아서 사용하도록 수정되었습니다.
14-15: 상태 관리 및 데이터 페칭이 적절하게 구현되었습니다.
useState를 사용하여 페이지 상태를 초기화하고 관리합니다.useGetCrewReviewsQuery를 사용하여crewId와page를 기반으로 리뷰 데이터를 가져옵니다.
17-19: 페이지 변경 핸들러가 올바르게 구현되었습니다.
handlePageChange 함수를 통해 페이지 네이션 시 새로운 페이지로 상태를 업데이트합니다.
21-21: 로딩 상태 처리 로직이 적절합니다.
데이터 로딩 중에 ReviewListSkeleton 컴포넌트를 표시하여 사용자 경험을 향상시킵니다.
23-35: 에러 상태 처리
데이터 로드 실패 시 사용자에게 친숙한 에러 메시지를 표시하여 문제를 인지할 수 있게 합니다.
37-44: 리뷰가 없을 때의 처리
리뷰 목록이 비어 있을 경우 사용자에게 안내 메시지를 표시하여 상황을 명확히 전달합니다.
49-49: RatingDisplay 컴포넌트에 필요한 데이터가 정확히 전달되었습니다.
reviewRateInfo 프로퍼티에 데이터를 전달하여 평점 표시가 정확하게 이루어집니다.
51-56: CrewReviewList 컴포넌트에 필수 데이터와 핸들러가 올바르게 전달되었습니다.
reviews,totalPages,currentPage등 필요한 데이터가 정확히 전달됩니다.onPageChange핸들러를 통해 페이지 변경 시 상태 업데이트가 가능합니다.
| export function useGetCrewReviewsQuery(crewId: number, page: number) { | ||
| return useQuery({ | ||
| queryKey: ['crewReviews', crewId, page], | ||
| queryFn: () => getCrewReviews(crewId, page - 1), | ||
| }); | ||
| } |
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
쿼리 설정을 개선해주세요.
현재 구현에서 몇 가지 개선이 필요합니다:
page - 1변환에 대한 주석 설명이 필요합니다- 에러 처리와 재시도 로직이 누락되었습니다
- 캐시 전략이 정의되지 않았습니다
다음과 같이 개선해보세요:
export function useGetCrewReviewsQuery(crewId: number, page: number) {
return useQuery({
queryKey: ['crewReviews', crewId, page],
- queryFn: () => getCrewReviews(crewId, page - 1),
+ queryFn: () => getCrewReviews(crewId, page - 1), // API는 0-based 페이지네이션을 사용
+ staleTime: 5 * 60 * 1000, // 5분
+ retry: 1,
+ onError: (error) => {
+ console.error('크루 리뷰를 불러오는데 실패했습니다:', error);
+ },
});
}📝 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.
| export function useGetCrewReviewsQuery(crewId: number, page: number) { | |
| return useQuery({ | |
| queryKey: ['crewReviews', crewId, page], | |
| queryFn: () => getCrewReviews(crewId, page - 1), | |
| }); | |
| } | |
| export function useGetCrewReviewsQuery(crewId: number, page: number) { | |
| return useQuery({ | |
| queryKey: ['crewReviews', crewId, page], | |
| queryFn: () => getCrewReviews(crewId, page - 1), // API는 0-based 페이지네이션을 사용 | |
| staleTime: 5 * 60 * 1000, // 5분 | |
| retry: 1, | |
| onError: (error) => { | |
| console.error('크루 리뷰를 불러오는데 실패했습니다:', error); | |
| }, | |
| }); | |
| } |
| <div className="flex min-w-0 flex-1 flex-col"> | ||
| <div className="mb-[15px] space-y-1"> | ||
| <Skeleton className="h-6 w-1/2" /> | ||
| <Skeleton className="h-5 w-1/3" /> | ||
| </div> | ||
| <div className="flex items-center gap-2 pb-1"> | ||
| <Skeleton className="h-5 w-5 rounded-full" /> | ||
| <Skeleton className="h-5 w-12" /> | ||
| </div> | ||
| <div className="mt-auto"> | ||
| <Skeleton className="h-4 w-1/4" /> | ||
| </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
접근성 향상을 위한 aria-label 추가가 필요합니다.
스켈레톤 UI의 구조는 잘 잡혀있지만, 스크린 리더 사용자를 위해 로딩 상태임을 알려주는 aria-label을 추가하면 좋을 것 같습니다.
-<div className="flex min-w-0 flex-1 flex-col">
+<div className="flex min-w-0 flex-1 flex-col" aria-label="리뷰 콘텐츠 로딩 중">
<div className="mb-[15px] space-y-1">
- <Skeleton className="h-6 w-1/2" />
- <Skeleton className="h-5 w-1/3" />
+ <Skeleton className="h-6 w-1/2" aria-label="리뷰 제목 로딩 중" />
+ <Skeleton className="h-5 w-1/3" aria-label="리뷰 설명 로딩 중" />
</div>Committable suggestion skipped: line range outside the PR's diff.
| export interface ReviewList { | ||
| content: ReviewContent[]; | ||
| pageNumber: number; | ||
| pageSize: number; | ||
| totalElements: number; | ||
| totalPages: number; | ||
| } |
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.
💡 Codebase verification
ReviewContent 타입이 정의되어 있지 않은 문제 발견
review.d.ts 파일에서 ReviewList 인터페이스가 ReviewContent[] 타입을 참조하고 있지만, 이 타입이 프로젝트 어디에도 정의되어 있지 않습니다. CrewReview 타입을 사용해야 할 것으로 보입니다.
src/types/review.d.ts:ReviewList인터페이스의content속성 타입을CrewReview[]로 수정이 필요합니다.
🔗 Analysis chain
ReviewContent 타입 정의 확인 필요
ReviewContent 타입이 현재 파일에서 정의되어 있지 않습니다. 이 타입이 다른 파일에 정의되어 있는지 확인이 필요합니다.
Let me search for any type definitions or imports related to ReviewContent.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Search for ReviewContent type definition
# Search for ReviewContent type definition
ast-grep --pattern 'interface ReviewContent {
$$$
}'
# Search for ReviewContent type usage
rg "ReviewContent" --type ts
Length of output: 135
Script:
#!/bin/bash
# Check for any imports in review.d.ts
rg "import.*ReviewContent" src/types/review.d.ts
# Check the entire content of review.d.ts
cat src/types/review.d.ts
# Search for type/interface declarations that might contain ReviewContent
ast-grep --pattern 'type ReviewContent = $_'
Length of output: 1274
| id: 1, | ||
| gatheringName: '모임모임모임모임모임모밈미밈미미미미미미', | ||
| location: '모임모임모임모임모임모밈미밈미미미미미미', | ||
| dateTime: '2024-11-21T18:30:00.000Z', | ||
| currentCount: 8, | ||
| totalCount: 12, | ||
| imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.jpg', |
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
테스트 데이터 개선이 필요합니다.
현재 테스트 데이터에 다음과 같은 개선이 필요합니다:
gatheringName과location의 길이가 비현실적으로 깁니다. 실제 사용 사례를 반영한 적절한 길이의 데이터로 수정해주세요.dateTime형식이 올바른지 확인이 필요합니다.
다음과 같이 수정을 제안드립니다:
- gatheringName: '모임모임모임모임모임모밈미밈미미미미미미',
- location: '모임모임모임모임모임모밈미밈미미미미미미',
+ gatheringName: '주말 독서 모임',
+ location: '강남역 카페',📝 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.
| id: 1, | |
| gatheringName: '모임모임모임모임모임모밈미밈미미미미미미', | |
| location: '모임모임모임모임모임모밈미밈미미미미미미', | |
| dateTime: '2024-11-21T18:30:00.000Z', | |
| currentCount: 8, | |
| totalCount: 12, | |
| imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.jpg', | |
| id: 1, | |
| gatheringName: '주말 독서 모임', | |
| location: '강남역 카페', | |
| dateTime: '2024-11-21T18:30:00.000Z', | |
| currentCount: 8, | |
| totalCount: 12, | |
| imageUrl: 'https://i.pinimg.com/736x/5d/83/ce/5d83cec545201e5ca8f2477070e2eac9.jpg', |
| import { Skeleton } from '@mantine/core'; | ||
|
|
||
| interface ReviewCardSkeletonProps { | ||
| isMine?: boolean; | ||
| } | ||
|
|
||
| export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) { | ||
| return ( | ||
| <div className="w-full"> | ||
| {isMine && <Skeleton className="mb-3 h-6 w-48" />} | ||
| <div | ||
| role="presentation" | ||
| className={`flex h-full items-end gap-[15px] ${ | ||
| isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4' | ||
| } bg-white lg:gap-[40px]`} | ||
| > | ||
| <div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]"> | ||
| {isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />} | ||
|
|
||
| <div className="flex-start flex flex-col space-y-4"> | ||
| <Skeleton className="h-6 w-32" /> | ||
| <Skeleton className="h-4 w-full" /> | ||
| <Skeleton className="h-4 w-3/4" /> | ||
| </div> | ||
|
|
||
| <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}> | ||
| {!isMine && ( | ||
| <> | ||
| <Skeleton className="h-8 w-8 rounded-full" /> | ||
| <Skeleton className="h-4 w-24" /> | ||
| </> | ||
| )} | ||
| <Skeleton className="h-4 w-12" /> | ||
| </div> | ||
| </div> | ||
| </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에 대한 접근성 고려가 필요합니다. 스크린 리더 사용자를 위한 적절한 aria 속성을 추가하면 좋겠습니다.
다음과 같이 개선해보세요:
<div className="w-full">
+ <div aria-live="polite" aria-busy="true">
{isMine && <Skeleton className="mb-3 h-6 w-48" />}
<div
role="presentation"
+ aria-label="리뷰 카드 로딩 중"
className={...}>
...
</div>
+ </div>
</div>📝 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.
| import { Skeleton } from '@mantine/core'; | |
| interface ReviewCardSkeletonProps { | |
| isMine?: boolean; | |
| } | |
| export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) { | |
| return ( | |
| <div className="w-full"> | |
| {isMine && <Skeleton className="mb-3 h-6 w-48" />} | |
| <div | |
| role="presentation" | |
| className={`flex h-full items-end gap-[15px] ${ | |
| isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4' | |
| } bg-white lg:gap-[40px]`} | |
| > | |
| <div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]"> | |
| {isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />} | |
| <div className="flex-start flex flex-col space-y-4"> | |
| <Skeleton className="h-6 w-32" /> | |
| <Skeleton className="h-4 w-full" /> | |
| <Skeleton className="h-4 w-3/4" /> | |
| </div> | |
| <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}> | |
| {!isMine && ( | |
| <> | |
| <Skeleton className="h-8 w-8 rounded-full" /> | |
| <Skeleton className="h-4 w-24" /> | |
| </> | |
| )} | |
| <Skeleton className="h-4 w-12" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| import { Skeleton } from '@mantine/core'; | |
| interface ReviewCardSkeletonProps { | |
| isMine?: boolean; | |
| } | |
| export default function ReviewCardSkeleton({ isMine = false }: ReviewCardSkeletonProps) { | |
| return ( | |
| <div className="w-full"> | |
| <div aria-live="polite" aria-busy="true"> | |
| {isMine && <Skeleton className="mb-3 h-6 w-48" />} | |
| <div | |
| role="presentation" | |
| aria-label="리뷰 카드 로딩 중" | |
| className={`flex h-full items-end gap-[15px] ${ | |
| isMine ? 'rounded-[12px] p-6 shadow-bg' : 'border-b-[2px] border-[#F3F4F6] py-4' | |
| } bg-white lg:gap-[40px]`} | |
| > | |
| <div className="flex-start flex w-full flex-col items-start justify-between pr-[20px] lg:pr-[40px]"> | |
| {isMine && <Skeleton className="mb-6 h-5 w-full border-b-[2px] border-[#E5E7EB] pb-2" />} | |
| <div className="flex-start flex flex-col space-y-4"> | |
| <Skeleton className="h-6 w-32" /> | |
| <Skeleton className="h-4 w-full" /> | |
| <Skeleton className="h-4 w-3/4" /> | |
| </div> | |
| <div className={`flex w-fit flex-shrink-0 items-center text-xs ${isMine ? 'mt-4' : ''}`}> | |
| {!isMine && ( | |
| <> | |
| <Skeleton className="h-8 w-8 rounded-full" /> | |
| <Skeleton className="h-4 w-24" /> | |
| </> | |
| )} | |
| <Skeleton className="h-4 w-12" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
| classNames={{ | ||
| control: cn( | ||
| 'data-[active="true"]:text-blue-500 data-[active="true"]:font-bold', | ||
| 'border-none bg-transparent hover:bg-transparent', | ||
| ), | ||
| }} |
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
페이지네이션 컴포넌트의 접근성 개선이 필요합니다.
현재 스타일링은 깔끔하게 구현되었지만, 접근성 측면에서 개선이 필요합니다.
다음과 같은 접근성 개선사항을 추가해보세요:
classNames={{
control: cn(
'data-[active="true"]:text-blue-500 data-[active="true"]:font-bold',
- 'border-none bg-transparent hover:bg-transparent',
+ 'border-none bg-transparent hover:bg-transparent focus:ring-2 focus:ring-blue-500',
+ 'aria-label="페이지 이동" role="button"',
),
}}추가로 다음 속성들도 Pagination 컴포넌트에 추가하는 것이 좋습니다:
<Pagination
total={totalPages}
value={currentPage}
onChange={onPageChange}
+ aria-label="페이지 네비게이션"
+ getItemAriaLabel={(page) => `${page}페이지로 이동`}
classNames={{Committable suggestion skipped: line range outside the PR's diff.
| <Button | ||
| className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base" | ||
| onClick={() => setIsModalOpened(true)} | ||
| > | ||
| <span className="block md:hidden">리뷰 작성</span> | ||
| <span className="hidden md:block">리뷰 작성하기</span> | ||
| </Button> |
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
버튼 접근성 개선이 필요합니다.
버튼 구현이 깔끔하나, 스크린 리더 지원을 위한 추가적인 속성이 필요합니다:
<Button
className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base"
onClick={() => setIsModalOpened(true)}
+ aria-label="모임 리뷰 작성하기"
>
<span className="block md:hidden">리뷰 작성</span>
<span className="hidden md:block">리뷰 작성하기</span>
</Button>📝 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.
| <Button | |
| className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base" | |
| onClick={() => setIsModalOpened(true)} | |
| > | |
| <span className="block md:hidden">리뷰 작성</span> | |
| <span className="hidden md:block">리뷰 작성하기</span> | |
| </Button> | |
| <Button | |
| className="btn-filled absolute bottom-6 right-0 p-[4px_10px] text-sm md:p-[6px_14px] md:text-base" | |
| onClick={() => setIsModalOpened(true)} | |
| aria-label="모임 리뷰 작성하기" | |
| > | |
| <span className="block md:hidden">리뷰 작성</span> | |
| <span className="hidden md:block">리뷰 작성하기</span> | |
| </Button> |
| <div className="mb-[15px] space-y-1"> | ||
| <span className="block truncate text-xl font-semibold text-gray-800"> | ||
| {gatheringName} | ||
| </span> | ||
| <span className="block truncate text-base font-medium text-gray-700">{location}</span> | ||
| </div> | ||
| {/* 참여 인원 */} | ||
| <div className="flex items-center pb-1"> | ||
| <div className="relative inline-block h-[20px] w-[20px] flex-shrink-0"> | ||
| <Image | ||
| alt="참여 인원" | ||
| src={person} | ||
| fill | ||
| sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw" | ||
| className="h-full w-full object-contain" | ||
| /> | ||
| </div> | ||
| <div className="text-sm font-medium text-gray-700">{formatDate}</div> | ||
| <span className="mr-2 text-base font-medium text-gray-700"> | ||
| {currentCount}/{totalCount} | ||
| </span> | ||
| <Profiles profiles={profiles} /> | ||
| </div> | ||
| <Button | ||
| className="bg-blue-500 p-[6px_14px] text-base font-semibold text-white" | ||
| onClick={() => setIsModalOpened(true)} | ||
| > | ||
| 리뷰 작성하기 | ||
| </Button> | ||
| {/* 날짜 */} | ||
| <div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</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
접근성 개선을 위한 제안사항입니다.
텍스트 구조화가 잘 되어있으나, 스크린 리더 사용자를 위한 접근성 개선이 필요합니다:
-<span className="block truncate text-xl font-semibold text-gray-800">
+<h2 className="block truncate text-xl font-semibold text-gray-800">
{gatheringName}
-</span>
+</h2>
-<span className="block truncate text-base font-medium text-gray-700">{location}</span>
+<p className="block truncate text-base font-medium text-gray-700" aria-label="모임 장소">{location}</p>
-<span className="mr-2 text-base font-medium text-gray-700">
+<p className="mr-2 text-base font-medium text-gray-700" aria-label="참여 인원">
{currentCount}/{totalCount}
-</span>
+</p>📝 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.
| <div className="mb-[15px] space-y-1"> | |
| <span className="block truncate text-xl font-semibold text-gray-800"> | |
| {gatheringName} | |
| </span> | |
| <span className="block truncate text-base font-medium text-gray-700">{location}</span> | |
| </div> | |
| {/* 참여 인원 */} | |
| <div className="flex items-center pb-1"> | |
| <div className="relative inline-block h-[20px] w-[20px] flex-shrink-0"> | |
| <Image | |
| alt="참여 인원" | |
| src={person} | |
| fill | |
| sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw" | |
| className="h-full w-full object-contain" | |
| /> | |
| </div> | |
| <div className="text-sm font-medium text-gray-700">{formatDate}</div> | |
| <span className="mr-2 text-base font-medium text-gray-700"> | |
| {currentCount}/{totalCount} | |
| </span> | |
| <Profiles profiles={profiles} /> | |
| </div> | |
| <Button | |
| className="bg-blue-500 p-[6px_14px] text-base font-semibold text-white" | |
| onClick={() => setIsModalOpened(true)} | |
| > | |
| 리뷰 작성하기 | |
| </Button> | |
| {/* 날짜 */} | |
| <div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</div> | |
| <div className="mb-[15px] space-y-1"> | |
| <h2 className="block truncate text-xl font-semibold text-gray-800"> | |
| {gatheringName} | |
| </h2> | |
| <p className="block truncate text-base font-medium text-gray-700" aria-label="모임 장소">{location}</p> | |
| </div> | |
| {/* 참여 인원 */} | |
| <div className="flex items-center pb-1"> | |
| <div className="relative inline-block h-[20px] w-[20px] flex-shrink-0"> | |
| <Image | |
| alt="참여 인원" | |
| src={person} | |
| fill | |
| sizes="(max-width: 744px) 100vw, (max-width: 1200px) 50vw, 33vw" | |
| className="h-full w-full object-contain" | |
| /> | |
| </div> | |
| <p className="mr-2 text-base font-medium text-gray-700" aria-label="참여 인원"> | |
| {currentCount}/{totalCount} | |
| </p> | |
| <Profiles profiles={profiles} /> | |
| </div> | |
| {/* 날짜 */} | |
| <div className="mt-auto text-sm font-medium text-gray-700">{formatDate}</div> |
|
|
||
| export interface ReviewRateInfo { | ||
| totalReviewCount: number; | ||
| totalRate: number; |
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.
💡 Codebase verification
스토리북 파일의 인터페이스가 업데이트되지 않았습니다
rating-display.stories.tsx 파일에서 여전히 totalReviewCount를 사용하고 있으나, 실제 컴포넌트와 타입 정의(review.d.ts)에서는 totalRate로 변경되었습니다. 다음 사항들을 수정해야 합니다:
rating-display.stories.tsx파일의RatingDisplayStoryProps인터페이스에서totalReviewCount를totalRate로 변경argTypes정의에서totalReviewCount를totalRate로 변경- 모든 스토리 예제(
Default,HighRating,LowRating)의 props 이름을totalRate로 업데이트
🔗 Analysis chain
인터페이스 속성 변경에 따른 영향도 확인 필요
totalReviewCount에서 totalRate로의 속성 이름 변경이 적절해 보입니다. 다만, 이 인터페이스를 사용하는 다른 컴포넌트들에서도 일관되게 변경되었는지 확인이 필요합니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: 인터페이스 사용 현황 확인
# ReviewRateInfo 인터페이스를 사용하는 모든 파일 검색
rg -l "ReviewRateInfo"
# totalReviewCount나 totalRate 속성을 사용하는 코드 검색
rg "totalReviewCount|totalRate"
Length of output: 1653
Script:
#!/bin/bash
# Let's check the content of review.d.ts to understand the interface definition
cat src/types/review.d.ts
# Let's also check the stories file to understand the discrepancy
cat src/app/\(crew\)/crew/detail/\[id\]/_components/rating-display.stories.tsx
Length of output: 3065
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: 0
🧹 Outside diff range and nitpick comments (2)
src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx (2)
23-27: 템플릿 함수를 더 간결하게 작성할 수 있습니다.현재 구현이 명확하지만, 다음과 같이 더 간결하게 작성할 수 있습니다.
-const Template: StoryFn<RatingDisplayStoryProps> = function Template(args) { - const { totalRate, averageRate, ratingsData } = args; - const reviewRateInfo: ReviewRateInfo = { totalRate, averageRate, ratingsData }; - return <RatingDisplay reviewRateInfo={reviewRateInfo} />; +const Template: StoryFn<RatingDisplayStoryProps> = (args) => ( + <RatingDisplay reviewRateInfo={args} /> +);
Line range hint
31-68: 테스트 케이스 보완이 필요합니다.현재 기본, 높은 평점, 낮은 평점의 시나리오가 잘 구현되어 있습니다. 하지만 다음과 같은 엣지 케이스를 추가하면 좋을 것 같습니다:
- 리뷰가 없는 경우 (
totalRate: 0)- 모든 리뷰가 동일한 점수인 경우
- 최대 가능한 리뷰 수를 가진 경우
예시 구현:
export const NoReviews = Template.bind({}); NoReviews.args = { totalRate: 0, averageRate: 0, ratingsData: [ { score: 5, count: 0 }, { score: 4, count: 0 }, { score: 3, count: 0 }, { score: 2, count: 0 }, { score: 1, count: 0 }, ], };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (1)
src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx(3 hunks)
🔇 Additional comments (2)
src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx (2)
10-13: 속성 이름 변경이 적절히 이루어졌습니다.
totalReviewCount에서 totalRate로의 이름 변경이 일관되게 적용되었으며, 컨트롤 타입과 설명이 적절합니다.
16-20: 인터페이스 정의가 깔끔합니다.
RatingDisplayStoryProps 인터페이스의 타입 정의가 명확하고 일관성 있게 구성되어 있습니다.
yulrang
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.
Actionable comments posted: 2
🧹 Outside diff range and nitpick comments (6)
src/app/(crew)/my-page/_components/profile-card/container.tsx (3)
18-23: 타입 안전성 개선이 필요합니다
user객체의 타입 안전성을 높이기 위해 옵셔널 체이닝 이외의 추가적인 타입 가드를 고려해보세요.다음과 같이 타입 가드를 추가하는 것을 제안합니다:
- const profileImageUrl = user?.profileImageUrl || ''; + const profileImageUrl = user && 'profileImageUrl' in user ? user.profileImageUrl : '';
Line range hint
26-50: 파일 처리 로직 개선이 필요합니다파일 크기 제한과 파일 타입을 상수로 분리하고, 파일 타입 검증을 추가하면 좋을 것 같습니다.
다음과 같은 개선을 제안합니다:
+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_FILE_TYPES = ['.png', '.jpg', '.jpeg']; + const handleEdit = () => { const input = document.createElement('input'); input.type = 'file'; - input.accept = '.png,.jpg,.jpeg'; + input.accept = ALLOWED_FILE_TYPES.join(','); input.onchange = async (event) => { const file = (event.target as HTMLInputElement)?.files?.[0]; if (file) { - if (file.size > 5 * 1024 * 1024) { + if (file.size > MAX_FILE_SIZE) { toast.error('5MB 이하의 파일만 업로드 가능합니다.'); return; } + if (!ALLOWED_FILE_TYPES.some(type => file.name.toLowerCase().endsWith(type))) { + toast.error('지원하지 않는 파일 형식입니다.'); + return; + }
Line range hint
52-61: 삭제 중 로딩 상태 처리가 필요합니다프로필 이미지 삭제 중에 사용자에게 진행 상태를 표시하면 좋을 것 같습니다.
다음과 같은 개선을 제안합니다:
+ const [isDeleting, setIsDeleting] = useState(false); + const handleDeleteProfile = async () => { + if (isDeleting) return; + setIsDeleting(true); try { await resetUserProfileImage(); await refetchUser(); toast.success('프로필 이미지가 초기화되었습니다.'); } catch (error) { toast.error('프로필 이미지 초기화에 실패했습니다.'); + } finally { + setIsDeleting(false); } };src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx (1)
38-42: 에러 처리 로직 개선 필요현재 구현은 기본적인 기능은 잘 동작하지만, 다음 사항들을 고려해보시기 바랍니다:
- 일반 에러 메시지가 너무 포괄적입니다. 사용자에게 더 구체적인 안내가 필요할 수 있습니다.
window.location.pathname은 SSR 환경에서 문제가 될 수 있습니다.다음과 같이 개선해보세요:
if (error instanceof ApiError && error.status === 401) { - const redirectUrl = `/login?redirect=${encodeURIComponent(window.location.pathname)}`; + const redirectUrl = `/login?redirect=${encodeURIComponent(typeof window !== 'undefined' ? window.location.pathname : '')}`; router.push(redirectUrl); } else { - showToast('참여 중 에러가 발생했습니다.', 'error'); + showToast('모임 참여 처리 중 문제가 발생했습니다. 잠시 후 다시 시도해주세요.', 'error'); }src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx (1)
25-30: 에러 처리 로직 개선이 필요합니다에러 처리가 개선되었지만, 다음과 같은 사항들을 고려해보시면 좋을 것 같습니다:
- 에러 처리 로직이
handleUnlike와 중복되어 있습니다- 401 에러 발생 시 모달 표시 여부를 재검토해볼 필요가 있습니다
다음과 같이 공통 에러 처리 함수를 만들어 사용하는 것을 제안드립니다:
+const handleApiError = (error: unknown, action: string) => { + if (error instanceof ApiError && error.status === 401) { + setShowLoginModal(true); + return; + } + toast.error(`${action}에 실패했습니다.`); +}; const handleLike = async (gatheringId: number) => { try { await addLike(gatheringId); toast.success('찜하기가 완료되었습니다!'); } catch (error) { - if (apiError instanceof ApiError && apiError.status === 401) { - toast.error('로그인이 필요합니다.'); - } else { - toast.error('찜하기에 실패했습니다.'); - } + handleApiError(error, '찜하기'); } };src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx (1)
Line range hint
1-180: 컴포넌트 구조 개선을 위한 제안전반적으로 잘 구현되어 있으나, 다음과 같은 개선사항을 고려해보시면 좋겠습니다:
- 크루 관련 상태 관리 로직(isCaptain, isMember, isJoining, isConfirmed)을 별도의 커스텀 훅으로 분리하여 재사용성과 테스트 용이성을 높일 수 있습니다.
- API 에러 처리 로직을 통일된 방식으로 구현하면 좋겠습니다.
다음과 같은 리팩토링을 제안드립니다:
// useCrewState.ts export function useCrewState(crewData: CrewDetailType, currentUserId?: number) { const [isCaptain, setIsCaptain] = useState(false); const [isMember, setIsMember] = useState(false); const [isConfirmed, setIsConfirmed] = useState(false); useEffect(() => { if (crewData) { setIsConfirmed(crewData.participantCount === crewData.totalCount); if (currentUserId) { const captain = crewData.crewMembers.find((member) => member.captain); const memberExists = crewData.crewMembers.some( (member) => member.id === currentUserId ); setIsCaptain(captain?.id === currentUserId); setIsMember(memberExists); } } }, [currentUserId, crewData]); return { isCaptain, isMember, isConfirmed }; }// useApiErrorHandler.ts export function useApiErrorHandler(router: NextRouter) { return useCallback((error: unknown, defaultMessage: string) => { if (error instanceof ApiError) { if (error.status === 401) { router.push( `/login?redirect=${encodeURIComponent(window.location.pathname)}` ); return; } toast.error(error.message); } else { toast.error(defaultMessage); } }, [router]); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (4)
src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx(1 hunks)src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx(2 hunks)src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx(1 hunks)src/app/(crew)/my-page/_components/profile-card/container.tsx(2 hunks)
🔇 Additional comments (4)
src/app/(crew)/crew/detail/[id]/_components/gathering-detail-modal/container.tsx (2)
4-4: 적절한 라우터 임포트 추가
Next.js의 라우팅 기능을 위한 useRouter 임포트가 올바르게 추가되었습니다.
30-30: 라우터 훅 초기화 확인
컴포넌트 레벨에서 useRouter 훅이 올바르게 초기화되었습니다.
src/app/(crew)/crew/detail/[id]/_components/gathering-list-section.tsx (2)
38-43: 중복된 에러 처리 로직 발견
handleLike와 동일한 에러 처리 패턴이 반복되고 있습니다. 위에서 제안된 handleApiError 함수를 활용하여 다음과 같이 개선할 수 있습니다:
const handleUnlike = async (gatheringId: number) => {
try {
await removeLike(gatheringId);
toast.success('찜하기 해제가 완료되었습니다!');
} catch (error) {
- if (apiError instanceof ApiError && apiError.status === 401) {
- toast.error('로그인이 필요합니다.');
- } else {
- toast.error('찜하기 해제에 실패했습니다.');
- }
+ handleApiError(error, '찜하기 해제');
}
};Line range hint 1-99: 전반적인 구현이 잘 되어있습니다! 👍
- 로딩, 에러, 빈 상태 처리가 잘 구현되어 있습니다
- TypeScript 타입 정의가 명확합니다
- 컴포넌트 구조가 깔끔하게 분리되어 있습니다
위에서 제안된 리팩토링 외에는 특별한 문제점이 보이지 않습니다.
| const tempUrl = URL.createObjectURL(file); | ||
| setProfileImageUrl(tempUrl); | ||
|
|
||
| await refetchUser(); | ||
| // 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌 | ||
| toast.success('프로필 이미지가 업데이트되었습니다.'); | ||
| await refetchUser(); |
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 정리가 필요합니다
URL.createObjectURL로 생성된 임시 URL을 정리하지 않으면 메모리 누수가 발생할 수 있습니다.
다음과 같이 임시 URL을 정리하는 코드를 추가해주세요:
const tempUrl = URL.createObjectURL(file);
+ // React Query가 refetch를 완료한 후 임시 URL 정리
await refetchUser();
+ URL.revokeObjectURL(tempUrl);📝 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.
| const tempUrl = URL.createObjectURL(file); | |
| setProfileImageUrl(tempUrl); | |
| await refetchUser(); | |
| // 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌 | |
| toast.success('프로필 이미지가 업데이트되었습니다.'); | |
| await refetchUser(); | |
| const tempUrl = URL.createObjectURL(file); | |
| // 임시 URL을 React Query가 refetch로 갱신될 때까지 보여줌 | |
| toast.success('프로필 이미지가 업데이트되었습니다.'); | |
| await refetchUser(); | |
| URL.revokeObjectURL(tempUrl); |
| if (joinError.status === 401) { | ||
| router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`); | ||
| } else { | ||
| toast.error(joinError.message); | ||
| } |
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.
💡 Codebase verification
401 에러 처리가 일관되지 않은 부분이 발견되었습니다.
handleLeaveCrew와 handleConfirmCancel 함수에서 401 에러에 대한 처리가 누락되어 있습니다. 다음 위치에서 수정이 필요합니다:
handleLeaveCrew함수의 에러 처리 부분에 401 상태 체크 및 로그인 페이지 리다이렉션 추가 필요handleConfirmCancel함수의 에러 처리 부분에 401 상태 체크 및 로그인 페이지 리다이렉션 추가 필요
🔗 Analysis chain
401 에러 처리 로직이 적절하게 구현되었습니다.
인증되지 않은 사용자의 크루 참여 시도를 적절하게 처리하고 있습니다. 로그인 후 원래 페이지로 돌아올 수 있도록 redirect 파라미터를 포함한 것이 좋은 UX 구현입니다.
다만, 다음 사항을 확인해 주시면 좋겠습니다:
- 다른 API 호출 함수들(
handleLeaveCrew,handleConfirmCancel)에도 동일한 401 에러 처리가 필요한지 검토
아래 스크립트로 다른 API 호출의 401 에러 처리 여부를 확인해보겠습니다:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if other API calls handle 401 errors consistently
# Search for API call implementations
ast-grep --pattern 'catch (${_}Error) {
if (${_}Error instanceof ApiError) {
if (${_}Error.status === 401) {
$$$
}
}
}'
Length of output: 139
Script:
#!/bin/bash
# Search for handleLeaveCrew and handleConfirmCancel functions
ast-grep --pattern 'async function handleLeaveCrew() {
$$$
}'
ast-grep --pattern 'async function handleConfirmCancel() {
$$$
}'
# Also search for any error handling patterns in these functions
rg "handleLeaveCrew|handleConfirmCancel" -A 10 -B 10
Length of output: 14725
🔖 Issue Ticket
#159
✍️ Description
크루 리뷰 리스트 api 연결
자잘한 ui수정이 있습니다..
✅ Checklist
PR
Test
Summary by CodeRabbit
getCrewReviews추가useGetCrewReviewsQuery추가로 크루 리뷰를 효율적으로 가져옴ReviewListSkeleton컴포넌트 추가ReviewCardSkeleton컴포넌트 추가로 리뷰 카드의 로딩 UI 제공location속성이 추가된ReviewableGatheringCard컴포넌트 개선id와refetch를 선택적으로 처리하여 오류 방지CreateGathering및ReviewableGatheringCard컴포넌트의 레이아웃 및 스타일 조정GatheringListSection의 좋아요/싫어요 기능에 대한 성공 및 오류 메시지 개선ReviewListSkeleton에 대한 스토리북 설정 추가