Api(client): Dashboard article 전체 조회 API 연결#82
Conversation
WalkthroughMyBookmark과 Remind 페이지에 타입 정의, Axios 페처, React Query 훅을 추가하고 컴포넌트를 API 호출로 전환하여 읽음/미읽음 북마크 및 리마인드 아티클을 페이지네이션·필터 파라미터로 조회하도록 연결했다. 모든 페처는 서버 응답의 Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Component
participant RQ as React Query Hook
participant AX as Axios Fetcher
participant API as Server API
UI->>RQ: useGetBookmarkArticles(page,size) / useGetBookmarkUnreadArticles(page,size)
RQ->>AX: getBookmarkArticles / getBookmarkUnreadArticles(page,size)
AX->>API: GET /api/v1/articles[(/unread)]?page=&size=
API-->>AX: 200 { data: { data: ... } }
AX-->>RQ: data.data
RQ-->>UI: UseQueryResult (data / error / status)
note right of RQ: 쿼리 키에 page,size 포함
sequenceDiagram
participant UI as Component
participant RQ as useGetRemindArticles
participant AX as getRemindArticles
participant API as /api/v1/articles/remind
UI->>RQ: useGetRemindArticles(nowDate,readStatus,page,size)
RQ->>AX: fetch(now, readStatus, page, size)
AX->>API: GET /api/v1/articles/remind?now=&readStatus=&page=&size=
API-->>AX: 200 { data: { data: ... } }
AX-->>RQ: data.data
RQ-->>UI: UseQueryResult (data / error / status)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks (4 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (7)
apps/client/src/pages/myBookmark/types/api.ts (2)
1-7: 아이템 타입도 export 해주세요BookmarkArticle를 외부에서 재사용할 가능성이 높습니다(예: 컴포넌트 props, 셀 렌더러). 현재 내부 타입이라 직접 참조가 불가합니다.
-interface BookmarkArticle { +export interface BookmarkArticle { articleId: number; url: string; memo: string; createdAt: string; isRead: boolean; }
11-19: 카운트 필드 네이밍 불일치 — 백엔드 의도 확인 또는 FE 정규화 적용 필요
- 문제: Bookmark는 totalArticle / totalUnreadArticle, Remind는 readArticleCount / unreadArticleCount로 불일치합니다.
- 위치: apps/client/src/pages/myBookmark/types/api.ts (lines 11–19), apps/client/src/pages/remind/types/api.ts (lines 19–20).
- 권장조치: (A) 백엔드 응답이 의도된 스키마라면 FE에서 axios 층에 정규화 함수 추가(totalArticle → totalCount, totalUnreadArticle → unreadCount 등). (B) 의도치 않은 불일치라면 백엔드와 스키마 통일.
apps/client/src/pages/remind/types/api.ts (2)
8-16: 아이템 타입 export로 재사용성 확보
ArticleListResponse['articles'][number]로도 추출 가능하지만, 명시적 export가 가독성과 재사용성에 유리합니다.-interface ArticleWithCategory { +export interface ArticleWithCategory { articleId: number; url: string; memo: string; createdAt: string; isRead: boolean; remindAt: string; - category: Category; + category: RemindCategory; }
18-22: 카운트 필드 명칭 일관성 재확인Bookmark 스키마와의 불일치가 의도인지 확인 필요합니다. 동일 도메인(기사/북마크)에서는
totalCount/unreadCount등으로 통일한 뷰모델 제공을 권장합니다(axios 레이어에서 매핑).apps/client/src/pages/remind/apis/queries.ts (1)
12-15: 페이지네이션 UX 개선 — 이전 데이터 유지 + 불필요 호출 차단pnpm-lock.yaml에서 @tanstack/react-query@5.85.5 확인됨; v5에서는 기존 keepPreviousData 옵션이 제거되고 placeholderData(또는 내장 keepPreviousData 유틸)를 사용해 동일 동작을 구현합니다. (tanstack.com)
- 수정(파일: apps/client/src/pages/remind/apis/queries.ts)
- import에 keepPreviousData 추가: import { useQuery, UseQueryResult, keepPreviousData } from '@tanstack/react-query';
- useQuery 옵션 추가: placeholderData: keepPreviousData, staleTime: 30_000, enabled: Boolean(nowDate) && Number.isFinite(page) && Number.isFinite(size)
apps/client/src/pages/myBookmark/apis/queries.ts (2)
13-16: 쿼리 키/함수명 동기화 및 이전 데이터 유지axios에서 All로 정리 시 쿼리 키도 의미를 맞추는 것이 깔끔합니다. 또한 페이지 전환 시 이전 데이터 유지를 권장합니다.
-import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useQuery, UseQueryResult, keepPreviousData } from '@tanstack/react-query'; -import { getBookmarkReadArticles, getBookmarkUnreadArticles } from './axios'; +import { getBookmarkAllArticles, getBookmarkReadArticles, getBookmarkUnreadArticles } from './axios'; @@ -export const useGetBookmarkReadArticles = ( +export const useGetBookmarkAllArticles = ( page: number, size: number ): UseQueryResult<BookmarkArticleResponse, AxiosError> => { return useQuery({ - queryKey: ['bookmarkReadArticles', page, size], - queryFn: () => getBookmarkReadArticles(page, size), + queryKey: ['bookmarkAllArticles', page, size], + queryFn: () => (getBookmarkAllArticles ?? getBookmarkReadArticles)(page, size), + placeholderData: keepPreviousData, + staleTime: 30_000, + enabled: Number.isFinite(page) && Number.isFinite(size), }); };설명:
getBookmarkAllArticles ?? getBookmarkReadArticles는 단계적 변경 중 호환성 유지용입니다.
23-26: 동일한 페이지네이션 옵션 적용Unread 쿼리에도 동일한 옵션을 적용해 UX/캐싱 일관성을 유지하세요.
return useQuery({ queryKey: ['bookmarkUnreadArticles', page, size], queryFn: () => getBookmarkUnreadArticles(page, size), + placeholderData: keepPreviousData, + staleTime: 30_000, + enabled: Number.isFinite(page) && Number.isFinite(size), });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/client/src/pages/myBookmark/apis/axios.ts(1 hunks)apps/client/src/pages/myBookmark/apis/queries.ts(1 hunks)apps/client/src/pages/myBookmark/types/api.ts(1 hunks)apps/client/src/pages/remind/apis/axios.ts(1 hunks)apps/client/src/pages/remind/apis/queries.ts(1 hunks)apps/client/src/pages/remind/types/api.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🧬 Code graph analysis (3)
apps/client/src/pages/myBookmark/apis/queries.ts (2)
apps/client/src/pages/myBookmark/types/api.ts (2)
BookmarkArticleResponse(10-14)UnreadBookmarkArticleResponse(17-20)apps/client/src/pages/myBookmark/apis/axios.ts (2)
getBookmarkReadArticles(3-8)getBookmarkUnreadArticles(10-15)
apps/client/src/pages/remind/types/api.ts (1)
apps/client/src/shared/types/api.ts (1)
Category(1-5)
apps/client/src/pages/remind/apis/queries.ts (2)
apps/client/src/pages/remind/types/api.ts (1)
ArticleListResponse(18-22)apps/client/src/pages/remind/apis/axios.ts (1)
getRemindArticles(3-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (1)
apps/client/src/pages/remind/apis/axios.ts (1)
3-8: nowDate 포맷(서버 기대값) 확인 및 포맷/인코딩 일관화 필요getRemindArticles(nowDate: string)는 now를 쿼리 문자열에 직접 삽입합니다; 리포지토리에는 formatLocalDateTime 사용처(apps/client/src/shared/apis/axios.ts)와 new Date().toISOString() 사용(apps/extension/src/App.tsx)이 혼재되어 있습니다.
- 서버가 기대하는 now 포맷(예: "YYYY-MM-DD" vs ISO 8601 전체
"2025-09-12T09:00:00Z")을 백엔드/API 문서나 서버 코드에서 확인하세요.- 권장: Date를 인자로 받게 하여 내부에서 일관되게 포맷하거나(또는 string이면 전역 유틸로 포맷 통일) 모든 호출이 동일 포맷을 사용하도록 정리하세요.
- 쿼리 빌드는 문자열 보간 대신 axios params 사용 또는 최소한 encodeURIComponent(nowDate) 적용으로 인코딩/안전성 확보하세요. (수정 위치: apps/client/src/pages/remind/apis/axios.ts — getRemindArticles)
| export const getBookmarkReadArticles = async (page: number, size: number) => { | ||
| const { data } = await apiRequest.get( | ||
| `/api/v1/articles?page=${page}&size=${size}` | ||
| ); | ||
| return data.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
이름과 실제 동작 불일치(“Read”가 아닌 “All”)
/api/v1/articles는 전부 조회(ALL)로 보입니다. 함수명을 getBookmarkAllArticles로 바꾸거나 최소한 주석으로 명확히 해주세요. 혼동은 호출부/키 캐싱에도 영향을 줍니다.
-import apiRequest from '@shared/apis/setting/axiosInstance';
+import apiRequest from '@shared/apis/setting/axiosInstance';
+import { BookmarkArticleResponse, UnreadBookmarkArticleResponse } from '@pages/myBookmark/types/api';
+
+type ApiResponse<T> = { data: T };
-export const getBookmarkReadArticles = async (page: number, size: number) => {
- const { data } = await apiRequest.get(
- `/api/v1/articles?page=${page}&size=${size}`
- );
- return data.data;
-};
+// NOTE: returns ALL bookmark articles (read + unread)
+export const getBookmarkAllArticles = async (page: number, size: number): Promise<BookmarkArticleResponse> => {
+ const { data } = await apiRequest.get<ApiResponse<BookmarkArticleResponse>>(
+ '/api/v1/articles',
+ { params: { page, size } }
+ );
+ return data.data;
+};호환성 필요 시, 아래 별칭도 함께 유지 가능합니다:
+export const getBookmarkReadArticles = getBookmarkAllArticles;📝 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.
| export const getBookmarkReadArticles = async (page: number, size: number) => { | |
| const { data } = await apiRequest.get( | |
| `/api/v1/articles?page=${page}&size=${size}` | |
| ); | |
| return data.data; | |
| }; | |
| import apiRequest from '@shared/apis/setting/axiosInstance'; | |
| import { BookmarkArticleResponse, UnreadBookmarkArticleResponse } from '@pages/myBookmark/types/api'; | |
| type ApiResponse<T> = { data: T }; | |
| // NOTE: returns ALL bookmark articles (read + unread) | |
| export const getBookmarkAllArticles = async (page: number, size: number): Promise<BookmarkArticleResponse> => { | |
| const { data } = await apiRequest.get<ApiResponse<BookmarkArticleResponse>>( | |
| '/api/v1/articles', | |
| { params: { page, size } } | |
| ); | |
| return data.data; | |
| }; | |
| export const getBookmarkReadArticles = getBookmarkAllArticles; |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/apis/axios.ts around lines 3 to 8, the
function name getBookmarkReadArticles misleadingly implies it fetches "read"
articles while the endpoint /api/v1/articles returns all articles; rename the
function to getBookmarkAllArticles (or export a new alias with the new name and
keep the old name for backward compatibility), update the JSDoc/comment to state
it returns ALL bookmarked articles, and search/update callers/cache keys to use
the new name or alias to avoid key collisions.
| export const getBookmarkUnreadArticles = async (page: number, size: number) => { | ||
| const { data } = await apiRequest.get( | ||
| `/api/v1/articles/unread?page=${page}&size=${size}` | ||
| ); | ||
| return data.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
axios params 사용 및 반환 타입 명시
동일하게 params 사용과 제네릭으로 안전성을 높여주세요.
-export const getBookmarkUnreadArticles = async (page: number, size: number) => {
- const { data } = await apiRequest.get(
- `/api/v1/articles/unread?page=${page}&size=${size}`
- );
- return data.data;
-};
+export const getBookmarkUnreadArticles = async (page: number, size: number): Promise<UnreadBookmarkArticleResponse> => {
+ const { data } = await apiRequest.get<ApiResponse<UnreadBookmarkArticleResponse>>(
+ '/api/v1/articles/unread',
+ { params: { page, size } }
+ );
+ return data.data;
+};📝 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.
| export const getBookmarkUnreadArticles = async (page: number, size: number) => { | |
| const { data } = await apiRequest.get( | |
| `/api/v1/articles/unread?page=${page}&size=${size}` | |
| ); | |
| return data.data; | |
| }; | |
| export const getBookmarkUnreadArticles = async (page: number, size: number): Promise<UnreadBookmarkArticleResponse> => { | |
| const { data } = await apiRequest.get<ApiResponse<UnreadBookmarkArticleResponse>>( | |
| '/api/v1/articles/unread', | |
| { params: { page, size } } | |
| ); | |
| return data.data; | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/apis/axios.ts around lines 10 to 15, the
request builds the query string inline and doesn't use axios params or generics
for typing; change the call to use apiRequest.get<TResponse> with a typed
response/interface for the returned shape and pass { params: { page, size } }
instead of interpolating into the URL, and annotate the function return type
(e.g., Promise<YourDataType>) so consumers get compile-time safety.
| export const getRemindArticles = async ( | ||
| nowDate: string, | ||
| readStatus: boolean, | ||
| page: number, | ||
| size: number | ||
| ) => { | ||
| const { data } = await apiRequest.get( | ||
| `/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}` | ||
| ); | ||
| return data.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
쿼리 문자열 직접 합성 지양하고 axios params 사용 + 반환 타입 명시
직접 문자열 합성은 인코딩 이슈(nowDate에 : 등)와 유지보수 비용을 유발합니다. axios params와 제네릭으로 응답 타입을 명확히 해주세요.
+import { ArticleListResponse } from '@pages/remind/types/api';
+
+type ApiResponse<T> = { data: T };
+
export const getRemindArticles = async (
nowDate: string,
readStatus: boolean,
page: number,
size: number
-) => {
- const { data } = await apiRequest.get(
- `/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}`
- );
+) : Promise<ArticleListResponse> => {
+ const { data } = await apiRequest.get<ApiResponse<ArticleListResponse>>(
+ '/api/v1/articles/remind',
+ { params: { now: nowDate, readStatus, page, size } }
+ );
return data.data;
};📝 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.
| export const getRemindArticles = async ( | |
| nowDate: string, | |
| readStatus: boolean, | |
| page: number, | |
| size: number | |
| ) => { | |
| const { data } = await apiRequest.get( | |
| `/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}` | |
| ); | |
| return data.data; | |
| }; | |
| import { ArticleListResponse } from '@pages/remind/types/api'; | |
| type ApiResponse<T> = { data: T }; | |
| export const getRemindArticles = async ( | |
| nowDate: string, | |
| readStatus: boolean, | |
| page: number, | |
| size: number | |
| ): Promise<ArticleListResponse> => { | |
| const { data } = await apiRequest.get<ApiResponse<ArticleListResponse>>( | |
| '/api/v1/articles/remind', | |
| { params: { now: nowDate, readStatus, page, size } } | |
| ); | |
| return data.data; | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/apis/axios.ts around lines 3 to 13, the function
builds the query string manually which can cause encoding issues and lacks an
explicit return type; change the axios call to use the params option (passing
nowDate, readStatus, page, size as separate keys) and add a TypeScript generic
for the expected response shape (e.g., ApiResponse<{ data: Article[] }> or the
correct DTO) on apiRequest.get so the function returns a typed value (update the
function signature to return Promise<YourReturnType> and return the typed
response.data).
| interface Category { | ||
| categoryId: number; | ||
| categoryName: string; | ||
| categoryColor: string; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
공유 Category 타입과의 혼동 방지(이름 충돌 회피)
@shared/types/api.ts에도 Category가 있어 의미가 다른 동일 명칭이 혼선을 줄 수 있습니다. 지역 타입명을 구체화하거나(예: RemindCategory) 또는 공유 타입을 확장/별칭 처리하세요.
-interface Category {
+interface RemindCategory {
categoryId: number;
categoryName: string;
categoryColor: string;
}그리고 Line 15의 참조를 함께 갱신하세요:
- category: Category;
+ category: RemindCategory;🤖 Prompt for AI Agents
In apps/client/src/pages/remind/types/api.ts around lines 2 to 6, the local
interface named Category conflicts with a shared Category type in
@shared/types/api.ts; rename the local type to a more specific name (e.g.,
RemindCategory) or create an alias/extend the shared type, and update any
references (including the use at line 15) to the new name so there is no
ambiguous/duplicate Category identifier.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
apps/client/src/pages/remind/Remind.tsx (7)
41-49: timeRemaining에 ISO 그대로 전달됨: 남은 시간 문자열로 포맷하세요.Card 스토리 예시상 timeRemaining은 사람이 읽는 남은 시간(예: "3시간 25분")을 기대합니다. 서버 datetime을 그대로 넣지 말고 상대시간으로 변환하세요.
- timeRemaining={article.remindAt} + timeRemaining={formatTimeRemaining(article.remindAt, formattedDate)}추가 유틸(이 파일 상단에 배치 가능):
function formatTimeRemaining(remindAt: string, now: string) { const delta = new Date(remindAt).getTime() - new Date(now).getTime(); if (Number.isNaN(delta)) return ''; if (delta <= 0) return '만료'; const min = Math.floor(delta / 60000); const h = Math.floor(min / 60); const m = min % 60; if (h >= 24) { const d = Math.floor(h / 24); return `${d}일 ${h % 24}시간`; } if (h > 0 && m > 0) return `${h}시간 ${m}분`; if (h > 0) return `${h}시간`; return `${m}분`; }
27-27: 널 병합 연산자로 명확히 표현하세요.카운트가 0일 때도 의도대로 동작하지만, 의미상 ??가 더 적확합니다.
- countNum={data?.unreadArticleCount || 0} + countNum={data?.unreadArticleCount ?? 0}- countNum={data?.readArticleCount || 0} + countNum={data?.readArticleCount ?? 0}Also applies to: 33-33
7-7: 배지 타입을 리터럴 유니온으로 좁혀 타입 안정성 강화.- const [activeBadge, setActiveBadge] = useState('notRead'); + const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead');- const handleBadgeClick = (badgeType: string) => { + const handleBadgeClick = (badgeType: 'read' | 'notRead') => {Also applies to: 17-19
48-48: 카테고리 널 안전성 보강(백엔드 필드가 옵션일 가능성 대비).- category={article.category.categoryName} + category={article.category?.categoryName ?? ''}
40-41: 불필요한 TODO 제거 또는 업데이트.이미 API 데이터 사용 중입니다. 주석을 정리해 주세요.
- {/* TODO: API 연결 후 수정 */}
21-53: 로딩/에러/빈 상태 처리 추가 제안.UX 품질을 위해 isLoading/isError/빈 목록 처리 컴포넌트를 간단히 추가하세요.
예시:
const { data, isLoading, isError } = useGetRemindArticles(/* ... */); if (isLoading) return <div className="py-[5.2rem] pl-[8rem]">로딩 중…</div>; if (isError) return <div className="py-[5.2rem] pl-[8rem]">오류가 발생했습니다.</div>; {data?.articles?.length === 0 && <div className="mt-[2rem] text-text-weak">표시할 아티클이 없습니다.</div>}원하시면 스켈레톤 Card도 제안드리겠습니다.
45-46: 제목에 URL 대신 사람이 읽기 좋은 텍스트 사용 검토.API에 title 또는 domain이 있으면 title 우선, 없으면 URL의 호스트만 노출하는 등 가독성을 개선하세요.
예:
title={article.title ?? new URL(article.url).hostname}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/pages/remind/Remind.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🧬 Code graph analysis (1)
apps/client/src/pages/remind/Remind.tsx (3)
packages/design-system/src/components/card/Card.stories.tsx (1)
Remind(42-53)apps/client/src/shared/utils/formatDateTime.ts (1)
formatLocalDateTime(1-9)apps/client/src/pages/remind/apis/queries.ts (1)
useGetRemindArticles(6-16)
🔇 Additional comments (1)
apps/client/src/pages/remind/Remind.tsx (1)
8-15: nowDate 형식/타임존 확인 필요formatLocalDateTime()가 오프셋 없는 로컬 문자열(예: 2025-09-12T10:30:05)을 반환하고, 이 값이 쿼리 파라미터(now)로 그대로 전송됩니다. 백엔드가 오프셋 포함 ISO(예: 2025-09-12T10:30:05+09:00 또는 Z)를 요구하면 조회 결과가 달라질 수 있으니 API 스펙을 확인하고, 필요하면 formatLocalDateTime을 date.toISOString() 또는 오프셋 포함 포맷으로 변경하세요.
확인 위치: apps/client/src/shared/utils/formatDateTime.ts, apps/client/src/pages/remind/apis/axios.ts, apps/client/src/shared/apis/axios.ts, apps/client/src/pages/remind/Remind.tsx
| const formattedDate = formatLocalDateTime(); | ||
|
|
||
| const { data } = useGetRemindArticles( | ||
| formattedDate, | ||
| activeBadge === 'read', | ||
| 1, | ||
| 10 | ||
| ); |
There was a problem hiding this comment.
쿼리 키가 매 렌더마다 바뀌어 재요청/루프 위험: nowDate를 한 번만 고정하세요.
formatLocalDateTime()가 초 단위까지 포함한 현재시각 문자열을 매 렌더마다 생성합니다. 이 값이 queryKey에 들어가 재렌더마다 키가 바뀌고, 불필요한 재요청이나 갱신 루프를 유발할 수 있습니다. useRef로 컴포넌트 수명 동안 고정하세요.
- import { useState } from 'react';
+ import { useState, useRef } from 'react';
- const formattedDate = formatLocalDateTime();
+ const nowRef = useRef(formatLocalDateTime());
+ const formattedDate = nowRef.current;Also applies to: 2-2
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 8 to 15,
formatLocalDateTime() is called on every render producing a new timestamp that
becomes part of the query key and triggers unnecessary refetch loops; capture
the time once for the component lifetime (e.g., useRef to store
formatLocalDateTime() on first render) and use that stable value in
useGetRemindArticles instead of calling formatLocalDateTime() directly so the
query key no longer changes on each render.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/client/src/pages/myBookmark/apis/axios.ts (1)
3-7: 함수/캐시 키 네이밍 “ALL vs READ” 혼선 정리 필요이 파일의 함수는 “전체 조회”인데, 관련 쿼리 키가
bookmarkReadArticles로 되어 있습니다(queries.ts 참조). 키를bookmarkAllArticles(혹은bookmarkArticles)로 통일 권장. 기존 키 유지가 필요하면 마이그레이션 기간 동안만 alias 키를 중복으로 prefetch해 주세요.#!/bin/bash # 레거시 이름/키 사용처 스캔 rg -nP -C2 '\bgetBookmark(Read|All)?Articles\b|bookmarkReadArticles|bookmarkAllArticles'참고(별도 파일 수정 예시 - apps/client/src/pages/myBookmark/apis/queries.ts):
// before queryKey: ['bookmarkReadArticles', page, size], // after queryKey: ['bookmarkAllArticles', page, size],Also applies to: 10-15
🧹 Nitpick comments (4)
apps/client/src/pages/myBookmark/apis/axios.ts (1)
1-15: Axios 호출을 타입 안전하게 개선 + 명확한 함수명으로 정리
- params 옵션/제네릭으로 응답 타입을 명시하고, AbortSignal을 지원해 쿼리 취소가 가능하도록 했습니다.
- “전체 조회” 의미가 드러나도록 getBookmarkAllArticles로 노출하고, 현 호출부 유지를 위해 기존 이름은 alias로 남겼습니다.
-import apiRequest from '@shared/apis/setting/axiosInstance'; +import apiRequest from '@shared/apis/setting/axiosInstance'; +import { BookmarkArticleResponse, UnreadBookmarkArticleResponse } from '@pages/myBookmark/types/api'; + +type ApiResponse<T> = { data: T }; -export const getBookmarkArticles = async (page: number, size: number) => { - const { data } = await apiRequest.get( - `/api/v1/articles?page=${page}&size=${size}` - ); - return data.data; -}; +export const getBookmarkAllArticles = async ( + page: number, + size: number, + signal?: AbortSignal +): Promise<BookmarkArticleResponse> => { + const { data } = await apiRequest.get<ApiResponse<BookmarkArticleResponse>>( + '/api/v1/articles', + { params: { page, size }, signal } + ); + return data.data; +}; +// 호환용 alias (현 호출부 유지) +export const getBookmarkArticles = getBookmarkAllArticles; -export const getBookmarkUnreadArticles = async (page: number, size: number) => { - const { data } = await apiRequest.get( - `/api/v1/articles/unread?page=${page}&size=${size}` - ); - return data.data; -}; +export const getBookmarkUnreadArticles = async ( + page: number, + size: number, + signal?: AbortSignal +): Promise<UnreadBookmarkArticleResponse> => { + const { data } = await apiRequest.get<ApiResponse<UnreadBookmarkArticleResponse>>( + '/api/v1/articles/unread', + { params: { page, size }, signal } + ); + return data.data; +};apps/client/src/pages/myBookmark/MyBookmark.tsx (3)
11-12: 변수/표기 정리(“read” → “all”) + 제목 매핑 보강
- “전체 조회” 데이터를
readArticles로 부르는 것은 혼선을 줍니다.allArticles로 교체하면 의도가 명확해집니다.- 카드 제목은
title필드가 있으면 우선 사용하고, 없을 때만url로 폴백하세요.- const { data: readArticles } = useGetBookmarkArticles(1, 10); + const { data: allArticles } = useGetBookmarkArticles(1, 10); const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10); @@ - countNum={readArticles?.totalArticle || 0} + countNum={allArticles?.totalArticle || 0} @@ - countNum={readArticles?.totalUnreadArticle || 0} + countNum={allArticles?.totalUnreadArticle || 0} @@ - {activeBadge === 'all' && - readArticles?.articles.map((article) => ( + {activeBadge === 'all' && + allArticles?.articles.map((article) => ( <Card key={article.articleId} type="bookmark" - title={article.url} + title={article.title ?? article.url} content={article.memo} // category={article.category.categoryName} date={new Date(article.createdAt).toLocaleDateString('ko-KR')} /> ))} @@ - {activeBadge === 'notRead' && + {activeBadge === 'notRead' && unreadArticles?.articles.map((article) => ( <Card key={article.articleId} type="bookmark" - title={article.url} + title={article.title ?? article.url} content={article.memo} // category={article.} date={new Date(article.createdAt).toLocaleDateString('ko-KR')} /> ))}Also applies to: 24-24, 30-30, 39-49, 50-60
36-38: 불필요한 TODO 제거 및 빈 상태 처리 권장API 연결이 완료된 상태이므로 TODO는 제거하고, 리스트가 비었을 때의 빈 상태 UI를 간단히라도 추가하면 UX가 좋아집니다.
- {/* TODO: API 연결 후 수정 */} + {/* 데이터 없음 상태 */} + {activeBadge === 'all' && (allArticles?.articles?.length ?? 0) === 0 && ( + <p className="body2 text-gray-400">저장된 북마크가 없어요.</p> + )} + {activeBadge === 'notRead' && (unreadArticles?.articles?.length ?? 0) === 0 && ( + <p className="body2 text-gray-400">아직 안 읽은 북마크가 없어요.</p> + )}
11-12: 불필요한 초기 네트워크 호출 줄이기(조건부 enable)초기 뷰가 ‘전체’일 때 ‘안 읽음’ 쿼리까지 동시에 불러올 필요가 없다면, hook에 options를 받아
enabled를 제어하세요. 아래처럼 queries.ts를 확장한 뒤, 컴포넌트에서enabled: activeBadge === 'notRead'를 전달하면 됩니다.예시(queries.ts 수정):
export const useGetBookmarkUnreadArticles = ( page: number, size: number, options?: Omit<UseQueryOptions<UnreadBookmarkArticleResponse, AxiosError>, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: ['bookmarkUnreadArticles', page, size], queryFn: ({ signal }) => getBookmarkUnreadArticles(page, size, signal), ...options, }); };컴포넌트 적용:
- const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10); + const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10, { + enabled: activeBadge === 'notRead', + });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/myBookmark/MyBookmark.tsx(1 hunks)apps/client/src/pages/myBookmark/apis/axios.ts(1 hunks)apps/client/src/pages/myBookmark/apis/queries.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/pages/myBookmark/apis/queries.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🧬 Code graph analysis (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (1)
apps/client/src/pages/myBookmark/apis/queries.ts (2)
useGetBookmarkArticles(9-17)useGetBookmarkUnreadArticles(19-27)
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
Summary by CodeRabbit