Repository navigation
05.06 최신화 - #302
05.06 최신화#302
Conversation
fix: 구독 관련 이미지 수정 및 타입 가드 적용
feat: 상태 관리 아키텍처 구현
feat: 구독자 삭제 및 구독 취소 시 항목이 즉시 사라지지 않고 버튼 상태만 변경되도록 수정
feat: 알림 조회 요청 관련 enable 옵션 추가 및 레드닷 로직 추가
feat: 비공개 모임에 대한 공지사항 및 책장 UI 구현
fix: 배너에 프로모션 리스트 수정
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCentralizes avatar fallbacks via DEFAULT_PROFILE_IMAGE, tightens URL validation to a type predicate, unifies avatar rendering, adds framer-motion header/notification UX, implements report/block modal flow with animated toasts, tweaks optimistic mutation cache behavior, derives promotion slides by publishEndAt, and adds landing/layout/docs. ChangesConsolidated Update
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request centralizes the default profile image path into a constant, refactors profile image rendering across numerous components, and improves the user experience for follower deletion and book/story liking through optimistic updates and refined cache invalidation strategies. It also enhances the news banner with promotion filtering and sorting, adds an unread notification indicator to the header, and updates various empty state UIs. Feedback was provided regarding code formatting in NotificationDropdown.tsx and potential date parsing issues in NewsListBanner.tsx.
| const { mutate: readNotification } = useReadNotificationMutation(); | ||
| const { isLoggedIn } = useAuthStore(); | ||
| const { data: notificationsData, isLoading } = useNotificationPreviewQuery(5, isLoggedIn); | ||
| const notifications = isLoggedIn ? notificationsData : []; const { mutate: readNotification } = useReadNotificationMutation(); |
There was a problem hiding this comment.
한 줄에 두 개의 const 선언이 포함되어 있어 가독성이 떨어집니다. 코드의 명확성을 위해 각 선언을 별도의 줄로 분리하는 것이 좋습니다.
| const notifications = isLoggedIn ? notificationsData : []; const { mutate: readNotification } = useReadNotificationMutation(); | |
| const notifications = isLoggedIn ? notificationsData : []; | |
| const { mutate: readNotification } = useReadNotificationMutation(); |
| const promotionList = useMemo(() => { | ||
| return newsList | ||
| .filter((news) => news.carousel === "PROMOTION") | ||
| .sort((a, b) => new Date(b.publishEndAt).getTime() - new Date(a.publishEndAt).getTime()) |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/utils/url.ts (1)
4-25:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRestrict this type guard to image-safe schemes.
new URL(url)treats any parseable absolute scheme as valid, so this helper now narrowsdata:,ftp:,javascript:, and other non-HTTP schemes tostringas well. Since all 30+ call sites in the codebase use it to decide whether to pass the value straight into Next.jsImagecomponents, the guard should only accept/...,//...,http:, orhttps:URLs.Suggested fix
export const isValidUrl = (url: string | null | undefined): url is string => { - if (!url || url === "string" || url.trim() === "") return false; + if (!url || url === "string" || url.trim() === "") return false; + const trimmed = url.trim(); // 허용되는 상대 경로 패턴 (예: /profile2.svg) - if (url.startsWith("/")) return true; + if (trimmed.startsWith("/")) return true; try { - new URL(url); - return true; // http, https 등 유효한 scheme이 있는 경우 + const parsed = new URL(trimmed); + return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { // 프로토콜 상대 URL 지원 (예: //example.com/image.png) - if (url.startsWith("//")) { + if (trimmed.startsWith("//")) { try { - new URL(`https:${url}`); - return true; + const parsed = new URL(`https:${trimmed}`); + return parsed.protocol === "https:"; } catch { return false; } } return false; } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/url.ts` around lines 4 - 25, The isValidUrl type guard currently accepts any absolute URL scheme; update it so it only returns true for relative paths starting with "/" , protocol-relative URLs starting with "//" (validate by prefixing "https:"), or absolute URLs whose URL.prototype.protocol is exactly "http:" or "https:". Keep the existing null/empty checks (including trimming) and the special-case handling for strings starting with "/", but after constructing new URL(url) verify urlObj.protocol === "http:" || urlObj.protocol === "https:"; for the "//" branch construct new URL(`https:${url}`) and treat that as valid only if it parses successfully.src/hooks/mutations/useBookMutations.ts (1)
145-156:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep the
likedBookscache consistent during the optimistic toggle.
fullBookToInjectis captured from the pre-mutation caches, sofullBookToInject.likedByMeis stillfalseon a fresh like and the inject branch never runs. On the unlike path, the new.map()-only update leaves the book insidelikedBookswithlikedByMe: false. Since Lines 217-219 now skip the immediate refetch, both cases leave "내 서재" stale until a later manual refresh.🐛 Suggested fix
- let updatedBooks = page.books - .map((book) => - book.isbn === isbn ? { ...book, likedByMe: !book.likedByMe } : book - ); + let updatedBooks = page.books + .map((book) => + book.isbn === isbn ? { ...book, likedByMe: !book.likedByMe } : book + ) + .filter((book) => !isMyLibrary || book.likedByMe); // Inject the new book into the very first page if it's not already in the library, // we are actually liking it, and we are looking at My Library if (index === 0 && isMyLibrary && !isAlreadyInLibrary && fullBookToInject) { - const isLikedAfterMutation = fullBookToInject.likedByMe; // This is the state *after* the mutation execution context (already toggled by previous logic) - if (isLikedAfterMutation) { - // Make sure to add it as liked - updatedBooks = [{ ...fullBookToInject, likedByMe: true }, ...updatedBooks]; - } + updatedBooks = [{ ...fullBookToInject, likedByMe: true }, ...updatedBooks]; }Also applies to: 217-219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/mutations/useBookMutations.ts` around lines 145 - 156, The optimistic update fails to keep the likedBooks cache consistent because fullBookToInject is captured pre-mutation (so fullBookToInject.likedByMe is stale); change the logic that decides whether to inject the book into the first page to use the intended post-toggle state (i.e., compute isLikedAfterMutation as the toggled value, for example using the isbn-targeting map result or !fullBookToInject.likedByMe / !current value) instead of reading fullBookToInject.likedByMe, and when unliking ensure you remove or update the book in the likedBooks cache (updatedBooks) accordingly; update the branch that mutates updatedBooks (in the function handling optimistic toggle where variables index, isMyLibrary, isAlreadyInLibrary, fullBookToInject, updatedBooks and likedByMe appear) so the inject and removal behavior uses the computed isLikedAfterMutation to keep "내 서재" consistent without requiring an immediate refetch.
🧹 Nitpick comments (1)
src/components/layout/NotificationDropdown.tsx (1)
13-15: ⚡ Quick winSubscribe only to
isLoggedInfrom the auth store.
useAuthStore()here subscribes the dropdown to the entire auth state, so unrelated auth-store updates will rerender a layout-level component and re-evaluate the notification query wiring. A selector keeps this scoped to the one field the component actually uses. Please verify this against your current Zustand selector guidance.♻️ Suggested change
- const { isLoggedIn } = useAuthStore(); + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const { data: notificationsData, isLoading } = useNotificationPreviewQuery(5, isLoggedIn); - const notifications = isLoggedIn ? notificationsData : []; const { mutate: readNotification } = useReadNotificationMutation(); + const notifications = isLoggedIn ? notificationsData : []; + const { mutate: readNotification } = useReadNotificationMutation();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/layout/NotificationDropdown.tsx` around lines 13 - 15, The component currently calls useAuthStore() without a selector causing the whole auth store to be subscribed; change it to select only isLoggedIn (e.g., const isLoggedIn = useAuthStore(state => state.isLoggedIn)) so NotificationDropdown only re-renders when that boolean changes, and leave useNotificationPreviewQuery(5, isLoggedIn) and const { mutate: readNotification } = useReadNotificationMutation() as-is; update the import/usages in NotificationDropdown.tsx accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/base-ui/Profile/Follow/FollowItem.tsx`:
- Around line 24-27: The delete handler allows duplicate calls because it only
checks isDeleted; add a local in-flight lock (e.g., isDeleting state or ref) and
include it in the guard for handleDelete so repeated clicks are ignored while a
delete is in progress: in FollowItem.tsx update the handleDelete function to
check both isDeleted and isDeleting, set isDeleting = true before invoking
onDelete(user.nickname), and keep it true until the parent finishes (if onDelete
returns a Promise await it and then clear or leave flagged if parent removes the
item); apply the same pattern to the other similar delete/follow handlers
referenced (lines ~45-58).
In `@src/hooks/mutations/useMemberMutations.ts`:
- Around line 335-343: Replace the explicit any in the onError handler with the
ApiError type: import ApiError from "@/lib/api/ApiError" and change the onError
signature to use (error: ApiError, nickname, context) so the code can safely
access error.response?.data?.message and error.message; update the import list
at the top of useMemberMutations.ts to include ApiError and ensure no other any
remains in the onError callback for delete-follower logic (the function name to
edit is the onError handler within useMemberMutations).
- Around line 291-292: The followers/followings invalidation is set to
refetchType: 'none' which leaves optimistic updates in place when
useToggleFollowMutation's mutationFn early-returns due to its throttle check;
either move the throttle guard from mutationFn (the early-return in
useToggleFollowMutation.mutationFn) into onMutate so throttled clicks never
apply optimistic changes, or remove refetchType: 'none' for the
queryClient.invalidateQueries calls that reference memberKeys.followers() and
memberKeys.followings() in the onSettled block so those caches are refetched on
settle and cannot remain stale after a throttled mutation.
- Around line 305-333: The onSuccess handler in useDeleteFollowerMutation
currently only invalidates memberKeys.followers(), leaving the follower count
stale; update the onSuccess (and/or the finalization) to also invalidate
memberKeys.followCount() so components using useFollowCountQuery() refresh —
locate useDeleteFollowerMutation and add queryClient.invalidateQueries({
queryKey: memberKeys.followCount(), refetchType: "none" }) alongside the
existing invalidate for memberKeys.followers().
---
Outside diff comments:
In `@src/hooks/mutations/useBookMutations.ts`:
- Around line 145-156: The optimistic update fails to keep the likedBooks cache
consistent because fullBookToInject is captured pre-mutation (so
fullBookToInject.likedByMe is stale); change the logic that decides whether to
inject the book into the first page to use the intended post-toggle state (i.e.,
compute isLikedAfterMutation as the toggled value, for example using the
isbn-targeting map result or !fullBookToInject.likedByMe / !current value)
instead of reading fullBookToInject.likedByMe, and when unliking ensure you
remove or update the book in the likedBooks cache (updatedBooks) accordingly;
update the branch that mutates updatedBooks (in the function handling optimistic
toggle where variables index, isMyLibrary, isAlreadyInLibrary, fullBookToInject,
updatedBooks and likedByMe appear) so the inject and removal behavior uses the
computed isLikedAfterMutation to keep "내 서재" consistent without requiring an
immediate refetch.
In `@src/utils/url.ts`:
- Around line 4-25: The isValidUrl type guard currently accepts any absolute URL
scheme; update it so it only returns true for relative paths starting with "/" ,
protocol-relative URLs starting with "//" (validate by prefixing "https:"), or
absolute URLs whose URL.prototype.protocol is exactly "http:" or "https:". Keep
the existing null/empty checks (including trimming) and the special-case
handling for strings starting with "/", but after constructing new URL(url)
verify urlObj.protocol === "http:" || urlObj.protocol === "https:"; for the "//"
branch construct new URL(`https:${url}`) and treat that as valid only if it
parses successfully.
---
Nitpick comments:
In `@src/components/layout/NotificationDropdown.tsx`:
- Around line 13-15: The component currently calls useAuthStore() without a
selector causing the whole auth store to be subscribed; change it to select only
isLoggedIn (e.g., const isLoggedIn = useAuthStore(state => state.isLoggedIn))
so NotificationDropdown only re-renders when that boolean changes, and leave
useNotificationPreviewQuery(5, isLoggedIn) and const { mutate: readNotification
} = useReadNotificationMutation() as-is; update the import/usages in
NotificationDropdown.tsx accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ed7fb9b4-9eed-40d2-8e2f-32f8a65b9a87
📒 Files selected for processing (44)
src/app/(admin)/admin/(app)/stories/[id]/page.tsxsrc/app/(main)/profile/[nickname]/follows/page.tsxsrc/app/(main)/profile/mypage/follows/page.tsxsrc/app/(main)/stories/[id]/page.tsxsrc/app/groups/[id]/admin/applicant/page.tsxsrc/app/groups/[id]/admin/members/page.tsxsrc/app/groups/[id]/bookcase/page.tsxsrc/app/groups/[id]/layout.tsxsrc/app/groups/[id]/notice/page.tsxsrc/app/groups/[id]/page.tsxsrc/components/base-ui/Admin/stories/bookstory_detail.tsxsrc/components/base-ui/Admin/stories/comment_section.tsxsrc/components/base-ui/Admin/users/items/AdminBookStoryCard.tsxsrc/components/base-ui/Admin/users/items/AdminReportItem.tsxsrc/components/base-ui/BookStory/Common/bookstory_card.tsxsrc/components/base-ui/BookStory/Detatil/bookstory_detail.tsxsrc/components/base-ui/Bookcase/Admin/bookdetailgrouping/MemberPool.tsxsrc/components/base-ui/Bookcase/bookid/TeamMemberItem.tsxsrc/components/base-ui/Comment/comment_item.tsxsrc/components/base-ui/Comment/comment_section_bookcase 2.tsxsrc/components/base-ui/Comment/comment_section_bookcase.tsxsrc/components/base-ui/Join/steps/ProfileImage/ProfileImageUploader.tsxsrc/components/base-ui/Join/steps/ProfileImage/useProfileImage.tssrc/components/base-ui/MyPage/ProfileSection/UserProfile.tsxsrc/components/base-ui/News/NewsListBanner.tsxsrc/components/base-ui/Profile/Follow/FollowItem.tsxsrc/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsxsrc/components/base-ui/Settings/EditProfile/ProfileImageSection.tsxsrc/components/base-ui/Settings/Report/ReportItem.tsxsrc/components/base-ui/home/Recommendation/list_subscribe_element.tsxsrc/components/base-ui/home/Recommendation/list_subscribe_large.tsxsrc/components/base-ui/home/Subscription/list_subscribe_element.tsxsrc/components/base-ui/home/Subscription/list_subscribe_large.tsxsrc/components/layout/AdminHeader.tsxsrc/components/layout/Header.tsxsrc/components/layout/NotificationDropdown.tsxsrc/constants/images.tssrc/hooks/mutations/useBookMutations.tssrc/hooks/mutations/useMemberMutations.tssrc/hooks/mutations/useStoryMutations.tssrc/hooks/queries/useNotificationQueries.tssrc/types/member.tssrc/types/news.tssrc/utils/url.ts
| onError: (error: any, nickname, context) => { | ||
| console.error("Failed to delete follower:", error); | ||
| const errorMessage = error.response?.data?.message || error.message || "구독자 삭제에 실패했습니다."; | ||
| toast.error(errorMessage); | ||
|
|
||
| // 에러 발생 시 이전 상태로 롤백 | ||
| if (context?.previousFollowers) { | ||
| queryClient.setQueryData(memberKeys.followers(), context.previousFollowers); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "(useMemberMutations|eslint)" | head -20Repository: checkmo2025/FE
Length of output: 118
🏁 Script executed:
cat -n src/hooks/mutations/useMemberMutations.ts | sed -n '330,350p'Repository: checkmo2025/FE
Length of output: 978
🏁 Script executed:
fd -type f -name '.eslintrc*' -o -name 'eslint.config.*'Repository: checkmo2025/FE
Length of output: 229
🏁 Script executed:
find . -name '.eslintrc*' -o -name 'eslint.config.*' 2>/dev/nullRepository: checkmo2025/FE
Length of output: 10098
🏁 Script executed:
cat eslint.config.mjs | head -100Repository: checkmo2025/FE
Length of output: 523
🏁 Script executed:
head -30 src/hooks/mutations/useMemberMutations.tsRepository: checkmo2025/FE
Length of output: 1316
🏁 Script executed:
rg "axios|fetch|http" src/hooks/mutations/useMemberMutations.ts | head -20Repository: checkmo2025/FE
Length of output: 440
🏁 Script executed:
cat -n src/services/memberService.ts | head -50Repository: checkmo2025/FE
Length of output: 2641
🏁 Script executed:
rg "AxiosError|Error\)" src/hooks/mutations/ -A 2 -B 2Repository: checkmo2025/FE
Length of output: 485
🏁 Script executed:
grep -r "error.response" src/hooks/mutations/ --include="*.ts" --include="*.tsx"Repository: checkmo2025/FE
Length of output: 1079
🏁 Script executed:
cat -n src/lib/api/client.ts | head -80Repository: checkmo2025/FE
Length of output: 2987
🏁 Script executed:
rg "AxiosError|import.*Error" src/lib/api/ -A 2Repository: checkmo2025/FE
Length of output: 472
🏁 Script executed:
grep -r "onError.*error" src/hooks/mutations/useAuthMutations.ts -A 3 | head -40Repository: checkmo2025/FE
Length of output: 572
🏁 Script executed:
cat -n src/lib/api/ApiError.tsRepository: checkmo2025/FE
Length of output: 395
🏁 Script executed:
cat -n src/lib/api/client.ts | sed -n '80,120p'Repository: checkmo2025/FE
Length of output: 1854
🏁 Script executed:
rg "import.*ApiError" src/hooks/mutations/useMemberMutations.tsRepository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
rg "import.*ApiError" src/hooks/mutations/Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
cat -n src/hooks/mutations/useMemberMutations.ts | sed -n '1,20p'Repository: checkmo2025/FE
Length of output: 922
🏁 Script executed:
rg "onError.*error.*:" src/hooks/mutations/useMemberMutations.ts -B 3 -A 5Repository: checkmo2025/FE
Length of output: 2623
Replace the any type with ApiError in this error handler.
Line 335 uses any which violates the @typescript-eslint/no-explicit-any rule enabled in your ESLint configuration. Import ApiError from @/lib/api/ApiError and use it as the error parameter type instead—it properly matches the code's access pattern for error.response?.data?.message and error.message.
🧰 Tools
🪛 ESLint
[error] 335-335: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/mutations/useMemberMutations.ts` around lines 335 - 343, Replace
the explicit any in the onError handler with the ApiError type: import ApiError
from "@/lib/api/ApiError" and change the onError signature to use (error:
ApiError, nickname, context) so the code can safely access
error.response?.data?.message and error.message; update the import list at the
top of useMemberMutations.ts to include ApiError and ensure no other any remains
in the onError callback for delete-follower logic (the function name to edit is
the onError handler within useMemberMutations).
…ation refactor: 레이아웃 파일에 대한 main 태그 HTML 표준에 맞도록 수정
|
@coderabbitai |
- Hero 배경: landing-first-background.svg - 홈 섹션: landing-second-screen.svg (메인) + carrecel/bookstory 플로팅 카드 배치 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 캐러셀 우하단, 북스토리 좌측 배치로 변경 - 두 플로팅 카드 opacity 제거 (완전 불투명) - 북스토리 카드 위치 상향 조정 - 다크 패널 색상 #3C2A22 적용 - 캐러셀 파일명 오타 수정 (carrecel → carousel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 북스토리 카드 축소 (48% → 36%) - 캐러셀 카드 확대 (52% → 78%) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 캐러셀 카드 위치 조정 (우측 상단) - 북스토리/캐러셀 opacity-70 적용 - public/landing/ 에 club 섹션 이미지 4개 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 배경 이미지 적용 (landing-third-backgroud.svg) - 메인 스크린 + 생성/가입 버튼 오버레이 - 플로팅 클럽 카드 6장 섹션 edge 잘림 효과 적용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 책 이야기: 좌측 텍스트 + 우측 스크린 + 책 스택 데코 - 소식: 배경 풀블리드 + 뉴스 카드 3장 cascade 쌓임 효과 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 좌측: 텍스트 + 폰 프레임 안 모바일 화면 - 우측: 신청 관리 / 투표 화면 2장 + screen3 투표 항목 오버레이 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 배경: landing-seventh-background.svg - 상단 책 아이콘: landing-seventh-book.svg - screen1 (앱 화면) + screen2 (한줄평 팝업) 배치 - 한줄평 작성 소개 텍스트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /landing 경로로 랜딩 페이지 분리, /home으로 메인 홈 라우트 생성 - 루트(/)에서 localStorage 기반 seen 체크 후 조건부 리다이렉트 (30일 주기) - 랜딩 진입 시 seen_landing_v1 타임스탬프 기록 - LandingNav 텍스트/링크 변경: 로그인→바로가기(/home), 지금 가입하기→로그인/회원가입(/login) - LandingChatSection: bookcard1 오버레이, comment1~4 스택, screen3+select1/2 발제 섹션 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LandingChatSection: 채팅 기능 활용 섹션 추가 (screen4/5, chat1-4, group1, 점선 연결, chat-input) - LandingChatSection: 발제 섹션 select 크기 확대, 채팅 버블 좌우 정렬 - LandingHero: 지금 가입하기 클릭 시 /home 이동 + 로그인 모달 오픈 - LandingHomeSection: 대각선 클립 수정 (왼쪽 아래→위쪽 중앙), 투명도 제거 - LandingManageSection: 폰 프레임 중앙 이동, 데스크탑 화면 오른쪽 배치 - LandingNav: 로그인/회원가입 클릭 시 /home 이동 + 로그인 모달 오픈 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- seventh → sixth 파일명 일괄 변경 (public/landing) - LandingChatSection: 채팅 버블 HTML로 교체 및 좌우 튀어나오는 효과 적용 - LandingHomeSection: 대각선 클립 수정 (아래 중앙-10% → 위 중앙+10%) - LandingManageSection: 데스크탑 화면 위치 조정, 모바일 화면 상단 이동 - LandingNewsSection: 뉴스 카드 순서 반전 (4→3→2→1) 및 위치 상향 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 모바일(375) / 태블릿(768) / 데스크탑 반응형 레이아웃 적용 - 각 섹션 모바일에서 설명 위 / 컨텐츠(스크린) 아래 구조로 변경 - LandingNav: 모바일 패딩 축소, 바로가기 숨김, 버튼 크기 조정 - LandingHomeSection: 모바일 전체 다크배경, 대각선 태블릿+만 표시 - LandingNewsSection: 모바일 텍스트 위/카드 아래 order 적용 - LandingManageSection: 모바일 심플 스택 레이아웃 추가 - LandingChatSection: 섹션별 모바일 레이아웃 분리, 하단 가입하기 버튼 추가 - 폰트 raw pixel 값 디자인 시스템 유틸리티로 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LandingManageSection: 모바일/태블릿/데스크탑 3단계 레이아웃 분리 - 모바일(~767): 화면만 수직 스택, 폰 이미지 숨김 - 태블릿(768~1440): 폰 + 화면 가로 배치, 같이 축소 - 데스크탑(1440+): 기존 absolute 복잡 레이아웃 유지 - LandingManageSection: 768~1024에서 absolute 레이아웃 제거 - LandingChatSection: 채팅 버블 컨테이너 배경 #EAE5E2 적용 - LandingChatSection: A조 헤더 텍스트 Gray 컬러로 변경 - LandingChatSection: 채팅 버블 텍스트 break-words, body_2_2 적용 - 전체 섹션 h2 타이틀 태블릿에서 subhead_1, 데스크탑에서만 headline_3 - 랜딩 하단 로고 + 지금 가입하기 버튼 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FadeInSection 컴포넌트 추가 (Framer Motion whileInView 기반) - 전 섹션 모바일(375)/태블릿(768)/데스크탑(1440) 반응형 레이아웃 구현 - 헤딩 폰트 통일: subhead_1 → d:headline_3 (데스크탑만 확대) - LandingManageSection 태블릿 레이아웃 겹침 문제 수정 - 채팅 버블 컨테이너 내부 배치로 반응형 깨짐 해결 - 랜딩 최하단 로고 + '지금 가입하기' 버튼 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: 랜딩페이지 작업
fix: 소식 캐러셀 이미지 크롭 기준을 왼쪽 상단으로 변경
feat: 책 이야기 이어쓰기 및 임시 저장 api 연동
|
@coderabbitai 리뷰 내용 중에서 중요하게 수정되어야 할 부분들에 대해서 새로운 이슈로 만들어주세요. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainPR 🏁 Script executed: Length of output: 45
🔗 #324 PR
(´。• ᵕ •。`)🐇 |
fix: critical 문제 해결
There was a problem hiding this comment.
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/app/(main)/stories/new/page.tsx (1)
55-60:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not always send
descriptionfor drafts.Line 58 always sends
description, even when draft content is empty. Since this field is optional, send it only when non-empty to avoid avoidable API validation failures in draft saves.🐛 Proposed fix
- createStoryMutation.mutate({ - isbn: selectedBook.isbn, - title, - description: detail, - status, - }, { + const trimmedTitle = title.trim(); + const trimmedDetail = detail.trim(); + + createStoryMutation.mutate({ + isbn: selectedBook.isbn, + title: trimmedTitle, + ...(trimmedDetail ? { description: trimmedDetail } : {}), + status, + }, {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(main)/stories/new/page.tsx around lines 55 - 60, The payload passed to createStoryMutation.mutate always includes description (detail) even for drafts; change the call in page.tsx so the object only includes the description property when detail is non-empty (e.g., build a payload object with isbn, title, status and conditionally add description if detail.trim() !== "" or if detail is defined); update the createStoryMutation.mutate invocation to send that conditional payload instead of always passing description so drafts without content don't send an empty description field.
🧹 Nitpick comments (2)
src/app/(main)/home/page.tsx (1)
43-43: ⚡ Quick winEncode nickname before building the dynamic profile route.
Line 43 should encode the path segment to prevent malformed routes for special characters in nicknames.
♻️ Proposed change
- onProfileClick={(nickname) => router.push(`/profile/${nickname}`)} + onProfileClick={(nickname) => router.push(`/profile/${encodeURIComponent(nickname)}`)}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(main)/home/page.tsx at line 43, The onProfileClick handler currently builds the dynamic route directly with router.push(`/profile/${nickname}`) which can break for special characters; change it to encode the nickname path segment (e.g., use encodeURIComponent on the nickname) before passing it to router.push so the route becomes router.push(`/profile/${encodedNickname}`), ensuring correct, safe URLs; update the onProfileClick usage accordingly.src/components/base-ui/Landing/LandingNav.tsx (1)
12-13: ⚡ Quick winUse a selector for auth-store subscription.
Line 12 currently subscribes to the full auth store. Select only
openLoginModalto avoid unnecessary re-renders on unrelated auth-state updates.♻️ Proposed change
- const { openLoginModal } = useAuthStore(); + const openLoginModal = useAuthStore((state) => state.openLoginModal);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/base-ui/Landing/LandingNav.tsx` around lines 12 - 13, The component currently calls useAuthStore() without a selector which subscribes to the entire auth store and may cause unnecessary re-renders; change the call in LandingNav to select only the openLoginModal property (e.g., useAuthStore(state => state.openLoginModal)) so the component subscribes only to that piece of state and avoids updates from unrelated auth changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/page.tsx`:
- Around line 13-16: The code reads localStorage.getItem(LANDING_KEY) into seen
and uses parseInt(seen, 10) to compute expiry, but parseInt can produce NaN
which makes NaN > EXPIRY_MS false and treats malformed values as not expired;
update the logic around parseInt(seen, 10) (the seen parsing/expiry check in
page.tsx) to first parse into a number (e.g., const ts = parseInt(seen, 10)) and
use Number.isFinite(ts) (or Number.isFinite(parseInt(...))) to detect malformed
values and treat them as expired when false—only consider the timestamp valid
when Number.isFinite(ts) && (Date.now() - ts) <= EXPIRY_MS so malformed or
non-finite values route to the landing flow instead of /home.
In `@src/hooks/mutations/useMemberMutations.ts`:
- Around line 159-171: The throttle currently lives in onMutate but that only
affects optimistic updates; move the 500ms guard into the mutation function so
API calls are prevented: inside the existing mutationFn (the async ({ nickname,
isFollowing }) => { ... }) check followThrottleMap[nickname] and Date.now(),
return early if within 500ms, otherwise set followThrottleMap[nickname] = now
and then call memberService.followMember or memberService.unfollowMember as
appropriate; keep onMutate for optimistic state updates but remove/adjust its
throttle so it no longer needs to block API calls.
---
Outside diff comments:
In `@src/app/`(main)/stories/new/page.tsx:
- Around line 55-60: The payload passed to createStoryMutation.mutate always
includes description (detail) even for drafts; change the call in page.tsx so
the object only includes the description property when detail is non-empty
(e.g., build a payload object with isbn, title, status and conditionally add
description if detail.trim() !== "" or if detail is defined); update the
createStoryMutation.mutate invocation to send that conditional payload instead
of always passing description so drafts without content don't send an empty
description field.
---
Nitpick comments:
In `@src/app/`(main)/home/page.tsx:
- Line 43: The onProfileClick handler currently builds the dynamic route
directly with router.push(`/profile/${nickname}`) which can break for special
characters; change it to encode the nickname path segment (e.g., use
encodeURIComponent on the nickname) before passing it to router.push so the
route becomes router.push(`/profile/${encodedNickname}`), ensuring correct, safe
URLs; update the onProfileClick usage accordingly.
In `@src/components/base-ui/Landing/LandingNav.tsx`:
- Around line 12-13: The component currently calls useAuthStore() without a
selector which subscribes to the entire auth store and may cause unnecessary
re-renders; change the call in LandingNav to select only the openLoginModal
property (e.g., useAuthStore(state => state.openLoginModal)) so the component
subscribes only to that piece of state and avoids updates from unrelated auth
changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b9ff1ed9-faa0-46a5-b217-26796875cc03
⛔ Files ignored due to path filters (44)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/landing/landing-fifth-screen.svgis excluded by!**/*.svgpublic/landing/landing-fifth-screen1.svgis excluded by!**/*.svgpublic/landing/landing-fifth-screen2.svgis excluded by!**/*.svgpublic/landing/landing-fifth-screen3.svgis excluded by!**/*.svgpublic/landing/landing-first-background.svgis excluded by!**/*.svgpublic/landing/landing-fourth-background.svgis excluded by!**/*.svgpublic/landing/landing-fourth-books.svgis excluded by!**/*.svgpublic/landing/landing-fourth-news1.svgis excluded by!**/*.svgpublic/landing/landing-fourth-news2.svgis excluded by!**/*.svgpublic/landing/landing-fourth-news3.svgis excluded by!**/*.svgpublic/landing/landing-fourth-news4.svgis excluded by!**/*.svgpublic/landing/landing-fourth-screen.svgis excluded by!**/*.svgpublic/landing/landing-second-bookstory.svgis excluded by!**/*.svgpublic/landing/landing-second-carousel.svgis excluded by!**/*.svgpublic/landing/landing-second-screen.svgis excluded by!**/*.svgpublic/landing/landing-sixth-background.svgis excluded by!**/*.svgpublic/landing/landing-sixth-book.svgis excluded by!**/*.svgpublic/landing/landing-sixth-bookcard1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-chat-input.svgis excluded by!**/*.svgpublic/landing/landing-sixth-chat1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-chat2.svgis excluded by!**/*.svgpublic/landing/landing-sixth-chat3.svgis excluded by!**/*.svgpublic/landing/landing-sixth-chat4.svgis excluded by!**/*.svgpublic/landing/landing-sixth-comment1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-comment2.svgis excluded by!**/*.svgpublic/landing/landing-sixth-comment3.svgis excluded by!**/*.svgpublic/landing/landing-sixth-comment4.svgis excluded by!**/*.svgpublic/landing/landing-sixth-group1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-screen1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-screen2.svgis excluded by!**/*.svgpublic/landing/landing-sixth-screen3.svgis excluded by!**/*.svgpublic/landing/landing-sixth-screen4.svgis excluded by!**/*.svgpublic/landing/landing-sixth-screen5.svgis excluded by!**/*.svgpublic/landing/landing-sixth-select1.svgis excluded by!**/*.svgpublic/landing/landing-sixth-select2.svgis excluded by!**/*.svgpublic/landing/landing-third-backgroud.svgis excluded by!**/*.svgpublic/landing/landing-third-club1.svgis excluded by!**/*.svgpublic/landing/landing-third-club2.svgis excluded by!**/*.svgpublic/landing/landing-third-club3.svgis excluded by!**/*.svgpublic/landing/landing-third-club4.svgis excluded by!**/*.svgpublic/landing/landing-third-create-button.svgis excluded by!**/*.svgpublic/landing/landing-third-join-button.svgis excluded by!**/*.svgpublic/landing/landing-third-screen.svgis excluded by!**/*.svg
📒 Files selected for processing (27)
src/app/(main)/home/page.tsxsrc/app/(main)/stories/[id]/edit/page.tsxsrc/app/(main)/stories/new/page.tsxsrc/app/(public)/find-account/page.tsxsrc/app/landing/page.tsxsrc/app/page.tsxsrc/components/base-ui/BookStory/Common/BookStoryInfiniteList.tsxsrc/components/base-ui/BookStory/Common/bookstory_card.tsxsrc/components/base-ui/Landing/FadeInSection.tsxsrc/components/base-ui/Landing/LandingChatSection.tsxsrc/components/base-ui/Landing/LandingClubSection.tsxsrc/components/base-ui/Landing/LandingHero.tsxsrc/components/base-ui/Landing/LandingHomeSection.tsxsrc/components/base-ui/Landing/LandingManageSection.tsxsrc/components/base-ui/Landing/LandingNav.tsxsrc/components/base-ui/Landing/LandingNewsSection.tsxsrc/components/base-ui/Landing/LandingStorySection.tsxsrc/components/base-ui/Landing/shared/DesktopMockup.tsxsrc/components/base-ui/Landing/shared/MobileMockup.tsxsrc/components/base-ui/Profile/Follow/FollowItem.tsxsrc/components/base-ui/home/Banner/NewsBannerSlider.tsxsrc/components/base-ui/home/News/NewsBannerSlider.tsxsrc/hooks/mutations/useBookMutations.tssrc/hooks/mutations/useMemberMutations.tssrc/hooks/mutations/useStoryMutations.tssrc/types/story.tssrc/utils/url.ts
✅ Files skipped from review due to trivial changes (1)
- src/app/(public)/find-account/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- src/hooks/mutations/useBookMutations.ts
- src/components/base-ui/home/News/NewsBannerSlider.tsx
- src/components/base-ui/Profile/Follow/FollowItem.tsx
| const seen = localStorage.getItem(LANDING_KEY); | ||
| if (seen) { | ||
| const expired = Date.now() - parseInt(seen, 10) > EXPIRY_MS; | ||
| if (!expired) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/page.tsx | head -30Repository: checkmo2025/FE
Length of output: 787
🏁 Script executed:
node << 'EOF'
// Test 1: parseInt with invalid input
const seen = "invalid_value";
const result = parseInt(seen, 10);
console.log("parseInt('invalid_value', 10):", result);
console.log("NaN check:", result === NaN, Number.isNaN(result));
// Test 2: NaN comparison behavior
const EXPIRY_MS = 30 * 24 * 60 * 60 * 1000;
const expired = Date.now() - result > EXPIRY_MS;
console.log("NaN > EXPIRY_MS evaluates to:", expired);
// Test 3: Flow with current code
const seen2 = "not_a_number";
if (seen2) {
const expired2 = Date.now() - parseInt(seen2, 10) > EXPIRY_MS;
console.log("With malformed value, expired:", expired2);
console.log("With malformed value, !expired:", !expired2);
if (!expired2) {
console.log("WOULD ROUTE TO /home (INCORRECT)");
}
}
// Test 4: Verify the proposed fix
const seenAt = Number(seen2);
console.log("\nProposed fix - Number(seen):", seenAt);
console.log("Number.isFinite(NaN):", Number.isFinite(seenAt));
const expiredFixed = !Number.isFinite(seenAt) || Date.now() - seenAt > EXPIRY_MS;
console.log("With fix, expired:", expiredFixed);
console.log("With fix, !expired:", !expiredFixed);
if (!expiredFixed) {
console.log("WOULD ROUTE TO /home");
} else {
console.log("WOULD ROUTE TO /landing (CORRECT)");
}
// Test 5: Valid recent timestamp
const validRecent = String(Date.now());
const seenAtValid = Number(validRecent);
console.log("\nWith valid recent timestamp:", validRecent);
const expiredValidRecent = !Number.isFinite(seenAtValid) || Date.now() - seenAtValid > EXPIRY_MS;
console.log("Is expired:", expiredValidRecent);
console.log("Routes to:", !expiredValidRecent ? "/home" : "/landing");
EOFRepository: checkmo2025/FE
Length of output: 483
Treat malformed seen_landing_v1 values as expired.
At Line 15, parseInt(seen, 10) can be NaN; then NaN > EXPIRY_MS is false, so invalid storage values are treated as not expired and incorrectly route to /home. Use Number.isFinite() to detect and handle malformed values.
Proposed fix
useEffect(() => {
const seen = localStorage.getItem(LANDING_KEY);
if (seen) {
- const expired = Date.now() - parseInt(seen, 10) > EXPIRY_MS;
+ const seenAt = Number(seen);
+ const expired = !Number.isFinite(seenAt) || Date.now() - seenAt > EXPIRY_MS;
if (!expired) {
router.replace("/home");
return;
}
}📝 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 seen = localStorage.getItem(LANDING_KEY); | |
| if (seen) { | |
| const expired = Date.now() - parseInt(seen, 10) > EXPIRY_MS; | |
| if (!expired) { | |
| const seen = localStorage.getItem(LANDING_KEY); | |
| if (seen) { | |
| const seenAt = Number(seen); | |
| const expired = !Number.isFinite(seenAt) || Date.now() - seenAt > EXPIRY_MS; | |
| if (!expired) { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/page.tsx` around lines 13 - 16, The code reads
localStorage.getItem(LANDING_KEY) into seen and uses parseInt(seen, 10) to
compute expiry, but parseInt can produce NaN which makes NaN > EXPIRY_MS false
and treats malformed values as not expired; update the logic around
parseInt(seen, 10) (the seen parsing/expiry check in page.tsx) to first parse
into a number (e.g., const ts = parseInt(seen, 10)) and use Number.isFinite(ts)
(or Number.isFinite(parseInt(...))) to detect malformed values and treat them as
expired when false—only consider the timestamp valid when Number.isFinite(ts) &&
(Date.now() - ts) <= EXPIRY_MS so malformed or non-finite values route to the
landing flow instead of /home.
| mutationFn: async ({ nickname, isFollowing }: { nickname: string; isFollowing: boolean }) => { | ||
| const now = Date.now(); | ||
| const lastTime = followThrottleMap[nickname] || 0; | ||
|
|
||
| // Throttle: 500ms | ||
| if (now - lastTime < 500) { | ||
| return; // Ignore rapid clicks | ||
| } | ||
| followThrottleMap[nickname] = now; | ||
|
|
||
| if (isFollowing) { | ||
| await memberService.unfollowMember(nickname); | ||
| } else { | ||
| await memberService.followMember(nickname); | ||
| } | ||
| }, | ||
| onMutate: async ({ nickname, isFollowing }) => { | ||
| // Throttle: 500ms — 낙관적 업데이트 자체를 막아 캐시 고착 방지 | ||
| const now = Date.now(); | ||
| const lastTime = followThrottleMap[nickname] || 0; | ||
| if (now - lastTime < 500) return; | ||
| followThrottleMap[nickname] = now; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify throttle location: should exist in mutationFn, not only onMutate.
cat -n src/hooks/mutations/useMemberMutations.ts | sed -n '155,210p'Repository: checkmo2025/FE
Length of output: 3345
🏁 Script executed:
#!/bin/bash
# Check if there are any debounce/throttle wrappers around the mutation call
rg "useToggleFollowMutation|toggleFollowMutation" --context=5 -A 10Repository: checkmo2025/FE
Length of output: 16454
🏁 Script executed:
#!/bin/bash
# Check the entire file for throttle-related logic
rg "throttle|debounce|500" src/hooks/mutations/useMemberMutations.ts -nRepository: checkmo2025/FE
Length of output: 170
🏁 Script executed:
#!/bin/bash
# Check if followThrottleMap is used elsewhere or has other safeguards
rg "followThrottleMap" src/hooks/mutations/useMemberMutations.ts -B 2 -A 2Repository: checkmo2025/FE
Length of output: 514
Throttle currently does not prevent API calls—only optimistic updates are skipped.
The 500ms guard in onMutate (lines 167–171) does not prevent mutationFn (lines 159–165) from executing. React Query runs mutationFn regardless of early returns in onMutate, so rapid clicks still spam the follow/unfollow API.
Move the throttle check into mutationFn:
Fix
mutationFn: async ({ nickname, isFollowing }: { nickname: string; isFollowing: boolean }) => {
+ const now = Date.now();
+ const lastTime = followThrottleMap[nickname] || 0;
+ if (now - lastTime < 500) return;
+ followThrottleMap[nickname] = now;
+
if (isFollowing) {
await memberService.unfollowMember(nickname);
} else {
await memberService.followMember(nickname);
}
},
onMutate: async ({ nickname, isFollowing }) => {
- // Throttle: 500ms — 낙관적 업데이트 자체를 막아 캐시 고착 방지
- const now = Date.now();
- const lastTime = followThrottleMap[nickname] || 0;
- if (now - lastTime < 500) return;
- followThrottleMap[nickname] = now;
-
// Cancel outgoing refetches🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/mutations/useMemberMutations.ts` around lines 159 - 171, The
throttle currently lives in onMutate but that only affects optimistic updates;
move the 500ms guard into the mutation function so API calls are prevented:
inside the existing mutationFn (the async ({ nickname, isFollowing }) => { ...
}) check followThrottleMap[nickname] and Date.now(), return early if within
500ms, otherwise set followThrottleMap[nickname] = now and then call
memberService.followMember or memberService.unfollowMember as appropriate; keep
onMutate for optimistic state updates but remove/adjust its throttle so it no
longer needs to block API calls.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit
New Features
Bug Fixes
Style
Documentation
Chores