Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Walkthrough모달 오버레이 불투명도 조정, BookInfoCard 클릭 경로를 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SearchPage
participant IntersectionObserver
participant API
User->>SearchPage: 검색 실행
SearchPage->>API: getSearchBooks(page=1)
API-->>SearchPage: 결과(최대 페이지 크기)
SearchPage->>IntersectionObserver: 마지막 아이템 observe
IntersectionObserver-->>SearchPage: 교차 이벤트(보임)
SearchPage->>API: getSearchBooks(page=next)
API-->>SearchPage: 추가 결과
SearchPage->>IntersectionObserver: 새 마지막 아이템 재-observe
sequenceDiagram
participant User
participant ProfileItem
participant FollowAPI
participant PopupStore
User->>ProfileItem: 팔로우/언팔로우 클릭
ProfileItem->>FollowAPI: 요청 전송
FollowAPI-->>ProfileItem: 성공 응답(상태 토글)
ProfileItem->>PopupStore: openPopup('snackbar', {message, variant:'top'})
PopupStore-->>User: 스낵바 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
src/components/common/Modal/ReplyModal.tsx (1)
79-87: 자기 댓글/타인 댓글 분기 조건이 반대로 적용됨 (삭제/신고가 뒤바뀜)isMyReply가 true일 때는 "삭제하기"가, false일 때는 "신고하기"가 보여야 합니다. 현재 반대로 되어 있어 다른 사람 댓글을 삭제할 수 있는 UI가 노출됩니다. 아래와 같이 분기 및 클래스/클릭 핸들러를 교정해 주세요.
- {isMyReply ? ( - <OptionItem className="report"> - <div className="option-text">신고하기</div> - </OptionItem> - ) : ( - <OptionItem onClick={handleDelete} className="delete"> - <div className="option-text">삭제하기</div> - </OptionItem> - )} + {isMyReply ? ( + <OptionItem onClick={handleDelete} className="delete"> + <div className="option-text">삭제하기</div> + </OptionItem> + ) : ( + <OptionItem className="report"> + <div className="option-text">신고하기</div> + </OptionItem> + )}
🧹 Nitpick comments (12)
src/components/common/Modal/ReplyModal.tsx (1)
114-116: 반투명 컨테이너 배경은 대비 저하 가능성 — 실색 배경 유지 권장컨텍스트 메뉴(드롭다운/팝오버) 성격의 컨테이너 배경을 반투명으로 변경하면 텍스트 대비가 떨어질 수 있습니다. 특히 다양한 페이지 배경색 위에서 가독성 저하가 발생할 수 있어, 실색 배경(colors.black.main) 유지가 안전합니다.
- background-color: rgba(18, 18, 18, 0.3); + background-color: ${colors.black.main};src/components/feed/UserProfileItem.tsx (1)
22-36: 중복 클릭/레이스 가드 및 실패 시 스낵바 추가 권장빠른 연타 시 중복 요청/레이스가 발생할 수 있습니다. 요청 중 가드를 두고 실패 시에도 스낵바로 안내하면 UX가 좋아집니다.
const [followed, setFollowed] = useState(isFollowing); + const [isPending, setIsPending] = useState(false); const { openPopup } = usePopupStore(); const toggleFollow = async (e: React.MouseEvent) => { e.stopPropagation(); try { + if (isPending) return; + setIsPending(true); const response = await postFollow(userId, !followed); // API 응답으로 팔로우 상태 업데이트 setFollowed(response.data.isFollowing); console.log(`${nickname} - ${response.data.isFollowing ? '띱 완료' : '띱 취소'}`); // Snackbar 표시 const message = response.data.isFollowing ? `${nickname}님을 띱 했어요.` : `${nickname}님을 띱 취소했어요.`; openPopup('snackbar', { message, variant: 'top', onClose: () => {} }); } catch (error) { console.error('팔로우/언팔로우 실패:', error); + openPopup('snackbar', { + message: '팔로우/언팔로우에 실패했어요. 잠시 후 다시 시도해주세요.', + variant: 'top', + onClose: () => {} + }); + } finally { + setIsPending(false); } };Also applies to: 38-49, 66-68
src/components/feed/Profile.tsx (1)
30-35: 초깃값 불리언 캐스팅, 중복 클릭 가드, 실패 스낵바 권장isFollowing이 undefined일 수 있어 불리언 캐스팅으로 일관성 확보가 필요합니다. 또한 요청 중 중복 클릭 방지와 실패 시 스낵바 안내를 추가하면 안정성이 높아집니다.
- const [followed, setFollowed] = useState(isFollowing); + const [followed, setFollowed] = useState<boolean>(!!isFollowing); + const [isPending, setIsPending] = useState(false); const { openPopup } = usePopupStore(); useEffect(() => { - setFollowed(isFollowing); + setFollowed(!!isFollowing); }, [isFollowing]); const toggleFollow = async () => { if (!userId) { console.error('userId가 없습니다.'); return; } try { + if (isPending) return; + setIsPending(true); console.log('현재 팔로우 상태:', followed); console.log('요청할 타입:', !followed); // 현재 팔로우 상태의 반대값으로 API 호출 const response = await postFollow(userId, !followed); console.log('API 응답:', response); // API 응답으로 팔로우 상태 업데이트 setFollowed(response.data.isFollowing); console.log(`${nickname} - ${response.data.isFollowing ? '띱 완료' : '띱 취소'}`); // Snackbar 표시 const message = response.data.isFollowing ? `${nickname}님을 띱 했어요.` : `${nickname}님을 띱 취소했어요.`; openPopup('snackbar', { message, variant: 'top', onClose: () => {} }); } catch (error) { console.error('팔로우/언팔로우 실패:', error); // 에러 발생 시 상태 변경하지 않음 + openPopup('snackbar', { + message: '팔로우/언팔로우에 실패했어요. 잠시 후 다시 시도해주세요.', + variant: 'top', + onClose: () => {} + }); + } finally { + setIsPending(false); } };Also applies to: 37-69, 85-88
src/components/feed/BookInfoCard.tsx (1)
24-25: 장식용 아이콘 접근성 개선: alt 빈값/ARIA 숨김 추가장식용 화살표 이미지는 보조기기에 노이즈가 되므로 alt=""와 aria-hidden을 권장합니다.
- <img src={rightArrow} /> + <img src={rightArrow} alt="" aria-hidden="true" />src/components/search/BookSearchResult.tsx (4)
49-52: 작은 최적화: 더 불러올 항목이 없을 때는 ref 부착 생략last 아이템 ref 부착 전에 hasMore를 함께 체크하면, 더 이상 로드할 데이터가 없을 때 불필요한 IntersectionObserver 관찰을 방지할 수 있습니다. 현재 콜백 내부에서도 가드가 있지만, 불필요한 observe 자체를 줄이는 편이 낫습니다.
- ref={ - index === searchedBookList.length - 1 && lastBookElementCallback - ? lastBookElementCallback - : undefined - } + ref={ + index === searchedBookList.length - 1 && + hasMore && + lastBookElementCallback + ? lastBookElementCallback + : undefined + }
65-69: 로딩/마지막 상태가 빈 Fragment로 표시됨 — 사용자 피드백 UI 추가 권장하단 로딩 스피너/메시지와 “더 이상 결과 없음” 같은 피드백을 제공하면 UX가 개선됩니다.
- {isLoading && searchedBookList.length > 0 && <></>} + {isLoading && searchedBookList.length > 0 && <StatusRow>로딩 중...</StatusRow>} - {!hasMore && searchedBookList.length > 0 && <></>} + {!hasMore && searchedBookList.length > 0 && <StatusRow>마지막 결과입니다</StatusRow>}컴포넌트 하단에 추가(지원 코드):
const StatusRow = styled.div` display: flex; justify-content: center; align-items: center; padding: 12px 0; color: ${colors.grey[200]}; font-size: ${typography.fontSize.sm}; `;
45-53: 접근성: 클릭 가능한 div에 키보드 접근성 보강div에 onClick만 있으면 키보드 사용자가 접근하기 어렵습니다. role, tabIndex, Enter 키 핸들링을 추가해 주세요. (a/Link 사용이 더 이상적이지만, 최소한의 보강 방안 제안)
<BookItem key={book.isbn} onClick={() => navigate(`/search/book/${book.isbn}`)} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === 'Enter') { + navigate(`/search/book/${book.isbn}`); + } + }}
95-100: 카운트 라벨 문구 개선 제안“전체 N”은 전체 검색 결과 수로 오해될 수 있습니다. 현재는 ‘로딩된 항목 수’이므로 문구를 명확히 하면 좋겠습니다.
- {type === 'searching' ? <></> : <ResultHeader>전체 {searchedBookList.length}</ResultHeader>} + {type === 'searching' ? <></> : <ResultHeader>검색 결과 {searchedBookList.length}건</ResultHeader>}src/pages/search/Search.tsx (4)
42-45: 미사용 ref 제거로 간소화lastBookElementRef는 선언만 되고 실사용이 없습니다(할당만 1회). 제거해도 동작에 영향이 없습니다.
- const lastBookElementRef = useRef<HTMLDivElement | null>(null); ... - lastBookElementRef.current = node;Also applies to: 79-80
66-83: 무한 스크롤 콜백의 의존성/클로저 안정화 및 관찰 타이밍 개선
- lastBookElementCallback이 loadMore에 의존하지만 deps에 loadMore가 없어 ESLint(exhaustive-deps) 경고 및 잠재적 스테일 클로저 이슈가 발생할 수 있습니다.
- IntersectionObserver에 rootMargin/threshold를 지정해 ‘근접 사전 로드’가 가능하도록 하면 UX가 부드러워집니다.
- loadMore를 useCallback으로 래핑하고, append 시 isbn 기준으로 중복 제거를 권장합니다(서버 페이지 경계 중복 방지).
Observer 옵션 및 deps 보강:
observerRef.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasMore && !isLoadingMore) { loadMore(); } - }); + }, { root: null, rootMargin: '200px 0px', threshold: 0.01 }); ... - [isLoadingMore, hasMore], + [isLoadingMore, hasMore, loadMore],loadMore를 useCallback으로 전환 + 중복 제거:
- const loadMore = async () => { + const loadMore = useCallback(async () => { if (!searchTerm.trim() || isLoadingMore || !hasMore) return; try { setIsLoadingMore(true); const nextPage = page + 1; const response = await getSearchBooks(searchTerm, nextPage, isFinalized); if (response.isSuccess) { const newResults = convertToSearchedBooks(response.data.searchResult); if (newResults.length > 0) { - setSearchResults(prev => [...prev, ...newResults]); + // isbn 기준으로 중복 제거 + setSearchResults(prev => { + const seen = new Set(prev.map(b => b.isbn)); + const merged = [...prev]; + for (const b of newResults) { + if (!seen.has(b.isbn)) { + merged.push(b); + seen.add(b.isbn); + } + } + return merged; + }); setPage(nextPage); - // 더 이상 데이터가 없으면 hasMore를 false로 설정 - setHasMore(newResults.length === 10); // size가 10이므로 + // 더 이상 데이터가 없으면 hasMore를 false로 설정 + setHasMore(newResults.length === PAGE_SIZE); } else { setHasMore(false); } } else { console.error('추가 데이터 로드 실패:', response.message); setHasMore(false); } } catch (error) { console.error('추가 데이터 로드 중 오류 발생:', error); setHasMore(false); } finally { setIsLoadingMore(false); } - }; + }, [searchTerm, page, isFinalized, isLoadingMore, hasMore]);상단에 PAGE_SIZE 상수 정의는 아래 코멘트 참고.
Also applies to: 85-116
101-102: 매직 넘버(10) 상수화페이지 크기 10을 여러 곳에서 직접 사용하고 있습니다. 상수로 추출해 가독성/일관성을 높여주세요.
- setHasMore(newResults.length === 10); // size가 10이므로 + setHasMore(newResults.length === PAGE_SIZE); // size가 PAGE_SIZE이므로- setHasMore(convertedResults.length === 10); // size가 10이므로 + setHasMore(convertedResults.length === PAGE_SIZE); // size가 PAGE_SIZE이므로지원 코드(파일 상단 import 아래 적절한 위치에 추가):
const PAGE_SIZE = 10;Also applies to: 170-171
150-190: 검색 경쟁 상태(race condition) 방지 가드 권장빠르게 검색어가 바뀌거나 수동검색/자동검색이 뒤섞이면, 늦게 도착한 이전 요청의 응답이 최신 결과를 덮어쓸 수 있습니다. 요청 ID(useRef) 또는 AbortController로 “구식 응답 무시” 가드를 두는 것을 권장합니다.
원리 예시(지원 코드):
// 상단에 추가 const latestRequestIdRef = useRef(0); // handleSearch 내부 const requestId = ++latestRequestIdRef.current; try { const response = await getSearchBooks(term, 1, isManualSearch); if (latestRequestIdRef.current !== requestId) return; // 구식 응답 무시 ... } catch (e) { if (latestRequestIdRef.current !== requestId) return; ... }원하시면 AbortController 기반으로 apiClient 요청 취소 패턴으로도 변환해 드릴게요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
src/components/common/Modal/PopupContainer.tsx(1 hunks)src/components/common/Modal/ReplyModal.tsx(1 hunks)src/components/feed/BookInfoCard.tsx(1 hunks)src/components/feed/Profile.tsx(3 hunks)src/components/feed/UserProfileItem.tsx(3 hunks)src/components/search/BookSearchResult.tsx(4 hunks)src/pages/search/Search.tsx(9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/feed/UserProfileItem.tsx (1)
src/stores/usePopupStore.ts (1)
usePopupStore(51-57)
src/components/feed/Profile.tsx (1)
src/stores/usePopupStore.ts (1)
usePopupStore(51-57)
src/pages/search/Search.tsx (3)
src/api/recentsearch/getRecentSearch.ts (1)
RecentSearchData(7-10)src/api/books/getSearchBooks.ts (2)
getSearchBooks(41-59)convertToSearchedBooks(61-70)src/components/search/BookSearchResult.tsx (1)
BookSearchResult(14-73)
src/components/search/BookSearchResult.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
BookItem(165-177)
🔇 Additional comments (6)
src/components/common/Modal/PopupContainer.tsx (1)
72-75: 오버레이 투명도 상향 일관성 확보, OK모달 오버레이를 rgba(18, 18, 18, 0.3)로 올려 시각적 집중도를 높인 점 좋습니다. ReplyModal과의 일관성도 확보됩니다.
src/components/feed/UserProfileItem.tsx (1)
38-47: 팔로우/언팔로우 후 스낵바 피드백 도입, UX 향상 👍API 응답에 따라 메시지를 분기해 상단 스낵바로 즉시 피드백을 주는 흐름이 명확하고 일관적입니다.
src/components/feed/Profile.tsx (1)
56-65: 팔로우/언팔로우 스낵바 피드백 추가, OK상태 업데이트 직후 사용자에게 명확한 피드백을 제공해 상호작용성이 좋아졌습니다.
src/components/feed/BookInfoCard.tsx (1)
14-16: 루트 설정에/search/book/:isbn경로 정의 확인 필요자동 스크립트 검색 결과 해당 경로가 발견되지 않아, 아래 파일들에서 수동으로 검증 부탁드립니다:
src/routes.ts또는routes.tsx(객체 스타일 경로 정의)src/App.tsx등<Route path="/search/book/:isbn" …>JSX 사용 위치- 기타 커스텀 라우터 설정 파일
경로가 없다면 검색 상세 페이지로 네비게이션이 동작하지 않으니, 정의를 추가하거나 올바른 파일 위치로 이동시켜 주세요.
src/components/search/BookSearchResult.tsx (1)
47-47: 라우팅 경로 확인 필요
/search/book/${book.isbn}로 이동하는 링크(/search/book/:isbn)가 실제 라우터 설정에 등록되어 있는지 확인해주세요. 등록되지 않으면 클릭 시 빈 화면 또는 404가 발생할 수 있습니다.확인 방법 예시:
rg -nF '/search/book/:isbn' --glob '*.tsx' --glob '*.ts' --glob '*.jsx' --glob '*.js'결과가 없다면
src/routes.tsx,App.tsx(또는 라우트 정의 파일)에 다음과 같이 경로를 추가해주세요:<Route path="/search/book/:isbn" element={<BookDetail />} />src/pages/search/Search.tsx (1)
287-296: 무한 스크롤 연동 전반적으로 잘 구성되었습니다초기 로딩 분기, 하위 컴포넌트에 hasMore/isLoading/lastBookElementCallback 전달 흐름 모두 자연스럽습니다. 상단 개선 사항만 보완하면 안정적으로 동작할 것으로 보입니다.
| searchedBookList.map((book, index) => ( | ||
| <BookItem | ||
| key={book.id} | ||
| onClick={() => navigate(`/search/book/${book.isbn}`)} | ||
| ref={ | ||
| index === searchedBookList.length - 1 && lastBookElementCallback | ||
| ? lastBookElementCallback | ||
| : undefined | ||
| } | ||
| > |
There was a problem hiding this comment.
key 충돌 가능성: 페이지 append 시 동일 id 재사용 → key를 isbn으로 전환 권장
convertToSearchedBooks가 각 페이지 내 index 기반 id를 부여하고 있습니다. 무한 스크롤에서 페이지 단위로 결과를 append하면 id가 중복되어 React key 충돌이 발생할 수 있어 리스트 업데이트/관찰자(ref) 부착이 오작동할 수 있습니다. 안정적인 고유키인 isbn을 key로 사용해 주세요.
아래처럼 변경을 제안드립니다:
- <BookItem
- key={book.id}
+ <BookItem
+ key={book.isbn}
onClick={() => navigate(`/search/book/${book.isbn}`)}📝 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.
| searchedBookList.map((book, index) => ( | |
| <BookItem | |
| key={book.id} | |
| onClick={() => navigate(`/search/book/${book.isbn}`)} | |
| ref={ | |
| index === searchedBookList.length - 1 && lastBookElementCallback | |
| ? lastBookElementCallback | |
| : undefined | |
| } | |
| > | |
| searchedBookList.map((book, index) => ( | |
| <BookItem | |
| key={book.isbn} | |
| onClick={() => navigate(`/search/book/${book.isbn}`)} | |
| ref={ | |
| index === searchedBookList.length - 1 && lastBookElementCallback | |
| ? lastBookElementCallback | |
| : undefined | |
| } | |
| > |
🤖 Prompt for AI Agents
In src/components/search/BookSearchResult.tsx around lines 44-53, the current
key uses book.id (which convertToSearchedBooks assigns per-page/index and can
duplicate when appending pages); change the key prop to use the stable unique
identifier book.isbn instead (i.e. key={book.isbn}) so React sees unique items
across appended pages; if some items may lack isbn, fallback to a deterministic
unique string that includes page/index (e.g. `${book.isbn ??
`${book.id}-${index}-${page}`}`) to avoid collisions; keep the ref and onClick
logic unchanged.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (9)
src/pages/signup/SignupGenre.tsx (3)
46-51: fetch 취소 처리 및 에러 로깅 보완 제안컴포넌트 언마운트 시 fetch를 취소하지 않으면 불필요한 setState 경고/리소스 낭비가 발생할 수 있습니다. AbortController로 보완을 권장합니다.
아래처럼 개선할 수 있습니다:
useEffect(() => { - fetch('/genres.json') - .then(res => res.json()) - .then(data => setGenres(data)) - .catch(console.error); + const controller = new AbortController(); + fetch('/genres.json', { signal: controller.signal }) + .then(res => res.json()) + .then(data => setGenres(data)) + .catch(err => { + if (err?.name !== 'AbortError') { + console.error(err); + } + }); + return () => controller.abort(); }, []);
29-44: 디버그 로그 정리 또는 환경 분기 권장console.log가 다수 존재합니다. 디버깅 시 유용하지만, 배포 빌드에서는 노이즈가 될 수 있습니다. NODE_ENV 분기나 로거 유틸로 관리하거나 제거하는 것을 권장합니다.
Also applies to: 60-66
72-86: 회원가입 실패 시 사용자 피드백 보완 제안현재 실패/예외는 console로만 기록됩니다. 본 PR에서 스낵바 피드백이 도입된 만큼, 여기서도 동일한 패턴으로 안내(스낵바/토스트)를 제공하면 UX 일관성이 좋아집니다.
원하시면 스낵바 훅/컴포넌트에 맞춘 에러 핸들링 코드도 함께 제안드리겠습니다.
src/pages/index.tsx (1)
47-52: 레거시 경로 리디렉션(하위 호환) 고려기존 북마크/외부 링크가 '/signupdone'를 참조할 수 있습니다. 부드러운 마이그레이션을 위해 리디렉션 라우트를 추가하는 것을 권장합니다.
아래와 같이 Navigate를 사용한 리디렉션을 추가할 수 있습니다:
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider, + Navigate, } from 'react-router-dom'; @@ - <Route path="signup/guide" element={<Guide />} /> - <Route path="signup/done" element={<SignupDone />} /> + <Route path="signup/guide" element={<Guide />} /> + <Route path="signup/done" element={<SignupDone />} /> + {/* Legacy route redirect */} + <Route path="signupdone" element={<Navigate to="/signup/done" replace />} />src/pages/Guide.tsx (5)
183-185: dangerouslySetInnerHTML 제거 또는 최소한의 안전장치 적용현재는 하드코딩된 문자열이라 실질적 XSS 위험은 낮지만, lint/security 경고를 피하고 향후 동적 콘텐츠로 전환될 가능성을 대비해 React 노드로 표현하는 방식을 권장합니다.
아래처럼 description 타입을 React.ReactNode로 바꾸고
를 JSX로 표기하면 dangerouslySetInnerHTML 없이 동일한 UI를 구현할 수 있습니다.-interface GuideStep { - id: number; - title: string; - description: string; - image: string; -} +interface GuideStep { + id: number; + title: string; + description: React.ReactNode; + image: string; +}- const guideSteps: GuideStep[] = [ + const guideSteps: GuideStep[] = [ { id: 1, title: '피드', - description: '피드에서 책과 독서에 대한 생각을<br/>자유롭게 나누어보세요!', + description: <>피드에서 책과 독서에 대한 생각을<br />자유롭게 나누어보세요!</>, image: guide1, }, { id: 2, title: '피드', - description: - "칭호를 통해 내 독서 취향을 드러내고,<br/>마음에 드는 유저를 '띱'하고 감상을 공유해보세요!", + description: <>칭호를 통해 내 독서 취향을 드러내고,<br />마음에 드는 유저를 '띱'하고 감상을 공유해보세요!</>, image: guide2, }, { id: 3, title: '모임', - description: '모임방에서는 글은 물론 투표 기능을 통해<br/>감상과 의견을 나눌 수 있어요.', + description: <>모임방에서는 글은 물론 투표 기능을 통해<br />감상과 의견을 나눌 수 있어요.</>, image: guide3, }, { id: 4, title: '모임', - description: - '읽고 싶은 책으로 나만의 독서 모임을 만들고,<br/>독서메이트와 함께 기록을 나눌 수 있어요. ', + description: <>읽고 싶은 책으로 나만의 독서 모임을 만들고,<br />독서메이트와 함께 기록을 나눌 수 있어요. </>, image: guide4, }, { id: 5, title: 'Thip+', - description: - '기록은 자유롭게, 감상은 방해없이.<br/>읽지 않은 페이지에 대한 기록은<br/>블라인드되어 스포일러 걱정없이 몰입할 수 있어요.', + description: <>기록은 자유롭게, 감상은 방해없이.<br />읽지 않은 페이지에 대한 기록은<br />블라인드되어 스포일러 걱정없이 몰입할 수 있어요.</>, image: guide5, }, { id: 6, title: 'Thip+', - description: "모임방의 인상깊은 기록을<br/>'핀하기'로 피드에 다시 공유해보세요.", + description: <>모임방의 인상깊은 기록을<br />'핀하기'로 피드에 다시 공유해보세요.</>, image: guide6, }, ];- <Description dangerouslySetInnerHTML={{ __html: guideSteps[currentStep].description }} /> + <Description>{guideSteps[currentStep].description}</Description>대안: 만약 문자열 유지가 필요하면 DOMPurify로 sanitize 후 주입하는 방식도 가능합니다.
Also applies to: 14-19, 94-134
176-179: 마지막 단계에서는 버튼 라벨을 ‘완료’로 표기현재 모든 단계에서 "다음"으로 노출되며, 마지막 단계에서도 "다음"입니다. 마지막 단계에서 "완료"로 표기하면 사용성이 좋아집니다.
- <div className="next-button" onClick={handleNext}> - 다음 - </div> + <div className="next-button" onClick={handleNext}> + {currentStep === guideSteps.length - 1 ? '완료' : '다음'} + </div>
27-28: containerRef 미사용ref를 선언하고 넘기지만 실제로 사용하지 않습니다. 제거하거나, 추후 키보드 포커스/스크롤 제어 등 용도로 활용 예정이라면 TODO를 남겨주세요.
- const containerRef = useRef<HTMLDivElement>(null); @@ - <Container - ref={containerRef} + <Container isDragging={isDragging}Also applies to: 165-174
137-139: 주석/인터랙션 불일치 정리주석은 “active 상태일 때만 다음 단계로 이동”이라 돼 있으나, 실제로는 어떤 단계에서도 클릭 시 진행됩니다. 주석을 정정하거나, active가 아닌 경우 클릭을 무시하도록 로직/스타일을 통일해 주세요.
Also applies to: 239-253
214-219: 스타일 상수(폭) 일관성 제안max-width가 766/767로 혼재되어 있습니다. 하나의 상수로 통일하면 유지보수성이 좋아집니다.
Also applies to: 234-236, 302-305
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (6)
src/assets/signup/guide1.svgis excluded by!**/*.svgsrc/assets/signup/guide2.svgis excluded by!**/*.svgsrc/assets/signup/guide3.svgis excluded by!**/*.svgsrc/assets/signup/guide4.svgis excluded by!**/*.svgsrc/assets/signup/guide5.svgis excluded by!**/*.svgsrc/assets/signup/guide6.svgis excluded by!**/*.svg
📒 Files selected for processing (4)
src/pages/Guide.tsx(1 hunks)src/pages/index.tsx(2 hunks)src/pages/signup/SignupDone.tsx(1 hunks)src/pages/signup/SignupGenre.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/Guide.tsx (1)
src/styles/global/global.ts (2)
colors(4-53)typography(56-77)
🪛 Biome (2.1.2)
src/pages/Guide.tsx
[error] 208-209: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🪛 ast-grep (0.38.6)
src/pages/Guide.tsx
[warning] 183-183: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🔇 Additional comments (5)
src/pages/signup/SignupGenre.tsx (2)
74-79: 온보딩 플로우에 맞춘 라우팅 변경: LGTM회원가입 성공 시 '/signup/guide'로 네비게이션하는 변경이 신규 가이드 플로우(/signup/guide → /signup/done)와 일치합니다. 상태로 aliasName, nickname을 넘기는 부분도 Guide에서 기대하는 키와 일치합니다.
90-97: 헤더 단계 표기 검토현재 타이틀이 "설정 2/2"로 고정되어 있으나, 온보딩에 Guide(여러 단계)가 추가되었습니다. 사용자 여정과의 일관성을 위해 단계 표기 업데이트가 필요한지 확인 부탁드립니다.
src/pages/signup/SignupDone.tsx (2)
15-16: 뒤로가기 경로 변경: LGTM신규 온보딩 흐름에 맞춰 '/signup/guide'로 이동하도록 한 변경은 타 파일(Guide, Router)과 일관됩니다.
22-27: 상태 미존재 시 라우팅 방침 확인 요청완료 화면은 공유/외부 진입 가능성이 있습니다. 상태가 없는 경우에도 기본값으로 표시하는 현재 접근이 의도된 제품 결정인지, 아니면 '/signup/guide'로 안내하는 것이 맞는지 확인 부탁드립니다.
Also applies to: 38-41
src/pages/index.tsx (1)
41-41: Guide 및 Done 라우트 추가: LGTM신규 플로우에 맞춰 '/signup/guide', '/signup/done' 라우트를 추가한 변경이 다른 파일들과 잘 정합됩니다.
Also applies to: 50-51
| if (!touchStart || !touchEnd) return; | ||
|
|
There was a problem hiding this comment.
터치 시작/종료 좌표가 0일 때 스와이프가 동작하지 않는 버그
0은 falsy라 좌측 가장자리(0px)에서 스와이프하면 조건문이 조기 return 됩니다. null 비교로 바꿔주세요.
다음과 같이 수정하면 됩니다:
- if (!touchStart || !touchEnd) return;
+ if (touchStart === null || touchEnd === null) 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.
| if (!touchStart || !touchEnd) return; | |
| if (touchStart === null || touchEnd === null) return; |
🤖 Prompt for AI Agents
In src/pages/Guide.tsx around lines 44-45, the current guard uses falsy checks
(if (!touchStart || !touchEnd) return;) which treats 0 as absent and prevents
edge swipes; change the condition to explicitly check for null/undefined (e.g.
if (touchStart == null || touchEnd == null) return;) so zero coordinates are
allowed while still guarding against missing values.
| @@ -12,19 +12,13 @@ const SignupDone = () => { | |||
| const { nickName, aliasName } = location.state || {}; | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion
직접 진입 시 undefined 노출 가능성 해결 및 접근성 보완
pre-render 가드 제거로 상태 없이 접근하면 닉네임/칭호가 undefined로 노출될 수 있습니다. 기본값을 부여하거나 가벼운 가드를 유지하는 것을 권장합니다. 또한 프로필 이미지에 alt가 없습니다.
아래처럼 최소한의 기본값과 alt를 추가하면 UX/접근성이 개선됩니다.
- const { nickName, aliasName } = location.state || {};
+ const { nickName = '사용자', aliasName = '독서가' } = location.state || {};- <img src={art} />
+ <img src={art} alt="가입 완료 일러스트" />Also applies to: 28-37
🤖 Prompt for AI Agents
In src/pages/signup/SignupDone.tsx around line 12 (and also lines 28-37),
location.state may be undefined causing nickName/aliasName to render as
undefined and the profile image lacks an alt attribute; change the destructure
to provide safe defaults (e.g., const { nickName = '회원', aliasName = '' } =
location.state || {}) or add a lightweight guard that redirects to the signup
start if state is missing, and add a meaningful alt prop to the profile image
(e.g., alt={`프로필 이미지 of ${nickName}` or alt="프로필 이미지") to fix UX/accessibility
issues.
#️⃣연관된 이슈
없음
📝작업 내용
- 띱취소, 띱하기 snackbar 동작 추가
- bookinfocard 네비게이션 추가
- 책 검색 무한스크롤 구현
- 모달 배경 색 수정
💬리뷰 요구사항
없음
Summary by CodeRabbit
New Features
Style
Chores