Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughLevel 페이지와 Sidebar에 도토리 관련 오버레이(Toast/Balloon) 및 Lottie 애니메이션을 추가하고, 카테고리 액션을 훅으로 분리했습니다. Balloon 스타일 조정과 JobPin 가이드 포털 컴포넌트도 도입되었습니다. (≤50단어) Changes
Sequence DiagramsequenceDiagram
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 (상태 정리)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
🧹 Nitpick comments (6)
apps/client/src/pages/level/Level.tsx (3)
29-29:isPending분기는 도달 불가능한 코드입니다 (dead code).Line 19에서
isPending이true일 때 이미 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-width나overflow-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-react는framer-motion과react사이에 위치해야 합니다.🔧 알파벳 순서 정렬 제안
"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
📒 Files selected for processing (5)
apps/client/package.jsonapps/client/src/assets/5_chippiface.jsonapps/client/src/pages/level/Level.tsxapps/client/src/shared/components/balloon/Balloon.tsxapps/client/src/shared/components/sidebar/Sidebar.tsx
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
apps/client/src/shared/components/sidebar/Sidebar.tsxapps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts
| setToastIsOpen(false); | ||
| openDelete(id, name); | ||
| }} | ||
| onEdit={(id, name) => openEdit(id, name)} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -a 'Sidebar\.tsx' apps/client/srcRepository: 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/componentsRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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 3Repository: 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/srcRepository: 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.tsxRepository: 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.tsRepository: 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.tsRepository: 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.tsxRepository: 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.
| 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.
| onDelete={(id, name) => openDelete(id, name)} | ||
| onClose={closeMenu} |
There was a problem hiding this comment.
기본 카테고리 ‘안 읽은 정보’는 삭제 금지 처리가 필요합니다.
기본 카테고리는 삭제가 불가능하도록 메뉴/액션 레벨에서 차단해야 합니다.
🛡️ 수정 제안
- 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` 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.
into feat/#264/tooltip-connection
constantly-dev
left a comment
There was a problem hiding this comment.
굿굿~ 카테고리 부분 훅으로 잘 분리해주신 것 같아요~ 로직 분리가 돼서 확실히 깔끔해졌네요 👍
| } from '@shared/apis/queries'; | ||
| import { SidebarTab } from '@shared/hooks/useSidebarNav'; | ||
|
|
||
| interface Params { |
There was a problem hiding this comment.
interface naming 수정해주세요!
| const canCreateMore = categoryCount < MAX_CATEGORIES; | ||
|
|
||
| useEffect(() => { | ||
| if (isPending) return; |
There was a problem hiding this comment.
isPending이 무엇에 대한 것인지 몰라서, 위에서 rename 해주는 것이 좋을 것 같아요!
…ctions의 Params 인터페이스 이름 수정
There was a problem hiding this comment.
♻️ 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이기 때문에 스크롤에 의한 위치 오차는 발생하지 않습니다. 일회성 가이드 툴팁 특성상 실제 영향은 제한적이지만, 완전한 대응이 필요하다면ResizeObserver나windowresize 이벤트 리스너를 추가해 위치를 재계산하는 방식을 고려할 수 있습니다.🤖 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
📒 Files selected for processing (3)
apps/client/src/shared/components/balloon/Balloon.tsxapps/client/src/shared/components/sidebar/JobPinGuidePortal.tsxapps/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
There was a problem hiding this comment.
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에서invalidateQueries와moveNewCategory호출 순서 확인 필요.
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 전체를 감싸면 초기 로드 시 레이아웃 시프트가 발생할 수 있습니다.
isAcornPending이true인 동안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
📒 Files selected for processing (2)
apps/client/src/shared/components/sidebar/Sidebar.tsxapps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts
| const handleCreateCategory = () => { | ||
| createCategory(newCategoryName, { | ||
| onSuccess: () => { | ||
| setNewCategoryName(''); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ['dashboardCategories'], | ||
| }); | ||
| close(); | ||
| }, | ||
| onError: () => setToastIsOpen(true), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "useCategoryActions.ts" -type fRepository: Pinback-Team/pinback-client
Length of output: 142
🏁 Script executed:
rg -n "handleCreateCategory|handlePatchCategory" --type=ts -B 2 -A 15Repository: Pinback-Team/pinback-client
Length of output: 7095
🏁 Script executed:
rg -n "createCategory" --type=ts -B 2 -A 5 | head -100Repository: Pinback-Team/pinback-client
Length of output: 1758
🏁 Script executed:
rg -n "moveNewCategory" --type=ts -B 2 -A 3Repository: Pinback-Team/pinback-client
Length of output: 1247
🏁 Script executed:
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "usePostCategory" | head -5Repository: Pinback-Team/pinback-client
Length of output: 183
🏁 Script executed:
rg -n "usePostCategory" --type=ts -B 3 -A 10Repository: Pinback-Team/pinback-client
Length of output: 3944
🏁 Script executed:
rg -n "export.*usePostCategory|const usePostCategory" --type=ts -B 2 -A 20Repository: Pinback-Team/pinback-client
Length of output: 1621
🏁 Script executed:
rg -n "const postCategory|function postCategory|export.*postCategory" --type=ts -B 2 -A 10Repository: 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 -60Repository: Pinback-Team/pinback-client
Length of output: 3217
🏁 Script executed:
cat -n apps/client/src/shared/apis/axios.tsRepository: Pinback-Team/pinback-client
Length of output: 2978
🏁 Script executed:
rg -n "response\.data|\.data\." apps/client/src/shared/apis/axios.ts | head -20Repository: Pinback-Team/pinback-client
Length of output: 53
handleCreateCategory의 onSuccess 콜백에서 생성된 카테고리 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).
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
초기 로딩 시 말풍선 표시 방지
사이드바에서 초기 페이지 진입 또는 새로고침 시 React Query 데이터 로딩 과정에서 말풍선이 표시되는 문제가 있어 이를 수정했습니다.
처음에는 state로 관리하다가 새로고침시에도 pending 상태일때와 비교하기에 수정하였습니다
📷 Screenshot
Summary by CodeRabbit
릴리스 노트
New Features
Style