Api(client): 카테고리별 아티클 조회 API 연결 & 아티클 없는 경우 UI 분기 추가#91
Api(client): 카테고리별 아티클 조회 API 연결 & 아티클 없는 경우 UI 분기 추가#91constantly-dev merged 13 commits intodevelopfrom
Conversation
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... 📒 Files selected for processing (2)
Tip CodeRabbit can suggest fixes for GitHub Check annotations.Configure Walkthrough카테고리별 북마크 조회 API/훅/타입을 추가하고 MyBookmark/Remind 페이지에 카테고리 및 읽음 상태 기반 UI 분기와 빈 상태 컴포넌트를 도입했다. 사이드바의 selectCategory 시그니처와 라우팅 쿼리 형식이 변경되었고, 디자인 시스템의 아이콘 목록 및 Icon 색상 타입이 업데이트됐다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant Sidebar as 사이드바
participant NavHook as useSidebarNav
participant Router as Router
participant MyBookmark as MyBookmark 페이지
participant Query as ReactQuery
participant API as /api
User->>Sidebar: 카테고리 클릭(id, name)
Sidebar->>NavHook: selectCategory(id, name)
NavHook->>Router: push /my-bookmarks?id={id}&category={name}
Router-->>MyBookmark: 쿼리파라미터 전달(id, category)
MyBookmark->>Query: useGetCategoryBookmarkArticles(id, page, size)
Query->>API: GET /api/v1/articles/category?categoryId={id}&page={page}&size={size}
API-->>Query: CategoryBookmarkArticleResponse
Query-->>MyBookmark: categoryArticles
alt 결과 있음
MyBookmark-->>User: 카드 그리드(카테고리 표기)
else 비어있음
MyBookmark-->>User: NoArticles 표시
end
sequenceDiagram
autonumber
actor User as 사용자
participant Remind as Remind 페이지
participant Query as ReactQuery
participant API as /api
participant UI as 빈상태컴포넌트
User->>Remind: 배지 선택(read / notRead)
alt read
Remind->>Query: useGetRemindArticles('read')
Query->>API: GET /remind?read-status=read
else notRead
Remind->>Query: useGetRemindArticles('notRead')
Query->>API: GET /remind?read-status=notRead
end
API-->>Query: { articles }
Query-->>Remind: data
alt articles 존재
Remind-->>User: 리스트 렌더
else 없음
Remind->>UI: NoReadArticles / NoUnreadArticles
UI-->>User: 빈 상태 표시
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
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 Pre-merge checks❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (16)
apps/client/src/pages/remind/apis/axios.ts (2)
10-11: 문자열 보간 대신 axiosparams사용으로 안전한 인코딩/유지보수성 향상쿼리스트링을 수동으로 조합하면 인코딩 누락 및 파라미터 변경 시 실수가 잦습니다. 아래처럼
params옵션을 사용해주세요.- const { data } = await apiRequest.get( - `/api/v1/articles/remind?now=${nowDate}&read-status=${readStatus}&page=${page}&size=${size}` - ); + const { data } = await apiRequest.get('/api/v1/articles/remind', { + params: { now: nowDate, 'read-status': readStatus, page, size }, + });
3-8: 반환 타입 명시 및 응답 제네릭 도입 제안API 응답 스키마가 정해져 있다면 반환 타입을 명시하고 axios 제네릭을 사용해 타입 안정성을 높이는 것을 권장합니다.
-export const getRemindArticles = async ( +export const getRemindArticles = async ( nowDate: string, readStatus: boolean, page: number, size: number -) => { +): Promise<RemindArticlesResponse> => { - const { data } = await apiRequest.get('/api/v1/articles/remind', { + const { data } = await apiRequest.get<ApiResponse<RemindArticlesResponse>>('/api/v1/articles/remind', { params: { now: nowDate, 'read-status': readStatus, page, size }, }); return data.data; }(참고: RemindArticlesResponse, ApiResponse 타입은 프로젝트 공용 타입 위치에 정의되어 있어야 합니다.)
apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx (2)
6-11: 복수의 상태에 맞는 카피/대체 텍스트로 정확도 및 접근성 개선읽은 아티클이 없을 때 노출되므로 복수의 문구와 alt를 구체화하면 UX가 개선됩니다.
- <img src={chippiNoArticles} alt="No Articles" /> + <img src={chippiNoArticles} alt="읽은 아티클이 없어요" /> - <p className="body1-m text-font-gray-3 mt-[0.8rem]"> - 저장된 정보가 없어요 + <p className="body1-m text-font-gray-3 mt-[0.8rem]"> + 아직 읽은 아티클이 없어요
8-8: 의미론적 태그 사용 제안: 제목은<h2>스크린리더 구조 인식 향상을 위해 제목은 heading 요소를 권장합니다.
- <p className="head2 mt-[1.6rem]">앗..</p> + <h2 className="head2 mt-[1.6rem]">앗..</h2>apps/client/src/pages/remind/components/noUnreadArticles/NoUnreadArticles.tsx (1)
1-1: 변수명과 alt 정교화로 가독성/접근성 개선컴포넌트 목적에 맞게 변수명을
NoUnread로, alt도 구체적으로 수정 제안드립니다.-import chippiNoRemindArticles from '@assets/chippi_remindx.svg'; +import chippiNoUnreadArticles from '@assets/chippi_remindx.svg'; ... - <img src={chippiNoRemindArticles} alt="No Articles" /> + <img src={chippiNoUnreadArticles} alt="읽지 않은 아티클이 없어요" />Also applies to: 6-6
apps/client/src/pages/remind/Remind.tsx (2)
41-57: 중복 렌더링 블록 제거로 DRY 적용두 분기에서 동일한 카드 리스트를 중복 렌더링합니다. 한 블록으로 합치고 placeholder만 분기하세요.
- {activeBadge === 'read' && - (data?.articles && data.articles.length > 0 ? ( - <div className="scrollbar-hide mt-[2.6rem] flex max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> - {data.articles.map((article) => ( - <Card - key={article.articleId} - type="remind" - title={article.url} - content={article.memo} - timeRemaining={article.remindAt} - category={article.category.categoryName} - /> - ))} - </div> - ) : ( - <NoReadArticles /> - ))} - - {activeBadge === 'notRead' && - (data?.articles && data.articles.length > 0 ? ( - <div className="scrollbar-hide mt-[2.6rem] flex max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> - {data.articles.map((article) => ( - <Card - key={article.articleId} - type="remind" - title={article.url} - content={article.memo} - timeRemaining={article.remindAt} - category={article.category.categoryName} - /> - ))} - </div> - ) : ( - <NoUnreadArticles /> - ))} + {data?.articles && data.articles.length > 0 ? ( + <div className="scrollbar-hide mt-[2.6rem] flex max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> + {data.articles.map((article) => ( + <Card + key={article.articleId} + type="remind" + title={article.url} + content={article.memo} + timeRemaining={article.remindAt} + category={article.category.categoryName} + /> + ))} + </div> + ) : activeBadge === 'read' ? ( + <NoReadArticles /> + ) : ( + <NoUnreadArticles /> + )}Also applies to: 59-75
12-17: 로딩/에러 시 빈 상태 깜빡임 방지현재 로딩 중에도 빈 상태 컴포넌트가 먼저 보일 수 있습니다.
isLoading/isError를 활용해 UX를 다듬는 것을 권장합니다.- const { data } = useGetRemindArticles( + const { data, isLoading, isError } = useGetRemindArticles( formattedDate, activeBadge === 'read', 1, 10 ); + if (isLoading) { + return <div className="flex flex-col py-[5.2rem] pl-[8rem]">로딩 중…</div>; + } + if (isError) { + return <div className="flex flex-col py-[5.2rem] pl-[8rem]">데이터를 불러오지 못했습니다.</div>; + }Also applies to: 41-75
apps/client/src/pages/myBookmark/types/api.ts (2)
1-5: 내부 Category 타입 네이밍 충돌/혼동 방지동일 리포 내
apps/client/src/shared/types/api.ts에도Category가 존재합니다. 현재 파일의Category는 필드명이 다르므로 혼동을 줄이기 위해 별도 이름(예:BookmarkCategory)으로 분리하는 것을 권장합니다.-interface Category { +interface BookmarkCategory { categoryId: number; categoryName: string; categoryColor: string; } ... - category: Category; + category: BookmarkCategory;Also applies to: 13-13
31-31: 카테고리 응답 타입을 전용 인터페이스로 분리하고 totalUnreadArticle을 optional로 변경하세요.MyBookmark.tsx가 articles?.totalUnreadArticle || 0로 사용하므로 optional로 변경해도 호환되며, alias는 백엔드 스펙 변경에 취약합니다.
수정 위치: apps/client/src/pages/myBookmark/types/api.ts (현재 31행). 사용처: apps/client/src/pages/myBookmark/MyBookmark.tsx:60-export type CategoryBookmarkArticleResponse = UnreadBookmarkArticleResponse; +export interface CategoryBookmarkArticleResponse { + totalArticle: number; + // 카테고리 API가 제공하는 경우에만 사용 + totalUnreadArticle?: number; + articles: BookmarkArticle[]; +}packages/design-system/src/icons/components/icon.tsx (1)
107-107: CSS 변수 미정의 시 안전한 폴백 추가 제안토큰이 누락되어도 현재 텍스트 색상(currentColor)을 사용하도록 폴백을 추가하세요.
- style={color ? { color: `var(--color-${color})` } : undefined} + style={color ? { color: `var(--color-${color}, currentColor)` } : undefined}apps/client/src/pages/myBookmark/components/NoArticles/NoArticles.tsx (2)
7-7: 대체 텍스트 현지화 제안시각적 맥락에 맞춰 alt를 한국어로 제공하세요.
- <img src={chippiNoArticles} alt="No Articles" /> + <img src={chippiNoArticles} alt="저장된 북마크 없음" />
10-16: gap 클래스 적용 위치 수정
gap은 flex 컨테이너에 적용해야 효력이 있습니다. 현재<p>에 적용되어 무의미합니다. 컨테이너로 이동하세요.- <div className="mt-[0.8rem] flex items-center"> - <p className="body1-m text-font-gray-3 gap-[0.4rem]"> + <div className="mt-[0.8rem] flex items-center gap-[0.4rem]"> + <p className="body1-m text-font-gray-3"> 원하는 페이지에서 </p> <div className="flex items-center text-center"> <Icon name="ic_extension" width={28} height={28} /> <p className="body1-m text-font-gray-3">아이콘을 눌러주세요.</p> </div> </div>apps/client/src/pages/myBookmark/apis/queries.ts (1)
34-44: React Query 옵션 보완 제안 (UX 안정화)페이지/사이즈 변경 시 스피너 점멸을 줄이기 위해 캐싱 파라미터를 보완하는 것을 권장합니다.
다음과 같이
staleTime/gcTime과 placeholder를 추가해 보세요 (v5 기준):export const useGetCategoryBookmarkArticles = ( categoryId: string | null, page: number, size: number ): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => { return useQuery({ queryKey: ['categoryBookmarkArticles', categoryId, page, size], queryFn: () => getCategoryBookmarkArticles(categoryId, page, size), enabled: !!categoryId, + staleTime: 30_000, + gcTime: 5 * 60_000, + placeholderData: (prev) => prev, // 페이지 전환 시 이전 데이터 유지 }); };apps/client/src/pages/myBookmark/MyBookmark.tsx (3)
26-27: 디버그 콘솔 제거불필요한 콘솔은 머지 전 제거해주세요.
-// 임시 콘솔 -console.log('categoryArticles', categoryArticles);
84-100: 카테고리 필터 적용 시 '안 읽음' 섹션 동작 스펙 확인 필요현재 '안 읽음'은 전역 미읽음 목록을 그대로 사용합니다. 카테고리 필터 선택 상태에서 미읽음도 해당 카테고리로 제한해야 하는지 요구사항 확인이 필요합니다.
원한다면 다음 두 가지 중 하나를 구현해야 합니다.
- 서버: "카테고리별 미읽음" API와 훅 추가.
- 클라이언트:
unreadArticles.articles를article.category.categoryId === categoryId로 필터링 (페이징 정확도는 떨어짐).필요 시 API/훅 스캐폴딩 또는 클라이언트 필터 버전을 빠르게 제안드릴게요.
34-49: 중첩 h-screen로 인한 이중 스크롤 가능성루트 컨테이너와 리스트 컨테이너 모두
h-screen을 사용 중입니다. 레이아웃에 따라 이중 스크롤이 발생할 수 있습니다.루트에
min-h-screen+ 내부에flex-1 overflow-y-auto를 권장합니다.-<div className="flex h-screen flex-col py-[5.2rem] pl-[8rem]"> +<div className="flex min-h-screen flex-col py-[5.2rem] pl-[8rem]"> ... - <div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> + <div className="scrollbar-hide mt-[2.6rem] flex flex-1 max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">Also applies to: 66-69, 86-87
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
apps/client/src/assets/chippi_remindx.svgis excluded by!**/*.svgapps/client/src/assets/chippi_x.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/ic_extension.svgis excluded by!**/*.svg
📒 Files selected for processing (14)
apps/client/src/layout/Layout.tsx(1 hunks)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(2 hunks)apps/client/src/pages/myBookmark/components/NoArticles/NoArticles.tsx(1 hunks)apps/client/src/pages/myBookmark/types/api.ts(1 hunks)apps/client/src/pages/remind/Remind.tsx(3 hunks)apps/client/src/pages/remind/apis/axios.ts(1 hunks)apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx(1 hunks)apps/client/src/pages/remind/components/noUnreadArticles/NoUnreadArticles.tsx(1 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(1 hunks)apps/client/src/shared/hooks/useSidebarNav.ts(1 hunks)packages/design-system/src/icons/components/icon.tsx(1 hunks)packages/design-system/src/icons/iconNames.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 (4)
apps/client/src/pages/myBookmark/apis/queries.ts (2)
apps/client/src/pages/myBookmark/types/api.ts (1)
CategoryBookmarkArticleResponse(31-31)apps/client/src/pages/myBookmark/apis/axios.ts (1)
getCategoryBookmarkArticles(17-26)
apps/client/src/pages/remind/Remind.tsx (1)
packages/design-system/src/components/card/Card.stories.tsx (1)
Remind(42-53)
apps/client/src/pages/myBookmark/MyBookmark.tsx (2)
apps/client/src/pages/myBookmark/apis/queries.ts (3)
useGetBookmarkArticles(14-22)useGetBookmarkUnreadArticles(24-32)useGetCategoryBookmarkArticles(34-44)packages/design-system/src/icons/components/icon.tsx (1)
Icon(71-114)
apps/client/src/pages/myBookmark/types/api.ts (1)
apps/client/src/shared/types/api.ts (1)
Category(1-5)
🔇 Additional comments (14)
apps/client/src/pages/remind/apis/axios.ts (1)
10-10: 백엔드 쿼리 키 확인 필요:read-statusvsreadStatus
현재 apps/client/src/pages/remind/apis/axios.ts (라인 10)에서 쿼리키를read-status로 요청합니다; 서버가 하이픈 표기를 기대하는지 확인하고 백엔드와 합의된 표기법으로 일관화하세요.apps/client/src/pages/remind/components/noUnreadArticles/NoUnreadArticles.tsx (1)
1-1: 확인 — 에셋 파일 존재(Import 유효)apps/client/src/assets/chippi_remindx.svg 파일이 리포지토리에 존재하므로 import 경로는 유효합니다.
apps/client/src/pages/remind/Remind.tsx (3)
9-9: 유니온 타입으로 상호배타 상태 모델링: 굿
'read' | 'notRead'유니온과 핸들러 시그니처가 명확합니다. 상태 오타를 방지합니다.Also applies to: 19-21
28-36: 검증 완료 — API 필드명(readArticleCount, unreadArticleCount) 일치apps/client/src/pages/remind/types/api.ts의 ArticleListResponse에 두 필드가 정의되어 있으며 apps/client/src/pages/remind/Remind.tsx에서 동일 필드를 사용하고 있어 카운트가 0으로 고정될 위험은 없습니다.
48-52: Card props — title / timeRemaining 전달 방식 확인 필요
- packages/design-system/src/components/card/Card.tsx: CardProps = RemindProps | BookmarkProps이며
timeRemaining?: never로 선언되어 있어, Remind에서 넘기는article.remindAt이 현재 타입에 맞지 않거나 허용되지 않을 가능성 있음.- apps/client/src/pages/remind/Remind.tsx (48–52, 66–70):
title={article.url}대신article.title ?? article.url로 폴백 적용.remindAt은 Card가 기대하는 형식(예: '남은 시간' 문자열)으로 포맷해 전달하거나 Card의 RemindProps 타입을 수정해 일치시키기.- 참고: 레포 검색에서 Article/Remind 응답 타입 정의는 발견되지 않음 — 도메인 타입(필드명·타입)을 직접 확인해 변경 영향 검증 필요.
apps/client/src/pages/myBookmark/types/api.ts (1)
20-20: isNewUser 필드 도입 검증 필요
타입 선언이 추가되었으나 코드베이스에서 사용처가 없고 선언부만 존재합니다 (apps/client/src/pages/myBookmark/types/api.ts:20). /api/v1/articles 응답에 isNewUser가 실제 포함되는지 확인하세요. 미포함이면 타입을 optional로 변경하거나 응답 파싱 시 기본값을 할당하세요.packages/design-system/src/icons/iconNames.ts (1)
13-19: 아이콘 등록 확인 — 스프라이트/빌드 포함 여부만 검증 필요
- 확인: packages/design-system/src/icons/iconNames.ts에 'ic_extension'(13행), 'main_header_logo'(17행), 'main_logo'(18행) 등록됨; 중복 없음.
- 조치: 빌드 산출물(또는 아이콘 스프라이트)에서 해당 심볼(id)이 실제로 포함되는지 확인(예: 출력 스프라이트에서 등 검색).
packages/design-system/src/icons/components/icon.tsx (1)
30-30: 검증 완료: --color-black CSS 변수 존재 확인됨packages/tailwind-config/shared-styles.css:176에 --color-black: #000000; 정의되어 있으므로 'black' 토큰 추가 시 의도한 색상이 적용됩니다.
apps/client/src/layout/Layout.tsx (1)
2-2: 경로 별칭 사용 전환 LGTM
@shared경로 별칭 사용으로 일관성/가독성 개선되었습니다. tsconfig/webpack 설정과 ESLint import-resolver가 이미 정합하면 문제 없습니다.apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
119-119: 카테고리 선택 시 name 전달 변경 LGTM
selectCategory(id, category.name)로 시그니처 변경 반영이 적절합니다. URL 인코딩은 훅 내부에서 처리하도록 해 두셨는지 확인만 부탁드립니다(별도 코멘트 참조).apps/client/src/pages/myBookmark/apis/queries.ts (2)
3-7: 신규 API 의존성 추가 적절
getCategoryBookmarkArticles추가 및 기존 import 구성과 일관성이 유지됩니다.
10-10: 타입 별칭 import OK
CategoryBookmarkArticleResponse도입이 반환 타입과 맞물려 명확합니다.apps/client/src/pages/myBookmark/MyBookmark.tsx (2)
6-7: 신규 훅 의존성 추가 적절카테고리 전용 훅 도입이 PR 목적과 부합합니다.
14-17: 쿼리 파라미터 중복 및 명명 혼선:categoryvscategoryIdFile: apps/client/src/pages/myBookmark/MyBookmark.tsx (Lines 14-17)
동일한 쿼리 키를 두 변수에 중복 할당하고 있어 혼선 발생. API는 id를, UI는 이름을 기대할 가능성이 있으므로 분리하세요.-const [searchParams] = useSearchParams(); -const category = searchParams.get('category'); -const categoryId = searchParams.get('category'); +const [searchParams] = useSearchParams(); +const categoryId = searchParams.get('categoryId'); // API 호출용 +const categoryName = searchParams.get('category'); // 헤더 표시용헤더(표시) 영역과 axios 호출(쿼리 키)도 함께 갱신하세요.
백엔드가 실제로categoryId키를 기대하는지 확인. 백엔드가category를 기대하면 axios 쿼리 키를 그에 맞춰 유지/조정하세요.
| export const getCategoryBookmarkArticles = async ( | ||
| categoryId: string | null, | ||
| page: number, | ||
| size: number | ||
| ) => { | ||
| const { data } = await apiRequest.get( | ||
| `/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}` | ||
| ); | ||
| return data.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
null 카테고리 방지 및 쿼리스트링 조합 방식 개선 제안
- categoryId가 null일 때
...categoryId=null로 호출될 수 있습니다. 안전하게 가드하고, 템플릿 문자열 대신 axiosparams를 사용해 인코딩/누락 처리 이점을 얻는 편이 좋습니다.
다음 diff 적용 제안:
export const getCategoryBookmarkArticles = async (
categoryId: string | null,
page: number,
size: number
) => {
- const { data } = await apiRequest.get(
- `/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}`
- );
+ if (!categoryId) {
+ throw new Error('categoryId is required');
+ }
+ const { data } = await apiRequest.get('/api/v1/articles/category', {
+ params: { categoryId, page, size },
+ });
return data.data;
};추가로, 반환 타입을 명확히 하려면 API envelope 타입을 도입하거나 캐스팅을 통해 CategoryBookmarkArticleResponse를 지정하는 것도 고려해주세요.
📝 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 getCategoryBookmarkArticles = async ( | |
| categoryId: string | null, | |
| page: number, | |
| size: number | |
| ) => { | |
| const { data } = await apiRequest.get( | |
| `/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}` | |
| ); | |
| return data.data; | |
| }; | |
| export const getCategoryBookmarkArticles = async ( | |
| categoryId: string | null, | |
| page: number, | |
| size: number | |
| ) => { | |
| if (!categoryId) { | |
| throw new Error('categoryId is required'); | |
| } | |
| const { data } = await apiRequest.get('/api/v1/articles/category', { | |
| params: { categoryId, page, size }, | |
| }); | |
| return data.data; | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/apis/axios.ts around lines 17-26, the
current implementation interpolates categoryId into the querystring which can
produce categoryId=null; change to call apiRequest.get with axios params (e.g.,
{ params: { page, size, ...(categoryId ? { categoryId } : {}) } }) so null is
omitted and axios handles encoding; also annotate the function return type by
mapping the response envelope to CategoryBookmarkArticleResponse (or cast
data.data) before returning.
💡 Verification agent
🧩 Analysis chain
URL 파라미터 네이밍 일관성 확인 필요 (id vs categoryId)
라우팅에서는 /my-bookmarks?id=...를 사용하고, API에서는 categoryId를 기대합니다. 코드베이스 전반의 파라미터 키가 혼용되지 않는지 점검 부탁드립니다.
다음 스크립트로 사용처를 빠르게 확인할 수 있습니다:
🏁 Script executed:
#!/bin/bash
# id / categoryId 파라미터 사용 현황 점검
rg -n --type ts --type tsx -C2 '\bcreateSearchParams\(|useSearchParams\(|categoryId\b|\bid='Length of output: 27
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# TS/TSX 파일에서 createSearchParams, useSearchParams, categoryId, id= 사용 현황 재검색
echo "검색 시작: createSearchParams, useSearchParams, categoryId, id= (ts/tsx/js/jsx)..."
rg -n -C2 -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' '\b(createSearchParams\(|useSearchParams\(|categoryId\b|\bid=)' || true
# 문자열로 포함된 쿼리 파라미터도 확인
echo "검색 추가: 파일 전체에서 '?id=' 포함 여부 확인..."
rg -n -C2 -g '!**/node_modules/**' '\?id=' || trueLength of output: 9237
쿼리 파라미터 키 불일치 — 긴급 수정 필요
라우팅은 id 및 category(name)을 쿼리로 보내는데, MyBookmark는 'category'를 categoryId로 사용하고 API는 'categoryId'를 기대합니다. 이로 인해 올바른 카테고리 ID가 API에 전달되지 않습니다.
-
발견 위치:
- apps/client/src/shared/hooks/useSidebarNav.ts — navigate(
/my-bookmarks?id=${id}&category=${name}) - apps/client/src/pages/myBookmark/MyBookmark.tsx — searchParams.get('category')를 categoryId로 사용
- apps/client/src/pages/myBookmark/apis/axios.ts — getCategoryBookmarkArticles가
?categoryId=${categoryId}로 호출 - apps/client/src/pages/myBookmark/apis/queries.ts — queryKey / enabled: !!categoryId 사용
- apps/client/src/shared/hooks/useSidebarNav.ts — navigate(
-
권장 조치:
- 일관화(택1): MyBookmark에서 searchParams.get('id')로 실제 ID를 읽어 API에 전달하거나, useSidebarNav에서 쿼리 키를
categoryId로 변경. - API 전송 전 string → number 변환 확인 및 enabled 조건이 의도대로 동작하는지 검증.
- 일관화(택1): MyBookmark에서 searchParams.get('id')로 실제 ID를 읽어 API에 전달하거나, useSidebarNav에서 쿼리 키를
| const { data: articles } = useGetBookmarkArticles(1, 10); | ||
| const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10); | ||
| const { data: categoryArticles } = useGetCategoryBookmarkArticles( | ||
| categoryId, | ||
| 1, | ||
| 10 | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카테고리 API를 호출하지만 UI에 반영되지 않음
categoryArticles를 가져오지만 렌더링·카운트에 사용하지 않아 필터 효과가 없습니다. 카테고리 존재 시 목록/카운트를 카테고리 응답으로 스위칭하세요.
아래 코멘트(라인 66-82, 51-64)에 JSX와 카운트 반영용 diff를 제안했습니다.
| {category && ( | ||
| <Icon | ||
| name="ic_arrow_down_disable" | ||
| width={24} | ||
| height={24} | ||
| rotate={270} | ||
| color="black" | ||
| /> | ||
| )} | ||
| </div> | ||
| <p className="head3 text-main500">{category || ''}</p> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
헤더 카테고리 표시 변수 교체
표시용은 categoryName으로 교체해 의미를 명확히 하세요.
- {category && (
+ {categoryName && (
<Icon
name="ic_arrow_down_disable"
width={24}
height={24}
rotate={270}
color="black"
/>
)}
- <p className="head3 text-main500">{category || ''}</p>
+ <p className="head3 text-main500">{categoryName || ''}</p>📝 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.
| {category && ( | |
| <Icon | |
| name="ic_arrow_down_disable" | |
| width={24} | |
| height={24} | |
| rotate={270} | |
| color="black" | |
| /> | |
| )} | |
| </div> | |
| <p className="head3 text-main500">{category || ''}</p> | |
| </div> | |
| {categoryName && ( | |
| <Icon | |
| name="ic_arrow_down_disable" | |
| width={24} | |
| height={24} | |
| rotate={270} | |
| color="black" | |
| /> | |
| )} | |
| </div> | |
| <p className="head3 text-main500">{categoryName || ''}</p> | |
| </div> |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/MyBookmark.tsx around lines 38 to 49, the
header is using the prop/state variable category for display; change both the
conditional render (the Icon block) and the paragraph text to use categoryName
instead of category, keeping the existing fallback (empty string) for the
paragraph and preserving the Icon render condition to only show when
categoryName is truthy.
| <div className="mt-[3rem] flex gap-[2.4rem]"> | ||
| <Badge | ||
| text="전체보기" | ||
| countNum={readArticles?.totalArticle || 0} | ||
| countNum={articles?.totalArticle || 0} | ||
| onClick={() => handleBadgeClick('all')} | ||
| isActive={activeBadge === 'all'} | ||
| /> | ||
| <Badge | ||
| text="안 읽음" | ||
| countNum={readArticles?.totalUnreadArticle || 0} | ||
| countNum={articles?.totalUnreadArticle || 0} | ||
| onClick={() => handleBadgeClick('notRead')} | ||
| isActive={activeBadge === 'notRead'} | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
뱃지 카운트 소스 잘못 지정 및 카테고리 연동 필요
- "안 읽음" 뱃지 카운트가
articles에서 오고 있어 잘못된 값입니다. - 카테고리 필터가 있을 때 "전체보기" 카운트는
categoryArticles를 사용해야 일관됩니다.
-<Badge
- text="전체보기"
- countNum={articles?.totalArticle || 0}
+<Badge
+ text="전체보기"
+ countNum={(categoryId ? categoryArticles?.totalArticle : articles?.totalArticle) || 0}
onClick={() => handleBadgeClick('all')}
isActive={activeBadge === 'all')}
/>
<Badge
text="안 읽음"
- countNum={articles?.totalUnreadArticle || 0}
+ countNum={unreadArticles?.totalArticle || 0}
onClick={() => handleBadgeClick('notRead')}
isActive={activeBadge === 'notRead')}
/>📝 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.
| <div className="mt-[3rem] flex gap-[2.4rem]"> | |
| <Badge | |
| text="전체보기" | |
| countNum={readArticles?.totalArticle || 0} | |
| countNum={articles?.totalArticle || 0} | |
| onClick={() => handleBadgeClick('all')} | |
| isActive={activeBadge === 'all'} | |
| /> | |
| <Badge | |
| text="안 읽음" | |
| countNum={readArticles?.totalUnreadArticle || 0} | |
| countNum={articles?.totalUnreadArticle || 0} | |
| onClick={() => handleBadgeClick('notRead')} | |
| isActive={activeBadge === 'notRead'} | |
| /> | |
| </div> | |
| <div className="mt-[3rem] flex gap-[2.4rem]"> | |
| <Badge | |
| text="전체보기" | |
| countNum={(categoryId ? categoryArticles?.totalArticle : articles?.totalArticle) || 0} | |
| onClick={() => handleBadgeClick('all')} | |
| isActive={activeBadge === 'all'} | |
| /> | |
| <Badge | |
| text="안 읽음" | |
| countNum={unreadArticles?.totalArticle || 0} | |
| onClick={() => handleBadgeClick('notRead')} | |
| isActive={activeBadge === 'notRead'} | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/MyBookmark.tsx around lines 51-64, the badge
count sources are wrong: the "안 읽음" badge is using articles instead of the
unread count from the currently active data set, and "전체보기" should show counts
from categoryArticles when a category filter is applied. Update the countNum
props so both badges derive their values from categoryArticles when a category
is selected (e.g., categoryArticles?.totalArticle and
categoryArticles?.totalUnreadArticle) otherwise fall back to
articles?.totalArticle and articles?.totalUnreadArticle, and keep null-safe
fallbacks to 0.
| {activeBadge === 'all' && | ||
| (articles?.articles && articles.articles.length > 0 ? ( | ||
| <div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> | ||
| {articles.articles.map((article) => ( | ||
| <Card | ||
| key={article.articleId} | ||
| type="bookmark" | ||
| title={article.url} | ||
| content={article.memo} | ||
| category={article.category.categoryName} | ||
| date={new Date(article.createdAt).toLocaleDateString('ko-KR')} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <NoArticles /> | ||
| ))} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
"전체보기" 렌더링에 카테고리 결과 적용
카테고리 존재 시 categoryArticles를 사용하도록 전환하세요.
컴포넌트 상단(쿼리 호출 직후)에 파생 값을 하나 두고:
+// 파생 리스트 (카테고리 유무에 따라 전환)
+const listAll = categoryId ? categoryArticles?.articles : articles?.articles;렌더링 분기는 다음처럼 수정:
-{activeBadge === 'all' &&
- (articles?.articles && articles.articles.length > 0 ? (
+{activeBadge === 'all' &&
+ (listAll && listAll.length > 0 ? (
<div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">
- {articles.articles.map((article) => (
+ {listAll.map((article) => (
<Card
key={article.articleId}
type="bookmark"
title={article.url}
content={article.memo}
category={article.category.categoryName}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
/>
))}
</div>
) : (
<NoArticles />
))}📝 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.
| {activeBadge === 'all' && | |
| (articles?.articles && articles.articles.length > 0 ? ( | |
| <div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> | |
| {articles.articles.map((article) => ( | |
| <Card | |
| key={article.articleId} | |
| type="bookmark" | |
| title={article.url} | |
| content={article.memo} | |
| category={article.category.categoryName} | |
| date={new Date(article.createdAt).toLocaleDateString('ko-KR')} | |
| /> | |
| ))} | |
| </div> | |
| ) : ( | |
| <NoArticles /> | |
| ))} | |
| // 파생 리스트 (카테고리 유무에 따라 전환) | |
| const listAll = categoryId ? categoryArticles?.articles : articles?.articles; | |
| {activeBadge === 'all' && | |
| (listAll && listAll.length > 0 ? ( | |
| <div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> | |
| {listAll.map((article) => ( | |
| <Card | |
| key={article.articleId} | |
| type="bookmark" | |
| title={article.url} | |
| content={article.memo} | |
| category={article.category.categoryName} | |
| date={new Date(article.createdAt).toLocaleDateString('ko-KR')} | |
| /> | |
| ))} | |
| </div> | |
| ) : ( | |
| <NoArticles /> | |
| ))} |
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/MyBookmark.tsx around lines 66 to 82, the
"all" view always renders from articles.articles even when a category filter is
present; create a derived variable (e.g. const displayedArticles =
categoryArticles?.articles?.length ? categoryArticles : articles) right after
the query returns and then update the JSX branch to use displayedArticles (check
length and map displayedArticles.articles) so the "전체보기" view shows category
results when available and falls back to the full articles list otherwise.
| (id: number, name: string) => { | ||
| setActiveTab('mybookmark'); | ||
| setSelectedCategoryId(id); | ||
| navigate(`/my-bookmarks?categoryId=${id}`); | ||
| navigate(`/my-bookmarks?id=${id}&category=${name}`); | ||
| }, | ||
| [navigate] |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카테고리 이름 쿼리스트링 인코딩 필요
한글/공백/특수문자를 포함한 카테고리명이 있을 수 있어 URL 인코딩이 필요합니다.
- navigate(`/my-bookmarks?id=${id}&category=${name}`);
+ navigate(`/my-bookmarks?id=${id}&category=${encodeURIComponent(name)}`);또는 createSearchParams 사용을 권장합니다:
navigate({
pathname: '/my-bookmarks',
search: createSearchParams({ id: String(id), category: name }).toString(),
});🤖 Prompt for AI Agents
In apps/client/src/shared/hooks/useSidebarNav.ts around lines 26 to 31, the code
constructs the category query string without URL-encoding which will break for
Korean, spaces or special characters; update the navigate call to URL-encode the
category value (e.g., use encodeURIComponent(String(name))) or use
createSearchParams to build the search string (ensuring id is stringified) so
the resulting URL is safe; keep setActiveTab and setSelectedCategoryId as-is and
pass the encoded search string to navigate.
💡 Verification agent
🧩 Analysis chain
URL 파라미터 키 일관성 검토 (id ↔ categoryId)
현재 쿼리 키가 id인데, API는 categoryId를 사용합니다. 페이지에서 useSearchParams로 읽어오는 키와 훅/axios에 넘기는 키가 일치하는지 한 번 더 확인해주세요.
🏁 Script executed:
#!/bin/bash
# MyBookmark 관련 파일에서 파라미터 키 점검
rg -n -C2 --type ts --type tsx 'useSearchParams|categoryId\b|\bid=' apps/client/src/pages/myBookmarkLength of output: 27
추가 확인을 위해 아래 스크립트를 실행합니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "추가 검색: useSearchParams, categoryId, id=, my-bookmarks 관련 항목을 apps/client에서 찾습니다..."
if ! command -v rg >/dev/null 2>&1; then
echo "rg 미설치: grep으로 대체 검색 실행"
grep -R -n -E --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' 'useSearchParams|categoryId\b|\bid=|\bmy-bookmark|/my-bookmarks' apps/client || true
exit 0
fi
echo "rg 버전: $(rg --version | head -n1 || true)"
rg -n -C2 'useSearchParams|categoryId\b|\bid=|\bmy-bookmark|/my-bookmarks' apps/client -g '**/*.ts' -g '**/*.tsx' -g '**/*.js' -g '**/*.jsx' -S || trueLength of output: 10212
쿼리 파라미터 키 통일 — categoryId 사용 권장
useSidebarNav가 /my-bookmarks?id=${id}&category=${name}로 네비게이트하는 반면 MyBookmark는 searchParams.get('category')로 id를 읽고, API는 ?categoryId= 를 기대합니다. 쿼리 키를 일관되게 정리하세요.
- 수정 제안: apps/client/src/shared/hooks/useSidebarNav.ts —
navigate(/my-bookmarks?categoryId=${id}&category=${name})로 변경. - 앱 측 정리: apps/client/src/pages/myBookmark/MyBookmark.tsx — 카테고리 ID는
searchParams.get('categoryId')에서 읽고, 이름은searchParams.get('category')에서 읽도록 분리(현재 둘 다 'category'로 읽음). - API: apps/client/src/pages/myBookmark/apis/axios.ts는
categoryId를 사용하므로 변경 불필요; 변경 후categoryId로 전달되는 값이 숫자 ID인지 확인.
🤖 Prompt for AI Agents
In apps/client/src/shared/hooks/useSidebarNav.ts around lines 26-31, the
navigation query keys are inconsistent with MyBookmark and the API — change the
navigate call to use categoryId for the numeric id
(navigate(`/my-bookmarks?categoryId=${id}&category=${name}`)) so the URL
contains both categoryId and category name; then update
apps/client/src/pages/myBookmark/MyBookmark.tsx to read the id from
searchParams.get('categoryId') and the name from searchParams.get('category'),
and ensure the value sent to the API
(apps/client/src/pages/myBookmark/apis/axios.ts) uses the numeric categoryId
(parse/validate as number) so keys and types are consistent across the app.
bbe8f1f to
08d37d8
Compare
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
apps/client/src/pages/myBookmark/MyBookmark.tsx (5)
14-14: 대소문자 경로 수정 확인 (CI용 리눅스 호환성 보장)
components/NoArticles/NoArticles로 교정된 점 좋습니다. 이전 코멘트 반영되어 import 실패 리스크 해소되었습니다.
55-66: 헤더 카테고리 표시 변수 교체표시는
categoryName을 사용하는 편이 명확합니다. 이전 제안과 동일합니다.- {category && ( + {categoryName && ( <Icon name="ic_arrow_down_disable" width={24} height={24} rotate={270} color="black" /> )} - <p className="head3 text-main500">{category || ''}</p> + <p className="head3 text-main500">{categoryName || ''}</p>
71-80: 뱃지 카운트 소스 수정 (카테고리/안 읽음 일관성)
- 전체보기: 카테고리 선택 시
categoryArticles의 총계를 사용해야 합니다.- 안 읽음:
articles?.totalUnreadArticle대신unreadArticles?.totalArticle을 사용하세요.<Badge text="전체보기" - countNum={articles?.totalArticle || 0} + countNum={allCount} onClick={() => handleBadgeClick('all')} isActive={activeBadge === 'all'} /> <Badge text="안 읽음" - countNum={articles?.totalUnreadArticle || 0} + countNum={unreadArticles?.totalArticle || 0} onClick={() => handleBadgeClick('notRead')} isActive={activeBadge === 'notRead'} />
83-120: "전체보기" 렌더링 데이터 소스 전환 (카테고리 반영)카테고리 선택 시에도 항상
articles.articles를 사용하고 있어 필터가 UI에 반영되지 않습니다. 파생 리스트listAll을 사용하도록 바꾸세요.-{activeBadge === 'all' && - (articles?.articles && articles.articles.length > 0 ? ( +{activeBadge === 'all' && + (listAll && listAll.length > 0 ? ( <div className="scrollbar-hide mt-[2.6rem] flex h-screen max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> - {articles.articles.map((article) => ( + {listAll.map((article) => ( <Card key={article.articleId} type="bookmark" title={article.url} content={article.memo} category={article.category.categoryName} date={new Date(article.createdAt).toLocaleDateString('ko-KR')} onClick={() => {}} onOptionsClick={(e) => openMenu(article.articleId, e.currentTarget) } /> ))}
35-41: 카테고리 API 응답을 실제 렌더·카운트에 연결하세요현재
useGetCategoryBookmarkArticles를 호출하지만 렌더/카운트에서 사용하지 않아 필터 효과가 없습니다. 파생 값을 두고 UI 전환하세요.const { data: articles } = useGetBookmarkArticles(1, 10); const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10); const { data: categoryArticles } = useGetCategoryBookmarkArticles( categoryId, 1, 10 ); + + // 카테고리 선택 시 카테고리 결과로 전환 + const listAll = categoryId ? categoryArticles?.articles : articles?.articles; + const allCount = (categoryId ? categoryArticles?.totalArticle : articles?.totalArticle) ?? 0;
🧹 Nitpick comments (3)
apps/client/src/pages/myBookmark/MyBookmark.tsx (3)
19-21: URL 파라미터 네이밍 명확화 (category → categoryName) 제안표시 전용 문자열임이 드러나도록
categoryName으로 명시하면 가독성이 좋아집니다. 아래 헤더 사용처도 함께 교체 권장합니다.- const category = searchParams.get('category'); + const categoryName = searchParams.get('category'); const categoryId = searchParams.get('id');
43-44: 임시 콘솔 제거배포 전 불필요한 콘솔은 삭제하세요.
- // 임시 콘솔 - console.log('categoryArticles', categoryArticles); + // TODO: 필요 시 개발 환경에서만 디버깅 로깅
83-120: 로딩 중 빈 상태 깜빡임 방지초기 로딩 시 데이터 없음으로 판단해
<NoArticles />가 먼저 보일 수 있습니다.isLoading/isFetching으로 분기하거나keepPreviousData를 활용하세요.예:
-const { data: articles } = useGetBookmarkArticles(1, 10); +const { data: articles, isLoading: isAllLoading } = useGetBookmarkArticles(1, 10); ... -{activeBadge === 'all' && - (listAll && listAll.length > 0 ? ( ... ) : ( <NoArticles /> ))} +{activeBadge === 'all' && + (isAllLoading ? <SkeletonGrid /> : (listAll && listAll.length > 0 ? ( ... ) : ( <NoArticles /> )))}Also applies to: 121-157
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
apps/client/src/assets/chippi_remindx.svgis excluded by!**/*.svgapps/client/src/assets/chippi_x.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/ic_extension.svgis excluded by!**/*.svg
📒 Files selected for processing (14)
apps/client/src/layout/Layout.tsx(1 hunks)apps/client/src/pages/myBookmark/MyBookmark.tsx(2 hunks)apps/client/src/pages/myBookmark/apis/axios.ts(1 hunks)apps/client/src/pages/myBookmark/apis/queries.ts(2 hunks)apps/client/src/pages/myBookmark/components/NoArticles/NoArticles.tsx(1 hunks)apps/client/src/pages/myBookmark/types/api.ts(1 hunks)apps/client/src/pages/remind/Remind.tsx(4 hunks)apps/client/src/pages/remind/apis/axios.ts(1 hunks)apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx(1 hunks)apps/client/src/pages/remind/components/noUnreadArticles/NoUnreadArticles.tsx(1 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(1 hunks)apps/client/src/shared/hooks/useSidebarNav.ts(1 hunks)packages/design-system/src/icons/components/icon.tsx(1 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/client/src/pages/remind/apis/axios.ts
- apps/client/src/pages/myBookmark/apis/queries.ts
- apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx
- apps/client/src/pages/myBookmark/types/api.ts
- packages/design-system/src/icons/iconNames.ts
- packages/design-system/src/icons/components/icon.tsx
- apps/client/src/shared/hooks/useSidebarNav.ts
- apps/client/src/pages/remind/components/noUnreadArticles/NoUnreadArticles.tsx
- apps/client/src/layout/Layout.tsx
- apps/client/src/pages/myBookmark/apis/axios.ts
- apps/client/src/pages/myBookmark/components/NoArticles/NoArticles.tsx
- apps/client/src/shared/components/sidebar/Sidebar.tsx
- apps/client/src/pages/remind/Remind.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 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 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/client/src/pages/myBookmark/MyBookmark.tsx
🧬 Code graph analysis (1)
apps/client/src/pages/myBookmark/MyBookmark.tsx (3)
apps/client/src/pages/myBookmark/apis/queries.ts (3)
useGetBookmarkArticles(14-22)useGetBookmarkUnreadArticles(24-32)useGetCategoryBookmarkArticles(34-44)packages/design-system/src/icons/components/icon.tsx (1)
Icon(71-114)apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
OptionsMenuPortal(15-45)
🔇 Additional comments (4)
apps/client/src/pages/myBookmark/MyBookmark.tsx (4)
6-8: 카테고리/라우터 훅 import 추가 적절카테고리별 조회와 URL 파라미터 연동을 위한 훅 추가 방향 좋습니다.
15-15: Icon import 사용 OK디자인시스템 아이콘 별도 import로 분리한 점 문제 없습니다.
55-63: Icon color 토큰 확인 필요
color="black"은 내부에서var(--color-black)을 참조합니다. 디자인 토큰에--color-black이 존재하는지 확인해주세요. 없다면className="text-neutral900"등 토큰 기반 색상으로 교체하세요.
100-115: OptionsMenuPortal 네이밍·사용 의도 불일치 — 확인 필요문제: 포털 Props(categoryId, getCategoryName)는 사이드바 카테고리 편집용으로 보이지만, MyBookmark에서 articleId를 menu.categoryId로 전달해 아티클 옵션으로 사용하고 있습니다. 네이밍·도메인 혼동이 큽니다.
위치: apps/client/src/pages/myBookmark/MyBookmark.tsx (약 100–115, 138–153)
권장(간결):
- 아티클 전용 포털 분리(ArticleOptionsMenuPortal) 또는 useAnchoredMenu의 categoryId → targetId 등으로 일반화하고 포털 Props 제네릭화.
- REMIND_MOCK_DATA 기반 getBookmarkTitle 의존 제거 — 실제 아티클 데이터에서 라벨 계산하거나 ID 전달 방식으로 수정.
검증 참고: 이전 rg 실행이 "unrecognized file type: tsx"로 실패했습니다. 코드베이스 전반에서 OptionsMenuPortal 사용 여부를 type 필터 없이 재검색해 확인하세요 (예: rg -nC3 'OptionsMenuPortal').
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/client/src/pages/remind/Remind.tsx (3)
10-11: 빈 상태 컴포넌트는 지연 로딩(lazy) 고려초기 번들을 줄여야 한다면
React.lazy+Suspense로 두 컴포넌트를 지연 로딩하는 것을 검토해주세요. 영향이 작다면 현 상태도 무방합니다.
40-41: 네이밍/표현 미세 개선여기 값은 컴포넌트 타입이 아니라 JSX 엘리먼트입니다.
EmptyStateComponent대신emptyState정도로 이름을 바꾸거나, else 분기에서 바로 삼항을 인라인 렌더링하면 가독성이 좋아집니다.
79-80: 로딩 상태와 빈 상태 분리 제안초기 로딩에 빈 상태가 깜빡 보일 수 있습니다.
useGetRemindArticles의isLoading(또는isFetching)을 노출받아 로딩/빈/컨텐츠 3-way 분기를 고려해주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/pages/myBookmark/MyBookmark.tsx(2 hunks)apps/client/src/pages/remind/Remind.tsx(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/pages/myBookmark/MyBookmark.tsx
🧰 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 (1)
apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (1)
OptionsMenuPortal(15-45)
⏰ 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 (4)
apps/client/src/pages/remind/Remind.tsx (4)
8-8: 경로 별칭(import) 정리 좋습니다.모듈 경로가 명확해져 유지보수성이 좋아졌습니다.
36-38: 배지 타입을 리터럴 유니온으로 좁힌 점 좋습니다.클릭 핸들러 시그니처도 일관되어 타입 안정성이 향상됐습니다.
61-63: 데이터 가드 후 그리드 렌더링 전환 👍빈 배열/undefined 안전성이 개선되었습니다.
67-69: Card prop 매핑(타입/의도) 확인 필요
- packages/design-system/src/components/card/Card.tsx (RemindProps)와 packages/design-system/src/components/card/RemindCard.tsx를 보면 remind 타입은 title: string, timeRemaining: string(스토리/RemindCard는 '3시간 25분' 같은 포맷의 문자열을 기대)입니다.
- apps/client/src/pages/remind/Remind.tsx (줄 66–69) 현재: title={article.url} content={article.memo} timeRemaining={article.remindAt}
- article.url 대신 article.title을 써야 하는지 확인하세요.
- article.remindAt이 이미 사용자용 "남은 시간" 문자열인지(예: "3시간 25분") 아니면 ISO/timestamp인지 확인하고, ISO/timestamp면 표시 전에 포맷(remaining-time 문자열)으로 변환하세요.
| const getItemTitle = (id: number | null) => | ||
| id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? ''); | ||
| const [activeBadge, setActiveBadge] = useState('notRead'); | ||
| const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead'); |
There was a problem hiding this comment.
카테고리명 조회가 목데이터에 의존 — 실제 API 데이터로 교체 필요
getItemTitle이 REMIND_MOCK_DATA를 사용하고 있어 실제 categoryId와 매칭되지 않을 가능성이 큽니다. 옵션 메뉴에 잘못된 이름(혹은 빈 문자열)이 전달될 수 있습니다. API 응답(data.articles) 기반으로 조회하도록 교체해주세요.
적용 예시:
- const getItemTitle = (id: number | null) =>
- id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
+ const getCategoryName = (id: number | null) =>
+ id == null
+ ? ''
+ : data?.articles?.find((a) => a.category.categoryId === id)?.category
+ .categoryName ?? '';추가로, 상단의 REMIND_MOCK_DATA import는 제거해주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 24-26, getItemTitle
currently looks up REMIND_MOCK_DATA which can mismatch real categoryIds; change
it to look up the title from the API response (data.articles or the correct
articles/categories array available in this component) using the passed id,
return empty string when not found, and remove the REMIND_MOCK_DATA import at
the top; ensure you handle the case where data is undefined (safe optional
chaining) and keep the function signature the same.
| <OptionsMenuPortal | ||
| open={menu.open} | ||
| style={style ?? undefined} | ||
| containerRef={containerRef} | ||
| categoryId={menu.categoryId} | ||
| getCategoryName={getItemTitle} | ||
| onEdit={() => { | ||
| setIsEditOpen(true); | ||
| closeMenu(); | ||
| }} | ||
| onDelete={(id) => { | ||
| console.log('delete', id); | ||
| closeMenu(); | ||
| }} | ||
| onClose={closeMenu} | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
옵션 메뉴에 실제 카테고리명 전달 + style 전달 단순화
상단 수정에 맞춰 getCategoryName을 넘기고, style ?? undefined는 불필요합니다.
- <OptionsMenuPortal
- open={menu.open}
- style={style ?? undefined}
+ <OptionsMenuPortal
+ open={menu.open}
+ style={style}
containerRef={containerRef}
categoryId={menu.categoryId}
- getCategoryName={getItemTitle}
+ getCategoryName={getCategoryName}📝 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.
| <OptionsMenuPortal | |
| open={menu.open} | |
| style={style ?? undefined} | |
| containerRef={containerRef} | |
| categoryId={menu.categoryId} | |
| getCategoryName={getItemTitle} | |
| onEdit={() => { | |
| setIsEditOpen(true); | |
| closeMenu(); | |
| }} | |
| onDelete={(id) => { | |
| console.log('delete', id); | |
| closeMenu(); | |
| }} | |
| onClose={closeMenu} | |
| /> | |
| <OptionsMenuPortal | |
| open={menu.open} | |
| style={style} | |
| containerRef={containerRef} | |
| categoryId={menu.categoryId} | |
| getCategoryName={getCategoryName} | |
| onEdit={() => { | |
| setIsEditOpen(true); | |
| closeMenu(); | |
| }} | |
| onDelete={(id) => { | |
| console.log('delete', id); | |
| closeMenu(); | |
| }} | |
| onClose={closeMenu} | |
| /> |
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 82 to 97, the
OptionsMenuPortal should receive the actual category name and the style prop
should be simplified; replace the getCategoryName={getItemTitle} prop with
categoryName={getItemTitle(menu.categoryId)} (or the equivalent call to resolve
the name before passing) and change style={style ?? undefined} to simply
style={style} (or remove the prop when style is falsy) so the component gets a
string name and a direct style value.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/design-system/src/components/card/BaseCard.tsx (2)
7-7: border 전환에 따른 클래스 유효성·접근성·레이아웃 영향 확인
- 토큰:
border-gray200가 Tailwind 테마에 실제 정의돼 있는지 확인 필요. 미정의면 기본색(현재컬러)로 렌더링됩니다. 필요 시border-gray-200로 교정하세요.- 접근성: outline→border 전환으로 포커스 표시가 사라질 수 있습니다(카드가 클릭/포커스가능한 경우). 포커스 링을 추가하는 편이 안전합니다.
- 레이아웃: border는 콘텐츠 영역을 1px 줄입니다(글로벌 box-sizing이 border-box라도 내부 영역 감소). 카드 내부 텍스트/줄바꿈에 영향 없는지 확인 부탁드립니다.
대안 A(색상 토큰 교정):
-<div className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"> +<div className="w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white border border-gray-200">대안 B(레이아웃 영향 최소화: ring 사용, 포커스 가시성 확보):
-<div className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"> +<div className="w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white ring-1 ring-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500">
1-3: BaseCard 확장성·타입 개선 제안(className/DOM props 패스스루)디자인 시스템 컴포넌트로서 외부에서 className/aria/tabIndex 등 전달이 필요할 수 있습니다. 다음처럼 타입·시그니처를 확장하면 재사용성이 좋아집니다.
+import type { HTMLAttributes, ReactNode } from 'react'; - -interface BaseCardProps { - children: React.ReactNode; -} +interface BaseCardProps extends HTMLAttributes<HTMLDivElement> { + children: ReactNode; + className?: string; +} -const BaseCard = ({ children }: BaseCardProps) => { +const BaseCard = ({ children, className, ...props }: BaseCardProps) => { return ( - <div className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"> + <div + className={`w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white border border-gray-200 ${className ?? ''}`} + {...props} + > {children} </div> ); };Also applies to: 5-5, 7-7
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/layout/Layout.tsx(1 hunks)packages/design-system/src/components/card/BaseCard.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/layout/Layout.tsx
🧰 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 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
jjangminii
left a comment
There was a problem hiding this comment.
아티클 없을 때 분기처리까지 좋네요 👍 👍 고생하셨습니다
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
크게 추가된 것은 없는데 카테고리별 아티클 조회 API 연결하고 아티클 없는 경우 각 fallback UI 추가했습니다!
그리고 카테고리별 아티클 조회도
전체/안 읽음이 두개 상태에 따라서 다른 데이터가 와야하는데 아직 서버에 반영이 안 됐다고 해서 서버 수정 및 배포 되면 새로 이슈 파서 그 부분 적용하겠습니다!그래서
categoryArticles를 에러 안 뜨게console에 임시로 넣어놨습니다! 그리고 정민님이 작업하신 수정/삭제 popup UI 충돌나서 rebase로 해결하고 올렸습니다.📷 Screenshot
2025-09-13.8.06.29.mov
Summary by CodeRabbit