Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough빈 상태 처리 로직을 세분화하고 관련 컴포넌트를 추가/수정했으며, 카드 베이스 컴포넌트에 className 전달을 허용하도록 API를 확장했습니다. 북마크/리마인드 페이지의 레이아웃 여백과 목록 래퍼 스타일을 조정했고, 사이드바 및 옵션 메뉴 버튼의 스타일/동작을 미세 조정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant MyBookmark as MyBookmark Page
participant Store as Data (total, filtered)
participant UI as Empty State
User->>MyBookmark: 페이지 진입
MyBookmark->>Store: totalArticle, filteredArticles 조회
Store-->>MyBookmark: { total, filteredCount }
alt filteredCount == 0
alt total == 0
MyBookmark->>UI: 렌더 NoArticles
else total > 0
MyBookmark->>UI: 렌더 NoUnreadArticles
end
else filteredCount > 0
MyBookmark->>UI: 기사 목록 렌더
end
sequenceDiagram
autonumber
actor User
participant Remind as Remind Page
participant Hook as useGetRemindArticles
participant UI as Empty State
User->>Remind: 페이지 진입
Remind->>Hook: 데이터 요청
alt isPending
Hook-->>Remind: isPending = true
Remind->>User: "Loading..."
else 완료
Hook-->>Remind: { readCount, unreadCount, activeBadge }
alt readCount == 0 and unreadCount == 0
Remind->>UI: 렌더 NoRemindArticles
else activeBadge == "읽음"
Remind->>UI: 조건 NoReadArticles (필터 결과 0일 때)
else activeBadge == "안 읽음"
Remind->>UI: 조건 NoUnreadArticles (필터 결과 0일 때)
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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 (3 warnings)
✅ Passed checks (2 passed)
|
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
3-8: ref 전달 방식 버그: forwardRef로 전환 필요함수형 컴포넌트에서
ref는 props로 받을 수 없고, 현재 구현은 항상undefined가 되어 레퍼런스가 깨집니다.forwardRef로 전환해주세요.-import { cn } from '@pinback/design-system/utils'; +import { cn } from '@pinback/design-system/utils'; +import { forwardRef } from 'react'; -export interface OptionsMenuButtonProps { - ref?: React.Ref<HTMLDivElement>; +export interface OptionsMenuButtonProps extends React.ComponentPropsWithoutRef<'div'> { onEdit: () => void; onDelete: () => void; className?: string; } -const ITEM_STYLE = +const ITEM_STYLE = 'body4-r text-font-black-1 h-[3.6rem] w-full ' + 'flex items-center pl-[0.8rem] ' + 'hover:bg-gray100 focus-visible:bg-gray100 active:bg-gray200 ' + 'outline-none transition-colors'; -export default function OptionsMenuButton({ - ref, - onEdit, - onDelete, - className, -}: OptionsMenuButtonProps) { - return ( - <div - ref={ref} +const OptionsMenuButton = forwardRef<HTMLDivElement, OptionsMenuButtonProps>(function OptionsMenuButton( + { onEdit, onDelete, className, ...rest }, + ref +) { + return ( + <div + ref={ref} role="menu" aria-label="옵션 메뉴" className={cn( 'bg-white-bg common-shadow rounded-[0.4rem] px-0 py-[1.2rem]', 'flex h-[9.6rem] w-[12.4rem] flex-col items-center justify-center', className )} + {...rest} > <button type="button" onClick={onEdit} className={ITEM_STYLE}> 수정하기 </button> <button type="button" onClick={onDelete} className={ITEM_STYLE}> 삭제하기 </button> </div> ); -} +}); + +export default OptionsMenuButton;Also applies to: 16-21, 23-33
apps/client/src/pages/myBookmark/MyBookmark.tsx (4)
71-72: 런타임 오류: 존재하지 않는 close() 호출useAnchoredMenu에서 가져온 함수명은 closeMenu인데 close()를 호출하고 있습니다. 즉시 수정 필요.
- close(); + closeMenu();
56-58: 카테고리 필터 미적용으로 목록/카운트 불일치categoryId가 존재해도 목록은 항상 전체/미읽음 쿼리를 사용합니다. 카테고리 전용 쿼리(categoryArticles)를 적용하세요.
- const articlesToDisplay = - activeBadge === 'all' ? articles?.articles : unreadArticles?.articles; + const listSource = categoryId + ? categoryArticles + : activeBadge === 'all' + ? articles + : unreadArticles; + const articlesToDisplay = listSource?.articles;동일 맥락으로 배지 카운트도 아래 코멘트 패치를 참고하세요.
114-117: 배지 카운트도 카테고리 컨텍스트 반영 필요카테고리 선택 시 카운트가 전체 기준으로 표시됩니다.
- countNum={articles?.totalArticle || 0} + countNum={(categoryId ? categoryArticles?.totalArticle : articles?.totalArticle) || 0} ... - countNum={articles?.totalUnreadArticle || 0} + countNum={(categoryId ? categoryArticles?.totalUnreadArticle : articles?.totalUnreadArticle) || 0}Also applies to: 119-123
136-138: 새 탭 오픈 시 reverse‑tabnabbing 방지window.open에 noopener,noreferrer 특성을 추가하세요.
- window.open(article.url, '_blank'); + window.open(article.url, '_blank', 'noopener,noreferrer');apps/client/src/pages/remind/Remind.tsx (3)
52-54: 런타임 오류: 존재하지 않는 close() 호출여기서도 closeMenu가 아닌 close()를 호출하고 있습니다. 즉시 수정 필요.
- queryClient.invalidateQueries({ queryKey: ['remindArticles'] }); - close(); + queryClient.invalidateQueries({ queryKey: ['remindArticles'] }); + closeMenu();
111-112: reverse‑tabnabbing 방지새 탭 오픈 시 보안 특성 추가 필요.
- onClick={() => { - window.open(article.url, '_blank'); + onClick={() => { + window.open(article.url, '_blank', 'noopener,noreferrer');
104-110: Options 메뉴 ID 불일치 — categoryId가 articleId로 사용되고 있습니다 (긴급 수정 필요)
openMenu에 article.category.categoryId를 전달하는 반면, onEdit 핸들러에서 전달된 id로 getArticleDetail(id)를 호출해 categoryId로 아티클 상세를 조회하는 흐름이 확인되었습니다. openMenu에 articleId를 전달하거나(onOptionsClick → openMenu(article.id,...)), 아니면 onEdit에서 id 의미에 따라 분기/매핑하여 getArticleDetail에 올바른 articleId만 전달하도록 수정하세요.
위치: apps/client/src/pages/remind/Remind.tsx (openMenu 호출부 ≈ 104–110, onEdit 핸들러 ≈ 137–141), 참고: apps/client/src/shared/apis/queries.ts (getArticleDetail).
🧹 Nitpick comments (14)
apps/client/src/shared/components/sidebar/SideItem.tsx (1)
34-42: div 클릭 대상의 접근성 보완 필요: role/tabIndex/키보드 핸들링 추가 권장컨테이너가 div이므로 키보드 접근(Enter/Space)이 불가합니다. 내부에 button이 존재해 부모를 button으로 바꾸기 어렵기 때문에, role="button"과 tabIndex, onKeyDown을 추가하는 쪽을 추천합니다.
- <div + <div + role="button" + tabIndex={0} className={cn( 'flex h-[4.4rem] items-center gap-[0.8rem] rounded-[0.4rem] px-[0.8rem] py-[1.2rem]', 'cursor-pointer transition-colors', active ? 'bg-main0 text-main600' : 'bg-white-bg text-font-gray-2', className )} - onClick={onClick} + onClick={onClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} >apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx (1)
6-11: 빈 상태 일러스트 alt 텍스트 정리해당 이미지는 장식적 용도라면 alt를 빈 문자열로 두는 편이 스크린 리더에 적합합니다. (현재 "No Articles"는 불필요한 영문 아나운스가 됩니다.)
- <img src={chippiNoArticles} alt="No Articles" /> + <img src={chippiNoArticles} alt="" />apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
25-27: ARIA 메뉴 패턴 불일치: role 사용을 간소화하거나 menuitem 역할 부여
role="menu"를 사용하면 자식에role="menuitem"과 키보드 네비게이션(↑/↓)이 필요합니다. 구현 부담을 줄이려면 role을 제거(혹은group)하고 기본 버튼 접근성에 맡기는 것을 권장합니다.- role="menu" - aria-label="옵션 메뉴" + role="group" + aria-label="옵션 메뉴"Also applies to: 33-38
apps/client/src/pages/remind/components/noRemindArticles/NoRemindArticles.tsx (2)
6-11: 장식 이미지 alt 정리장식적 이미지라면 alt를 빈 문자열로 두어 불필요한 낭독을 막아주세요.
- <img src={chippiNoArticles} alt="No Articles" /> + <img src={chippiNoArticles} alt="" />
3-14: EmptyState 패턴 공통화 제안동일한 레이아웃/타이포 조합의 EmptyState 컴포넌트가 다수 생기고 있습니다.
EmptyState({ image, title, description })같은 프레젠테이셔널 컴포넌트를 공용으로 추출하면 중복 제거와 카피/스타일 일관성 유지가 용이합니다.apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
106-112: 로고에 커서 포인터만 있고 클릭 동작 없음 → UX 혼동
cursor-pointer가 추가됐지만 클릭 핸들러가 없어 사용자에게 혼동을 줄 수 있습니다. 로고를 홈/리마인드로 이동시키거나 포인터를 제거하세요.- <header className="px-[0.8rem] py-[2.8rem]"> - <Icon - name="logo" - aria-label="Pinback 로고" - className="h-[2.4rem] w-[8.7rem] cursor-pointer" - /> - </header> + <header className="px-[0.8rem] py-[2.8rem]"> + <button + type="button" + aria-label="대시보드로 이동" + className="cursor-pointer" + onClick={() => { + closeMenu(); + goRemind(); + }} + > + <Icon name="logo" aria-hidden className="h-[2.4rem] w-[8.7rem]" /> + </button> + </header>apps/client/src/pages/myBookmark/components/noUnreadArticles/NoUnreadArticles.tsx (2)
6-11: 장식 이미지 alt 정리장식 목적이면 alt를 빈 문자열로 두세요. 불필요한 영문 아나운스가 제거됩니다.
- <img src={chippiNoRemindArticles} alt="No Articles" /> + <img src={chippiNoRemindArticles} alt="" />
3-14: EmptyState 공용 컴포넌트화 제안
NoReadArticles,NoRemindArticles와 동일한 레이아웃입니다. 공통 EmptyState로 추출해 복붙을 줄이고 카피/스타일을 한 곳에서 유지하세요.packages/design-system/src/components/card/BaseCard.tsx (1)
3-7: DOM 속성 위임/forwardRef 고려id/aria-* 전달 및 포커스 제어를 위해 ...props/forwardRef를 도입하는 것을 권장합니다.
원하시면 전체 적용 패치도 제공 가능합니다.
Also applies to: 9-9
apps/client/src/pages/myBookmark/MyBookmark.tsx (4)
86-92: 빈 상태 분기 보강: 뷰 컨텍스트 고려현재는 “모아보기(all)”에서도 항상 NoUnreadArticles가 표시될 수 있습니다. activeBadge·카테고리 기준으로 분기하세요.
- const EmptyStateComponent = () => { - if (articles?.totalArticle === 0) { - return <NoArticles />; - } - return <NoUnreadArticles />; - }; + const EmptyStateComponent = () => { + const totalAll = + (categoryId ? categoryArticles?.totalArticle : articles?.totalArticle) ?? 0; + if (totalAll === 0) return <NoArticles />; + return activeBadge === 'notRead' ? <NoUnreadArticles /> : <NoArticles />; + };
43-44: 디버그 로그 제거불필요한 console.log는 제거해주세요.
- console.log('categoryArticles', categoryArticles);
94-94: 스크롤 컨테이너 구조 정리(내부 h-screen 제거, flex-1 사용)중첩 h-screen은 이중 스크롤/레이아웃 깨짐을 유발할 수 있습니다.
- <div className="flex h-screen flex-col py-[5.2rem] pl-[8rem] pr-[5rem]"> + <div className="flex h-screen flex-col py-[5.2rem] pl-[8rem] pr-[5rem]"> ... - <div className="scrollbar-hide mt-[2.6rem] flex h-screen flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> + <div className="scrollbar-hide mt-[2.6rem] flex flex-1 flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">Also applies to: 127-127
59-77: 쿼리 키 팩토리 도입 권장문자열 키 오타/중복을 방지하기 위해 queryKey factory로 통합 관리하세요.
원하시면 키 팩토리 초안 제공 가능합니다.
apps/client/src/pages/remind/Remind.tsx (1)
79-80: 스크롤 레이아웃 일관성 확보MyBookmark와 동일하게 화면 높이 기준 내부 스크롤을 권장합니다.
- <div className="flex flex-col py-[5.2rem] pl-[8rem] pr-[5rem]"> + <div className="flex h-screen flex-col py-[5.2rem] pl-[8rem] pr-[5rem]"> ... - <div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth"> + <div className="scrollbar-hide mt-[2.6rem] flex flex-1 flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">Also applies to: 97-98
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/client/src/pages/myBookmark/MyBookmark.tsx(4 hunks)apps/client/src/pages/myBookmark/components/noUnreadArticles/NoUnreadArticles.tsx(1 hunks)apps/client/src/pages/remind/Remind.tsx(5 hunks)apps/client/src/pages/remind/components/noReadArticles/NoReadArticles.tsx(1 hunks)apps/client/src/pages/remind/components/noRemindArticles/NoRemindArticles.tsx(1 hunks)apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx(1 hunks)apps/client/src/shared/components/sidebar/SideItem.tsx(1 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(1 hunks)packages/design-system/src/components/card/BaseCard.tsx(1 hunks)packages/design-system/src/components/card/MyBookmarkCard.tsx(1 hunks)packages/design-system/src/components/card/RemindCard.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 (3)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
packages/design-system/src/icons/components/icon.tsx (1)
Icon(71-114)
packages/design-system/src/components/card/BaseCard.tsx (1)
packages/design-system/src/lib/utils.ts (1)
cn(108-110)
apps/client/src/pages/remind/Remind.tsx (1)
apps/client/src/pages/remind/apis/queries.ts (1)
useGetRemindArticles(6-16)
⏰ 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 (9)
apps/client/src/shared/components/sidebar/SideItem.tsx (1)
37-37: 시각적 힌트 추가는 적절합니다
cursor-pointer와transition-colors추가로 상호작용 affordance가 좋아졌습니다.apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
12-12: 좌측 정렬 패딩 변경 OK
justify-center제거 후pl-[0.8rem]으로 좌측 정렬 의도 반영된 것 확인했습니다.packages/design-system/src/components/card/RemindCard.tsx (1)
25-25: BaseCard height를 외부에서 주입하도록 변경한 점 좋습니다
className="h-[35.6rem]"으로 카드 높이를 명시적으로 제어하게 되어 베이스 컴포넌트의 응집도가 좋아졌습니다.packages/design-system/src/components/card/MyBookmarkCard.tsx (1)
25-25: BaseCard height 주입 패턴 일관성 확보 👍
RemindCard와 동일한 패턴으로 높이를 주입하여 디자인 일관성과 재사용성을 높였습니다.packages/design-system/src/components/card/BaseCard.tsx (2)
6-6: className 위임 및 cn 머지 사용 변경은 적절합니다레이아웃 확장성(높이/여백 제어)이 확보되었습니다. 디자인시스템 컴포넌트로서 방향성 👍
Also applies to: 13-16
1-1: 확인 완료 — '../../lib'가 cn을 재export합니다
packages/design-system/src/lib/index.ts가 './utils'를 export * 하고 있으며 packages/design-system/src/lib/utils.ts에 cn이 정의되어 있으므로 import 경로 변경 불필요.apps/client/src/pages/myBookmark/MyBookmark.tsx (1)
79-81: REMIND_MOCK_DATA 사용 의도 확인북마크 페이지에서 리마인드 상수로 카테고리명을 조회하는 것이 의도인지 확인이 필요합니다(네이밍/도메인 혼동 가능).
원하시면 카테고리 소스 일원화/타입 보강을 위한 리팩터를 제안하겠습니다.
apps/client/src/pages/remind/Remind.tsx (2)
65-71: 빈 상태 분기 LGTM전체 무자료/읽음/안읽음 상태 분리 로직이 명확합니다.
29-35: React Query v5 사용 여부 확인 및 로딩 UI 통일package.json에서 @tanstack/react-query를 찾을 수 없습니다 (실행한 jq 스크립트 결과: null). isPending는 v5 전제입니다 — 코드베이스에서 '@tanstack/react-query' 또는 'react-query' 임포트/lockfile을 검색해 실제 설치 버전을 확인하고, v5가 아니라면 isPending 사용을 교체(예: isLoading/isFetching)하세요. 로딩 UI는 디자인시스템의 Progress 컴포넌트로 통일 권장. (위치: apps/client/src/pages/remind/Remind.tsx: 29-35, 73-76)
| const BaseCard = ({ children, onClick, className }: BaseCardProps) => { | ||
| return ( | ||
| <div | ||
| onClick={onClick} | ||
| className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white h-[12rem]" | ||
| className={cn( | ||
| 'border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white', | ||
| className | ||
| )} |
There was a problem hiding this comment.
클릭 가능한 div의 키보드 접근성 보완 필요
onClick만 부여된 div는 키보드로 포커스/활성화가 어려워 a11y 이슈입니다. onClick 존재 시 role/tabIndex/Enter·Space 핸들링을 추가하세요.
다음 패치를 제안합니다:
-const BaseCard = ({ children, onClick, className }: BaseCardProps) => {
+const BaseCard = ({ children, onClick, className }: BaseCardProps) => {
+ const isInteractive = Boolean(onClick);
return (
<div
- onClick={onClick}
+ onClick={onClick}
+ role={isInteractive ? 'button' : undefined}
+ tabIndex={isInteractive ? 0 : undefined}
+ onKeyDown={
+ isInteractive
+ ? (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick?.();
+ }
+ }
+ : undefined
+ }
className={cn(
'border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white',
className
)}
>
{children}
</div>
);
}📝 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.
| const BaseCard = ({ children, onClick, className }: BaseCardProps) => { | |
| return ( | |
| <div | |
| onClick={onClick} | |
| className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white h-[12rem]" | |
| className={cn( | |
| 'border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white', | |
| className | |
| )} | |
| const BaseCard = ({ children, onClick, className }: BaseCardProps) => { | |
| const isInteractive = Boolean(onClick); | |
| return ( | |
| <div | |
| onClick={onClick} | |
| role={isInteractive ? 'button' : undefined} | |
| tabIndex={isInteractive ? 0 : undefined} | |
| onKeyDown={ | |
| isInteractive | |
| ? (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onClick?.(); | |
| } | |
| } | |
| : undefined | |
| } | |
| className={cn( | |
| 'border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white', | |
| className | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/BaseCard.tsx around lines 9 to 16,
the component renders a div with an onClick but no keyboard accessibility; when
onClick is provided add role="button", tabIndex={0}, and an onKeyDown handler
that calls the same onClick for Enter (key === 'Enter') and Space (key === ' '
or key === 'Spacebar') while preventing default for Space to avoid page scroll;
ensure these additions are only applied when onClick is present and preserve
existing className/children behavior.
📌 Related Issues
📄 Tasks
ETC. 사소한 리팩토링
Summary by CodeRabbit