[FEAT] 공통 컴포넌트 구현 - #8
Conversation
…into feature/create-common-components
juengseulki
left a comment
There was a problem hiding this comment.
📋 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접두사를 추가하기보다, 팀에서 사용할 토큰 명명 규칙을 먼저 통일하는 것이 좋을 것 같습니다.
전체적으로 많은 공통 컴포넌트를 한 번에 구현했음에도 역할별 파일 분리와 재사용 구조가 잘 잡혀 있었습니다. 실제 화면에서 안정적으로 활용할 수 있을 것 같습니다. 수고하셨습니다! 😊
There was a problem hiding this comment.
우선 디자인 시스템이 각잡혀있지 않고 제가 중간중간 수정도 했었는데 빠른 시간 내에 공통 컴포넌트들을 만드시느라 수고하셨습니다! 합성 패턴 등 확장성, 사용성과 접근성을 고려한 점이 느껴졌습니다👍 footer 디자인도 깔끔하고 기존 디자인과 잘 어울리는 것 같아요!
리뷰 수는 많지만 대부분 버튼 내 아이콘에 alt와 aria-hidden이 적용된 부분에 대한 것이라 리뷰 내용 자체는 많지 않습니다!
그런데 혹시 현재 아이콘들의 크기가 잘 제어되나요? 그리고 hover나 disabled 같은 상태에 따라 아이콘 색상이 정상적으로 변경되나요? next/image로 SVG 아이콘을 사용할 경우에 저 두 가지가 잘 되지 않았어서 질문 드립니다. 문제 없이 잘 동작한다면 다행이지만, 잘 되지 않는다면 SVGR 도입 등의 해결 방안이 존재합니다
…into feature/create-common-components
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough공통 레이아웃에 Header와 Footer를 연결하고, 버튼·폼 입력·모달·Select·Pagination·Toast 등 재사용 UI 컴포넌트와 상호작용 훅, 디자인 토큰을 추가했다. 홈 페이지는 임시 콘텐츠로 단순화했다. Changes공통 UI 시스템
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: 선택 값 호출
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 valueProps 타입을 인터페이스로 분리해 주세요.
수정 예시
-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
⛔ Files ignored due to path filters (2)
public/icons/logo_full.svgis excluded by!**/*.svgsrc/icons/profile.svgis excluded by!**/*.svg
📒 Files selected for processing (27)
.gitignorepublic/icons/.gitkeepsrc/app/layout.tsxsrc/app/page.tsxsrc/components/common/Button/Button.tsxsrc/components/common/Footer/Footer.tsxsrc/components/common/FormField/FormField.tsxsrc/components/common/Header/Header.tsxsrc/components/common/Input/Input.tsxsrc/components/common/Input/PasswordInput.tsxsrc/components/common/Input/Textarea.tsxsrc/components/common/Modal/Modal.tsxsrc/components/common/Modal/ModalClose.tsxsrc/components/common/Modal/ModalDescription.tsxsrc/components/common/Modal/ModalMain.tsxsrc/components/common/Modal/ModalTitle.tsxsrc/components/common/Pagination/Pagination.tsxsrc/components/common/Pagination/PaginationEllipsis.tsxsrc/components/common/Search/Search.tsxsrc/components/common/Select/Select.tsxsrc/components/common/Select/SelectMain.tsxsrc/components/common/Select/SelectOption.tsxsrc/components/common/Toast/Toast.tsxsrc/hooks/useClickOutside.tssrc/hooks/useIsClient.tssrc/hooks/useListboxKeyboardNav.tssrc/styles/tokens.theme.css
|
@coderabbitai configuration |
|
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
|
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
public/icons/profile-default.svgis excluded by!**/*.svg
📒 Files selected for processing (6)
.gitignoresrc/app/layout.tsxsrc/components/common/Button/Button.tsxsrc/components/common/Header/Header.tsxsrc/components/common/Input/Input.tsxsrc/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
There was a problem hiding this comment.
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 winSelect 그림자에
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
📒 Files selected for processing (2)
src/components/common/Select/SelectMain.tsxsrc/components/common/Select/SelectOption.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/common/Select/SelectOption.tsx
| useEffect(() => { | ||
| panelRef.current?.focus(); |
There was a problem hiding this comment.
첫 렌더 시점엔 패널이 마운트 전(isMounted === false)이라 focus()가 실행돼도 포커스가 이동하지 않을 수 있을 것 같습니다. 마우스로 동작할땐 문제가 없을 것 같은데, 키보드나 스크린리더 사용일 때 문제가 될 수 있습니다.
모달을 연 직후에 콘솔에서 document.activeElement가 패널인지 확인해보시고, 아니라면 deps에 isMounted 추가를 권장드립니다!
|
구현하느라 고생하셨습니다 😃 |
📋 작업 내용
Input/Textarea계열은forwardRef+errorprop 기반으로 리팩터링하여react-hook-form/zod등 외부 검증 로직과 결합하기 쉽도록 설계하였습니다.Select,Modal을 Compound Component 패턴(합성 컴포넌트)으로 구현했습니다. (Select.Option,Modal.Title/Modal.Desc/Modal.Close/Modal.Button)Header/Footer공통 레이아웃 컴포넌트 작성 및layout.tsx에 연결하였고,Header는 로그인 여부(isLoginprop, 추후 실제 auth 연동 전 임시)에 따라 네비게이션 분기 및 현재 경로 기준 활성 링크 스타일 적용하였습니다.useClickOutside,useIsClient추가하였습니다. (드롭다운 외부 클릭 감지, SSR-safe 클라이언트 감지 -> client 임을 확인하는 과정)--shadow-select토큰 추가하였습니다.🔥 변경 사항
Button,Input,PasswordInput,Textarea,Search,Select,Modal,Pagination,FormField,Toast,Footer,Header등 12개 공통 컴포넌트 추가하였습니다.src/app/layout.tsx에Header/Footer추가 적용하였습니다.next/image기반 아이콘 에셋 적용하였습니다.✅ 체크리스트
📷 스크린샷 (선택)
💬 To Reviewer
Summary by CodeRabbit
Summary by CodeRabbit
.gitignore규칙과.zip무시 구간을 정리했습니다.