Api(client): Dashboard category 전체 조회 & 생성 API 연결#73
Api(client): Dashboard category 전체 조회 & 생성 API 연결#73constantly-dev merged 10 commits intodevelopfrom
Conversation
Walkthrough사이드바에서 정적 CATEGORIES를 제거하고 서버의 대시보드 카테고리 API를 사용하도록 변경했으며, 이를 위한 Axios 헬퍼, React Query 훅, 타입 정의를 추가하고 PopupPortal에 입력 변경(onChange) 핸들러를 연결했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor 사용자
participant Sidebar as Sidebar.tsx
participant QHook as useGetDashboardCategories
participant API as getDashboardCategories
participant Server as /api/v1/categories/dashboard
Note over Sidebar,QHook: 초기 렌더링 흐름
사용자->>Sidebar: 사이드바 진입 / 마운트
Sidebar->>QHook: useQuery(['dashboardCategories']) 호출
QHook->>API: getDashboardCategories() 요청 (GET)
API->>Server: GET /api/v1/categories/dashboard
Server-->>API: 200 OK (DashboardCategoriesResponse)
API-->>QHook: 응답 데이터 반환
QHook-->>Sidebar: data.categories
Sidebar->>Sidebar: categories 매핑 후 렌더링 (id, name, unreadCount)
Note over Sidebar: PopupPortal에 onChange/onCreateConfirm 전달
사용자->>Sidebar: 카테고리 생성 입력 및 확인
Sidebar->>Sidebar: handleCreateCategory -> usePostCategory.mutate
Sidebar->>QHook: queryClient.invalidateQueries(['dashboardCategories']) onSuccess
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–30 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks (2 passed, 3 warnings)❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
Poem
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (10)
apps/client/src/shared/components/sidebar/types/api.ts (3)
1-5: 타입 불변성 강화: 필드에 readonly 부여 권장컴포넌트 트리에서 불필요한 객체 변이를 예방하고 예측 가능성을 높일 수 있습니다.
다음처럼 변경을 제안합니다:
export interface Category { - categoryId: number; - categoryName: string; - unreadCount: number; + readonly categoryId: number; + readonly categoryName: string; + readonly unreadCount: number; }
7-9: 생성 API 연동 대비: 요청/응답 타입 추가 제안Issue #71 범위(카테고리 생성)까지 고려하면 요청/응답 타입을 함께 정의해두는 편이 좋습니다.
아래 타입을 동일 파일 하단에 추가하는 것을 제안합니다:
export interface DashboardCategoriesResponse { categories: Category[]; } + +export interface CreateDashboardCategoryRequest { + categoryName: string; +} + +export interface CreateDashboardCategoryResponse { + category: Category; +}
1-9: 타입 파일 위치 재검토컴포넌트 하위가 아닌 공유 타입 경로(예: @shared/types/category.ts)로 이동하면 재사용성이 높아집니다.
apps/client/src/shared/components/sidebar/axios.ts (1)
3-6: 응답 타입 명시 + AbortSignal 지원으로 취소 가능 요청 구현React Query의 signal을 전달하면 탭 전환 시 불필요한 네트워크/상태 업데이트를 줄일 수 있습니다.
적용 예시는 아래와 같습니다:
+import type { + DashboardCategoriesResponse, + CreateDashboardCategoryRequest, + CreateDashboardCategoryResponse, +} from './types/api'; + +const DASHBOARD_CATEGORIES_ENDPOINT = '/api/v1/categories/dashboard'; + -export const getDashboardCategories = async () => { - const { data } = await apiRequest.get('/api/v1/categories/dashboard'); - return data; -}; +export const getDashboardCategories = async ( + signal?: AbortSignal +): Promise<DashboardCategoriesResponse> => { + const { data } = await apiRequest.get<DashboardCategoriesResponse>( + DASHBOARD_CATEGORIES_ENDPOINT, + { signal } + ); + return data; +}; + +export const createDashboardCategory = async ( + body: CreateDashboardCategoryRequest, + signal?: AbortSignal +): Promise<CreateDashboardCategoryResponse> => { + const { data } = await apiRequest.post<CreateDashboardCategoryResponse>( + DASHBOARD_CATEGORIES_ENDPOINT, + body, + { signal } + ); + return data; +};apps/client/src/shared/components/sidebar/queries.ts (2)
6-13: React Query: abort 신호 전달 및 캐시 파라미터 튜닝리스트 성격상 신선도 요구가 낮으므로 staleTime/gcTime을 설정하고, signal을 axios로 전달하세요.
아래처럼 변경 제안합니다:
-import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { getDashboardCategories } from './axios'; +import { useQuery, UseQueryResult, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getDashboardCategories, createDashboardCategory } from './axios'; -import { DashboardCategoriesResponse } from './types/api'; +import { DashboardCategoriesResponse, CreateDashboardCategoryRequest } from './types/api'; export const useGetDashboardCategories = (): UseQueryResult< DashboardCategoriesResponse, AxiosError > => { return useQuery({ queryKey: ['dashboardCategories'], - queryFn: () => getDashboardCategories(), + queryFn: ({ signal }) => getDashboardCategories(signal), + staleTime: 5 * 60 * 1000, + gcTime: 30 * 60 * 1000, }); }; + +export const useCreateDashboardCategory = () => { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: CreateDashboardCategoryRequest) => + createDashboardCategory(body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['dashboardCategories'] }); + }, + }); +};
1-14: Issue #71 범위와의 정합성: 생성 훅 추가 필요PR 목표에 “카테고리 생성 API 연결”이 포함되어 있습니다. 위 diff처럼 useCreateDashboardCategory 훅을 노출해 Sidebar에서 사용할 수 있게 해주세요.
원하시면 Sidebar까지 연결하는 커밋 패치를 만들어드릴게요.
apps/client/src/shared/components/sidebar/Sidebar.tsx (4)
16-16: 로딩/에러 상태 처리 및 리스트 디폴트값 지정현재는 data 미존재 시 UI 피드백이 없습니다. UX 보완을 위해 간단한 처리 추가를 권장합니다.
-import { useGetDashboardCategories } from './queries'; +import { useGetDashboardCategories, useCreateDashboardCategory } from './queries'; +import { useMemo } from 'react'; ... - const { data: categories } = useGetDashboardCategories(); + const { data: categories, isLoading, isError } = useGetDashboardCategories(); + const list = categories?.categories ?? []; + const createCategory = useCreateDashboardCategory();필요 시 로딩 상태에 스켈레톤/플레이스홀더를 렌더링해 주세요.
39-41: getCategoryName의 선형 탐색 비용 제거반복 호출 시 O(n) 탐색이 됩니다. 맵 캐싱으로 상수 시간 조회로 바꾸세요.
- const getCategoryName = (id: number | null) => - categories?.categories.find((category) => category.categoryId === id) - ?.categoryName ?? ''; + const categoryNameMap = useMemo( + () => new Map(list.map((c) => [c.categoryId, c.categoryName])), + [list] + ); + const getCategoryName = (id: number | null) => + id == null ? '' : categoryNameMap.get(id) ?? '';
79-91: 보호 카테고리(“안 읽은 정보”) 옵션 비활성화장기 학습에 따르면 첫 기본 카테고리는 삭제 불가여야 합니다. 옵션 버튼을 비활성화하여 실수 삭제를 예방하세요.
- {categories?.categories?.map((category) => ( + {list.map((category) => ( <CategoryItem key={category.categoryId} id={category.categoryId} label={category.categoryName} active={selectedCategoryId === category.categoryId} onClick={(id) => { closeMenu(); selectCategory(id); }} - onOptionsClick={(id, el) => openMenu(id, el)} + onOptionsClick={ + category.categoryName === '안 읽은 정보' + ? undefined + : (id, el) => openMenu(id, el) + } /> ))}백엔드에서 별도 플래그(예: isDefault/isDeletable)를 내려준다면 해당 플래그 기반으로 분기하도록 개선해 주세요.
122-137: 생성/수정/삭제 TODO 연결Issue #71 충족을 위해 최소 생성은 본 PR에서 마무리하는 것이 좋아 보입니다.
아래처럼 생성 확인 콜백에서 변이를 호출해 주세요(Modal이 name을 전달한다는 가정):
- onCreateConfirm={() => { - // TODO: 생성 API - close(); - }} + onCreateConfirm={(name: string) => { + createCategory.mutate( + { categoryName: name }, + { onSuccess: () => close() } + ); + }}PopupPortal 콜백 시그니처가 다르다면 알려 주세요. 거기에 맞춰 패치를 제공하겠습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/client/src/shared/components/sidebar/Sidebar.tsx(3 hunks)apps/client/src/shared/components/sidebar/axios.ts(1 hunks)apps/client/src/shared/components/sidebar/queries.ts(1 hunks)apps/client/src/shared/components/sidebar/types/api.ts(1 hunks)
🧰 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/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (2)
apps/client/src/shared/components/sidebar/queries.ts (2)
apps/client/src/shared/components/sidebar/types/api.ts (1)
DashboardCategoriesResponse(7-9)apps/client/src/shared/components/sidebar/axios.ts (1)
getDashboardCategories(3-6)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/components/sidebar/queries.ts (1)
useGetDashboardCategories(6-14)apps/client/src/shared/components/sidebar/CategoryItem.tsx (1)
CategoryItem(13-61)
⏰ 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/shared/components/sidebar/axios.ts (1)
3-6: 엔드포인트 확인 필요생성도 동일 엔드포인트(DASHBOARD_CATEGORIES_ENDPOINT)로 POST가 맞는지 백엔드 스펙 확인 부탁드립니다. 경로가 다를 경우 상수만 조정하면 됩니다.
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 (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
125-136: 카테고리 생성 API 미연결 — 이슈 #71 목표 미충족onCreateConfirm에 TODO가 남아 있어 생성 API가 실제로 호출되지 않습니다. 생성 성공 후 목록 무효화도 필요합니다.
아래와 같이 최소 배선 제안드립니다.
- API 추가(axios.ts):
export const createDashboardCategory = (name: string) => apiRequest .post('/api/v1/categories/dashboard', { name }) .then((res) => res.data);
- 훅 추가(queries.ts):
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { createDashboardCategory } from './axios'; export const useCreateDashboardCategory = () => { const qc = useQueryClient(); return useMutation({ mutationFn: createDashboardCategory, onSuccess: () => qc.invalidateQueries({ queryKey: ['dashboardCategories'] }), }); };
- 본 컴포넌트 적용:
- 상단:
const createCategory = useCreateDashboardCategory();- 콜백 수정(이름 인자 전달 필요: PopupPortal의 시그니처를
(name: string)으로 확장):- onCreateConfirm={() => { - // TODO: 생성 API - close(); - }} + onCreateConfirm={(name) => { + createCategory.mutate(name, { onSuccess: close }); + }}필요 시 PopupPortal의 onCreateConfirm 시그니처 확장을 함께 PR에 포함해 주세요.
🧹 Nitpick comments (5)
apps/client/src/shared/components/sidebar/apis/axios.ts (1)
3-6: 런타임 스키마 검증(zod) 도입 검토백엔드 응답 스키마 변경 시 조기 탐지에 유용합니다. 과하지 않다면 아래처럼 parse 한 번 거치는 것을 권장합니다.
import { z } from 'zod'; const DashboardCategoriesResponseSchema = z.object({ categories: z.array( z.object({ categoryId: z.number(), categoryName: z.string(), unreadCount: z.number(), }) ), }); // ... const { data } = await apiRequest.get<DashboardCategoriesResponse>(/* ... */); return DashboardCategoriesResponseSchema.parse(data);apps/client/src/shared/components/sidebar/apis/queries.ts (1)
10-13: React Query 취소 신호 전달 + 캐시 전략 기본값 보강취소 신호를 전달하고, 불필요한 재요청을 줄이기 위해 기본 staleTime을 설정하세요.
- return useQuery({ - queryKey: ['dashboardCategories'], - queryFn: () => getDashboardCategories(), - }); + return useQuery({ + queryKey: ['dashboardCategories'], + queryFn: ({ signal }) => getDashboardCategories(signal), + staleTime: 60_000, + });추가로 queryKey를 상수(예: sidebarKeys.dashboardCategories)로 추출하면 invalidate 시 오타를 줄일 수 있습니다.
apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
16-16: 로딩/에러/빈 상태 처리로 UX 보완데이터 지연·실패·빈 목록을 구분해 피드백을 주세요.
- const { data: categories } = useGetDashboardCategories(); + const { data: categories, isLoading, isError } = useGetDashboardCategories();예시(추가 코드): isLoading 시 스켈레톤, isError 시 재시도 버튼, 빈 배열 시 안내문구 표시.
38-41: 경계값 빠른 리턴으로 가독성/미세 최적화null일 때 탐색을 건너뜁니다.
- const getCategoryName = (id: number | null) => - categories?.categories.find((category) => category.categoryId === id) - ?.categoryName ?? ''; + const getCategoryName = (id: number | null) => { + if (id == null) return ''; + return ( + categories?.categories.find((c) => c.categoryId === id)?.categoryName ?? '' + ); + };
79-85: 빈 목록/로딩 시 안전 렌더링map 이전에 안전 가드 또는 대체 UI를 두면 깜빡임을 줄일 수 있습니다. 예:
(categories?.categories ?? []).map(...)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/shared/components/sidebar/Sidebar.tsx(3 hunks)apps/client/src/shared/components/sidebar/apis/axios.ts(1 hunks)apps/client/src/shared/components/sidebar/apis/queries.ts(1 hunks)
🧰 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/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (2)
apps/client/src/shared/components/sidebar/apis/queries.ts (2)
apps/client/src/shared/components/sidebar/types/api.ts (1)
DashboardCategoriesResponse(7-9)apps/client/src/shared/components/sidebar/apis/axios.ts (1)
getDashboardCategories(3-6)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/components/sidebar/apis/queries.ts (1)
useGetDashboardCategories(6-14)apps/client/src/shared/components/sidebar/CategoryItem.tsx (1)
CategoryItem(13-61)
⏰ 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/shared/components/sidebar/Sidebar.tsx (1)
97-106: 기본 카테고리("안 읽은 정보") 편집/삭제 비활성화 여부 확인 필요Sidebar에서 OptionsMenuPortal에 전달되는 categoryId가 기본 카테고리인지 판별해 편집/삭제 옵션이 비활성화되도록 구현되어 있는지 확인 필요. (파일: apps/client/src/shared/components/sidebar/Sidebar.tsx:97-106)
자동 스크립트 실행 결과 관련 식별자(안 읽은 정보, UNREAD, DEFAULT_CATEGORY), OptionsMenuPortal 내 비활성화 로직 또는 Category 타입의 기본 구분 필드가 검색되지 않았음 — 레포에서 OptionsMenuPortal.tsx와 Category 타입을 직접 확인할 것.
| import apiRequest from '@shared/apis/axiosInstance'; | ||
|
|
||
| export const getDashboardCategories = async () => { | ||
| const { data } = await apiRequest.get('/api/v1/categories/dashboard'); | ||
| return data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
반환 타입 명시 + 요청 취소(AbortSignal) 지원으로 타입 안정성과 UX 개선
응답 타입을 명시하고, React Query의 cancellation 신호를 전달받아 Axios에 전달해주세요.
-import apiRequest from '@shared/apis/axiosInstance';
+import apiRequest from '@shared/apis/axiosInstance';
+import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api';
-export const getDashboardCategories = async () => {
- const { data } = await apiRequest.get('/api/v1/categories/dashboard');
- return data;
-};
+export const getDashboardCategories = async (
+ signal?: AbortSignal
+): Promise<DashboardCategoriesResponse> => {
+ const { data } = await apiRequest.get<DashboardCategoriesResponse>(
+ '/api/v1/categories/dashboard',
+ { signal }
+ );
+ return 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.
| import apiRequest from '@shared/apis/axiosInstance'; | |
| export const getDashboardCategories = async () => { | |
| const { data } = await apiRequest.get('/api/v1/categories/dashboard'); | |
| return data; | |
| }; | |
| import apiRequest from '@shared/apis/axiosInstance'; | |
| import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api'; | |
| export const getDashboardCategories = async ( | |
| signal?: AbortSignal | |
| ): Promise<DashboardCategoriesResponse> => { | |
| const { data } = await apiRequest.get<DashboardCategoriesResponse>( | |
| '/api/v1/categories/dashboard', | |
| { signal } | |
| ); | |
| return data; | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/apis/axios.ts around lines 1 to 6,
the exported getDashboardCategories function lacks an explicit return type and
does not accept an AbortSignal for request cancellation; update the signature to
accept an optional AbortSignal parameter and return a typed Promise (e.g.,
Promise<Category[]> or the correct response DTO), pass that signal into the
axios request config (apiRequest.get<YourType>(url, { signal })), and ensure the
function uses the generic type on apiRequest.get and returns the typed data.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
apps/client/src/shared/components/sidebar/apis/queries.ts (2)
9-17: useQuery 제네릭 지정 + 상수 키 사용제네릭을 명시하지 않으면 TData/TError가 넓게 추론됩니다. 함수 레퍼런스로 간결화하고 staleTime도 기본값 설정을 제안합니다.
-export const useGetDashboardCategories = (): UseQueryResult< - DashboardCategoriesResponse, - AxiosError -> => { - return useQuery({ - queryKey: ['dashboardCategories'], - queryFn: () => getDashboardCategories(), - }); -}; +export const useGetDashboardCategories = () => { + return useQuery<DashboardCategoriesResponse, AxiosError>({ + queryKey: DASHBOARD_CATEGORIES_QUERY_KEY, + queryFn: getDashboardCategories, + staleTime: 30_000, + }); +};
19-23: API 헬퍼 반환값 일관화(data만 반환)로 DX 개선GET은
data를 반환하지만 POST는 AxiosResponse 전체를 반환합니다. 일관화를 권장합니다(후속 타입 추론/DevTools 가독성 개선).apps/client/src/shared/components/sidebar/apis/axios.ts (참고용, 파일 외 수정):
-export const postCategory = async (categoryName: string) => { - const response = await apiRequest.post('/api/v1/categories', { - categoryName, - }); - return response; -}; +export const postCategory = async (categoryName: string) => { + const { data } = await apiRequest.post(/* <TResponse> 제네릭 지정 가능 */ '/api/v1/categories', { + categoryName, + }); + return data; +};
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/shared/components/sidebar/apis/axios.ts(1 hunks)apps/client/src/shared/components/sidebar/apis/queries.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/shared/components/sidebar/apis/axios.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/shared/components/sidebar/apis/queries.ts (2)
apps/client/src/shared/components/sidebar/types/api.ts (1)
DashboardCategoriesResponse(7-9)apps/client/src/shared/components/sidebar/apis/axios.ts (2)
getDashboardCategories(3-6)postCategory(8-13)
| import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; | ||
| import { | ||
| getDashboardCategories, | ||
| postCategory, | ||
| } from '@shared/components/sidebar/apis/axios'; | ||
| import { AxiosError } from 'axios'; | ||
| import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
쿼리 무효화를 위한 useQueryClient 추가 + 쿼리 키 상수화
생성 후 목록 갱신을 위해 queryClient가 필요합니다. 키를 상수로 노출해 오탈자/중복을 줄이는 것도 권장합니다.
- import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
+ import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query';
@@
import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api';
+
+export const DASHBOARD_CATEGORIES_QUERY_KEY = ['dashboardCategories'] as const;📝 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.
| import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; | |
| import { | |
| getDashboardCategories, | |
| postCategory, | |
| } from '@shared/components/sidebar/apis/axios'; | |
| import { AxiosError } from 'axios'; | |
| import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api'; | |
| import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query'; | |
| import { | |
| getDashboardCategories, | |
| postCategory, | |
| } from '@shared/components/sidebar/apis/axios'; | |
| import { AxiosError } from 'axios'; | |
| import { DashboardCategoriesResponse } from '@shared/components/sidebar/types/api'; | |
| export const DASHBOARD_CATEGORIES_QUERY_KEY = ['dashboardCategories'] as const; |
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/apis/queries.ts around lines 1 to
7, add useQueryClient import and expose a constant query key for dashboard
categories; update the category mutation to call
queryClient.invalidateQueries(QUERY_KEYS.dashboardCategories) (or
.refetchQueries) after a successful post so the list refreshes, and export the
QUERY_KEYS object to avoid typos/duplication across code.
| export const usePostCategory = (categoryName: string) => { | ||
| return useMutation({ | ||
| mutationFn: () => postCategory(categoryName), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
생성 후 목록 자동 반영: invalidateQueries/캐시 업데이트 추가 필요
현재 생성 성공 후 목록이 갱신되지 않습니다. 최소한 invalidateQueries가 필요합니다. 권장안(변수 기반)과 최소 수정안을 함께 드립니다.
권장안(호출부에서 mutate(categoryName)로 전달):
-export const usePostCategory = (categoryName: string) => {
- return useMutation({
- mutationFn: () => postCategory(categoryName),
- });
-};
+export const usePostCategory = () => {
+ const queryClient = useQueryClient();
+ return useMutation<unknown, AxiosError, string>({
+ mutationKey: ['createCategory'],
+ mutationFn: (name) => postCategory(name),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: DASHBOARD_CATEGORIES_QUERY_KEY });
+ },
+ });
+};최소 수정안(현 시그니처 유지):
export const usePostCategory = (categoryName: string) => {
- return useMutation({
- mutationFn: () => postCategory(categoryName),
- });
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: () => postCategory(categoryName),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: DASHBOARD_CATEGORIES_QUERY_KEY });
+ },
+ });
};📝 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 usePostCategory = (categoryName: string) => { | |
| return useMutation({ | |
| mutationFn: () => postCategory(categoryName), | |
| }); | |
| }; | |
| export const usePostCategory = (categoryName: string) => { | |
| const queryClient = useQueryClient(); | |
| return useMutation({ | |
| mutationFn: () => postCategory(categoryName), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: DASHBOARD_CATEGORIES_QUERY_KEY }); | |
| }, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/apis/queries.ts around lines 19-23,
the mutation does not update the categories list after creating a category; add
cache invalidation: either (recommended) change the mutation to accept the name
at call site (mutationFn: (name: string) => postCategory(name)) and call
queryClient.invalidateQueries with the variable-aware key onSuccess, or
(minimal) keep the current signature and add an onSuccess handler that calls
queryClient.invalidateQueries(['categories']) (or the exact categories query key
used elsewhere) so the list refreshes after creation.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
apps/client/src/shared/components/sidebar/PopupPortal.tsx (1)
8-8: 입력 변경 전파 범위 확장 + 편집 팝업 초기값 설정 필요
- create 경로의
onInputChange={onChange}연결은 적절합니다. 다만 edit 경로에서도 동일하게 연결하지 않으면 사이드바의newCategoryName이 갱신되지 않습니다.- 편집 시 현재 이름을 placeholder가 아닌 초기값으로 넣는 것이 일반적입니다. 디자인시스템 Popup이 지원하는 prop(
defaultValue또는value)을 사용해 주세요.예시(참고용, edit 블록 내부 수정):
<Popup type="input" title="카테고리 수정하기" left="취소" right="확인" /* 디자인시스템 명세에 따라 defaultValue 또는 value 사용 */ defaultValue={popup.name} onInputChange={onChange} onLeftClick={onClose} onRightClick={() => onEditConfirm?.(popup.id)} />추가로, prop 명을
onChange→onDraftChange등으로 구체화하면 의도가 더 분명해집니다(선택).디자인시스템 Popup의 입력값 제어 prop 명(
defaultValue/value)을 확인해 주세요. 지원 prop이 다르면 그에 맞춰 반영이 필요합니다.Also applies to: 17-17, 34-34
apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
17-19: 임시 콘솔 로그 제거머지 전
console.log는 제거해 주세요.적용 diff:
- console.log(newCategoryName); // 임시 로그
20-20: 가독성: 변수명 명확화(categories.categories→dashboardCategories.categories)중첩된 이름이 읽기 어렵습니다. 조회 결과 변수명을 분리하면 명확해집니다.
적용 diff:
- const { data: categories } = useGetDashboardCategories(); + const { data: dashboardCategories } = useGetDashboardCategories();- const getCategoryName = (id: number | null) => - categories?.categories.find((category) => category.categoryId === id) - ?.categoryName ?? ''; + const getCategoryName = (id: number | null) => + dashboardCategories?.categories.find((category) => category.categoryId === id) + ?.categoryName ?? '';- {categories?.categories?.map((category) => ( + {dashboardCategories?.categories?.map((category) => ( <CategoryItem - key={category.categoryId} - id={category.categoryId} - label={category.categoryName} - active={selectedCategoryId === category.categoryId} + key={category.categoryId} + id={category.categoryId} + label={category.categoryName} + active={selectedCategoryId === category.categoryId} /> ))}Also applies to: 42-45, 87-93
42-45: getCategoryName 선형 탐색 비용 축소(useMemo Map 캐싱)호출이 잦아질 경우를 대비해 맵으로 캐싱하면 좋습니다.
예시:
const categoryMap = useMemo(() => { const map = new Map<number, string>(); dashboardCategories?.categories.forEach(c => map.set(c.categoryId, c.categoryName)); return map; }, [dashboardCategories?.categories]); const getCategoryName = (id: number | null) => (id == null ? '' : categoryMap.get(id) ?? '');
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/shared/components/sidebar/PopupPortal.tsx(3 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(4 hunks)
🧰 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/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/components/sidebar/apis/queries.ts (1)
useGetDashboardCategories(9-17)apps/client/src/shared/components/sidebar/CategoryItem.tsx (1)
CategoryItem(13-61)
🔇 Additional comments (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
87-99: 기본 카테고리(“안 읽은 정보”) 삭제 방지 확인학습 메모에 따르면 해당 기본 카테고리는 첫번째로 노출되며 삭제가 불가합니다. 메뉴/옵션에서 삭제 항목을 숨기거나 비활성화하는 방어 로직이 필요한지 확인하세요.
옵션:
- 서버에서 구분 플래그 제공 시 해당 플래그 기준으로 처리
- 임시로
category.categoryName === '안 읽은 정보'또는 특정 ID 고정 값으로 처리(서버 확정 전)
13-13: 카테고리 생성 API 즉시 연결 + 입력값 검증(trim 후 빈값 차단)
- onCreateConfirm에서 newCategoryName.trim() 후 빈값이면 조기 리턴, mutateAsync로 생성 API 호출, 성공 시 상태 초기화 및 close().
- 서버 페이로드 키(
categoryName등)와 usePostCategory 훅 시그니처(반환값/인자 형태)를 반드시 확인해 페이로드를 맞출 것.적용 diff:
-import { useGetDashboardCategories } from '@shared/components/sidebar/apis/queries'; +import { useGetDashboardCategories, usePostCategory } from '@shared/components/sidebar/apis/queries';- onCreateConfirm={() => { - // TODO: 생성 API - close(); - }} + onCreateConfirm={async () => { + const name = newCategoryName.trim(); + if (!name) return; + await createCategory({ categoryName: name }); // API 스펙에 맞게 payload 키 확인 필요 + setNewCategoryName(''); + close(); + }}훅 인스턴스(추천 위치: 다른 훅 선언부 인근):
const { mutateAsync: createCategory } = usePostCategory();
b8c7630 to
240e16a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
apps/client/src/shared/components/sidebar/types/api.ts (1)
1-5: 백엔드 스키마와 필드명 동기화 + 불변 속성 권장API가
unread_count(snake_case)로 내려줄 가능성 등 명명 차이가 있으면 런타임에서 터집니다. 백엔드 스키마와 정확히 일치하는지 확인하고, 클라이언트 모델은 불변으로 두는 걸 권장합니다.다음처럼 불변 속성으로 두는 것도 좋습니다:
-export interface Category { - id: number; - name: string; - unreadCount: number; -} +export interface Category { + readonly id: number; + readonly name: string; + readonly unreadCount: number; +}필요 시 zod로 런타임 검증을 붙이면 안전합니다.
apps/client/src/shared/components/sidebar/Sidebar.tsx (4)
13-19: React Query 키 상수화로 오타/중복 방지
'dashboardCategories'키 문자열을 여기저기 중복 사용 중입니다. queries.ts에 키 상수를 export하고 공용 사용을 권장합니다.queries.ts 예시(파일 외 변경):
// apps/client/src/shared/components/sidebar/apis/queries.ts export const QK_DASHBOARD_CATEGORIES = ['dashboardCategories'] as const;이 파일 적용:
- queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); + queryClient.invalidateQueries({ queryKey: QK_DASHBOARD_CATEGORIES });
21-26: LGTM + 로딩/에러 UI 처리 제안상태 관리와 훅 연결은 적절합니다. 다만
isLoading/error처리로 UX 보강을 추천합니다(스켈레톤/토스트 등).
49-50: 카테고리명 조회 O(1)로 최적화(useMemo Map 캐싱)매번
find로 O(n)입니다. 맵 캐싱으로 단순/효율 개선을 제안합니다.추가 import:
-import { useState } from 'react'; +import { useState, useMemo } from 'react';치환:
-const getCategoryName = (id: number | null) => - categories?.categories.find((category) => category.id === id)?.name ?? ''; +const nameById = useMemo( + () => new Map(categories?.categories?.map((c) => [c.id, c.name]) ?? []), + [categories] +); +const getCategoryName = (id: number | null) => + id == null ? '' : nameById.get(id) ?? '';
151-153: PopupPortal prop 연결은 적절 — 바쁜 상태에서 중복 제출 방지 검토연결은 좋아 보입니다. 생성 중(disabled 처리 등) 중복 제출 방지도 고려해 주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/client/src/shared/components/sidebar/PopupPortal.tsx(3 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(5 hunks)apps/client/src/shared/components/sidebar/apis/axios.ts(1 hunks)apps/client/src/shared/components/sidebar/apis/queries.ts(1 hunks)apps/client/src/shared/components/sidebar/types/api.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/client/src/shared/components/sidebar/PopupPortal.tsx
- apps/client/src/shared/components/sidebar/apis/axios.ts
- apps/client/src/shared/components/sidebar/apis/queries.ts
🧰 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/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
apps/client/src/shared/components/sidebar/apis/queries.ts (2)
useGetDashboardCategories(9-17)usePostCategory(19-23)apps/client/src/shared/hooks/useCategoryPopups.ts (1)
useCategoryPopups(9-18)apps/client/src/shared/components/sidebar/CategoryItem.tsx (1)
CategoryItem(13-61)
🔇 Additional comments (4)
apps/client/src/shared/components/sidebar/types/api.ts (1)
7-9: 응답 래퍼 타입과 axios 반환값(data / data.data) 일치 여부 확인 필요
- 현황: apps/client/src/shared/components/sidebar/apis/axios.ts의 getDashboardCategories가
return data.data로 내부 payload를 반환합니다.- 조치(짧게):
data.data가{ categories: Category[] }형태인지 확인하세요. 아니라면 apps/client/src/shared/components/sidebar/types/api.ts의 타입을 API 응답에 맞게 수정하거나, getDashboardCategories의 반환값(혹은 반환 타입 선언)을 API 구조에 맞게 조정(예:return data또는return data.data.categories및Promise<...>명시)하세요. 관련 호출: apps/client/src/shared/components/sidebar/apis/queries.ts.apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
37-39: 팝업 상태 훅 사용 적절팝업 상태 분리는 깔끔합니다.
51-54: 입력 핸들러는 OK트림은 제출 시점에서 처리하는 편이 UX에 자연스럽습니다.
105-117: 기본 카테고리(안 읽은 정보) 고정·옵션 비활성화 필요 — API 식별자 확인 요망장기 학습에 따르면 기본 카테고리는 항상 첫 항목이고 삭제 불가여야 합니다. 현재 apps/client/src/shared/components/sidebar/Sidebar.tsx에서 categories를 그대로 map하고 있어 프런트 보호 로직이 없습니다.
- 확인 요청: 백엔드가 기본 카테고리를 구분하는 값(name / id / isDefault 중 무엇)을 알려주세요. 코드베이스에서 "안 읽음"은 발견되지만 "안 읽은 정보"는 존재하지 않습니다.
- 권장 구현(간단): map 전에 기본 카테고리를 맨 앞으로 정렬하고(CategoryItem 렌더링 전), 기본 카테고리의 옵션 버튼은 비활성화 또는 숨기기. 예:
- 정렬: categories?.categories?.slice().sort((a,b)=> a.isDefault ? -1 : b.isDefault ? 1 : 0).map(...)
- 옵션 비활성화: onOptionsClick={category.isDefault || category.name === '안 읽은 정보' ? undefined : (id, el) => openMenu(id, el)}
| const handleCreateCategory = () => { | ||
| createCategory(newCategoryName, { | ||
| onSuccess: () => { | ||
| handleCategoryChange(''); | ||
| queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); | ||
| close(); | ||
| }, | ||
| onError: (error) => { | ||
| console.error('카테고리 생성 실패:', error); | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
생성 유효성 검증(빈값/중복) + 키 상수화 반영
빈 문자열/공백만 입력, 중복 이름 방지를 최소한으로 막아 주세요. 동시에 앞서 제안한 쿼리 키 상수 사용으로 일관성을 높일 수 있습니다.
-const handleCreateCategory = () => {
- createCategory(newCategoryName, {
+const handleCreateCategory = () => {
+ const name = newCategoryName.trim();
+ if (!name) return;
+ if (categories?.categories?.some((c) => c.name === name)) {
+ console.warn('중복 카테고리명입니다.');
+ return;
+ }
+ createCategory(name, {
onSuccess: () => {
- handleCategoryChange('');
- queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
+ handleCategoryChange('');
+ // QK_DASHBOARD_CATEGORIES 상수 사용 권장
+ queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
close();
},
onError: (error) => {
console.error('카테고리 생성 실패:', error);
},
});
};📝 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 handleCreateCategory = () => { | |
| createCategory(newCategoryName, { | |
| onSuccess: () => { | |
| handleCategoryChange(''); | |
| queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); | |
| close(); | |
| }, | |
| onError: (error) => { | |
| console.error('카테고리 생성 실패:', error); | |
| }, | |
| }); | |
| }; | |
| const handleCreateCategory = () => { | |
| const name = newCategoryName.trim(); | |
| if (!name) return; | |
| if (categories?.categories?.some((c) => c.name === name)) { | |
| console.warn('중복 카테고리명입니다.'); | |
| return; | |
| } | |
| createCategory(name, { | |
| onSuccess: () => { | |
| handleCategoryChange(''); | |
| // QK_DASHBOARD_CATEGORIES 상수 사용 권장 | |
| queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); | |
| close(); | |
| }, | |
| onError: (error) => { | |
| console.error('카테고리 생성 실패:', error); | |
| }, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/Sidebar.tsx around lines 55 to 66,
add pre-submit validation and use the shared query key constant: trim
newCategoryName and if the result is empty (''), set a local validation error or
show a user-facing message and return early; check the trimmed name against the
current categories list (case-insensitive) to detect duplicates and similarly
show an error and return; only call createCategory when validation passes; and
replace the literal queryClient.invalidateQueries({ queryKey:
['dashboardCategories'] }) with the centralized constant (e.g.,
QUERY_KEYS.dashboardCategories) for consistency.
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
큰 이슈와 이해할 때 어려운 점이 없는 것 같아서 간단하게만 남깁니다!
Dashboard category 생성 API는
onSuccess에invalidateQueries를 통해 대시보드 전체 카테고리 캐시를 무효화해서 바로 업데이트 돼서 반영되도록 설정해줬어요!error 관련 UI처리는 이후에 스켈레톤 UI나 로딩바 등을 디자이너 분들게 요청해서 작업하면 좋겠네요! 하나 하나 하기보다
error boundary나roading boundary등으로 중앙화해주면 좋을 것 같아요 👍📷 Screenshot
2025-09-11.9.15.34.mov
Summary by CodeRabbit
신규 기능
리팩터링