feat: 무한 스크롤 API 수정#221
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough저장/그룹 도서 목록에 커서 기반 페이지네이션을 도입하고, BottomSheet와 훅에 탭별 로딩·다음페이지 상태 분기를 추가했으며, BookList의 검색 모드 제약을 제거했습니다. 또한 API 요청 인터셉터에 하드코드된 Bearer 토큰 대체 로직과 index.html의 가로 스와이프 방지 스니펫, feed 페이지의 토큰 초기 저장이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as BookSearchBottomSheet
participant Hook as useBookSearch
participant List as BookList
participant API as /books/selectable-list
rect rgba(200,230,255,0.25)
User->>UI: 스크롤로 마지막 항목 노출
UI->>List: props(hasNextPage, isLoadingMore, onLoadMore)
List-->>UI: last item intersect
end
alt 검색 모드
UI->>Hook: loadMoreSearchResults()
Hook->>API: GET /books/selectable-list?type=SEARCH&cursor=<searchCursor>
API-->>Hook: items, nextCursor, isLast
Hook-->>UI: 검색 결과·커서 갱신
else 저장 탭
UI->>Hook: loadMoreSavedBooks()
Hook->>API: GET /books/selectable-list?type=SAVED&cursor=<savedCursor>
API-->>Hook: items, nextCursor, isLast
Hook-->>UI: 저장 리스트·커서 갱신
else 그룹 탭
UI->>Hook: loadMoreGroupBooks()
Hook->>API: GET /books/selectable-list?type=JOINING&cursor=<groupCursor>
API-->>Hook: items, nextCursor, isLast
Hook-->>UI: 그룹 리스트·커서 갱신
end
Note over Hook,UI: isLast=true → hasNext=false
sequenceDiagram
autonumber
participant Caller as useBookSearch.fetchSavedBooks
participant API as getSavedBooks(type, cursor?)
Caller->>API: type='saved', cursor=<nullable>
API->>API: params 구성 (type 대문자, cursor 조건부)
API->>/books/selectable-list: GET with params
/books/selectable-list-->>API: {items, nextCursor, isLast}
API-->>Caller: items + nextCursor/isLast
Caller->>Caller: 목록 병합 또는 교체, cursor/hasNext 갱신
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 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: 2
🧹 Nitpick comments (7)
src/components/common/BookSearchBottomSheet/BookList.tsx (3)
51-53: 교차 시 중복 호출 방지: 관찰 해제 후 재관찰로 onLoadMore 폭주 예방빠른 연속 교차 이벤트로 onLoadMore가 중복 호출될 수 있습니다. 콜백에서 먼저 unobserve 후 호출하고, 완료 시 재관찰해 주세요.
- observerRef.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && hasNextPage && onLoadMore) { - onLoadMore(); - } - }); + observerRef.current = new IntersectionObserver((entries, observer) => { + const entry = entries[0]; + if (entry.isIntersecting && hasNextPage && onLoadMore) { + observer.unobserve(entry.target); + void onLoadMore().finally(() => observer.observe(entry.target)); + } + } + // , { root: null, rootMargin: '200px 0px', threshold: 0 } // 사전 로드가 필요하면 옵션 추가 + );
57-57: useCallback deps 정리: 불필요한 isSearchMode 제거콜백 내부에서 isSearchMode를 사용하지 않습니다. 재생성 최소화를 위해 deps에서 제거하세요.
- }, [isLoadingMore, hasNextPage, onLoadMore, isSearchMode]); + }, [isLoadingMore, hasNextPage, onLoadMore]);
38-39: 미사용 ref 정리loadingRef는 선언만 되고 동작에 사용되지 않습니다. 삭제로 단순화하세요.
- const loadingRef = useRef<HTMLDivElement | null>(null); ... - {isLoadingMore && ( - <LoadingContainer ref={loadingRef}> + {isLoadingMore && ( + <LoadingContainer> <LoadingText>더 많은 책을 불러오는 중...</LoadingText> </LoadingContainer> )}Also applies to: 86-89
src/api/books/getSavedBooks.ts (2)
16-18: nextCursor null 가능성 반영 필요백엔드에서 마지막 페이지일 때 nextCursor를 null로 돌려줄 수 있으면 현재 string 타입이 부정확합니다. 런타임 타입 불일치/파싱 오류 예방을 위해 union으로 두세요.
export interface SavedBooksData { bookList: SavedBook[]; - nextCursor: string; + nextCursor: string | null; isLast: boolean; }백엔드 스펙이 “isLast=true일 때 nextCursor=null”인지 확인 부탁드립니다. 확인되면 useBookSearch에서 cursor 상태(type도 string | null)에 그대로 호환됩니다.
35-36: 전달 파라미터 대소문자 계약 확인type.toUpperCase()로 “SAVED/JOINING”을 전달합니다. 서버가 반드시 UPPER를 요구하는지, 소문자도 허용하는지 확인해 주세요. 허용된다면 변환을 제거해도 됩니다.
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (2)
85-93: onLoadMore 핸들러는 “호출 결과”가 아닌 “함수 참조”를 직접 전달getLoadMoreHandler()를 렌더 중 호출하면 매 렌더마다 새로운 함수 인스턴스가 내려가 BookList의 observer가 과도하게 재설정될 수 있습니다. 함수 참조를 바로 선택해 전달하거나 memoize하세요.
- // 현재 탭에 맞는 무한 스크롤 핸들러 결정 - const getLoadMoreHandler = () => { - if (isSearchMode) { - return loadMoreSearchResults; - } - return activeTab === 'saved' ? loadMoreSavedBooks : loadMoreGroupBooks; - }; + // 현재 탭에 맞는 무한 스크롤 핸들러 결정 (함수 참조 선택) + const onLoadMoreHandler = + isSearchMode + ? loadMoreSearchResults + : (activeTab === 'saved' ? loadMoreSavedBooks : loadMoreGroupBooks); ... - onLoadMore={getLoadMoreHandler()} + onLoadMore={onLoadMoreHandler}Also applies to: 124-132
131-131: BookList의 isSearchMode prop 제거 고려현 시점 BookList 내부에서 isSearchMode를 사용하지 않습니다. 양쪽 파일에서 prop/의존성 제거로 인터페이스를 단순화할 수 있습니다.
📜 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 (4)
src/api/books/getSavedBooks.ts(2 hunks)src/components/common/BookSearchBottomSheet/BookList.tsx(2 hunks)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(3 hunks)src/components/common/BookSearchBottomSheet/useBookSearch.ts(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/common/BookSearchBottomSheet/useBookSearch.ts (1)
src/api/books/getSavedBooks.ts (1)
getSavedBooks(29-50)
src/api/books/getSavedBooks.ts (1)
src/api/index.ts (1)
apiClient(7-14)
🔇 Additional comments (5)
src/components/common/BookSearchBottomSheet/BookList.tsx (1)
51-53: isSearchMode 가드 제거로 전 탭 무한 스크롤 활성화된 점 좋습니다의도와 일치하며 BookSearchBottomSheet의 핸들러 분기와도 정합적입니다.
Also applies to: 84-89
src/api/books/getSavedBooks.ts (1)
42-45: API 호출 자체는 깔끔합니다params 객체 구성과 선택적 cursor 포함 로직이 명확합니다.
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
31-40: 분기/상태 바인딩 전반적으로 정합적입니다검색/탭 모드에 따른 hasNext/loadingMore 라우팅과 핸들러 선택이 명확합니다.
Also applies to: 96-98, 124-132
src/components/common/BookSearchBottomSheet/useBookSearch.ts (2)
21-27: 탭별 커서/로딩 상태 분리, 방향성 좋습니다saved/group 각각의 cursor/hasNext/isLoadingMore 분리는 확장성과 가독성에 유리합니다.
62-69: 데이터 머지/다음 페이지 상태 업데이트, 파생 상태 노출 모두 적절합니다append/replace 처리와 currentTabHasNext/currentTabIsLoadingMore 파생은 소비 측 단순화에 도움 됩니다.
Also applies to: 100-106, 266-269, 291-302
| // 저장한 책 데이터 가져오기 (무한 스크롤) | ||
| const fetchSavedBooks = async (isLoadMore: boolean = false) => { | ||
| try { | ||
| setIsLoading(true); | ||
| if (isLoadMore) { | ||
| setIsLoadingMoreSavedBooks(true); | ||
| } else { | ||
| setIsLoading(true); | ||
| setSavedBooksCursor(null); | ||
| } | ||
| setError(null); |
There was a problem hiding this comment.
초기 로드 중 중복 로드 레이스 가능성: hasNext/로딩 가드 초기화 필요
초기 로드(isLoadMore=false) 시 기존 리스트가 남아 있고 hasSavedBooksNext/hasGroupBooksNext가 true인 상태면, 마지막 아이템 관찰로 loadMore가 동시에 트리거될 수 있습니다(초기 로드는 isLoading 플래그만 세우고, isLoadingMore는 false라 가드에 걸리지 않음).
- 초기 로드 시작 시 각 has*Next를 false로 리셋하여 트리거 차단
- loadMore* 가드에 isLoading도 포함
} else {
setIsLoading(true);
- setSavedBooksCursor(null);
+ setSavedBooksCursor(null);
+ setHasSavedBooksNext(false);
}
...
} else {
setIsLoading(true);
- setGroupBooksCursor(null);
+ setGroupBooksCursor(null);
+ setHasGroupBooksNext(false);
}Also applies to: 84-93
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/useBookSearch.ts around lines
47-56 and also apply same change at 84-93: during initial load (isLoadMore ===
false) reset the paginated flags hasSavedBooksNext and hasGroupBooksNext to
false to prevent concurrent loadMore triggers from observer callbacks, and
modify the loadMore* guard conditions to include isLoading (i.e., only proceed
if not isLoading and not isLoadingMore* and has*Next) so initial load blocks
loadMore; ensure the reset happens before clearing/setting cursors and before
setting isLoading to true so the guard state is consistent.
| // 더 많은 저장한 책 로드 | ||
| const loadMoreSavedBooks = async () => { | ||
| if (isLoadingMoreSavedBooks || !hasSavedBooksNext) { | ||
| return; | ||
| } | ||
|
|
||
| await fetchSavedBooks(true); | ||
| }; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
loadMore 가드 강화: 초기 로딩 중 트리거 방지
isLoading 동안에도 더 불러오기를 막아 중복 요청을 제거하세요.
- const loadMoreSavedBooks = async () => {
- if (isLoadingMoreSavedBooks || !hasSavedBooksNext) {
+ const loadMoreSavedBooks = async () => {
+ if (isLoading || isLoadingMoreSavedBooks || !hasSavedBooksNext) {
return;
}
await fetchSavedBooks(true);
};
...
- const loadMoreGroupBooks = async () => {
- if (isLoadingMoreGroupBooks || !hasGroupBooksNext) {
+ const loadMoreGroupBooks = async () => {
+ if (isLoading || isLoadingMoreGroupBooks || !hasGroupBooksNext) {
return;
}
await fetchGroupBooks(true);
};Also applies to: 193-200
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/useBookSearch.ts around lines
183-191 (and similarly lines 193-200), the load-more handlers currently only
guard against isLoadingMoreSavedBooks and hasSavedBooksNext; enhance the guard
to also prevent triggering while the initial isLoading is true. Update both
loadMoreSavedBooks and the other load-more function to return early when
isLoading is true (e.g., add a check combined with the existing conditions) so
duplicate requests during initial load are avoided.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
index.html (1)
4-12: 글로벌 CSS 범위 조정 제안body/* 전역 */에 overscroll/touch-action을 강제하면 예기치 않은 UI 영향이 큽니다. 앱 컨테이너(#root 등)에 한정하거나 페이지별로 적용하는 것을 권장합니다.
📜 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 (3)
index.html(1 hunks)src/api/index.ts(1 hunks)src/pages/feed/Feed.tsx(1 hunks)
🧰 Additional context used
🪛 Gitleaks (8.27.2)
src/api/index.ts
28-28: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
| document.addEventListener('touchmove', function(e) { | ||
| if (!startX) return; | ||
|
|
||
| let currentX = e.touches[0].clientX; | ||
| let diffX = Math.abs(currentX - startX); | ||
| let diffY = Math.abs(e.touches[0].clientY - (e.touches[0].clientY || 0)); | ||
|
|
||
| // 수평 스와이프가 수직보다 크면 preventDefault | ||
| if (diffX > diffY && diffX > 10) { | ||
| if ((currentX > startX && startX < 50) || (currentX < startX && startX > window.innerWidth - 50)) { | ||
| e.preventDefault(); | ||
| } | ||
| } | ||
| }, { passive: false }); | ||
| </script> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
수평 제스처 차단 스크립트의 diffY 계산 버그로 과도한 preventDefault 가능
diffY가 항상 0으로 계산되어(현재 Y에서 자기 자신을 뺌) 수평 판단이 왜곡됩니다. 또한 제스처 종료 시 상태 초기화가 없습니다. 엣지 근접/멀티터치/인터랙티브 요소 예외 처리도 필요합니다.
수정 diff:
- let startX = null;
+ let startX = null;
+ let startY = null;
@@
- document.addEventListener('touchstart', function(e) {
- startX = e.touches[0].clientX;
- }, { passive: true });
+ document.addEventListener('touchstart', function(e) {
+ if (e.touches.length !== 1) return; // 멀티터치 예외
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+ }, { passive: true });
@@
- document.addEventListener('touchmove', function(e) {
+ document.addEventListener('touchmove', function(e) {
if (!startX) return;
-
- let currentX = e.touches[0].clientX;
- let diffX = Math.abs(currentX - startX);
- let diffY = Math.abs(e.touches[0].clientY - (e.touches[0].clientY || 0));
-
- // 수평 스와이프가 수직보다 크면 preventDefault
- if (diffX > diffY && diffX > 10) {
- if ((currentX > startX && startX < 50) || (currentX < startX && startX > window.innerWidth - 50)) {
- e.preventDefault();
- }
- }
- }, { passive: false });
+ if (e.touches.length !== 1) return;
+ const target = e.target as HTMLElement | null;
+ if (target && target.closest('input, textarea, select, button, a')) return; // 폼/링크 예외
+
+ const currentX = e.touches[0].clientX;
+ const currentY = e.touches[0].clientY;
+ const diffX = Math.abs(currentX - startX);
+ const diffY = Math.abs(currentY - (startY ?? currentY));
+
+ const EDGE_PX = 50;
+ const nearLeft = startX < EDGE_PX;
+ const nearRight = startX > (window.innerWidth - EDGE_PX);
+ if (diffX > diffY && diffX > 10 && (nearLeft || nearRight)) {
+ e.preventDefault();
+ }
+ }, { passive: false });
+
+ document.addEventListener('touchend', function() {
+ startX = null;
+ startY = null;
+ }, { passive: true });Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In index.html around lines 22 to 36, the touchmove handler computes diffY
incorrectly (subtracts the same Y value), never resets touch state, and doesn't
handle multi-touch or interactive elements; fix by storing startY on touchstart
alongside startX, compute diffY = Math.abs(currentY - startY) in touchmove,
early-return when e.touches.length > 1 or the event target is an interactive
element (input, textarea, select, [contenteditable=true]), add
touchend/touchcancel handlers to clear startX/startY, and keep passive: false
only for the listener that requires preventDefault.
| config.headers.Authorization = | ||
| 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc'; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
하드코딩된 JWT가 Authorization 헤더에 주입됩니다 — 즉시 제거 필요
소스에 토큰이 노출되었고 요청 시 항상 사용됩니다. 보안사고·권한오남용 리스크가 큽니다. 토큰이 없을 때는 헤더를 설정하지 말고, 정상 인증 플로우(로그인/401 처리)를 타도록 하세요.
적용 diff:
- config.headers.Authorization =
- 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc';
+ // no authToken: do not set Authorization; let upstream 401 handler work
+ if (config.headers && 'Authorization' in config.headers) {
+ delete (config.headers as Record<string, unknown>).Authorization;
+ }추가 권고:
- 필요 시 개발 환경에서만 사용되는 모의 토큰은 .env(예: VITE_DEV_JWT)로 분리하고 플래그로 가드하세요.
- 커밋 내 다른 하드코딩 토큰 유무를 점검하세요.
다음으로 토큰 문자열/하드코딩 Authorization 사용을 스캔해 주세요:
🏁 Script executed:
#!/bin/bash
rg -nP "(Bearer\\s+[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+|eyJhbGciOiJIUzI1NiJ9|Authorization\\s*=\\s*`?Bearer)" -SLength of output: 116
스크립트 인용부 오류가 발생했습니다. 작은따옴표로 감싸 백틱을 이스케이프 없이 포함한 패턴으로 재실행해 주세요.
🏁 Script executed:
#!/bin/bash
# 하드코딩된 JWT 또는 Authorization 헤더 사용 스캔
rg -nP '(Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+|eyJhbGciOiJIUzI1NiJ9|Authorization\s*=\s*`?Bearer)' -n .Length of output: 547
하드코딩된 JWT 토큰 제거 필요 (index.ts, Feed.tsx)
소스에 토큰이 노출되어 보안사고·권한 오남용 리스크가 큽니다.
- src/api/index.ts(24–28):
- 하드코딩된 토큰 즉시 제거
config.headers.Authorization = \Bearer ${authToken}`는authToken`이 있을 때만 설정하고, 없으면 헤더를 삭제하거나 설정하지 않도록 가드 추가
- src/pages/feed/Feed.tsx(165):
- 하드코딩된 토큰 즉시 제거
- 실제 사용자 토큰을 참조하도록 교체
필요 시 개발 환경 모의 토큰은 .env(예: VITE_DEV_JWT)로 분리하고, 플래그로 가드하세요. 커밋 내 모든 하드코딩된 JWT 전수 점검 후 제거 바랍니다.
🧰 Tools
🪛 Gitleaks (8.27.2)
28-28: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
src/api/index.ts lines 24–28 and src/pages/feed/Feed.tsx line 165: remove the
hardcoded JWT string immediately; in src/api/index.ts replace the assignment
with logic that sets config.headers.Authorization = `Bearer ${authToken}` only
when authToken exists and otherwise deletes or leaves Authorization unset, and
in src/pages/feed/Feed.tsx remove the hardcoded token and wire the request to
use the real user token from auth state/props/context; for local dev only, move
any mock token to an env var (e.g., VITE_DEV_JWT) and gate its use behind a
development-only flag, and scan the repo to remove any other hardcoded JWTs.
| localStorage.setItem( | ||
| 'authToken', | ||
| 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc', | ||
| ); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
하드코딩 토큰을 localStorage에 저장 — 보안 이슈 + 초기 로딩 영구화(side effect)
- 보안: 유저 토큰이 소스에 노출됩니다.
- 기능: 아래의 return(라인 168, 기존 코드)이 그대로라면 setState 없이 함수가 종료되어 initialLoading이 true로 고정되어 스피너가 계속 뜰 가능성이 큼.
하드코딩 토큰 저장을 제거하고, 정상 인증/토큰 발급 플로우만 사용하세요.
적용 diff(하드코딩 토큰 제거):
- localStorage.setItem(
- 'authToken',
- 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc',
- );추가 조치(선택): 라인 168의 return;도 제거하여 첫 진입 시 바로 피드 로딩이 진행되도록 해 주세요.
예시:
// 하드코딩 저장 제거 후, return 없이 아래 setTabLoading/try-finally 흐름을 그대로 태웁니다.개발용 모의 토큰이 필요하면
- .env로 분리(VITE_DEV_JWT),
- import.meta.env.DEV && VITE_USE_MOCK_AUTH === 'true' 조건으로만 주입,
- 주입 후에도 초기 로딩이 멈추지 않도록 즉시 loadTotalFeeds/loadMyFeeds를 호출
을 고려하세요.
초기 로딩이 멈추지 않는지 빠르게 확인:
🏁 Script executed:
#!/bin/bash
rg -n "initialLoading|setInitialLoading|return;\\s*$" src/pages/feed/Feed.tsx -n -C2Length of output: 930
🏁 Script executed:
#!/bin/bash
# 출력: 140-200행
sed -n '140,200p' src/pages/feed/Feed.tsxLength of output: 1757
하드코딩된 토큰 저장 및 초기 로딩 분기 수정
if (!authToken)분기에서 하드코딩된localStorage.setItem('authToken', …)제거- 같은 분기 내
return;제거해 finally에서setInitialLoading(false)가 항상 호출되도록 보장 - 개발용 모의 토큰은
import.meta.env.DEV && VITE_USE_MOCK_AUTH==='true'조건 하에서만 설정하고, 설정 후 즉시loadTotalFeeds/loadMyFeeds를 호출해 초기 로딩이 멈추지 않게 처리하세요.
🤖 Prompt for AI Agents
In src/pages/feed/Feed.tsx around lines 163 to 166, remove the hardcoded
localStorage.setItem('authToken', ...) and the early return inside the if
(!authToken) branch; instead, only set a mock token when import.meta.env.DEV &&
VITE_USE_MOCK_AUTH === 'true', then immediately call loadTotalFeeds() and
loadMyFeeds() so initialization continues; ensure the function no longer returns
early and that setInitialLoading(false) is always invoked in the finally block
so initial loading is cleared regardless of auth path.
#️⃣ 연관된 이슈
📝 작업 내용
저장한 책 또는 모임의 책 조회 API가 커서 기반 무한 스크롤을 지원하도록 업데이트 했습니다.
API 타입 및 함수 업데이트
src/api/books/getSavedBooks.ts의SavedBooksData인터페이스에nextCursor와isLast필드를 추가했습니다. 새로운 API 응답 구조에 맞춰 커서 정보와 마지막 페이지 여부를 포함하도록 수정했습니다.getSavedBooks함수에 선택적cursor매개변수를 추가하여 첫 페이지 요청 시에는 null을, 이후 페이지 요청 시에는 이전 응답의nextCursor값을 전달할 수 있도록 했습니다. 커서가 null이 아닐 때만 요청 파라미터에 포함되어 API 호출됩니다.무한 스크롤 훅 로직 개선
src/components/common/BookSearchBottomSheet/useBookSearch.ts에서 저장한 책과 모임 책 각각에 대한 독립적인 커서 상태 관리를 추가했습니다.savedBooksCursor,groupBooksCursor,hasSavedBooksNext,hasGroupBooksNext등의 상태를 통해 각 탭별로 무한 스크롤 상태를 분리 관리합니다.fetchSavedBooks와fetchGroupBooks함수를 수정하여isLoadMore매개변수를 받아 첫 로딩인지 추가 로딩인지 구분하도록 했습니다. 첫 로딩 시에는 기존 데이터를 덮어쓰고, 추가 로딩 시에는 기존 데이터에 새 데이터를 추가합니다.loadMoreSavedBooks와loadMoreGroupBooks함수를 새로 추가하여 각 탭별로 독립적인 무한 스크롤 로딩을 처리할 수 있도록 했습니다.컴포넌트 무한 스크롤 통합
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx에서 검색 모드와 저장한 책/모임 책 탭 모두에서 무한 스크롤이 작동하도록 로직을 통합했습니다.getLoadMoreHandler함수를 통해 현재 상태(검색 모드 또는 탭)에 따라 적절한 로드 모어 핸들러를 선택하여 BookList 컴포넌트에 전달합니다.BookList 컴포넌트 개선
src/components/common/BookSearchBottomSheet/BookList.tsx에서 기존에는 검색 모드에서만 작동하던 무한 스크롤을 모든 탭에서 작동하도록 수정했습니다. Intersection Observer의 조건에서isSearchMode체크를 제거하여 저장한 책과 모임 책 탭에서도 스크롤 감지가 가능하도록 했습니다.Summary by CodeRabbit
New Features
Refactor
Chores