Skip to content

refactor: 권한별 Route Group 폴더 구조 분리#5

Merged
saechimdaeki merged 15 commits into
mainfrom
feature/route-group-permission
Apr 4, 2026
Merged

refactor: 권한별 Route Group 폴더 구조 분리#5
saechimdaeki merged 15 commits into
mainfrom
feature/route-group-permission

Conversation

@jinokiim

@jinokiim jinokiim commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Next.js App Router의 Route Group을 활용해 권한 수준별 폴더 분리
    • (public) — 인증 불필요: /, /login, /auth/continue, /auth/callback, /pending
    • (member) — 승인된 멤버: /dashboard, /timeline
    • (admin) — 관리자(admin, super_admin): /candidates/new, /profiles/[id], /profiles/[id]/edit
    • (super-admin) — 최고 관리자: /admin
  • URL 변경 없음, 로직 변경 없음 — 순수 폴더 이동만
  • page-patterns.md rules 문서를 새 구조에 맞게 업데이트

Test plan

  • npm run typecheck 통과 확인
  • 각 라우트 접속 시 기존과 동일하게 동작하는지 확인
  • 권한 없는 사용자 리다이렉트 정상 동작 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated shadcn/ui component library for improved UI consistency
    • Added Super Admin control panel for managing user roles and memberships
    • Reorganized app navigation with permission-based route groups
  • Style

    • Refreshed design system with updated color tokens and typography standards
    • Implemented theme-aware styling with CSS variables
  • Refactor

    • Restructured pages and components to align with new routing hierarchy
    • Modernized existing components to use new design system primitives
  • Documentation

    • Added color system, typography, and UI token guidelines
    • Documented routing structure and component patterns

jinokiim and others added 15 commits April 4, 2026 23:24
shadcn@latest init으로 Tailwind v4 호환 base-nova 스타일 초기화.
clsx + tailwind-merge 기반 cn() 유틸 및 components.json 설정 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
npx shadcn@latest add --all로 55개 UI 컴포넌트 설치.
Button, Input, Badge, Card, Dialog, Select 등 기본 컴포넌트 포함.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn 기본 neutral 색상을 프로젝트 톤(#b46d55 rose, #c7a56a gold)으로 교체.
기존 셸 변수(--bg, --panel 등)를 --shell- 접두사로 리네이밍하여 충돌 방지.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- status-badge → shadcn Badge 기반
- form-submit-button → shadcn Button 기반
- auth-forms → shadcn Input/Label/Button/Card 적용
- match-detail-modal → shadcn Dialog 적용 (createPortal 제거)
- candidate-card → shadcn Card/Badge 적용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
account-panel, back-nav-button, gender-toggle-field, home-account-shell,
lazy-home-account-shell에서 #ead8cf, #b46d59 등 하드코딩 hex를
--border, --primary 등 CSS 변수와 shadcn Button/Card로 통일.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ui-components.md에 shadcn/ui 사용 규칙 반영 (render prop, CSS 변수 체계).
셸 변수 --shell- 접두사 가이드 추가. 기획서 파일 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UI 구현 시 shadcn 컴포넌트 우선 사용 원칙 명시.
리뷰 시 shadcn 사용 여부, hex 하드코딩, cn() 사용 등 확인 항목 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 수동 button 탭 전환 → Tabs/TabsList/TabsTrigger/TabsContent
- 메시지/안내 div → Alert/AlertDescription
- useState 제거 (Tabs 자체 상태 관리)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn/ui 마이그레이션 시 의도치 않게 테라코타(#b46d55)로 변경된
primary, ring, sidebar-primary 색상을 원래 로즈 핑크로 되돌림

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Button base에 cursor-pointer 추가
- default variant hover: [a]:hover → hover로 수정 (일반 버튼에도 적용)
- 오버레이 닫기 버튼에 cursor-pointer 추가
- 커스텀 button에 cursor-pointer, focus-visible 링 추가
- template literal className → cn() 변환

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn 시맨틱 토큰, Shell 토큰, Sidebar/Chart 토큰,
컬러 팔레트 요약, 사용 규칙, 변경 이력 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pretendard 패키지 설치, next/font/local로 로드
- globals.css에 h1~h4 태그 오버라이딩 (크기/행간/자간/굵기)
- Geist 제거, --font-body CSS 변수로 Pretendard 연결

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- docs/typography.md: 헤딩/본문 스케일, font-weight, 색상 조합, 말줄임, 링크, 금지사항
- docs/ui-tokens.md: spacing, border-radius, shadow, z-index, transition, 아이콘 규격
- .claude/rules/ui-components.md: 타이포그래피/UI 토큰 섹션 추가, 리뷰 체크리스트 확장 (16항목)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(public), (member), (admin), (super-admin) 4개 그룹으로 페이지 분류.
로직 변경 없이 폴더 이동만 수행. URL 변경 없음.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
auth/callback, dashboard/loading, profiles/[id]/loading을 각 그룹으로 이동.
page-patterns.md 라우팅 구조를 Route Group 기준으로 갱신.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
project-cupid Ready Ready Preview, Comment Apr 4, 2026 4:08pm

@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a major UI system overhaul: introducing shadcn/ui component library with 60+ styled components, migrating routes to permission-based groups ((public), (member), (admin), (super-admin)), establishing centralized design tokens and typography rules, refactoring existing components to use the new system, and adding comprehensive design documentation.

Changes

Cohort / File(s) Summary
Planning & Architecture Docs
.claude/plans/route-group-permission.md, .claude/plans/shadcn-ui-migration.md, .claude/rules/page-patterns.md, .claude/rules/ui-components.md
New planning documents outline route group separation by permission level and phased shadcn/ui migration strategy; rules updated with new shadcn/ui priority, shell/theme token naming, typography constraints, and design-token usage guidelines.
Design System Documentation
docs/color-system.md, docs/typography.md, docs/ui-tokens.md
New design docs establish color system (4 token axes: shadcn semantic, shell/app-frame, sidebar, chart), typography scale (heading tags + body utilities), and UI token rules (spacing, radius, shadow, z-index, transitions, icons).
New UI Component Library
components/ui/accordion.tsx, components/ui/alert-dialog.tsx, components/ui/alert.tsx, components/ui/aspect-ratio.tsx, components/ui/avatar.tsx, components/ui/badge.tsx, components/ui/breadcrumb.tsx, components/ui/button-group.tsx, components/ui/button.tsx, components/ui/calendar.tsx, components/ui/card.tsx, components/ui/carousel.tsx, components/ui/chart.tsx, components/ui/checkbox.tsx, components/ui/collapsible.tsx, components/ui/combobox.tsx, components/ui/command.tsx, components/ui/context-menu.tsx, components/ui/dialog.tsx, components/ui/direction.tsx, components/ui/drawer.tsx, components/ui/dropdown-menu.tsx, components/ui/empty.tsx, components/ui/field.tsx, components/ui/hover-card.tsx, components/ui/input-group.tsx, components/ui/input-otp.tsx, components/ui/input.tsx, components/ui/item.tsx, components/ui/kbd.tsx, components/ui/label.tsx, components/ui/menubar.tsx, components/ui/native-select.tsx, components/ui/navigation-menu.tsx, components/ui/pagination.tsx, components/ui/popover.tsx, components/ui/progress.tsx, components/ui/radio-group.tsx, components/ui/resizable.tsx, components/ui/scroll-area.tsx, components/ui/select.tsx, components/ui/separator.tsx, components/ui/sheet.tsx, components/ui/sidebar.tsx, components/ui/skeleton.tsx, components/ui/slider.tsx, components/ui/sonner.tsx, components/ui/spinner.tsx, components/ui/switch.tsx, components/ui/table.tsx, components/ui/tabs.tsx, components/ui/textarea.tsx, components/ui/toggle-group.tsx, components/ui/toggle.tsx, components/ui/tooltip.tsx
60+ new shadcn/ui component wrappers built on Base UI and supporting libraries (embla-carousel, cmdk, sonner, recharts, etc.), providing styled, accessible primitives with consistent data-slot attributes, Tailwind theming, and variant support via class-variance-authority.
Route Restructuring
app/(admin)/candidates/new/page.tsx, app/(super-admin)/admin/page.tsx, app/(public)/page.tsx
New route group pages: candidate form under (admin) group, super-admin dashboard under (super-admin) group, landing page moved to (public) and refactored with Button/Card components; role checks enforced via requireMembershipRole().
Removed Old Routes
app/admin/page.tsx, app/candidates/new/page.tsx
Old flat-structure pages removed and replaced by route-group equivalents (284 lines and 159 lines respectively deleted).
Existing Component Refactoring
components/account-panel.tsx, components/auth-forms.tsx, components/back-nav-button.tsx, components/candidate-card.tsx, components/dashboard-flow-board.tsx, components/dashboard-logout-button.tsx, components/form-submit-button.tsx, components/gender-toggle-field.tsx, components/home-account-shell.tsx, components/lazy-home-account-shell.tsx, components/manager-dashboard.tsx, components/match-detail-modal.tsx, components/match-history-list-modal.tsx, components/operator-desk-controls.tsx, components/pending-invite-view.tsx, components/photo-upload-field.tsx, components/profile-interactive-gallery.tsx, components/profile-match-kanban.tsx, components/profile-past-match-records.tsx, components/splash-intro.tsx, components/status-badge.tsx
Native HTML elements and custom styling replaced with shadcn/ui components (Button, Card, Badge, Dialog, etc.); hardcoded colors migrated to CSS variable tokens (text-foreground, bg-card, border-border, etc.); custom modals replaced with Dialog; cn() utility used for class composition.
Global Styling & Theme
app/globals.css, app/layout.tsx
CSS variables reorganized into shell namespace (--shell-*) and new shadcn theme tokens (--background, --foreground, --card, --primary, etc.); heading styles (h1–h4) added; Tailwind @theme integration; local Pretendard font configured.
Utilities & Configuration
lib/cn.ts, lib/utils.ts, hooks/use-mobile.ts, components.json, package.json
New cn() utility for class merging via clsx + tailwind-merge; useIsMobile() hook for responsive logic; shadcn components.json config added; 19 new dependencies added (Base UI, class-variance-authority, lucide-react, recharts, date-fns, embla-carousel, etc.).

Sequence Diagram(s)

The changes do not introduce new runtime interaction flows or significantly alter existing control flow in a way that would benefit from sequence visualization. Route restructuring is configuration-based, UI components are presentational primitives, and refactoring is largely mechanical (replacing elements with styled equivalents). Conditions for diagram generation are not met.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Rationale: This is a substantial, multi-faceted refactoring affecting 100+ files across distinct domains (60+ new UI components, route restructuring, design token system, existing component updates). While the UI component additions follow repetitive patterns (reducing individual review effort per component), the scope is very large and includes logic-dense areas (Dialog/Sidebar state management, Chart integration, responsive behaviors). Additionally, the design token system, CSS variable restructuring, and cross-cutting updates across existing components require careful verification for consistency and unintended side effects.

Possibly related PRs

  • saechimdaeki/project-cupid#1: Directly related through shared .claude/rules/ documentation updates and architectural/design system alignment work.

Suggested reviewers

  • saechimdaeki

🐰 Hop, hop! New components bloom in fields so wide,
Tokens dance in harmony, colors unified—
Routes grouped by role, permissions crystalline,
A design system gleams, oh how divine!
From chaos springs order, from fragments, a whole.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/route-group-permission

@jinokiim jinokiim changed the base branch from feature/shadcn-ui-migration to main April 4, 2026 15:26
@jinokiim jinokiim force-pushed the feature/route-group-permission branch from d6dda34 to e28fb20 Compare April 4, 2026 16:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (15)
docs/color-system.md-108-137 (1)

108-137: ⚠️ Potential issue | 🟡 Minor

Specify a language for the fenced block (markdownlint MD040).

At Line 108, add a language (e.g. text) to keep docs lint-clean.

✏️ Proposed fix
-```
+```text
 브랜드 코어
 ├── 로즈 핑크      `#f43f5e`  (primary, ring, sidebar-primary)
 ...
 └── 레드           `#ef4444`  (destructive, 위험)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/color-system.md around lines 108 - 137, The fenced code block in
docs/color-system.md lacks a language specifier (markdownlint MD040); update the
opening triple-backtick for the color palette block (the block showing "브랜드 코어",
"배경 계열 (라이트)", etc.) to include a language token such as text (e.g., change totext) so the linter recognizes the block language and the doc stays
lint-clean.


</details>

</blockquote></details>
<details>
<summary>docs/typography.md-23-27 (1)</summary><blockquote>

`23-27`: _⚠️ Potential issue_ | _🟡 Minor_

**Add a language tag to fenced code blocks for lint compliance.**

Use an explicit language (e.g., `tsx` or `text`) to satisfy markdownlint MD040.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/typography.md around lines 23 - 27, The fenced code block containing
the example HTML/JSX snippet (

매칭 대시보드

and

소개 이력

) is missing an explicit language tag; update the opening fence to include a language (e.g., ```tsx or ```text) so the block complies with markdownlint MD040 and lints correctly. ````
docs/typography.md-56-58 (1)

56-58: ⚠️ Potential issue | 🟡 Minor

Heading override guidance is internally contradictory.

Line [56]–Line [58] says heading size override is allowed, but Line [175]–Line [180] marks that exact pattern as forbidden. Please unify to one rule to avoid ambiguous implementation.

Also applies to: 175-180

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

In `@docs/typography.md` around lines 56 - 58, The documentation currently
contradicts itself: the snippet under the "Tailwind로 크기 덮어쓰기도 가능 (예외적 상황)"
heading that shows <h2 className="text-2xl">고정 크기 제목</h2> allows overriding
heading sizes, while a later section (the one that marks that same pattern as
forbidden) forbids it; pick one policy and make both sections consistent by
either removing the permissive example and updating the "예외적 상황" text to explain
why overrides are disallowed, or by updating the forbidden section to permit
controlled Tailwind overrides and describing constraints; update the text
surrounding the <h2 className="text-2xl"> example and the later forbidden rule
so they convey the same unified rule and rationale.
.claude/rules/page-patterns.md-31-53 (1)

31-53: ⚠️ Potential issue | 🟡 Minor

Specify a language for the fenced tree block (MD040).

Please change the unlabeled code fence to something like ```text for markdownlint compliance.

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

In @.claude/rules/page-patterns.md around lines 31 - 53, The fenced tree block
in page-patterns.md is unlabeled which breaks MD040; update the opening code
fence to include a language/token (e.g., change the backticks before the tree to
include "text" so it reads ```text) to make the block explicitly labeled and
satisfy markdownlint; locate the unnamed fenced block that contains the app/
tree and replace its delimiter accordingly.
.claude/rules/page-patterns.md-35-40 (1)

35-40: ⚠️ Potential issue | 🟡 Minor

Public route group example is missing /auth/callback.

The route tree under (public) does not include /auth/callback, which is part of the stated target structure for this refactor. Please add it so the rules match the implemented routing model.

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

In @.claude/rules/page-patterns.md around lines 35 - 40, The (public) route
group listing is missing the /auth/callback route—add an entry for
auth/callback/page.tsx under the (public) tree so the documented route structure
matches the implemented routing model; update the list that currently shows
page.tsx, login/page.tsx, auth/continue/page.tsx, and pending/page.tsx to
include auth/callback/page.tsx alongside those entries.
components/ui/kbd.tsx-16-24 (1)

16-24: ⚠️ Potential issue | 🟡 Minor

Type mismatch: KbdGroup accepts div props but renders a kbd element.

The component is typed with React.ComponentProps<"div"> but renders a <kbd> element. This mismatch could lead to type errors or unexpected behavior if div-specific attributes are passed.

Suggested fix
-function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
+function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
   return (
     <kbd
       data-slot="kbd-group"
       className={cn("inline-flex items-center gap-1", className)}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/kbd.tsx` around lines 16 - 24, KbdGroup is typed as
React.ComponentProps<"div"> but renders a <kbd>, causing a props/type mismatch;
change the prop type to React.ComponentProps<"kbd"> (or
React.ComponentPropsWithoutRef<"kbd">) and update the function signature for
KbdGroup to use that type so only valid kbd attributes are accepted while
preserving className and ...props forwarding.
components/ui/slider.tsx-16-24 (1)

16-24: ⚠️ Potential issue | 🟡 Minor

Mismatch between value prop and rendered thumbs count.

The component passes value and defaultValue directly to SliderPrimitive.Root (lines 30-31), but computes _values independently for rendering the thumbs (line 47). The fallback logic in _values creates a mismatch:

  • If value={50} (single number) is passed, SliderPrimitive.Root receives value={50}
  • But _values becomes [0, 100], rendering 2 thumbs instead of 1
  • The underlying component state and rendered UI are out of sync

Apply the suggested fix to handle single-value cases explicitly:

Suggested improvement
 const _values = React.useMemo(
   () =>
     Array.isArray(value)
       ? value
       : Array.isArray(defaultValue)
         ? defaultValue
+        : typeof value === "number"
+          ? [value]
+        : typeof defaultValue === "number"
+          ? [defaultValue]
         : [min, max],
   [value, defaultValue, min, max]
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/slider.tsx` around lines 16 - 24, The bug is that _values
computes a fallback array [min, max] and can render two thumbs while
SliderPrimitive.Root receives the raw value/defaultValue (a single number),
causing UI/state mismatch; fix it by normalizing incoming value and defaultValue
to arrays consistently and using that normalized array everywhere: update the
logic around _values (and the props passed to SliderPrimitive.Root) to treat a
single-number value or defaultValue as a single-element array (e.g., if typeof
value === 'number' => [value]) and only fall back to [min, max] when neither
value nor defaultValue is provided, ensuring the rendered thumbs count matches
the value shape; adjust references to _values, value, defaultValue, and
SliderPrimitive.Root so they use the same normalized array form.
components/ui/carousel.tsx-96-105 (1)

96-105: ⚠️ Potential issue | 🟡 Minor

Missing cleanup for reInit event listener.

The effect subscribes to both reInit and select events but only cleans up the select listener. This could cause memory leaks or stale callbacks if the component remounts.

🐛 Proposed fix
   React.useEffect(() => {
     if (!api) return
     onSelect(api)
     api.on("reInit", onSelect)
     api.on("select", onSelect)

     return () => {
+      api?.off("reInit", onSelect)
       api?.off("select", onSelect)
     }
   }, [api, onSelect])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/carousel.tsx` around lines 96 - 105, The effect in
React.useEffect attaches both "reInit" and "select" listeners on api using
onSelect but only removes the "select" handler; update the cleanup to also
detach the "reInit" listener by calling api.off("reInit", onSelect) (or api?.off
for safety) alongside the existing api.off("select", onSelect) so both
subscriptions are removed when the effect unmounts or dependencies change.
components/ui/toggle-group.tsx-43-47 (1)

43-47: ⚠️ Potential issue | 🟡 Minor

Fix the gap utility syntax and CSS variable units.

The class gap-[--spacing(var(--gap))] has two issues:

  1. --spacing() is a Tailwind v4 function that expects numeric literals (e.g., --spacing(4)), not CSS variables. Using --spacing(var(--gap)) is invalid.
  2. The --gap CSS variable is set to a unitless number, but CSS gap requires units.

Consider this fix:

Suggested change
- style={{ "--gap": spacing } as React.CSSProperties}
+ style={{ "--gap": `${spacing}px` } as React.CSSProperties}
  className={cn(
-   "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
+   "group/toggle-group flex w-fit flex-row items-center gap-[var(--gap)] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",

Or use Tailwind v4's native CSS variable syntax: gap-(--gap) (also requires units in the CSS variable).

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

In `@components/ui/toggle-group.tsx` around lines 43 - 47, Replace the invalid
Tailwind gap utility and make the CSS variable include units: change the class
string token "gap-[--spacing(var(--gap))]" to "gap-[var(--gap)]" (or Tailwind v4
syntax "gap-(--gap)") and update the inline style that sets "--gap" so it
contains units (e.g., change style={{ "--gap": spacing }} to style={{ "--gap":
typeof spacing === "number" ? `${spacing}px` : spacing }}), keeping the
React.CSSProperties cast; this ensures the gap uses a valid CSS variable value
with units and a valid Tailwind utility.
components/ui/empty.tsx-71-80 (1)

71-80: ⚠️ Potential issue | 🟡 Minor

Render EmptyDescription as <p> to match its declared prop type and semantics.

React.ComponentProps<"p"> is declared, but a div is rendered.

💡 Suggested fix
 function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
   return (
-    <div
+    <p
       data-slot="empty-description"
       className={cn(
         "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
         className
       )}
       {...props}
-    />
+    />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/empty.tsx` around lines 71 - 80, The EmptyDescription component
currently declares props as React.ComponentProps<"p"> but renders a <div>, so
change the rendered element in the EmptyDescription function from a div to a p,
preserving the data-slot="empty-description", className composition via cn(...,
className), and spreading {...props} so semantics and prop typing match; update
only the JSX tag used by the EmptyDescription function.
components/ui/command.tsx-50-66 (1)

50-66: ⚠️ Potential issue | 🟡 Minor

DialogHeader is rendered outside DialogContent, breaking the modal structure.

The DialogHeader (lines 52-55) is placed as a sibling to DialogContent, but it should be rendered inside the content for proper DOM structure and accessibility. While the sr-only class hides it visually, placing it outside the portal means it may not be associated with the dialog correctly.

🛠️ Suggested fix
   return (
     <Dialog {...props}>
-      <DialogHeader className="sr-only">
-        <DialogTitle>{title}</DialogTitle>
-        <DialogDescription>{description}</DialogDescription>
-      </DialogHeader>
       <DialogContent
         className={cn(
           "top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
           className
         )}
         showCloseButton={showCloseButton}
       >
+        <DialogHeader className="sr-only">
+          <DialogTitle>{title}</DialogTitle>
+          <DialogDescription>{description}</DialogDescription>
+        </DialogHeader>
         {children}
       </DialogContent>
     </Dialog>
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/command.tsx` around lines 50 - 66, DialogHeader is currently
rendered as a sibling of DialogContent which breaks modal semantics and
accessibility; move the DialogHeader (including DialogTitle and
DialogDescription) inside the DialogContent component so it is part of the
dialog portal and associated with the dialog element. Update the return in the
component that renders Dialog, placing the DialogHeader (with its sr-only class)
immediately inside DialogContent (before children) and keep existing props like
className and showCloseButton unchanged; ensure DialogTitle and
DialogDescription remain children of DialogHeader so screen readers can
associate them with the Dialog.
components/ui/breadcrumb.tsx-99-114 (1)

99-114: ⚠️ Potential issue | 🟡 Minor

aria-hidden="true" hides the screen reader text.

The <span className="sr-only">More</span> on line 112 is intended for screen readers, but it's nested inside a parent with aria-hidden="true" (line 103), which hides the entire subtree from assistive technologies. The "More" label will not be announced.

♿ Suggested fix — move sr-only text outside aria-hidden container
 function BreadcrumbEllipsis({
   className,
   ...props
 }: React.ComponentProps<"span">) {
   return (
-    <span
-      data-slot="breadcrumb-ellipsis"
-      role="presentation"
-      aria-hidden="true"
-      className={cn(
-        "flex size-5 items-center justify-center [&>svg]:size-4",
-        className
-      )}
-      {...props}
-    >
-      <MoreHorizontalIcon
-      />
+    <>
       <span className="sr-only">More</span>
-    </span>
+      <span
+        data-slot="breadcrumb-ellipsis"
+        role="presentation"
+        aria-hidden="true"
+        className={cn(
+          "flex size-5 items-center justify-center [&>svg]:size-4",
+          className
+        )}
+        {...props}
+      >
+        <MoreHorizontalIcon />
+      </span>
+    </>
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/breadcrumb.tsx` around lines 99 - 114, The "More" screen-reader
text is inside a container with aria-hidden="true" so it will be ignored; update
the JSX so the aria-hidden is applied only to the icon element
(MoreHorizontalIcon) or the inner element with data-slot="breadcrumb-ellipsis",
and move the <span className="sr-only">More</span> outside that aria-hidden
subtree (as a sibling) so assistive tech can announce it; locate the return
block that renders the element with data-slot="breadcrumb-ellipsis" and
MoreHorizontalIcon and adjust those two elements accordingly.
components/ui/pagination.tsx-101-120 (1)

101-120: ⚠️ Potential issue | 🟡 Minor

Same accessibility issue: aria-hidden hides screen reader text.

The <span className="sr-only">More pages</span> is nested inside a span with aria-hidden (shorthand for aria-hidden="true"), which prevents assistive technologies from announcing it.

♿ Suggested fix
 function PaginationEllipsis({
   className,
   ...props
 }: React.ComponentProps<"span">) {
   return (
-    <span
-      aria-hidden
-      data-slot="pagination-ellipsis"
-      className={cn(
-        "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
-        className
-      )}
-      {...props}
-    >
-      <MoreHorizontalIcon
-      />
+    <>
       <span className="sr-only">More pages</span>
-    </span>
+      <span
+        aria-hidden="true"
+        data-slot="pagination-ellipsis"
+        className={cn(
+          "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
+          className
+        )}
+        {...props}
+      >
+        <MoreHorizontalIcon />
+      </span>
+    </>
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/pagination.tsx` around lines 101 - 120, PaginationEllipsis
currently sets aria-hidden on the wrapper span which hides the nested
screen-reader text; remove the aria-hidden attribute from the outer span in
PaginationEllipsis so the "sr-only" label can be announced, and instead mark the
decorative icon as hidden from assistive tech (set aria-hidden or equivalent on
MoreHorizontalIcon and/or make it focusable={false}) so the visual icon is
ignored but the "More pages" sr-only text remains reachable.
components/ui/field.tsx-193-199 (1)

193-199: ⚠️ Potential issue | 🟡 Minor

Deduplication maps undefined messages to a single entry, and use === for strict comparison.

Two issues:

  1. Line 194 uses error?.message as the Map key. If multiple errors have undefined messages, they collapse into one entry—likely intended, but worth noting.
  2. Line 197 uses == instead of ===. Use strict equality for consistency.
Proposed fix
-    if (uniqueErrors?.length == 1) {
+    if (uniqueErrors?.length === 1) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/field.tsx` around lines 193 - 199, The dedupe logic uses
error?.message as the Map key which collapses multiple errors with undefined
messages into a single entry and it uses loose equality when checking count;
update the Map creation to use a key that preserves distinct undefined-message
errors (for example use error?.message when defined, otherwise a per-item unique
marker such as including the index or a Symbol) so undefined messages don't all
collapse, and change the length check from == to ===; refer to the variables
uniqueErrors and errors and the Map(...) creation and the subsequent if
(uniqueErrors?.length == 1) check to make these changes.
components/ui/sidebar.tsx-600-636 (1)

600-636: ⚠️ Potential issue | 🟡 Minor

Potential SSR hydration mismatch with random skeleton width.

useState(() => Math.floor(Math.random() * 40) + 50}%}) initializes differently on server vs client, causing a hydration mismatch warning in React. Since this is a "use client" component, it may work in Next.js App Router, but the initial server render will differ from client.

Consider using useEffect to set the random width after mount, or use a deterministic value based on an index prop.

Proposed fix using useEffect
 function SidebarMenuSkeleton({
   className,
   showIcon = false,
   ...props
 }: React.ComponentProps<"div"> & {
   showIcon?: boolean
 }) {
-  // Random width between 50 to 90%.
-  const [width] = React.useState(() => {
-    return `${Math.floor(Math.random() * 40) + 50}%`
-  })
+  const [width, setWidth] = React.useState("70%")
+
+  React.useEffect(() => {
+    // Random width between 50 to 90%.
+    setWidth(`${Math.floor(Math.random() * 40) + 50}%`)
+  }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/sidebar.tsx` around lines 600 - 636, SidebarMenuSkeleton
currently seeds a random width in useState during render which can differ
between server and client and cause React hydration mismatches; change it to set
the random width only after mount (e.g., move the random assignment into a
useEffect that calls setWidth) or accept a deterministic prop (e.g., index or
width) so the initial render is identical on server and client. Locate the
SidebarMenuSkeleton function and replace the current useState initializer for
width with a stable initial value (or undefined) and populate the actual random
width inside useEffect (or derive it from the provided prop) so the component
renders the same on server and client and updates only on the client after
mount.
🧹 Nitpick comments (21)
hooks/use-mobile.ts (1)

6-14: Use mql.matches and avoid initial false-negative state.

Line 6 initializes state to undefined and Line 18 coerces it to false via !!isMobile, causing a false-negative on first render until the effect runs and corrects the value. Additionally, Lines 11 and 14 should read from mql.matches (the subscribed query's actual state) instead of recomputing with window.innerWidth.

♻️ Proposed refactor
 export function useIsMobile() {
-  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+  const [isMobile, setIsMobile] = React.useState<boolean>(() => {
+    if (typeof window === "undefined") return false
+    return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches
+  })

   React.useEffect(() => {
     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
-    const onChange = () => {
-      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    const onChange = (event: MediaQueryListEvent) => {
+      setIsMobile(event.matches)
     }
     mql.addEventListener("change", onChange)
-    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    setIsMobile(mql.matches)
     return () => mql.removeEventListener("change", onChange)
   }, [])

-  return !!isMobile
+  return isMobile
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-mobile.ts` around lines 6 - 14, The hook currently initializes
isMobile to undefined and then uses window.innerWidth causing a false-negative
until the effect runs; fix by deriving the initial state and updates from the
MediaQueryList.matches value instead of recomputing window.innerWidth: create
the mql with window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
(guarded for SSR), initialize useState with mql.matches when window exists, use
onChange = () => setIsMobile(mql.matches) (or read the event.matches when
handling the listener) and replace any window.innerWidth checks with
mql.matches; keep using setIsMobile and the existing MOBILE_BREAKPOINT and mql
variable names so the change is localized to the useEffect and state
initialization.
components/ui/collapsible.tsx (1)

5-19: Minor: Simplify props destructuring.

The { ...props } pattern in function parameters is redundant when you're just spreading them back. You can use props directly.

✨ Suggested simplification
-function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
+function Collapsible(props: CollapsiblePrimitive.Root.Props) {
   return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
 }

-function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
+function CollapsibleTrigger(props: CollapsiblePrimitive.Trigger.Props) {
   return (
     <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
   )
 }

-function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
+function CollapsibleContent(props: CollapsiblePrimitive.Panel.Props) {
   return (
     <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/collapsible.tsx` around lines 5 - 19, The parameter
destructuring "{ ...props }" is redundant in Collapsible, CollapsibleTrigger,
and CollapsibleContent; change each function signature to accept the props
object directly (e.g., use "props: CollapsiblePrimitive.Root.Props" for
Collapsible, "props: CollapsiblePrimitive.Trigger.Props" for CollapsibleTrigger,
and "props: CollapsiblePrimitive.Panel.Props" for CollapsibleContent) and keep
the existing JSX that spreads props into the underlying CollapsiblePrimitive
components.
.claude/plans/route-group-permission.md (1)

25-25: Add language specifier to fenced code block.

The directory structure code block is missing a language identifier. While plaintext or text would work, an empty language is flagged by markdownlint.

📝 Suggested fix
-```
+```text
 app/
 ├── layout.tsx                          # 유지
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/plans/route-group-permission.md at line 25, The fenced code block
containing the directory structure (the block starting with "app/") lacks a
language specifier; update its opening backticks to include a language (e.g.,
use ```text or ```plaintext) so markdownlint stops flagging it — locate the code
block in .claude/plans/route-group-permission.md and change the opening ``` to
```text (or ```plaintext) while leaving the block contents unchanged.
package.json (1)

32-32: Consider moving shadcn to devDependencies.

The shadcn package is a CLI tool used for scaffolding and adding components during development. It's not used at runtime, so it would be more appropriate in devDependencies to reduce production bundle concerns.

🔧 Suggested change
  "dependencies": {
    ...
-   "shadcn": "^4.1.2",
    ...
  },
  "devDependencies": {
+   "shadcn": "^4.1.2",
    ...
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 32, Move the "shadcn" dependency from runtime
dependencies to devDependencies in package.json: open package.json, remove the
"shadcn": "^4.1.2" entry from the top-level "dependencies" object and add the
same entry under "devDependencies" (create "devDependencies" block if absent),
then run your package manager's install (npm/yarn/pnpm) to update lockfile so
the change takes effect; ensure any CI or build scripts that expect shadcn
remain able to access it from devDependencies.
components/profile-past-match-records.tsx (1)

54-67: Consider using semantic color tokens instead of slate-* here.

This keeps the new design system easier to theme globally.

🎨 Suggested refactor
-<Badge variant="secondary" className="rounded-full bg-slate-200 text-slate-600">
+<Badge variant="secondary" className="rounded-full bg-muted text-muted-foreground">
...
-  className="h-auto px-1 py-0 text-[11px] font-medium text-slate-400 hover:text-slate-700"
+  className="h-auto px-1 py-0 text-[11px] font-medium text-muted-foreground hover:text-foreground"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/profile-past-match-records.tsx` around lines 54 - 67, Replace the
hard-coded slate-* utility classes in the ProfilePastMatchRecords UI (Badge, the
date <span>, and the Button className) with semantic design-token classes so the
component follows the new theme system; specifically update the Badge's
className (currently "rounded-full bg-slate-200 text-slate-600") to use semantic
tokens like "rounded-full bg-[color-bg-muted] text-[color-text-muted]" (or your
project's equivalent token names), change the date span "text-xs font-medium
text-slate-500" to a semantic token such as "text-xs font-medium
text-[color-text-secondary]", and replace the Button's "text-[11px] font-medium
text-slate-400 hover:text-slate-700" with token-based classes like "text-[11px]
font-medium text-[color-text-subtle] hover:text-[color-text-default]"; also
ensure any referenced token names match your tailwind/config mapping so theming
works across the app.
components/gender-toggle-field.tsx (1)

46-60: Extract duplicated option class to reduce drift risk.

The class string for both gender option labels is identical; pulling it into a shared constant will make future style changes safer.

♻️ Suggested refactor
+const optionClass =
+  "flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5";
...
-          <span className="flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5">
+          <span className={optionClass}>
             남
           </span>
...
-          <span className="flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5">
+          <span className={optionClass}>
             여
           </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/gender-toggle-field.tsx` around lines 46 - 60, In
components/gender-toggle-field.tsx the long duplicated class string used on both
option <span> elements should be pulled into a shared constant (e.g.,
optionClass or GENDER_OPTION_CLASS) so style updates aren’t missed; define the
constant near the top of the GenderToggleField component and replace the
duplicated className values on both spans with that constant, leaving the input
props (name, value, defaultChecked, required) unchanged.
.claude/rules/page-patterns.md (1)

58-59: Update example file-path comments to grouped App Router paths.

Several example headers still point to pre-group paths (e.g., app/profiles/[id]/page.tsx). For consistency with the new permission route groups, these should use grouped paths.

Also applies to: 90-91, 110-111, 139-140

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

In @.claude/rules/page-patterns.md around lines 58 - 59, The example header
paths still use pre-grouped App Router paths like "app/profiles/[id]/page.tsx";
update these example file-path comments to the new grouped App Router form
(e.g., include the appropriate route group wrapper such as "(profiles)" so the
example becomes "app/(profiles)/[id]/page.tsx"), and apply the same replacement
to the other occurrences noted (the other example headers referenced in the
comment). Locate and replace the literal example strings in
.claude/rules/page-patterns.md (instances of "app/profiles/[id]/page.tsx" and
the other similar examples) so all sample paths reflect the grouped App Router
convention.
components/ui/slider.tsx (1)

47-53: Consider using a stable key instead of array index.

Using index as a key for Thumb elements may cause issues if the slider values are reordered dynamically. If the slider supports drag-to-reorder or dynamic value changes that could swap positions, consider using a more stable key.

However, for typical range slider usage where thumb positions are fixed, this is acceptable.

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

In `@components/ui/slider.tsx` around lines 47 - 53, Use stable keys for the
SliderPrimitive.Thumb elements instead of the array index: when rendering thumbs
from _values, derive a stable identifier (e.g., the actual numeric value
_values[index], or an associated id if values are objects) and use that as the
key for SliderPrimitive.Thumb so reordering or dynamic updates won’t cause
incorrect reconciliation; update the map call that currently uses key={index} to
use key based on the value/id.
components/match-detail-modal.tsx (2)

5-5: Consider migrating to types/domain.ts.

The import uses @/lib/types which is being phased out according to coding guidelines. When updating this component, consider migrating to types/domain.ts for domain types.

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

In `@components/match-detail-modal.tsx` at line 5, Update the import of domain
types in MatchDetailModal: replace the current import line that brings in
Candidate, MatchOutcome, and TimelineEvent from "@/lib/types" with an import
from the new domain types module (types/domain.ts); locate the import statement
in components/match-detail-modal.tsx where Candidate, MatchOutcome, and
TimelineEvent are referenced and change the path so the component uses the
canonical domain types file.

201-206: Redundant open condition since early return already handles falsy event.

Line 201 returns null when event is falsy, so Boolean(event) on line 206 will always be true when the Dialog renders. This is harmless but slightly redundant.

🔧 Simplify by removing the redundant condition
   return (
-    <Dialog open={Boolean(event)} onOpenChange={(open) => { if (!open) onClose(); }}>
+    <Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/match-detail-modal.tsx` around lines 201 - 206, The Dialog's open
prop is redundant because you already early-return when event is falsy; replace
open={Boolean(event)} with a constant true (open={true}) or simply open so the
Dialog always mounts when event exists. Update the JSX in
components/match-detail-modal.tsx where Dialog is rendered (the Dialog component
and its onOpenChange prop remain unchanged) to remove the Boolean(event)
expression.
components/ui/select.tsx (1)

164-167: Minor: Unusual formatting on icon elements.

The ChevronUpIcon and ChevronDownIcon have empty space between the opening tag and closing bracket. This is functional but inconsistent with the rest of the file's formatting.

🔧 Tidy up icon formatting
     >
-      <ChevronUpIcon
-      />
+      <ChevronUpIcon />
     </SelectPrimitive.ScrollUpArrow>
     >
-      <ChevronDownIcon
-      />
+      <ChevronDownIcon />
     </SelectPrimitive.ScrollDownArrow>

Also applies to: 183-186

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

In `@components/ui/select.tsx` around lines 164 - 167, The icon elements have
extra whitespace/newlines between the tag name and the self-closing slash which
is inconsistent with the file; update the instances of ChevronUpIcon and
ChevronDownIcon inside SelectPrimitive.ScrollUpArrow and
SelectPrimitive.ScrollDownArrow so they are formatted as single-line
self-closing JSX tags (e.g., <ChevronUpIcon />) matching the rest of the file's
style; ensure both occurrences (the up arrow block and the down arrow block) are
corrected.
components/ui/progress.tsx (1)

19-24: Auto-included ProgressTrack may cause unexpected duplication.

The Progress component renders both {children} and a hardcoded <ProgressTrack><ProgressIndicator /></ProgressTrack>. If a consumer passes a custom ProgressTrack as children, they'll get duplicate tracks.

Consider either:

  1. Document that children should only be ProgressLabel/ProgressValue
  2. Or remove the auto-included track and require consumers to compose explicitly
♻️ Option: Make composition explicit
 function Progress({
   className,
   children,
   value,
   ...props
 }: ProgressPrimitive.Root.Props) {
   return (
     <ProgressPrimitive.Root
       value={value}
       data-slot="progress"
       className={cn("flex flex-wrap gap-3", className)}
       {...props}
     >
       {children}
-      <ProgressTrack>
-        <ProgressIndicator />
-      </ProgressTrack>
     </ProgressPrimitive.Root>
   )
 }

Then consumers would use:

<Progress value={50}>
  <ProgressLabel>Loading...</ProgressLabel>
  <ProgressValue />
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/progress.tsx` around lines 19 - 24, The Progress component
currently renders both {children} and a hardcoded
ProgressTrack/ProgressIndicator which causes duplicate tracks if a consumer
passes a custom ProgressTrack; update Progress to remove the auto-included
<ProgressTrack><ProgressIndicator/></ProgressTrack> and require consumers to
compose their own track/indicator (document that children may include
ProgressLabel, ProgressValue, and an explicit ProgressTrack containing
ProgressIndicator), or alternatively add a prop (e.g., autoTrack = false) to opt
into the included track; locate the logic in the Progress component where
children are rendered and remove or gate the hardcoded
ProgressTrack/ProgressIndicator to prevent duplication.
components/ui/scroll-area.tsx (1)

19-26: Consider exposing scrollbar configuration for flexibility.

The ScrollArea component hardcodes a single vertical ScrollBar. If horizontal scrolling or dual-axis scrolling is needed, consumers would need to modify this component or create custom compositions.

This is acceptable for simple use cases, but consider exposing an orientation prop or allowing children to include custom ScrollBar elements if more flexibility is needed later.

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

In `@components/ui/scroll-area.tsx` around lines 19 - 26, The ScrollArea currently
always renders a single vertical ScrollBar which prevents horizontal or
dual-axis setups; update the ScrollArea component to accept an optional prop
(e.g., orientation: 'vertical' | 'horizontal' | 'both' = 'vertical') and use it
to conditionally render ScrollBar instances (e.g., render <ScrollBar
orientation="vertical" /> and/or <ScrollBar orientation="horizontal" />), or
alternatively accept an optional prop like customScrollBars: ReactNode to allow
consumers to pass their own ScrollBar elements; modify the render around
ScrollAreaPrimitive.Viewport / ScrollBar so the default behavior stays the same
but consumers can enable horizontal or custom scrollbars via the new prop (refer
to ScrollArea component, ScrollBar, and ScrollAreaPrimitive.Viewport to locate
the changes).
components/ui/menubar.tsx (1)

38-52: Consider delegating menubar layers to menubar-native primitives instead of menu-based wrappers.

MenubarPrimitive (which provides roving-focus and menubar keyboard semantics) at the root is composed with DropdownMenu* wrappers that delegate to MenuPrimitive from @base-ui/react/menu. This creates a semantic mismatch: menubar-specific keyboard navigation (e.g., arrow keys, roving focus) may not function as expected when descendants use generic menu primitives instead of menubar-native ones.

Consider refactoring MenubarMenu, MenubarGroup, and MenubarPortal to use menubar-native primitives from @base-ui/react/menubar (similar to how MenubarCheckboxItem and MenubarRadioItem use direct MenuPrimitive wrappers for their semantics) rather than delegating to menu-based DropdownMenu* components.

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

In `@components/ui/menubar.tsx` around lines 38 - 52, The menubar wrappers
MenubarMenu, MenubarGroup, and MenubarPortal currently delegate to
DropdownMenu*, causing a semantic mismatch with the MenubarPrimitive root;
replace those wrappers to use the menubar-native primitives from
`@base-ui/react/menubar` (e.g., MenubarMenuPrimitive / MenubarGroupPrimitive /
MenubarPortalPrimitive or equivalent exports) so the component tree honors
roving-focus and menubar keyboard semantics; follow the pattern used by
MenubarCheckboxItem and MenubarRadioItem which wrap MenuPrimitive directly, and
update imports and JSX for MenubarMenu, MenubarGroup, and MenubarPortal to
render the menubar-native primitives instead of DropdownMenu, DropdownMenuGroup,
and DropdownMenuPortal.
components/ui/table.tsx (1)

1-1: Consider removing "use client" directive.

These table components are stateless wrappers that don't use client-side hooks or browser APIs. They could function as Server Components, reducing client bundle size. The "use client" directive is only needed if consumers require client-side interactivity, which can be handled by wrapping at the usage site.

Based on learnings: "Use Server Components by default in Next.js App Router; add "use client" only to leaf components to minimize client bundle size."

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

In `@components/ui/table.tsx` at line 1, Remove the top-level "use client"
directive from this file so the table components become Server Components;
confirm that none of the exported components (e.g., Table, TableHeader,
TableBody, TableRow, TableCell or similarly named stateless wrappers in this
file) use client-side hooks or browser APIs, and if any consumer requires
client-side interactivity add "use client" at the usage site or to a small
interactive wrapper component instead.
components/ui/carousel.tsx (1)

78-89: Consider adding ArrowUp/ArrowDown for vertical orientation.

Keyboard navigation only handles ArrowLeft/ArrowRight. For vertical carousels, users might intuitively expect ArrowUp/ArrowDown to navigate slides.

♻️ Optional enhancement
   const handleKeyDown = React.useCallback(
     (event: React.KeyboardEvent<HTMLDivElement>) => {
-      if (event.key === "ArrowLeft") {
+      if (event.key === "ArrowLeft" || (orientation === "vertical" && event.key === "ArrowUp")) {
         event.preventDefault()
         scrollPrev()
-      } else if (event.key === "ArrowRight") {
+      } else if (event.key === "ArrowRight" || (orientation === "vertical" && event.key === "ArrowDown")) {
         event.preventDefault()
         scrollNext()
       }
     },
-    [scrollPrev, scrollNext]
+    [scrollPrev, scrollNext, orientation]
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/carousel.tsx` around lines 78 - 89, handleKeyDown currently
only maps ArrowLeft/ArrowRight to scrollPrev/scrollNext; add handling for
ArrowUp/ArrowDown when the carousel is vertical so vertical carousels respond to
expected keys. Update the handleKeyDown callback to check the carousel
orientation (e.g., a prop or state like orientation === "vertical") and call
scrollPrev on ArrowUp and scrollNext on ArrowDown while preserving the existing
ArrowLeft/ArrowRight behavior; reference the handleKeyDown function and the
scrollPrev/scrollNext callbacks so the new branches integrate with the existing
keyboard handling.
components/ui/dropdown-menu.tsx (1)

136-145: data-slot prop is not applied to the popup element.

The data-slot="dropdown-menu-sub-content" passed on line 137 will be spread onto DropdownMenuContent's outer element, but DropdownMenuContent internally sets data-slot="dropdown-menu-content" on the MenuPrimitive.Popup (line 43). The submenu-specific slot identifier won't reach the actual popup element.

Consider adding explicit support for a data-slot override in DropdownMenuContent if distinguishing submenus is needed.

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

In `@components/ui/dropdown-menu.tsx` around lines 136 - 145, The
DropdownMenuContent component currently forces data-slot="dropdown-menu-content"
on MenuPrimitive.Popup, so a passed prop data-slot (e.g.
"dropdown-menu-sub-content") from the parent never reaches the actual popup;
update DropdownMenuContent to accept and forward an overriding data-slot prop to
MenuPrimitive.Popup (preserve the default "dropdown-menu-content" when no
override provided) and ensure the prop is applied to the outer popup element
instead of hard-coding the attribute in the component implementation.
components/ui/context-menu.tsx (1)

145-152: Same data-slot override issue as dropdown menu.

The data-slot="context-menu-sub-content" on line 147 won't reach the actual popup element since ContextMenuContent sets its own data-slot internally.

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

In `@components/ui/context-menu.tsx` around lines 145 - 152, The data-slot
attribute on the ContextMenuContent JSX won’t be applied because
ContextMenuContent renders its own element and overrides data-slot; change to
use the component’s asChild prop and wrap it with a native element (e.g., <div>
or <span>) that sets data-slot="context-menu-sub-content" so the attribute lands
on the real popup node (modify the ContextMenuContent usage in
components/ui/context-menu.tsx to use asChild and a wrapped element with the
data-slot, preserving className, side and spreading {...props} onto the
wrapper).
components/ui/input-group.tsx (1)

57-61: Click handler assumes single input per group.

The querySelector("input") will only focus the first input element if multiple exist within the group. This is likely fine for typical usage patterns, but worth noting if complex multi-input groups are planned.

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

In `@components/ui/input-group.tsx` around lines 57 - 61, The onClick handler in
the InputGroup component currently uses parentElement?.querySelector("input")
which only targets the first input and fails for multi-input groups; update the
handler (the arrow function passed to onClick) to collect inputs via
parentElement?.querySelectorAll("input") and then pick the appropriate element
to focus (e.g., the first visible, enabled input: filter out inputs with
type="hidden" and disabled, then call focus() on the first match) to ensure the
correct input is focused when multiple inputs exist.
components/ui/combobox.tsx (1)

215-230: Redundant type intersection in ComboboxChips props.

React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> and ComboboxPrimitive.Chips.Props likely overlap significantly. Consider using just one to avoid type complexity.

Suggested simplification
 function ComboboxChips({
   className,
   ...props
-}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
-  ComboboxPrimitive.Chips.Props) {
+}: ComboboxPrimitive.Chips.Props & { className?: string }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/combobox.tsx` around lines 215 - 230, The props type for
ComboboxChips is overly complex due to the intersection of
React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> and
ComboboxPrimitive.Chips.Props; simplify by using a single source of truth
(preferably React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips>) for the
function signature in ComboboxChips, removing the redundant
ComboboxPrimitive.Chips.Props part and keeping the rest of the component
implementation unchanged so types remain accurate and less noisy.
components/ui/chart.tsx (1)

94-115: dangerouslySetInnerHTML usage is acceptable here but document the trust boundary.

The generated CSS is constructed from ChartConfig keys and color values. Since ChartConfig is typically defined by developers (not user input), XSS risk is minimal. However, if config keys or colors ever derive from untrusted sources, this could inject arbitrary CSS or escape the style block.

Consider adding a brief comment documenting that config keys and color values must be trusted/sanitized if sourced externally.

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

In `@components/ui/chart.tsx` around lines 94 - 115, Add a short inline comment
near the style block that uses dangerouslySetInnerHTML (the block that builds
CSS from THEMES, id and colorConfig) documenting the trust boundary: state that
ChartConfig keys and color values must be developer-controlled or sanitized
before use, and if they can be derived from external/untrusted sources they must
be validated/escaped to prevent CSS/HTML injection; reference the variables
THEMES, id and colorConfig in the comment so future readers know which inputs
require trust or sanitization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 837e1a03-cd25-457b-bbf8-06a5e51cbbd1

📥 Commits

Reviewing files that changed from the base of the PR and between 62bcaf5 and e28fb20.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (106)
  • .claude/plans/route-group-permission.md
  • .claude/plans/shadcn-ui-migration.md
  • .claude/rules/page-patterns.md
  • .claude/rules/ui-components.md
  • app/(admin)/candidates/new/page.tsx
  • app/(admin)/profiles/[id]/edit/page.tsx
  • app/(admin)/profiles/[id]/loading.tsx
  • app/(admin)/profiles/[id]/page.tsx
  • app/(member)/dashboard/loading.tsx
  • app/(member)/dashboard/page.tsx
  • app/(member)/timeline/page.tsx
  • app/(public)/auth/callback/route.ts
  • app/(public)/auth/continue/page.tsx
  • app/(public)/login/page.tsx
  • app/(public)/page.tsx
  • app/(public)/pending/page.tsx
  • app/(super-admin)/admin/page.tsx
  • app/admin/page.tsx
  • app/candidates/new/page.tsx
  • app/globals.css
  • app/layout.tsx
  • components.json
  • components/account-panel.tsx
  • components/auth-forms.tsx
  • components/back-nav-button.tsx
  • components/candidate-card.tsx
  • components/dashboard-flow-board.tsx
  • components/dashboard-logout-button.tsx
  • components/form-submit-button.tsx
  • components/gender-toggle-field.tsx
  • components/home-account-shell.tsx
  • components/lazy-home-account-shell.tsx
  • components/manager-dashboard.tsx
  • components/match-detail-modal.tsx
  • components/match-history-list-modal.tsx
  • components/operator-desk-controls.tsx
  • components/pending-invite-view.tsx
  • components/photo-upload-field.tsx
  • components/profile-interactive-gallery.tsx
  • components/profile-match-kanban.tsx
  • components/profile-past-match-records.tsx
  • components/splash-intro.tsx
  • components/status-badge.tsx
  • components/ui/accordion.tsx
  • components/ui/alert-dialog.tsx
  • components/ui/alert.tsx
  • components/ui/aspect-ratio.tsx
  • components/ui/avatar.tsx
  • components/ui/badge.tsx
  • components/ui/breadcrumb.tsx
  • components/ui/button-group.tsx
  • components/ui/button.tsx
  • components/ui/calendar.tsx
  • components/ui/card.tsx
  • components/ui/carousel.tsx
  • components/ui/chart.tsx
  • components/ui/checkbox.tsx
  • components/ui/collapsible.tsx
  • components/ui/combobox.tsx
  • components/ui/command.tsx
  • components/ui/context-menu.tsx
  • components/ui/dialog.tsx
  • components/ui/direction.tsx
  • components/ui/drawer.tsx
  • components/ui/dropdown-menu.tsx
  • components/ui/empty.tsx
  • components/ui/field.tsx
  • components/ui/hover-card.tsx
  • components/ui/input-group.tsx
  • components/ui/input-otp.tsx
  • components/ui/input.tsx
  • components/ui/item.tsx
  • components/ui/kbd.tsx
  • components/ui/label.tsx
  • components/ui/menubar.tsx
  • components/ui/native-select.tsx
  • components/ui/navigation-menu.tsx
  • components/ui/pagination.tsx
  • components/ui/popover.tsx
  • components/ui/progress.tsx
  • components/ui/radio-group.tsx
  • components/ui/resizable.tsx
  • components/ui/scroll-area.tsx
  • components/ui/select.tsx
  • components/ui/separator.tsx
  • components/ui/sheet.tsx
  • components/ui/sidebar.tsx
  • components/ui/skeleton.tsx
  • components/ui/slider.tsx
  • components/ui/sonner.tsx
  • components/ui/spinner.tsx
  • components/ui/switch.tsx
  • components/ui/table.tsx
  • components/ui/tabs.tsx
  • components/ui/textarea.tsx
  • components/ui/toggle-group.tsx
  • components/ui/toggle.tsx
  • components/ui/tooltip.tsx
  • docs/color-system.md
  • docs/typography.md
  • docs/ui-tokens.md
  • hooks/use-mobile.ts
  • lib/cn.ts
  • lib/utils.ts
  • package.json
  • tsconfig.tsbuildinfo
💤 Files with no reviewable changes (2)
  • app/candidates/new/page.tsx
  • app/admin/page.tsx

import { rejectMembership, updateMembershipRole } from "@/lib/admin-actions";
import { getApprovedMemberships, getPendingMemberships } from "@/lib/data";
import { requireMembershipRole } from "@/lib/permissions";
import type { AppRole, Membership } from "@/lib/types";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the domain model from types/domain.ts instead of legacy lib/types.

This new page introduces fresh dependencies on legacy @/lib/types and snake_case member fields, which conflicts with the ongoing domain-model migration and increases shape inconsistency in the app layer.

Suggested direction
-import type { AppRole, Membership } from "@/lib/types";
+import type { AppRole, Membership } from "@/types/domain";

Then align usages to camelCase domain fields (e.g., userId, createdAt, fullName) in this page.

As per coding guidelines: **/*.{ts,tsx}: Use types/domain.ts for all new code; define domain types with camelCase fields (legacy lib/types.ts is being phased out).

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

In `@app/`(super-admin)/admin/page.tsx at line 8, The file imports legacy types
(AppRole, Membership) from "@/lib/types" and uses snake_case member fields;
replace that import to use the domain model from "types/domain.ts" and update
all usages in this module (e.g., any references to Membership, AppRole, and
member properties) to the camelCase domain fields such as userId, createdAt,
fullName (and other camelCased members) so the page aligns with the new domain
types and removes dependency on legacy "@/lib/types".

Comment thread app/layout.tsx
import { cn } from "@/lib/utils";

const pretendard = localFont({
src: "../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Next.js 15, is using next/font/localwithsrcpointing to../node_modules/...officially supported/recommended in production, or is colocating fonts in app source (e.g.,app/fonts) the recommended approach?

💡 Result:

Colocating fonts in the app source (e.g., app/fonts) is the recommended approach for Next.js 15. Using next/font/local with src pointing to ../node_modules/... is not officially recommended or shown in examples, and may cause issues in production due to build-time resolution and deployment constraints.

Citations:


Move font to project-local path instead of sourcing from ../node_modules/.

Next.js 15 recommends colocating fonts in the app source (e.g., app/fonts/) rather than referencing ../node_modules/. Using node_modules-relative paths can cause build-time resolution and deployment issues in production.

Suggested adjustment
-  src: "../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2",
+  src: "./fonts/PretendardVariable.woff2",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/layout.tsx` at line 7, The font is referenced from node_modules using
src:
"../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2" in
app/layout.tsx which can break builds; copy the PretendardVariable.woff2 file
into a project-local directory (e.g., app/fonts/) and update the font reference
in app/layout.tsx (where you configure the font) to use the new local path
(e.g., "/fonts/PretendardVariable.woff2" or import from app/fonts) so Next.js
can resolve it at build and runtime.

Comment thread components/auth-forms.tsx
Comment on lines +64 to +89
<form className="mt-6 grid gap-4" action={signInWithPassword}>
<FieldShell label="id">
<Input
className="h-12 rounded-xl"
name="username"
placeholder="junseong"
required
minLength={4}
maxLength={20}
pattern="[a-z0-9._-]{4,20}"
/>
</FieldShell>
<FieldShell label="password">
<Input
className="h-12 rounded-xl"
name="password"
type="password"
placeholder="비밀번호 입력"
required
/>
</FieldShell>

{tab === "signin" ? (
<form className="mt-6 grid gap-4" action={signInWithPassword}>
<FieldShell label="id">
<input
className={inputClassName}
name="username"
placeholder="junseong"
required
minLength={4}
maxLength={20}
pattern="[a-z0-9._-]{4,20}"
/>
</FieldShell>
<FieldShell label="password">
<input
className={inputClassName}
name="password"
type="password"
placeholder="비밀번호 입력"
required
/>
</FieldShell>
<Button className="mt-2 h-12 rounded-full text-sm font-semibold" type="submit">
로그인
</Button>
</form>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether form components currently include RHF/Zod usage.
fd -i 'form' components -e tsx -e ts | xargs rg -n --no-heading 'useForm|zodResolver|from "zod"|from '\''zod'\'''

Repository: saechimdaeki/project-cupid

Length of output: 224


🏁 Script executed:

cat -n components/auth-forms.tsx | head -160

Repository: saechimdaeki/project-cupid

Length of output: 6656


Implement React Hook Form + Zod validation for both auth forms.

The sign-in and sign-up forms currently use only native HTML validation. Project guidelines require components/**/*form*.{ts,tsx} to implement React Hook Form with Zod validation alongside Server Actions. Server Actions are already in place (signInWithPassword, signUpWithPassword); add RHF + Zod schemas for robust client and server validation.

Affects lines 64-89 (sign-in) and 109-138 (sign-up).

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

In `@components/auth-forms.tsx` around lines 64 - 89, Replace the native HTML
validation with React Hook Form + Zod: create zod schemas (e.g., signInSchema
and signUpSchema) matching the same rules (username pattern, min/max lengths,
password requirements), wire them into useForm via the zod resolver (useForm({
resolver: zodResolver(signInSchema) }) and similarly for signUpSchema), replace
native input attributes with RHF register/controlled inputs (use register for
"username" and "password" or Controller if needed) and call the existing Server
Actions (signInWithPassword and signUpWithPassword) from the form submit
handlers (handleSubmit) passing validated values; ensure FieldShell/Input/Button
components receive RHF props (error display from formState.errors) so
client-side validation uses Zod while server actions remain unchanged.

Comment on lines +11 to +17
style={
{
"--ratio": ratio,
} as React.CSSProperties
}
className={cn("relative aspect-(--ratio)", className)}
{...props}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify call sites that pass inline style to AspectRatio (higher risk of override).
rg -nP --type=tsx -C2 '<AspectRatio\b[^>]*\bstyle='

Repository: saechimdaeki/project-cupid

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Find the aspect-ratio file and check its current state
find . -name "aspect-ratio.tsx" -o -name "aspect-ratio.ts" | head -20

Repository: saechimdaeki/project-cupid

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Search for AspectRatio component usage (adjust for file type detection)
rg "AspectRatio" --type typescript --type typescriptreact -C2 | head -100

Repository: saechimdaeki/project-cupid

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Alternative: search by filename patterns
fd "aspect-ratio" -e tsx -e ts

Repository: saechimdaeki/project-cupid

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Read the aspect-ratio.tsx file to see current implementation
cat -n ./components/ui/aspect-ratio.tsx

Repository: saechimdaeki/project-cupid

Length of output: 626


🏁 Script executed:

#!/bin/bash
# Search for AspectRatio usage without type filters
rg "AspectRatio" -C3 | head -150

Repository: saechimdaeki/project-cupid

Length of output: 502


🏁 Script executed:

#!/bin/bash
# More comprehensive search for AspectRatio usage - search all files
rg "AspectRatio" --no-heading -B2 -A2

Repository: saechimdaeki/project-cupid

Length of output: 425


🏁 Script executed:

#!/bin/bash
# Check if there are any imports of AspectRatio
rg "from.*aspect-ratio|import.*AspectRatio" -B1 -A1

Repository: saechimdaeki/project-cupid

Length of output: 50383


🏁 Script executed:

#!/bin/bash
# List all tsx/ts files that might use AspectRatio
fd "\.(tsx|ts)$" | xargs rg "AspectRatio" 2>/dev/null | head -50

Repository: saechimdaeki/project-cupid

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Broader search for AspectRatio - look in all files for any reference
find . -type f \( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \) -not -path "./node_modules/*" | xargs grep -l "AspectRatio" 2>/dev/null

Repository: saechimdaeki/project-cupid

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Check if there's a tailwind.config file to understand custom class definitions
find . -type f -name "tailwind.config.*" | head -5

Repository: saechimdaeki/project-cupid

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Look for any imports of AspectRatio in the codebase
rg "import.*AspectRatio|from.*aspect-ratio" --no-heading

Repository: saechimdaeki/project-cupid

Length of output: 50383


Prevent accidental style override from caller props.

In React JSX, spreading ...props after an explicit style attribute means a caller-provided style prop will override the inline styles entirely, which can drop the --ratio CSS custom property. While this component currently has no callers, the fix is a defensive best practice.

💡 Proposed fix
 function AspectRatio({
   ratio,
   className,
+  style,
   ...props
 }: React.ComponentProps<"div"> & { ratio: number }) {
   return (
     <div
       data-slot="aspect-ratio"
-      style={
-        {
-          "--ratio": ratio,
-        } as React.CSSProperties
-      }
+      style={{ ...(style as React.CSSProperties), "--ratio": ratio } as React.CSSProperties}
       className={cn("relative aspect-(--ratio)", className)}
       {...props}
     />
   )
 }
📝 Committable suggestion

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

Suggested change
style={
{
"--ratio": ratio,
} as React.CSSProperties
}
className={cn("relative aspect-(--ratio)", className)}
{...props}
function AspectRatio({
ratio,
className,
style,
...props
}: React.ComponentProps<"div"> & { ratio: number }) {
return (
<div
data-slot="aspect-ratio"
style={{ ...(style || {}), "--ratio": ratio } as React.CSSProperties}
className={cn("relative aspect-(--ratio)", className)}
{...props}
/>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/aspect-ratio.tsx` around lines 11 - 17, The inline style
setting the CSS variable "--ratio" on the AspectRatio component can be
accidentally overridden because props are spread after the explicit style;
change the prop handling to merge any incoming style with the required "--ratio"
instead of letting incoming style replace it (e.g., extract style from props and
set style = { ...style, ["--ratio"]: ratio } cast as React.CSSProperties) and
continue to spread the remaining props (rest) so caller styles are preserved but
the "--ratio" custom property remains enforced; keep the className merge using
cn("relative aspect-(--ratio)", className) and spread the rest of props (not the
original props object) into the element.

Comment on lines +42 to +60
function BreadcrumbLink({
className,
render,
...props
}: useRender.ComponentProps<"a">) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
className: cn("transition-colors hover:text-foreground", className),
},
props
),
render,
state: {
slot: "breadcrumb-link",
},
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check `@base-ui/react` useRender documentation and usage patterns
ast-grep --pattern 'useRender({
  $$$
  state: {
    $$$
  }
  $$$
})'

Repository: saechimdaeki/project-cupid

Length of output: 52


🏁 Script executed:

# First, examine the breadcrumb.tsx file to understand context and imports
cat -n components/ui/breadcrumb.tsx

Repository: saechimdaeki/project-cupid

Length of output: 3530


🏁 Script executed:

# Search for useRender imports to find where the hook comes from
rg -i "useRender|import.*useRender" components/ui/

Repository: saechimdaeki/project-cupid

Length of output: 1622


🏁 Script executed:

# Look for useRender hook definition or implementation in the codebase
fd useRender -t f

Repository: saechimdaeki/project-cupid

Length of output: 52


🏁 Script executed:

# Search for other usages of useRender with state parameter
rg -B2 -A5 "state:\s*{" --type ts --type tsx | head -100

Repository: saechimdaeki/project-cupid

Length of output: 153


🏁 Script executed:

# Search for useRender calls with state parameter across files
rg -A 10 "useRender\(\{" components/ui/ | grep -A 10 "state:"

Repository: saechimdaeki/project-cupid

Length of output: 2506


🏁 Script executed:

# Check sidebar.tsx for useRender usage patterns
sed -n '1,100p' components/ui/sidebar.tsx | grep -A 15 "useRender"

Repository: saechimdaeki/project-cupid

Length of output: 586


🏁 Script executed:

# Search for `@base-ui/react` types or documentation in node_modules or package info
fd package.json | xargs grep -l "@base-ui/react"

Repository: saechimdaeki/project-cupid

Length of output: 82


🏁 Script executed:

# Check package.json for `@base-ui/react` version and dependencies
cat package.json | grep -A 50 "dependencies"

Repository: saechimdaeki/project-cupid

Length of output: 1088


🏁 Script executed:

# Search for any tests or documentation about useRender in the codebase
fd -e test.ts -e spec.ts -e .md | xargs grep -l "useRender\|state.*slot" 2>/dev/null

Repository: saechimdaeki/project-cupid

Length of output: 54


🏁 Script executed:

# Check if there's a types directory or any type definitions related to useRender
fd -i "types\|types.ts" | head -20

Repository: saechimdaeki/project-cupid

Length of output: 52


🌐 Web query:

@base-ui/react useRender hook state parameter data-slot attribute

💡 Result:

The @base-ui/react useRender hook enables a render prop in custom components for polymorphism and composition. It accepts a state parameter of type State, which is passed to the render callback function: render={(props, state) => <Element {...props} data-state={state.someState ? 'on' : 'off'} />}. This allows custom rendering based on component state. The state parameter, combined with stateAttributesMapping (type StateAttributesMapping), automatically generates data-* attributes on the rendered element from state properties. For example, boolean state keys like disabled produce data-disabled attributes (data-disabled="" when true). Custom mapping via stateAttributesMapping overrides defaults for converting state to data attributes. There is no data-slot attribute directly on useRender elements. Base UI components use data-* attributes for state styling (e.g., data-open, data-disabled), not data-slot. data-slot appears in web component contexts (HTML ), but Base UI uses React render props, not native slots. MUI's older Base UI had slots/slotProps props, but current @base-ui/react (successor) uses render with useRender. Example usage: import {useRender} from '@base-ui/react/use-render'; function CustomButton({render, disabled, ...props}) { const state = {disabled}; return useRender({ defaultTagName: 'button', render, props, state, // stateAttributesMapping: customMapping // optional }); } In render callback: access state for conditional logic/attributes. Official docs confirm state for render prop and data attribute generation.

Citations:


🏁 Script executed:

# Look for any comments or implementation details in files that use useRender
rg -B 5 "state:" components/ui/ | grep -E "//|/\*"

Repository: saechimdaeki/project-cupid

Length of output: 52


🏁 Script executed:

# Check if there's any documentation files in the repo
fd -e md -e txt | xargs grep -l "useRender\|base-ui" 2>/dev/null

Repository: saechimdaeki/project-cupid

Length of output: 54


Add stateAttributesMapping to convert state.slot to data-slot attribute.

The useRender hook's state parameter does not automatically convert state properties to data-* attributes. The slot: "breadcrumb-link" in the state object will not render as data-slot="breadcrumb-link" without explicit stateAttributesMapping. Either add stateAttributesMapping: { slot: 'slot' } to the useRender call, or use direct JSX like the other breadcrumb components (Breadcrumb, BreadcrumbList, etc.).

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

In `@components/ui/breadcrumb.tsx` around lines 42 - 60, BreadcrumbLink's
useRender call sets state: { slot: "breadcrumb-link" } but doesn't map that
state to a data attribute, so add a stateAttributesMapping to the useRender
invocation (e.g., stateAttributesMapping: { slot: "slot" }) so state.slot
becomes data-slot="breadcrumb-link"; update the useRender call inside the
BreadcrumbLink function (the one that currently passes defaultTagName, props via
mergeProps, render, and state) to include stateAttributesMapping: { slot: "slot"
} so the component outputs data-slot correctly.

Comment on lines +192 to +196
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider adding ref to Button.

The ref is created but never passed to the Button component. The focus effect won't work as intended since ref.current will always be null.

🐛 Proposed fix
 function CalendarDayButton({
   className,
   day,
   modifiers,
   locale,
   ...props
 }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
   const defaultClassNames = getDefaultClassNames()

   const ref = React.useRef<HTMLButtonElement>(null)
   React.useEffect(() => {
     if (modifiers.focused) ref.current?.focus()
   }, [modifiers.focused])

   return (
     <Button
+      ref={ref}
       variant="ghost"
       size="icon"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/calendar.tsx` around lines 192 - 196, The focus ref is created
with React.useRef and used in a React.useEffect watching modifiers.focused, but
it is never passed to the Button, so ref.current remains null; fix by forwarding
the ref to the rendered Button element (pass ref={ref} to the Button instance)
or use the Button's forwardedRef prop if it expects a different prop name,
ensuring the ref variable declared in this file is the one attached so the
useEffect can call ref.current.focus() when modifiers.focused changes.

Comment thread components/ui/empty.tsx
Comment on lines +1 to +6
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify React namespace usage vs import in this file
rg -n 'React\.' components/ui/empty.tsx
rg -n '^import .*react' components/ui/empty.tsx

Repository: saechimdaeki/project-cupid

Length of output: 544


🏁 Script executed:

cat -n components/ui/empty.tsx | head -100

Repository: saechimdaeki/project-cupid

Length of output: 3114


Add missing React type import.

File uses React.ComponentProps in multiple function signatures (lines 5, 18, 47, 58, 71, 84) but doesn't import React, causing TypeScript compilation to fail.

Suggested fix
+import type * as React from "react"
 import { cva, type VariantProps } from "class-variance-authority"
📝 Committable suggestion

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

Suggested change
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/empty.tsx` around lines 1 - 6, Add the missing React type
import at the top of the file so usages of React.ComponentProps in the component
signatures (e.g., Empty and the other functions using React.ComponentProps)
compile; insert an import for React (or an "import type React from 'react'")
near the existing imports so all component props typed as
React.ComponentProps<"div"> (and similar) resolve correctly.

Comment on lines +1 to +3
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { cva } from "class-variance-authority"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify React namespace usage and whether a react import exists
rg -n 'React\.' components/ui/navigation-menu.tsx
rg -n '^import .*react' components/ui/navigation-menu.tsx

Repository: saechimdaeki/project-cupid

Length of output: 434


Add missing React type import for React.ComponentPropsWithRef usages.

The file uses React.ComponentPropsWithRef on lines 32, 48, and 143 but doesn't import React. Add the type import:

Suggested fix
+import type * as React from "react"
 import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
 import { cva } from "class-variance-authority"
📝 Committable suggestion

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

Suggested change
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { cva } from "class-variance-authority"
import type * as React from "react"
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { cva } from "class-variance-authority"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/navigation-menu.tsx` around lines 1 - 3, This file uses
React.ComponentPropsWithRef in several component prop typings but doesn't import
React; add a type-only import and update usages: add "import type {
ComponentPropsWithRef } from 'react'" at the top, then replace occurrences of
React.ComponentPropsWithRef with ComponentPropsWithRef (the type import) for the
components that reference it (the props typed via ComponentPropsWithRef used
alongside NavigationMenuPrimitive and your NavigationMenu items), ensuring
TypeScript resolves the type without importing runtime React.

<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Tailwind CSS v4, for data-attribute value selectors, is data-activation-direction=left:valid syntax, or should it bedata-[activation-direction=left]:?

💡 Result:

In Tailwind CSS v4, the valid syntax for data-attribute value selectors is data-[attribute=value]:, such as data-[activation-direction=left]:. The shorthand data-activation-direction=left: is not valid syntax.

Citations:


Fix malformed Tailwind data-attribute variants in transition classes.

The syntax data-activation-direction=left: is invalid in Tailwind CSS v4 and will be purged during build. Use the proper bracket notation data-[activation-direction=left]: instead. This affects all motion-related variants in the className string.

Suggested fix
-        "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] ...
+        "data-ending-style:data-[activation-direction=left]:translate-x-[50%] data-ending-style:data-[activation-direction=right]:translate-x-[-50%] data-starting-style:data-[activation-direction=left]:translate-x-[-50%] data-starting-style:data-[activation-direction=right]:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] ...
📝 Committable suggestion

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

Suggested change
"data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95",
"data-ending-style:data-[activation-direction=left]:translate-x-[50%] data-ending-style:data-[activation-direction=right]:translate-x-[-50%] data-starting-style:data-[activation-direction=left]:translate-x-[-50%] data-starting-style:data-[activation-direction=right]:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/navigation-menu.tsx` at line 87, The long className string in
the NavigationMenu component contains malformed Tailwind variants like
data-activation-direction=left: which will be purged; update every occurrence to
use bracket notation (e.g. change data-activation-direction=left: →
data-[activation-direction=left]: and data-activation-direction=right: →
data-[activation-direction=right]:) and similarly convert any other
data-attribute variants (e.g. data-[motion=from-end],
data-[slot=navigation-menu-link], data-starting-style/data-ending-style) to the
correct data-[...=...] or data-[...]-style bracket form so Tailwind v4
recognizes them in the className string used by the NavigationMenu element.

Comment thread components/ui/tabs.tsx
Comment on lines +8 to +22
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify orientation is destructured and whether it is forwarded to Root
rg -n -C3 'function Tabs\(|orientation\s*=' components/ui/tabs.tsx
rg -n -C4 '<TabsPrimitive\.Root|data-orientation|orientation=' components/ui/tabs.tsx

Repository: saechimdaeki/project-cupid

Length of output: 743


Forward orientation prop to TabsPrimitive.Root.

orientation is destructured from props but only used for the data-orientation attribute. The underlying component needs the actual orientation prop to function correctly.

Suggested fix
     <TabsPrimitive.Root
       data-slot="tabs"
+      orientation={orientation}
       data-orientation={orientation}
       className={cn(
         "group/tabs flex gap-2 data-horizontal:flex-col",
         className
       )}
       {...props}
     />
📝 Committable suggestion

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

Suggested change
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
orientation={orientation}
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/tabs.tsx` around lines 8 - 22, The Tabs component destructures
orientation but only sets data-orientation; forward the actual orientation prop
into TabsPrimitive.Root so the underlying component receives it (update Tabs
function to pass orientation={orientation} into TabsPrimitive.Root alongside
data-orientation and existing props); ensure you modify the Tabs function
signature/JSX where TabsPrimitive.Root is rendered to include the orientation
prop.

@saechimdaeki saechimdaeki merged commit e07329e into main Apr 4, 2026
2 checks passed
@saechimdaeki saechimdaeki deleted the feature/route-group-permission branch April 5, 2026 00:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants