Skip to content

feat: 무한 스크롤 API 수정#221

Merged
ljh130334 merged 5 commits into
developfrom
feat/savedbook-hotfix
Sep 1, 2025
Merged

feat: 무한 스크롤 API 수정#221
ljh130334 merged 5 commits into
developfrom
feat/savedbook-hotfix

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Sep 1, 2025

#️⃣ 연관된 이슈

#220

📝 작업 내용

저장한 책 또는 모임의 책 조회 API가 커서 기반 무한 스크롤을 지원하도록 업데이트 했습니다.

API 타입 및 함수 업데이트

  • src/api/books/getSavedBooks.tsSavedBooksData 인터페이스에 nextCursorisLast 필드를 추가했습니다. 새로운 API 응답 구조에 맞춰 커서 정보와 마지막 페이지 여부를 포함하도록 수정했습니다.
  • getSavedBooks 함수에 선택적 cursor 매개변수를 추가하여 첫 페이지 요청 시에는 null을, 이후 페이지 요청 시에는 이전 응답의 nextCursor 값을 전달할 수 있도록 했습니다. 커서가 null이 아닐 때만 요청 파라미터에 포함되어 API 호출됩니다.

무한 스크롤 훅 로직 개선

  • src/components/common/BookSearchBottomSheet/useBookSearch.ts에서 저장한 책과 모임 책 각각에 대한 독립적인 커서 상태 관리를 추가했습니다. savedBooksCursor, groupBooksCursor, hasSavedBooksNext, hasGroupBooksNext 등의 상태를 통해 각 탭별로 무한 스크롤 상태를 분리 관리합니다.
  • 기존 fetchSavedBooksfetchGroupBooks 함수를 수정하여 isLoadMore 매개변수를 받아 첫 로딩인지 추가 로딩인지 구분하도록 했습니다. 첫 로딩 시에는 기존 데이터를 덮어쓰고, 추가 로딩 시에는 기존 데이터에 새 데이터를 추가합니다.
  • loadMoreSavedBooksloadMoreGroupBooks 함수를 새로 추가하여 각 탭별로 독립적인 무한 스크롤 로딩을 처리할 수 있도록 했습니다.

컴포넌트 무한 스크롤 통합

  • src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx에서 검색 모드와 저장한 책/모임 책 탭 모두에서 무한 스크롤이 작동하도록 로직을 통합했습니다. getLoadMoreHandler 함수를 통해 현재 상태(검색 모드 또는 탭)에 따라 적절한 로드 모어 핸들러를 선택하여 BookList 컴포넌트에 전달합니다.
  • 검색 모드일 때는 기존 검색 무한 스크롤을, 저장한 책/모임 책 탭일 때는 각각의 전용 무한 스크롤 함수를 사용하도록 조건부 처리를 구현했습니다.

BookList 컴포넌트 개선

  • src/components/common/BookSearchBottomSheet/BookList.tsx에서 기존에는 검색 모드에서만 작동하던 무한 스크롤을 모든 탭에서 작동하도록 수정했습니다. Intersection Observer의 조건에서 isSearchMode 체크를 제거하여 저장한 책과 모임 책 탭에서도 스크롤 감지가 가능하도록 했습니다.

Summary by CodeRabbit

  • New Features

    • 저장/그룹 탭별 무한 스크롤 도입으로 각 탭에서 상황에 맞게 다음 페이지 로딩 지원
    • 커서 기반 페이지네이션 도입으로 더 안정적인 다음 페이지 탐색 제공
    • 화면 모서리 스와이프 탐색 억제로 의도치 않은 뒤로/앞으로 이동 방지
  • Refactor

    • 검색/비검색 모드 구분 없이 올바른 더보기 핸들러와 로딩 상태 선택으로 UX 일관성 개선
    • 하단 목록의 로딩 인디케이터 및 추가 로딩 트리거 일관화
  • Chores

    • API 에러 로그 강화로 문제 진단성 향상

@ljh130334 ljh130334 self-assigned this Sep 1, 2025
@ljh130334 ljh130334 added the ✨ Feature 기능 개발 label Sep 1, 2025
@vercel
Copy link
Copy Markdown

vercel Bot commented Sep 1, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Sep 1, 2025 6:50am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 1, 2025

Walkthrough

저장/그룹 도서 목록에 커서 기반 페이지네이션을 도입하고, BottomSheet와 훅에 탭별 로딩·다음페이지 상태 분기를 추가했으며, BookList의 검색 모드 제약을 제거했습니다. 또한 API 요청 인터셉터에 하드코드된 Bearer 토큰 대체 로직과 index.html의 가로 스와이프 방지 스니펫, feed 페이지의 토큰 초기 저장이 추가되었습니다.

Changes

Cohort / File(s) Change Summary
API: 저장 도서 페이징
src/api/books/getSavedBooks.ts
getSavedBooks(type, cursor?) 시그니처 추가, 요청 파라미터에 선택적 cursor 포함, 응답 타입에 nextCursorisLast 필드 추가, 에러 로깅 추가
훅: 탭별 커서/무한스크롤
src/components/common/BookSearchBottomSheet/useBookSearch.ts
저장/그룹 커서·hasNext·isLoadingMore 상태 추가, fetch 함수에 isLoadMore 분기, loadMoreSavedBooks/loadMoreGroupBookscurrentTabHasNext/currentTabIsLoadingMore 공개
UI: BottomSheet 로딩/분기
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx, src/components/common/BookSearchBottomSheet/BookList.tsx
BookSearchBottomSheet가 검색 모드/탭별로 적절한 load-more 핸들러와 상태를 선택하도록 변경. BookList의 IntersectionObserver에서 isSearchMode 가드 제거, 로딩 인디케이터는 isLoadingMore로만 제어
앱 셸: 가로 스와이프 차단
index.html
CSS로 수평 오버스크롤 억제 추가 및 터치 이벤트에서 엣지 스와이프(좌/우) 네비게이션을 예방하는 JS 스니펫 삽입
API: 요청 인터셉터 토큰 처리 변경
src/api/index.ts
로컬스토리지에 토큰이 없을 때도 정적 Bearer 토큰을 Authorization 헤더에 설정하도록 변경(이전엔 설정하지 않음)
페이즈/디버그: feed 초기 토큰 저장
src/pages/feed/Feed.tsx
로컬스토리지에 authToken이 없을 경우 하드코드된 JWT를 저장하는 호출을 활성화(기존 주석 해제 및 early return 유지)

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
Loading
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 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

📬 API

Suggested reviewers

  • heeeeyong
  • ho0010

Poem

귀 쫑긋, 스크롤 따라 춤추네 🐇
커서 한 줌 담아 또 앞으로,
탭마다 길 달라도 발은 가벼워,
nextCursor 불러오면 한숨 돌려요.
토끼는 말하네 — 로드 더! 📚

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/savedbook-hotfix

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a 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

🧹 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 9651f7e and ebf3340.

📒 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

Comment on lines +47 to 56
// 저장한 책 데이터 가져오기 (무한 스크롤)
const fetchSavedBooks = async (isLoadMore: boolean = false) => {
try {
setIsLoading(true);
if (isLoadMore) {
setIsLoadingMoreSavedBooks(true);
} else {
setIsLoading(true);
setSavedBooksCursor(null);
}
setError(null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

초기 로드 중 중복 로드 레이스 가능성: 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.

Comment on lines +183 to +191
// 더 많은 저장한 책 로드
const loadMoreSavedBooks = async () => {
if (isLoadingMoreSavedBooks || !hasSavedBooksNext) {
return;
}

await fetchSavedBooks(true);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between ebf3340 and 08e10cd.

📒 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)

Comment thread index.html
Comment on lines +22 to +36
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

수평 제스처 차단 스크립트의 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.

Comment thread src/api/index.ts Outdated
Comment on lines +27 to +28
config.headers.Authorization =
'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)" -S

Length 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.

Comment thread src/pages/feed/Feed.tsx Outdated
Comment on lines +163 to +166
localStorage.setItem(
'authToken',
'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 -C2

Length of output: 930


🏁 Script executed:

#!/bin/bash
# 출력: 140-200행
sed -n '140,200p' src/pages/feed/Feed.tsx

Length 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.

@ljh130334 ljh130334 merged commit a2c4500 into develop Sep 1, 2025
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Sep 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant