feat: header, option 공통 컴포넌트#11
Conversation
* feat: 아이콘 공통 컴포넌트 초기 세팅 (#3) * feat: 모바일 레이아웃 세팅 (#3) * feat: 폰트 추가 (#3) * feat: 디자인 시스템 컬러 및 타이포그래피 토큰 추가 (#3) - 그레이 스케일 컬러 토큰 추가 (gray-0 ~ gray-700) - shadcn/ui 호환 시맨틱 컬러 토큰 추가 - 타이포그래피 유틸리티 클래스 추가 (head, subhead, body, caption, title) - 앱 기본 배경색 설정 및 base 스타일 보완 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: UI 마크업 가이드 문서 추가 (#3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: 아이콘 가이드 추가 및 문서 정리 (#3) - icon-guide.md 분리 신규 추가 - ui-markup-guide.md 아이콘 섹션 제거 및 링크로 대체 - CLAUDE.md 참고 문서 목록 업데이트 - tsconfig.json baseUrl 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: 앱 배경색 gray-50 토큰으로 변경 (#3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: shadcn/ui 호환 토큰 누락 추가 (#3) - destructive-foreground, accent, accent-foreground 토큰 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRadix UI 의존성을 추가하고 이를 기반으로 드롭다운 메뉴, 옵션 메뉴, 헤더 공통 컴포넌트를 새롭게 구현합니다. 아이콘 컴포넌트 8개를 추가하고, 설정 및 문서를 업데이트합니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 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 docstrings
🧪 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.
<Header
title="캘린더"
onBack={~}
headerOptions={
<OptionMenu
items={
optionMenuVariant === "default"
? [
{ icon: <IcEdit />, label: "수정하기", onClick: onEdit },
{ icon: <IcShare />, label: "공유하기", onClick: onShare },
{ icon: <IcTrash />, label: "삭제", onClick: onDelete, isDestructive: true },
]
: [
{ icon: <IcReport />, label: "신고하기", onClick: onReport },
{ icon: <IcShare />, label: "공유하기", onClick: onShare },
]
}
/>
}요런식으로 헤더 옵션 메뉴를 props로 전달할 수 있도록 하고, Header는 레이아웃을 그리는 역할만 하는 컴포넌트면 더 확장성 높은 컴포넌트가 될 것 같아요!
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (7)
src/shared/ui/icons/IcBack.tsx (1)
9-9:strokeWidth정밀도 정리(선택).
"1.75008"은 Figma export의 잔여 부동소수점 값으로 보입니다. 시각적 차이가 없으므로"1.75"로 정리하면 가독성이 좋아집니다.♻️ 제안 수정
- strokeWidth="1.75008" + strokeWidth="1.75"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/icons/IcBack.tsx` at line 9, IcBack 컴포넌트의 SVG 속성에 남아있는 과도한 부동소수점 값은 가독성을 해치므로, 아이콘 JSX에서 strokeWidth="1.75008"을 찾아 간단히 strokeWidth="1.75"로 정리해 주세요; 관련 식별자는 IcBack (파일 내 SVG element의 strokeWidth 속성)입니다.docs/naming-convention.md (1)
14-17: 아이콘 파일 예외 규칙을 명시해 주세요.
shared/ui/컴포넌트는kebab-case.tsx로 적었지만, 실제 본 PR의shared/ui/icons/아래 파일들(IcBack.tsx,IcOption.tsx,IcReport.tsx등)은PascalCase.tsx입니다.ui-markup-guide.md의 예시와 일관되게 "단,shared/ui/icons/아이콘 컴포넌트는PascalCase.tsx(IcHome.tsx등)"라는 예외를 한 줄 추가해 두면 신규 기여자가 헷갈리지 않습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/naming-convention.md` around lines 14 - 17, The naming guideline lacks an explicit exception for icon components under shared/ui/icons; update docs/naming-convention.md to add one sentence clarifying that while shared/ui components use kebab-case (e.g., button.tsx, option-menu.tsx), icon components in shared/ui/icons should use PascalCase like IcBack.tsx, IcOption.tsx, IcReport.tsx (examples: IcHome.tsx), so new contributors know to treat icons as PascalCase.tsx.package.json (1)
18-25:radix-ui통합 패키지와@radix-ui/react-slot이 공존합니다.shadcn 최신 가이드에 맞춰 통합 패키지(
radix-ui)를 추가한 것은 적절합니다. 다만 기존@radix-ui/react-slot이 그대로 남아 있어, 이후 컴포넌트마다from "radix-ui"/from "@radix-ui/react-*"두 가지 임포트 스타일이 섞일 수 있습니다. 팀 컨벤션으로 한 가지 스타일을 정해두는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 18 - 25, The package list contains both the unified radix-ui package and the legacy scoped package `@radix-ui/react-slot` which will lead to mixed import styles; decide on one convention and update package.json accordingly by removing the redundant package (e.g., delete "@radix-ui/react-slot": "^1.2.4" if you adopt "radix-ui") and then refactor imports across the codebase to use the chosen symbol (either imports from "radix-ui" or from "@radix-ui/react-*" but not both), ensuring modules that previously imported from "@radix-ui/react-slot" are changed to the corresponding "radix-ui" import.biome.json (1)
100-103:src/shared/ui/*.tsx글롭이 비재귀적입니다.현재 글롭은
src/shared/ui/직속 파일만 매칭하며,src/shared/ui/icons/하위 컴포넌트는 포함하지 않습니다. 다행히 현재 모든 컴포넌트(button.tsx, dropdown-menu.tsx, option-menu.tsx, BaseIcon.tsx, IcBack.tsx 등)는 이미: React.ReactElement명시적 반환 타입을 선언하고 있어useExplicitType규칙 위반이 없습니다.다만 향후 shadcn 기반 컴포넌트가 하위 폴더에 추가될 때 의도치 않게 린트 경고가 발생할 수 있으므로, 글롭을 재귀 패턴으로 변경하면 더욱 안정적입니다.
♻️ 선택적 개선: 재귀 글롭으로 통일
{ - "includes": ["src/shared/ui/*.tsx"], + "includes": ["src/shared/ui/**/*.tsx"], "linter": { "rules": { "nursery": { "useExplicitType": "off" } } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@biome.json` around lines 100 - 103, The include glob "src/shared/ui/*.tsx" is non-recursive and will miss files in subfolders like src/shared/ui/icons, so update the includes entry in biome.json to use a recursive glob (e.g., change "src/shared/ui/*.tsx" to "src/shared/ui/**/*.tsx") so all nested TSX components are linted consistently and avoid future unexpected useExplicitType warnings; keep the existing linter rule override ("nursery": { "useExplicitType": "off" }) intact.src/shared/ui/dropdown-menu.tsx (1)
9-210: Arrow Function + 명시적 반환 타입 컨벤션 권장코딩 가이드라인은 모든 TypeScript 함수에 대해 화살표 함수와 명시적 반환 타입을 사용하도록 규정하고 있습니다. 본 파일은 shadcn/ui 보일러플레이트라 일괄 적용이 부담스러울 수 있지만, 프로젝트 일관성을 위해 점진적 마이그레이션을 고려해보세요.
예시:
-function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { - return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; -} +const DropdownMenu = ( + props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>, +): React.ReactElement => <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;As per coding guidelines: "Use Arrow Functions with explicit return type annotations for all functions in TypeScript".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/dropdown-menu.tsx` around lines 9 - 210, Convert the listed named functions (e.g., DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent) to arrow functions with explicit return type annotations (e.g., : React.ReactElement or JSX.Element) while preserving their prop types (React.ComponentProps<typeof DropdownMenuPrimitive.*> and the custom prop intersections like in DropdownMenuItem and DropdownMenuLabel) and all internal behavior/JSX (including data-slot attributes, className handling via cn, default props such as sideOffset or variant, and children forwarding); update each function signature only (no logic changes) so the file conforms to the "arrow functions with explicit return type" convention.src/shared/ui/option-menu.tsx (1)
30-33: 백드롭 onClick 부재 — UX 의도 확인 요청
isOpen일 때 dim 오버레이가 깔리지만 클릭/터치에 대한 핸들러가 없어 닫기는 Radix의 outside-click 처리에 의존합니다. 일반적으로 dim을 탭하면 즉시 닫히는 UX를 기대하는 사용자가 많으므로,onClick={() => setIsOpen(false)}을 부여하거나pointer-events-none을 명시해 의도를 분명히 하는 편이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/option-menu.tsx` around lines 30 - 33, The backdrop div rendered when isOpen is true should explicitly handle taps/clicks instead of silently relying on Radix: add an onClick handler to the overlay div that calls setIsOpen(false) so tapping the dim closes the menu (reference: the isOpen state, setIsOpen setter and the surrounding DropdownMenu component), or if the intent is to make the backdrop non-interactive, mark it with pointer-events-none to make that behavior explicit; update the overlay div accordingly to reflect the chosen UX.src/widgets/header/ui/CheckButton.tsx (1)
4-14: 컴포넌트 위치 검토 (선택)
CheckButton은 헤더write변형 외에서도 충분히 재사용 가능한 일반 토글 버튼처럼 보입니다. 코딩 가이드라인상 "재사용 UI는shared/ui/, 멀티 기능 UI 조합은widgets/{widgetName}/"이므로, 추후 다른 페이지/위젯에서도 쓰인다면src/shared/ui/check-button/로 옮기는 것을 고려해보세요. 헤더 전용으로만 쓰일 계획이라면 현재 위치도 무방합니다.As per coding guidelines,
Place reusable UI components in shared/ui//Place multi-feature UI combinations in widgets/{widgetName}/.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/widgets/header/ui/CheckButton.tsx` around lines 4 - 14, CheckButton appears to be a generic reusable toggle rather than header-specific; move the CheckButton component (interface CheckButtonProps and the exported CheckButton) from its current widgets/header/ui location into a new shared UI path (e.g., src/shared/ui/check-button/) and update imports where it's used, or if it truly will remain header-only, document that decision in a comment near the CheckButton export; ensure the exported name CheckButton and its props interface remain unchanged so callers need only update import paths.
🤖 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/app/globals.css`:
- Line 28: The destructive token `--color-destructive: `#ff3b30`` fails WCAG AA
contrast on white; add a darker foreground token (e.g.
`--color-destructive-foreground` or `--color-destructive-text` with a hex like
`#d70015` or `#c91a09`) alongside the existing `--color-destructive` in
src/app/globals.css, leaving `--color-destructive` for background use and using
the new token wherever destructive is rendered as text or small icons; update
the components that render destructive text (search for `isDestructive` in
option-menu.tsx and the destructive variant in dropdown-menu.tsx) to reference
the new foreground token.
- Around line 59-65: The CSS currently applies cursor: pointer to
[role="button"] regardless of aria-disabled, causing disabled custom buttons to
show a pointer; update the selectors so aria-disabled="true" is treated like
:disabled — either narrow the pointer rule to exclude [aria-disabled="true"]
(e.g. button:not(:disabled), [role="button"]:not([aria-disabled="true"])) or add
a rule that sets cursor: not-allowed for [role="button"][aria-disabled="true"],
adjusting the existing selectors (button:not(:disabled), [role="button"],
button:disabled) and the disabled cursor rule to include the aria-disabled
variant.
In `@src/shared/ui/option-menu.tsx`:
- Around line 48-68: The current items.map uses key={item.label} which can
produce duplicate React keys; switch to a stable unique identifier (e.g.,
item.id) and fall back to index only if no stable id exists when rendering in
DropdownMenuItem, and update any tests/consumers accordingly; also preserve
caller-provided classes when calling cloneElement(item.icon) by merging the
original icon's className (item.icon.props?.className) into the cn(...) call so
the caller's styling isn't overwritten.
- Around line 35-42: Replace the wrapper button's class "outline-none" with
"outline-hidden" and add focus-visible outline utility classes (e.g.,
focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary) on
the trigger so keyboard focus is visible and consistent with other dropdown
items (refer to DropdownMenuTrigger and IcOption). Also change rendering so that
when a custom trigger prop is provided you do not wrap it in an extra <button>
or unconditionally apply aria-label="더보기" — instead branch: if trigger exists
render the trigger as-is (letting callers supply accessible labeling) else
render the internal button with the aria-label and the new outline/focus-visible
classes to avoid nested buttons and duplicate labels.
In `@src/widgets/header/ui/CheckButton.tsx`:
- Around line 19-27: The check icon currently renders with a fixed class
"text-gray-0" inside the CheckButton component regardless of isChecked, causing
poor contrast when isChecked is false; update the rendering logic in
CheckButton.tsx so that IcCheck uses a conditional class (e.g., "text-gray-0"
when isChecked is true, and "text-gray-400" when false) or hide the IcCheck
entirely when unchecked; modify the JSX around the IcCheck usage and the
isChecked prop handling to apply the conditional class or conditional render.
- Around line 1-2: CheckButton is an interactive component that accepts an
onClick prop and can be imported into Server Components via
src/widgets/header/index.ts, which causes runtime serialization errors; fix it
by adding the "use client" directive as the very first line of
src/widgets/header/ui/CheckButton.tsx so the component (CheckButton) is treated
as a Client Component and can accept function props like onClick safely, keeping
behavior consistent with Header, NavBar, and dropdown-menu.
In `@src/widgets/header/ui/Header.tsx`:
- Around line 30-36: The centered title span (the JSX block rendering {title} in
Header.tsx) is absolutely positioned and can overlap the left back button or the
{right} slot when the title is long; update the title span styling to constrain
its width and apply truncation (e.g., set a max-width based on available header
space or use calc(100% - [leftAreaWidth] - [rightAreaWidth]), and add CSS for
overflow-hidden, white-space-nowrap and text-overflow:ellipsis) so long Korean
titles are clipped with an ellipsis and cannot visually collide with the left
button or the right slot.
---
Nitpick comments:
In `@biome.json`:
- Around line 100-103: The include glob "src/shared/ui/*.tsx" is non-recursive
and will miss files in subfolders like src/shared/ui/icons, so update the
includes entry in biome.json to use a recursive glob (e.g., change
"src/shared/ui/*.tsx" to "src/shared/ui/**/*.tsx") so all nested TSX components
are linted consistently and avoid future unexpected useExplicitType warnings;
keep the existing linter rule override ("nursery": { "useExplicitType": "off" })
intact.
In `@docs/naming-convention.md`:
- Around line 14-17: The naming guideline lacks an explicit exception for icon
components under shared/ui/icons; update docs/naming-convention.md to add one
sentence clarifying that while shared/ui components use kebab-case (e.g.,
button.tsx, option-menu.tsx), icon components in shared/ui/icons should use
PascalCase like IcBack.tsx, IcOption.tsx, IcReport.tsx (examples: IcHome.tsx),
so new contributors know to treat icons as PascalCase.tsx.
In `@package.json`:
- Around line 18-25: The package list contains both the unified radix-ui package
and the legacy scoped package `@radix-ui/react-slot` which will lead to mixed
import styles; decide on one convention and update package.json accordingly by
removing the redundant package (e.g., delete "@radix-ui/react-slot": "^1.2.4" if
you adopt "radix-ui") and then refactor imports across the codebase to use the
chosen symbol (either imports from "radix-ui" or from "@radix-ui/react-*" but
not both), ensuring modules that previously imported from "@radix-ui/react-slot"
are changed to the corresponding "radix-ui" import.
In `@src/shared/ui/dropdown-menu.tsx`:
- Around line 9-210: Convert the listed named functions (e.g., DropdownMenu,
DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger,
DropdownMenuSubContent) to arrow functions with explicit return type annotations
(e.g., : React.ReactElement or JSX.Element) while preserving their prop types
(React.ComponentProps<typeof DropdownMenuPrimitive.*> and the custom prop
intersections like in DropdownMenuItem and DropdownMenuLabel) and all internal
behavior/JSX (including data-slot attributes, className handling via cn, default
props such as sideOffset or variant, and children forwarding); update each
function signature only (no logic changes) so the file conforms to the "arrow
functions with explicit return type" convention.
In `@src/shared/ui/icons/IcBack.tsx`:
- Line 9: IcBack 컴포넌트의 SVG 속성에 남아있는 과도한 부동소수점 값은 가독성을 해치므로, 아이콘 JSX에서
strokeWidth="1.75008"을 찾아 간단히 strokeWidth="1.75"로 정리해 주세요; 관련 식별자는 IcBack (파일 내
SVG element의 strokeWidth 속성)입니다.
In `@src/shared/ui/option-menu.tsx`:
- Around line 30-33: The backdrop div rendered when isOpen is true should
explicitly handle taps/clicks instead of silently relying on Radix: add an
onClick handler to the overlay div that calls setIsOpen(false) so tapping the
dim closes the menu (reference: the isOpen state, setIsOpen setter and the
surrounding DropdownMenu component), or if the intent is to make the backdrop
non-interactive, mark it with pointer-events-none to make that behavior
explicit; update the overlay div accordingly to reflect the chosen UX.
In `@src/widgets/header/ui/CheckButton.tsx`:
- Around line 4-14: CheckButton appears to be a generic reusable toggle rather
than header-specific; move the CheckButton component (interface CheckButtonProps
and the exported CheckButton) from its current widgets/header/ui location into a
new shared UI path (e.g., src/shared/ui/check-button/) and update imports where
it's used, or if it truly will remain header-only, document that decision in a
comment near the CheckButton export; ensure the exported name CheckButton and
its props interface remain unchanged so callers need only update import paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d229556b-a627-4aaa-a2f8-26c486b0c1b4
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!**/pnpm-lock.yaml
📒 Files selected for processing (19)
biome.jsondocs/naming-convention.mddocs/ui-markup-guide.mdpackage.jsonsrc/app/globals.csssrc/shared/ui/dropdown-menu.tsxsrc/shared/ui/icons/IcBack.tsxsrc/shared/ui/icons/IcCheck.tsxsrc/shared/ui/icons/IcEdit.tsxsrc/shared/ui/icons/IcOption.tsxsrc/shared/ui/icons/IcReport.tsxsrc/shared/ui/icons/IcSetting.tsxsrc/shared/ui/icons/IcShare.tsxsrc/shared/ui/icons/IcTrash.tsxsrc/shared/ui/icons/index.tssrc/shared/ui/option-menu.tsxsrc/widgets/header/index.tssrc/widgets/header/ui/CheckButton.tsxsrc/widgets/header/ui/Header.tsx
| --color-secondary: #f7f7f7; | ||
| --color-secondary-foreground: #090909; | ||
| --color-destructive: #e53e3e; | ||
| --color-destructive: #ff3b30; |
There was a problem hiding this comment.
#ff3b30의 흰 배경 대비비를 확인해 주세요.
#ff3b30는 흰 배경(#ffffff) 기준 명도 대비가 약 3.76:1로 WCAG AA(일반 텍스트 4.5:1)을 충족하지 못합니다. text-destructive가 본문 텍스트나 작은 아이콘 위에 사용될 경우(예: option-menu.tsx의 isDestructive 라벨, dropdown-menu.tsx의 destructive variant) 가독성이 떨어질 수 있으니, 텍스트로 사용하는 자리에는 조금 더 어두운 톤(예: #d70015 / #c91a09)을 별도 토큰(--color-destructive-foreground 또는 --color-destructive-text)으로 두는 것을 검토해 주세요. 배경색으로만 쓰일 때는 문제없습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/globals.css` at line 28, The destructive token `--color-destructive:
`#ff3b30`` fails WCAG AA contrast on white; add a darker foreground token (e.g.
`--color-destructive-foreground` or `--color-destructive-text` with a hex like
`#d70015` or `#c91a09`) alongside the existing `--color-destructive` in
src/app/globals.css, leaving `--color-destructive` for background use and using
the new token wherever destructive is rendered as text or small icons; update
the components that render destructive text (search for `isDestructive` in
option-menu.tsx and the destructive variant in dropdown-menu.tsx) to reference
the new foreground token.
| button:not(:disabled), | ||
| [role="button"] { | ||
| cursor: pointer; | ||
| } | ||
| button:disabled { | ||
| cursor: not-allowed; | ||
| } |
There was a problem hiding this comment.
aria-disabled 케이스가 누락되어 있습니다.
[role="button"]에 aria-disabled="true"가 붙은 커스텀 버튼은 비활성 상태임에도 cursor: pointer가 적용됩니다. 셀렉터를 보강해 두면 일관됩니다.
🛡️ 제안 수정
- button:not(:disabled),
- [role="button"] {
+ button:not(:disabled),
+ [role="button"]:not([aria-disabled="true"]) {
cursor: pointer;
}
- button:disabled {
+ button:disabled,
+ [role="button"][aria-disabled="true"] {
cursor: not-allowed;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/globals.css` around lines 59 - 65, The CSS currently applies cursor:
pointer to [role="button"] regardless of aria-disabled, causing disabled custom
buttons to show a pointer; update the selectors so aria-disabled="true" is
treated like :disabled — either narrow the pointer rule to exclude
[aria-disabled="true"] (e.g. button:not(:disabled),
[role="button"]:not([aria-disabled="true"])) or add a rule that sets cursor:
not-allowed for [role="button"][aria-disabled="true"], adjusting the existing
selectors (button:not(:disabled), [role="button"], button:disabled) and the
disabled cursor rule to include the aria-disabled variant.
| <button | ||
| type="button" | ||
| className="flex items-center justify-center text-gray-700 outline-none" | ||
| aria-label="더보기" | ||
| > | ||
| {trigger ?? <IcOption size={24} className="size-6" />} | ||
| </button> | ||
| </DropdownMenuTrigger> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Tailwind CSS v4 outline-none vs outline-hidden behavior change
💡 Result:
In Tailwind CSS v4, the behavior around “hiding outlines” changed because the meaning of the old outline-none name was clarified and a new utility was introduced. 1) What happened to outline-none - In Tailwind v3, outline-none did not set outline-style: none; instead it used a transparent/hidden outline so it could still be visible in forced-colors / high-contrast mode. - In Tailwind v4, outline-none was redefined to actually remove the outline by setting outline-style: none. 2) What outline-hidden does in v4 - outline-hidden is the successor to the old outline-none semantics: it hides the default browser outline on normal rendering, but still preserves a visible outline in forced-colors mode for accessibility. 3) Migration / recommended mapping - The Tailwind v4 upgrade guidance says to replace usages of outline-none (from v3) with outline-hidden (in v4) to preserve the old forced-colors-friendly behavior. Practical summary - If you previously used focus:outline-none in v3 specifically to “hide but keep forced-colors accessibility”, switch to focus:outline-hidden in v4. - If you truly want to remove the outline entirely (including in forced-colors mode), use focus:outline-none in v4 and then provide your own focus styling. Sources (authoritative): Tailwind v4 upgrade guide and the outline-style docs, plus the Tailwind CSS PR that explains the rationale and implementation.
Citations:
- 1: Replace
outline-nonewithoutline-hidden, add newoutline-nonetailwindlabs/tailwindcss#14926 - 2: https://tailwindcss.com/docs/outline-style
- 3: https://tailwindcss.com/docs/upgrade-guide
- 4:
outline-hiddenonly applies transparent outline whenforced-colorsis active? tailwindlabs/tailwindcss#16926
🏁 Script executed:
find src -name "option-menu.tsx" -o -name "dropdown-menu.tsx" | head -20Repository: depromeet/18th-team1-FE
Length of output: 129
🏁 Script executed:
wc -l src/shared/ui/option-menu.tsxRepository: depromeet/18th-team1-FE
Length of output: 100
🏁 Script executed:
cat -n src/shared/ui/option-menu.tsxRepository: depromeet/18th-team1-FE
Length of output: 2855
🏁 Script executed:
cat -n src/shared/ui/dropdown-menu.tsx | head -50Repository: depromeet/18th-team1-FE
Length of output: 2452
🏁 Script executed:
rg "outline" src/shared/ui/dropdown-menu.tsxRepository: depromeet/18th-team1-FE
Length of output: 1696
🏁 Script executed:
rg "key=" src/shared/ui/option-menu.tsxRepository: depromeet/18th-team1-FE
Length of output: 98
🏁 Script executed:
git log --oneline -n 5 -- src/shared/ui/option-menu.tsxRepository: depromeet/18th-team1-FE
Length of output: 145
🏁 Script executed:
rg "items.*map|OptionMenuItem" src -t tsx -A 3 | head -40Repository: depromeet/18th-team1-FE
Length of output: 95
🏁 Script executed:
rg "item\.label" src -t tsx | head -20Repository: depromeet/18th-team1-FE
Length of output: 95
🏁 Script executed:
rg "OptionMenu" src --max-count=5Repository: depromeet/18th-team1-FE
Length of output: 365
🏁 Script executed:
rg "OptionMenuItem" src -A 2 | head -30Repository: depromeet/18th-team1-FE
Length of output: 414
outline-none → outline-hidden 권장 + 일관성 및 접근성 개선
Tailwind v4부터 outline-none은 outline-style: none을 적용하여 outline을 완전히 제거합니다. 같은 PR의 dropdown-menu.tsx의 DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuSubTrigger에서는 이미 outline-hidden을 사용하고 있어 일관성을 맞출 필요가 있습니다. 또한 outline-hidden은 강제 색상 모드(forced-colors)에서도 outline을 표시하여 접근성이 우수하므로, 클래스를 outline-hidden으로 교체하고 focus-visible: 변형을 추가하여 키보드 사용자를 위한 포커스 표시를 제공하는 것을 권장합니다.
추가로, 커스텀 trigger가 전달될 때도 래퍼 <button>의 aria-label="더보기"가 항상 적용됩니다. 사용자가 이미 라벨링된 요소를 trigger로 넘기는 경우 라벨이 이중/충돌될 수 있고, trigger가 자체 <button>이라면 <button> 중첩으로 인한 잘못된 마크업이 됩니다. trigger 존재 여부에 따라 래퍼 마크업과 라벨을 분기 처리하는 방안을 검토해주세요.
♻️ 제안 diff
- <DropdownMenuTrigger asChild>
- <button
- type="button"
- className="flex items-center justify-center text-gray-700 outline-none"
- aria-label="더보기"
- >
- {trigger ?? <IcOption size={24} className="size-6" />}
- </button>
- </DropdownMenuTrigger>
+ <DropdownMenuTrigger asChild>
+ {trigger ?? (
+ <button
+ type="button"
+ className="flex items-center justify-center text-gray-700 outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
+ aria-label="더보기"
+ >
+ <IcOption size={24} className="size-6" />
+ </button>
+ )}
+ </DropdownMenuTrigger>📝 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.
| <button | |
| type="button" | |
| className="flex items-center justify-center text-gray-700 outline-none" | |
| aria-label="더보기" | |
| > | |
| {trigger ?? <IcOption size={24} className="size-6" />} | |
| </button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuTrigger asChild> | |
| {trigger ?? ( | |
| <button | |
| type="button" | |
| className="flex items-center justify-center text-gray-700 outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| aria-label="더보기" | |
| > | |
| <IcOption size={24} className="size-6" /> | |
| </button> | |
| )} | |
| </DropdownMenuTrigger> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/ui/option-menu.tsx` around lines 35 - 42, Replace the wrapper
button's class "outline-none" with "outline-hidden" and add focus-visible
outline utility classes (e.g., focus-visible:outline focus-visible:outline-2
focus-visible:outline-primary) on the trigger so keyboard focus is visible and
consistent with other dropdown items (refer to DropdownMenuTrigger and
IcOption). Also change rendering so that when a custom trigger prop is provided
you do not wrap it in an extra <button> or unconditionally apply
aria-label="더보기" — instead branch: if trigger exists render the trigger as-is
(letting callers supply accessible labeling) else render the internal button
with the aria-label and the new outline/focus-visible classes to avoid nested
buttons and duplicate labels.
| {items.map((item) => ( | ||
| <DropdownMenuItem | ||
| key={item.label} | ||
| className={cn( | ||
| "cursor-pointer gap-3.5 p-0 focus:bg-transparent", | ||
| item.isDestructive | ||
| ? "text-destructive focus:text-destructive" | ||
| : "text-gray-700 focus:text-gray-700", | ||
| )} | ||
| onClick={item.onClick} | ||
| > | ||
| {cloneElement(item.icon, { | ||
| size: 24, | ||
| className: cn( | ||
| "size-6 shrink-0 -translate-y-[0.5px]", | ||
| item.isDestructive ? "text-destructive" : "text-gray-700", | ||
| ), | ||
| })} | ||
| <span className="subhead4">{item.label}</span> | ||
| </DropdownMenuItem> | ||
| ))} |
There was a problem hiding this comment.
key로 label 사용 시 중복 키 위험 및 cloneElement가 caller className을 덮어씀
두 가지를 함께 검토해주세요.
key={item.label}— 같은 라벨을 가진 항목이 둘 이상 있으면 React 키 중복 경고/렌더 이슈가 발생합니다. 인덱스 또는 안정적인 ID를 함께 사용하는 편이 안전합니다.cloneElement(item.icon, { className: cn(...) })— 호출 측이item.icon에 className을 부여했다면 그 값이 그대로 사라집니다. 의도가 사용자 className을 보존하는 것이라면 원본 className을cn에 합쳐야 합니다.
♻️ 제안 diff
- {items.map((item) => (
+ {items.map((item, index) => (
<DropdownMenuItem
- key={item.label}
+ key={`${item.label}-${index}`}
className={cn(
"cursor-pointer gap-3.5 p-0 focus:bg-transparent",
item.isDestructive
? "text-destructive focus:text-destructive"
: "text-gray-700 focus:text-gray-700",
)}
onClick={item.onClick}
>
{cloneElement(item.icon, {
size: 24,
className: cn(
"size-6 shrink-0 -translate-y-[0.5px]",
item.isDestructive ? "text-destructive" : "text-gray-700",
+ item.icon.props.className,
),
})}
<span className="subhead4">{item.label}</span>
</DropdownMenuItem>
))}📝 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.
| {items.map((item) => ( | |
| <DropdownMenuItem | |
| key={item.label} | |
| className={cn( | |
| "cursor-pointer gap-3.5 p-0 focus:bg-transparent", | |
| item.isDestructive | |
| ? "text-destructive focus:text-destructive" | |
| : "text-gray-700 focus:text-gray-700", | |
| )} | |
| onClick={item.onClick} | |
| > | |
| {cloneElement(item.icon, { | |
| size: 24, | |
| className: cn( | |
| "size-6 shrink-0 -translate-y-[0.5px]", | |
| item.isDestructive ? "text-destructive" : "text-gray-700", | |
| ), | |
| })} | |
| <span className="subhead4">{item.label}</span> | |
| </DropdownMenuItem> | |
| ))} | |
| {items.map((item, index) => ( | |
| <DropdownMenuItem | |
| key={`${item.label}-${index}`} | |
| className={cn( | |
| "cursor-pointer gap-3.5 p-0 focus:bg-transparent", | |
| item.isDestructive | |
| ? "text-destructive focus:text-destructive" | |
| : "text-gray-700 focus:text-gray-700", | |
| )} | |
| onClick={item.onClick} | |
| > | |
| {cloneElement(item.icon, { | |
| size: 24, | |
| className: cn( | |
| "size-6 shrink-0 -translate-y-[0.5px]", | |
| item.isDestructive ? "text-destructive" : "text-gray-700", | |
| item.icon.props.className, | |
| ), | |
| })} | |
| <span className="subhead4">{item.label}</span> | |
| </DropdownMenuItem> | |
| ))} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/ui/option-menu.tsx` around lines 48 - 68, The current items.map
uses key={item.label} which can produce duplicate React keys; switch to a stable
unique identifier (e.g., item.id) and fall back to index only if no stable id
exists when rendering in DropdownMenuItem, and update any tests/consumers
accordingly; also preserve caller-provided classes when calling
cloneElement(item.icon) by merging the original icon's className
(item.icon.props?.className) into the cn(...) call so the caller's styling isn't
overwritten.
| import { cn } from "@/shared/lib/utils"; | ||
| import { IcCheck } from "@/shared/ui/icons"; |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
#!/bin/bash
# CheckButton의 사용처를 찾고, 호출부가 Server Component인지 Client Component인지 확인
rg -nP --type=tsx -C3 '\bCheckButton\b'
# widgets 내부 다른 파일들의 "use client" 사용 패턴 확인
fd -e tsx . src/widgets src/shared/ui --exec sh -c 'echo "=== {} ==="; head -1 {}'Repository: depromeet/18th-team1-FE
Repository: depromeet/18th-team1-FE
Exit code: 0
stdout:
=== src/widgets/header/ui/Header.tsx ===
"use client";
=== src/shared/ui/dropdown-menu.tsx ===
"use client";
=== src/shared/ui/option-menu.tsx ===
"use client";
=== src/shared/ui/button.tsx ===
import { Slot } from "@radix-ui/react-slot";
=== src/shared/ui/icons/IcOption.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/widgets/nav-bar/ui/NavBar.tsx ===
"use client";
=== src/shared/ui/icons/IcHome.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcTrash.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcReport.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcShare.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcCheck.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcEdit.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/widgets/header/ui/CheckButton.tsx ===
import { cn } from "@/shared/lib/utils";
=== src/shared/ui/icons/IcSetting.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcCalendar.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/IcBack.tsx ===
import { BaseIcon } from "./BaseIcon";
=== src/shared/ui/icons/BaseIcon.tsx ===
import type { IconProps } from "./icon.types";
stderr:
unrecognized file type: tsx
Script executed:
# Find all imports of CheckButton
rg -n "import.*CheckButton" --glob="*.tsx" --glob="*.ts"
# Check if Header.tsx actually uses CheckButton
rg -n "CheckButton" src/widgets/header/ui/Header.tsx -C 5
# View the actual CheckButton component to understand its props
head -30 src/widgets/header/ui/CheckButton.tsxRepository: depromeet/18th-team1-FE
Repository: depromeet/18th-team1-FE
Exit code: 0
stdout:
import { cn } from "@/shared/lib/utils";
import { IcCheck } from "@/shared/ui/icons";
interface CheckButtonProps {
isChecked: boolean;
onClick: () => void;
className?: string;
}
export const CheckButton = ({
isChecked,
onClick,
className,
}: CheckButtonProps): React.ReactElement => {
return (
<button
type="button"
onClick={onClick}
className={cn(
"flex size-7 items-center justify-center rounded-full transition-colors",
isChecked ? "bg-gray-700" : "bg-gray-200",
className,
)}
aria-pressed={isChecked}
aria-label="선택 확인"
>
<IcCheck size={24} className="text-gray-0" />
</button>
);
};
Script executed:
# Search for CheckButton imports across the entire codebase
rg "CheckButton" --glob="*.tsx" --glob="*.ts" -n
# Also check if there are any direct imports
rg "from.*CheckButton\|import.*CheckButton" -nRepository: depromeet/18th-team1-FE
Repository: depromeet/18th-team1-FE
Exit code: 1
stdout:
src/widgets/header/ui/CheckButton.tsx:4:interface CheckButtonProps {
src/widgets/header/ui/CheckButton.tsx:10:export const CheckButton = ({
src/widgets/header/ui/CheckButton.tsx:14:}: CheckButtonProps): React.ReactElement => {
src/widgets/header/index.ts:1:export { CheckButton } from "./ui/CheckButton";
Script executed:
# Check what Header.tsx exports and uses
head -50 src/widgets/header/ui/Header.tsx
# Check the index.ts of header to see full exports
cat src/widgets/header/index.ts
# Search for CheckButton usage in Header.tsx
rg -n "CheckButton" src/widgets/header/ui/Header.tsx -B 2 -A 2Repository: depromeet/18th-team1-FE
Repository: depromeet/18th-team1-FE
Exit code: 1
stdout:
"use client";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { cn } from "@/shared/lib/utils";
import { IcBack } from "@/shared/ui/icons";
interface HeaderProps {
title?: string;
onBack?: () => void;
right?: ReactNode;
className?: string;
}
export const Header = ({ title, onBack, right, className }: HeaderProps): React.ReactElement => {
const router = useRouter();
const handleBack =
onBack ??
((): void => {
router.back();
});
return (
<header className={cn("relative flex h-15 w-full items-center justify-between", className)}>
<button type="button" onClick={handleBack} className="text-gray-700" aria-label="뒤로가기">
<IcBack size={24} />
</button>
{title && (
<span className="subhead2 absolute left-1/2 -translate-x-1/2 cursor-default select-none text-gray-700">
{title}
</span>
)}
{right}
</header>
);
};
export { CheckButton } from "./ui/CheckButton";
export { Header } from "./ui/Header";
CheckButton에 "use client" 지시문 추가 필요
CheckButton은 onClick 함수를 props로 받는 인터랙티브 컴포넌트이며, src/widgets/header/index.ts를 통해 별도로 내보내집니다. Next.js App Router에서 Server Component에서 직접 임포트될 경우 함수 prop을 직렬화할 수 없어 런타임 에러가 발생합니다. 같은 경로의 Header.tsx와 다른 위젯 컴포넌트들(NavBar.tsx, dropdown-menu.tsx 등)이 모두 "use client"를 선언하므로, 일관성과 재사용성을 위해 파일 상단에 "use client"를 추가하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/header/ui/CheckButton.tsx` around lines 1 - 2, CheckButton is an
interactive component that accepts an onClick prop and can be imported into
Server Components via src/widgets/header/index.ts, which causes runtime
serialization errors; fix it by adding the "use client" directive as the very
first line of src/widgets/header/ui/CheckButton.tsx so the component
(CheckButton) is treated as a Client Component and can accept function props
like onClick safely, keeping behavior consistent with Header, NavBar, and
dropdown-menu.
| className={cn( | ||
| "flex size-7 items-center justify-center rounded-full transition-colors", | ||
| isChecked ? "bg-gray-700" : "bg-gray-200", | ||
| className, | ||
| )} | ||
| aria-pressed={isChecked} | ||
| aria-label="선택 확인" | ||
| > | ||
| <IcCheck size={24} className="text-gray-0" /> |
There was a problem hiding this comment.
Unchecked 상태의 시각적 대비 확인 권장
isChecked === false일 때 배경은 bg-gray-200(연회색)인데 내부 체크 아이콘은 text-gray-0(흰색)으로 항상 동일하게 렌더됩니다. 토큰 정의에 따라 다르지만 흰색 체크가 연회색 배경 위에서 거의 보이지 않을 수 있어 사용자가 토글 상태를 인지하기 어려울 수 있습니다. 디자인 의도(빈 상태 표현)이면 그대로 두어도 되지만, 의도가 아니라면 unchecked 시 아이콘 색을 한 단계 어둡게(예: text-gray-400) 조정하거나 아이콘 자체를 숨기는 방법을 검토해주세요.
💡 제안 diff (예시)
- <IcCheck size={24} className="text-gray-0" />
+ <IcCheck size={24} className={cn(isChecked ? "text-gray-0" : "text-gray-400")} />📝 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.
| className={cn( | |
| "flex size-7 items-center justify-center rounded-full transition-colors", | |
| isChecked ? "bg-gray-700" : "bg-gray-200", | |
| className, | |
| )} | |
| aria-pressed={isChecked} | |
| aria-label="선택 확인" | |
| > | |
| <IcCheck size={24} className="text-gray-0" /> | |
| className={cn( | |
| "flex size-7 items-center justify-center rounded-full transition-colors", | |
| isChecked ? "bg-gray-700" : "bg-gray-200", | |
| className, | |
| )} | |
| aria-pressed={isChecked} | |
| aria-label="선택 확인" | |
| > | |
| <IcCheck size={24} className={cn(isChecked ? "text-gray-0" : "text-gray-400")} /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/header/ui/CheckButton.tsx` around lines 19 - 27, The check icon
currently renders with a fixed class "text-gray-0" inside the CheckButton
component regardless of isChecked, causing poor contrast when isChecked is
false; update the rendering logic in CheckButton.tsx so that IcCheck uses a
conditional class (e.g., "text-gray-0" when isChecked is true, and
"text-gray-400" when false) or hide the IcCheck entirely when unchecked; modify
the JSX around the IcCheck usage and the isChecked prop handling to apply the
conditional class or conditional render.
| {title && ( | ||
| <span className="subhead2 absolute left-1/2 -translate-x-1/2 cursor-default select-none text-gray-700"> | ||
| {title} | ||
| </span> | ||
| )} | ||
|
|
||
| {right} |
There was a problem hiding this comment.
제목이 길 경우 좌/우 영역과 겹칠 가능성
title은 absolute left-1/2 -translate-x-1/2로 화면 정중앙에 고정되어 있어, 한국어 긴 제목이나 right 슬롯에 여러 버튼(예: 공유 + 설정)이 들어올 때 좌측 back 버튼 또는 우측 슬롯과 겹쳐 보일 수 있습니다. 폭 제한과 ellipsis 처리를 추가하는 것을 권장드려요.
💡 제안 diff
{title && (
- <span className="subhead2 absolute left-1/2 -translate-x-1/2 cursor-default select-none text-gray-700">
+ <span className="subhead2 absolute left-1/2 max-w-[60%] -translate-x-1/2 cursor-default select-none truncate text-center text-gray-700">
{title}
</span>
)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/header/ui/Header.tsx` around lines 30 - 36, The centered title
span (the JSX block rendering {title} in Header.tsx) is absolutely positioned
and can overlap the left back button or the {right} slot when the title is long;
update the title span styling to constrain its width and apply truncation (e.g.,
set a max-width based on available header space or use calc(100% -
[leftAreaWidth] - [rightAreaWidth]), and add CSS for overflow-hidden,
white-space-nowrap and text-overflow:ellipsis) so long Korean titles are clipped
with an ellipsis and cannot visually collide with the left button or the right
slot.
🔗 연결된 이슈
📝 작업 요약
공통 헤더 위젯과 OptionMenu 컴포넌트를 구현하고, 관련 아이콘을 추가했습니다. OptionMenu는 items 배열 prop 기반으로 설계해 어떤 아이템 조합도 자유롭게 사용할 수 있습니다.
🔍 주요 변경사항
📸 스크린샷 (선택사항)
📌 PR 중요도
💬 기타 사항