Skip to content

feat: 저장한 책 및 피드 조회 API 연동#163

Merged
heeeeyong merged 7 commits intodevelopfrom
chore/minor-updates
Aug 18, 2025
Merged

feat: 저장한 책 및 피드 조회 API 연동#163
heeeeyong merged 7 commits intodevelopfrom
chore/minor-updates

Conversation

@heeeeyong
Copy link
Collaborator

@heeeeyong heeeeyong commented Aug 18, 2025

#️⃣연관된 이슈

#74 [API] users API 연동

📝작업 내용

  • 마이페이지 버튼 수정
  • 내 피드 Footer (isPublic 처리)
  • 저장한 책 및 피드 조회 API 연동

스크린샷

💬리뷰 요구사항

없음

Summary by CodeRabbit

  • 신기능
    • 저장한 피드·책을 API로 불러오도록 전환하고 무한 스크롤·페이지네이션과 페이징용 커서 지원을 도입했습니다.
    • 책 저장 토글이 서버에 반영되도록 변경했습니다.
  • 리팩터링
    • 내 피드(MyFeed)를 항상 ‘내 피드’ 컨텍스트로 일관되게 렌더링하도록 단순화했습니다.
  • UI 변경
    • 마이페이지 헤더를 ‘내 정보’로 변경했고 메뉴 항목(공지, 약관, 버전) 클릭 동작과 버전 표기를 업데이트했습니다.
    • 로딩 컴포넌트의 기본 메시지를 비워 기본 텍스트 노출을 제거하고 빈 상태 문구를 갱신했습니다.

@vercel
Copy link

vercel bot commented Aug 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 18, 2025 6:19pm

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

MyFeed의 공개 API에서 isMyFeed를 제거하고 내부에서 항상 true로 고정합니다. 마이페이지와 저장 목록 페이지들이 서버 API(getSavedFeedsInMy, getSavedBooksInMy, postSaveBook)로 전환되어 무한 스크롤과 책 저장 토글을 지원하며 메뉴 클릭 핸들러와 일부 UI 텍스트가 변경되었습니다.

Changes

Cohort / File(s) Summary
Feed 컴포넌트 API 조정
src/components/feed/MyFeed.tsx
컴포넌트 시그니처에서 isMyFeed 제거; 내부에서 FeedPost에 isMyFeed: true로 고정 전달; 기타 렌더 로직 유지.
마이페이지 UI/핸들러 수정
src/pages/mypage/Mypage.tsx
헤더 텍스트("마이페이지"→"내 정보") 변경, handleTerms/handleVersion 추가, handleService URL 변경, 메뉴 항목에 onClick 핸들러 연결 및 버튼 속성 조정.
저장된 피드 API 추가
src/api/feeds/getSavedFeedsInMy.ts
SavedFeedInMy, SavedFeedsInMyData, SavedFeedsInMyResponse 타입 추가 및 `getSavedFeedsInMy(cursor: string
저장된 책 API 추가
src/api/books/getSavedBooksInMy.ts
SavedBookInMy, SavedBooksInMyData, SavedBooksInMyResponse 타입 추가 및 getSavedBooksInMy() 구현(오류 로깅 포함).
SavePage: mock → API 전환 및 무한 스크롤
src/pages/mypage/SavePage.tsx
mock 데이터 제거, getSavedFeedsInMy/getSavedBooksInMy로 데이터 로드, feed 무한 스크롤(IntersectionObserver) 도입, postSaveBook로 책 저장 토글 수행 및 로컬 상태 갱신, 초기 로드/탭 전환 로직 추가.
책 저장 API 사용 추가
src/api/books/postSaveBook (사용)
SavePage에서 postSaveBook(isbn, isSaved) 호출로 저장 토글 수행(기존 로컬 토글 제거).
로딩 UI 기본 메시지 변경
src/components/common/LoadingSpinner.tsx
message 기본값을 '로딩 중...'에서 ''로 변경 — 기본값 비어있을 때 텍스트 미표시.
임포트/타입 정리 및 훅 추가
src/pages/mypage/SavePage.tsx, ...
useRef, useCallback, LoadingSpinner 및 새 API 모듈들(import) 추가; 타입 선언(import) 업데이트.

Sequence Diagram(s)

sequenceDiagram
  participant Parent as Parent
  participant MyFeed as MyFeed
  participant FeedPost as FeedPost

  Parent->>MyFeed: render({ showHeader, posts, isLast })
  loop for each post
    MyFeed->>FeedPost: render({ post, isMyFeed: true, isLast? })
  end
Loading
sequenceDiagram
  participant User as 사용자
  participant SavePage as SavePage
  participant API as 서버(API)
  participant Browser as 브라우저

  User->>SavePage: 페이지 진입 / 탭 선택
  SavePage->>API: getSavedFeedsInMy(cursor=null), getSavedBooksInMy()
  API-->>SavePage: feedList, nextCursor, isLast / bookList
  loop 스크롤 도달
    SavePage->>SavePage: IntersectionObserver 감지
    SavePage->>API: getSavedFeedsInMy(cursor=nextCursor)
    API-->>SavePage: 다음 페이지 데이터
  end
  User->>SavePage: 책 저장 토글 클릭
  SavePage->>API: postSaveBook(isbn, isSaved)
  API-->>SavePage: 저장 결과
  SavePage->>SavePage: 로컬 상태 업데이트 (savedBooks)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • [API] Books API 연동 #86: "Books API 연동" 관련 — 본 PR의 getSavedBooksInMy 및 책 저장 토글(postSaveBook) 연동 목적과 일치함.

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • ho0010
  • ljh130334

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.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/minor-updates

🪧 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 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

@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 (1)
src/pages/mypage/Mypage.tsx (1)

125-125: UI 카피 오탈자 및 버전 표기 개선 제안

  • "버젼" → 표준 표기 "버전" 권장.
  • 고정 문자열 대신 빌드 타임/환경 변수에서 버전 표기를 주입하면 유지보수에 유리합니다.
-<MenuButton src={ver} name="버젼 1.0.0" isButton onClick={handleVersion} />
+<MenuButton src={ver} name="버전 1.0.0" isButton onClick={handleVersion} />

옵션: Vite 기준

// 예: .env에 VITE_APP_VERSION=1.0.0
const appVersion = import.meta.env.VITE_APP_VERSION ?? '1.0.0';
// name={`버전 ${appVersion}`}
📜 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 f741c33 and ce8b05e.

📒 Files selected for processing (2)
  • src/components/feed/MyFeed.tsx (2 hunks)
  • src/pages/mypage/Mypage.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/feed/MyFeed.tsx (1)
src/types/post.ts (1)
  • FeedListProps (21-27)
🔇 Additional comments (4)
src/pages/mypage/Mypage.tsx (3)

90-90: 카피 업데이트 LGTM

헤더 텍스트를 "내 정보"로 변경한 부분 문제 없습니다. 디자인 가이드와 일관성만 한 번 더 확인해 주세요.


117-117: 공지사항 버튼 핸들러 연결 LGTM

미연결 상태에서 onClick={handleNotice}로 연결한 점 적절합니다.


122-122: 약관/개인정보 메뉴 핸들러 분리 LGTM

handleNotice에서 handleTerms로 분리한 변경 👍

src/components/feed/MyFeed.tsx (1)

58-58: 하드코딩된 isMyFeed=true 전달 LGTM

MyFeed의 성격상 자신의 피드로 고정하는 의도에 부합합니다. FeedPost에서 해당 플래그가 권한/버튼 노출 제어에 사용된다면 의도된 동작으로 보입니다.

해당 컴포넌트가 타 화면(예: 전체 피드)에서 재사용되지 않는지 한 번만 확인 부탁드립니다. 재사용 중이라면 분리(composition) 또는 prop 기반 분기 구조가 필요합니다.

import LoadingSpinner from '@/components/common/LoadingSpinner';

const MyFeed = ({ showHeader, posts = [], isMyFeed, isLast = false }: FeedListProps) => {
const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Props 타입을 좁혀 불필요한 prop 전달을 방지하세요

공개 API에서 isMyFeed를 제거했지만, 현재 시그니처가 여전히 FeedListProps를 사용하고 있어 호출부가 isMyFeed를 넘겨도 TS에서 잡히지 않고 컴포넌트 내부에서 무시됩니다. 혼란 방지를 위해 타입을 좁히는 것을 권장합니다.

-const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {
+const MyFeed = (
+  { showHeader, posts = [], isLast = false }: Omit<FeedListProps, 'isMyFeed'>
+) => {

호출부에 남아있는 isMyFeed 전달을 점검하려면 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# MyFeed에 isMyFeed prop을 전달하는 사용처 탐지
rg -nP --type=tsx -C2 '<MyFeed[^>]*\bisMyFeed\s*='

Length of output: 80


아래 스크립트로 isMyFeed prop 전달 여부를 다시 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# MyFeed에 isMyFeed prop 전달 여부 확인
rg -nP -C2 "<MyFeed[^>]*\bisMyFeed\s*=" -g '*.tsx'

Length of output: 330


Props 타입 좁히기 및 호출부 isMyFeed 제거

MyFeed 컴포넌트에서 더 이상 사용하지 않는 isMyFeed prop을 타입에서 제외하고, 호출부에서도 관련 전달을 제거하세요.

문제 요약

  • 공개 API에서 isMyFeed를 삭제했음에도 여전히 FeedListProps에 포함되어 있어, 호출부에서 넘겨도 TS가 잡아내지 않고 무시됩니다.
  • 컴포넌트 시그니처를 좁히면 불필요한 prop 전달을 컴파일 단계에서 차단할 수 있습니다.

수정할 위치

  • src/components/feed/MyFeed.tsx
  • src/pages/feed/Feed.tsx (라인 185)

제안하는 변경사항

  1. MyFeed 시그니처 수정 (타입에서 isMyFeed 제외)

    -const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {
    +const MyFeed = (
    +  { showHeader, posts = [], isLast = false }: Omit<FeedListProps, 'isMyFeed'>
    +) => {
  2. 호출부에서 isMyFeed prop 제거

    -<MyFeed showHeader={false} posts={myFeedPosts} isMyFeed={true} isLast={myIsLast} />
    +<MyFeed showHeader={false} posts={myFeedPosts} isLast={myIsLast} />
📝 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.

Suggested change
const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {
// In src/components/feed/MyFeed.tsx
const MyFeed = (
{ showHeader, posts = [], isLast = false }: Omit<FeedListProps, 'isMyFeed'>
) => {
🤖 Prompt for AI Agents
In src/components/feed/MyFeed.tsx around line 12 and in src/pages/feed/Feed.tsx
at line 185, remove the unused isMyFeed property from the FeedListProps type and
from the MyFeed component signature so the prop is no longer accepted; then
update the caller in Feed.tsx (line 185) to stop passing isMyFeed. Specifically,
narrow the FeedListProps interface/type to exclude isMyFeed, change the MyFeed
parameter list to only accept the actual props (showHeader, posts, isLast), and
delete the isMyFeed argument from the MyFeed JSX invocation in Feed.tsx.

Comment on lines 76 to +78
const handleService = () => {
window.open('https://slashpage.com/thip/xjqy1g2vw7vejm6vd54z', '_blank');
window.open('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz', '_blank');
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

보안: window.open('_blank') 사용 시 reverse tabnabbing 방어 추가 필요

외부 링크를 새 탭으로 열 때는 opener 참조 차단이 필요합니다. 현재 구현은 탭 하이재킹 위험이 있습니다. noopener,noreferrer를 사용하거나, 공용 헬퍼로 처리해 주세요.

아래처럼 교체하는 것을 권장합니다.

-  const handleService = () => {
-    window.open('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz', '_blank');
-  };
+  const handleService = () => {
+    openExternal('https://slashpage.com/thip/dk58wg2e6yy3zmnqevxz');
+  };

-  const handleTerms = () => {
-    window.open('https://slashpage.com/thip/7916x82r8y74n24kpyg3', '_blank');
-  };
+  const handleTerms = () => {
+    openExternal('https://slashpage.com/thip/7916x82r8y74n24kpyg3');
+  };

-  const handleVersion = () => {
-    window.open('https://slashpage.com/thip/1q3vdn2p9w93pmxy49pr', '_blank');
-  };
+  const handleVersion = () => {
+    openExternal('https://slashpage.com/thip/1q3vdn2p9w93pmxy49pr');
+  };

헬퍼 예시(이 파일 상단 또는 공용 유틸에 추가):

// utils/window.ts (예시)
export const openExternal = (url: string) => {
  const w = window.open(url, '_blank', 'noopener,noreferrer');
  if (w) w.opener = null;
};

또한, 동일한 형태의 외부 링크가 늘어나는 추세라면 URL들을 상수로 중앙집중화 하는 것도 유지보수에 유리합니다. 예: src/constants/externalLinks.ts에 보관.

Also applies to: 80-83, 84-86

🤖 Prompt for AI Agents
In src/pages/mypage/Mypage.tsx around lines 76-78 (and similarly at 80-83 and
84-86), the component opens external links with window.open(url, '_blank') which
is vulnerable to reverse tabnabbing; replace these direct calls with a safe
helper that opens the URL using window.open(url, '_blank',
'noopener,noreferrer') and then sets the returned window's opener to null if
present, and update the call sites to use this helper; additionally consider
centralizing external URLs into a constants file (e.g.,
src/constants/externalLinks.ts) and reference those constants from the
component.

Copy link

@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: 1

🧹 Nitpick comments (6)
src/api/feeds/getSavedFeedsInMy.ts (2)

25-29: nextCursor의 null 가능성 반영 필요

SavePage에서 feedNextCursorstring | null로 다루고 있고, 마지막 페이지에서는 커서가 null/없음으로 내려오는 API가 흔합니다. 런타임 스키마가 null을 허용한다면 타입을 아래처럼 정렬하는 것을 권장합니다.

적용 제안:

 export interface SavedFeedsInMyData {
   feedList: SavedFeedInMy[];
-  nextCursor: string;
+  nextCursor: string | null;
   isLast: boolean;
 }

39-55: 반환 타입 명시 및 params 타입 좁히기

가독성과 오타 방지를 위해 반환 타입을 명시하고, 실제로는 null을 쿼리스트링으로 보내지 않으므로 params 타입에서 null을 제거하면 좋습니다.

적용 제안:

-export const getSavedFeedsInMy = async (cursor: string | null = null) => {
+export const getSavedFeedsInMy = async (
+  cursor: string | null = null,
+): Promise<SavedFeedsInMyResponse> => {
   try {
-    const params: { cursor?: string | null } = {};
+    const params: { cursor?: string } = {};
     if (cursor !== null) {
       params.cursor = cursor;
     }

-    const response = await apiClient.get<SavedFeedsInMyResponse>('/feeds/saved', {
+    const response = await apiClient.get<SavedFeedsInMyResponse>('/feeds/saved', {
       params,
     });
     return response.data;
   } catch (error) {
     console.error('개인 저장 피드 조회 API 오류:', error);
     throw error;
   }
 };
src/api/books/getSavedBooksInMy.ts (1)

27-36: 반환 타입을 명시해 타입 안정성 향상

TS가 추론하긴 하지만, API 헬퍼는 반환 타입을 명시하는 편이 유지보수에 유리합니다.

적용 제안:

-export const getSavedBooksInMy = async () => {
+export const getSavedBooksInMy = async (): Promise<SavedBooksInMyResponse> => {
   try {
     const response = await apiClient.get<SavedBooksInMyResponse>('/books/saved');
     return response.data;
   } catch (error) {
     console.error('개인 저장 책 조회 API 오류:', error);
     throw error;
   }
 };
src/pages/mypage/SavePage.tsx (3)

31-33: 무한 스크롤 중복 호출 방지를 위한 로딩 ref 추가 제안

state 기반 로딩 플래그만으로는 IntersectionObserver 콜백의 빠른 연속 이벤트에서 중복 호출이 발생할 수 있습니다. ref 기반 락을 추가해 방지하는 것을 권장합니다.

아래 줄을 추가하세요:

   // Intersection Observer ref
   const feedObserverRef = useRef<HTMLDivElement>(null);
+  const feedLoadingRef = useRef(false);

71-77: 탭 전환 시 피드 상태 초기화로 UX 플리커 방지

'책' → '피드' 전환 시 이전 목록이 잠시 보였다가 새 데이터로 바뀔 수 있습니다. 전환 직후 상태를 초기화해 깔끔한 UX를 권장합니다.

적용 제안:

   useEffect(() => {
     if (activeTab === '피드') {
+      setSavedFeeds([]);
+      setFeedNextCursor(null);
+      setFeedIsLast(false);
       loadSavedFeeds(null);
     }
   }, [activeTab, loadSavedFeeds]);

85-104: Observer 생성 조건/의존성 최적화 및 조건식 안전화

  • '피드' 탭이 아닐 때 Observer를 만들지 않도록 가드하면 불필요한 생성/해제가 줄어듭니다.
  • 조건식에서 feedNextCursor != null로 체크하면 빈 문자열 등의 예외를 안전하게 처리합니다.
  • 프리페치 여유를 위해 rootMargin을 추가하면 스크롤 체감이 좋아집니다.

적용 제안:

   useEffect(() => {
-    const observer = new IntersectionObserver(
+    if (activeTab !== '피드') return;
+    const observer = new IntersectionObserver(
       entries => {
         entries.forEach(entry => {
-          if (entry.isIntersecting && !feedIsLast && !feedLoading && feedNextCursor) {
-            loadSavedFeeds(feedNextCursor);
+          if (entry.isIntersecting && !feedIsLast && !feedLoadingRef.current && feedNextCursor != null) {
+            loadSavedFeeds(feedNextCursor);
           }
         });
       },
-      { threshold: 0.1 },
+      { threshold: 0.1, rootMargin: '200px 0px' },
     );

     if (feedObserverRef.current) {
       observer.observe(feedObserverRef.current);
     }

     return () => observer.disconnect();
-  }, [feedIsLast, feedLoading, feedNextCursor, loadSavedFeeds]);
+  }, [activeTab, feedIsLast, feedNextCursor, loadSavedFeeds]);
📜 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 ce8b05e and c2451e7.

📒 Files selected for processing (4)
  • src/api/books/getSavedBooksInMy.ts (1 hunks)
  • src/api/feeds/getSavedFeedsInMy.ts (1 hunks)
  • src/components/common/LoadingSpinner.tsx (1 hunks)
  • src/pages/mypage/SavePage.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/api/books/getSavedBooksInMy.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/api/feeds/getSavedFeedsInMy.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/pages/mypage/SavePage.tsx (5)
src/api/feeds/getSavedFeedsInMy.ts (2)
  • SavedFeedInMy (4-22)
  • getSavedFeedsInMy (40-55)
src/api/books/getSavedBooksInMy.ts (2)
  • SavedBookInMy (4-12)
  • getSavedBooksInMy (28-36)
src/api/books/postSaveBook.ts (1)
  • postSaveBook (22-32)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (2)
  • BookList (159-163)
  • BookItem (165-177)
src/components/memory/PageRangeModal.styled.ts (1)
  • Title (43-48)
🔇 Additional comments (3)
src/components/common/LoadingSpinner.tsx (1)

12-12: LoadingSpinner 사용 시 message prop 확인 필요

로딩 스피너의 기본 메시지가 빈 문자열('')로 변경되었습니다.
아이콘만 표시하려는 의도라면 별도 조치가 필요 없지만, “로딩 중…”과 같은 텍스트가 필요하다면 아래 위치에 message="로딩 중…", 또는 원하는 메시지를 명시적으로 넘겨주세요.

• src/pages/today-words/TodayWords.tsx 라인 290
<LoadingSpinner />
• src/pages/today-words/TodayWords.tsx 라인 302
<LoadingSpinner />
• src/pages/mypage/SavePage.tsx 라인 153
<LoadingSpinner fullHeight={false} size="small" />

src/pages/mypage/SavePage.tsx (2)

152-154: 무한 스크롤 로딩 스피너 사용 LGTM

LoadingSpinner의 기본 message가 빈 문자열로 바뀌어 텍스트가 노출되지 않는 현재 사용 방식과도 잘 맞습니다. 스켈레톤이 필요할 정도의 로딩 시간이 아니라면 현재 접근이 충분합니다.


142-148: FeedPost의 isMyFeed prop 사용 확인 완료

  • src/components/feed/FeedPost.tsx 내 FeedPostPropsisMyFeed가 여전히 정의되어 있습니다.
  • 컴포넌트 구현부에서도 isMyFeed를 받아 PostFooter isMyFeed={!!isMyFeed}로 전달·사용 중입니다.
  • 따라서 SavePage에서 isMyFeed={false}를 넘겨주는 것은 올바르며, “MyFeed 관련 isMyFeed 제거”라는 PR 요약은 실제 코드와 불일치합니다.
  • PR 설명을 이 사실에 맞게 업데이트해 주세요.

Comment on lines +38 to +59
// 저장된 피드 로드
const loadSavedFeeds = useCallback(async (cursor: string | null = null) => {
try {
setFeedLoading(true);
const response = await getSavedFeedsInMy(cursor);

if (cursor === null) {
// 첫 로드
setSavedFeeds(response.data.feedList);
} else {
// 추가 로드
setSavedFeeds(prev => [...prev, ...response.data.feedList]);
}

setFeedNextCursor(response.data.nextCursor);
setFeedIsLast(response.data.isLast);
} catch (error) {
console.error('저장된 피드 로드 실패:', error);
} finally {
setFeedLoading(false);
}
}, []);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

loadSavedFeeds 중복 요청 방지 및 안정성 개선

IntersectionObserver가 동일 커서로 여러 번 트리거될 여지가 있습니다. 함수 초반에 ref 기반 가드와 락 설정/해제를 추가하면 안정적입니다.

적용 제안:

   const loadSavedFeeds = useCallback(async (cursor: string | null = null) => {
     try {
-      setFeedLoading(true);
+      if (feedLoadingRef.current) return;
+      setFeedLoading(true);
+      feedLoadingRef.current = true;
       const response = await getSavedFeedsInMy(cursor);

       if (cursor === null) {
         // 첫 로드
         setSavedFeeds(response.data.feedList);
       } else {
         // 추가 로드
         setSavedFeeds(prev => [...prev, ...response.data.feedList]);
       }

       setFeedNextCursor(response.data.nextCursor);
       setFeedIsLast(response.data.isLast);
     } catch (error) {
       console.error('저장된 피드 로드 실패:', error);
     } finally {
-      setFeedLoading(false);
+      setFeedLoading(false);
+      feedLoadingRef.current = false;
     }
   }, []);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/mypage/SavePage.tsx around lines 38 to 59, loadSavedFeeds can be
called multiple times for the same cursor by IntersectionObserver; add a
ref-based guard and a loading lock to prevent duplicate/concurrent requests:
create refs (e.g., currentCursorRef and isLoadingRef), at the start return early
if isLoadingRef.current is true or if cursor === currentCursorRef.current, then
set isLoadingRef.current = true and currentCursorRef.current = cursor before
calling getSavedFeedsInMy, and in the finally block set isLoadingRef.current =
false (and if cursor is null also reset currentCursorRef.current appropriately);
ensure setFeedLoading remains for UI but the refs control request concurrency
and duplicate cursor calls.

Copy link

@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: 1

♻️ Duplicate comments (2)
src/pages/mypage/SavePage.tsx (2)

31-36: 무한 스크롤 중복 요청 방지를 위한 ref 도입

동시성 제어와 동일 커서 중복 요청 차단을 위해 아래 ref를 추가하세요. loadSavedFeeds에서 사용하는 전역적 락/커서 가드입니다.

   // 초기 로딩 상태
   const [initialLoading, setInitialLoading] = useState(true);

+  // 무한 스크롤 중복 요청/커서 가드
+  const feedLoadingRef = useRef(false);
+  const feedCursorRef = useRef<string | null>(null);
+
   // Intersection Observer ref
   const feedObserverRef = useRef<HTMLDivElement>(null);

41-62: API 래퍼 반환 타입 오해로 인한 undefined 접근 + 무한 스크롤 중복 호출 가드 추가 제안

  • getSavedFeedsInMy는 response.data가 아닌 data 자체를 반환합니다. 현재 response.data.* 접근은 undefined를 유발합니다.
  • IntersectionObserver가 같은 커서로 중복 호출을 트리거할 수 있습니다. setState만으로는 동시성 가드가 약하므로 ref 기반 락을 권장합니다. (이 점은 이전 코멘트에서도 지적되었습니다.)

아래 패치로 데이터 접근 버그를 수정하고, ref 락/커서 가드를 도입하세요. 이 변경은 [22-36행 코멘트]에서 제안한 ref 선언을 전제로 합니다.

   const loadSavedFeeds = useCallback(async (cursor: string | null = null) => {
     try {
-      setFeedLoading(true);
-      const response = await getSavedFeedsInMy(cursor);
+      // 중복 호출 가드 (ref 기반)
+      if (feedLoadingRef.current) return;
+      if (cursor !== null && cursor === feedCursorRef.current) return;
+
+      setFeedLoading(true);
+      feedLoadingRef.current = true;
+      feedCursorRef.current = cursor;
+
+      const data = await getSavedFeedsInMy(cursor);

-      if (cursor === null) {
+      if (cursor === null) {
         // 첫 로드
-        setSavedFeeds(response.data.feedList);
+        setSavedFeeds(data.feedList);
       } else {
         // 추가 로드
-        setSavedFeeds(prev => [...prev, ...response.data.feedList]);
+        setSavedFeeds(prev => [...prev, ...data.feedList]);
       }

-      setFeedNextCursor(response.data.nextCursor);
-      setFeedIsLast(response.data.isLast);
+      setFeedNextCursor(data.nextCursor);
+      setFeedIsLast(data.isLast);
     } catch (error) {
       console.error('저장된 피드 로드 실패:', error);
     } finally {
       setFeedLoading(false);
+      feedLoadingRef.current = false;
     }
   }, []);
🧹 Nitpick comments (3)
src/pages/mypage/SavePage.tsx (3)

95-111: IntersectionObserver 사전 로딩 및 의존성 최적화

  • rootMargin을 사용해 바닥 근처에서 미리 불러오면 UX가 부드러워집니다.
  • feedLoading 상태 의존성을 제거하고 ref를 사용하면 불필요한 observer 재생성을 줄일 수 있습니다.
-    const observer = new IntersectionObserver(
+    const observer = new IntersectionObserver(
       entries => {
         entries.forEach(entry => {
-          if (entry.isIntersecting && !feedIsLast && !feedLoading && feedNextCursor) {
+          if (entry.isIntersecting && !feedIsLast && !feedLoadingRef.current && feedNextCursor) {
             loadSavedFeeds(feedNextCursor);
           }
         });
       },
-      { threshold: 0.1 },
+      { threshold: 0.1, rootMargin: '200px 0px' },
     );
@@
-  }, [feedIsLast, feedLoading, feedNextCursor, loadSavedFeeds]);
+  }, [feedIsLast, feedNextCursor, loadSavedFeeds]);

117-134: 저장 토글 UX 개선: 낙관적 업데이트/중복 클릭 방지/롤백 고려

  • 클릭 시 즉시 UI에 반영(낙관적 업데이트)하고 실패 시 롤백하면 체감 속도가 개선됩니다.
  • 동일 ISBN 연타를 막기 위한 per-item 진행중 상태가 있으면 안전합니다.
  • “저장 목록”에서 해제 시 목록에서 제거할지(현재는 isSaved=false만 반영) 정책 확인 필요.

원하시면 per-item saving flag(ref 또는 state) 적용과 낙관적 업데이트/롤백 패턴을 반영한 코드 패치를 제안하겠습니다.


130-131: 불필요한 console.log 제거

운영 로그 오염을 방지하기 위해 제거 또는 dev 환경에서만 출력되도록 해주세요.

-      console.log('저장 토글:', isbn, newSaveState);
📜 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 c2451e7 and 4789e6c.

📒 Files selected for processing (1)
  • src/pages/mypage/SavePage.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/mypage/SavePage.tsx (4)
src/api/feeds/getSavedFeedsInMy.ts (2)
  • SavedFeedInMy (4-22)
  • getSavedFeedsInMy (40-55)
src/api/books/getSavedBooksInMy.ts (2)
  • SavedBookInMy (4-12)
  • getSavedBooksInMy (28-36)
src/api/books/postSaveBook.ts (1)
  • postSaveBook (22-32)
src/components/memory/PageRangeModal.styled.ts (1)
  • Title (43-48)
🔇 Additional comments (1)
src/pages/mypage/SavePage.tsx (1)

151-157: FeedPost의 isMyFeed prop 사용 여부 확인 필요

최근 변경으로 MyFeed 쪽 공개 API에서 isMyFeed가 제거되었다는 맥락이 있습니다. 현재 SavePage는 FeedPost에 isMyFeed={false}를 전달합니다. FeedPost의 최신 props 정의에서 isMyFeed가 여전히 필요한지 확인해주세요. 불필요하다면 제거가 깔끔합니다.

Comment on lines +71 to +83
const [feedsResponse, booksResponse] = await Promise.all([
getSavedFeedsInMy(null),
getSavedBooksInMy(),
]);

// 피드 데이터 설정
setSavedFeeds(feedsResponse.data.feedList);
setFeedNextCursor(feedsResponse.data.nextCursor);
setFeedIsLast(feedsResponse.data.isLast);

// 책 데이터 설정
setSavedBooks(booksResponse.data.bookList);
} catch (error) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

초기 데이터 병렬 로드에서도 반환 타입 오해로 인한 접근 버그

getSavedFeedsInMy/getSavedBooksInMy 모두 data 자체를 반환합니다. *.data.* 접근을 제거해야 합니다.

-        const [feedsResponse, booksResponse] = await Promise.all([
+        const [feedsData, booksData] = await Promise.all([
           getSavedFeedsInMy(null),
           getSavedBooksInMy(),
         ]);

         // 피드 데이터 설정
-        setSavedFeeds(feedsResponse.data.feedList);
-        setFeedNextCursor(feedsResponse.data.nextCursor);
-        setFeedIsLast(feedsResponse.data.isLast);
+        setSavedFeeds(feedsData.feedList);
+        setFeedNextCursor(feedsData.nextCursor);
+        setFeedIsLast(feedsData.isLast);

         // 책 데이터 설정
-        setSavedBooks(booksResponse.data.bookList);
+        setSavedBooks(booksData.bookList);

추가 제안: 한쪽 API 실패 시 다른 데이터는 보여주고 싶다면 Promise.all 대신 allSettled로 부분 성공을 허용하는 패턴을 고려하세요. 필요하시면 패치 제안 드리겠습니다.

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

Suggested change
const [feedsResponse, booksResponse] = await Promise.all([
getSavedFeedsInMy(null),
getSavedBooksInMy(),
]);
// 피드 데이터 설정
setSavedFeeds(feedsResponse.data.feedList);
setFeedNextCursor(feedsResponse.data.nextCursor);
setFeedIsLast(feedsResponse.data.isLast);
// 책 데이터 설정
setSavedBooks(booksResponse.data.bookList);
} catch (error) {
const [feedsData, booksData] = await Promise.all([
getSavedFeedsInMy(null),
getSavedBooksInMy(),
]);
// 피드 데이터 설정
setSavedFeeds(feedsData.feedList);
setFeedNextCursor(feedsData.nextCursor);
setFeedIsLast(feedsData.isLast);
// 책 데이터 설정
setSavedBooks(booksData.bookList);
} catch (error) {
🤖 Prompt for AI Agents
In src/pages/mypage/SavePage.tsx around lines 71 to 83, the code incorrectly
assumes the API helpers return an object with a data field and uses
feedsResponse.data.feedList / booksResponse.data.bookList; both
getSavedFeedsInMy and getSavedBooksInMy return the data directly, so remove the
extra `.data` access and use feedsResponse.feedList / feedsResponse.nextCursor /
feedsResponse.isLast and booksResponse.bookList instead; optionally, if you want
partial results when one API fails, replace Promise.all with Promise.allSettled
and handle fulfilled/rejected cases accordingly.

@heeeeyong heeeeyong merged commit 62c2187 into develop Aug 18, 2025
3 checks passed
@heeeeyong heeeeyong self-assigned this Aug 18, 2025
@heeeeyong heeeeyong added ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant