Skip to content

fix: 발견 탭 필터 칩 전체 표시 및 모달 북마크 API 연동#99

Open
ballsona wants to merge 5 commits into
developfrom
feat/#91-discover-api
Open

fix: 발견 탭 필터 칩 전체 표시 및 모달 북마크 API 연동#99
ballsona wants to merge 5 commits into
developfrom
feat/#91-discover-api

Conversation

@ballsona

Copy link
Copy Markdown
Collaborator

📝 작업 내용

📸 스크린샷

📌 PR 중요도

  • 🔴 High: 중요한 기능 추가/버그 수정 (반드시 리뷰 필요)
  • 🟡 Medium: 일반적인 기능 개선
  • 🟢 Low: 사소한 수정/문서 업데이트

💬 기타 사항

ballsona added 5 commits June 18, 2026 00:10
- Discovery DTO 타입 및 API 함수/쿼리 훅 추가 (피드, 검색)
- 스크랩 PUT/DELETE API 및 optimistic update 뮤테이션 훅 추가
- 75% 스크롤 임계치 기반 무한 스크롤 훅 추가
- DiscoverFeed, DiscoverSearchResults 실API 연동 완료
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
first-penguins-fe Ready Ready Preview, Comment Jun 17, 2026 5:06pm

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Discover 탭의 피드·검색 위젯을 MOCK 데이터에서 실제 API 기반으로 전환했다. Discovery DTO 타입, API 함수, TanStack 무한 쿼리 훅, 낙관적 업데이트를 포함한 스크랩 뮤테이션, 공용 useInfiniteScroll 훅을 추가하고, 장르 필터 값·공용 컴포넌트·NavBar 스크롤 로직도 함께 조정했다.

Changes

Discovery 탭 API 연동 및 무한 스크롤

Layer / File(s) Summary
Discovery DTO 타입 및 배럴 export 확장
src/entities/post/model/post.types.ts, src/entities/post/index.ts
DiscoveryNeedTag, DiscoveryEmotion, DiscoveryQuoteDto, DiscoveryQuoteSearchDto, DiscoveryQuoteListResponse, DiscoveryQuoteSearchListResponse 타입을 추가하고 배럴 export를 확장했다.
Discovery API 함수 및 무한 쿼리 훅
src/entities/post/api/postApi.ts, src/entities/post/api/postQueries.ts
fetchDiscoveryQuotes·fetchDiscoveryQuotesSearch API 함수와 discoveryKeys, useDiscoveryFeedQuery·useDiscoverySearchQuery 무한 쿼리 훅을 구현했다.
스크랩 API 및 낙관적 업데이트 뮤테이션
src/features/post-bookmark/api/scrapApi.ts, src/features/post-bookmark/model/useScrapMutation.ts, src/features/post-bookmark/index.ts
scrapQuote(PUT)·unscrapQuote(DELETE)를 추가하고, useScrapMutation에서 피드/검색 캐시 낙관적 패치·실패 롤백·쿼리 무효화 흐름을 구현했다.
공용 useInfiniteScroll 훅
src/shared/hooks/useInfiniteScroll.ts
scroll 이벤트로 스크롤 비율이 threshold(기본 0.75)를 초과하면 onLoad를 호출하는 훅을 추가하고 컨테이너 연결용 ref를 반환한다.
장르 필터 값 정규화
src/features/discover-filter/model/useDiscoverFilter.ts, src/features/discover-filter/ui/GenreFilterChips.tsx, src/features/discover-filter/ui/*.stories.tsx
GENRE_FILTERS 배열과 초기값을 "전체" → "모든 장르"로 변경하고, 칩 표시 시 "모든 장르"를 "전체"로 치환하도록 수정했다. Storybook 초기값도 동기화했다.
PostListItem·PostShareModal 공용 컴포넌트 조정
src/entities/post/ui/PostListItem.tsx, src/widgets/post-share-modal/ui/PostShareModal.tsx
emotionTag/toneTag를 선택 prop으로 변경하고 toneTag 우선 조건부 렌더링으로 수정했다. PostShareModalonToggleBookmark 콜백을 추가해 handleToggleBookmark로 외부 호출을 연결했다.
DiscoverFeed·DiscoverSearchResults 위젯 실API 전환
src/widgets/discover-feed/ui/DiscoverFeed.tsx, src/widgets/discover-search-results/ui/DiscoverSearchResults.tsx
MOCK 데이터를 제거하고 쿼리 훅 기반 무한 스크롤 렌더링으로 교체했다. getMood·formatRelativeTime 유틸, selectedQuote 상태, useScrapMutation 연결, 로딩/빈 결과 UI를 구현했다.
NavBar 스크롤 숨김 로직 제거 및 기타
src/widgets/nav-bar/ui/NavBar.tsx, src/app/layout.tsx, docs/plans/discover-tab-api.md
NavBar에서 scroll 이벤트 기반 가시성 토글 로직을 제거하고, metadatametadataBase URL을 추가했다. Discovery API 연동 계획 문서를 신규 추가했다.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Widget as DiscoverFeed / DiscoverSearchResults
  participant InfiniteScroll as useInfiniteScroll
  participant Query as useDiscoveryFeedQuery / useDiscoverySearchQuery
  participant ScrapMutation as useScrapMutation
  participant Server as Discovery API Server

  User->>Widget: 탭 진입 / 검색어 입력
  Widget->>Query: genre / query, sort 전달
  Query->>Server: GET /discovery/quotes[/search]?cursor=&genre=
  Server-->>Query: DiscoveryQuoteListResponse (items, nextCursor, hasNext)
  Query-->>Widget: quotes 배열 렌더링

  User->>InfiniteScroll: 스크롤 하단 도달 (threshold 초과)
  InfiniteScroll->>Widget: onLoad() 콜백 호출
  Widget->>Query: fetchNextPage()
  Query->>Server: GET /discovery/quotes?cursor={nextCursor}
  Server-->>Query: 다음 페이지 데이터
  Query-->>Widget: 추가 quotes 평탄화 렌더링

  User->>Widget: 북마크 버튼 클릭
  Widget->>ScrapMutation: toggle(quoteId, currentIsScrapped)
  ScrapMutation->>Query: cancelQueries + 낙관적 isScrapped 패치
  ScrapMutation->>Server: PUT or DELETE /quotes/{quoteId}/scrap
  alt 성공
    Server-->>ScrapMutation: 200 OK
  else 실패
    ScrapMutation->>Query: 이전 캐시 롤백
  end
  ScrapMutation->>Query: invalidateQueries(discoveryKeys.all)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • depromeet/18th-team1-FE#76: widgets/discover-feed, widgets/discover-search-results, widgets/post-share-modal의 초기 UI 구현 범위를 직접 이어받아 MOCK 데이터를 실제 API로 교체하는 변경이므로 코드 수준에서 연관된다.

Suggested labels

🔗API, ✨Feature

Suggested reviewers

  • nakyeongg
  • cindy-chaewon
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿 구조만 포함하고 있으며, 실제 작업 내용, 변경 동기, 영향도에 대한 구체적인 정보가 전혀 제공되지 않음. PR 설명에 변경사항의 목적, 주요 구현 내용, 테스트 사항 등 구체적인 정보를 작성해주세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '발견 탭 필터 칩 전체 표시 및 모달 북마크 API 연동'으로, 전체 변경사항의 핵심 부분을 일부 다루지만 주요 변경인 Discovery API 통합 구현을 충분히 반영하지 못함.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#91-discover-api
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/#91-discover-api

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@ballsona ballsona changed the title Feat/#91 discover api fix: 발견 탭 필터 칩 전체 표시 및 모달 북마크 API 연동 Jun 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/discover-filter/ui/GenreFilterChips.tsx (1)

14-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

필터 칩 선택 상태를 접근성 속성으로 노출해주세요.

현재 선택 여부가 시각 스타일에만 반영됩니다. 토글 버튼 패턴이므로 aria-pressed를 함께 넣는 것이 좋습니다.

수정 제안
       <button
         key={genre}
         type="button"
         onClick={() => onChange(genre)}
+        aria-pressed={selected === genre}
         className={`caption1 shrink-0 whitespace-nowrap rounded-full px-3 py-1 ${
           selected === genre
             ? "bg-gray-700 text-white"
             : "border border-gray-200 bg-background text-gray-500"
         }`}
       >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/discover-filter/ui/GenreFilterChips.tsx` around lines 14 - 25,
The button element in the GenreFilterChips component lacks accessibility
attributes to communicate its selection state to assistive technologies. Add the
aria-pressed attribute to the button element, setting its value to true when
selected === genre and false otherwise, so that screen readers and other
assistive tools can properly convey the toggle button's current state to users.
🧹 Nitpick comments (3)
src/features/post-bookmark/model/useScrapMutation.ts (1)

25-25: ⚡ Quick win

약어 변수명 q는 가독성과 가이드라인 일관성을 깨뜨립니다.

의미가 명확한 이름으로 바꿔 주세요.

✏️ 제안 수정
-      quotes: page.quotes.map((q) => (q.quoteId === quoteId ? { ...q, isScrapped } : q)),
+      quotes: page.quotes.map((quote) =>
+        quote.quoteId === quoteId ? { ...quote, isScrapped } : quote,
+      ),

As per coding guidelines, "Avoid abbreviations in variable and function names (e.g., use button instead of btn)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/post-bookmark/model/useScrapMutation.ts` at line 25, The
variable name `q` used as the parameter in the map function is an abbreviation
that violates coding guidelines and reduces readability. Replace the abbreviated
parameter name `q` with a more descriptive name such as `quote` throughout the
arrow function in the quotes map operation. This means updating the parameter
itself and all references to `q` within the function body (including `q.quoteId`
and the spread operator `...q`) to use the new descriptive variable name.

Source: Coding guidelines

src/shared/hooks/useInfiniteScroll.ts (1)

11-11: ⚡ Quick win

el 대신 의미 있는 변수명을 사용해 주세요.

As per coding guidelines, "Avoid abbreviations in variable and function names (e.g., use button instead of btn)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/hooks/useInfiniteScroll.ts` at line 11, The variable name `el` in
the useInfiniteScroll hook is an abbreviation that violates the coding
guidelines requiring full, meaningful variable names. Rename the `el` variable
to a more descriptive name such as `element` or `scrollElement` (choose based on
the actual purpose of the variable in the context where it's used) to improve
code readability and consistency with the guidelines that recommend avoiding
abbreviations like `el` in favor of complete names.

Source: Coding guidelines

src/widgets/discover-feed/ui/DiscoverFeed.tsx (1)

16-31: ⚡ Quick win

시간/무드 포맷터가 검색 위젯과 중복되어 유지보수 포인트가 분산됩니다.

동일 로직(getMood, formatRelativeTime)이 DiscoverSearchResults에도 복제되어 있어 기준 변경 시 쉽게 드리프트가 납니다. 공용 유틸로 추출해 단일 소스로 관리하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/widgets/discover-feed/ui/DiscoverFeed.tsx` around lines 16 - 31, The
functions getMood and formatRelativeTime are duplicated in both DiscoverFeed.tsx
and DiscoverSearchResults, creating a maintenance risk where logic changes must
be applied in multiple locations. Extract both getMood and formatRelativeTime
functions into a shared utility module in the appropriate location (such as
src/widgets/common/utils or src/utils), then import and use these functions from
the utility in both DiscoverFeed.tsx and DiscoverSearchResults instead of
keeping local copies. This ensures a single source of truth for the formatting
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/entities/post/api/postQueries.ts`:
- Around line 47-65: The enabled condition in the useDiscoverySearchQuery
function currently checks only if query.length > 0, which allows whitespace-only
strings to trigger unnecessary API calls. Modify the enabled condition to use
query.trim().length > 0 instead to filter out whitespace-only queries.
Additionally, consider trimming the query parameter when passing it to
fetchDiscoveryQuotesSearch to ensure consistency and prevent sending empty or
whitespace-only search terms to the backend API.

In `@src/features/post-bookmark/model/useScrapMutation.ts`:
- Around line 62-70: The catch block in useScrapMutation rolls back the query
data by restoring previousFeed and previousSearch, but does not re-throw the
error after rollback. This causes the mutation to appear successful to the
caller even though it failed, preventing the caller from detecting the failure
or performing recovery actions like UI re-synchronization or displaying error
toasts. After the rollback loops that restore previousFeed and previousSearch,
add a throw statement to re-propagate the error to the caller so they can handle
the failure appropriately.

In `@src/shared/hooks/useInfiniteScroll.ts`:
- Around line 10-23: The useInfiniteScroll hook only calls stableOnLoad when a
scroll event occurs, which means if the initial content is too short to fill the
container, no scrolling is possible and the next page is never loaded. After the
addEventListener call in the useEffect hook, immediately invoke the handleScroll
function once to check the initial scroll position and trigger onLoad if needed,
ensuring that content loads even when the initial page doesn't fill the
viewport.

In `@src/widgets/discover-feed/ui/DiscoverFeed.tsx`:
- Around line 39-40: The useDiscoveryFeedQuery hook call is missing the isError
property in its destructuring, which prevents the component from distinguishing
between network/server errors and genuinely empty results. Add isError to the
destructured properties from useDiscoveryFeedQuery and then update the rendering
logic in the section around lines 63-103 to check for isError first and display
an appropriate error message to users, before falling through to the empty state
message. This ensures users see meaningful feedback about actual failures
instead of confusing "empty results" messages when the query encounters errors.

In `@src/widgets/discover-search-results/ui/DiscoverSearchResults.tsx`:
- Around line 50-52: The useDiscoverySearchQuery hook call in the
DiscoverSearchResults component is missing error state handling, causing search
failures to be displayed as empty results instead of showing a distinct error
UI. Add isError to the destructured values from useDiscoverySearchQuery, then
add a separate conditional branch to render error UI when isError is true, and
ensure the empty state display (referenced in the render logic around lines
108-148) only shows when the query is successful (not loading and not in error
state) but returns no data.

In `@src/widgets/post-share-modal/ui/PostShareModal.tsx`:
- Around line 11-12: The onToggleBookmark callback has a void return type which
prevents error handling and causes the local bookmark state to be toggled
immediately without waiting for the API operation to complete or fail. Change
the onToggleBookmark callback signature to return a Promise (async callback)
instead of void, then update all places where this callback is invoked
(including the calls in the locations around lines 30-33 and 67-71) to await the
promise before toggling the local bookmark state. This ensures the UI state only
updates after the API call succeeds, preventing the state from becoming out of
sync when the operation fails.

---

Outside diff comments:
In `@src/features/discover-filter/ui/GenreFilterChips.tsx`:
- Around line 14-25: The button element in the GenreFilterChips component lacks
accessibility attributes to communicate its selection state to assistive
technologies. Add the aria-pressed attribute to the button element, setting its
value to true when selected === genre and false otherwise, so that screen
readers and other assistive tools can properly convey the toggle button's
current state to users.

---

Nitpick comments:
In `@src/features/post-bookmark/model/useScrapMutation.ts`:
- Line 25: The variable name `q` used as the parameter in the map function is an
abbreviation that violates coding guidelines and reduces readability. Replace
the abbreviated parameter name `q` with a more descriptive name such as `quote`
throughout the arrow function in the quotes map operation. This means updating
the parameter itself and all references to `q` within the function body
(including `q.quoteId` and the spread operator `...q`) to use the new
descriptive variable name.

In `@src/shared/hooks/useInfiniteScroll.ts`:
- Line 11: The variable name `el` in the useInfiniteScroll hook is an
abbreviation that violates the coding guidelines requiring full, meaningful
variable names. Rename the `el` variable to a more descriptive name such as
`element` or `scrollElement` (choose based on the actual purpose of the variable
in the context where it's used) to improve code readability and consistency with
the guidelines that recommend avoiding abbreviations like `el` in favor of
complete names.

In `@src/widgets/discover-feed/ui/DiscoverFeed.tsx`:
- Around line 16-31: The functions getMood and formatRelativeTime are duplicated
in both DiscoverFeed.tsx and DiscoverSearchResults, creating a maintenance risk
where logic changes must be applied in multiple locations. Extract both getMood
and formatRelativeTime functions into a shared utility module in the appropriate
location (such as src/widgets/common/utils or src/utils), then import and use
these functions from the utility in both DiscoverFeed.tsx and
DiscoverSearchResults instead of keeping local copies. This ensures a single
source of truth for the formatting logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5499661a-700d-4aaf-978a-6ee2a16e8ba1

📥 Commits

Reviewing files that changed from the base of the PR and between e0e4695 and 8557112.

⛔ Files ignored due to path filters (3)
  • public/images/favicon.svg is excluded by !**/*.svg, !**/*.svg, !**/public/**
  • public/images/og_image.png is excluded by !**/*.png, !**/*.png, !**/public/**
  • src/app/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (19)
  • docs/plans/discover-tab-api.md
  • src/app/layout.tsx
  • src/entities/post/api/postApi.ts
  • src/entities/post/api/postQueries.ts
  • src/entities/post/index.ts
  • src/entities/post/model/post.types.ts
  • src/entities/post/ui/PostListItem.tsx
  • src/features/discover-filter/model/useDiscoverFilter.ts
  • src/features/discover-filter/ui/GenreFilterChips.stories.tsx
  • src/features/discover-filter/ui/GenreFilterChips.tsx
  • src/features/discover-filter/ui/GenreFilterDropdown.stories.tsx
  • src/features/post-bookmark/api/scrapApi.ts
  • src/features/post-bookmark/index.ts
  • src/features/post-bookmark/model/useScrapMutation.ts
  • src/shared/hooks/useInfiniteScroll.ts
  • src/widgets/discover-feed/ui/DiscoverFeed.tsx
  • src/widgets/discover-search-results/ui/DiscoverSearchResults.tsx
  • src/widgets/nav-bar/ui/NavBar.tsx
  • src/widgets/post-share-modal/ui/PostShareModal.tsx

Comment on lines +47 to +65
export const useDiscoverySearchQuery = (
query: string,
sort: "latest" | "scrap" = "latest",
genre?: string,
) =>
useInfiniteQuery({
queryKey: discoveryKeys.search(query, sort, genre),
queryFn: ({ pageParam }) =>
fetchDiscoveryQuotesSearch({
query,
sort,
cursor: pageParam as string | undefined,
genre,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined,
enabled: query.length > 0,
staleTime: 30 * 1000,

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 | 🟡 Minor | ⚡ Quick win

공백-only 검색어가 불필요한 API 호출을 유발합니다.

현재 enabled: query.length > 0 조건이라 공백 문자열도 요청이 나갑니다. trim() 기준으로 활성화/전달을 맞추는 게 안전합니다.

수정 제안
 export const useDiscoverySearchQuery = (
   query: string,
   sort: "latest" | "scrap" = "latest",
   genre?: string,
 ) =>
+  {
+    const normalizedQuery = query.trim();
+    return useInfiniteQuery({
+      queryKey: discoveryKeys.search(normalizedQuery, sort, genre),
+      queryFn: ({ pageParam }) =>
+        fetchDiscoveryQuotesSearch({
+          query: normalizedQuery,
+          sort,
+          cursor: pageParam as string | undefined,
+          genre,
+        }),
+      initialPageParam: undefined as string | undefined,
+      getNextPageParam: (lastPage) =>
+        lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined,
+      enabled: normalizedQuery.length > 0,
+      staleTime: 30 * 1000,
+    });
+  };
-  useInfiniteQuery({
-    queryKey: discoveryKeys.search(query, sort, genre),
-    queryFn: ({ pageParam }) =>
-      fetchDiscoveryQuotesSearch({
-        query,
-        sort,
-        cursor: pageParam as string | undefined,
-        genre,
-      }),
-    initialPageParam: undefined as string | undefined,
-    getNextPageParam: (lastPage) =>
-      lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined,
-    enabled: query.length > 0,
-    staleTime: 30 * 1000,
-  });
📝 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
export const useDiscoverySearchQuery = (
query: string,
sort: "latest" | "scrap" = "latest",
genre?: string,
) =>
useInfiniteQuery({
queryKey: discoveryKeys.search(query, sort, genre),
queryFn: ({ pageParam }) =>
fetchDiscoveryQuotesSearch({
query,
sort,
cursor: pageParam as string | undefined,
genre,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined,
enabled: query.length > 0,
staleTime: 30 * 1000,
export const useDiscoverySearchQuery = (
query: string,
sort: "latest" | "scrap" = "latest",
genre?: string,
) => {
const normalizedQuery = query.trim();
return useInfiniteQuery({
queryKey: discoveryKeys.search(normalizedQuery, sort, genre),
queryFn: ({ pageParam }) =>
fetchDiscoveryQuotesSearch({
query: normalizedQuery,
sort,
cursor: pageParam as string | undefined,
genre,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined,
enabled: normalizedQuery.length > 0,
staleTime: 30 * 1000,
});
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/post/api/postQueries.ts` around lines 47 - 65, The enabled
condition in the useDiscoverySearchQuery function currently checks only if
query.length > 0, which allows whitespace-only strings to trigger unnecessary
API calls. Modify the enabled condition to use query.trim().length > 0 instead
to filter out whitespace-only queries. Additionally, consider trimming the query
parameter when passing it to fetchDiscoveryQuotesSearch to ensure consistency
and prevent sending empty or whitespace-only search terms to the backend API.

Comment on lines +62 to +70
} catch (error) {
for (const [key, value] of previousFeed) {
queryClient.setQueryData(key, value);
}
for (const [key, value] of previousSearch) {
queryClient.setQueryData(key, value);
}
console.error("스크랩 처리 실패, 롤백됨:", error);
} finally {

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 | 🟠 Major | ⚡ Quick win

실패를 삼키면 호출자 상태가 성공으로 고정됩니다.

현재 롤백 후 에러를 다시 던지지 않아, 호출부가 실패를 감지하거나 후속 복구(UI 재동기화/토스트)를 할 수 없습니다. 롤백 뒤 throw로 전파해 주세요.

🔧 제안 수정
     } catch (error) {
       for (const [key, value] of previousFeed) {
         queryClient.setQueryData(key, value);
       }
       for (const [key, value] of previousSearch) {
         queryClient.setQueryData(key, value);
       }
       console.error("스크랩 처리 실패, 롤백됨:", error);
+      throw error;
     } finally {
       queryClient.invalidateQueries({ queryKey: discoveryKeys.all });
     }
📝 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
} catch (error) {
for (const [key, value] of previousFeed) {
queryClient.setQueryData(key, value);
}
for (const [key, value] of previousSearch) {
queryClient.setQueryData(key, value);
}
console.error("스크랩 처리 실패, 롤백됨:", error);
} finally {
} catch (error) {
for (const [key, value] of previousFeed) {
queryClient.setQueryData(key, value);
}
for (const [key, value] of previousSearch) {
queryClient.setQueryData(key, value);
}
console.error("스크랩 처리 실패, 롤백됨:", error);
throw error;
} finally {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/post-bookmark/model/useScrapMutation.ts` around lines 62 - 70,
The catch block in useScrapMutation rolls back the query data by restoring
previousFeed and previousSearch, but does not re-throw the error after rollback.
This causes the mutation to appear successful to the caller even though it
failed, preventing the caller from detecting the failure or performing recovery
actions like UI re-synchronization or displaying error toasts. After the
rollback loops that restore previousFeed and previousSearch, add a throw
statement to re-propagate the error to the caller so they can handle the failure
appropriately.

Comment on lines +10 to +23
useEffect(() => {
const el = ref.current;
if (!el) return;

const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el;
if ((scrollTop + clientHeight) / scrollHeight >= threshold) {
stableOnLoad();
}
};

el.addEventListener("scroll", handleScroll, { passive: true });
return () => el.removeEventListener("scroll", handleScroll);
}, [stableOnLoad, threshold]);

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 | 🟠 Major | ⚡ Quick win

초기 콘텐츠가 짧은 경우 다음 페이지 로드가 영구 정지될 수 있습니다.

현재는 scroll 이벤트가 발생해야만 onLoad가 실행됩니다. 첫 페이지가 컨테이너를 채우지 못하면 스크롤 자체가 불가능해 다음 페이지를 못 가져옵니다. 리스너 등록 직후 한 번 체크를 실행해 주세요.

🔧 제안 수정
     el.addEventListener("scroll", handleScroll, { passive: true });
+    handleScroll();
     return () => el.removeEventListener("scroll", handleScroll);
📝 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
useEffect(() => {
const el = ref.current;
if (!el) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el;
if ((scrollTop + clientHeight) / scrollHeight >= threshold) {
stableOnLoad();
}
};
el.addEventListener("scroll", handleScroll, { passive: true });
return () => el.removeEventListener("scroll", handleScroll);
}, [stableOnLoad, threshold]);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el;
if ((scrollTop + clientHeight) / scrollHeight >= threshold) {
stableOnLoad();
}
};
el.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
return () => el.removeEventListener("scroll", handleScroll);
}, [stableOnLoad, threshold]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/hooks/useInfiniteScroll.ts` around lines 10 - 23, The
useInfiniteScroll hook only calls stableOnLoad when a scroll event occurs, which
means if the initial content is too short to fill the container, no scrolling is
possible and the next page is never loaded. After the addEventListener call in
the useEffect hook, immediately invoke the handleScroll function once to check
the initial scroll position and trigger onLoad if needed, ensuring that content
loads even when the initial page doesn't fill the viewport.

Comment on lines +39 to +40
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
useDiscoveryFeedQuery(genre);

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 | 🟠 Major | ⚡ Quick win

쿼리 실패가 “빈 결과”로 오인 표시됩니다.

isError 분기가 없어 네트워크/서버 오류 시에도 빈 상태 메시지로 떨어집니다. 에러 상태를 별도로 렌더링해 사용자 혼란을 막아 주세요.

🔧 제안 수정
-  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
+  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } =
     useDiscoveryFeedQuery(genre);

@@
-        {!isLoading &&
+        {!isLoading && isError && (
+          <div className="flex items-center justify-center py-20">
+            <span className="caption1 text-gray-300">문장을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.</span>
+          </div>
+        )}
+
+        {!isLoading && !isError &&
           quotes.map((quote) => (
             ...
           ))}
@@
-        {!isLoading && quotes.length === 0 && (
+        {!isLoading && !isError && quotes.length === 0 && (
           <div className="flex items-center justify-center py-20">
             <span className="caption1 text-gray-300">아직 추천된 문장이 없어요.</span>
           </div>
         )}

Also applies to: 63-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/widgets/discover-feed/ui/DiscoverFeed.tsx` around lines 39 - 40, The
useDiscoveryFeedQuery hook call is missing the isError property in its
destructuring, which prevents the component from distinguishing between
network/server errors and genuinely empty results. Add isError to the
destructured properties from useDiscoveryFeedQuery and then update the rendering
logic in the section around lines 63-103 to check for isError first and display
an appropriate error message to users, before falling through to the empty state
message. This ensures users see meaningful feedback about actual failures
instead of confusing "empty results" messages when the query encounters errors.

Comment on lines +50 to +52
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
useDiscoverySearchQuery(initialQuery, activeSort, genre);

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 | 🟠 Major | ⚡ Quick win

검색 쿼리 실패가 빈 결과로 표시됩니다.

isError 분기가 없어 실패와 “결과 없음”이 구분되지 않습니다. 에러 UI를 분리하고 빈 상태는 성공 응답일 때만 보여 주세요.

Also applies to: 108-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/widgets/discover-search-results/ui/DiscoverSearchResults.tsx` around
lines 50 - 52, The useDiscoverySearchQuery hook call in the
DiscoverSearchResults component is missing error state handling, causing search
failures to be displayed as empty results instead of showing a distinct error
UI. Add isError to the destructured values from useDiscoverySearchQuery, then
add a separate conditional branch to render error UI when isError is true, and
ensure the empty state display (referenced in the render logic around lines
108-148) only shows when the query is successful (not loading and not in error
state) but returns no data.

Comment on lines +11 to 12
onToggleBookmark?: (currentIsBookmarked: boolean) => void;
}

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 | 🟠 Major | ⚡ Quick win

북마크 로컬 상태가 API 실패와 분리되어 잘못 고정될 수 있습니다.

콜백 시그니처가 void라 실패 전파를 받을 수 없고, 현재는 호출 직후 로컬 상태를 무조건 토글합니다. 비동기 콜백으로 바꾸고 성공 시에만 토글(또는 실패 시 롤백)해 주세요.

🔧 제안 수정
 interface PostShareModalProps {
   post: Post;
   isOpen: boolean;
   onClose: () => void;
-  onToggleBookmark?: (currentIsBookmarked: boolean) => void;
+  onToggleBookmark?: (currentIsBookmarked: boolean) => Promise<void>;
 }

-const handleToggleBookmark = () => {
-  onToggleBookmark?.(isBookmarked);
-  setIsBookmarked((prev) => !prev);
+const handleToggleBookmark = async () => {
+  try {
+    await onToggleBookmark?.(isBookmarked);
+    setIsBookmarked((prev) => !prev);
+  } catch {
+    // 실패 알림/복구 처리 지점
+  }
 };

Also applies to: 30-33, 67-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/widgets/post-share-modal/ui/PostShareModal.tsx` around lines 11 - 12, The
onToggleBookmark callback has a void return type which prevents error handling
and causes the local bookmark state to be toggled immediately without waiting
for the API operation to complete or fail. Change the onToggleBookmark callback
signature to return a Promise (async callback) instead of void, then update all
places where this callback is invoked (including the calls in the locations
around lines 30-33 and 67-71) to await the promise before toggling the local
bookmark state. This ensures the UI state only updates after the API call
succeeds, preventing the state from becoming out of sync when the operation
fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant