Skip to content

Api(client): Dashboard article 전체 조회 API 연결#82

Merged
constantly-dev merged 6 commits intodevelopfrom
api/#76/dashboard-articles-get-api
Sep 13, 2025
Merged

Api(client): Dashboard article 전체 조회 API 연결#82
constantly-dev merged 6 commits intodevelopfrom
api/#76/dashboard-articles-get-api

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Sep 12, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • Dashboard 리마인드 아티클 조회 API 연결
  • Dashboard 북마크 아티클 전체 조회 API 연결
  • Dashboard 북마크 안읽은 아티클 조회 API 연결

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • New Features
    • 북마크한 기사 목록을 읽음/미읽음으로 분류해 페이징 조회할 수 있습니다.
    • 리마인드 화면에서 특정 날짜·읽음 상태로 기사 목록을 API로 조회해 표시합니다.
    • 리마인드와 북마크에 읽음/미읽음 집계가 배지로 표시됩니다.
    • 목록 항목이 실제 API 데이터를 기반으로 렌더링되며 빈 데이터에 대한 안전한 처리(옵셔널 체이닝)가 적용됩니다.

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

MyBookmark과 Remind 페이지에 타입 정의, Axios 페처, React Query 훅을 추가하고 컴포넌트를 API 호출로 전환하여 읽음/미읽음 북마크 및 리마인드 아티클을 페이지네이션·필터 파라미터로 조회하도록 연결했다. 모든 페처는 서버 응답의 data.data를 반환한다.

Changes

Cohort / File(s) Summary
MyBookmark API fetchers
apps/client/src/pages/myBookmark/apis/axios.ts
읽음/미읽음 북마크 아티클 조회 GET 페처 2종 추가 (/api/v1/articles, /api/v1/articles/unread), page/size 쿼리 사용, data.data 반환
MyBookmark React Query hooks
apps/client/src/pages/myBookmark/apis/queries.ts
useGetBookmarkArticles, useGetBookmarkUnreadArticles 훅 추가; 쿼리 키에 page, size 포함, 내부에서 해당 페처 호출
MyBookmark API types
apps/client/src/pages/myBookmark/types/api.ts
BookmarkArticle 내부 타입 및 BookmarkArticleResponse, UnreadBookmarkArticleResponse 인터페이스 추가
Remind API fetcher
apps/client/src/pages/remind/apis/axios.ts
리마인드 아티클 조회 GET 페처 추가 (/api/v1/articles/remind), now, readStatus, page, size 쿼리 사용, data.data 반환
Remind React Query hook & component
apps/client/src/pages/remind/apis/queries.ts
apps/client/src/pages/remind/Remind.tsx
useGetRemindArticles 훅 추가; Remind 컴포넌트에서 목업 대신 API 훅 사용으로 전환, 배지 카운트·리스트를 API 응답으로 렌더링
Remind API types
apps/client/src/pages/remind/types/api.ts
Category, ArticleWithCategory, ArticleListResponse 인터페이스 정의

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 포함
Loading
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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

🛠️ Feature, frontend

Suggested reviewers

  • jllee000
  • jjangminii

Pre-merge checks (4 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning PR은 대시보드 리마인드 및 북마크(전체·미읽음) API 통합을 위해 axios 페처·react-query 훅·타입 정의·컴포넌트 반영 등 [#76]의 코드 요구사항을 충족하고 있습니다. 반면 디자인 시스템의 Progress 컴포넌트 구현과 스토리/테스트를 요구하는 [#25]는 변경 내역에 포함되어 있지 않습니다. 따라서 연결된 모든 이슈를 완전히 준수하지는 못해 전체적으로 불충분합니다. 해결 방안: 만약 #25가 본 PR 범위라면 Progress 컴포넌트 구현과 관련 스토리·테스트를 추가하거나 #25를 본 PR에서 제거해 별도 PR로 분리하세요. 또한 각 커밋 및 PR 설명에 어떤 이슈를 해결하는지 명확히 표기하면 검증이 더 쉬워집니다.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "Api(client): Dashboard article 전체 조회 API 연결"은 변경사항의 핵심(대시보드용 기사 조회 API 연동)을 명확히 나타내고 있어 관련성이 높고 간결합니다. 제목만으로도 리뷰어가 주요 목적을 빠르게 파악할 수 있습니다. 다만 '전체 조회' 표현이 리마인드·북마크(전체·미읽음) 등 세부 대상을 완전히 설명하지 못해 약간의 모호함이 있으므로 더 구체화하면 좋겠습니다.
Out of Scope Changes Check ✅ Passed 변경 파일은 주로 apps/client/src/pages/myBookmark 및 apps/client/src/pages/remind의 axios, queries, types, 컴포넌트 관련 항목으로 한정되어 있어 #76의 목표 범위와 일치합니다. 따옴표 스타일 변경이나 클래스 조정 같은 포맷/스타일 수정을 제외하면 범위를 벗어나는 기능 변경은 발견되지 않습니다. 따라서 범위 외 변경은 없다고 판단합니다.
Description Check ✅ Passed 현재 PR 설명은 템플릿의 필수 항목인 관련 이슈(close #76)와 작업 목록(리마인드·북마크 전체·미읽음 API 연결)을 포함하고 있어 주요 요구사항을 충족합니다. PR Point와 Screenshot 섹션은 비어 있지만 이는 비핵심 항목으로 설명의 완성도에는 큰 영향이 없습니다. 따라서 PR 설명은 대부분 완전하다고 판단합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

폴짝폴짝, 북마크 숲을 건너
읽음·미읽음 줄 맞춰 달려요 🐇
훅이 부르면 데이터가 와요—
캐시 키에 별빛을 묶어 담고,
카드마다 새 소식 한 줌 전해요.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch api/#76/dashboard-articles-get-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.

@github-actions
Copy link

github-actions bot commented Sep 12, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

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

📥 Commits

Reviewing files that changed from the base of the PR and between e981d92 and d255a18.

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

Comment on lines 3 to 8
export const getBookmarkReadArticles = async (page: number, size: number) => {
const { data } = await apiRequest.get(
`/api/v1/articles?page=${page}&size=${size}`
);
return data.data;
};
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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.

Comment on lines +10 to +15
export const getBookmarkUnreadArticles = async (page: number, size: number) => {
const { data } = await apiRequest.get(
`/api/v1/articles/unread?page=${page}&size=${size}`
);
return data.data;
};
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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.

Comment on lines +3 to +13
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;
};
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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).

Comment on lines +2 to +6
interface Category {
categoryId: number;
categoryName: string;
categoryColor: string;
}
Copy link

Choose a reason for hiding this comment

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

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d255a18 and 1db4d82.

📒 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

Comment on lines +8 to +15
const formattedDate = formatLocalDateTime();

const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿼리 키가 매 렌더마다 바뀌어 재요청/루프 위험: 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1db4d82 and a7ef5cd.

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

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

Labels

api api 연결

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Api] Dashboard article 전체 조회 API 연결

1 participant