Skip to content

Comments

Feat(client): 툴팁 연결#265

Merged
jjangminii merged 11 commits intodevelopfrom
feat/#264/tooltip-connection
Feb 24, 2026
Merged

Feat(client): 툴팁 연결#265
jjangminii merged 11 commits intodevelopfrom
feat/#264/tooltip-connection

Conversation

@jjangminii
Copy link
Collaborator

@jjangminii jjangminii commented Feb 23, 2026

📌 Related Issues

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

📄 Tasks

  • 레벨페이지 ui 변경 반영
  • 레벨페이지 툴팁 연결
  • 사이드바 도토리 획득시 말풍선 연결

⭐ PR Point (To Reviewer)

초기 로딩 시 말풍선 표시 방지

사이드바에서 초기 페이지 진입 또는 새로고침 시 React Query 데이터 로딩 과정에서 말풍선이 표시되는 문제가 있어 이를 수정했습니다.
처음에는 state로 관리하다가 새로고침시에도 pending 상태일때와 비교하기에 수정하였습니다

  • isPending 상태일 때는 말풍선 로직을 실행하지 않도록 처리
  • 최초 데이터 로딩 시에는 기준값만 저장하도록 수정

📷 Screenshot

image image

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 도토리 수집 시 애니메이션 포함 자동 소멸 토스트 알림 추가
    • 도토리 수집 현황에 따른 격려 메시지를 풍선형 UI로 표시
    • 작업 고정(Job Pin) 안내를 풍선형 포탈로 제공
    • 카테고리 생성/수정/삭제 흐름을 사용자 관점에서 단순화
  • Style

    • 풍선 UI의 간격·패딩·둥근 모서리 등 시각 개선
    • 레벨 정보 버튼의 아이콘·텍스트 간격 조정

@jjangminii jjangminii self-assigned this Feb 23, 2026
@jjangminii jjangminii linked an issue Feb 23, 2026 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pinback-client-client Ready Ready Preview, Comment Feb 24, 2026 3:55pm
pinback-client-landing Ready Ready Preview, Comment Feb 24, 2026 3:55pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Walkthrough

Level 페이지와 Sidebar에 도토리 관련 오버레이(Toast/Balloon) 및 Lottie 애니메이션을 추가하고, 카테고리 액션을 훅으로 분리했습니다. Balloon 스타일 조정과 JobPin 가이드 포털 컴포넌트도 도입되었습니다. (≤50단어)

Changes

Cohort / File(s) Summary
의존성
apps/client/package.json
lottie-react ^2.4.1 추가 및 react-router-dom 항목에 트레일링 콤마 조정.
Level 페이지
apps/client/src/pages/level/Level.tsx
acornCount 기반 메시지 로직 추가, Balloon 도입, TreeStatusCard 제거, 로딩/에러 빈 엘리먼트 정리 및 info 버튼 스타일 조정.
Balloon 컴포넌트
apps/client/src/shared/components/balloon/Balloon.tsx
컨테이너 클래스, 패딩 및 border-radius 조정; 닫기 아이콘 크기 16→20으로 변경.
Sidebar 및 토스트
apps/client/src/shared/components/sidebar/Sidebar.tsx
Lottie(chippiface) 통합, acornCount 증가 감지(useEffect)로 AutoDismissToast+Balloon 토스트 표시, 토스트 상태/키/prevRef 관리, 카테고리 액션 훅 도입 및 레이아웃/팝업 흐름 조정.
카테고리 액션 훅
apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts
새 훅 useCategoryActions 추가: create/patch/delete 카테고리 처리, 모달/토스트 관리, 대시보드 카테고리 무효화 및 네비게이션 로직 제공.
JobPin 가이드 포털
apps/client/src/shared/components/sidebar/JobPinGuidePortal.tsx
anchorEl 위치 기반으로 우측 중앙 정렬되는 Balloon 포털 컴포넌트 추가(열림/닫힘 처리).

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant Sidebar as Sidebar 컴포넌트
    participant Hook as useEffect / useCategoryActions
    participant Toast as AutoDismissToast
    participant Lottie as Lottie 애니메이션
    participant Balloon as Balloon 컴포넌트

    User->>Sidebar: 액션으로 acornCount 변경
    Sidebar->>Hook: acornCount 변경 감지
    Hook->>Hook: prevAcornRef와 비교 (증가 판단)
    Hook->>Sidebar: acornToastOpen=true, acornToastKey 갱신
    Sidebar->>Toast: 토스트 렌더 (key 전달)
    Toast->>Lottie: chippiface 애니메이션 재생
    Toast->>Balloon: 축하/메시지 표시
    Lottie->>Toast: 재생 완료/이벤트
    Toast->>Sidebar: 자동 닫힘 신호
    Sidebar->>Hook: acornToastOpen=false (상태 정리)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🛠️ Feature, frontend

Suggested reviewers

  • constantly-dev
  • jllee000

Poem

🐰 도토리 톡톡, 풍선에 실어 띄워요,
반짝 토스트에 웃음 한 줌 담아,
로티가 춤추면 기분은 더 좋아,
오늘도 하나둘 모아볼게요! 🌰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '툴팁 연결'로 주요 변경 사항(툴팁/말풍선 연결)을 명확하게 요약하고 있으며, 변경 내용과 밀접하게 관련되어 있습니다.
Description check ✅ Passed PR 설명은 템플릿 구조를 잘 따르고 있으며, 관련 이슈, 작업 내용, 리뷰어 주의사항, 스크린샷 등이 모두 포함되어 있습니다.
Linked Issues check ✅ Passed 변경 사항이 이슈 #264 (레벨페이지 및 사이드바 툴팁 연결)의 요구사항을 충족합니다: Level.tsx에서 말풍선 UI 추가, Sidebar.tsx에서 도토리 획득 시 말풍선 연결, JobPinGuidePortal 컴포넌트 추가됨.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 #264 이슈의 범위 내에 있습니다: package.json의 lottie-react 추가는 말풍선 애니메이션 구현을 위한 의존성이며, 모든 코드 변경도 UI 연결 목표와 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#264/tooltip-connection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
apps/client/src/pages/level/Level.tsx (3)

29-29: isPending 분기는 도달 불가능한 코드입니다 (dead code).

Line 19에서 isPendingtrue일 때 이미 early return하므로, Line 29의 isPending ? defaultLevel : ... 삼항 조건은 항상 false 분기만 실행됩니다. defaultLevel 변수(Line 28)도 함께 불필요합니다.

♻️ dead code 제거 제안
-  const defaultLevel: TreeLevel = 1;
-  const level = isPending ? defaultLevel : (info.level as TreeLevel);
+  const level = info.level as TreeLevel;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/level/Level.tsx` at line 29, The ternary using
isPending is dead code because isPending already triggers an early return;
replace the declaration "const level = isPending ? defaultLevel : (info.level as
TreeLevel)" with a direct assignment "const level = info.level as TreeLevel" and
remove the now-unused defaultLevel variable (and any related unused
imports/refs) to simplify the code.

31-36: balloonText의 홀짝(parity) 기반 로직이 자명하지 않습니다.

acornCount === 0 || acornCount % 2 === 0 ? 1 : 2로 "다시 읽을 북마크 수"를 결정하는 이유가 코드만으로는 파악하기 어렵습니다. 레벨 임계값과의 관계를 설명하는 짧은 주석을 추가하면 유지보수에 도움이 됩니다.

또한, acornCount === 0은 이미 짝수이므로 acornCount % 2 === 0 조건에 포함됩니다. 별도로 체크할 필요가 없습니다.

♻️ 중복 조건 제거 및 주석 추가 제안
   const balloonText =
     acornCount >= 7
       ? '도토리를 모두 모았어요!'
-      : `다음 레벨까지 저장한 북마크 ${
-          acornCount === 0 || acornCount % 2 === 0 ? 1 : 2
-        }개 다시 읽기`;
+      : `다음 레벨까지 저장한 북마크 ${
+          // 레벨 임계값이 짝수 단위이므로, 짝수 acorn → 1개, 홀수 → 2개 남음
+          acornCount % 2 === 0 ? 1 : 2
+        }개 다시 읽기`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/level/Level.tsx` around lines 31 - 36, The ternary
inside balloonText uses a redundant acornCount === 0 check and lacks context:
simplify the condition to use acornCount % 2 === 0 ? 1 : 2 and add a short
comment near balloonText explaining why parity determines "bookmarks to reread"
(e.g., how acornCount maps to the next level threshold or pairing logic). Update
the expression that computes "다시 읽기" to remove the duplicate zero check and
place a one-line comment referencing acornCount parity and the level threshold
behavior so future readers understand the rationale.

76-88: Balloon 위치 지정에 매직 넘버 -translate-x-[53px]가 사용되었습니다.

Line 83의 -translate-x-[53px]은 Badge 너비에 의존하는 하드코딩 값으로 보입니다. Badge 텍스트나 폰트 크기가 변경되면 Balloon 꼬리와 Badge의 정렬이 깨질 수 있습니다. 가능하다면 CSS 기반 동적 정렬(예: left-0 또는 Badge 중앙 기준 계산)을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/level/Level.tsx` around lines 76 - 88, The Balloon is
positioned with a hardcoded magic number (-translate-x-[53px]) which breaks
alignment if Badge size changes; update the layout to center the Balloon
relative to the Badge by using a CSS centering approach instead of a fixed pixel
translate: keep the wrapper as the same relative inline-block, target the
Balloon container (the div with absolute left/top classes) and replace the
hardcoded translate with a percent-based centering strategy (e.g., left: 50%
plus translateX(-50%) or use inset-x-0 with mx-auto) so the Balloon stays
horizontally centered to the Badge regardless of Badge text/size; apply this
change where Badge and Balloon are rendered in Level.tsx.
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)

286-289: 토스트 메시지가 항상 "도토리 1개"로 고정되어 있습니다.

현재 acornCount > prevAcornRef.current 조건만 확인하고, 실제 증가분(acornCount - prevAcornRef.current)은 사용하지 않습니다. 만약 한 번에 2개 이상 증가하는 시나리오가 가능하다면, 메시지가 부정확할 수 있습니다. 현재 비즈니스 로직상 항상 1개씩만 증가한다면 문제없습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` around lines 286 -
289, Toast message in Sidebar.tsx is hardcoded to "도토리 1개" even when acornCount
increases by more than one; compute the actual delta and use it in the toast
text. Change the logic that currently checks acornCount > prevAcornRef.current
to calculate const delta = acornCount - prevAcornRef.current, ensure delta > 0
before showing the toast, and replace the hardcoded "도토리 1개" string with a
template using delta (e.g., `도토리 ${delta}개를 모았어요!`); keep existing updates to
prevAcornRef.current after showing the toast.
apps/client/src/shared/components/balloon/Balloon.tsx (1)

29-29: whitespace-nowrap 추가 시 긴 텍스트 오버플로우 가능성

whitespace-nowrap이 추가되어 텍스트 줄바꿈이 방지됩니다. 현재 사용처(Level.tsx, Sidebar.tsx)에서는 짧은 텍스트만 전달하므로 문제없지만, 향후 긴 텍스트가 전달되면 컨테이너 밖으로 오버플로우될 수 있습니다. max-widthoverflow-hidden 같은 안전장치를 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/balloon/Balloon.tsx` at line 29, The
Balloon component's class list currently includes "whitespace-nowrap", which
prevents wrapping and can cause long text to overflow; update the Balloon
component styling (in Balloon.tsx) to prevent overflow for future long content
by either removing "whitespace-nowrap" or adding safe guards such as a max-width
and overflow handling (e.g., max-w-full / overflow-hidden with
text-overflow/truncation) so text from consumers like Level.tsx or Sidebar.tsx
won't escape the container.
apps/client/package.json (1)

21-22: lottie-react 의존성이 알파벳 순서에 맞지 않게 추가되었습니다.

기존 dependencies가 알파벳 순서로 정렬되어 있으므로, lottie-reactframer-motionreact 사이에 위치해야 합니다.

🔧 알파벳 순서 정렬 제안
     "framer-motion": "^12.23.12",
+    "lottie-react": "^2.4.1",
     "react": "^19.1.1",
     "react-dom": "^19.1.1",
     "react-error-boundary": "^6.0.0",
-    "react-router-dom": "^7.8.2",
-    "lottie-react": "^2.4.1"
+    "react-router-dom": "^7.8.2"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/package.json` around lines 21 - 22, Dependencies in
apps/client/package.json are not alphabetically ordered: "lottie-react" is
placed after "react-router-dom" but should be between "framer-motion" and
"react"; open package.json and move the "lottie-react" entry so the
"dependencies" list is alphabetized (place "lottie-react": "^2.4.1" between
"framer-motion" and "react") to restore consistent ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/client/package.json`:
- Around line 21-22: Dependencies in apps/client/package.json are not
alphabetically ordered: "lottie-react" is placed after "react-router-dom" but
should be between "framer-motion" and "react"; open package.json and move the
"lottie-react" entry so the "dependencies" list is alphabetized (place
"lottie-react": "^2.4.1" between "framer-motion" and "react") to restore
consistent ordering.

In `@apps/client/src/pages/level/Level.tsx`:
- Line 29: The ternary using isPending is dead code because isPending already
triggers an early return; replace the declaration "const level = isPending ?
defaultLevel : (info.level as TreeLevel)" with a direct assignment "const level
= info.level as TreeLevel" and remove the now-unused defaultLevel variable (and
any related unused imports/refs) to simplify the code.
- Around line 31-36: The ternary inside balloonText uses a redundant acornCount
=== 0 check and lacks context: simplify the condition to use acornCount % 2 ===
0 ? 1 : 2 and add a short comment near balloonText explaining why parity
determines "bookmarks to reread" (e.g., how acornCount maps to the next level
threshold or pairing logic). Update the expression that computes "다시 읽기" to
remove the duplicate zero check and place a one-line comment referencing
acornCount parity and the level threshold behavior so future readers understand
the rationale.
- Around line 76-88: The Balloon is positioned with a hardcoded magic number
(-translate-x-[53px]) which breaks alignment if Badge size changes; update the
layout to center the Balloon relative to the Badge by using a CSS centering
approach instead of a fixed pixel translate: keep the wrapper as the same
relative inline-block, target the Balloon container (the div with absolute
left/top classes) and replace the hardcoded translate with a percent-based
centering strategy (e.g., left: 50% plus translateX(-50%) or use inset-x-0 with
mx-auto) so the Balloon stays horizontally centered to the Badge regardless of
Badge text/size; apply this change where Badge and Balloon are rendered in
Level.tsx.

In `@apps/client/src/shared/components/balloon/Balloon.tsx`:
- Line 29: The Balloon component's class list currently includes
"whitespace-nowrap", which prevents wrapping and can cause long text to
overflow; update the Balloon component styling (in Balloon.tsx) to prevent
overflow for future long content by either removing "whitespace-nowrap" or
adding safe guards such as a max-width and overflow handling (e.g., max-w-full /
overflow-hidden with text-overflow/truncation) so text from consumers like
Level.tsx or Sidebar.tsx won't escape the container.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Around line 286-289: Toast message in Sidebar.tsx is hardcoded to "도토리 1개"
even when acornCount increases by more than one; compute the actual delta and
use it in the toast text. Change the logic that currently checks acornCount >
prevAcornRef.current to calculate const delta = acornCount -
prevAcornRef.current, ensure delta > 0 before showing the toast, and replace the
hardcoded "도토리 1개" string with a template using delta (e.g., `도토리 ${delta}개를
모았어요!`); keep existing updates to prevAcornRef.current after showing the toast.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17daed1 and b93e6ef.

📒 Files selected for processing (5)
  • apps/client/package.json
  • apps/client/src/assets/5_chippiface.json
  • apps/client/src/pages/level/Level.tsx
  • apps/client/src/shared/components/balloon/Balloon.tsx
  • apps/client/src/shared/components/sidebar/Sidebar.tsx

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Feb 23, 2026
@github-actions
Copy link

github-actions bot commented Feb 23, 2026

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts (1)

42-69: 빈/공백 카테고리명은 사전에 차단하는 편이 UX에 유리합니다.
현재는 API 에러 후 토스트만 띄우므로, 클라이언트에서 trim() 후 빈 값이면 즉시 안내하는 흐름을 권장합니다.

♻️ 수정 제안
   const handleCreateCategory = () => {
-    createCategory(newCategoryName, {
+    const normalizedName = newCategoryName.trim();
+    if (!normalizedName) {
+      setToastIsOpen(true);
+      return;
+    }
+    createCategory(normalizedName, {
       onSuccess: () => {
         setNewCategoryName('');
         queryClient.invalidateQueries({
           queryKey: ['dashboardCategories'],
         });
         close();
       },
       onError: () => setToastIsOpen(true),
     });
   };

   const handlePatchCategory = (id: number) => {
-    patchCategory(
-      { id, categoryName: newCategoryName },
+    const normalizedName = newCategoryName.trim();
+    if (!normalizedName) {
+      setToastIsOpen(true);
+      return;
+    }
+    patchCategory(
+      { id, categoryName: normalizedName },
       {
         onSuccess: () => {
           queryClient.invalidateQueries({
             queryKey: ['dashboardCategories'],
           });
           setNewCategoryName('');
           close();
           moveNewCategory(id);
         },
         onError: () => setToastIsOpen(true),
       }
     );
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts` around
lines 42 - 69, Prevent submitting empty/whitespace-only names in
handleCreateCategory and handlePatchCategory by trimming newCategoryName before
calling createCategory/patchCategory; if trimmed value is empty, call
setToastIsOpen(true) (or another immediate client-side validation handler) and
return early without invoking createCategory/patchCategory or changing state.
Update both functions to use the trimmed name when sending to the API (pass {
id, categoryName: trimmedName } to patchCategory and trimmedName to
createCategory) and only clear/set state (setNewCategoryName(''),
moveNewCategory, close) after a successful API call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts`:
- Around line 36-40: The URL built in moveNewCategory currently interpolates
newCategoryName raw into navigate which can break routing when the name contains
&, ?, or spaces; update the navigate call inside moveNewCategory to pass
encodeURIComponent(newCategoryName) (keep id as-is) so the query param is
properly URL-encoded, leaving setActiveTab('mybookmark') and
setSelectedCategoryId(id) unchanged.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Around line 190-191: The Sidebar component is allowing deletion for the
default category "안 읽은 정보"; update the delete flow so the categories array
includes "안 읽은 정보" as the first default item and block deletion at the UI/action
layer by checking the category name or id before invoking openDelete: in the
Sidebar (and the child component that renders the menu/delete button where
onDelete calls openDelete) ensure onDelete is a no-op or hides the delete option
when the category equals "안 읽은 정보" (or its canonical id), and keep closeMenu
behavior unchanged; this enforces the non-deletable default category without
touching backend logic.
- Line 189: Before calling openEdit in the onEdit handler, initialize the local
category name state by calling handleCategoryChange(name) so the input-backed
state (newCategoryName) matches popup.name when the edit modal opens; this
ensures PopupPortal receives the correct initial value even if the user doesn't
type (since popup.name sets the input but onChange only fires on user input).
Locate the onEdit prop usage that currently calls openEdit(id, name) and add a
call to handleCategoryChange(name) immediately before invoking openEdit.

---

Nitpick comments:
In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts`:
- Around line 42-69: Prevent submitting empty/whitespace-only names in
handleCreateCategory and handlePatchCategory by trimming newCategoryName before
calling createCategory/patchCategory; if trimmed value is empty, call
setToastIsOpen(true) (or another immediate client-side validation handler) and
return early without invoking createCategory/patchCategory or changing state.
Update both functions to use the trimmed name when sending to the API (pass {
id, categoryName: trimmedName } to patchCategory and trimmedName to
createCategory) and only clear/set state (setNewCategoryName(''),
moveNewCategory, close) after a successful API call.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b93e6ef and 474907b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • apps/client/src/shared/components/sidebar/Sidebar.tsx
  • apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts

setToastIsOpen(false);
openDelete(id, name);
}}
onEdit={(id, name) => openEdit(id, name)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -a 'Sidebar\.tsx' apps/client/src

Repository: Pinback-Team/pinback-client

Length of output: 143


🏁 Script executed:

fd -a 'PopupPortal\.(t|j)sx?$'

Repository: Pinback-Team/pinback-client

Length of output: 236


🏁 Script executed:

# Find modal or popup related files
fd -a '(Modal|Popup|Edit).*\.(t|j)sx?$' apps/client/src/shared/components

Repository: Pinback-Team/pinback-client

Length of output: 404


🏁 Script executed:

# Read Sidebar.tsx around line 189
wc -l apps/client/src/shared/components/sidebar/Sidebar.tsx

Repository: Pinback-Team/pinback-client

Length of output: 128


🏁 Script executed:

# Read relevant sections of Sidebar.tsx
sed -n '180,210p' apps/client/src/shared/components/sidebar/Sidebar.tsx

Repository: Pinback-Team/pinback-client

Length of output: 1251


🏁 Script executed:

# Read PopupPortal.tsx to understand state management
cat -n apps/client/src/shared/components/sidebar/PopupPortal.tsx

Repository: Pinback-Team/pinback-client

Length of output: 5503


🏁 Script executed:

# Read more of Sidebar.tsx to understand openEdit and onEditConfirm handlers
sed -n '1,100p' apps/client/src/shared/components/sidebar/Sidebar.tsx

Repository: Pinback-Team/pinback-client

Length of output: 3144


🏁 Script executed:

# Search for openEdit, handleCategoryChange, onEditConfirm in Sidebar.tsx
rg -n "openEdit|handleCategoryChange|onEditConfirm" apps/client/src/shared/components/sidebar/Sidebar.tsx -A 3

Repository: Pinback-Team/pinback-client

Length of output: 815


🏁 Script executed:

# Check useCategoryActions hook to see how handlePatchCategory is implemented
fd -a 'useCategoryActions\.(t|j)s' apps/client/src

Repository: Pinback-Team/pinback-client

Length of output: 159


🏁 Script executed:

# Get more context around line 250-255 in Sidebar.tsx to see the full PopupPortal props
sed -n '245,260p' apps/client/src/shared/components/sidebar/Sidebar.tsx

Repository: Pinback-Team/pinback-client

Length of output: 524


🏁 Script executed:

# Read useCategoryActions.ts to see how handlePatchCategory works
cat -n apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts

Repository: Pinback-Team/pinback-client

Length of output: 3273


🏁 Script executed:

# Also check useCategoryPopups hook to understand openEdit
fd -a 'useCategoryPopups\.(t|j)s'

Repository: Pinback-Team/pinback-client

Length of output: 139


🏁 Script executed:

# Read useCategoryPopups.ts to understand openEdit
cat -n apps/client/src/shared/hooks/useCategoryPopups.ts

Repository: Pinback-Team/pinback-client

Length of output: 824


🏁 Script executed:

# Verify the exact flow by checking if onChange is called on popup mount
# Check PopupPortal useEffect again and trace when onChange is called
sed -n '36,75p' apps/client/src/shared/components/sidebar/PopupPortal.tsx

Repository: Pinback-Team/pinback-client

Length of output: 1069


openEdit 호출 전에 handleCategoryChange(name)으로 현재 카테고리 이름을 초기화하세요.

PopupPortal은 popup.name으로 input 값을 올바르게 초기화하지만, onChange는 사용자가 입력할 때만 호출됩니다. 사용자가 편집 팝업을 열었으나 수정하지 않고 확인하면 newCategoryName이 이전 값으로 남아 PATCH 요청 시 잘못된 이름이 전송됩니다.

🛠️ 수정 제안
-            onEdit={(id, name) => openEdit(id, name)}
+            onEdit={(id, name) => {
+              handleCategoryChange(name);
+              openEdit(id, name);
+            }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onEdit={(id, name) => openEdit(id, name)}
onEdit={(id, name) => {
handleCategoryChange(name);
openEdit(id, name);
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 189, Before
calling openEdit in the onEdit handler, initialize the local category name state
by calling handleCategoryChange(name) so the input-backed state
(newCategoryName) matches popup.name when the edit modal opens; this ensures
PopupPortal receives the correct initial value even if the user doesn't type
(since popup.name sets the input but onChange only fires on user input). Locate
the onEdit prop usage that currently calls openEdit(id, name) and add a call to
handleCategoryChange(name) immediately before invoking openEdit.

Comment on lines +190 to 191
onDelete={(id, name) => openDelete(id, name)}
onClose={closeMenu}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

기본 카테고리 ‘안 읽은 정보’는 삭제 금지 처리가 필요합니다.
기본 카테고리는 삭제가 불가능하도록 메뉴/액션 레벨에서 차단해야 합니다.

🛡️ 수정 제안
-            onDelete={(id, name) => openDelete(id, name)}
+            onDelete={(id, name) => {
+              if (name === '안 읽은 정보') {
+                setToastIsOpen(true);
+                return;
+              }
+              openDelete(id, name);
+            }}
Based on learnings "categories array should include '안 읽은 정보' (Unread Information) as the first default category that cannot be deleted."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` around lines 190 -
191, The Sidebar component is allowing deletion for the default category "안 읽은
정보"; update the delete flow so the categories array includes "안 읽은 정보" as the
first default item and block deletion at the UI/action layer by checking the
category name or id before invoking openDelete: in the Sidebar (and the child
component that renders the menu/delete button where onDelete calls openDelete)
ensure onDelete is a no-op or hides the delete option when the category equals
"안 읽은 정보" (or its canonical id), and keep closeMenu behavior unchanged; this
enforces the non-deletable default category without touching backend logic.

Copy link
Member

@constantly-dev constantly-dev left a comment

Choose a reason for hiding this comment

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

굿굿~ 카테고리 부분 훅으로 잘 분리해주신 것 같아요~ 로직 분리가 돼서 확실히 깔끔해졌네요 👍

Copy link
Member

Choose a reason for hiding this comment

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

👍 분리 좋네요~~

} from '@shared/apis/queries';
import { SidebarTab } from '@shared/hooks/useSidebarNav';

interface Params {
Copy link
Member

Choose a reason for hiding this comment

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

interface naming 수정해주세요!

const canCreateMore = categoryCount < MAX_CATEGORIES;

useEffect(() => {
if (isPending) return;
Copy link
Member

Choose a reason for hiding this comment

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

isPending이 무엇에 대한 것인지 몰라서, 위에서 rename 해주는 것이 좋을 것 같아요!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
apps/client/src/shared/components/sidebar/Sidebar.tsx (3)

38-38: isPending 변수명 명확화 (이전 리뷰 중복)

useGetArcons()에서 구조 분해한 isPending 임을 사용 시점에서 알기 어렵습니다. isAcornsPending 등으로 rename해 도토리 데이터 로딩 상태임을 명시적으로 표현하면 가독성이 향상됩니다.

♻️ 개선 제안
-  const { data, isPending } = useGetArcons();
+  const { data, isPending: isAcornsPending } = useGetArcons();

이후 isPending 참조 위치도 isAcornsPending으로 변경합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 38, 현재
useGetArcons()에서 구조 분해한 isPending 변수명이 모호합니다; useGetArcons() 호출부에서 const { data,
isPending } = useGetArcons(); 를 isAcornsPending으로 변경하고 해당 파일의 모든 isPending 참조를
isAcornsPending으로 업데이트하여 도토리(acorns) 로딩 상태임을 명시적으로 표현하세요. 참조 대상: useGetArcons,
isPending → isAcornsPending, 그리고 Sidebar.tsx 내부의 모든 사용 지점들을 일관되게 교체하세요.

207-207: ⚠️ Potential issue | 🟠 Major

기본 카테고리 '안 읽은 정보' 삭제 방지 처리 누락 (이전 리뷰 중복)

현재 onDelete는 기본 카테고리에 대해서도 삭제 팝업을 그대로 열어줍니다. 기본 카테고리는 삭제가 불가능하도록 차단해야 합니다.

🛡️ 수정 제안
-            onDelete={(id, name) => openDelete(id, name)}
+            onDelete={(id, name) => {
+              if (name === '안 읽은 정보') {
+                setToastIsOpen(true);
+                return;
+              }
+              openDelete(id, name);
+            }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 207, The
onDelete handler currently calls openDelete for every category; prevent deletion
of the default category "안 읽은 정보" by adding a guard either in the onDelete arrow
or inside the openDelete function: check the category id or name (compare to "안
읽은 정보" or use whatever isDefaultCategory/id check exists) and if it matches, do
not open the delete modal (optionally show a toast/warn), otherwise call
openDelete(id, name) as before; update references to onDelete and openDelete
accordingly.

206-206: ⚠️ Potential issue | 🟠 Major

openEdit 호출 전 카테고리 이름 초기화 누락 (이전 리뷰 중복)

편집 팝업을 열었으나 입력 필드를 수정하지 않고 확인하면 onChange가 호출되지 않아 이전 상태의 이름으로 PATCH 요청이 전송될 수 있습니다.

🛠️ 수정 제안
-            onEdit={(id, name) => openEdit(id, name)}
+            onEdit={(id, name) => {
+              handleCategoryChange(name);
+              openEdit(id, name);
+            }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 206, The
onEdit handler currently calls openEdit(id, name) without resetting the
edit-input state, so if the user opens the edit modal and presses confirm
without changing the input the previous name may be PATCHed; before calling
openEdit(id, name) clear or initialize the edit-input state (e.g., call the
component's edit-name setter like setEditName('') or setCategoryEditValue('')
depending on your state name) so the modal input starts in a known state and
onChange will run when the user types, then call openEdit(id, name).
🧹 Nitpick comments (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)

215-252: isPending 동안 MyLevelItem 까지 숨겨져 레이아웃 이동이 발생합니다

!isPending && 조건이 toast와 MyLevelItem을 함께 감싸고 있어, 초기 로딩 중에는 푸터 영역이 완전히 비어 있다가 데이터 도착 후 갑자기 나타납니다. toast 표시 방지가 목적이라면 두 조건을 분리하거나, MyLevelItem 에 대해 스켈레톤을 제공하면 레이아웃 이동 없이 더 부드러운 UX를 제공할 수 있습니다.

♻️ 개선 예시 (스켈레톤 분리)
-          {!isPending && (
-            <>
-              {acornToastOpen && (
-                <div className="absolute bottom-[10.2rem] left-1/2 -translate-x-1/2">
-                  ...
-                </div>
-              )}
-
-              <MyLevelItem
-                ...
-              />
-            </>
-          )}
+          {!isAcornsPending && acornToastOpen && (
+            <div className="absolute bottom-[10.2rem] left-1/2 -translate-x-1/2">
+              ...
+            </div>
+          )}
+
+          {isAcornsPending ? (
+            <div className="h-[4.8rem] animate-pulse rounded bg-gray-100" />
+          ) : (
+            <MyLevelItem
+              ...
+            />
+          )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` around lines 215 -
252, The current conditional wraps both the toast and MyLevelItem with
!isPending, causing layout shift when loading; change the logic so toast
rendering is gated by isPending (e.g., keep the acornToastOpen &&
AutoDismissToast block inside a guard) but render MyLevelItem regardless (or
render a skeleton/placeholder when isPending) — update the JSX around
acornToastOpen, AutoDismissToast, and MyLevelItem so that AutoDismissToast
remains hidden during isPending but MyLevelItem (or a skeleton variant of
MyLevelItem) is always mounted to preserve footer layout.
apps/client/src/shared/components/sidebar/JobPinGuidePortal.tsx (1)

13-23: 윈도우 리사이즈 시 위치가 갱신되지 않습니다 (선택적 개선)

getBoundingClientRect() 는 렌더 시점에 한 번만 호출되므로, 브라우저 창 크기가 변경되어도 위치가 갱신되지 않습니다. 사이드바가 sticky top-0 h-screen이기 때문에 스크롤에 의한 위치 오차는 발생하지 않습니다. 일회성 가이드 툴팁 특성상 실제 영향은 제한적이지만, 완전한 대응이 필요하다면 ResizeObserverwindow resize 이벤트 리스너를 추가해 위치를 재계산하는 방식을 고려할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/JobPinGuidePortal.tsx` around lines
13 - 23, The tooltip position is computed once from
anchorEl.getBoundingClientRect() so it won't update on window resize; modify
JobPinGuidePortal to track the anchor rect in state and recompute it on resize
by adding a ResizeObserver on anchorEl (or a window 'resize' listener) that
updates the stored rect used by createPortal, and ensure you clean up the
observer/listener on unmount and when anchorEl changes so the left/top values
(derived from rect.right/top/height) are always current.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Line 38: 현재 useGetArcons()에서 구조 분해한 isPending 변수명이 모호합니다; useGetArcons() 호출부에서
const { data, isPending } = useGetArcons(); 를 isAcornsPending으로 변경하고 해당 파일의 모든
isPending 참조를 isAcornsPending으로 업데이트하여 도토리(acorns) 로딩 상태임을 명시적으로 표현하세요. 참조 대상:
useGetArcons, isPending → isAcornsPending, 그리고 Sidebar.tsx 내부의 모든 사용 지점들을 일관되게
교체하세요.
- Line 207: The onDelete handler currently calls openDelete for every category;
prevent deletion of the default category "안 읽은 정보" by adding a guard either in
the onDelete arrow or inside the openDelete function: check the category id or
name (compare to "안 읽은 정보" or use whatever isDefaultCategory/id check exists)
and if it matches, do not open the delete modal (optionally show a toast/warn),
otherwise call openDelete(id, name) as before; update references to onDelete and
openDelete accordingly.
- Line 206: The onEdit handler currently calls openEdit(id, name) without
resetting the edit-input state, so if the user opens the edit modal and presses
confirm without changing the input the previous name may be PATCHed; before
calling openEdit(id, name) clear or initialize the edit-input state (e.g., call
the component's edit-name setter like setEditName('') or
setCategoryEditValue('') depending on your state name) so the modal input starts
in a known state and onChange will run when the user types, then call
openEdit(id, name).

---

Nitpick comments:
In `@apps/client/src/shared/components/sidebar/JobPinGuidePortal.tsx`:
- Around line 13-23: The tooltip position is computed once from
anchorEl.getBoundingClientRect() so it won't update on window resize; modify
JobPinGuidePortal to track the anchor rect in state and recompute it on resize
by adding a ResizeObserver on anchorEl (or a window 'resize' listener) that
updates the stored rect used by createPortal, and ensure you clean up the
observer/listener on unmount and when anchorEl changes so the left/top values
(derived from rect.right/top/height) are always current.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Around line 215-252: The current conditional wraps both the toast and
MyLevelItem with !isPending, causing layout shift when loading; change the logic
so toast rendering is gated by isPending (e.g., keep the acornToastOpen &&
AutoDismissToast block inside a guard) but render MyLevelItem regardless (or
render a skeleton/placeholder when isPending) — update the JSX around
acornToastOpen, AutoDismissToast, and MyLevelItem so that AutoDismissToast
remains hidden during isPending but MyLevelItem (or a skeleton variant of
MyLevelItem) is always mounted to preserve footer layout.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 474907b and 588b87e.

📒 Files selected for processing (3)
  • apps/client/src/shared/components/balloon/Balloon.tsx
  • apps/client/src/shared/components/sidebar/JobPinGuidePortal.tsx
  • apps/client/src/shared/components/sidebar/Sidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/shared/components/balloon/Balloon.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)

206-206: ⚠️ Potential issue | 🟠 Major

openEdit 호출 전 handleCategoryChange(name) 초기화가 여전히 누락되어 있습니다.

사용자가 편집 팝업을 열고 이름을 수정하지 않은 채 확인하면, newCategoryName이 빈 문자열 또는 이전 값으로 남아 잘못된 PATCH 요청이 전송됩니다.

수정 제안
-            onEdit={(id, name) => openEdit(id, name)}
+            onEdit={(id, name) => {
+              handleCategoryChange(name);
+              openEdit(id, name);
+            }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 206,
openEdit(id, name)을 호출하기 전에 newCategoryName 상태를 올바르게 초기화하도록
handleCategoryChange(name)를 호출해 주세요; 구체적으로 Sidebar 컴포넌트의 onEdit handler에서
openEdit(id, name) 호출 직전에 handleCategoryChange(name)를 실행하여 편집 모달이 열릴 때
newCategoryName이 현재 카테고리 이름으로 설정되도록 하고 불필요한 빈 또는 이전 값으로 인한 잘못된 PATCH 요청이 전송되는 것을
방지하세요.

207-207: ⚠️ Potential issue | 🟠 Major

기본 카테고리 '안 읽은 정보' 삭제 방지가 여전히 구현되지 않았습니다.

기본 카테고리는 삭제 불가능하도록 UI/액션 레벨에서 차단해야 합니다.

수정 제안
-            onDelete={(id, name) => openDelete(id, name)}
+            onDelete={(id, name) => {
+              if (name === '안 읽은 정보') return;
+              openDelete(id, name);
+            }}

Based on learnings "categories array should include '안 읽은 정보' (Unread Information) as the first default category that cannot be deleted."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` at line 207, Prevent
deletion of the default "안 읽은 정보" category by short-circuiting the delete action
in the Sidebar: update the onDelete handler (where onDelete={(id, name) =>
openDelete(id, name)} is passed) to return early when the category name is "안 읽은
정보" (or matches a known DEFAULT_UNREAD_ID if available) instead of calling
openDelete; additionally, reflect this in the UI by disabling or hiding the
delete control for that category so the action cannot be triggered.
🧹 Nitpick comments (2)
apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts (1)

57-72: handlePatchCategory에서 invalidateQueriesmoveNewCategory 호출 순서 확인 필요.

invalidateQueries는 비동기적으로 리페치를 트리거하며 await하지 않고 있습니다. 이후 moveNewCategory(id)가 즉시 호출되는데, 네비게이션 대상 페이지가 아직 업데이트되지 않은 캐시 데이터를 읽을 수 있습니다.

현재 동작에 문제가 없다면 무시하셔도 되지만, 만약 이동 후 이전 카테고리명이 잠깐 보이는 현상이 있다면 await을 추가하는 것을 고려해 주세요:

제안
-        onSuccess: () => {
-          queryClient.invalidateQueries({
+        onSuccess: async () => {
+          await queryClient.invalidateQueries({
             queryKey: ['dashboardCategories'],
           });
           setNewCategoryName('');
           close();
           moveNewCategory(id);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts` around
lines 57 - 72, handlePatchCategory currently calls queryClient.invalidateQueries
(which triggers async refetch) and then immediately calls moveNewCategory(id),
which can navigate using stale cache; make handlePatchCategory async and await
the invalidateQueries call (await queryClient.invalidateQueries({ queryKey:
['dashboardCategories'] })) before calling moveNewCategory(id) so navigation
happens after refetch completes; keep setNewCategoryName('') and close() as
needed but ensure moveNewCategory runs only after the await.
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)

214-252: !isAcornPending 조건으로 Footer 전체를 감싸면 초기 로드 시 레이아웃 시프트가 발생할 수 있습니다.

isAcornPendingtrue인 동안 MyLevelItem이 아예 렌더링되지 않아 Footer 영역이 비어 있다가 데이터 로드 후 갑자기 나타납니다. 토스트 깜빡임 방지는 토스트 부분에만 적용하고, MyLevelItem은 기본값(0)으로 항상 렌더링하는 것을 고려해 주세요.

제안
         <footer className="relative pb-[2.8rem] pt-[1.2rem]">
-          {!isAcornPending && (
-            <>
-              {acornToastOpen && (
-                <div className="absolute bottom-[10.2rem] left-1/2 -translate-x-1/2">
-                  <AutoDismissToast
-                    key={acornToastKey}
-                    duration={3000}
-                    onClose={() => setAcornToastOpen(false)}
-                  >
-                    <Balloon variant="main" side="bottom">
-                      ...
-                    </Balloon>
-                  </AutoDismissToast>
-                </div>
-              )}
-
-              <MyLevelItem
-                acorns={acornCount}
-                isActive={activeTab === 'level'}
-                onClick={() => {
-                  closeMenu();
-                  setSelectedCategoryId(null);
-                  goLevel();
-                }}
-              />
-            </>
-          )}
+          {!isAcornPending && acornToastOpen && (
+            <div className="absolute bottom-[10.2rem] left-1/2 -translate-x-1/2">
+              <AutoDismissToast
+                key={acornToastKey}
+                duration={3000}
+                onClose={() => setAcornToastOpen(false)}
+              >
+                <Balloon variant="main" side="bottom">
+                  ...
+                </Balloon>
+              </AutoDismissToast>
+            </div>
+          )}
+
+          <MyLevelItem
+            acorns={acornCount}
+            isActive={activeTab === 'level'}
+            onClick={() => {
+              closeMenu();
+              setSelectedCategoryId(null);
+              goLevel();
+            }}
+          />
         </footer>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx` around lines 214 -
252, The footer currently wraps both the toast and MyLevelItem in a single
!isAcornPending check which causes layout shift; instead render MyLevelItem
unconditionally (pass acorns={isAcornPending ? acornCount : 0} so it shows a 0
placeholder while loading) and move the conditional so only the toast block uses
!isAcornPending && acornToastOpen (keep acornToastKey, acornCount, isActive
logic and the onClick handler for MyLevelItem intact); in short, remove the
outer !isAcornPending wrapper, always render <MyLevelItem ... />, and only
conditionally render the <AutoDismissToast> using !isAcornPending &&
acornToastOpen.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts`:
- Around line 44-55: handleCreateCategory currently ignores the API response in
the createCategory onSuccess callback; update the onSuccess handler of
createCategory to accept the response, call setNewCategoryName(''), invalidate
the ['dashboardCategories'] query via queryClient.invalidateQueries, call
close(), and then call moveNewCategory(response.data.id) to navigate to the
newly created category (referencing handleCreateCategory, createCategory,
queryClient.invalidateQueries, moveNewCategory, setNewCategoryName, and close).

---

Duplicate comments:
In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Line 206: openEdit(id, name)을 호출하기 전에 newCategoryName 상태를 올바르게 초기화하도록
handleCategoryChange(name)를 호출해 주세요; 구체적으로 Sidebar 컴포넌트의 onEdit handler에서
openEdit(id, name) 호출 직전에 handleCategoryChange(name)를 실행하여 편집 모달이 열릴 때
newCategoryName이 현재 카테고리 이름으로 설정되도록 하고 불필요한 빈 또는 이전 값으로 인한 잘못된 PATCH 요청이 전송되는 것을
방지하세요.
- Line 207: Prevent deletion of the default "안 읽은 정보" category by
short-circuiting the delete action in the Sidebar: update the onDelete handler
(where onDelete={(id, name) => openDelete(id, name)} is passed) to return early
when the category name is "안 읽은 정보" (or matches a known DEFAULT_UNREAD_ID if
available) instead of calling openDelete; additionally, reflect this in the UI
by disabling or hiding the delete control for that category so the action cannot
be triggered.

---

Nitpick comments:
In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts`:
- Around line 57-72: handlePatchCategory currently calls
queryClient.invalidateQueries (which triggers async refetch) and then
immediately calls moveNewCategory(id), which can navigate using stale cache;
make handlePatchCategory async and await the invalidateQueries call (await
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] })) before
calling moveNewCategory(id) so navigation happens after refetch completes; keep
setNewCategoryName('') and close() as needed but ensure moveNewCategory runs
only after the await.

In `@apps/client/src/shared/components/sidebar/Sidebar.tsx`:
- Around line 214-252: The footer currently wraps both the toast and MyLevelItem
in a single !isAcornPending check which causes layout shift; instead render
MyLevelItem unconditionally (pass acorns={isAcornPending ? acornCount : 0} so it
shows a 0 placeholder while loading) and move the conditional so only the toast
block uses !isAcornPending && acornToastOpen (keep acornToastKey, acornCount,
isActive logic and the onClick handler for MyLevelItem intact); in short, remove
the outer !isAcornPending wrapper, always render <MyLevelItem ... />, and only
conditionally render the <AutoDismissToast> using !isAcornPending &&
acornToastOpen.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 588b87e and 24fd40a.

📒 Files selected for processing (2)
  • apps/client/src/shared/components/sidebar/Sidebar.tsx
  • apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts

Comment on lines +44 to +55
const handleCreateCategory = () => {
createCategory(newCategoryName, {
onSuccess: () => {
setNewCategoryName('');
queryClient.invalidateQueries({
queryKey: ['dashboardCategories'],
});
close();
},
onError: () => setToastIsOpen(true),
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "useCategoryActions.ts" -type f

Repository: Pinback-Team/pinback-client

Length of output: 142


🏁 Script executed:

rg -n "handleCreateCategory|handlePatchCategory" --type=ts -B 2 -A 15

Repository: Pinback-Team/pinback-client

Length of output: 7095


🏁 Script executed:

rg -n "createCategory" --type=ts -B 2 -A 5 | head -100

Repository: Pinback-Team/pinback-client

Length of output: 1758


🏁 Script executed:

rg -n "moveNewCategory" --type=ts -B 2 -A 3

Repository: Pinback-Team/pinback-client

Length of output: 1247


🏁 Script executed:

find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "usePostCategory" | head -5

Repository: Pinback-Team/pinback-client

Length of output: 183


🏁 Script executed:

rg -n "usePostCategory" --type=ts -B 3 -A 10

Repository: Pinback-Team/pinback-client

Length of output: 3944


🏁 Script executed:

rg -n "export.*usePostCategory|const usePostCategory" --type=ts -B 2 -A 20

Repository: Pinback-Team/pinback-client

Length of output: 1621


🏁 Script executed:

rg -n "const postCategory|function postCategory|export.*postCategory" --type=ts -B 2 -A 10

Repository: Pinback-Team/pinback-client

Length of output: 933


🏁 Script executed:

rg -n "postCategory" --type=ts -B 5 -A 10 | grep -E "^[^-]*-[0-9]+|postCategory|return|interface|type" | head -60

Repository: Pinback-Team/pinback-client

Length of output: 3217


🏁 Script executed:

cat -n apps/client/src/shared/apis/axios.ts

Repository: Pinback-Team/pinback-client

Length of output: 2978


🏁 Script executed:

rg -n "response\.data|\.data\." apps/client/src/shared/apis/axios.ts | head -20

Repository: Pinback-Team/pinback-client

Length of output: 53


handleCreateCategoryonSuccess 콜백에서 생성된 카테고리 ID를 추출하여 네비게이션이 필요합니다.

handlePatchCategory는 수정 성공 시 moveNewCategory(id)로 해당 카테고리로 이동하지만, handleCreateCategory는 API 응답에서 생성된 카테고리 ID를 받을 수 있음에도 불구하고 네비게이션하지 않습니다.

createCategory 뮤테이션의 onSuccess 콜백이 응답 데이터를 수신하므로, 다음과 같이 수정하세요:

const handleCreateCategory = () => {
  createCategory(newCategoryName, {
    onSuccess: (response) => {
      setNewCategoryName('');
      queryClient.invalidateQueries({
        queryKey: ['dashboardCategories'],
      });
      close();
      moveNewCategory(response.data.id);  // 생성된 카테고리 ID로 네비게이션
    },
    onError: () => setToastIsOpen(true),
  });
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts` around
lines 44 - 55, handleCreateCategory currently ignores the API response in the
createCategory onSuccess callback; update the onSuccess handler of
createCategory to accept the response, call setNewCategoryName(''), invalidate
the ['dashboardCategories'] query via queryClient.invalidateQueries, call
close(), and then call moveNewCategory(response.data.id) to navigate to the
newly created category (referencing handleCreateCategory, createCategory,
queryClient.invalidateQueries, moveNewCategory, setNewCategoryName, and close).

@jjangminii jjangminii merged commit 5ff49c2 into develop Feb 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 툴팁 연결

2 participants