-
Notifications
You must be signed in to change notification settings - Fork 1
release/v1.3.3 #51
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
base: main
Are you sure you want to change the base?
release/v1.3.3 #51
Conversation
리뷰 삭제 보상형 광고 시청 기능 구현
- 로컬 상태와 서버 응답 자동 동기화 - 호출 에러 시 롤백 함수 사용
- 유저 role이 아닌, 작성된 리뷰 type에 따라 라우팅 처리 - 예비교사일 경우 '리뷰쓰기' 버튼 비활성화
- 질문/답변 4항목 추가 - 임의의 디자인 적용
Walkthrough리뷰 무한 스크롤과 페이징 도입, 리뷰 삭제 시 보상 광고 흐름 통합, 낙관적 좋아요 업데이트 훅 추가, FAQ 섹션 및 여러 UI 레이블·툴팁 개선 등이 포함된 기능 확장입니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ReviewCard
participant useOptimisticUpdate
participant handleLike (mutateAsync)
participant API
participant Server
User->>ReviewCard: 좋아요 클릭
ReviewCard->>useOptimisticUpdate: setValue (낙관적 상태 적용)
useOptimisticUpdate-->>ReviewCard: 즉시 반영된 상태
ReviewCard->>handleLike: mutateAsync 호출
handleLike->>API: 좋아요/취소 요청
API->>Server: HTTP 요청
alt 성공
Server-->>API: 200 OK
API-->>handleLike: 성공
handleLike->>ReviewCard: 쿼리 무효화
else 실패
Server-->>API: 에러
API-->>handleLike: 에러
handleLike->>useOptimisticUpdate: rollback 호출
useOptimisticUpdate-->>ReviewCard: 이전 상태 복구
ReviewCard->>User: 실패 토스트 표시
end
sequenceDiagram
participant User
participant ReviewPanel
participant useReviewPage
participant useInfiniteWorkReviews
participant API
participant Server
User->>ReviewPanel: 페이지 진입
ReviewPanel->>useReviewPage: 초기화
useReviewPage->>useInfiniteWorkReviews: 첫 페이지 요청
useInfiniteWorkReviews->>API: getWorkReviews(page=1)
API->>Server: 페이지1 데이터 요청
Server-->>API: 페이지1 응답 (totalPages 등)
API-->>useInfiniteWorkReviews: 데이터 반환
useInfiniteWorkReviews-->>useReviewPage: 페이지 데이터 제공
useReviewPage-->>ReviewPanel: reviews 렌더링 데이터
User->>ReviewPanel: 스크롤로 더보기 요구
ReviewPanel->>useReviewPage: fetchNextPage 호출
useReviewPage->>useInfiniteWorkReviews: 다음 페이지 요청
useInfiniteWorkReviews->>API: getWorkReviews(page=2)
API->>Server: 페이지2 데이터 요청
Server-->>API: 페이지2 응답
API-->>useInfiniteWorkReviews: 누적 데이터 반환
useInfiniteWorkReviews-->>ReviewPanel: 추가된 리뷰 표시
sequenceDiagram
participant User
participant UI (삭제 트리거)
participant useDeleteWorkReview
participant useRewardAd
participant FlutterWebView
participant API
participant Server
User->>UI: 삭제 요청 (skipAd=false)
UI->>useDeleteWorkReview: mutate({ workReviewId, skipAd:false })
useDeleteWorkReview->>useRewardAd: showRewardAd()
useRewardAd->>FlutterWebView: REQUEST_REWARD_AD 메시지
FlutterWebView->>User: 광고 노출
alt 광고 시청 완료
FlutterWebView-->>useRewardAd: { status: 'success', rewarded: true/false }
useRewardAd-->>useDeleteWorkReview: 결과 반환
useDeleteWorkReview->>API: deleteWorkReview 요청
else 광고 취소
FlutterWebView-->>useRewardAd: { status: 'cancelled' }
useRewardAd-->>useDeleteWorkReview: 취소 반환
useDeleteWorkReview->>useDeleteWorkReview: 에러 throw (삭제 중단)
end
API->>Server: 삭제 요청
Server-->>API: 200 OK
API-->>useDeleteWorkReview: 성공
useDeleteWorkReview->>UI: 쿼리 무효화 + 완료 토스트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 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.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @src/pages/user/inquiry/index.tsx:
- Around line 44-49: The MenuItem for "1:1 문의하기" is using the wrong icon
constant; change the iconPath prop on the MenuItem (currently set to
SVG_PATHS.USER_MENU.LOGOUT) to SVG_PATHS.USER_MENU.INQUIRY so the displayed icon
matches the label and iconAlt; update the iconPath value in the <MenuItem ... />
invocation accordingly.
In @src/shared/utils/webViewCommunication.ts:
- Around line 214-245: In requestRewardAd the setTimeout created for
timeoutPromise isn't cleared when adPromise wins, leaking timers; capture the
timer id returned by setTimeout, then after awaiting Promise.race (using the
existing adPromise/timeoutPromise/result flow) call clearTimeout(timerId) before
any return path so the timer is always cleared (including the legacy-response
branch and normal return). Ensure the timer variable is defined in the outer
scope of the race so it can be cleared regardless of which promise resolved.
In @src/widgets/review-list/ui/ReviewCardList.tsx:
- Around line 93-96: The optimistic local like state is initialized to false
causing a UI flash; change the initializer passed into useOptimisticUpdate from
the literal false to the actual prop value isLiked (or a safe boolean fallback
like !!isLiked) so localIsLiked starts in sync with the real state; update the
call to useOptimisticUpdate(...) to use isLiked as the initial value and keep
the rest (setLocalIsLiked, rollbackIsLiked) unchanged.
- Around line 113-123: handleOptimisticLike lacks an isPending guard so rapid
clicks can trigger duplicate mutations; add an early return if isPending is
true, then set a pending flag (isPending = true) before calling handleLike and
clear it (isPending = false) in both the try and catch paths; keep the existing
optimistic updates (setLocalIsLiked, setLikeCount) and existing rollback calls
(rollbackIsLiked, rollbackLikeCount) but ensure isPending prevents re-entry
while the mutation is in-flight.
In @src/widgets/review-panel/lib/useReviewPage.tsx:
- Around line 30-38: Both hooks useSuspenseInfiniteQuery under the hood so
calling useInfiniteWorkReviews and useInfiniteInternshipReviews unconditionally
triggers both API requests; only the selected review type should mount its hook.
Fix by ensuring only the hook for the active type is invoked: either (A) split
the UI into two components (e.g., WorkReviewsPanel and InternshipReviewsPanel)
and move useInfiniteWorkReviews and useInfiniteInternshipReviews into those
components so only the mounted component runs its hook, or (B) switch the hooks
to useInfiniteQuery with an enabled flag and use the existing conditional
selection (keep function names
useInfiniteWorkReviews/useInfiniteInternshipReviews but implement them to use
useInfiniteQuery + enabled based on the passed type), or (C) wrap each hook call
behind a conditional render wrapper that only calls useInfiniteWorkReviews when
type === REVIEW_TYPES.WORK and useInfiniteInternshipReviews when type ===
REVIEW_TYPES.INTERNSHIP so the non-selected hook is never invoked.
🧹 Nitpick comments (5)
src/entities/community/hooks/useToggleLike.tsx (1)
11-17: 에러 메시지 개선이 좋습니다!타이틀과 설명을 추가하여 사용자에게 더 명확한 피드백을 제공합니다.
🔍 선택적 개선: 디버깅을 위한 에러 로깅
에러 객체를 활용하여 디버깅 정보를 로깅하면 문제 해결에 도움이 될 수 있습니다:
- onError: () => { + onError: (error) => { + console.error('좋아요 토글 실패:', error); toast({ title: "좋아요 실패", description: "잠시 후 다시 시도해주세요.", variant: "destructive", }); },src/shared/constants/inquiry.ts (1)
30-55: FAQ 콘텐츠가 적절하게 구성되었습니다.FAQ 항목들이 사용자의 주요 질문들을 잘 다루고 있습니다. 다만 2번 답변이 매우 길어서(약 432자) 모바일 화면에서 가독성에 영향을 줄 수 있습니다. 향후 FAQ가 늘어날 경우 CMS나 백엔드 API로 관리하는 것을 고려해볼 수 있습니다.
src/widgets/user-dashboard/faq-list/ui/FaqItem.tsx (1)
18-38: 접근성 개선:aria-expanded속성 추가 권장스크린 리더 사용자를 위해 버튼에
aria-expanded속성을 추가하면 현재 확장 상태를 명확히 전달할 수 있습니다.InquiryExpandButton컴포넌트에서도 유사한 패턴을 사용하고 있습니다.♻️ 접근성 개선 제안
<button onClick={onToggle} + aria-expanded={expanded} className="relative flex items-start gap-3 rounded-lg border border-primary-light02 bg-white p-4 text-left duration-200 active:brightness-95" >src/entities/review/hooks/useDeleteInternshipReview.tsx (1)
24-41: 광고 처리 로직이useDeleteWorkReview와 중복됩니다.광고 표시 및 결과 처리 로직이 두 훅에서 동일하게 반복됩니다. 재사용 가능한 헬퍼 함수로 추출하면 유지보수성이 향상됩니다.
♻️ 공통 로직 추출 예시
// 예: src/shared/utils/adGatedMutation.ts export async function executeWithAdGate( showRewardAd: () => Promise<RewardAdResult>, skipAd: boolean ): Promise<void> { if (!skipAd) { const adResult = await showRewardAd(); if (adResult.status === "cancelled") { throw new Error("광고를 끝까지 시청해야 리뷰를 삭제할 수 있습니다."); } } }src/entities/review/hooks/useGetReview.tsx (1)
213-247: 구현은 올바르지만, 중복 코드 리팩토링을 고려해보세요.
useInfiniteWorkReviews와 거의 동일한 로직입니다. 향후 유지보수를 위해 제네릭 훅으로 통합하는 것을 고려해볼 수 있습니다.♻️ 제네릭 훅으로 통합 제안 (선택사항)
const useInfiniteReviews = <T>( queryKeyPrefix: string, kindergartenId: string, fetchFn: (id: number, params: ReviewQueryParams) => Promise<PaginatedResponse<T>>, sortType?: SortType, pageSize: number = 10 ) => { const numericId = safeParseId(kindergartenId); return useSuspenseInfiniteQuery({ queryKey: [queryKeyPrefix, kindergartenId, sortType, "infinite", pageSize], queryFn: ({ pageParam = 0 }) => { if (!numericId) { return Promise.resolve({ content: [], totalPages: 0 }); } return fetchFn(numericId, { page: pageParam as number, size: pageSize, sortType }); }, getNextPageParam: (lastPage, allPages) => { if (!lastPage?.content || lastPage.content.length === 0) return undefined; const currentPage = allPages.length - 1; return currentPage + 1 >= lastPage.totalPages ? undefined : currentPage + 1; }, initialPageParam: 0, ...DYNAMIC_CACHE_CONFIG, }); }; // Usage export const useInfiniteWorkReviews = (kindergartenId: string, sortType?: SortType, pageSize?: number) => useInfiniteReviews("workReviews", kindergartenId, getWorkReviews, sortType, pageSize);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
src/entities/community/hooks/useToggleLike.tsxsrc/entities/review/api.tssrc/entities/review/hooks/useDeleteInternshipReview.tsxsrc/entities/review/hooks/useDeleteWorkReview.tsxsrc/entities/review/hooks/useGetReview.tsxsrc/entities/review/hooks/useReviewLike.tsxsrc/features/form/ui/fields/ScoredCommentField.tsxsrc/pages/user/inquiry/index.tsxsrc/shared/constants/inquiry.tssrc/shared/hooks/useFlutterCommunication.tssrc/shared/hooks/useOptimisticUpdate.tssrc/shared/utils/webViewCommunication.tssrc/widgets/review-editor/ui/WorkReviewForm.tsxsrc/widgets/review-list/ui/ReviewCardList.tsxsrc/widgets/review-panel/index.tsxsrc/widgets/review-panel/lib/getFieldConfigsByType.tssrc/widgets/review-panel/lib/useReviewPage.tsxsrc/widgets/user-dashboard/faq-list/index.tsxsrc/widgets/user-dashboard/faq-list/ui/FaqItem.tsxsrc/widgets/user-dashboard/my-post-list/ui/MyPostItem.tsx
🧰 Additional context used
🧬 Code graph analysis (13)
src/widgets/user-dashboard/faq-list/index.tsx (2)
src/shared/constants/inquiry.ts (1)
FAQ_LIST(30-55)src/widgets/user-dashboard/faq-list/ui/FaqItem.tsx (1)
FaqItem(15-61)
src/widgets/review-panel/lib/getFieldConfigsByType.ts (1)
src/widgets/review-panel/lib/config.d.ts (1)
ReviewFieldConfig(1-4)
src/pages/user/inquiry/index.tsx (5)
src/entities/auth/model.ts (1)
userAtom(5-5)src/widgets/user-dashboard/ui/MenuItem.tsx (1)
MenuItem(13-55)src/shared/constants/assets-path.ts (1)
SVG_PATHS(101-261)src/widgets/user-dashboard/faq-list/index.tsx (1)
FaqList(7-26)src/shared/constants/url-path.ts (1)
URL_PATHS(1-59)
src/shared/hooks/useFlutterCommunication.ts (1)
src/pages/index.tsx (1)
getToken(25-37)
src/entities/review/hooks/useDeleteInternshipReview.tsx (3)
src/shared/hooks/useFlutterCommunication.ts (1)
useRewardAd(280-328)src/entities/review/DTO.d.ts (1)
LikeResponse(82-85)src/entities/review/api.ts (1)
deleteInternshipReview(194-202)
src/widgets/user-dashboard/faq-list/ui/FaqItem.tsx (4)
src/shared/constants/assets-path.ts (1)
SVG_PATHS(101-261)src/widgets/user-dashboard/inquiry-list/ui/InquiryExpandButton.tsx (2)
InquiryExpandButton(11-34)InquiryExpandButtonProps(5-9)src/widgets/review-list/ui/ReviewSummary.tsx (2)
ReviewSummary(16-80)ReviewSummaryProps(5-14)src/widgets/home-dashboard/ui/RecentReviewPreview.tsx (2)
review(30-60)RecentReviewPreview(14-66)
src/entities/review/api.ts (3)
src/entities/review/DTO.d.ts (4)
ReviewQueryParams(73-77)PaginatedReviewResponse(68-71)WorkReview(23-36)InternshipReview(38-46)src/shared/config/api.ts (1)
API_PATHS(2-98)src/entities/review/hooks/useGetAllReviews.tsx (2)
params(11-34)getAllWorkReviews(19-24)
src/widgets/review-editor/ui/WorkReviewForm.tsx (2)
src/widgets/review-editor/ui/LearningReviewForm.tsx (2)
LearningReviewFormValues(9-17)LearningReviewForm(24-80)src/entities/review/DTO.d.ts (1)
WorkReview(23-36)
src/entities/review/hooks/useReviewLike.tsx (1)
src/widgets/review-list/ui/ReviewActions.tsx (2)
ReviewActions(13-34)ReviewActionsProps(5-11)
src/widgets/review-panel/lib/useReviewPage.tsx (4)
src/entities/review/DTO.d.ts (2)
InternshipReview(38-46)WorkReview(23-36)src/widgets/review-panel/lib/getFieldConfigsByType.ts (1)
getFieldConfigsByType(47-59)src/entities/auth/model.ts (1)
userAtom(5-5)src/shared/constants/review.ts (1)
REVIEW_TYPES(4-7)
src/entities/review/hooks/useDeleteWorkReview.tsx (3)
src/shared/hooks/useFlutterCommunication.ts (1)
useRewardAd(280-328)src/entities/review/DTO.d.ts (1)
LikeResponse(82-85)src/entities/review/api.ts (1)
deleteWorkReview(184-192)
src/widgets/review-panel/index.tsx (5)
src/shared/constants/review.ts (1)
REVIEW_TYPES(4-7)src/widgets/review-panel/lib/useReviewPage.tsx (1)
useReviewPage(21-143)src/shared/ui/loading/loading-spinner/index.tsx (1)
LoadingSpinner(8-43)src/widgets/all-review-list/index.tsx (2)
AllReviewList(29-113)fetchNextPage(95-95)src/pages/review/index.tsx (1)
AllReviewPage(10-31)
src/widgets/review-list/ui/ReviewCardList.tsx (5)
src/entities/review/hooks/useReviewLike.tsx (1)
useReviewLike(6-43)src/shared/hooks/useOptimisticUpdate.ts (1)
useOptimisticUpdate(19-48)src/widgets/review-list/ui/ReviewContent.tsx (1)
ReviewContent(15-119)src/widgets/review-list/ui/ReviewActions.tsx (2)
ReviewActions(13-34)ReviewActionsProps(5-11)src/widgets/review-list/ui/ReviewSummary.tsx (2)
ReviewSummary(16-80)ReviewSummaryProps(5-14)
🔇 Additional comments (23)
src/shared/hooks/useOptimisticUpdate.ts (1)
40-45: 서버 동기화 후 롤백 동작을 확인해주세요.
serverValue가 변경되면previousValueRef도 함께 업데이트됩니다(Line 42). 이는 서버 동기화 이후rollback을 호출하면 낙관적 업데이트 이전 값이 아닌 서버 값으로 복원된다는 의미입니다.일반적인 낙관적 업데이트 패턴(업데이트 → 실패 시 롤백 → 성공 시 서버 동기화)에서는 문제가 없지만, 서버 동기화 후 롤백이 호출될 수 있는 엣지 케이스가 있다면 예상과 다른 동작이 발생할 수 있습니다. 이 동작이 의도된 것인지 확인해주세요.
src/features/form/ui/fields/ScoredCommentField.tsx (1)
10-10: LGTM!툴팁 기능 추가가 깔끔하게 구현되었습니다. 선택적 prop 패턴을 따르고 있으며, 조건부 렌더링이 올바르게 처리되었습니다.
Also applies to: 21-21, 31-31, 44-48
src/entities/review/api.ts (1)
18-54: LGTM!페이지네이션 지원을 위한 리팩토링이 올바르게 구현되었습니다.
ReviewQueryParams객체를 통한 파라미터 전달, 적절한 기본값 설정, 그리고URLSearchParams를 사용한 쿼리 문자열 구성이getAllWorkReviews및getAllInternshipReviews의 패턴과 일관성 있게 적용되었습니다.src/pages/user/inquiry/index.tsx (1)
2-2: LGTM!FAQ 섹션이 깔끔하게 추가되었습니다. 기본적으로 확장된 상태로 표시되며, 토글 기능이 올바르게 구현되었습니다.
Also applies to: 8-8, 13-13, 35-43
src/widgets/user-dashboard/my-post-list/ui/MyPostItem.tsx (1)
111-114: 광고 통합이 올바르게 구현되어 있습니다.
useDeleteWorkReview와useDeleteInternshipReview훅에서 보상 광고 플로우가 properly 구현되어 있음을 확인했습니다. 두 훅 모두skipAd옵션이 없는 경우showRewardAd()를 호출하고, 사용자가 광고를 중단하면 삭제를 막으며, 광고 시청 완료 후 리뷰 삭제를 진행합니다. UI 텍스트가 실제 구현과 일치합니다.src/widgets/review-panel/lib/getFieldConfigsByType.ts (1)
13-13: 라벨 변경이 사용자 이해도를 개선합니다."고객"에서 "학급 운영"으로 라벨을 변경하여 유치원 리뷰 맥락에서 필드의 의미가 더 명확해졌습니다. WorkReviewForm.tsx의 툴팁 설명과도 잘 연계되어 있습니다.
Also applies to: 21-21
src/widgets/review-editor/ui/WorkReviewForm.tsx (1)
16-16: 라벨 및 툴팁 추가로 사용자 경험이 개선되었습니다."학급 운영" 필드에 대한 상세한 설명을 툴팁으로 제공하여 사용자가 어떤 내용을 작성해야 하는지 명확하게 안내하고 있습니다. 라벨 변경도 getFieldConfigsByType.ts와 일관성 있게 적용되었습니다.
Also applies to: 102-103
src/widgets/user-dashboard/faq-list/index.tsx (1)
7-26: LGTM! 아코디언 동작이 올바르게 구현되었습니다.한 번에 하나의 FAQ만 펼쳐지도록 하는 로직이 정확하고, 상태 관리도 적절합니다. FaqItem 컴포넌트에 전달되는 props도 타입과 잘 맞습니다.
src/entities/review/hooks/useReviewLike.tsx (1)
11-11: Promise는 이미 올바르게 처리되고 있습니다.
handleLike함수가 Promise를 반환하지만,ReviewCardList.tsx에서handleOptimisticLike래퍼 함수로 감싸져 있으며, 이 함수 내부에서await handleLike()로 Promise를 처리하고 try-catch로 에러를 처리합니다. 따라서 처리되지 않은 Promise rejection이 발생할 가능성은 없습니다.다만 타입 안전성 관점에서,
handleOptimisticLike는 async 함수이지만ReviewActions의onLike: () => void시그니처와 맞지 않습니다. 현재 구현은 함수 내부에서 에러 처리를 완료하므로 실제 문제는 없지만, 타입 정확성을 위해 인터페이스를 명시적으로onLike: () => Promise<void>로 변경하는 것을 검토해보세요.src/widgets/user-dashboard/faq-list/ui/FaqItem.tsx (1)
1-61: 전반적인 구현이 적절합니다.FAQ 아이템 컴포넌트의 구조가 깔끔하고,
grid-rows기반의 확장/축소 애니메이션이 잘 구현되어 있습니다. 기존 코드베이스의 스타일링 패턴을 따르고 있습니다.src/shared/utils/webViewCommunication.ts (1)
246-254: 에러 시status: "success"반환은 의도된 설계로 보입니다.구버전 앱 대응 및 사용자 경험을 위해 에러 발생 시에도 삭제를 허용하는 graceful degradation 패턴이 적용되어 있습니다. 주석으로 의도가 명확히 설명되어 있어 좋습니다.
src/widgets/review-panel/index.tsx (1)
67-74: 무한 스크롤 구현이 적절합니다.
AllReviewList컴포넌트와 동일한 패턴으로 무한 스크롤이 구현되어 있어 코드베이스 일관성이 유지됩니다.AutoFetchSentinel과LoadingSpinner의 조합이 올바르게 사용되었습니다.src/entities/review/hooks/useDeleteInternshipReview.tsx (1)
42-60: 삭제 성공 후 처리 로직이 적절합니다.쿼리 무효화, 사용자 정보 갱신, 토스트 알림이 올바르게 구현되어 있습니다.
src/widgets/review-panel/lib/useReviewPage.tsx (1)
52-106: 평균 점수 계산 로직이 올바르게 구현되어 있습니다.리뷰 타입에 따른 점수 계산과
reviews.length가 0일 때의 fallback 처리(|| 0)가 적절합니다.src/shared/hooks/useFlutterCommunication.ts (1)
280-328:useRewardAd훅이 기존 패턴을 잘 따르고 있습니다.
useKakaoShare,useRequestFcmToken등 기존 훅들과 동일한 구조를 유지하며, 브라우저 환경과 에러 상황에 대한 graceful degradation이 적절히 구현되어 있습니다. Docblock 예시도 명확합니다.src/entities/review/hooks/useDeleteWorkReview.tsx (2)
24-41: 광고 게이팅 로직이 올바르게 구현되어 있습니다.사용자가 광고를 취소한 경우에만 삭제를 차단하고, 광고 로드 실패 등 사용자 책임이 아닌 상황에서는 삭제를 허용하는 방식이 적절합니다.
42-60: 이 코드는 정상적으로 작동합니다.
getUserInfo()는 반환값 외에도setUserInfo(user)를 통해 전역 상태를 업데이트하는 사이드 이펙트를 가집니다.await를 사용한 것은 유저 정보 갱신이 완료된 후 토스트 메시지를 표시하도록 순서를 보장하기 위한 것이므로, 반환값을 사용하지 않는 것은 문제가 아닙니다. 현재 구현이 의도대로 작동하고 있습니다.Likely an incorrect or invalid review comment.
src/widgets/review-list/ui/ReviewCardList.tsx (3)
42-52: LGTM!
workReviewId또는internshipReviewId를 추출하여 안정적인 React key를 사용하고,isLastItem을 인덱스 기반으로 계산하는 접근 방식이 적절합니다.
98-111: LGTM!
isWriteButtonDisabled로직과reviewContentProps객체를 통한 props 통합이 가독성과 유지보수성을 향상시킵니다.
125-131: LGTM!사용자 역할에 따라 리뷰 타입을 결정하는 라우팅 로직이 명확합니다.
PROSPECTIVE_TEACHER의 경우learning타입으로 전환되어 #47 이슈의 요구사항을 충족합니다.src/entities/review/hooks/useGetReview.tsx (3)
1-6: LGTM!
useSuspenseInfiniteQueryimport와DYNAMIC_CACHE_CONFIG설정이 무한 스크롤 기능 구현에 적절하게 추가되었습니다.
46-46: LGTM!API 호출 파라미터가
{ sortType }객체 형태로 변경되어ReviewQueryParams인터페이스와 일관성을 유지합니다.
180-208: 무한 스크롤 구현이 잘 되어있습니다.쿼리 키에
"infinite"와pageSize를 포함하여 일반 쿼리와의 캐시 충돌을 방지한 점이 좋습니다.getNextPageParam로직도 0-기반 페이지네이션에 맞게 올바르게 구현되었습니다.
DYNAMIC_CACHE_CONFIG의 설정값(staleTime 5분, gcTime 15분)은 사용자 생성 콘텐츠인 리뷰의 무한 스크롤 UX에 적합합니다. 5분의 stale time은 데이터 신선도와 성능 사이의 균형을 잘 맞추고 있으며, 15분의 garbage collection 설정은 페이지네이션된 데이터를 메모리에 유지하기에 적절합니다.
| <MenuItem | ||
| iconPath={SVG_PATHS.USER_MENU.LOGOUT} | ||
| to={URL_PATHS.INQUIRY_EDITOR} | ||
| iconAlt="유저 아이콘" | ||
| iconAlt="문의 아이콘" | ||
| label="1: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.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and read the file mentioned in the review
cat -n src/pages/user/inquiry/index.tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 2359
🏁 Script executed:
# Search for SVG_PATHS definitions to understand available icons
rg "SVG_PATHS" --type ts --type tsx -A 3 | head -100Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 104
🏁 Script executed:
# Look for INQUIRY or similar icon definitions
rg "INQUIRY.*ICON|INQUIRY.*SVG" --type ts --type tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 104
🏁 Script executed:
# Check the USER_MENU icon paths to understand the pattern
rg "USER_MENU\s*[:=]|LOGOUT" --type ts --type tsx -B 2 -A 10 | head -150Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 104
🏁 Script executed:
# Find SVG_PATHS definitions
find . -name "*asset*path*" -o -name "*svg*path*" -o -name "constants.ts" -o -name "constants.tsx" | grep -i assetsRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 114
🏁 Script executed:
# Search for SVG_PATHS definitions using rg with js/ts files
rg "SVG_PATHS\s*=" -t js -t ts --max-count 5 -A 30Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1779
🏁 Script executed:
# Look for where SVG_PATHS.QUESTION is defined
rg "QUESTION" --type js --type ts | grep -i "svg\|path" | head -20Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 364
🏁 Script executed:
# Read the full SVG_PATHS definition to see all available icons
cat -n src/shared/constants/assets-path.tsRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 11510
잘못된 아이콘 상수를 수정하세요.
이 MenuItem은 iconPath={SVG_PATHS.USER_MENU.LOGOUT}으로 설정되어 있지만, 기능은 "1:1 문의하기"입니다. iconAlt가 이미 "문의 아이콘"으로 변경되었으나, 실제 아이콘 경로는 로그아웃 아이콘을 가리키고 있습니다.
SVG_PATHS.USER_MENU.INQUIRY가 존재하므로, iconPath를 다음과 같이 변경하세요:
iconPath={SVG_PATHS.USER_MENU.INQUIRY}
🤖 Prompt for AI Agents
In @src/pages/user/inquiry/index.tsx around lines 44 - 49, The MenuItem for "1:1
문의하기" is using the wrong icon constant; change the iconPath prop on the MenuItem
(currently set to SVG_PATHS.USER_MENU.LOGOUT) to SVG_PATHS.USER_MENU.INQUIRY so
the displayed icon matches the label and iconAlt; update the iconPath value in
the <MenuItem ... /> invocation accordingly.
| export async function requestRewardAd(): Promise<RewardAdResult> { | ||
| try { | ||
| // 타임아웃 설정 (30초) - 광고 로드 및 시청 시간 고려 | ||
| const timeoutPromise = new Promise<RewardAdResult>((resolve) => { | ||
| setTimeout(() => { | ||
| console.warn("보상형 광고 응답 타임아웃"); | ||
| resolve({ | ||
| status: "error", | ||
| rewarded: false, | ||
| message: "광고를 불러오는데 시간이 너무 오래 걸립니다.", | ||
| }); | ||
| }, 30000); | ||
| }); | ||
|
|
||
| const adPromise = sendToFlutter< | ||
| Record<string, never>, | ||
| RewardAdResult | ||
| >(MessageType.REQUEST_REWARD_AD, {}); | ||
|
|
||
| const result = await Promise.race([adPromise, timeoutPromise]); | ||
|
|
||
| // 구버전 앱의 default 응답 처리 (rewarded 필드가 없는 경우) | ||
| if (result.status === "success" && result.rewarded === undefined) { | ||
| console.warn("구버전 앱 감지 - 광고 기능이 없는 버전입니다."); | ||
| return { | ||
| status: "success", | ||
| rewarded: false, | ||
| message: "광고 기능을 지원하지 않는 버전입니다.", | ||
| }; | ||
| } | ||
|
|
||
| return result; |
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.
타임아웃 타이머 정리 누락
Promise.race 패턴에서 adPromise가 먼저 완료되더라도 setTimeout이 계속 실행됩니다. 메모리 누수를 방지하기 위해 타이머를 정리해야 합니다.
🔧 타이머 정리 제안
export async function requestRewardAd(): Promise<RewardAdResult> {
try {
+ let timeoutId: ReturnType<typeof setTimeout>;
+
// 타임아웃 설정 (30초) - 광고 로드 및 시청 시간 고려
const timeoutPromise = new Promise<RewardAdResult>((resolve) => {
- setTimeout(() => {
+ timeoutId = setTimeout(() => {
console.warn("보상형 광고 응답 타임아웃");
resolve({
status: "error",
rewarded: false,
message: "광고를 불러오는데 시간이 너무 오래 걸립니다.",
});
}, 30000);
});
const adPromise = sendToFlutter<
Record<string, never>,
RewardAdResult
>(MessageType.REQUEST_REWARD_AD, {});
const result = await Promise.race([adPromise, timeoutPromise]);
+ clearTimeout(timeoutId!);
// 구버전 앱의 default 응답 처리 (rewarded 필드가 없는 경우)🤖 Prompt for AI Agents
In @src/shared/utils/webViewCommunication.ts around lines 214 - 245, In
requestRewardAd the setTimeout created for timeoutPromise isn't cleared when
adPromise wins, leaking timers; capture the timer id returned by setTimeout,
then after awaiting Promise.race (using the existing
adPromise/timeoutPromise/result flow) call clearTimeout(timerId) before any
return path so the timer is always cleared (including the legacy-response branch
and normal return). Ensure the timer variable is defined in the outer scope of
the race so it can be cleared regardless of which promise resolved.
| const [localIsLiked, setLocalIsLiked, rollbackIsLiked] = useOptimisticUpdate( | ||
| false, | ||
| isLiked | ||
| ); |
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.
초기값 설정으로 인한 UI 깜빡임 가능성.
localIsLiked가 false로 초기화된 후 useEffect에서 isLiked 값으로 동기화됩니다. 이로 인해 실제 좋아요 상태와 관계없이 첫 렌더링에서 "좋아요 안 함" 상태로 잠시 표시될 수 있습니다.
🔧 초기값을 isLiked로 설정하는 수정 제안
- const [localIsLiked, setLocalIsLiked, rollbackIsLiked] = useOptimisticUpdate(
- false,
- isLiked
- );
+ const [localIsLiked, setLocalIsLiked, rollbackIsLiked] = useOptimisticUpdate(
+ isLiked ?? false,
+ isLiked
+ );📝 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 [localIsLiked, setLocalIsLiked, rollbackIsLiked] = useOptimisticUpdate( | |
| false, | |
| isLiked | |
| ); | |
| const [localIsLiked, setLocalIsLiked, rollbackIsLiked] = useOptimisticUpdate( | |
| isLiked ?? false, | |
| isLiked | |
| ); |
🤖 Prompt for AI Agents
In @src/widgets/review-list/ui/ReviewCardList.tsx around lines 93 - 96, The
optimistic local like state is initialized to false causing a UI flash; change
the initializer passed into useOptimisticUpdate from the literal false to the
actual prop value isLiked (or a safe boolean fallback like !!isLiked) so
localIsLiked starts in sync with the real state; update the call to
useOptimisticUpdate(...) to use isLiked as the initial value and keep the rest
(setLocalIsLiked, rollbackIsLiked) unchanged.
| const handleOptimisticLike = async () => { | ||
| setLocalIsLiked((prev: boolean) => !prev); | ||
| setLikeCount((prev: number) => prev + (localIsLiked ? -1 : 1)); | ||
|
|
||
| handleLike(); | ||
| try { | ||
| await handleLike(); | ||
| } catch { | ||
| rollbackIsLiked(); | ||
| rollbackLikeCount(); | ||
| } | ||
| }; |
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.
좋아요 중복 클릭 방지 로직 누락.
handleOptimisticLike가 isPending 상태를 확인하지 않아 사용자가 빠르게 연속 클릭할 경우 중복 mutation이 발생할 수 있습니다.
🔧 isPending 체크 추가 제안
const handleOptimisticLike = async () => {
+ if (isPending) return;
+
setLocalIsLiked((prev: boolean) => !prev);
setLikeCount((prev: number) => prev + (localIsLiked ? -1 : 1));
try {
await handleLike();
} catch {
rollbackIsLiked();
rollbackLikeCount();
}
};📝 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 handleOptimisticLike = async () => { | |
| setLocalIsLiked((prev: boolean) => !prev); | |
| setLikeCount((prev: number) => prev + (localIsLiked ? -1 : 1)); | |
| handleLike(); | |
| try { | |
| await handleLike(); | |
| } catch { | |
| rollbackIsLiked(); | |
| rollbackLikeCount(); | |
| } | |
| }; | |
| const handleOptimisticLike = async () => { | |
| if (isPending) return; | |
| setLocalIsLiked((prev: boolean) => !prev); | |
| setLikeCount((prev: number) => prev + (localIsLiked ? -1 : 1)); | |
| try { | |
| await handleLike(); | |
| } catch { | |
| rollbackIsLiked(); | |
| rollbackLikeCount(); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In @src/widgets/review-list/ui/ReviewCardList.tsx around lines 113 - 123,
handleOptimisticLike lacks an isPending guard so rapid clicks can trigger
duplicate mutations; add an early return if isPending is true, then set a
pending flag (isPending = true) before calling handleLike and clear it
(isPending = false) in both the try and catch paths; keep the existing
optimistic updates (setLocalIsLiked, setLikeCount) and existing rollback calls
(rollbackIsLiked, rollbackLikeCount) but ensure isPending prevents re-entry
while the mutation is in-flight.
| const workReviews = useInfiniteWorkReviews(kindergartenId, sortType, 10); | ||
| const internshipReviews = useInfiniteInternshipReviews( | ||
| kindergartenId, | ||
| sortType, | ||
| 10 | ||
| ); | ||
|
|
||
| const infiniteQuery = | ||
| type === REVIEW_TYPES.WORK ? workReviews : internshipReviews; |
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.
🧩 Analysis chain
🏁 Script executed:
find src -type f -name "*.ts" -o -name "*.tsx" | grep -i hook | head -20Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 978
🏁 Script executed:
fd -e ts -e tsx "useInfiniteWorkReviews|useInfiniteInternshipReviews" src/Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 59
🏁 Script executed:
cat -n src/widgets/review-panel/lib/useReviewPage.tsx | head -50Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1995
🏁 Script executed:
rg -n "export.*useInfinite(Work|Internship)Reviews" --type=ts -B 2 -A 10Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1985
🏁 Script executed:
cat -n src/entities/review/hooks/useGetReview.tsx | sed -n '178,210p'Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1262
🏁 Script executed:
cat -n src/entities/review/hooks/useGetReview.tsx | sed -n '211,240p'Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1101
🏁 Script executed:
rg -n "useSuspenseInfiniteQuery" src/entities/review/hooks/useGetReview.tsx -B 2 -A 15Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1924
🏁 Script executed:
rg -n "DYNAMIC_CACHE_CONFIG" src/shared/config/query -B 2 -A 10Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 140
🏁 Script executed:
cat src/shared/config/query.tsRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 910
🏁 Script executed:
rg -n "enabled" src/entities/review/hooks/useGetReview.tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 59
🏁 Script executed:
web_search
useSuspenseInfiniteQuery enabled option tanstack react-queryRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 190
🌐 Web query:
useSuspenseInfiniteQuery enabled option tanstack react-query
💡 Result:
Short answer: useSuspenseInfiniteQuery does NOT support the enabled option. The hook's options are the same as useInfiniteQuery except that suspense, throwOnError, enabled and placeholderData are removed. See the docs. [1][2]
Sources:
- TanStack Query docs — useSuspenseInfiniteQuery. [1]
- Chinese mirror of TanStack docs showing the same options exclusion. [2]
🏁 Script executed:
rg -n "enabled\s*:" src/entities/review/hooks/ -A 2 -B 2Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 59
🏁 Script executed:
rg -n "useSuspenseQuery|useInfiniteQuery" src/entities/review/hooks/useGetReview.tsx -B 5 -A 20 | head -100Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 2931
조건부 호출로 개선 필요
useInfiniteWorkReviews와 useInfiniteInternshipReviews가 항상 함께 호출되어 선택되지 않은 리뷰 타입에 대해서도 API 요청이 발생합니다. 두 훅 모두 useSuspenseInfiniteQuery를 사용하므로 마운트 시 즉시 데이터를 가져옵니다.
주의: useSuspenseInfiniteQuery는 enabled 옵션을 지원하지 않습니다(공식 문서에서 제거된 옵션). 대신 다음 방안을 검토해 주세요:
useInfiniteQuery+enabled옵션으로 변경 (Suspense 제거 필요)- 리뷰 타입별로 컴포넌트 분리
- 조건부 훅 호출 패턴 적용 (별도 래퍼 컴포넌트)
🤖 Prompt for AI Agents
In @src/widgets/review-panel/lib/useReviewPage.tsx around lines 30 - 38, Both
hooks useSuspenseInfiniteQuery under the hood so calling useInfiniteWorkReviews
and useInfiniteInternshipReviews unconditionally triggers both API requests;
only the selected review type should mount its hook. Fix by ensuring only the
hook for the active type is invoked: either (A) split the UI into two components
(e.g., WorkReviewsPanel and InternshipReviewsPanel) and move
useInfiniteWorkReviews and useInfiniteInternshipReviews into those components so
only the mounted component runs its hook, or (B) switch the hooks to
useInfiniteQuery with an enabled flag and use the existing conditional selection
(keep function names useInfiniteWorkReviews/useInfiniteInternshipReviews but
implement them to use useInfiniteQuery + enabled based on the passed type), or
(C) wrap each hook call behind a conditional render wrapper that only calls
useInfiniteWorkReviews when type === REVIEW_TYPES.WORK and
useInfiniteInternshipReviews when type === REVIEW_TYPES.INTERNSHIP so the
non-selected hook is never invoked.
- 메인 화면 인기 게시글에 주간/월간/전체 탭 추가 - 커뮤니티 NavBar에 "인기글" 탭 추가 (제일 앞) - 커뮤니티 카테고리에서 TOP10 제거 - 인기글과 일반 게시글 UI/UX 명확하게 분리 - 새로운 API 엔드포인트 /community/popular 연동 BREAKING CHANGE: - 커뮤니티 URL 구조 변경: ?type=popular 추가 - 기존 top10 카테고리 제거 @0juicy 확인 부탁드립니다
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/widgets/community-feed/lib/category.ts (1)
92-92:top10라벨을 제거해야 합니다.
top10은CATEGORY_LABELS에 정의되어 있지만 코드베이스의 어디에서도 참조되지 않습니다.CATEGORY_INFO의 categories 배열에도 포함되지 않으며, 새로운 "인기글" 기능(type=popular)이 이를 대체했습니다. 불필요한 dead code이므로 92번 줄에서 제거하세요.
🤖 Fix all issues with AI agents
In @src/pages/community/index.tsx:
- Around line 25-27: The code casts the URL value to PeriodType using
(searchParams.get("period") || "weekly") as PeriodType which doesn't validate
unknown values; update the logic around period (where searchParams.get("period")
is read) to check the returned string against the allowed PeriodType values
(e.g., a whitelist/enum of valid periods) and if it is not one of them, fall
back to the default "weekly" before assigning to the period variable so period
always holds a valid PeriodType.
In @src/widgets/community-feed/ui/CategorySelector.tsx:
- Around line 37-46: The refetch call builds a queryKey as an object
(queryParams) which doesn't match the array shape used by useCommunityPosts;
update the refetch to build the same array key shape used elsewhere (e.g.
["communityPosts", categoryConst, pageSizeString, categoryNameOrAll, "", "",
""]). Concretely, replace the current queryParams object usage in
queryClient.refetchQueries with a computed array using the same elements as the
real key (use the category value mapped to "TEACHER"|"PROSPECTIVE_TEACHER",
String(pageSize) for "10", and categoryName ?? "all" for the fourth slot, plus
the remaining empty string placeholders) so the refetch key matches
useCommunityPosts.
🧹 Nitpick comments (5)
src/widgets/community-feed/ui/PeriodSelector.tsx (1)
17-28: URL 파라미터 타입 안전성 개선 권장Line 19에서
as PeriodType타입 단언을 사용하고 있는데, URL이 임의로 조작될 경우 유효하지 않은 값이 들어올 수 있습니다. 런타임 유효성 검사를 추가하는 것이 안전합니다.♻️ 타입 안전성을 위한 유효성 검사 추가
+const isValidPeriod = (value: string | null): value is PeriodType => { + return value === "weekly" || value === "monthly" || value === "all"; +}; + export default function PeriodSelector({ type }: PeriodSelectorProps) { const [searchParams, setSearchParams] = useSearchParams(); - const currentPeriod = (searchParams.get("period") || "weekly") as PeriodType; + const periodParam = searchParams.get("period"); + const currentPeriod: PeriodType = isValidPeriod(periodParam) ? periodParam : "weekly";src/entities/community/hooks/usePopularPostsByPeriod.tsx (1)
1-1: 파일 확장자 변경은 선택사항이 파일은 JSX를 포함하지 않는 것이 맞습니다. 다만 코드베이스의 관례상 hook 파일들은
useSocialAuth.ts를 제외하고 모두.tsx확장자를 사용하고 있습니다..ts로 변경하려면 프로젝트의 naming convention을 일관되게 조정해야 합니다.src/widgets/community-feed/post-list/ui/PopularPostList.tsx (1)
8-14:PeriodType과PERIOD_LABELS가 여러 파일에 중복 정의되어 있습니다.
PeriodType은 이미src/widgets/community-feed/ui/PeriodSelector.tsx에서 export되고 있으며,PERIOD_LABELS도PopularPostsPreview.tsx에 동일하게 정의되어 있습니다. 공통 상수 파일로 추출하여 재사용하는 것이 유지보수에 좋습니다.♻️ 제안: 공통 모듈에서 import하기
-type PeriodType = "weekly" | "monthly" | "all"; - -const PERIOD_LABELS = { - weekly: "주간", - monthly: "월간", - all: "전체", -} as const; +import { PeriodType, PERIOD_LABELS } from "@/widgets/community-feed/ui/PeriodSelector";또는
src/widgets/community-feed/lib/category.ts와 같은 공통 파일에 상수를 정의하고 import하는 방식을 권장합니다.src/widgets/home-dashboard/ui/PopularPostsPreview.tsx (2)
11-23:PeriodType,PERIOD_OPTIONS,PERIOD_LABELS가 중복 정의되어 있습니다.이 상수들이
PopularPostList.tsx와PeriodSelector.tsx에도 정의되어 있습니다. DRY 원칙에 따라 공통 모듈로 추출하여 일관성을 유지하고 향후 수정 시 한 곳만 변경하도록 하는 것이 좋습니다.
57-67:[0, 1, 2].map대신.slice(0, 3)을 사용하면 더 명확합니다.현재 구현도 동작하지만,
slice를 사용하면 의도가 더 분명해지고 조건부 null 체크가 불필요해집니다.♻️ 제안된 개선
- [0, 1, 2].map((i) => - currentPosts[i] ? ( - <PostCard - key={currentPosts[i].id} - post={currentPosts[i]} - index={i} - currentCategory="top10" - getCategoryLabel={getCategoryLabel} - /> - ) : null - ) + currentPosts.slice(0, 3).map((post, index) => ( + <PostCard + key={post.id} + post={post} + index={index} + currentCategory="top10" + getCategoryLabel={getCategoryLabel} + /> + ))
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
.storybook/preview.tsxsrc/entities/community/DTO.d.tssrc/entities/community/api.tssrc/entities/community/hooks/index.tssrc/entities/community/hooks/usePopularPostsByPeriod.tsxsrc/pages/community/index.tsxsrc/pages/home/index.tsxsrc/shared/config/api.tssrc/widgets/community-feed/lib/category.tssrc/widgets/community-feed/post-list/ui/PopularPostList.tsxsrc/widgets/community-feed/ui/CategorySelector.tsxsrc/widgets/community-feed/ui/PeriodSelector.tsxsrc/widgets/home-dashboard/ui/PopularPostsPreview.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
src/entities/community/hooks/usePopularPostsByPeriod.tsx (2)
src/entities/community/DTO.d.ts (1)
PopularPostsByPeriodResponse(102-110)src/entities/community/api.ts (1)
getPopularPostsByPeriod(29-34)
src/widgets/community-feed/ui/CategorySelector.tsx (5)
src/pages/community/post-editor.tsx (3)
currentCategoryName(64-83)categoryName(86-98)category(39-42)src/pages/search/community.tsx (1)
allPosts(56-62)src/shared/utils/lastVisitedPathUtils.ts (1)
setCommunityCategory(142-147)src/widgets/community-feed/post-list/index.tsx (1)
options(54-75)src/widgets/community-feed/post-list/ui/PostCard.tsx (1)
PostCard(19-97)
src/widgets/home-dashboard/ui/PopularPostsPreview.tsx (4)
src/widgets/community-feed/ui/PeriodSelector.tsx (1)
PeriodType(5-5)src/entities/community/hooks/usePopularPostsByPeriod.tsx (1)
usePopularPostsByPeriod(8-14)src/widgets/community-feed/post-list/ui/PostCard.tsx (1)
PostCard(19-97)src/shared/utils/categoryUtils.ts (1)
getCategoryLabel(12-14)
src/entities/community/api.ts (3)
src/entities/community/DTO.d.ts (1)
PopularPostsByPeriodResponse(102-110)src/shared/api/utils.ts (1)
apiCall(47-195)src/shared/config/api.ts (1)
API_PATHS(2-99)
src/widgets/community-feed/post-list/ui/PopularPostList.tsx (2)
src/widgets/community-feed/ui/PeriodSelector.tsx (1)
PeriodType(5-5)src/entities/community/hooks/usePopularPostsByPeriod.tsx (1)
usePopularPostsByPeriod(8-14)
src/pages/community/index.tsx (5)
src/widgets/community-feed/ui/PeriodSelector.tsx (2)
PeriodType(5-5)PeriodSelector(17-46)src/shared/utils/lastVisitedPathUtils.ts (1)
setCommunityState(119-123)src/features/nav/ui/NavBar.tsx (1)
NavBar(123-143)src/widgets/community-feed/post-list/ui/PopularPostList.tsx (1)
PopularPostList(20-79)src/widgets/community-feed/post-list/index.tsx (3)
PostList(37-165)PostListProps(169-174)window(109-109)
src/widgets/community-feed/ui/PeriodSelector.tsx (1)
src/widgets/all-review-list/index.tsx (1)
newSortType(52-58)
🔇 Additional comments (12)
src/pages/home/index.tsx (1)
54-60: LGTM!이미지 소스
IMAGE_PATHS.BANNER.COMMUNITY와 일치하도록 alt 텍스트를 "커뮤니티 배너"로 수정한 것은 적절합니다. 접근성 측면에서 더 정확한 이미지 설명을 제공합니다..storybook/preview.tsx (1)
5-5: QueryProvider 임포트 경로 변경이 올바르게 적용되었습니다.
src/app/providers/QueryProvider로의 경로 변경이 완료되었으며, 기존 경로는 제거되었습니다. 프로젝트 전반의 provider 구조 개편에 맞춘 적절한 변경입니다.src/widgets/community-feed/lib/category.ts (1)
20-29: LGTM!새로운 "인기글" 카테고리 아이콘 항목이 기존 항목들과 일관된 구조로 추가되었습니다.
SVG_PATHS.KINDERGARTEN_INFO.CHART경로도 유효하며, 아이콘 크기와 라벨 형식이 다른 항목들과 동일합니다.src/widgets/community-feed/ui/PeriodSelector.tsx (1)
30-44: LGTM!버튼 렌더링 로직이 명확하고,
PERIOD_OPTIONS를 활용한 매핑 방식이 유지보수에 용이합니다. 현재 선택된 기간에 따른 variant 스타일 처리도 적절합니다.src/shared/config/api.ts (1)
44-44: LGTM!기존 API 경로 패턴과 일관되게 POPULAR 엔드포인트가 추가되었습니다.
src/entities/community/hooks/index.ts (1)
7-7: LGTM!새로운 훅의 배럴 익스포트가 기존 패턴을 따르며 알파벳 순서대로 적절히 배치되었습니다.
src/widgets/community-feed/ui/CategorySelector.tsx (1)
19-19: LGTM!기본 카테고리가 "top10"에서 "all"로 변경되었으며, 이는 인기 게시물 흐름이 분리된 PR 변경사항과 일치합니다.
src/entities/community/hooks/usePopularPostsByPeriod.tsx (1)
1-14: LGTM!API가 모든 기간 데이터(weekly, monthly, all)를 한 번에 반환하므로 정적 쿼리키 사용이 적절합니다.
useSuspenseQuery사용으로 부모 컴포넌트에 Suspense 경계가 필요합니다.src/entities/community/api.ts (1)
26-34: LGTM!새로운
getPopularPostsByPeriodAPI 함수가 기존 패턴을 잘 따르고 있습니다.API_PATHS.COMMUNITY.POST.POPULAR경로를 사용하며, 반환 타입이 올바르게 지정되어 있습니다.src/widgets/community-feed/post-list/ui/PopularPostList.tsx (1)
20-22: LGTM!
periodprop을 통한 데이터 접근과 optional chaining을 사용한 안전한 데이터 처리가 잘 구현되어 있습니다.src/entities/community/DTO.d.ts (1)
101-110: LGTM!새로운
PopularPostsByPeriodResponse인터페이스가 기존 응답 타입 패턴과 일관성 있게 정의되어 있습니다.weekly,monthly,all속성이 명확하게 타입화되어 있습니다.src/pages/community/index.tsx (1)
77-100: LGTM!인기글 모드와 일반 카테고리 모드의 분기 처리가 명확하게 구현되어 있습니다.
Suspense를 활용한 로딩 처리와 각 모드에 맞는 컴포넌트 렌더링이 잘 구성되어 있습니다.
| const typeParam = searchParams.get("type") || "popular"; // 기본값: 인기글 | ||
| const period = (searchParams.get("period") || "weekly") as PeriodType; | ||
| const categoryName = searchParams.get("category") || "all"; |
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에서 가져온 period 값에 대한 유효성 검증이 없습니다.
as PeriodType 타입 단언은 URL에 잘못된 값(예: ?period=invalid)이 들어왔을 때 런타임 오류를 방지하지 못합니다. 유효하지 않은 값일 경우 기본값으로 폴백하는 것이 안전합니다.
🔧 제안된 수정
+const VALID_PERIODS = ["weekly", "monthly", "all"] as const;
+
const typeParam = searchParams.get("type") || "popular";
-const period = (searchParams.get("period") || "weekly") as PeriodType;
+const periodParam = searchParams.get("period");
+const period: PeriodType = VALID_PERIODS.includes(periodParam as PeriodType)
+ ? (periodParam as PeriodType)
+ : "weekly";
const categoryName = searchParams.get("category") || "all";📝 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 typeParam = searchParams.get("type") || "popular"; // 기본값: 인기글 | |
| const period = (searchParams.get("period") || "weekly") as PeriodType; | |
| const categoryName = searchParams.get("category") || "all"; | |
| const VALID_PERIODS = ["weekly", "monthly", "all"] as const; | |
| const typeParam = searchParams.get("type") || "popular"; // 기본값: 인기글 | |
| const periodParam = searchParams.get("period"); | |
| const period: PeriodType = VALID_PERIODS.includes(periodParam as PeriodType) | |
| ? (periodParam as PeriodType) | |
| : "weekly"; | |
| const categoryName = searchParams.get("category") || "all"; |
🤖 Prompt for AI Agents
In @src/pages/community/index.tsx around lines 25 - 27, The code casts the URL
value to PeriodType using (searchParams.get("period") || "weekly") as PeriodType
which doesn't validate unknown values; update the logic around period (where
searchParams.get("period") is read) to check the returned string against the
allowed PeriodType values (e.g., a whitelist/enum of valid periods) and if it is
not one of them, fall back to the default "weekly" before assigning to the
period variable so period always holds a valid PeriodType.
| const queryParams = { | ||
| pageSize: 10, | ||
| category: type === "teacher" ? "TEACHER" : "PROSPECTIVE_TEACHER", | ||
| categoryName: category !== "all" ? category : undefined, | ||
| }; | ||
|
|
||
| queryClient.refetchQueries({ | ||
| queryKey: ["communityPosts", queryParams], | ||
| exact: false, | ||
| }); | ||
| } else if (category === "top10") { | ||
| queryClient.refetchQueries({ | ||
| queryKey: ["popularPosts"], | ||
| }); | ||
| } | ||
| queryClient.refetchQueries({ | ||
| queryKey: ["communityPosts", queryParams], | ||
| exact: 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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: communityPosts 쿼리키 사용 패턴 확인
rg -n "queryKey.*communityPosts" --type=ts --type=tsx -C2Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 104
🏁 Script executed:
#!/bin/bash
# Search for communityPosts query key patterns in TypeScript files
rg -n "communityPosts" --type-list | grep -i type
rg -n "communityPosts" -t ts -C2Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 4670
🏁 Script executed:
#!/bin/bash
# Read useCommunityPosts to see the full queryKey structure
cat -n src/entities/community/hooks/useCommunityPosts.tsx | head -40
# Read CategorySelector to see queryParams structure
cat -n src/widgets/community-feed/ui/CategorySelector.tsx | head -50Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 3300
🏁 Script executed:
#!/bin/bash
# Check how refetchQueries is used elsewhere in the codebase
rg -n "refetchQueries" -C3
# Also search for how queryKey is being used with exact parameter
rg -n "exact:" -C2Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 1020
🏁 Script executed:
#!/bin/bash
# Check if there are other ways communityPosts queries are created
rg -n "useCommunityPosts\|useInfiniteQuery.*communityPosts" -A5 -B2
# Check the full CategorySelector file to understand the context
wc -l src/widgets/community-feed/ui/CategorySelector.tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 130
🏁 Script executed:
#!/bin/bash
# Read the entire CategorySelector file
cat -n src/widgets/community-feed/ui/CategorySelector.tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 2506
🏁 Script executed:
#!/bin/bash
# Check how post-list uses useCommunityPosts to confirm queryKey structure
grep -n "useCommunityPosts" src/widgets/community-feed/post-list/index.tsx -A10Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 872
🏁 Script executed:
#!/bin/bash
# Check what queryOptions are passed to useCommunityPosts
grep -n "queryOptions" src/widgets/community-feed/post-list/index.tsx -B15 | head -40Repository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 982
🏁 Script executed:
#!/bin/bash
# Get the full queryOptions definition
sed -n '54,75p' src/widgets/community-feed/post-list/index.tsxRepository: OneByOne-Kindergarten/OneByOne_FE
Length of output: 565
쿼리키 구조 불일치로 리패치가 작동하지 않음
queryParams 객체를 배열 요소로 전달하면 실제 쿼리키 구조와 매칭되지 않습니다.
실제 쿼리키: ["communityPosts", "TEACHER", "10", "all", "", "", ""]
전달된 쿼리키: ["communityPosts", {pageSize: 10, category: "TEACHER", categoryName: undefined}]
리패치가 실행되지 않아 카테고리 변경 시 데이터가 갱신되지 않습니다. useCommunityPosts의 queryKey 구조에 맞게 수정이 필요합니다.
🤖 Prompt for AI Agents
In @src/widgets/community-feed/ui/CategorySelector.tsx around lines 37 - 46, The
refetch call builds a queryKey as an object (queryParams) which doesn't match
the array shape used by useCommunityPosts; update the refetch to build the same
array key shape used elsewhere (e.g. ["communityPosts", categoryConst,
pageSizeString, categoryNameOrAll, "", "", ""]). Concretely, replace the current
queryParams object usage in queryClient.refetchQueries with a computed array
using the same elements as the real key (use the category value mapped to
"TEACHER"|"PROSPECTIVE_TEACHER", String(pageSize) for "10", and categoryName ??
"all" for the fourth slot, plus the remaining empty string placeholders) so the
refetch key matches useCommunityPosts.
⚒️ 작업 내용
⚙️ 관련 이슈
☎️ 관련 문의
Summary by CodeRabbit
새로운 기능
개선
문구 수정
✏️ Tip: You can customize this high-level summary in your review settings.