feat: 찻장 페이지 미니멀 리디자인 - 언더라인 탭 + accent bar + 비율바#278
Conversation
- 필터 pill 칩 → 언더라인 탭 (border-b-2, aria-pressed) - CellarCard: border box → 좌측 3px accent bar + 텍스트 링크 액션 - 요약 카드 → 인라인 통계 + 1px 차종 비율바 (data-testid) - sort dropdown role="listbox" → role="menu" (ARIA spec 준수) - react-router-dom import 병합, 미사용 아이콘 import 제거 - 테스트: hero/ratio-bar 신규 2건 + menuitem 역할 업데이트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughCellar 페이지 UI를 재구성하고 카드형을 행 기반 스와이프 행(Row)으로 교체, FilterTabBar 컴포넌트 추가, 행 단위 액션(차록/다회/수정/삭제)과 드롭다운 메뉴 도입, 일부 리포트 훅 및 렌더링 데이터(shape)에 teaId가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as CellarRow UI
participant Router as Router/Navigate
participant API as Server API
participant Toast as Toast
rect rgba(240,240,255,0.5)
User->>UI: swipe right / click More
UI->>UI: reveal action rail / open dropdown
end
rect rgba(255,240,240,0.5)
User->>UI: tap "삭제" or select "삭제" menuitem
UI->>API: DELETE /api/cellar/:id
API-->>UI: 200 OK
UI->>Toast: show success
UI->>Router: refresh / navigate (optional)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
- FilterTabBar 공통 컴포넌트 생성 (border-primary 언더라인, -mb-px) - Cellar.tsx: 차종 필터 탭 → FilterTabBar 사용으로 차담과 동일 스타일 - Community.tsx: 카테고리 탭 → FilterTabBar 사용으로 동일 컴포넌트 공유 - 탭 활성 색상 border-foreground → border-primary / text-primary 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/Cellar.tsx`:
- Around line 505-515: The menu items currently use role="menuitem" with
aria-checked which violates ARIA; update the list markup in the SORT_OPTIONS
mapping so each <li> has role="none" and the inner <button> uses
role="menuitemradio" (keep aria-checked={sortKey === opt.key} and the onClick
handler handleSortChange(opt.key)); ensure the surrounding container remains
role="menu" so the radio semantics are correctly exposed to assistive tech.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a777ebdc-7bbd-40ff-92b7-fdcf901ae6bc
📒 Files selected for processing (2)
src/pages/Cellar.tsxsrc/pages/__tests__/Cellar.test.tsx
| role="menu" | ||
| aria-label="정렬 옵션" | ||
| className="absolute right-0 top-full mt-1 z-20 bg-card border border-border rounded-xl shadow-sm py-1 min-w-28 overflow-hidden" | ||
| > | ||
| {SORT_OPTIONS.map((opt) => ( | ||
| <li key={opt.key}> | ||
| <button | ||
| type="button" | ||
| role="option" | ||
| aria-selected={sortKey === opt.key} | ||
| role="menuitem" | ||
| aria-checked={sortKey === opt.key} | ||
| onClick={() => handleSortChange(opt.key)} |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
WAI-ARIA 1.2 기준으로 role="menuitem"에서 aria-checked 사용이 허용되는지, 단일 선택 메뉴에 권장되는 role(menuitemradio) 정의를 확인해줘.
💡 Result:
WAI-ARIA 1.2에서 role="menuitem"은 aria-checked 속성을 허용하지 않습니다. aria-checked는 체크 가능한 상태를 나타내는 속성으로, role="menuitemcheckbox" (true/false/mixed) 또는 role="menuitemradio" (true/false)에만 사용됩니다. role="menuitem"은 일반 메뉴 아이템으로, 체크 상태가 없으므로 aria-checked를 사용하지 않습니다. 단일 선택 메뉴의 경우 role="menuitemradio"를 권장하며, 그룹 내 하나의 아이템만 선택 가능하고 aria-checked="true"로 선택 상태를 표시합니다. 이는 MDN 문서와 WAI-ARIA Practices에서 확인되며, menuitemradio는 radio와 menuitemcheckbox의 서브클래스로 정의됩니다.
Citations:
- 1: https://www.w3.org/TR/wai-aria-1.2/
- 2: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitem_role
- 3: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitemradio_role
- 4: https://aria-roles.info/role/menuitemradio.html
- 5: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menuitemcheckbox_role
- 6: https://www.digitala11y.com/menuitemradio-role/
- 7: https://www.w3.org/TR/2019/NOTE-wai-aria-practices-1.1-20190814/examples/menubar/menubar-2/menubar-2.html
🏁 Script executed:
# Check if the file exists and get its total line count
wc -l src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 83
🏁 Script executed:
# Read the specific section around lines 505-515
sed -n '500,520p' src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 800
🏁 Script executed:
# Get broader context to understand the component structure
sed -n '490,530p' src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 1795
🏁 Script executed:
# Get more context to see the ul element with role="menu"
sed -n '495,520p' src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 1050
🏁 Script executed:
# Search for the role="menu" to confirm its presence
rg -n 'role="menu"' src/pages/Cellar.tsx -A 5 -B 5Repository: FLYLIKEB/ChaLog
Length of output: 603
🏁 Script executed:
# Check what's at lines 505-515 as stated in the review comment
sed -n '505,515p' src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 316
🏁 Script executed:
# Get the full context of the menu implementation with broader scope
sed -n '480,525p' src/pages/Cellar.tsxRepository: FLYLIKEB/ChaLog
Length of output: 1950
정렬 메뉴의 role/상태 속성을 ARIA 규격에 맞게 수정해주세요.
현재 role="menuitem"에는 aria-checked 속성을 사용할 수 없습니다. WAI-ARIA 1.2 기준에 따르면 aria-checked는 role="menuitemradio" 또는 role="menuitemcheckbox"에만 사용 가능합니다. 단일 선택 메뉴이므로 role="menuitemradio"로 변경하고, <li> 요소에 role="none"을 추가하여 접근성 트리에서 리스트 의미를 제거해야 합니다.
🔧 제안 수정안
<ul
role="menu"
aria-label="정렬 옵션"
className="absolute right-0 top-full mt-1 z-20 bg-card border border-border rounded-xl shadow-sm py-1 min-w-28 overflow-hidden"
>
{SORT_OPTIONS.map((opt) => (
- <li key={opt.key}>
+ <li key={opt.key} role="none">
<button
type="button"
- role="menuitem"
+ role="menuitemradio"
aria-checked={sortKey === opt.key}
onClick={() => handleSortChange(opt.key)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| role="menu" | |
| aria-label="정렬 옵션" | |
| className="absolute right-0 top-full mt-1 z-20 bg-card border border-border rounded-xl shadow-sm py-1 min-w-28 overflow-hidden" | |
| > | |
| {SORT_OPTIONS.map((opt) => ( | |
| <li key={opt.key}> | |
| <button | |
| type="button" | |
| role="option" | |
| aria-selected={sortKey === opt.key} | |
| role="menuitem" | |
| aria-checked={sortKey === opt.key} | |
| onClick={() => handleSortChange(opt.key)} | |
| role="menu" | |
| aria-label="정렬 옵션" | |
| className="absolute right-0 top-full mt-1 z-20 bg-card border border-border rounded-xl shadow-sm py-1 min-w-28 overflow-hidden" | |
| > | |
| {SORT_OPTIONS.map((opt) => ( | |
| <li key={opt.key} role="none"> | |
| <button | |
| type="button" | |
| role="menuitemradio" | |
| aria-checked={sortKey === opt.key} | |
| onClick={() => handleSortChange(opt.key)} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Cellar.tsx` around lines 505 - 515, The menu items currently use
role="menuitem" with aria-checked which violates ARIA; update the list markup in
the SORT_OPTIONS mapping so each <li> has role="none" and the inner <button>
uses role="menuitemradio" (keep aria-checked={sortKey === opt.key} and the
onClick handler handleSortChange(opt.key)); ensure the surrounding container
remains role="menu" so the radio semantics are correctly exposed to assistive
tech.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MySQL DESC 정렬 시 NULL이 자동으로 마지막에 오므로 별도 IS NULL 정렬 불필요. TypeORM이 'note.overallRating IS NULL'을 컬럼 경로로 파싱 시도하여 databaseName 오류 발생. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CellarCard → CellarRow로 교체 (화면당 6-8개 표시) - 좌측 스와이프로 차록/다회/수정/삭제 액션 슬라이드 인 - 데스크톱 ⋮ DropdownMenu 폴백 유지 - 차종 색 dot + 이름 + 연도/판매처/메모 compact 표시 - 필터 sticky 영역: 전체 pill + 차종 탭 + 정렬 (차담 패턴 통일) - 구입처(판매처) 항상 표시 (판매처 미상 fallback) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/pages/Cellar.tsx (1)
492-502:⚠️ Potential issue | 🟠 Major정렬 메뉴의 ARIA role/state 조합을 수정해야 합니다.
Line 500-501에서
role="menuitem"에aria-checked를 함께 쓰고 있어 규격상 부적합합니다. 단일 선택 메뉴라면menuitemradio를 사용하고,li에는role="none"을 두는 구조가 맞습니다.🔧 제안 수정안
<ul role="menu" aria-label="정렬 옵션" className="absolute right-0 top-full mt-1 z-20 bg-card border border-border rounded-xl shadow-sm py-1 min-w-28 overflow-hidden" > {SORT_OPTIONS.map((opt) => ( - <li key={opt.key}> + <li key={opt.key} role="none"> <button type="button" - role="menuitem" + role="menuitemradio" aria-checked={sortKey === opt.key} onClick={() => handleSortChange(opt.key)}#!/bin/bash # 목적: Cellar 정렬 메뉴의 ARIA role/state 조합 검증 # 기대결과: # 1) role="menuitem" + aria-checked 조합이 없어야 함 # 2) role="menuitemradio" 사용, li는 role="none" 사용 rg -n -C2 'role="menuitem"|role="menuitemradio"|aria-checked|<li[^>]*role=' src/pages/Cellar.tsx🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Cellar.tsx` around lines 492 - 502, The menu items use role="menuitem" with aria-checked which is invalid; update the mapped items so each <li> has role="none" and the inner <button> uses role="menuitemradio" while keeping aria-checked={sortKey === opt.key} and the existing onClick handler (handleSortChange). Locate the SORT_OPTIONS.map usage and change the roles accordingly so the list remains role="menu" and the buttons become accessible menuitemradio controls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/hooks/useReportData.ts`:
- Around line 66-77: The aggregation groups by teaName which merges different
teas with the same name; change the logic in useReportData to key by teaId (use
a string key like `${n.teaId}`) when building teaMap from filteredNotes, store
both teaId and teaName on each entry, increment counts per teaId, then compute
topTeas by sorting entries and mapping to objects that include the correct teaId
and teaName so navigation goes to the correct detailed page; update references
to teaMap, filteredNotes, topTeas, teaId, and teaName accordingly.
---
Duplicate comments:
In `@src/pages/Cellar.tsx`:
- Around line 492-502: The menu items use role="menuitem" with aria-checked
which is invalid; update the mapped items so each <li> has role="none" and the
inner <button> uses role="menuitemradio" while keeping aria-checked={sortKey ===
opt.key} and the existing onClick handler (handleSortChange). Locate the
SORT_OPTIONS.map usage and change the roles accordingly so the list remains
role="menu" and the buttons become accessible menuitemradio controls.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 339f9867-d438-4232-af77-fbc7b8c3464c
📒 Files selected for processing (5)
src/components/FilterTabBar.tsxsrc/components/profile/ReportContent.tsxsrc/hooks/useReportData.tssrc/pages/Cellar.tsxsrc/pages/Community.tsx
| const teaMap: Record<string, { count: number; teaId?: number }> = {}; | ||
| filteredNotes.forEach((n) => { | ||
| if (n.teaName) teaMap[n.teaName] = (teaMap[n.teaName] || 0) + 1; | ||
| if (n.teaName) { | ||
| if (!teaMap[n.teaName]) teaMap[n.teaName] = { count: 0, teaId: n.teaId }; | ||
| teaMap[n.teaName].count++; | ||
| } | ||
| }); | ||
| const topTeas = Object.entries(teaMap) | ||
| .sort((a, b) => b[1] - a[1]) | ||
| .sort((a, b) => b[1].count - a[1].count) | ||
| .slice(0, 5) | ||
| .map(([teaName, count]) => ({ teaName, count })); | ||
| .map(([teaName, { count, teaId }]) => ({ teaName, count, teaId })); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
집계 키(teaName)와 네비게이션 식별자(teaId)를 맞춰주세요.
Line 69-70처럼 이름 기준으로 합산하면, 동일한 이름의 다른 차(다른 teaId)가 하나로 뭉쳐져 잘못된 상세 페이지로 이동할 수 있습니다. teaId 우선 키로 집계하는 쪽이 안전합니다.
🔧 제안 수정안
- const teaMap: Record<string, { count: number; teaId?: number }> = {};
+ const teaMap: Record<string, { teaName: string; count: number; teaId?: number }> = {};
filteredNotes.forEach((n) => {
if (n.teaName) {
- if (!teaMap[n.teaName]) teaMap[n.teaName] = { count: 0, teaId: n.teaId };
- teaMap[n.teaName].count++;
+ const key = n.teaId != null ? `id:${n.teaId}` : `name:${n.teaName}`;
+ if (!teaMap[key]) teaMap[key] = { teaName: n.teaName, count: 0, teaId: n.teaId };
+ teaMap[key].count++;
}
});
- const topTeas = Object.entries(teaMap)
- .sort((a, b) => b[1].count - a[1].count)
+ const topTeas = Object.values(teaMap)
+ .sort((a, b) => b.count - a.count)
.slice(0, 5)
- .map(([teaName, { count, teaId }]) => ({ teaName, count, teaId }));
+ .map(({ teaName, count, teaId }) => ({ teaName, count, teaId }));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const teaMap: Record<string, { count: number; teaId?: number }> = {}; | |
| filteredNotes.forEach((n) => { | |
| if (n.teaName) teaMap[n.teaName] = (teaMap[n.teaName] || 0) + 1; | |
| if (n.teaName) { | |
| if (!teaMap[n.teaName]) teaMap[n.teaName] = { count: 0, teaId: n.teaId }; | |
| teaMap[n.teaName].count++; | |
| } | |
| }); | |
| const topTeas = Object.entries(teaMap) | |
| .sort((a, b) => b[1] - a[1]) | |
| .sort((a, b) => b[1].count - a[1].count) | |
| .slice(0, 5) | |
| .map(([teaName, count]) => ({ teaName, count })); | |
| .map(([teaName, { count, teaId }]) => ({ teaName, count, teaId })); | |
| const teaMap: Record<string, { teaName: string; count: number; teaId?: number }> = {}; | |
| filteredNotes.forEach((n) => { | |
| if (n.teaName) { | |
| const key = n.teaId != null ? `id:${n.teaId}` : `name:${n.teaName}`; | |
| if (!teaMap[key]) teaMap[key] = { teaName: n.teaName, count: 0, teaId: n.teaId }; | |
| teaMap[key].count++; | |
| } | |
| }); | |
| const topTeas = Object.values(teaMap) | |
| .sort((a, b) => b.count - a.count) | |
| .slice(0, 5) | |
| .map(({ teaName, count, teaId }) => ({ teaName, count, teaId })); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useReportData.ts` around lines 66 - 77, The aggregation groups by
teaName which merges different teas with the same name; change the logic in
useReportData to key by teaId (use a string key like `${n.teaId}`) when building
teaMap from filteredNotes, store both teaId and teaName on each entry, increment
counts per teaId, then compute topTeas by sorting entries and mapping to objects
that include the correct teaId and teaName so navigation goes to the correct
detailed page; update references to teaMap, filteredNotes, topTeas, teaId, and
teaName accordingly.
Summary
role="listbox"→role="menu"(ARIA spec 준수, HIGH 이슈 해결)Test plan
npm run build— ✓npm run test:run— 16/16 PASS (신규 테스트 2건 포함)🤖 Generated with Claude Code
Summary by CodeRabbit
스타일 및 UI 개선
신규 기능
접근성
테스트
동작 개선