Skip to content

[FEAT] 공통 컴포넌트 구현 - #8

Merged
9g-g9 merged 20 commits into
devfrom
feature/create-common-components
Jul 23, 2026
Merged

[FEAT] 공통 컴포넌트 구현#8
9g-g9 merged 20 commits into
devfrom
feature/create-common-components

Conversation

@9g-g9

@9g-g9 9g-g9 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📋 작업 내용

  • 공통 컴포넌트 라이브러리를 작업하였습니다. (Input, PasswordInput, Textarea, Search, Button, FormField, Select, Modal, Pagination, Toast, Footer, Header)
  • Input/Textarea 계열은 forwardRef + error prop 기반으로 리팩터링하여 react-hook-form/zod 등 외부 검증 로직과 결합하기 쉽도록 설계하였습니다.
  • Select, Modal을 Compound Component 패턴(합성 컴포넌트)으로 구현했습니다. (Select.Option, Modal.Title/Modal.Desc/Modal.Close/Modal.Button)
  • Header/Footer 공통 레이아웃 컴포넌트 작성 및 layout.tsx에 연결하였고, Header는 로그인 여부(isLogin prop, 추후 실제 auth 연동 전 임시)에 따라 네비게이션 분기 및 현재 경로 기준 활성 링크 스타일 적용하였습니다.
  • 공용 훅 useClickOutside, useIsClient 추가하였습니다. (드롭다운 외부 클릭 감지, SSR-safe 클라이언트 감지 -> client 임을 확인하는 과정)
  • tokens.theme.css 에 --shadow-select 토큰 추가하였습니다.

🔥 변경 사항

  • Button, Input, PasswordInput, Textarea, Search, Select, Modal, Pagination, FormField, Toast, Footer, Header 등 12개 공통 컴포넌트 추가하였습니다.
  • src/app/layout.tsxHeader/Footer 추가 적용하였습니다.
  • next/image 기반 아이콘 에셋 적용하였습니다.

✅ 체크리스트

  • 로컬에서 정상 동작을 확인했습니다.
  • 기존 기능에 영향을 주지 않는지 확인했습니다.
  • 불필요한 console.log를 제거했습니다.
  • lint를 통과했습니다.
  • README 또는 문서를 수정했습니다. (필요 시)
  • API 명세와 일치하는지 확인했습니다.

📷 스크린샷 (선택)

  • header
image
  • input 관련 (number, text, password, textarea, search)
image image
  • button
image
  • select
image image
  • modal
image
  • pagination
image image
  • toast (3초 뒤 사라짐)
image
  • footer
image

💬 To Reviewer

  • 데스크탑 기준으로 작성된 버전입니다. 미리 공통 컴포넌트 작업을 올려둬야할 것 같아서 모바일을 아직 적용하지 않았는데, 지금 적용이 필요하다면 바로 적용하겠습니다.
  • token 쪽 놓친 부분이나 하드코딩된 부분이 있다면 말씀 부탁드리겠습니다.
  • token 쪽 Component Color 쪽은 접두사 color 가 들어가 있지 않아 bg-background ~~ 식으로 처리하지 못했는데, 혹시 추가하는게 맞는지 고민됩니다.
  • footer 디자인을 임시로 해두었는데 이상하다면 말씀 부탁드립니다. (스크린샷에 추가해두는 걸 까먹었었습니다!! 추가해두었습니다)
  • 빠진 부분이 있다면 가감없이 말씀해주세요!!! 감사합니다.

Summary by CodeRabbit

Summary by CodeRabbit

  • 새 기능
    • 공통 UI(버튼, 입력/비밀번호/텍스트 영역, 선택, 검색, 페이지네이션, 토스트, 모달) 및 헤더/푸터를 추가했습니다.
    • 모달은 포커스/키보드 동작 등 접근성 지원을 포함합니다.
  • 스타일
    • 테마 디자인 토큰 네이밍을 정규화하고 입력·선택 그림자/간격 토큰을 확장했습니다.
  • 기타
    • 기본 페이지 레이아웃 구조를 개선하고 화면 구성을 간소화했습니다.
    • Cursor 에디터 관련 .gitignore 규칙과 .zip 무시 구간을 정리했습니다.

@juengseulki juengseulki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📋 PR 리뷰

Input, Select, Modal, Pagination, Toast와 Header/Footer를 포함한 공통 컴포넌트 구현 작업 확인했습니다!

👍 좋았던 점

  • Input과 PasswordInput을 forwardRef 기반으로 구현하여 react-hook-form 등 외부 폼 라이브러리와 연결하기 좋은 구조였습니다.
  • Input의 error 상태와 aria-invalid를 함께 처리하여 유효성 검사 결과를 공통된 방식으로 표시할 수 있도록 한 점이 좋았습니다.
  • Select와 Modal을 Compound Component 패턴으로 구성하여 사용하는 화면에서 의미가 잘 드러나는 API를 제공한 점이 좋았습니다.
  • useClickOutside, useIsClient와 같이 여러 공통 컴포넌트에서 반복될 수 있는 동작을 별도 훅으로 분리한 점이 좋았습니다.
  • Modal에 Portal, ESC 닫기, 배경 클릭 닫기, 스크롤 잠금 및 패널 포커스 이동을 적용한 점도 좋았습니다.
  • Header에서 현재 경로를 기준으로 활성 링크와 aria-current를 적용한 점이 좋았습니다.
  • 스타일 대부분이 기존 디자인 토큰 기반 클래스와 CSS 변수를 사용하고 있어, 이번 변경에서 문제로 볼 정도의 색상 하드코딩은 확인되지 않았습니다.

🔍 확인 및 제안

  • Modal의 제목과 설명을 aria-labelledby, aria-describedby로 Dialog에 연결하는 방식을 고려해보면 좋을 것 같습니다.
  • Select에서 방향키 이동과 Enter 선택 등 키보드 조작을 추후 보완하면 좋을 것 같습니다.
  • 모바일 대응은 이미 데스크톱 우선 작업임을 PR에 명시해 두었으므로, 이번 공통 컴포넌트 작업과 반드시 함께 처리해야 하는 수정 사항으로 보이지는 않습니다.
  • Component Color 토큰은 현재처럼 의미 기반 클래스가 이미 생성되고 있다면 단순히 color 접두사를 추가하기보다, 팀에서 사용할 토큰 명명 규칙을 먼저 통일하는 것이 좋을 것 같습니다.

전체적으로 많은 공통 컴포넌트를 한 번에 구현했음에도 역할별 파일 분리와 재사용 구조가 잘 잡혀 있었습니다. 실제 화면에서 안정적으로 활용할 수 있을 것 같습니다. 수고하셨습니다! 😊

Comment thread src/components/common/Modal/ModalMain.tsx
Comment thread src/components/common/Select/SelectMain.tsx

@yooseohyeon yooseohyeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

우선 디자인 시스템이 각잡혀있지 않고 제가 중간중간 수정도 했었는데 빠른 시간 내에 공통 컴포넌트들을 만드시느라 수고하셨습니다! 합성 패턴 등 확장성, 사용성과 접근성을 고려한 점이 느껴졌습니다👍 footer 디자인도 깔끔하고 기존 디자인과 잘 어울리는 것 같아요!

리뷰 수는 많지만 대부분 버튼 내 아이콘에 alt와 aria-hidden이 적용된 부분에 대한 것이라 리뷰 내용 자체는 많지 않습니다!

그런데 혹시 현재 아이콘들의 크기가 잘 제어되나요? 그리고 hover나 disabled 같은 상태에 따라 아이콘 색상이 정상적으로 변경되나요? next/image로 SVG 아이콘을 사용할 경우에 저 두 가지가 잘 되지 않았어서 질문 드립니다. 문제 없이 잘 동작한다면 다행이지만, 잘 되지 않는다면 SVGR 도입 등의 해결 방안이 존재합니다

Comment thread src/components/common/Button/Button.tsx
Comment thread src/components/common/Button/Button.tsx
Comment thread src/components/common/Header/Header.tsx Outdated
Comment thread src/components/common/Header/Header.tsx Outdated
Comment thread src/components/common/Header/Header.tsx
Comment thread src/components/common/Pagination/Pagination.tsx Outdated
Comment thread src/components/common/Pagination/Pagination.tsx Outdated
Comment thread src/components/common/Search/Search.tsx
Comment thread src/components/common/Pagination/Pagination.tsx Outdated
Comment thread src/components/common/Pagination/Pagination.tsx Outdated
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 690c34fd-7586-40d6-a99e-26e0264d5c1e

📥 Commits

Reviewing files that changed from the base of the PR and between 93aa3b0 and 176fbb6.

📒 Files selected for processing (1)
  • src/components/common/Select/SelectMain.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/common/Select/SelectMain.tsx

📝 Walkthrough

Walkthrough

공통 레이아웃에 Header와 Footer를 연결하고, 버튼·폼 입력·모달·Select·Pagination·Toast 등 재사용 UI 컴포넌트와 상호작용 훅, 디자인 토큰을 추가했다. 홈 페이지는 임시 콘텐츠로 단순화했다.

Changes

공통 UI 시스템

Layer / File(s) Summary
상호작용 기반과 디자인 토큰
src/hooks/*, src/styles/tokens.theme.css, .gitignore
외부 클릭·클라이언트 판별·리스트박스 키보드 탐색 훅과 컴포넌트용 색상·그림자·spacing 토큰을 추가하고 Cursor 규칙을 무시하도록 구성했다.
애플리케이션 셸과 공통 내비게이션
src/app/layout.tsx, src/app/page.tsx, src/components/common/Header/*, src/components/common/Footer/*
전역 레이아웃에 Header와 Footer를 배치하고 본문을 세로 flex 컨테이너로 구성했으며, 경로 기반 활성 내비게이션과 로그인 상태별 UI를 추가했다. 홈 페이지는 임시 텍스트로 교체했다.
폼 및 입력 컨트롤
src/components/common/Button/*, src/components/common/Input/*, src/components/common/FormField/*, src/components/common/Search/*
버튼 변형, 오류 상태, 숫자 전용 입력, 비밀번호 표시 전환, Textarea, 검색 입력, 라벨 필드 컴포넌트를 추가했다.
모달 구성과 접근성 상태
src/components/common/Modal/*
포털 기반 모달에 스크롤 잠금, 포커스 트랩, Escape·오버레이 닫기, 제목·설명 ARIA 연결 및 정적 하위 컴포넌트를 추가했다.
선택 및 페이지 탐색
src/components/common/Select/*, src/components/common/Pagination/*
컨텍스트 기반 Select와 옵션, 페이지 번호 축약·생략 구간 선택·키보드 탐색을 구현했다.
알림 피드백
src/components/common/Toast/*
3초 후 자동 닫히며 role="status"aria-live="polite"를 사용하는 Toast를 추가했다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SelectMain
  participant useListboxKeyboardNav
  participant SelectOption
  participant onChange
  User->>SelectMain: 트리거 클릭 또는 키보드 입력
  SelectMain->>useListboxKeyboardNav: listbox 열기와 포커스 이동
  User->>SelectOption: 옵션 선택
  SelectOption->>SelectMain: 선택 값 전달
  SelectMain->>onChange: 선택 값 호출
Loading

Suggested labels: ♻️ refactor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 공통 컴포넌트와 관련 훅·레이아웃 연결까지 포함한 변경을 잘 요약하고 있습니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/create-common-components

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

@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: 10

🧹 Nitpick comments (8)
src/styles/tokens.theme.css (1)

112-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

shadow-select 토큰을 Select에 연결하세요.

src/components/common/Select/SelectMain.tsx가 동일한 그림자 값을 shadow-[...]로 직접 선언하고 있습니다. shadow-select로 교체해 토큰의 단일 관리 지점을 유지하세요.

As per path instructions, Input/Textarea/Select는 --shadow-input--shadow-select 토큰 사용을 확인해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/tokens.theme.css` around lines 112 - 113, Update the Select
styling in SelectMain to use the existing shadow-select theme token instead of
the inline shadow-[...] value, preserving the current visual shadow and
centralized token management.

Source: Path instructions

src/components/common/Modal/ModalTitle.tsx (1)

9-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Props 타입을 인터페이스로 분리해 주세요.

수정 예시
-const ModalTitle = ({ children }: { children: ReactNode }) => {
+interface ModalTitleProps {
+  children: ReactNode;
+}
+
+const ModalTitle = ({ children }: ModalTitleProps) => {

As per coding guidelines, “Props 는 interface XxxProps 형태로 명확히 정의합니다.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Modal/ModalTitle.tsx` at line 9, Define a named
interface following the XxxProps convention for the props accepted by
ModalTitle, including its children field, and update the ModalTitle function
signature to use that interface instead of an inline object type.

Sources: Coding guidelines, Path instructions

src/components/common/Select/SelectMain.tsx (1)

138-142: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

에러 메시지와 combobox 버튼이 aria-describedby로 연결돼있지 않습니다.

Input.tsx, Textarea.tsx와 동일한 패턴이라 아래 consolidated comment에서 함께 다룹니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Select/SelectMain.tsx` around lines 138 - 142, Connect
the error message rendered by SelectMain to the combobox button through
aria-describedby, matching the existing Input.tsx and Textarea.tsx pattern. Add
a stable identifier to the error Text and reference it from the combobox
control, ensuring the attribute is applied only when an error is present.
src/components/common/Search/Search.tsx (3)

13-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

타이포 스타일이 다른 입력 컴포넌트와 다르게 하드코딩되어 있습니다.

Input.tsx는 var(--font-size-*)/var(--line-height-*) 토큰을 쓰는데 Search는 text-sm/text-lg leading-6처럼 raw 값을 사용합니다. 일관성을 위해 동일한 토큰 방식을 쓰는 게 좋아 보입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Search/Search.tsx` around lines 13 - 19, Update the
size variants in the Search component’s styling configuration to use the same
var(--font-size-*) and var(--line-height-*) typography tokens as Input.tsx,
replacing the hardcoded text-sm, text-lg, and leading-6 classes while preserving
the existing width and size behavior.

Source: Path instructions


33-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

검색 아이콘에 aria-hidden이 없고, input 기본 접근 이름도 없습니다.

장식용 아이콘에는 aria-hidden="true"가 필요하고, placeholder는 label을 대체할 수 없으므로 기본 aria-label="검색"을 지정해두면 좋겠습니다.

♿️ 제안
 const Search = forwardRef<HTMLInputElement, SearchProps>(function Search(
-  { size, className, placeholder = "검색", ...props },
+  { size, className, placeholder = "검색", "aria-label": ariaLabel = "검색", ...props },
   ref,
 ) {
   return (
     <div className={cn(searchVariants({ size }), className)}>
-      <span className="text-icon-default">
+      <span className="text-icon-default" aria-hidden="true">
         <SearchIcon className="size-24" />
       </span>
       <input
         ref={ref}
         type="text"
         placeholder={placeholder}
+        aria-label={ariaLabel}
         className="placeholder:text-text-placeholder w-full bg-transparent focus:outline-none"
         {...props}
       />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Search/Search.tsx` around lines 33 - 42, Update the
Search component’s SearchIcon to include aria-hidden="true" and provide the
input with a default aria-label of "검색" while preserving any caller-supplied
aria-label through the existing props spread behavior.

Source: Path instructions


6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

공통 아이콘 경로로 통일해 주세요

공통 Search 컴포넌트가 페이지 전용 estimate 아이콘 모듈을 직접 참조합니다. SearchIcon@/icons로 노출하고 공통 컴포넌트는 공통 아이콘만 의존하게 맞춰 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Search/Search.tsx` around lines 6 - 7, Update the
Search component’s SearchIcon import to use the shared `@/icons` export instead of
the estimate-specific icons module, and ensure SearchIcon is exposed from
`@/icons`. Keep the existing Search component behavior unchanged while removing
its dependency on the estimate icon path.

Source: Path instructions

src/components/common/Input/Textarea.tsx (1)

33-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

에러 메시지와 textarea가 aria-describedby로 연결돼있지 않습니다.

Input.tsx, SelectMain.tsx와 동일한 패턴이라 아래 consolidated comment에서 함께 다룹니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Input/Textarea.tsx` around lines 33 - 37, Update the
Textarea component’s error rendering so the textarea and its error Text element
are connected through aria-describedby, following the existing pattern used by
Input.tsx and SelectMain.tsx. Ensure the referenced identifier is applied to the
error message and passed to the textarea while preserving the current
conditional rendering.
src/components/common/Input/Input.tsx (1)

67-71: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

에러 메시지와 input이 aria-describedby로 연결돼있지 않습니다.

Textarea.tsx, SelectMain.tsx에도 동일한 패턴이 있어 아래 consolidated comment에서 함께 다룹니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Input/Input.tsx` around lines 67 - 71, Update the Input
component and the corresponding Textarea and SelectMain implementations so each
rendered error message has a stable id and the associated control references it
through aria-describedby when an error is present. Preserve existing rendering
and behavior when no error exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 44-45: Update the .gitignore Cursor entry so it no longer ignores
the entire .cursor directory; ignore only local Cursor-generated artifacts while
allowing .cursor/rules/project.mdc and future files under .cursor/rules to
remain trackable.

In `@src/app/layout.tsx`:
- Around line 34-38: Update the page-content wrapper in the root layout around
the Header and Footer to use a semantic main landmark instead of a generic div,
while preserving its existing flex sizing classes and children rendering.

In `@src/components/common/Button/Button.tsx`:
- Around line 7-19: ButtonVariants의 기본 클래스에서 유효하지 않은 h-15 높이 지정으로 인해 Figma 스펙과
다른 높이가 적용됩니다. src/styles/tokens.theme.css에 정의된 유효한 spacing 토큰으로 고정 높이를 맞추거나, 패딩
기준 높이가 의도된 경우 h-* 지정 자체를 제거하세요.

In `@src/components/common/Footer/Footer.tsx`:
- Around line 6-9: Update the FOOTER_LINKS destinations so “자주 묻는 질문” and “문의하기”
point to their actual implemented page paths instead of “/”. If those pages are
not implemented, remove or hide these links until valid destinations exist.

In `@src/components/common/Header/Header.tsx`:
- Around line 28-30: Update the Header component’s responsive layout around the
header content container: use mobile/tablet-appropriate default padding and
spacing so the logo, menu, and login controls fit within narrow screens, then
apply the existing desktop padding and spacing only at the desktop breakpoint.
Keep this as one responsive Header implementation without duplicating
components.
- Line 38: Update the isActive calculation in the Header component to activate a
menu when pathname exactly matches link.href or is a boundary-aware descendant
path such as link.href followed by "/". Avoid treating unrelated prefixes as
active, such as "/movers2" for the "/movers" menu.

In `@src/components/common/Input/Input.tsx`:
- Around line 37-42: Update handleChange in the numericOnly path to preserve the
input caret position when sanitizing event.target.value. Calculate the selection
offset before replacement, adjust it for removed characters occurring before the
original caret, then restore the corrected selection after assigning the
sanitized value; keep the existing onChange behavior unchanged.

In `@src/components/common/Modal/ModalMain.tsx`:
- Around line 60-70: Update the ModalMain useEffect to save the previously
active element before focusing the panel, trap Tab and Shift+Tab navigation
within the modal’s focusable elements, and restore the saved element during
cleanup. Preserve the existing Escape-to-close behavior, and ensure the modal
remains configured with role="dialog", aria-modal, and background scroll
locking.

In `@src/components/common/Select/SelectMain.tsx`:
- Around line 70-82: Update the defaultValue synchronization in SelectMain so
changes after mount update both the selected value and selectedLabel, including
asynchronously loaded edit-form data. Reuse the existing child matching logic
and preserve the initial/default behavior while ensuring the option matching
defaultValue receives the corresponding selected state and aria-selected
indication.

In `@src/styles/tokens.theme.css`:
- Around line 90-110: Update the disabled-date color reference in the Calendar
component from the removed var(--date-disabled) token to the defined
--color-date-disabled token, preserving the existing disabled-date styling
behavior.

---

Nitpick comments:
In `@src/components/common/Input/Input.tsx`:
- Around line 67-71: Update the Input component and the corresponding Textarea
and SelectMain implementations so each rendered error message has a stable id
and the associated control references it through aria-describedby when an error
is present. Preserve existing rendering and behavior when no error exists.

In `@src/components/common/Input/Textarea.tsx`:
- Around line 33-37: Update the Textarea component’s error rendering so the
textarea and its error Text element are connected through aria-describedby,
following the existing pattern used by Input.tsx and SelectMain.tsx. Ensure the
referenced identifier is applied to the error message and passed to the textarea
while preserving the current conditional rendering.

In `@src/components/common/Modal/ModalTitle.tsx`:
- Line 9: Define a named interface following the XxxProps convention for the
props accepted by ModalTitle, including its children field, and update the
ModalTitle function signature to use that interface instead of an inline object
type.

In `@src/components/common/Search/Search.tsx`:
- Around line 13-19: Update the size variants in the Search component’s styling
configuration to use the same var(--font-size-*) and var(--line-height-*)
typography tokens as Input.tsx, replacing the hardcoded text-sm, text-lg, and
leading-6 classes while preserving the existing width and size behavior.
- Around line 33-42: Update the Search component’s SearchIcon to include
aria-hidden="true" and provide the input with a default aria-label of "검색" while
preserving any caller-supplied aria-label through the existing props spread
behavior.
- Around line 6-7: Update the Search component’s SearchIcon import to use the
shared `@/icons` export instead of the estimate-specific icons module, and ensure
SearchIcon is exposed from `@/icons`. Keep the existing Search component behavior
unchanged while removing its dependency on the estimate icon path.

In `@src/components/common/Select/SelectMain.tsx`:
- Around line 138-142: Connect the error message rendered by SelectMain to the
combobox button through aria-describedby, matching the existing Input.tsx and
Textarea.tsx pattern. Add a stable identifier to the error Text and reference it
from the combobox control, ensuring the attribute is applied only when an error
is present.

In `@src/styles/tokens.theme.css`:
- Around line 112-113: Update the Select styling in SelectMain to use the
existing shadow-select theme token instead of the inline shadow-[...] value,
preserving the current visual shadow and centralized token management.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a037eb5c-d026-495d-8e28-09eaf3975fde

📥 Commits

Reviewing files that changed from the base of the PR and between c0b9d55 and 743ddee.

⛔ Files ignored due to path filters (2)
  • public/icons/logo_full.svg is excluded by !**/*.svg
  • src/icons/profile.svg is excluded by !**/*.svg
📒 Files selected for processing (27)
  • .gitignore
  • public/icons/.gitkeep
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/components/common/Button/Button.tsx
  • src/components/common/Footer/Footer.tsx
  • src/components/common/FormField/FormField.tsx
  • src/components/common/Header/Header.tsx
  • src/components/common/Input/Input.tsx
  • src/components/common/Input/PasswordInput.tsx
  • src/components/common/Input/Textarea.tsx
  • src/components/common/Modal/Modal.tsx
  • src/components/common/Modal/ModalClose.tsx
  • src/components/common/Modal/ModalDescription.tsx
  • src/components/common/Modal/ModalMain.tsx
  • src/components/common/Modal/ModalTitle.tsx
  • src/components/common/Pagination/Pagination.tsx
  • src/components/common/Pagination/PaginationEllipsis.tsx
  • src/components/common/Search/Search.tsx
  • src/components/common/Select/Select.tsx
  • src/components/common/Select/SelectMain.tsx
  • src/components/common/Select/SelectOption.tsx
  • src/components/common/Toast/Toast.tsx
  • src/hooks/useClickOutside.ts
  • src/hooks/useIsClient.ts
  • src/hooks/useListboxKeyboardNav.ts
  • src/styles/tokens.theme.css

Comment thread .gitignore Outdated
Comment thread src/app/layout.tsx
Comment thread src/components/common/Button/Button.tsx
Comment thread src/components/common/Footer/Footer.tsx
Comment thread src/components/common/Header/Header.tsx
Comment thread src/components/common/Header/Header.tsx Outdated
Comment thread src/components/common/Input/Input.tsx
Comment thread src/components/common/Modal/ModalMain.tsx
Comment thread src/components/common/Select/SelectMain.tsx Outdated
Comment thread src/styles/tokens.theme.css
@karrum5692

Copy link
Copy Markdown
Contributor

@coderabbitai configuration

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Configuration used: Path: .coderabbit.yaml

CodeRabbit configuration
# Source: Repository YAML (base)
language: ko
# Source: Repository YAML (base)
tone_instructions: 간결하고 근거를 들어 리뷰해 주세요. 취향 차이보다 버그, 접근성, 데이터 정합성, 불필요한 리렌더링을 우선 지적해 주세요.
# Source: defaults
early_access: false
# Source: defaults
enable_free_tier: true
# Source: defaults
inheritance: false
reviews:
  # Source: Repository YAML (base)
  profile: chill
  # Source: Repository YAML (base)
  request_changes_workflow: false
  # Source: Repository YAML (base)
  high_level_summary: true
  # Source: defaults
  high_level_summary_instructions: ''
  # Source: defaults
  high_level_summary_placeholder: '`@coderabbitai` summary'
  # Source: defaults
  high_level_summary_in_walkthrough: false
  # Source: defaults
  auto_title_placeholder: '`@coderabbitai`'
  # Source: defaults
  auto_title_instructions: ''
  # Source: defaults
  review_status: true
  # Source: defaults
  review_details: false
  # Source: defaults
  review_progress: true
  # Source: defaults
  commit_status: true
  # Source: defaults
  fail_commit_status: false
  # Source: Repository YAML (base)
  collapse_walkthrough: true
  # Source: Repository YAML (base)
  changed_files_summary: true
  # Source: Repository YAML (base)
  sequence_diagrams: true
  # Source: defaults
  estimate_code_review_effort: true
  # Source: defaults
  assess_linked_issues: true
  # Source: defaults
  related_issues: true
  # Source: defaults
  related_prs: true
  # Source: defaults
  suggested_labels: true
  # Source: defaults
  labeling_instructions: []
  # Source: defaults
  mutually_exclusive_groups: {}
  # Source: defaults
  auto_apply_labels: false
  # Source: defaults
  suggested_reviewers: true
  # Source: defaults
  auto_assign_reviewers: false
  # Source: defaults
  suggested_reviewers_instructions: []
  # Source: defaults
  in_progress_fortune: true
  # Source: Repository YAML (base)
  poem: false
  # Source: defaults
  enable_prompt_for_ai_agents: true
  # Source: Repository YAML (base)
  path_filters:
    - '!**/package-lock.json'
    - '!**/*.log'
    - '!node_modules/**'
    - '!.next/**'
    - '!out/**'
    - '!build/**'
    - '!next-env.d.ts'
  # Source: Repository YAML (base)
  path_instructions:
    - path: src/**/*.{ts,tsx}
      instructions: |
        이사 견적 매칭 서비스(무빙)의 프론트엔드입니다.
        Next.js 16 App Router, React 19, TypeScript(strict), Tailwind CSS v4,
        TanStack Query, axios, react-hook-form + zod 를 사용합니다.
    
        팀 규칙이 `.cursor/rules/project.mdc` 에 정리되어 있습니다.
        아래는 그중 리뷰에서 특히 확인할 항목입니다.
    
        [공통 규칙]
        - `any` 사용을 금지합니다. 발견하면 반드시 지적해 주세요.
          불가피하면 `unknown` + 타입 가드를 제안해 주세요.
        - 타입 정의는 `interface` 를 우선 사용합니다.
        - import 는 `@/*` 절대 경로를 사용합니다 (tsconfig paths 설정됨).
          같은 폴더 내부 참조만 상대 경로를 허용합니다.
        - 매직 넘버·매직 스트링을 지양합니다. 특히 API 경로는
          `src/lib/constants/apiRoutes.ts` 의 `API_ROUTES` 를 사용해야 합니다.
        - 새 라이브러리 추가는 기존 라이브러리로 해결 가능한지 먼저 확인해야 합니다.
          package.json 에 의존성이 추가되면 대안이 있는지 언급해 주세요.
    
        [지적하지 않아도 되는 것]
        - Prettier(prettier-plugin-tailwindcss 포함)가 자동 정리하는 포맷팅,
          Tailwind 클래스 정렬 순서
        - ESLint(eslint-config-next)가 이미 잡는 규칙
    - path: src/app/**/*.tsx
      instructions: |
        - 기본은 Server Component 입니다. 상태·훅·이벤트 핸들러·브라우저 API 가
          필요할 때만 `"use client"` 를 붙여야 합니다.
        - `"use client"` 는 가능한 말단 컴포넌트에만 두고, 페이지 전체가
          클라이언트 컴포넌트가 되지 않도록 해야 합니다.
          페이지에 `"use client"` 가 붙으면 하위 트리 전체가 클라이언트로
          내려가므로 분리를 제안해 주세요.
        - Server Component 안에서 `useState`, `useEffect` 등 훅을 사용하면
          안 됩니다.
        - `metadata` export 로 페이지별 메타데이터를 설정했는지 확인해 주세요.
        - 클라이언트 번들에 노출되면 안 되는 값이 `NEXT_PUBLIC_` 접두사로
          선언되어 있지 않은지 확인해 주세요.
    - path: src/app/api/**/*.ts
      instructions: |
        서버에서 실행되는 Route Handler 입니다. 외부 API 키를 다루므로
        특히 주의 깊게 봐주세요.
    
        - API 키·시크릿이 클라이언트로 노출될 수 있는 형태인지 확인해 주세요.
          `NEXT_PUBLIC_` 접두사가 붙은 환경변수에 시크릿을 담으면 안 됩니다.
        - 환경변수가 없을 때의 처리가 있는지 확인해 주세요.
        - 외부 API 호출은 실패할 수 있습니다. try/catch 또는
          `Promise.allSettled` 로 처리하고, 사용자에게 의미 있는
          에러 메시지와 적절한 상태 코드를 반환해야 합니다.
        - 쿼리 파라미터 등 외부 입력은 검증 후 사용해야 합니다.
          (프로젝트에 zod 가 있으므로 활용을 제안해 주세요)
        - 외부 API 응답을 그대로 전달하지 말고, 프론트에서 쓰는 형태로
          매핑해야 합니다. 매핑 과정에서 필드가 누락되면
          (예: 빈 문자열로 하드코딩) 백엔드 검증에서 실패할 수 있으므로
          반드시 지적해 주세요.
        - 캐싱 전략(`cache`, `revalidate`)이 데이터 성격에 맞는지 확인해 주세요.
    - path: src/components/**/*.tsx
      instructions: |
        - React Functional Component 만 사용합니다.
        - 컴포넌트는 하나의 역할만 가집니다. 파일이 커지면 분리를 제안해 주세요.
        - UI 와 비즈니스 로직을 분리합니다. 컴포넌트 안에서 직접 API 를
          호출하거나 복잡한 상태 변환을 하면 훅으로 분리하도록 지적해 주세요.
        - Props 는 `interface XxxProps` 형태로 명확히 정의합니다.
    
        [공통 컴포넌트 재사용]
        - 새 컴포넌트를 만들기 전에 `src/components/common/` 의 기존
          컴포넌트를 재사용할 수 있는지 확인해야 합니다.
          현재 `Modal`, `Text` 가 있습니다.
        - 텍스트는 `Text` 컴포넌트의 variant 를 사용합니다.
          (예: `variant="lg-semibold"`) 직접 `text-[16px]` 같은 클래스를
          쓰면 지적해 주세요.
    
        [스타일링]
        - Tailwind 만 사용합니다. `style={{}}` 인라인 스타일과 `!important`
          는 금지입니다.
        - 클래스 조합은 `src/lib/utils/cn.ts` 의 `cn()` 을 사용합니다.
        - 색상·간격·radius 는 `src/styles/tokens.theme.css` 의 디자인 토큰을
          사용해야 합니다. `#f9502e` 같은 하드코딩된 색상값이나
          토큰에 없는 임의 값을 발견하면 지적해 주세요.
          (예: `bg-background-brand`, `text-text-primary` 형태 사용)
        - variant 가 여러 개인 컴포넌트는 `class-variance-authority(cva)` 를
          사용합니다. `Text.tsx` 가 참고 사례입니다.
    
        [아이콘]
        - 아이콘은 `src/icons/index.ts` 에서 import 합니다.
          (예: `import { CloseIcon } from "`@/icons`"`)
        - 컴포넌트 안에 SVG 를 인라인으로 정의하면 기존 아이콘으로
          대체 가능한지 확인하도록 지적해 주세요.
        - 새 아이콘 라이브러리를 설치하지 않습니다.
    
        [접근성]
        - 클릭 가능한 요소는 `<button type="button">` 을 사용합니다.
          `<div onClick>` 은 키보드 접근이 불가하므로 지적해 주세요.
        - 페이지 이동은 `next/link` 의 `Link` 를 사용합니다.
        - 아이콘만 있는 버튼에는 `aria-label` 이 필요합니다.
        - 장식용 SVG 에는 `aria-hidden="true"` 를 붙입니다.
        - input 에는 label 또는 `aria-label` 이 연결되어야 합니다.
        - 모달은 `role="dialog"`, `aria-modal`, 포커스 트랩, ESC 닫기,
          배경 스크롤 잠금이 필요합니다. `Modal.tsx` 가 참고 사례입니다.
    
        [반응형]
        - 하나의 컴포넌트에서 Tailwind 브레이크포인트로 처리합니다.
          화면 크기별로 컴포넌트를 따로 만들면 지적해 주세요.
    
        [성능]
        - 불필요한 리렌더링을 유발하는 구조인지 확인해 주세요.
          (렌더링마다 새로 만들어지는 객체·배열·함수를 자식 props 로 전달 등)
        - 다만 React 19 이므로 과도한 `useMemo`/`useCallback` 남용도
          함께 경계해 주세요. 실제 병목이 있을 때만 제안합니다.
    - path: src/hooks/**/*.{ts,tsx}
      instructions: |
        - 파일명과 함수명은 `use` 로 시작하는 camelCase 입니다.
        - `useEffect` 의 의존성 배열이 올바른지, 정리(cleanup) 함수가
          필요한 경우 반환하는지 확인해 주세요.
        - 서버 데이터는 `useState` + `useEffect` 로 직접 관리하지 않고
          TanStack Query 를 사용합니다.
        - 컴포넌트마다 중복되는 로직이 있으면 훅으로 분리를 제안해 주세요.
    - path: src/lib/api/**/*.ts
      instructions: |
        - 브라우저에서의 API 호출은 `src/lib/api/axiosInstance.ts` 를
          사용합니다. `fetch` 를 직접 쓰면 지적해 주세요.
          (단, Route Handler 내부의 외부 API 호출은 `fetch` 를 사용합니다)
        - 엔드포인트는 `API_ROUTES` 상수를 사용하고, 문자열을 직접
          작성하지 않습니다.
        - 요청/응답 타입을 명시해야 합니다.
    
        [백엔드 연동 시 확인]
        - 백엔드 응답은 `{ success, data }` 또는
          `{ success: false, error: { code, message } }` 형태입니다.
          에러 코드 기반으로 분기해야 하며, 메시지 문자열 비교는 피해 주세요.
        - 백엔드 API 문서는 개발 서버의 `/docs` 에서 확인할 수 있습니다.
    - path: src/lib/**/*.ts
      instructions: |
        - 순수 함수로 작성하고 부수효과를 두지 않습니다.
        - 외부 API 응답을 매핑하는 함수는 필드 누락·null 처리를
          꼼꼼히 확인해 주세요. 값이 없을 때 빈 문자열로 채우면
          이후 검증에서 실패할 수 있습니다.
        - 날짜 처리는 `src/lib/utils/date.ts` 의 기존 유틸을 재사용합니다.
          타임존 때문에 하루가 밀리지 않는지 확인해 주세요.
          (백엔드는 이사 예정일을 "YYYY-MM-DD" 문자열로 받습니다)
    - path: src/lib/constants/**/*.ts
      instructions: |
        - `as const` 를 붙여 리터럴 타입을 유지합니다.
        - TanStack Query 의 쿼리 키는 `queryKeys.ts` 의 `QUERY_KEYS` 를
          사용합니다. 컴포넌트나 훅에서 문자열 배열을 직접 만들면
          캐시 무효화가 어긋나므로 지적해 주세요.
        - 새 엔드포인트를 추가하면 `apiRoutes.ts` 에도 반영되어야 합니다.
    - path: src/types/**/*.ts
      instructions: |
        - 여러 곳에서 쓰이는 타입만 이곳에 둡니다.
          한 컴포넌트에서만 쓰는 타입은 해당 파일에 둡니다.
        - 백엔드 응답 타입은 실제 API 응답과 일치해야 합니다.
    - path: src/styles/**/*.css
      instructions: |
        - 디자인 토큰 정의 파일입니다. Tailwind v4 의 `@theme inline` 을
          사용합니다.
        - 토큰을 추가·변경하면 영향 범위가 넓습니다. 기존 토큰으로
          해결 가능한지 확인해 주세요.
        - primitive(색상 원값)와 semantic(용도) 토큰의 구분을 유지해 주세요.
          컴포넌트는 semantic 토큰을 사용해야 합니다.
    - path: src/app/globals.css
      instructions: |
        - 전역 스타일은 최소한으로 유지합니다.
        - 컴포넌트 단위 스타일은 Tailwind 유틸리티로 처리해야 합니다.
    - path: '{next.config.ts,svgr.config.ts,postcss.config.mjs,eslint.config.mjs}'
      instructions: |
        - 빌드 설정 변경은 팀 전체에 영향을 줍니다. 변경 사유를 함께
          확인해 주세요.
        - `ignoreBuildErrors`, `ignoreDuringBuilds` 같이 검사를 끄는 옵션이
          추가되면 반드시 지적해 주세요.
    - path: .github/**
      instructions: |
        - CI 워크플로에서 시크릿이 로그로 노출되지 않는지 확인해 주세요.
  # Source: defaults
  abort_on_close: true
  # Source: defaults
  disable_cache: false
  slop_detection:
    # Source: defaults
    enabled: true
  auto_review:
    # Source: Repository YAML (base)
    enabled: true
    # Source: defaults
    description_keyword: ''
    # Source: defaults
    auto_incremental_review: true
    # Source: defaults
    auto_pause_after_reviewed_commits: 5
    # Source: defaults
    ignore_title_keywords: []
    # Source: defaults
    labels: []
    # Source: Repository YAML (base)
    drafts: false
    # Source: Repository YAML (base)
    base_branches:
      - dev
      - main
    # Source: defaults
    ignore_usernames: []
  finishing_touches:
    docstrings:
      # Source: defaults
      enabled: true
    unit_tests:
      # Source: defaults
      enabled: true
    simplify:
      # Source: defaults
      enabled: false
    autofix:
      # Source: defaults
      enabled: true
    fix_ci:
      # Source: defaults
      enabled: true
    resolve_merge_conflict:
      # Source: defaults
      enabled: true
    # Source: defaults
    custom: []
  pre_merge_checks:
    # Source: defaults
    override_requested_reviewers_only: false
    docstrings:
      # Source: defaults
      mode: warning
      # Source: defaults
      threshold: 80
    title:
      # Source: defaults
      mode: warning
      # Source: defaults
      requirements: ''
    description:
      # Source: defaults
      mode: warning
    issue_assessment:
      # Source: defaults
      mode: warning
    # Source: defaults
    custom_checks: []
  # Source: defaults
  post_merge_actions: []
  tools:
    ast-grep:
      # Source: defaults
      rule_dirs: []
      # Source: defaults
      util_dirs: []
      # Source: defaults
      essential_rules: true
      # Source: defaults
      packages: []
    shellcheck:
      # Source: defaults
      enabled: true
    ruff:
      # Source: defaults
      enabled: true
    markdownlint:
      # Source: defaults
      enabled: true
    github-checks:
      # Source: defaults
      enabled: true
      # Source: defaults
      timeout_ms: 90000
    languagetool:
      # Source: defaults
      enabled: true
      # Source: defaults
      enabled_rules: []
      # Source: defaults
      disabled_rules: []
      # Source: defaults
      enabled_categories: []
      # Source: defaults
      disabled_categories: []
      # Source: defaults
      enabled_only: false
      # Source: defaults
      level: default
    biome:
      # Source: defaults
      enabled: true
    hadolint:
      # Source: defaults
      enabled: true
    swiftlint:
      # Source: defaults
      enabled: true
    phpstan:
      # Source: defaults
      enabled: true
      # Source: defaults
      level: default
    phpmd:
      # Source: defaults
      enabled: true
    phpcs:
      # Source: defaults
      enabled: true
    golangci-lint:
      # Source: defaults
      enabled: true
    yamllint:
      # Source: defaults
      enabled: true
    gitleaks:
      # Source: defaults
      enabled: true
    trufflehog:
      # Source: defaults
      enabled: true
    checkov:
      # Source: defaults
      enabled: true
    tflint:
      # Source: defaults
      enabled: true
    detekt:
      # Source: defaults
      enabled: true
    eslint:
      # Source: defaults
      enabled: true
      e18e:
        # Source: defaults
        enabled: true
    flake8:
      # Source: defaults
      enabled: true
    fbinfer:
      # Source: defaults
      enabled: true
      # Source: defaults
      enable_java: false
    fortitudeLint:
      # Source: defaults
      enabled: true
    rubocop:
      # Source: defaults
      enabled: true
    buf:
      # Source: defaults
      enabled: true
    regal:
      # Source: defaults
      enabled: true
    actionlint:
      # Source: defaults
      enabled: true
    zizmor:
      # Source: defaults
      enabled: true
    pmd:
      # Source: defaults
      enabled: true
    clang:
      # Source: defaults
      enabled: true
    cppcheck:
      # Source: defaults
      enabled: true
    opengrep:
      # Source: defaults
      enabled: true
    semgrep:
      # Source: defaults
      enabled: true
    circleci:
      # Source: defaults
      enabled: true
    clippy:
      # Source: defaults
      enabled: true
    sqlfluff:
      # Source: defaults
      enabled: true
    squawk:
      # Source: defaults
      enabled: true
    trivy:
      # Source: defaults
      enabled: true
    prismaLint:
      # Source: defaults
      enabled: true
    pylint:
      # Source: defaults
      enabled: true
    oxc:
      # Source: defaults
      enabled: true
    shopifyThemeCheck:
      # Source: defaults
      enabled: true
    luacheck:
      # Source: defaults
      enabled: true
    brakeman:
      # Source: defaults
      enabled: true
    dotenvLint:
      # Source: defaults
      enabled: true
    htmlhint:
      # Source: defaults
      enabled: true
    stylelint:
      # Source: defaults
      enabled: true
    checkmake:
      # Source: defaults
      enabled: true
    osvScanner:
      # Source: defaults
      enabled: true
    oasdiff:
      # Source: defaults
      enabled: true
    reactDoctor:
      # Source: defaults
      enabled: true
    presidio:
      # Source: defaults
      enabled: true
    blinter:
      # Source: defaults
      enabled: true
    smartyLint:
      # Source: defaults
      enabled: true
    emberTemplateLint:
      # Source: defaults
      enabled: true
    skillspector:
      # Source: defaults
      enabled: true
    psscriptanalyzer:
      # Source: defaults
      enabled: true
chat:
  # Source: defaults
  art: true
  # Source: defaults
  allow_non_org_members: true
  # Source: Repository YAML (base)
  auto_reply: true
  integrations:
    jira:
      # Source: defaults
      usage: auto
    linear:
      # Source: defaults
      usage: auto
knowledge_base:
  # Source: defaults
  opt_out: false
  web_search:
    # Source: defaults
    enabled: true
  code_guidelines:
    # Source: Repository YAML (base)
    enabled: true
    # Source: Repository YAML (base)
    filePatterns:
      - .cursor/rules/*.mdc
  learnings:
    # Source: Repository YAML (base)
    scope: local
    # Source: defaults
    approval_delay: 0
  issues:
    # Source: Repository YAML (base)
    scope: local
  jira:
    # Source: defaults
    usage: auto
    # Source: defaults
    project_keys: []
  linear:
    # Source: defaults
    usage: auto
    # Source: defaults
    team_keys: []
  pull_requests:
    # Source: Repository YAML (base)
    scope: local
  mcp:
    # Source: defaults
    usage: auto
    # Source: defaults
    disabled_servers: []
  # Source: defaults
  automatic_repository_linking: false
  # Source: defaults
  linked_repositories: []
code_generation:
  docstrings:
    # Source: defaults
    path_instructions: []
  unit_tests:
    # Source: defaults
    path_instructions: []
issue_enrichment:
  auto_enrich:
    # Source: defaults
    enabled: false
  planning:
    # Source: defaults
    enabled: true
    auto_planning:
      # Source: defaults
      enabled: true
      # Source: defaults
      labels: []
  labeling:
    # Source: defaults
    labeling_instructions: []
    # Source: defaults
    auto_apply_labels: false

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/common/Modal/ModalMain.tsx (1)

125-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

이름 없는 dialog를 허용하지 않도록 해 주세요.

hasTitle이 false이고 ariaLabel도 없으면 role="dialog"에 접근 가능한 이름이 없습니다. Modal.Title과 비어 있지 않은 aria-label 중 하나를 반드시 제공하도록 API 또는 런타임 검증을 추가해 주세요.

As per coding guidelines, 모달의 aria 속성과 접근성 상태를 확인해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Modal/ModalMain.tsx` around lines 125 - 132, Ensure the
dialog rendered by the ModalMain component always has an accessible name:
require Modal.Title or a non-empty ariaLabel through the API or runtime
validation. Update the existing hasTitle/ariaLabel handling around the dialog
element, while preserving aria-labelledby for titled modals and aria-label for
valid untitled modals.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/common/Modal/ModalMain.tsx`:
- Around line 18-20: Update FOCUSABLE_SELECTOR and the modal focus-trap logic to
exclude hidden inputs, [hidden] elements, and elements not rendered or visibly
focusable, while retaining only actually displayed focusable elements for
first/last Tab navigation. Ensure the filtering is applied before selecting trap
boundaries so hidden elements cannot disrupt keyboard cycling.

---

Outside diff comments:
In `@src/components/common/Modal/ModalMain.tsx`:
- Around line 125-132: Ensure the dialog rendered by the ModalMain component
always has an accessible name: require Modal.Title or a non-empty ariaLabel
through the API or runtime validation. Update the existing hasTitle/ariaLabel
handling around the dialog element, while preserving aria-labelledby for titled
modals and aria-label for valid untitled modals.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d4be3f3b-42c9-426e-8a88-e1edb8847c0c

📥 Commits

Reviewing files that changed from the base of the PR and between 743ddee and f40af3f.

⛔ Files ignored due to path filters (1)
  • public/icons/profile-default.svg is excluded by !**/*.svg
📒 Files selected for processing (6)
  • .gitignore
  • src/app/layout.tsx
  • src/components/common/Button/Button.tsx
  • src/components/common/Header/Header.tsx
  • src/components/common/Input/Input.tsx
  • src/components/common/Modal/ModalMain.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/common/Header/Header.tsx
  • src/components/common/Button/Button.tsx
  • src/app/layout.tsx
  • src/components/common/Input/Input.tsx
  • .gitignore

Comment thread src/components/common/Modal/ModalMain.tsx

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/common/Select/SelectMain.tsx (1)

99-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

에러 메시지를 Select 트리거와 연결해 주세요.

현재 트리거에는 aria-invalid만 있고, 에러 텍스트에는 id가 없어 스크린 리더가 어떤 입력의 오류인지 알 수 없습니다. aria-describedby와 에러 요소의 id를 연결해야 합니다.

수정 제안
+  const errorId = useId();

   <button
     ...
     aria-invalid={!!error}
+    aria-describedby={error ? errorId : undefined}
   >

   {error && (
-    <Text variant="xs-regular" className="text-text-error">
+    <Text id={errorId} variant="xs-regular" className="text-text-error">
       {error}
     </Text>
   )}

As per path instructions: src/components/**/*.tsx에서는 접근성 의미 구조와 ARIA 속성을 확인해야 합니다.

Also applies to: 141-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Select/SelectMain.tsx` around lines 99 - 107, Update
the Select trigger button in SelectMain to include aria-describedby referencing
the associated error message element, and assign that error element a stable
matching id. Preserve the existing aria-invalid behavior and ensure the
description association is only present when the error message is rendered.

Source: Path instructions

🧹 Nitpick comments (1)
src/components/common/Select/SelectMain.tsx (1)

112-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Select 그림자에 shadow-select 토큰을 사용하세요.

열린 트리거에는 새 선택 상태 그림자 토큰이 적용되지 않고, 드롭다운에는 하드코딩된 arbitrary shadow가 사용됩니다. 테마 변경과 디자인 일관성을 위해 shadow-select로 통일해 주세요.

수정 제안
-              isOpen && "border-border-brand bg-background-brand-muted text-text-brand",
+              isOpen &&
+                "border-border-brand bg-background-brand-muted text-text-brand shadow-select",
...
-                "border-border-default border shadow-[4px_4px_10px_0px_rgba(224,224,224,0.25)]",
+                "border-border-default border shadow-select",

As per path instructions: src/components/**/*.tsx에서는 기존 디자인 토큰을 사용하고 임의의 스타일 값을 최소화해야 합니다.

Also applies to: 133-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Select/SelectMain.tsx` around lines 112 - 114, Update
the SelectMain trigger and dropdown styling to use the existing shadow-select
design token consistently. Add the token to the open trigger state and replace
the dropdown’s arbitrary shadow value with the same token, preserving the
existing border, background, text, and error classes.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/common/Select/SelectMain.tsx`:
- Around line 81-86: Update the defaultValue change handling in SelectMain so an
empty or undefined defaultValue explicitly clears the selected state, while
retaining the existing assignment for non-empty values. Ensure the reset also
removes the stale aria-selected state when the selection is cleared.

---

Outside diff comments:
In `@src/components/common/Select/SelectMain.tsx`:
- Around line 99-107: Update the Select trigger button in SelectMain to include
aria-describedby referencing the associated error message element, and assign
that error element a stable matching id. Preserve the existing aria-invalid
behavior and ensure the description association is only present when the error
message is rendered.

---

Nitpick comments:
In `@src/components/common/Select/SelectMain.tsx`:
- Around line 112-114: Update the SelectMain trigger and dropdown styling to use
the existing shadow-select design token consistently. Add the token to the open
trigger state and replace the dropdown’s arbitrary shadow value with the same
token, preserving the existing border, background, text, and error classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca6c0d2b-7b56-4219-b730-882b4e69cf8f

📥 Commits

Reviewing files that changed from the base of the PR and between f40af3f and 93aa3b0.

📒 Files selected for processing (2)
  • src/components/common/Select/SelectMain.tsx
  • src/components/common/Select/SelectOption.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/common/Select/SelectOption.tsx

Comment thread src/components/common/Select/SelectMain.tsx
Comment on lines +65 to +66
useEffect(() => {
panelRef.current?.focus();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

첫 렌더 시점엔 패널이 마운트 전(isMounted === false)이라 focus()가 실행돼도 포커스가 이동하지 않을 수 있을 것 같습니다. 마우스로 동작할땐 문제가 없을 것 같은데, 키보드나 스크린리더 사용일 때 문제가 될 수 있습니다.

모달을 연 직후에 콘솔에서 document.activeElement가 패널인지 확인해보시고, 아니라면 deps에 isMounted 추가를 권장드립니다!

@wndnjs2037

Copy link
Copy Markdown
Collaborator

--color- 접두사는 추가하는 방향이 맞을 것 같습니다!
Tailwind v4는 @theme 안의 변수 이름을 보고 유틸리티 클래스를 만들어주는데, --color-로 시작해야 색상 변수로 인식해서 bg-*, text-*, border-* 클래스가 자동으로 생깁니다. 접두사가 없으면 bg-[var(--toast-background)]처럼 길게 써야 하지만, 지금처럼 --color-toast-background로 선언하면 bg-toast-background로 바로 쓸 수 있습니다.

구현하느라 고생하셨습니다 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants