Skip to content

Conversation

@leeleeleeleejun
Copy link
Member

@leeleeleeleejun leeleeleeleejun commented Aug 14, 2025

#️⃣연관된 이슈

📝작업 내용

  • Storybook configuration 수정 5d578fc

    • 컴포넌트의 관련된 로직들의 응집도를 높이기 위해 stories 파일 또한 각 컴포넌트의 폴더의 위치할 수 있도록 설정했습니다.
    • stories 위치 경로 수정 (packages/ui/src)
    • staticDirs 경로 수정(통일) (/packages/ui/public)
    • storybook 에서 tailwind 사용 설정
  • 다형성 컴포넌트 타입 정의 추가 713f198

    • 동일한 디자인의 컴포넌트를 다양한 HTML 요소로 렌더링하면서, 각 요소 고유의 프로퍼티까지 타입 안전하게 지원합니다.
    • 관련 포스트: Type-Safe하게 다형성 지원하기
  • 공통 Flex 컴포넌트 & 스토리북 구현 e030c72

    • 자주 사용하는 Flex의 속성을 선언적으로 재사용할 수 있도록 컴포넌트로 구현 했습니다.
  • Typography 컴포넌트 & 스토리북 구현 994d56a

  • Icon 컴포넌트 & 스토리북 추가 f8dcc6a

  • SearchBar 컴포넌트 & 스토리북 구현 57f31a9

  • Header 컴포넌트 & 스토리북 구현 36f9d3f

  • Textarea 컴포넌트 & 스토리북 구현 82dd54d

스크린샷 (선택)

🚀storybook: https://688ef65dab8c9268d9218107-lhmbhqcevi.chromatic.com/

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

  • New Features
    • 새로운 UI 컴포넌트 추가: 버튼(다형), 칩(토글), 헤더(좌/중앙/우 슬롯 및 좌측 전용), 아이콘 세트, 레이아웃 프리미티브(Flex/Column 등), 텍스트(타이포그래피), 텍스트에어리어(자동 높이/글자수 표시), 검색바.
    • Storybook에 스타일링 애드온 도입 및 UI 패키지 스토리 정비.
  • Changes
    • 웹 앱 기본 레이아웃 중앙 정렬 컨테이너로 개편; 랜딩 페이지를 간소화한 플레이스홀더로 교체.
    • 공통 스타일에 Tailwind 설정 통합, 스크롤바 숨김 유틸과 테마 색상 추가.
  • Chores
    • 개발 의존성/ESLint 설정 업데이트 및 정리.

@leeleeleeleejun leeleeleeleejun self-assigned this Aug 14, 2025
@leeleeleeleejun leeleeleeleejun added ⚙️ Chore 환경 셋팅 🎨 Style 스타일 관련 labels Aug 14, 2025
@leeleeleeleejun leeleeleeleejun linked an issue Aug 14, 2025 that may be closed by this pull request
8 tasks
@leeleeleeleejun leeleeleeleejun added the ✨ Feature 기능 구현 label Aug 14, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 14, 2025

Walkthrough

Storybook is reconfigured to load UI package stories with Tailwind styling, and template example stories are removed. The web app layout and globals are simplified and wired to new UI components. A substantial UI library expansion adds polymorphic layout, typography, buttons, chips, icons, search bar, and textarea, plus Tailwind config updates and ESLint adjustments.

Changes

Cohort / File(s) Summary
Storybook config (apps/storybook)
apps/storybook/.storybook/main.ts, apps/storybook/.storybook/preview.ts, apps/storybook/package.json, apps/storybook/postcss.config.mjs, apps/storybook/tsconfig.json
Stories glob narrowed to UI package; adds addon-styling-webpack with Tailwind PostCSS; imports global styles; updates devDeps; removes local postcss config; adds TS path for styles.
Remove Storybook templates
apps/storybook/stories/*
Deletes example Button/Header/Page components, stories, and their CSS assets.
Web app tweaks
apps/web/app/globals.css, apps/web/app/layout.tsx, apps/web/app/page.tsx, apps/web/eslint.config.js, apps/web/tsconfig.json
Globals reduced to Tailwind imports; layout wraps children with new Column and centered container; home page replaced with minimal Flex layout; disables prop-types for TS; adds UI paths.
Tailwind shared styles
packages/tailwind-config/shared-styles.css
Adds --color-red and .scrollbar-hide utility.
UI infra and cleanup
packages/ui/eslint.config.js, packages/ui/eslint.config.mjs, packages/ui/package.json, packages/ui/src/polymorphics.ts, packages/ui/tailwind.config.ts, packages/ui/src/styles.css, packages/ui/src/card.tsx, packages/ui/src/gradient.tsx, packages/ui/src/turborepo-logo.tsx
Switches ESLint config to JS with TS override; adds Storybook Next.js devDep; introduces polymorphic types; adds Tailwind config and stylesheet @config; removes legacy Card, Gradient, TurborepoLogo.
UI Layout components
packages/ui/src/components/Layout/*
Adds polymorphic Flex, Column, JustifyAround/Between/End with barrels and stories.
UI Typography
packages/ui/src/components/Text/*
Adds polymorphic Text component with variants and stories; barrel export.
UI Button
packages/ui/src/components/Button/*
Adds polymorphic Button with size/typography consts, stories, and barrel export.
UI Chip
packages/ui/src/components/Chip/*
Adds toggleable polymorphic Chip component, stories, and barrel export.
UI SearchBar
packages/ui/src/components/SearchBar/*
Adds SearchBar component, story, and barrel export.
UI Textarea
packages/ui/src/components/Textarea/*
Adds controlled auto-resizing Textarea, stories, and barrel export.
UI Icon system
packages/ui/src/components/Icon/*, .../assets/icons/**/*
Adds Icon component, centralized IconMap/types/list, and numerous SVG asset components (menu, navigation, header, tags, etc.); adds stories and barrel export.

Sequence Diagram(s)

sequenceDiagram
  participant App as apps/web
  participant UI as @repo/ui (Layout/Text/Button/...)
  participant SB as Storybook

  App->>UI: Import Layout (Column/Flex) and components
  App->>App: Render layout using UI primitives
  SB->>UI: Load stories from packages/ui/src/components/**/*
  SB->>SB: Apply Tailwind via styling addon
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

꼬리 톡톡, 깡충 뛰는 토끼 개발자
스토리북 길 정리하고, 새 UI 꽃 피웠다!
칩과 버튼, 아이콘이 반짝반짝 ✨
플렉스 줄 맞춰 헤더도 반듯반듯
바람 따라 Tailwind 휘이익—출근 완료 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#7

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

@github-actions
Copy link

@github-actions
Copy link

@leeleeleeleejun leeleeleeleejun marked this pull request as ready for review August 15, 2025 11:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 32

🔭 Outside diff range comments (1)
packages/ui/src/components/Icon/assets/icons/menu/bunsik.tsx (1)

1-49: rest props 미전달 + clipPath ID 충돌 가능성 — 접근성/스타일/이벤트 전달 및 안정성 개선 필요

  • 현재 {...props}가 SVG에 전달되지 않아 className, onClick, aria-* 등을 사용할 수 없습니다.
  • 고정 clipPath id(clip0_116_3529)는 다중 렌더링 시 충돌 위험이 있습니다.
    두 가지를 함께 보완하는 수정을 제안드립니다.

수정 예시:

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Bunsik = ({ width, height }: SVGProps<SVGSVGElement>) => (
-  <svg
+export const Bunsik = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId()
+  return (
+  <svg
     width={width ?? 40}
     height={height ?? 40}
     viewBox='0 0 41 42'
     fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-  >
-    <g clipPath='url(#clip0_116_3529)'>
+    xmlns='http://www.w3.org/2000/svg'
+    {...props}
+  >
+    <g clipPath={`url(#${clipPathId})`}>
@@
-    <defs>
-      <clipPath id='clip0_116_3529'>
+    <defs>
+      <clipPath id={clipPathId}>
         <rect
           width='40'
           height='40'
           fill='white'
           transform='translate(0.797485 0.943893) rotate(0.106333)'
         />
       </clipPath>
     </defs>
-  </svg>
-)
+  </svg>
+)}
♻️ Duplicate comments (1)
packages/ui/src/components/Layout/JustifyEnd/JustifyEnd.tsx (1)

13-16: 여기도 'as' prop을 그대로 전달하지 마세요 (Column과 동일 이슈)

DOM에 불필요한 속성이 내려가거나, Next.js Link 등과 충돌 가능성이 있습니다. 내부에서만 소모하세요.

아래처럼 제거해 주세요.

   return (
-    <Component
-      as={as}
+    <Component
       className={cn('ui:flex ui:items-center ui:justify-end', className)}
       {...restProps}
     >
🧹 Nitpick comments (105)
packages/ui/eslint.config.js (3)

3-3: Flat Config 타입 주석 수정 필요 (Linter.Config → Linter.FlatConfig[])
Flat Config 배열을 export하고 있으므로 JSDoc 타입은 Linter.FlatConfig[]가 맞습니다. 잘못된 타입은 에디터 인텔리센스를 혼동시킬 수 있습니다.

-/** @type {import("eslint").Linter.Config} */
+/** @type {import("eslint").Linter.FlatConfig[]} */

4-12: (선택) 동일 override 중복 제거를 위해 공통 설정으로 승격 제안
apps/web/eslint.config.js에도 동일한 override가 있습니다. 향후 일관성 유지를 위해 @repo/eslint-config/react-internal(또는 공통 베이스)에서 이 override를 포함하도록 올리는 방안을 고려해 보세요.


7-7: (사소) .mts/.cts 파일도 lint 대상으로 둘 계획이라면 glob 확장
Node ESM TS(.mts/.cts)를 쓸 가능성이 있다면 미리 포함해 두는 게 안전합니다.

-    files: ['**/*.{ts,tsx}'],
+    files: ['**/*.{ts,tsx,mts,cts}'],
apps/web/eslint.config.js (1)

3-3: Flat Config 타입 주석 수정 필요 (Linter.Config → Linter.FlatConfig[])
Flat Config 기반 설정이므로 타입 주석을 FlatConfig 배열로 교체해 주세요.

-/** @type {import("eslint").Linter.Config} */
+/** @type {import("eslint").Linter.FlatConfig[]} */
packages/ui/package.json (1)

30-30: UI 패키지에 @storybook/nextjs devDependency 추가는 중복 가능성 — 스토리북 앱으로 이동 권장

스토리북 환경은 apps/storybook에 독립적으로 구성되어 있어, UI 패키지(devDependencies)에 @storybook/nextjs를 추가할 필요가 없어 보입니다. 워크스페이스 전체 설치 관점에서 중복 설치/해결이 발생할 수 있으니 apps/storybook 쪽에만 두는 것을 권장합니다.

적용 예시:

   "devDependencies": {
     "@repo/eslint-config": "workspace:*",
     "@repo/tailwind-config": "workspace:*",
     "@repo/typescript-config": "workspace:*",
-    "@storybook/nextjs": "^9.1.0",
     "@tailwindcss/cli": "^4.1.5",
     "@types/react": "^19.1.0",
     "eslint": "^9.32.0",
     "tailwindcss": "^4.1.5",
     "typescript": "5.8.2"
   },
apps/web/tsconfig.json (1)

13-15: 웹 앱에서 UI 내부 소스 경로로의 깊은 import는 결합도를 높입니다 — 패키지 퍼블릭 API 사용 권장

"@repo/ui/components/*"로 packages/ui/src/components/*를 직접 가리키면, UI 패키지의 내부 구조 변경이 앱에 바로 파급됩니다. 가능하면 @repo/ui의 퍼블릭 export를 통해 가져오도록 하고, 필요한 모듈이 노출되어 있지 않다면 UI 패키지의 exports(또는 배럴)에서 노출해 주세요.

선호 순서:

  1. 앱에서 import '@repo/ui' 경로만 사용 (최선)
  2. 불가피하면 범위를 components/가 아니라 '@repo/ui/'로 잡아 UI 패키지 내부 레벨 전반을 덜 특정화

예시(차선책):

-      "@repo/ui/components/*": ["../../packages/ui/src/components/*"]
+      "@repo/ui/*": ["../../packages/ui/src/*"]

추가로, 앱 코드 어딘가에서 @repo/ui와 @repo/ui/components/*를 혼용하면 번들 중복/트리셰이킹 저하가 생길 수 있으니 한 경로 체계로 통일하는 것을 권장합니다.

apps/storybook/package.json (1)

14-15: (선택) 중복 devDependency 정리 제안

@storybook/nextjs는 스토리북 앱에 존재하면 충분합니다. UI 패키지에도 동일 항목이 devDeps로 추가되어 있다면(UI package.json 참조), 하나로 정리해도 됩니다.

packages/ui/src/components/Icon/assets/icons/header/heart.tsx (3)

3-3: 이름 충돌 가능성: 동일한 이름 Heart 아이콘이 navigation/heart.tsx에도 존재

navigation/heart.tsx에도 Heart가 네임드로 존재합니다. 아이콘 레지스트리/맵에서 와일드카드 import 또는 배럴 통합 시 충돌/혼동이 발생할 수 있습니다. 용도에 따라 명확히 구분되는 이름(예: HeaderHeart)으로 내보내거나, IconMap에서 명시 alias를 강제하세요.

예시:

-export const Heart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
+export const HeaderHeart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
   <svg

또는 IconMap.ts에서 import { Heart as HeaderHeart } … 형태로 일관 alias를 보장.


3-16: color prop 수용으로 일관성/재사용성 향상 (navigation/heart와 동작 통일)

navigation/heart는 color prop을 지원합니다. header/heart도 color를 받아 기본 색상을 override할 수 있도록 하면 테마/상태별 표현이 쉬워집니다.

적용 예시:

-import { SVGProps } from 'react'
+import { SVGProps } from 'react'

-export const Heart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
+export const Heart = ({
+  width,
+  height,
+  color,
+  ...props
+}: SVGProps<SVGSVGElement>) => (
   <svg
     width={width ?? 26}
     height={height ?? 26}
     viewBox='0 0 27 27'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >
     <g clipPath='url(#clip0_76_1134)'>
       <path
-        d='M11.8424 22.4952C12.5457 22.96 13.4538 22.96 14.1571 22.4952C16.3898 21.0197 21.2557 17.4954 23.3506 13.5506C26.1138 8.34665 22.869 3.1564 18.5809 3.1564C16.1363 3.1564 14.6654 4.43365 13.8522 5.5315C13.4239 6.11 12.5769 6.11 12.1479 5.5315C11.3348 4.43365 9.8638 3.1564 7.41915 3.1564C3.1311 3.1564 -0.113699 8.34665 2.64945 13.5506C4.74375 17.4948 9.60965 21.0197 11.8424 22.4952Z'
-        fill='#EF4452'
+        d='M11.8424 22.4952C12.5457 22.96 13.4538 22.96 14.1571 22.4952C16.3898 21.0197 21.2557 17.4954 23.3506 13.5506C26.1138 8.34665 22.869 3.1564 18.5809 3.1564C16.1363 3.1564 14.6654 4.43365 13.8522 5.5315C13.4239 6.11 12.5769 6.11 12.1479 5.5315C11.3348 4.43365 9.8638 3.1564 7.41915 3.1564C3.1311 3.1564 -0.113699 8.34665 2.64945 13.5506C4.74375 17.4948 9.60965 21.0197 11.8424 22.4952Z'
+        fill={color || '#EF4452'}
       />
     </g>

12-22: clipPath id 충돌 가능성 — useId로 고유 id 생성 권장

여러 아이콘 인스턴스를 동시에 렌더링하면 clipPath id가 충돌할 수 있습니다. React의 useId로 고유 id를 만들어 적용하면 안전합니다.

예시:

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'

-export const Heart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
-  <svg
+export const Heart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
+    <svg
       ...
-    <g clipPath='url(#clip0_76_1134)'>
+    <g clipPath={`url(#${clipId})`}>
       ...
-      <clipPath id='clip0_76_1134'>
+      <clipPath id={clipId}>
         <rect width='26' height='26' fill='white' />
       </clipPath>
     </defs>
-  </svg>
-)
+    </svg>
+  )
+}
packages/ui/src/components/Icon/assets/icons/navigation/user.tsx (2)

17-20: 빈 문자열을 기본값으로 잘못 처리하지 않도록 null 병합 연산자 사용 권장

color || '#B0B9C2'는 빈 문자열('')을 전달했을 때도 기본값으로 대체됩니다. 의도한 케이스가 아니라면 ??로 바꿔주는 것이 안전합니다.

-      fill={color || '#B0B9C2'}
+      fill={color ?? '#B0B9C2'}

9-16: SVG 접근성 기본 속성 추가 제안

아이콘이 장식 목적일 때는 스크린리더에서 숨기고(aria-hidden), 대체 텍스트를 제공할 때는 aria-label로 노출하는 패턴이 일반적입니다. 상위 Icon 컴포넌트가 이미 처리 중이라면 무시하셔도 됩니다.

   <svg
+    role='img'
+    aria-hidden={props['aria-label'] ? undefined : true}
     width={width ?? 26}
     height={height ?? 26}
     viewBox='0 0 27 27'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >

확인 요청: 상위 Icon 컴포넌트(예: packages/ui/src/components/Icon/Icon.tsx)에서 접근성 속성을 일괄 관리하고 있는지 확인해주세요. 중복 설정을 피할 수 있습니다.

packages/ui/src/components/Icon/assets/icons/note.tsx (1)

4-11: SVG 접근성 속성 기본값 추가 제안

장식용 아이콘이면 스크린리더에서 숨기고, 텍스트 의미가 있다면 aria-label을 제공하는 패턴을 권장합니다. 멀티컬러 아이콘이라 색상 prop은 불필요해 보입니다.

  <svg
+   role='img'
+   aria-hidden={props['aria-label'] ? undefined : true}
    width={width ?? 24}
    height={height ?? 24}
    viewBox='0 0 17 17'
    fill='none'
    xmlns='http://www.w3.org/2000/svg'
    {...props}
  >
packages/tailwind-config/shared-styles.css (1)

17-21: 중첩 셀렉터 사용 대신 납작한 셀렉터로 호환성 강화 제안

현대 브라우저는 CSS Nesting을 지원하지만, 빌드 파이프라인/도구에 따라 파싱 이슈가 발생할 수 있습니다. 간단히 평탄화하면 리스크를 줄일 수 있습니다.

-.scrollbar-hide {
-  &::-webkit-scrollbar {
-    display: none;
-  }
-  scrollbar-width: none;
-}
+.scrollbar-hide::-webkit-scrollbar {
+  display: none;
+}
+.scrollbar-hide {
+  scrollbar-width: none;
+}
packages/ui/src/components/Icon/assets/icons/fire.tsx (1)

4-11: SVG 접근성 속성 기본값 추가 제안

다른 아이콘과 동일하게 접근성 속성 기본값을 추가하면 일관성을 높일 수 있습니다. 상위 Icon 컴포넌트에서 일괄 처리 중이면 생략하세요.

  <svg
+   role='img'
+   aria-hidden={props['aria-label'] ? undefined : true}
    width={width ?? 24}
    height={height ?? 24}
    viewBox='0 0 25 25'
    fill='none'
    xmlns='http://www.w3.org/2000/svg'
    {...props}
  >
packages/ui/src/components/Icon/assets/icons/arrowLeft.tsx (1)

21-21: 단색 아이콘은 currentColor를 기본값으로 두어 테마/텍스트 컬러와 동기화하세요

CSS의 color로 제어 가능하도록 기본값을 currentColor로 두는 편이 재사용성과 일관성에 유리합니다.

아래처럼 수정 제안드립니다:

-      fill={color || '#6C7887'}
+      fill={color ?? 'currentColor'}
packages/ui/src/components/Icon/assets/icons/navigation/map.tsx (1)

19-19: 단색 아이콘은 currentColor 기본 채택 권장

테마/텍스트 컬러에 자연스럽게 대응되도록 currentColor 기본을 권장합니다.

-      fill={color || '#B0B9C2'}
+      fill={color ?? 'currentColor'}
packages/ui/src/components/Icon/assets/icons/menu/asian.tsx (2)

4-11: SVG 크기 기본값 처리 및 접근성 속성 보완 제안

  • 단일 치수만 전달될 때 비율 유지가 어긋날 수 있어 너비/높이 기본값을 서로 fallback하도록 권장합니다.
  • 기본적으로 장식용 아이콘일 가능성이 높으므로 접근성 속성(role/aria-hidden/focusable)을 명시해주는 것이 안전합니다. 사용자가 aria-label을 넘기면 자동으로 노출되도록 처리할 수 있습니다.

아래와 같이 수정 제안드립니다:

   <svg
-    width={width ?? 40}
-    height={height ?? 40}
+    width={width ?? height ?? 40}
+    height={height ?? width ?? 40}
     viewBox='0 0 41 41'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    role={props['aria-label'] ? 'img' : 'presentation'}
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable={false}
     {...props}
   >

1-1: clipPath id 충돌 가능성: 다중 인스턴스 렌더링 시 안전한 고유 id 필요

여러 개의 동일 아이콘이 한 페이지에 렌더링되면 정적 id(clip0_116_3709)가 충돌할 수 있습니다. React의 useId로 고유 id를 생성해 clipPath 참조에 사용하면 충돌을 방지할 수 있습니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Asian = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
+export const Asian = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
   <svg
@@
-    <g clipPath='url(#clip0_116_3709)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-      <clipPath id='clip0_116_3709'>
+      <clipPath id={clipId}>
@@
-  </svg>
-)
+  </svg>
+)}

Also applies to: 3-3, 12-12, 67-75, 77-77

packages/ui/src/components/Icon/assets/icons/tag/fingerUp.tsx (2)

9-15: SVG 크기 기본값 처리 및 접근성 속성 보완 제안

  • width/height 상호 fallback으로 단일 치수만 전달될 때도 비율 유지.
  • 접근성 속성(role/aria-hidden/focusable) 기본 제공.
   <svg
-    width={width ?? 14}
-    height={height ?? 14}
+    width={width ?? height ?? 14}
+    height={height ?? width ?? 14}
     viewBox='0 0 15 15'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    role={props['aria-label'] ? 'img' : 'presentation'}
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable={false}
     {...props}
   >

1-1: clipPath id 충돌 가능성: 안전한 고유 id 도입

정적 id(clip0_111_1365)는 동일 아이콘 다중 렌더링 시 충돌할 수 있습니다. useId를 사용해 해결하는 것을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const FingerUp = ({
+export const FingerUp = ({
   width,
   height,
   ...props
-}: SVGProps<SVGSVGElement>) => (
+}: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
   <svg
@@
-    <g clipPath='url(#clip0_111_1365)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-      <clipPath id='clip0_111_1365'>
+      <clipPath id={clipId}>
@@
-  </svg>
-)
+  </svg>
+)}

Also applies to: 3-7, 16-16, 43-51, 53-53

packages/ui/src/components/Icon/assets/icons/menu/western.tsx (2)

9-15: SVG 크기 기본값 처리 및 접근성 속성 보완

단일 치수 지정 시 비율 유지와 기본 접근성 속성 제공을 위해 아래 변경을 권장합니다.

   <svg
-    width={width ?? 40}
-    height={height ?? 40}
+    width={width ?? height ?? 40}
+    height={height ?? width ?? 40}
     viewBox='0 0 41 41'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    role={props['aria-label'] ? 'img' : 'presentation'}
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable={false}
     {...props}
   >

1-1: clipPath id 정적 사용으로 인한 충돌 위험 — useId 적용 제안

중복 마운트 시 id('clip0_116_3599') 충돌을 피하기 위해 고유 id를 생성해 참조하도록 변경하세요.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Western = ({
+export const Western = ({
   width,
   height,
   ...props
-}: SVGProps<SVGSVGElement>) => (
+}: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
   <svg
@@
-    <g clipPath='url(#clip0_116_3599)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-      <clipPath id='clip0_116_3599'>
+      <clipPath id={clipId}>
@@
-  </svg>
-)
+  </svg>
+)}

Also applies to: 3-7, 16-16, 111-119, 121-121

packages/ui/src/components/Icon/assets/icons/menu/korean.tsx (1)

8-15: SVG 크기 기본값 처리 및 접근성 속성 보완

  • width/height 상호 fallback으로 비율 유지.
  • 접근성 속성(role/aria-hidden/focusable) 기본 제공.
   <svg
-    width={width ?? 40}
-    height={height ?? 40}
+    width={width ?? height ?? 40}
+    height={height ?? width ?? 40}
     viewBox='0 0 42 42'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    role={props['aria-label'] ? 'img' : 'presentation'}
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable={false}
     {...props}
   >
packages/ui/src/components/Icon/assets/icons/header/logo.tsx (2)

5-11: SVG 크기 기본값 처리 및 접근성 속성 보완

헤더 로고도 동일하게 비율 유지 fallback과 접근성 속성을 권장합니다.

   <svg
-    width={width ?? 26}
-    height={height ?? 26}
+    width={width ?? height ?? 26}
+    height={height ?? width ?? 26}
     viewBox='0 0 27 27'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    role={props['aria-label'] ? 'img' : 'presentation'}
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable={false}
     {...props}
   >

1-1: clipPath id 재사용 충돌 방지: useId 적용

여러 로고 인스턴스가 동일 문서에 있을 때 id('clip0_116_3458') 충돌을 방지하세요.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Logo = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
+export const Logo = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
   <svg
@@
-    <g clipPath='url(#clip0_116_3458)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-      <clipPath id='clip0_116_3458'>
+      <clipPath id={clipId}>
@@
-  </svg>
-)
+  </svg>
+)}

Also applies to: 3-3, 12-12, 39-47, 49-49

packages/ui/src/components/Icon/assets/icons/search.tsx (2)

1-29: clipPath id 충돌 가능성 방지 + 접근성 기본값 + currentColor로 테마 연동

여러 인스턴스를 동시에 렌더링할 때 정적 clipPath id가 문서 전역에서 충돌할 수 있습니다. React 18의 useId를 사용하면 인스턴스별로 고유 id를 만들어 안전합니다. 또한 아이콘은 기본적으로 장식(decorative)일 가능성이 높아 aria-hidden 기본값과 role 설정을 권장합니다. 단색 아이콘은 currentColor를 사용하면 상위의 CSS color로 쉽게 테마 연동됩니다.

아래와 같이 수정하면 좋습니다:

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'

-export const Search = ({
-  width,
-  height,
-  color,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 24}
-    height={height ?? 24}
-    viewBox='0 0 16 16'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_202_3954)'>
-      <path
-        d='M13 6.5C13 7.93437 12.5344 9.25937 11.75 10.3344L15.7062 14.2937C16.0969 14.6844 16.0969 15.3187 15.7062 15.7094C15.3156 16.1 14.6812 16.1 14.2906 15.7094L10.3344 11.75C9.25937 12.5375 7.93437 13 6.5 13C2.90937 13 0 10.0906 0 6.5C0 2.90937 2.90937 0 6.5 0C10.0906 0 13 2.90937 13 6.5ZM6.5 11C7.09095 11 7.67611 10.8836 8.22207 10.6575C8.76804 10.4313 9.26412 10.0998 9.68198 9.68198C10.0998 9.26412 10.4313 8.76804 10.6575 8.22207C10.8836 7.67611 11 7.09095 11 6.5C11 5.90905 10.8836 5.32389 10.6575 4.77792C10.4313 4.23196 10.0998 3.73588 9.68198 3.31802C9.26412 2.90016 8.76804 2.56869 8.22207 2.34254C7.67611 2.1164 7.09095 2 6.5 2C5.90905 2 5.32389 2.1164 4.77792 2.34254C4.23196 2.56869 3.73588 2.90016 3.31802 3.31802C2.90016 3.73588 2.56869 4.23196 2.34254 4.77792C2.1164 5.32389 2 5.90905 2 6.5C2 7.09095 2.1164 7.67611 2.34254 8.22207C2.56869 8.76804 2.90016 9.26412 3.31802 9.68198C3.73588 10.0998 4.23196 10.4313 4.77792 10.6575C5.32389 10.8836 5.90905 11 6.5 11Z'
-        fill={color || '#B0B9C2'}
-      />
-    </g>
-    <defs>
-      <clipPath id='clip0_202_3954'>
-        <rect width='16' height='16' fill='white' />
-      </clipPath>
-    </defs>
-  </svg>
-)
+export const Search = ({
+  width,
+  height,
+  color,
+  ...props
+}: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId();
+  return (
+    <svg
+      width={width ?? 24}
+      height={height ?? 24}
+      viewBox='0 0 16 16'
+      fill='none'
+      xmlns='http://www.w3.org/2000/svg'
+      aria-hidden={props['aria-label'] ? undefined : true}
+      role={props['aria-label'] ? 'img' : undefined}
+      focusable={false}
+      {...props}
+    >
+      <g clipPath={`url(#${clipPathId})`}>
+        <path
+          d='M13 6.5C13 7.93437 12.5344 9.25937 11.75 10.3344L15.7062 14.2937C16.0969 14.6844 16.0969 15.3187 15.7062 15.7094C15.3156 16.1 14.6812 16.1 14.2906 15.7094L10.3344 11.75C9.25937 12.5375 7.93437 13 6.5 13C2.90937 13 0 10.0906 0 6.5C0 2.90937 2.90937 0 6.5 0C10.0906 0 13 2.90937 13 6.5ZM6.5 11C7.09095 11 7.67611 10.8836 8.22207 10.6575C8.76804 10.4313 9.26412 10.0998 9.68198 9.68198C10.0998 9.26412 10.4313 8.76804 10.6575 8.22207C10.8836 7.67611 11 7.09095 11 6.5C11 5.90905 10.8836 5.32389 10.6575 4.77792C10.4313 4.23196 10.0998 3.73588 9.68198 3.31802C9.26412 2.90016 8.76804 2.56869 8.22207 2.34254C7.67611 2.1164 7.09095 2 6.5 2C5.90905 2 5.32389 2.1164 4.77792 2.34254C4.23196 2.56869 3.73588 2.90016 3.31802 3.31802C2.90016 3.73588 2.56869 4.23196 2.34254 4.77792C2.1164 5.32389 2 5.90905 2 6.5C2 7.09095 2.1164 7.67611 2.34254 8.22207C2.56869 8.76804 2.90016 9.26412 3.31802 9.68198C3.73588 10.0998 4.23196 10.4313 4.77792 10.6575C5.32389 10.8836 5.90905 11 6.5 11Z'
+          fill={color ?? 'currentColor'}
+        />
+      </g>
+      <defs>
+        <clipPath id={clipPathId}>
+          <rect width='16' height='16' fill='white' />
+        </clipPath>
+      </defs>
+    </svg>
+  )
+}

참고:

  • useId 사용은 React 18 이상이 필요합니다.
  • aria-label을 넘길 경우 자동으로 role="img"로 노출되고, 없으면 aria-hidden으로 보조기기에서 숨겨집니다.

10-13: 기본 크기와 viewBox 불일치는 의도인가요?

viewBox는 16x16인데 기본 width/height가 24입니다. 디자인 토큰(예: 16, 20, 24 고정 그리드)에 맞춘 의도라면 유지해도 됩니다. 아니라면 기본값을 16으로 맞추면 픽셀 스냅이 더 깔끔합니다.

packages/ui/src/components/Icon/assets/icons/tag/waiter.tsx (2)

1-85: clipPath id 동적화(useId) + 접근성 기본값 설정 권장

정적 id 사용은 문서 전역에서 충돌 소지가 있습니다. useId로 인스턴스 고유 id를 생성하고, aria-hidden/role 기본값을 추가하는 것을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Waiter = ({
-  width,
-  height,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 14}
-    height={height ?? 14}
-    viewBox='0 0 15 15'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_111_1288)'>
+export const Waiter = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId();
+  return (
+    <svg
+      width={width ?? 14}
+      height={height ?? 14}
+      viewBox='0 0 15 15'
+      fill='none'
+      xmlns='http://www.w3.org/2000/svg'
+      aria-hidden={props['aria-label'] ? undefined : true}
+      role={props['aria-label'] ? 'img' : undefined}
+      focusable={false}
+      {...props}
+    >
+      <g clipPath={`url(#${clipPathId})`}>
@@
-    </g>
-    <defs>
-      <clipPath id='clip0_111_1288'>
+      </g>
+      <defs>
+        <clipPath id={clipPathId}>
@@
-      </clipPath>
-    </defs>
-  </svg>
-)
+        </clipPath>
+      </defs>
+    </svg>
+  )
+}

9-11: 기본 크기(14) vs viewBox(15) 확인

디자인 시스템에서 14px 아이콘 라인업을 사용한다면 괜찮습니다. 아니라면 기본 크기를 15로 맞추면 스케일링 없이 선명하게 렌더링됩니다.

packages/ui/src/components/Icon/assets/icons/menu/salad.tsx (2)

1-61: clipPath id 고유화(useId) + a11y 기본값 추가

반복 사용 시 clipPath id 충돌을 예방하고, 아이콘 접근성 기본값을 추가하는 개선을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Salad = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 40}
-    height={height ?? 40}
-    viewBox='0 0 41 41'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_116_3635)'>
+export const Salad = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId();
+  return (
+    <svg
+      width={width ?? 40}
+      height={height ?? 40}
+      viewBox='0 0 41 41'
+      fill='none'
+      xmlns='http://www.w3.org/2000/svg'
+      aria-hidden={props['aria-label'] ? undefined : true}
+      role={props['aria-label'] ? 'img' : undefined}
+      focusable={false}
+      {...props}
+    >
+      <g clipPath={`url(#${clipPathId})`}>
@@
-    </g>
-    <defs>
-      <clipPath id='clip0_116_3635'>
+      </g>
+      <defs>
+        <clipPath id={clipPathId}>
@@
-      </clipPath>
-    </defs>
-  </svg>
-)
+        </clipPath>
+      </defs>
+    </svg>
+  )
+}

5-7: 기본 크기(40)와 viewBox(41)의 불일치

의도된 스케일(40/41)이면 유지해도 됩니다. 픽셀 정렬 선명을 우선하면 기본 크기를 41로 맞추는 것도 옵션입니다.

packages/ui/src/components/Icon/assets/icons/fireHeart.tsx (2)

1-37: clipPath id 유니크 처리 + a11y 기본 설정

다수 렌더링 시 SVG id 충돌을 방지하고, 접근성 기본값을 추가하는 개선을 제안합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const FireHeart = ({
-  width,
-  height,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 24}
-    height={height ?? 24}
-    viewBox='0 0 25 25'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_116_3780)'>
+export const FireHeart = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId();
+  return (
+    <svg
+      width={width ?? 24}
+      height={height ?? 24}
+      viewBox='0 0 25 25'
+      fill='none'
+      xmlns='http://www.w3.org/2000/svg'
+      aria-hidden={props['aria-label'] ? undefined : true}
+      role={props['aria-label'] ? 'img' : undefined}
+      focusable={false}
+      {...props}
+    >
+      <g clipPath={`url(#${clipPathId})`}>
@@
-    </g>
-    <defs>
-      <clipPath id='clip0_116_3780'>
+      </g>
+      <defs>
+        <clipPath id={clipPathId}>
@@
-      </clipPath>
-    </defs>
-  </svg>
-)
+        </clipPath>
+      </defs>
+    </svg>
+  )
+}

9-11: 기본 크기(24) vs viewBox(25) 재확인

디자인 스펙이 24px라면 OK입니다. 선명도를 중시하면 기본 크기를 25로 맞추는 것도 고려할 수 있습니다.

packages/ui/src/components/Icon/assets/icons/smile.tsx (2)

1-45: clipPath id 동적화 + 기본 접근성 속성

useId로 clipPath id를 인스턴스별로 고유화하고, aria-hidden/role/focusable의 기본값을 부여하는 것을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
 
-export const Smile = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 24}
-    height={height ?? 24}
-    viewBox='0 0 17 17'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_111_1025)'>
+export const Smile = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => {
+  const clipPathId = useId();
+  return (
+    <svg
+      width={width ?? 24}
+      height={height ?? 24}
+      viewBox='0 0 17 17'
+      fill='none'
+      xmlns='http://www.w3.org/2000/svg'
+      aria-hidden={props['aria-label'] ? undefined : true}
+      role={props['aria-label'] ? 'img' : undefined}
+      focusable={false}
+      {...props}
+    >
+      <g clipPath={`url(#${clipPathId})`}>
@@
-    </g>
-    <defs>
-      <clipPath id='clip0_111_1025'>
+      </g>
+      <defs>
+        <clipPath id={clipPathId}>
@@
-      </clipPath>
-    </defs>
-  </svg>
-)
+        </clipPath>
+      </defs>
+    </svg>
+  )
+}

5-7: 기본 크기(24) vs viewBox(17)

여기도 의도된 스케일이면 유지해도 됩니다. 만약 16/20/24 고정 그리드가 아니라 소스의 viewBox 배율을 선호한다면 기본값을 17로 맞추는 옵션도 있습니다.

packages/ui/src/components/Icon/assets/icons/navigation/circlePlus.tsx (3)

16-19: 아이콘 색상 하드코딩(#313D4C) 대신 currentColor 사용 권장

테마/다크모드/상위 컨텍스트 색상 상속을 위해 path의 fill을 currentColor로 바꾸는 것을 권장합니다. Icon 컴포넌트에서 color를 제어하기 쉬워집니다.

-    <path
-      d='M44.3091 26.1203C44.3186 31.2206 42.3016 36.1158 38.7018 39.7289C35.102 43.3421 30.2144 45.3773 25.1141 45.3867C20.0138 45.3962 15.1186 43.3792 11.5054 39.7794C7.89229 36.1796 5.85712 31.292 5.84766 26.1917C5.83819 21.0914 7.8552 16.1962 11.455 12.583C15.0547 8.96988 19.9424 6.93471 25.0427 6.92525C30.143 6.91578 35.0382 8.93279 38.6513 12.5326C42.2645 16.1323 44.2997 21.02 44.3091 26.1203ZM26.2647 17.7403C26.2641 17.4215 26.1369 17.1161 25.9111 16.8911C25.6853 16.6661 25.3793 16.54 25.0605 16.5406C24.7418 16.5412 24.4363 16.6684 24.2113 16.8942C23.9863 17.1201 23.8603 17.426 23.8609 17.7448L23.8742 24.9563L16.6627 24.9697C16.3439 24.9703 16.0385 25.0975 15.8135 25.3233C15.5885 25.5491 15.4624 25.8551 15.463 26.1738C15.4636 26.4926 15.5908 26.7981 15.8166 27.0231C16.0425 27.248 16.3484 27.3741 16.6672 27.3735L23.8787 27.3601L23.8921 34.5717C23.8927 34.8904 24.0199 35.1959 24.2457 35.4209C24.4715 35.6459 24.7775 35.7719 25.0962 35.7714C25.415 35.7708 25.7205 35.6436 25.9455 35.4177C26.1705 35.1919 26.2965 34.886 26.2959 34.5672L26.2825 27.3557L33.4941 27.3423C33.8128 27.3417 34.1183 27.2145 34.3433 26.9887C34.5683 26.7629 34.6944 26.4569 34.6938 26.1381C34.6932 25.8194 34.566 25.5139 34.3401 25.2889C34.1143 25.0639 33.8084 24.9379 33.4896 24.9384L26.2781 24.9518L26.2647 17.7403Z'
-      fill='#313D4C'
-    />
+    <path
+      d='M44.3091 26.1203C44.3186 31.2206 42.3016 36.1158 38.7018 39.7289C35.102 43.3421 30.2144 45.3773 25.1141 45.3867C20.0138 45.3962 15.1186 43.3792 11.5054 39.7794C7.89229 36.1796 5.85712 31.292 5.84766 26.1917C5.83819 21.0914 7.8552 16.1962 11.455 12.583C15.0547 8.96988 19.9424 6.93471 25.0427 6.92525C30.143 6.91578 35.0382 8.93279 38.6513 12.5326C42.2645 16.1323 44.2997 21.02 44.3091 26.1203ZM26.2647 17.7403C26.2641 17.4215 26.1369 17.1161 25.9111 16.8911C25.6853 16.6661 25.3793 16.54 25.0605 16.5406C24.7418 16.5412 24.4363 16.6684 24.2113 16.8942C23.9863 17.1201 23.8603 17.426 23.8609 17.7448L23.8742 24.9563L16.6627 24.9697C16.3439 24.9703 16.0385 25.0975 15.8135 25.3233C15.5885 25.5491 15.4624 25.8551 15.463 26.1738C15.4636 26.4926 15.5908 26.7981 15.8166 27.0231C16.0425 27.248 16.3484 27.3741 16.6672 27.3735L23.8787 27.3601L23.8921 34.5717C23.8927 34.8904 24.0199 35.1959 24.2457 35.4209C24.4715 35.6459 24.7775 35.7719 25.0962 35.7714C25.415 35.7708 25.7205 35.6436 25.9455 35.4177C26.1705 35.1919 26.2965 34.886 26.2959 34.5672L26.2825 27.3557L33.4941 27.3423C33.8128 27.3417 34.1183 27.2145 34.3433 26.9887C34.5683 26.7629 34.6944 26.4569 34.6938 26.1381C34.6932 25.8194 34.566 25.5139 34.3401 25.2889C34.1143 25.0639 33.8084 24.9379 33.4896 24.9384L26.2781 24.9518L26.2647 17.7403Z'
+      fill='currentColor'
+    />

9-11: 기본 width/height 50과 viewBox(51) 불일치 및 라이브러리 내 기본 크기 일관성

다른 아이콘들이 24를 기본으로 사용하고 있어 일관성 측면에서 24로 맞추는 것을 제안합니다. 또는 viewBox와 동일한 51로 맞추어 반픽셀 스케일링을 피울 수도 있습니다(경미).

-    width={width ?? 50}
-    height={height ?? 50}
+    width={width ?? 24}
+    height={height ?? 24}

8-15: 접근성: role="img" 지정 권장

보조기기 명시적 역할 제공을 위해 role="img"를 추가하는 것을 권장합니다. aria-label은 props로 이미 전달 가능하므로 필요 시 호출부에서 지정하면 됩니다.

   <svg
+    role='img'
     width={width ?? 50}
     height={height ?? 50}
packages/ui/src/components/Icon/assets/icons/kakaoLogo.tsx (2)

17-22: 아이콘 색상 하드코딩(black) 대신 currentColor 사용 권장

테마 색상 상속과 다크모드 대응을 위해 currentColor 사용을 권장합니다.

-      <path
+      <path
         fillRule='evenodd'
         clipRule='evenodd'
         d='M18 1.19995C8.05835 1.19995 0 7.42587 0 15.1045C0 19.88 3.11681 24.0899 7.86305 26.5939L5.86606 33.8889C5.68962 34.5335 6.42683 35.0473 6.99293 34.6738L15.7467 28.8964C16.4854 28.9676 17.2362 29.0093 18 29.0093C27.9409 29.0093 35.9999 22.7836 35.9999 15.1045C35.9999 7.42587 27.9409 1.19995 18 1.19995Z'
-        fill='black'
+        fill='currentColor'
       />

1-30: clipPath id 중복 가능성: React useId로 인스턴스 유니크화 권장

동일 아이콘이 페이지에 여러 번 렌더링되면 정적 id(clip0_881_7729)가 충돌할 수 있습니다. React 18의 useId로 id를 인스턴스별 유니크하게 만드는 것을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
@@
-export const KakaoLogo = ({
-  width,
-  height,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
-    width={width ?? 24}
-    height={height ?? 24}
-    viewBox='0 0 36 36'
-    fill='none'
-    xmlns='http://www.w3.org/2000/svg'
-    {...props}
-  >
-    <g clipPath='url(#clip0_881_7729)'>
+export const KakaoLogo = ({
+  width,
+  height,
+  ...props
+}: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
+  <svg
+    width={width ?? 24}
+    height={height ?? 24}
+    viewBox='0 0 36 36'
+    fill='none'
+    xmlns='http://www.w3.org/2000/svg'
+    {...props}
+  >
+    <g clipPath={`url(#${clipId})`}>
@@
-    <defs>
-      <clipPath id='clip0_881_7729'>
+    <defs>
+      <clipPath id={clipId}>
         <rect width='35.9999' height='36' fill='white' />
       </clipPath>
     </defs>
   </svg>
-)
+)}
packages/ui/src/components/Icon/assets/icons/header/shakingHeart.tsx (3)

19-35: 색상 하드코딩(#EC4452) 대신 currentColor 사용 권장

여러 경로에 동일한 빨간색이 하드코딩되어 있습니다. currentColor로 변경하면 상위 컨텍스트(스타일/테마)로 색 제어가 쉬워집니다.

-        fill='#EC4452'
+        fill='currentColor'
@@
-        fill='#EC4452'
+        fill='currentColor'
@@
-        fill='#EC4452'
+        fill='currentColor'
@@
-        fill='#EC4452'
+        fill='currentColor'
@@
-        fill='#EC4452'
+        fill='currentColor'

1-49: clipPath id 중복 가능성: useId로 고유화 권장

정적 id(clip0_73_324)는 동일 컴포넌트 다중 렌더 시 충돌 우려가 있습니다. useId를 사용해 안전하게 만드는 것을 권장합니다.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
@@
-export const ShakingHeart = ({
-  width,
-  height,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
+export const ShakingHeart = ({
+  width,
+  height,
+  ...props
+}: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
+  <svg
     width={width ?? 26}
     height={height ?? 26}
     viewBox='0 0 27 27'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >
-    <g clipPath='url(#clip0_73_324)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-    <defs>
-      <clipPath id='clip0_73_324'>
+    <defs>
+      <clipPath id={clipId}>
         <rect
           width='24'
           height='24'
           fill='white'
           transform='translate(0.540451 0.268799)'
         />
       </clipPath>
     </defs>
   </svg>
-)
+)}

9-10: 기본 크기(26) vs 다른 아이콘(24) 일관성

다른 아이콘들은 대체로 24를 기본으로 사용하고 있습니다. 라이브러리 사용성 측면에서 24로 통일을 고려해 주세요.

-    width={width ?? 26}
-    height={height ?? 26}
+    width={width ?? 24}
+    height={height ?? 24}
apps/web/app/page.tsx (2)

6-11: placeholder 텍스트(gkldf) 제거 또는 의미 있는 복구

의도치 않은 플레이스홀더로 보입니다. 임시라면 TODO 주석 명시 또는 의미 있는 콘텐츠로 교체를 제안드립니다.

-      <Flex as={'main'}>
-        <div>gkldf</div>
-        <div>gkldf</div>
-        <div>gkldf</div>
-      </Flex>
+      <Flex as="main">
+        {/* TODO: 메인 섹션 구성요소 배치 */}
+        <section aria-label="검색 섹션" />
+        <section aria-label="추천 섹션" />
+        <section aria-label="피드 섹션" />
+      </Flex>

5-12: 불필요한 Fragment 제거 및 문자열 리터럴 단순화

불필요한 Fragment는 제거할 수 있고, as={'main'}는 as="main"으로 단순화 가능합니다.

-    <>
-      <Flex as={'main'}>
+      <Flex as="main">
         {/* ... */}
-      </Flex>
-    </>
+      </Flex>
packages/ui/src/components/Icon/assets/icons/crosshairs.tsx (4)

20-20: 기본 색상 '#3182F7' 대신 currentColor 사용 고려

디자인 토큰/테마 상속을 위해 color prop 미지정 시 currentColor를 따르도록 변경을 권장합니다. 필요 시 호출부에서 color로 override 가능합니다.

-        fill={color || '#3182F7'}
+        fill={color ?? 'currentColor'}

1-34: clipPath id 유니크화: useId 적용 권장

정적 id(clip0_59_830)는 동일 아이콘 다중 사용 시 충돌할 수 있습니다. useId로 인스턴스마다 고유 id를 부여해 주세요.

-import { SVGProps } from 'react'
+import { SVGProps, useId } from 'react'
@@
-export const Crosshairs = ({
+export const Crosshairs = ({
   width,
   height,
   color,
   ...props
-}: SVGProps<SVGSVGElement>) => (
-  <svg
+}: SVGProps<SVGSVGElement>) => {
+  const clipId = useId()
+  return (
+  <svg
     width={width ?? 24}
     height={height ?? 24}
     viewBox='0 0 26 26'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >
-    <g clipPath='url(#clip0_59_830)'>
+    <g clipPath={`url(#${clipId})`}>
@@
-    <defs>
-      <clipPath id='clip0_59_830'>
+    <defs>
+      <clipPath id={clipId}>
         <rect
           width='25.3333'
           height='25.3333'
           fill='white'
           transform='translate(0.333336 0.333344)'
         />
       </clipPath>
     </defs>
   </svg>
-)
+)}

8-15: 접근성: role="img" 추가 권장

명시적 역할 제공을 위해 role="img" 추가를 권장합니다.

   <svg
+    role='img'
     width={width ?? 24}
     height={height ?? 24}

1-34: 아이콘 API 일관성 정비 제안(color 지원 방식 통일)

현재 Crosshairs만 color prop을 직접 받습니다. 다른 아이콘도 currentColor 기반으로 통일하거나 color prop을 동일하게 지원하는 방향으로 일관성 정리를 제안드립니다.

packages/ui/src/components/Button/index.tsx (1)

1-1: 배럴(export) 구성은 적절합니다. 다만 타입까지 함께 re-export하는 패턴을 고려해주세요.

컴포넌트만 명시적으로 노출하는 것도 좋지만, 소비자 입장에서 Props/타입을 함께 가져오는 패턴이 DX에 유리합니다. 아래처럼 변경하면 추후 타입 추가/변경 시 배럴을 손대지 않아도 됩니다.

-export { Button } from './Button'
+export * from './Button'

추가로, 이 파일은 JSX를 사용하지 않으므로 index.tsx 대신 index.ts로 변경하면 컴파일 파이프라인이 약간 단순해지고(미세 최적화), Layout/JustifyEnd와의 확장자 컨벤션도 일관해집니다.

packages/ui/src/components/Layout/JustifyEnd/index.ts (1)

1-1: 배럴에서 타입까지 함께 re-export하여 DX 개선

구현 파일에서 타입(예: Props)을 함께 export한다면, 배럴에서도 한 번에 노출하는 편이 사용성이 좋습니다.

-export { JustifyEnd } from './JustifyEnd'
+export * from './JustifyEnd'
packages/ui/src/components/Icon/index.tsx (1)

1-1: Icon도 export * 패턴과 .ts 확장자 사용을 고려

  • 타입/유틸을 포함해 한 번에 노출하려면 export * from './Icon'이 편리합니다.
  • JSX가 없으므로 index.tsxindex.ts로 바꾸면 컴파일이 약간 단순해지고, Layout/JustifyEnd와의 컨벤션도 맞출 수 있습니다.
-export { Icon } from './Icon'
+export * from './Icon'
packages/ui/src/components/Chip/index.tsx (1)

1-1: 배럴에 타입 포함 re-export + 확장자 정리 제안

  • export *로 변경하면 ChipProps 등 타입을 함께 노출할 수 있어 사용성이 좋아집니다.
  • JSX가 없으므로 index.tsxindex.ts로의 정리를 고려해 주세요.
-export { Chip } from './Chip'
+export * from './Chip'
packages/ui/src/components/Layout/Flex/index.ts (1)

1-1: 배럴(export) 구성은 적절합니다 — 타입 재노출도 고려하면 DX가 좋아집니다

컴포넌트와 함께 관련 타입(예: props 타입)을 재노출하면 소비 측에서 깊은 경로 import를 피할 수 있어 편합니다.

가능하다면 아래처럼 타입 재노출을 추가해 주세요(존재하는 타입만 재노출됩니다).

 export { Flex } from './Flex'
+export type * from './Flex'
packages/ui/src/components/Layout/Column/index.ts (1)

1-1: 배럴(export) 구성은 적절합니다 — 타입 재노출 추가 제안

Flex와 동일하게 Column 관련 타입도 함께 노출하면 사용성이 올라갑니다.

 export { Column } from './Column'
+export type * from './Column'
packages/ui/tailwind.config.ts (1)

1-5: Tailwind 설정에 타입을 부여해 정적 검사를 강화하세요

TS 기반 설정 파일을 사용 중이므로 Config 타입을 명시하면 속성 오타나 스키마 변경에 강합니다. (참고: Tailwind v4를 사용 중이라면 content가 무시될 수 있으니, 버전 확인 후 단순화도 검토하세요.)

아래처럼 타입을 추가하는 것을 권장합니다.

-const config = {
-  content: ['./src/**/*.{ts,tsx,mdx}'],
-}
-
-export default config
+import type { Config } from 'tailwindcss'
+
+const config: Config = {
+  content: ['./src/**/*.{ts,tsx,mdx}'],
+}
+
+export default config
packages/ui/src/components/Textarea/index.tsx (1)

1-1: 배럴(export) 구성 LGTM — 타입 재노출로 사용성 개선 가능

Textarea 관련 타입(예: Props)을 함께 재노출하면 소비 측에서 경로 단순화가 가능합니다.

 export { Textarea } from './Textarea'
+export type * from './Textarea'
apps/storybook/.storybook/preview.ts (1)

4-13: 타입 안정성 미세 개선 제안: satisfies 사용

선호도의 영역이지만, Preview 타입 체크를 더 엄격히 하려면 satisfies를 사용할 수 있습니다(추론 유지 + 속성 오타 방지).

아래와 같이 변경 가능합니다:

-const preview: Preview = {
+const preview = {
   parameters: {
     controls: {
       matchers: {
         color: /(background|color)$/i,
         date: /Date$/i,
       },
     },
   },
-}
+} satisfies Preview
apps/web/app/layout.tsx (2)

29-31: 고정 폭 450px로 인한 모바일 가로 스크롤 가능성

w-[450px]과 max-w-[450px]를 함께 쓰면 작은 화면에서도 폭이 450px로 고정되어 오버플로우가 발생할 수 있습니다. 작은 화면에서는 가득 채우고, 큰 화면에서만 최대 450px을 제한하는 쪽이 안전합니다.

아래처럼 폭을 유동적으로 바꾸는 것을 제안합니다:

-            <Column className={'relative w-[450px] max-w-[450px] bg-white'}>
+            <Column className='relative w-full max-w-[450px] bg-white'>

필요하다면 sm 이상에서만 450px로 제한하도록 반응형도 고려할 수 있습니다:

  • className="relative w-full sm:w-[450px] sm:max-w-[450px] bg-white"

28-33: 레이아웃 컨테이너 접근성/반응형 미세 개선 아이디어

  • 배경색이 body 전역에 적용되지 않도록 wrapper에만 있는 점은 의도에 부합하나, Safe Area(특히 iOS) 고려가 필요하면 padding을 적절히 보정하는 것도 고려해 보세요.
  • 전체 높이를 강제(h-svh)하면 콘텐츠가 많은 화면에서 내부 스크롤이 발생합니다. 의도에 따라 min-h-svh로 바꾸면 더 자연스러울 수 있습니다.

원한다면 개선 패치 제안 드리겠습니다.

packages/ui/src/components/Text/index.tsx (1)

1-1: 타입도 함께 재노출하여 DX 개선 제안

TextProps, TextType, TypographyVariant 등을 함께 export하면 내부 파일 구조 변화에 대한 소비처의 결합도를 낮출 수 있습니다.

아래처럼 타입만 추가로 노출하는 것을 고려해 주세요.

 export { Text } from './Text'
+export type { TextProps, TextType, TypographyVariant } from './Text'
packages/ui/src/components/Layout/Column/Column.tsx (1)

4-6: forwardRef 지원 고려

레이아웃 프리미티브는 하위 DOM/컴포넌트 ref 전달이 필요한 경우가 많습니다. 향후 포커스 관리나 측정(IntersectionObserver, getBoundingClientRect 등) 요구가 생길 수 있으니 forwardRef를 도입하는 것을 권장합니다.

apps/storybook/.storybook/main.ts (1)

4-4: 스토리 경로 축소는 타당합니다 — MDX 스토리 계획이 있다면 glob 확장 검토

UI 패키지로 범위를 좁힌 것은 유지보수에 유리합니다. 향후 MDX 스토리를 사용할 계획이 있다면 **/*.mdx도 포함하는 것을 고려해 주세요.

packages/ui/src/components/Icon/assets/icons/menu/mexican.tsx (3)

3-11: width/height 기본값을 디폴트 파라미터로 단순화 제안

초깃값을 파라미터 레벨에서 지정하면 JSX가 간결해지고 추론도 명확해집니다.

아래처럼 정리할 수 있습니다.

-export const Mexican = ({
-  width,
-  height,
-  ...props
-}: SVGProps<SVGSVGElement>) => (
+export const Mexican = ({ width = 40, height = 40, ...props }: SVGProps<SVGSVGElement>) => (
   <svg
-    width={width ?? 40}
-    height={height ?? 40}
+    width={width}
+    height={height}
     viewBox='0 0 41 41'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >

8-15: 접근성 기본값 보완: role/aria-hidden/focusable 추가 제안

장식용 아이콘일 때는 스크린리더에서 무시되도록, 명명된 아이콘일 때는 role을 명시하는 것이 안전합니다.

아래처럼 보완해 주세요.

   <svg
+    role='img'
+    aria-hidden={props['aria-label'] ? undefined : true}
+    focusable='false'
     width={width}
     height={height}
     viewBox='0 0 41 41'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
     {...props}
   >

1-57: 아이콘 구성 자체는 문제 없습니다

SVG 구조/clipPath 사용과 속성 전달 방식이 일관적입니다. 팔레트 컬러가 고정이라도, 브랜드 컬러 테마 대응이 필요하면 currentColor 기반 버전도 별도로 고려해 볼 수 있겠습니다.

packages/ui/src/components/Layout/JustifyAround/JustifyAround.tsx (2)

13-17: DOM에 불필요한 as 속성이 전달됩니다 — 제거 권장

polymorphic 렌더링을 위해 as로 컴포넌트를 선택하는 패턴은 좋습니다. 다만 선택된 DOM 요소(

,
등)에 as 속성을 그대로 전달하면 React가 무시하는 불필요한 속성이 됩니다(커스텀 컴포넌트가 아닌 한 의미 없음). 깔끔하게 제거하는 편이 안전합니다.

아래처럼 as 전달을 제거해 주세요:

     <Component
-      as={as}
       className={cn('ui:flex ui:items-center ui:justify-around', className)}
       {...restProps}
     >

4-11: (선택) 기본 요소 지정 간소화로 가독성 개선

현재 const Component = as || 'div'도 충분히 명확하나, 디스트럭처링 단계에서 기본값을 지정하면 추가 변수 없이 간결합니다. 팀 내 기존 컴포넌트들과의 일관성을 고려해 선택적으로 적용하세요.

-export const JustifyAround: FlexType = ({
-  as,
-  className,
-  children,
-  ...restProps
-}) => {
-  const Component = as || 'div'
+export const JustifyAround: FlexType = ({
+  as: Component = 'div',
+  className,
+  children,
+  ...restProps
+}) => {
packages/ui/src/components/Layout/JustifyBetween/JustifyBetween.tsx (2)

13-17: DOM에 불필요한 as 속성이 전달됩니다 — 제거 권장

JustifyAround와 동일한 사유로 DOM 요소에 as를 넘기지 않는 것이 좋습니다.

     <Component
-      as={as}
       className={cn('ui:flex ui:items-center ui:justify-between', className)}
       {...restProps}
     >

4-11: (선택) 기본 요소 지정 간소화로 가독성 개선

위 컴포넌트와 동일하게 기본값을 디스트럭처링에서 지정하는 패턴을 고려해 볼 수 있습니다.

-export const JustifyBetween: FlexType = ({
-  as,
-  className,
-  children,
-  ...restProps
-}) => {
-  const Component = as || 'div'
+export const JustifyBetween: FlexType = ({
+  as: Component = 'div',
+  className,
+  children,
+  ...restProps
+}) => {
packages/ui/src/components/Icon/assets/icons/menu/burger.tsx (1)

9-13: 기본 width/height와 viewBox 비율 불일치 — 아이콘 패딩/왜곡 가능성

기본 크기(40x40)와 viewBox(41x42)가 달라 브라우저가 비율을 맞추면서 상하/좌우 여백이 생기거나 의도치 않은 스케일이 적용될 수 있습니다. 다음 둘 중 하나로 정렬을 권장합니다.

옵션 A: viewBox에 맞춰 기본 크기 조정

-    width={width ?? 40}
-    height={height ?? 40}
+    width={width ?? 41}
+    height={height ?? 42}

옵션 B: 기본 크기에 맞춰 viewBox 정규화(시각적 영향 검증 필요)

-    viewBox='0 0 41 42'
+    viewBox='0 0 40 40'

Storybook(아이콘 전체 갤러리)에서 렌더링이 중앙 정렬되고 여백 없이 표시되는지 확인 부탁드립니다. 필요 시 동일 세트의 다른 아이콘들과 기본 크기를 일관화하는 것도 추천합니다.

packages/ui/src/components/Icon/assets/icons/navigation/home.tsx (2)

2-2: 사이드 이펙트 import 재검토(일관성/번들 크기 관점)

이 파일만 @repo/tailwind-config를 사이드 이펙트로 임포트합니다. 아이콘 자체는 Tailwind 클래스를 사용하지 않으므로 불필요할 수 있습니다. 다른 아이콘들과의 일관성 및 번들 크기를 고려해 제거를 제안합니다. 만약 빌드/런타임 의존이 있다면 공용 엔트리에서 한 번만 로드하는 방식이 더 낫습니다.

-import '@repo/tailwind-config'

19-21: (선택) 기본 색상을 currentColor로 두어 테마 연동성 향상

현재 기본 색상이 고정(hex)입니다. 기본값을 currentColor로 두면 상위 컨텍스트의 글자색/테마 색상에 자연스럽게 동기화됩니다(명시적 color prop가 있을 경우 우선).

-      fill={color || '#B0B9C2'}
+      fill={color ?? 'currentColor'}
packages/ui/src/components/Icon/assets/icons/menu/lunchbox.tsx (1)

9-12: 기본 width/height(40)와 viewBox(41×41) 불일치

기본 사이즈(40) 대비 viewBox가 41이라 40px로 사용 시 0.9756배 스케일이 걸려 서브픽셀 렌더링/블러가 생길 수 있습니다. 디자인 시스템에서 기본 아이콘 크기를 40으로 가져가신다면 viewBox를 40×40으로 정규화하거나, 최소한 기본 width/height를 41로 맞추는 것을 고려해주세요. (Icon 래퍼에서 항상 size를 지정한다면 영향은 제한적)

packages/ui/src/components/Icon/assets/icons/arrowRight.tsx (2)

10-13: viewBox(25)와 기본 사이즈(24) 불일치로 인한 블러 가능성 + 색상 테마 연동 개선

  • 24px로 그릴 때 viewBox가 25라 소수 스케일이 생겨 가장자리 블러가 생길 수 있습니다. 기본 width/height를 25로 맞추거나, 뷰박스를 24×24로 재내보내는 쪽 중 하나로 정합성 맞추기를 권장합니다.
  • 색상 기본값을 currentColor로 두면 상위 CSS color에 자연스럽게 반응해 테마 적용이 쉬워집니다.
   <svg
-    width={width ?? 24}
-    height={height ?? 24}
+    width={width ?? 25}
+    height={height ?? 25}
@@
-      fill={color || '#6C7887'}
+      fill={color ?? 'currentColor'}

Also applies to: 21-21


9-16: 장식용 아이콘 접근성 처리 확인

해당 SVG는 텍스트 대체를 제공하지 않으므로 장식용으로 쓰인다면 aria-hidden을 기본 true로 두고, 필요한 경우 상위 Icon 컴포넌트에서 aria-label을 덮어쓰는 패턴을 권장합니다. 상위 래퍼에서 이미 처리 중이라면 그대로 유지해도 됩니다.

packages/ui/src/components/Icon/assets/icons/menu/soup.tsx (1)

5-7: 기본 width/height(40)와 viewBox(42×41) 불일치

뷰박스가 42×41인데 기본 크기는 40이라 비정수 스케일이 적용됩니다. 40px 그리드에서 픽셀 퍼펙트를 원하시면 viewBox를 40×40으로 정규화하거나, 최소 기본 width/height를 42/41로 맞추는 방안을 고려해주세요.

packages/ui/src/components/Icon/assets/icons/menu/meat.tsx (1)

5-7: 기본 width/height(40)와 viewBox(41×41) 불일치

서브픽셀 스케일로 인한 블러/왜곡 방지를 위해 기본 사이즈와 viewBox 정합성을 맞추는 것을 권장합니다. (디자인 시스템 기준에 맞춰 40 그리드로 맞추거나, 기본 사이즈를 41로 조정)

packages/ui/src/components/SearchBar/SearchBar.stories.tsx (1)

16-20: 스토리 기본 동작 검증 제안

href가 '#'로 고정되어 있어 클릭 시 페이지 상단 점프가 발생할 수 있습니다. 상호작용 데모를 위해 args에 실제 경로(예: '/search')를 주거나, onClick 핸들러 목업을 추가하는 것도 선택지입니다. 필수는 아닙니다.

packages/ui/src/polymorphics.ts (1)

9-12: forwardRef 대응 타입도 함께 제공 권장

다형 컴포넌트가 ref 전달을 지원한다면, ref-safe 타입도 같이 export하면 재사용성이 커집니다. 아래 보조 타입들을 추가 도입을 고려해주세요.

// 추가 제안 코드 (파일 하단 등 적절한 위치)
import { ComponentPropsWithRef } from 'react'

export type PolymorphicRef<C extends ElementType> =
  ComponentPropsWithRef<C>['ref']

export type PolymorphicComponentPropsWithRef<
  C extends ElementType,
  Props extends object = {},
> = PolymorphicComponentProps<C, Props> & { ref?: PolymorphicRef<C> }
packages/ui/src/components/Icon/assets/icons/menu/chicken.tsx (1)

8-15: SVG 접근성 기본값(aria-hidden) 제공 제안

대부분의 아이콘은 장식용입니다. aria-label이 없는 경우 스크린리더에서 숨기도록 기본값을 넣는 걸 권장합니다. props로 aria-label을 넘기면 자동으로 노출되도록 하면 일관성이 좋아집니다.

   <svg
     width={width ?? 40}
     height={height ?? 40}
     viewBox='0 0 41 42'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    aria-hidden={props['aria-label'] ? undefined : true}
     {...props}
   >

추가로, 여러 아이콘에서 유사한 clipPath id를 사용하고 있습니다. 일반적으로 각 <svg> 내부 참조라 문제는 드물지만, 추후 외부 참조나 SSR/최적화 과정에서 충돌을 피하려면 ID를 네임스페이스화하는 것도 고려할만합니다.

packages/ui/src/components/Icon/assets/icons/header/user.tsx (1)

3-20: 아이콘 색상 커스터마이징과 접근성 기본값 추가 제안

내비게이션의 user 아이콘은 color를 받아 채움색을 제어하는 반면, 본 헤더 아이콘은 고정 색입니다. 두 컴포넌트 간 API 일관성을 위해 color를 허용하고 기본값만 유지하는 형태를 제안합니다. 동시에 접근성 기본값도 추가합니다.

-import { SVGProps } from 'react'
+import { SVGProps } from 'react'

-export const User = ({ width, height, ...props }: SVGProps<SVGSVGElement>) => (
+export const User = ({
+  width,
+  height,
+  color,
+  ...props
+}: SVGProps<SVGSVGElement>) => (
   <svg
     width={width ?? 26}
     height={height ?? 26}
     viewBox='0 0 27 27'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    aria-hidden={props['aria-label'] ? undefined : true}
     {...props}
   >
     <g clipPath='url(#clip0_76_1057)'>
       <path
         d='M13.0002 12.5469C15.6567 12.5469 17.8102 10.3934 17.8102 7.73695C17.8102 5.08046 15.6567 2.92695 13.0002 2.92695C10.3437 2.92695 8.19019 5.08046 8.19019 7.73695C8.19019 10.3934 10.3437 12.5469 13.0002 12.5469Z'
-        fill='#3180F3'
+        fill={color ?? '#3180F3'}
       />
       <path
         d='M13.0001 14.0185C6.5352 14.0185 4.021 18.4268 4.021 20.4769C4.021 22.527 9.37375 23.073 13.0001 23.073C16.6264 23.073 21.9792 22.527 21.9792 20.4769C21.9792 18.4268 19.465 14.0185 13.0001 14.0185Z'
-        fill='#3180F3'
+        fill={color ?? '#3180F3'}
       />
packages/ui/src/components/Icon/assets/icons/tag/calculator.tsx (1)

8-15: 아이콘 접근성 기본값(aria-hidden) 추가 제안

장식 아이콘 기본값으로 aria-hidden을 설정하고, aria-label이 제공되면 자동 노출되도록 처리하면 접근성 품질이 좋아집니다.

   <svg
     width={width ?? 14}
     height={height ?? 14}
     viewBox='0 0 15 15'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    aria-hidden={props['aria-label'] ? undefined : true}
     {...props}
   >
packages/ui/src/components/Icon/assets/icons/cry.tsx (1)

4-11: 아이콘 접근성 기본값(aria-hidden) 추가 제안

다른 아이콘들과 동일하게 기본 aria-hidden을 제공하면 좋습니다.

   <svg
     width={width ?? 24}
     height={height ?? 24}
     viewBox='0 0 15 15'
     fill='none'
     xmlns='http://www.w3.org/2000/svg'
+    aria-hidden={props['aria-label'] ? undefined : true}
     {...props}
   >
packages/ui/src/components/SearchBar/SearchBar.tsx (1)

6-29: 컴포넌트 확장성 향상: 공용 앵커 속성/클래스 전달 지원

현재 SearchBarhref만 받도록 제한되어 있습니다. 외부에서 className, onClick, target, rel 등 앵커 속성을 함께 전달할 수 있도록 타입과 props 전달을 확장하면 재사용성이 크게 좋아집니다.

아래처럼 앵커 기본 속성을 수용하고, className 병합 및 나머지 props를 전달할 수 있습니다.

-import { Icon } from '../Icon'
+import { Icon } from '../Icon'
 import { Flex } from '../Layout'
 import { cn } from '../../utils/cn'
 import { Text } from '../Text'
 
-export const SearchBar = ({ href }: { href: string }) => {
-  return (
-    <Flex
-      as='a'
-      href={href || ''}
-      className={cn(
-        'ui:border ui:border-gray-200',
-        'ui:rounded-xl',
-        'ui:p-3.5',
-        'ui:items-center',
-        'ui:gap-2',
-      )}
-    >
+export const SearchBar = ({
+  href,
+  className,
+  ...rest
+}: React.ComponentPropsWithoutRef<'a'> & { href: string }) => {
+  return (
+    <Flex
+      as='a'
+      href={href}
+      className={cn(
+        'ui:border ui:border-gray-200',
+        'ui:rounded-xl',
+        'ui:p-3.5',
+        'ui:items-center',
+        'ui:gap-2',
+        className,
+      )}
+      {...rest}
+    >
       <Icon type={'search'} size={16} />
       <Text
         fontSize={'sm'}
         fontWeight={'normal'}
         className={cn('ui:text-gray-200', 'ui:outline-none', 'ui:w-full')}
       >
         식당을 검색해주세요
       </Text>
     </Flex>
   )
 }

참고: 실제 검색 입력 필드가 아니라 검색 페이지로 이동하는 링크라면 현재 구조가 맞고, 검색 상호작용을 직접 받는 경우에는 role="searchbox"/input 등으로 변경하는 접근성 개선을 고려할 수 있습니다.

packages/ui/src/components/Chip/Chip.stories.tsx (1)

25-46: Storybook 로그는 console.log 대신 Actions 애드온 사용 권장

스토리 실행 시 콘솔 노이즈를 줄이고, Storybook의 Actions 패널에서 상호작용을 확인할 수 있도록 @storybook/addon-actions 사용을 제안합니다.

다음 변경으로 대체 가능합니다.

+import { action } from '@storybook/addon-actions'
-          onToggle={() => {
-            console.log(`${type} 클릭됨!`)
-          }}
+          onToggle={action(`${type} 클릭됨`)}
packages/ui/src/components/Button/consts.ts (1)

4-16: 타입 안전성 개선: satisfies 사용 고려

현재도 충분히 타입 안전하지만, 리터럴 객체에 satisfies를 적용하면 불필요한 재선언을 줄이면서 키 누락을 컴파일 타임에 잡을 수 있습니다.

아래처럼 변환 가능합니다.

-export const BUTTON_SIZE: { [k in ButtonProps['size']]: string } = {
+export const BUTTON_SIZE = {
   large: 'ui:h-[60px] ui:min-h-[60px] ui:w-[335px] ui:min-w-[335px]',
   medium: 'ui:h-[50px] ui:min-h-[50px] ui:w-[180px] ui:min-w-[180px]',
   small: 'ui:h-[40px] ui:min-h-[40px] ui:w-[80px] ui:min-w-[80px]',
-}
+} satisfies Record<ButtonProps['size'], string>
 
-export const BUTTON_FONT_SIZE: {
-  [k in ButtonProps['size']]: TypographyVariant
-} = {
+export const BUTTON_FONT_SIZE = {
   large: 'heading1',
   medium: 'heading2',
   small: 'title1',
-}
+} satisfies Record<ButtonProps['size'], TypographyVariant>
packages/ui/src/components/Text/Text.stories.tsx (3)

64-75: Tailwind 접두사 누락으로 간격 클래스 미적용 가능

UI 패키지가 ui: 프리픽스를 사용 중인데, 여기서는 space-y-2가 일반 클래스여서 적용되지 않을 수 있습니다. ui:space-y-2로 교체를 권장합니다.

-    <div className='space-y-2'>
+    <div className='ui:space-y-2'>

34-49: variant 컨트롤에서 undefined 옵션은 혼동 가능

Storybook select 컨트롤에서 undefined를 값으로 노출하면 사용자가 선택 시 문자열 'undefined'로 전달될 수 있어 혼동을 줄 수 있습니다. 기본값/리셋은 Controls 패널의 Reset으로 처리하거나, 옵션 라벨을 '없음' 등의 명시적 값으로 바꾸는 것을 권장합니다.

가능한 대안:

  • 옵션에서 undefined 제거(기본 args가 이미 variant: 'body1').
  • 또는 mapping을 사용해 '없음' -> undefined로 매핑.

4-11: 메타 타입 표기는 satisfies 패턴 고려 가능

const meta = { ... } satisfies Meta<typeof Text> 형태를 쓰면 과잉 속성 방지와 추론 개선에 도움이 됩니다. 현재도 문제는 없으므로 선택 사항입니다.

예시(참고용):

const meta = {
  title: 'Typography/Text',
  component: Text,
  args: { children: 'Sample Text', variant: 'body1' },
  argTypes: { /* ... */ },
} satisfies Meta<typeof Text>
packages/ui/src/components/Icon/Icon.tsx (1)

5-16: 하드코딩된 색상 값을 별도 파일로 분리하는 것을 권장합니다

IconColor 객체에 하드코딩된 색상 값들이 있는데, 이를 별도의 색상 상수 파일이나 테마 설정으로 분리하면 유지보수성이 향상됩니다.

별도 파일로 분리하는 방안:

+// colors.ts 파일 생성
+export const COLORS = {
+  MAIN: '#313D4C',
+  BLUE: '#3182F7',
+  RED: '#EF4452',
+  WHITE: '#FFFFFF',
+  GRAY_50: '#F3F4F6',
+  GRAY_100: '#E4E7EB',
+  GRAY_200: '#B0B9C2',
+  GRAY_300: '#6C7887',
+  GRAY_400: '#3D3D3D',
+  GRAY_500: '#403E3F',
+} as const

-const IconColor = {
-  '--color-main': '#313D4C',
-  '--color-blue': '#3182F7',
-  // ... 나머지 색상들
-} as const
+const IconColor = {
+  '--color-main': COLORS.MAIN,
+  '--color-blue': COLORS.BLUE,
+  // ... 나머지 색상들
+} as const
packages/ui/src/components/Header/Header.tsx (1)

23-34: OnlyLeftHeader 컴포넌트의 인라인 props 타입 정의

OnlyLeftHeader의 props 타입을 인라인으로 정의하는 것보다 별도 타입으로 정의하는 것이 일관성과 가독성 측면에서 더 좋습니다.

타입을 별도로 정의하도록 개선:

+type OnlyLeftHeaderProps = {
+  icon: IconType
+  name: string
+}

-export const OnlyLeftHeader = ({
-  icon,
-  name,
-}: {
-  icon: IconType
-  name: string
-}) => (
+export const OnlyLeftHeader = ({
+  icon,
+  name,
+}: OnlyLeftHeaderProps) => (
packages/ui/src/components/Textarea/Textarea.stories.tsx (2)

16-26: Wrapper가 전달받은 props로 value/setValue를 덮어쓸 수 있음.

현재 <Textarea value={value} setValue={setValue} {...props} />에서 props가 뒤에 와서 외부에서 value/setValue를 덮어쓸 수 있습니다. 스토리 목적상 Wrapper가 항상 제어하도록 하려면 스프레드 순서를 바꾸는 편이 안전합니다.

적용 예시:

-      <Textarea value={value} setValue={setValue} {...props} />
+      <Textarea {...props} value={value} setValue={setValue} />

22-22: Tailwind 클래스 ui:w-100 대신 ui:w-full 권장.

Tailwind 기본 유틸에는 w-100가 없고(w-[100px], w-full 등 사용), 커스텀 스케일이 없다면 빌드 시 무시될 수 있습니다.

-    <div className={'ui:w-100'}>
+    <div className={'ui:w-full'}>
packages/ui/src/components/Icon/IconMap.ts (1)

94-98: 키 이름 오타 의심: kakkoLogokakaoLogo (+ 하위 호환 보장).

의도된 별칭이 아니라면 오타로 보입니다. 기존 사용처가 있을 수 있으므로 당장은 둘 다 제공하고, 다음 메이저에 kakkoLogo 제거를 권장합니다.

   smile: Smile,
   cry: Cry,
-  kakkoLogo: KakaoLogo,
+  kakkoLogo: KakaoLogo, // deprecated: will be removed
+  kakaoLogo: KakaoLogo,
   crosshairs: Crosshairs,

기존 사용 코드에서 어떤 키를 쓰고 있는지 확인이 필요하면 알려주세요. 검색/치환 스크립트 제공하겠습니다.

packages/ui/src/components/Header/Header.stories.tsx (1)

18-22: Tailwind 클래스 ui:w-100 대신 ui:w-full 권장.

w-100은 기본 제공되지 않습니다. 커스텀 설정이 없다면 w-full이 안전합니다.

-  <div className={'ui:mx-auto ui:bg-gray-100 ui:h-[800px] ui:w-100'}>
+  <div className={'ui:mx-auto ui:bg-gray-100 ui:h-[800px] ui:w-full'}>
packages/ui/src/components/Layout/Layout.stories.tsx (1)

5-9: 스토리 그룹명 케이스 통일 제안.

다른 스토리 파일들이 Components/...를 사용하고 있어 보입니다. 일관성을 위해 대문자로 맞추는 것을 권장합니다.

-  title: 'components/Layout',
+  title: 'Components/Layout',
packages/ui/src/components/Chip/Chip.tsx (2)

80-101: 접근성 및 기본 속성 개선 제안.

  • 토글 가능한 UI이므로 aria-pressed를 추가하면 접근성이 향상됩니다.
  • 기본이 button일 때만 type="button"을 부여하세요(다른 태그에는 제외).
     <Component
       className={cn(
         'ui:w-fit',
         'ui:bg-gray-50',
         'ui:rounded-full',
         'ui:flex',
         'ui:gap-1',
         'ui:px-2.5 ui:py-1',
         'ui:items-center',
         'ui:border-2 ui:border-gray-50',
         { 'ui:border-blue': isActive },
         className,
       )}
       onClick={onClick}
+      aria-pressed={isActive}
+      type={isButton ? 'button' : undefined}
       {...restProps}
     >

38-41: children 타입 제거 고려.

실제 렌더링에서 children을 사용하지 않습니다. API를 단순화하려면 PropsWithChildren를 제거하세요.

-export type ChipType = <C extends ElementType = 'button'>(
-  props: PropsWithChildren<ChipProps<C>>,
-) => JSX.Element
+export type ChipType = <C extends ElementType = 'button'>(
+  props: ChipProps<C>,
+) => JSX.Element
packages/ui/src/components/Button/Button.stories.tsx (1)

13-16: as 컨트롤을 select로 노출하여 사용성 개선 제안

폴리모픽 데모 목적이라면 대표 케이스 위주로 옵션을 제한(예: 'button' | 'a')하면 스토리에서 실수를 줄일 수 있습니다. 일반화가 더 중요하다면 현재도 OK입니다.

적용 diff:

-    as: {
-      control: { type: 'text' },
-      description: 'Polymorphic prop: 렌더링할 HTML 태그나 컴포넌트',
-    },
+    as: {
+      control: { type: 'select' },
+      options: ['button', 'a'],
+      description: 'Polymorphic prop: 렌더링할 HTML 태그나 컴포넌트',
+    },
packages/ui/src/components/Layout/Flex/Flex.tsx (1)

14-25: ref 포워딩 고려(접근성/포커스 관리/측정 용도)

폴리모픽 레이아웃 컴포넌트는 forwardRef 지원 시 포커스 제어, 스크롤/측정에 유용합니다. 지금도 충분히 사용 가능하나, 디자인 시스템 관점에서 추후 적용을 권장드립니다.

packages/ui/src/components/Button/Button.tsx (2)

25-35: 폼 내 기본 submit 방지를 위해 기본 type="button" 지정 제안

<button>의 기본 type은 submit입니다. 의도치 않은 폼 제출을 막기 위해 버튼 요소일 때만 기본 타입을 지정하는 것을 권장합니다.

적용 diff:

     <Component
       className={cn(
         'ui:flex',
         'ui:justify-center',
         'ui:items-center',
         'ui:bg-main',
         'ui:text-white',
         'ui:rounded-lg',
         BUTTON_SIZE[size],
         className,
       )}
+      {...(as === undefined || as === 'button' ? { type: 'button' } : {})}
       {...restProps}
     >

7-11: API 사용성 개선: size를 선택적으로 두고 기본값을 'medium'으로

디자인 토큰이 명확할 때 기본값을 제공하면 사용성이 좋아집니다. 타입을 선택적으로 바꾸고, 함수 매개변수에서도 기본값을 명시하는 방식을 제안드립니다. (선호에 따라 유지해도 무방)

적용 diff:

 export interface ButtonProps {
   // color: 'primary' | 'secondary' | 'mono'
   // variant: 'filled' | 'outlined' | 'subtle' | 'text'
-  size: 'small' | 'medium' | 'large'
+  size?: 'small' | 'medium' | 'large'
 }
-export const Button: ButtonType = ({ as, className, size, children, ...restProps }) => {
+export const Button: ButtonType = ({ as, className, size = 'medium', children, ...restProps }) => {

Also applies to: 21-21

packages/ui/src/components/Text/Text.tsx (1)

5-15: 타입 재사용성을 위해 FontSize/FontWeight 외부 노출 제안

외부에서 프리셋을 참조해야 하는 경우가 많아, 해당 타입을 export하면 DX가 좋아집니다. (선택 사항)

적용 diff:

-type FontSize =
+export type FontSize =
   | 'xs' // 12px
   | 'sm' // 14px
   | 'base' // 16px
   | 'lg' // 18px
   | 'xl' // 20px
   | '2xl' // 24px
   | '3xl' // 30px
   | '4xl' // 36px
   | '5xl' // 48px

-type FontWeight =
+export type FontWeight =
   | 'thin' // Figma: Thin, 100
   | 'extralight' // Figma: Extra Light, 200
   | 'light' // Figma: Light, 300
   | 'normal' // Figma: Regular / Normal, 400
   | 'medium' // Figma: Medium, 500
   | 'semibold' // Figma: Semi Bold, 600
   | 'bold' // Figma: Bold, 700
   | 'extrabold' // Figma: Extra Bold, 800
   | 'black' // Figma: Black, 900

Also applies to: 16-25

Comment on lines +17 to 19
"@/*": ["./src/*"],
"@repo/styles" : ["../../packages/ui/src/styles.css"]
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

paths 매핑 활성화를 위해 baseUrl이 필요하며, CSS 임포트에 대한 타입 선언도 추가하세요.

  • TypeScript에서 paths가 동작하려면 보통 baseUrl이 설정되어 있어야 합니다. 현재 파일에 baseUrl이 없어 에디터/tsc 타입체커에서 @repo/styles 해석에 실패할 수 있습니다.
  • @repo/styles.css 파일을 가리키므로, 타입체커 경고를 막기 위해 전역 선언(declare module '*.css')이 필요합니다. Storybook 런타임(웹팩/Vite)은 CSS를 처리하지만 TS 타입체커는 별개입니다.

변경 제안:

  • compilerOptions에 baseUrl 추가
{
  "compilerOptions": {
    // ...
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@repo/styles": ["../../packages/ui/src/styles.css"]
    }
  }
}
  • CSS 타입 선언 파일 추가: apps/storybook/types/styles.d.ts
declare module '*.css';

확인 스크립트:

참고:

  • Storybook이 TS 경로 별칭을 런타임에서 인식하려면, 사용 중인 빌더에 따라 tsconfig-paths 플러그인이 필요할 수 있습니다(예: Vite는 tsconfigPaths 플러그인, Webpack은 TsconfigPathsPlugin). Chromatic 프리뷰가 정상 동작하더라도, CI의 타입체크 단계에서 위 이슈로 실패할 수 있으니 확인 부탁드립니다.

🏁 Script executed:

#!/bin/bash
# baseUrl 설정 및 CSS 모듈 선언 존재 여부 확인
set -euo pipefail

echo "== baseUrl 설정 여부 =="
rg -nP '"baseUrl"\s*:\s*"' apps/storybook/tsconfig.json || echo "apps/storybook/tsconfig.json에 baseUrl 미설정"

echo
echo "== CSS 모듈 타입 선언 여부 =="
rg -nP "declare\s+module\s+['\"]\\*\\.css['\"]" -n || echo "repo 내 '*.css' 타입 선언 파일을 찾지 못했습니다"

Length of output: 418


기본 경로(baseUrl) 설정 및 CSS 타입 선언 추가 필요

TypeScript 경로 별칭과 CSS 모듈을 올바르게 해석하려면 아래 조치가 필요합니다.

– 대상 파일

  • apps/storybook/tsconfig.json
  • apps/storybook/types/styles.d.ts (신규)

– 변경 내용

  1. apps/storybook/tsconfig.json
{
  "compilerOptions": {
    // …
+   "baseUrl": ".",
    "incremental": true,
    "paths": {
      "@/*": ["./src/*"],
      "@repo/styles": ["../../packages/ui/src/styles.css"]
    }
  }
}
  1. apps/storybook/types/styles.d.ts (신규 생성)
declare module '*.css';

참고:

  • Vite 사용 시 @rollup/plugin-alias 또는 vite-tsconfig-paths 플러그인을,
    Webpack 사용 시 TsconfigPathsPlugin을 설정해야 런타임 경로 별칭이 정상 동작합니다.
  • CI의 tsc --noEmit 단계에서 에러가 발생하지 않는지 확인 부탁드립니다.
🤖 Prompt for AI Agents
In apps/storybook/tsconfig.json around lines 17-19, the current config lacks a
baseUrl and CSS module type declarations which prevents TypeScript from
resolving path aliases and importing .css files; add "baseUrl": "." under
compilerOptions and ensure the "paths" entries remain, then create a new file
apps/storybook/types/styles.d.ts containing a module declaration for '*.css'
(declare module '*.css';) so .css imports type-check; after adding these,
confirm CI passes tsc --noEmit and, if using Vite or Webpack, ensure
corresponding runtime alias plugins (vite-tsconfig-paths or TsconfigPathsPlugin)
are configured.

Comment on lines +30 to +36
export type ChipProps<C extends ElementType> = PolymorphicComponentProps<
C,
{
type: ChipTagKey
onToggle?: () => void
}
>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

type prop이 HTML button.type과 충돌합니다. 명시적 리네이밍 필요.

Polymorphic 타입 구성상 Chip의 type이 원소의 원래 type 속성을 덮어 Omit되며, 기본 버튼이 submit으로 동작하는 위험이 있습니다. 폼 내에서 예기치 않은 제출이 발생할 수 있습니다.

 export type ChipProps<C extends ElementType> = PolymorphicComponentProps<
   C,
   {
-    type: ChipTagKey
-    onToggle?: () => void
+    chipType: ChipTagKey
+    onToggle?: (next: boolean) => void
   }
 >

위 변경 후 스토리/사용처에서 typechipType로 교체가 필요합니다. 또한 onToggle에 토글 결과를 전달하면 상위에서 제어가 쉬워집니다.

📝 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
export type ChipProps<C extends ElementType> = PolymorphicComponentProps<
C,
{
type: ChipTagKey
onToggle?: () => void
}
>
export type ChipProps<C extends ElementType> = PolymorphicComponentProps<
C,
{
chipType: ChipTagKey
onToggle?: (next: boolean) => void
}
>
🤖 Prompt for AI Agents
In packages/ui/src/components/Chip/Chip.tsx around lines 30-36, the prop named
`type` collides with native element `type` (e.g. button.type) and can cause
buttons to default to `submit`; rename the prop to `chipType` in the
PolymorphicComponentProps generic and related types/props, update the component
to accept `chipType` instead of `type`, and ensure the rendered element forwards
its native `type` attribute untouched; also change `onToggle?: () => void` to
`onToggle?: (toggled: boolean) => void` so the handler receives the new toggle
state, and update all stories/usages to replace `type` with `chipType` and to
handle the new `onToggle` signature.

Comment on lines +41 to +43
useLayoutEffect(() => {
textareaRef.current?.focus()
}, [])
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

자동 포커스 동작에 대한 검토가 필요합니다

컴포넌트가 마운트될 때마다 자동으로 포커스가 설정되는데, 이는 접근성과 사용자 경험 측면에서 문제가 될 수 있습니다. 특히 여러 개의 Textarea가 있거나 의도하지 않은 포커스 이동이 발생할 수 있습니다.

자동 포커스를 선택적으로 적용할 수 있도록 개선:

 type Props = {
   value: string
   setValue: (value: React.SetStateAction<string>) => void
   placeholder?: string
   maxLength?: number
   maxHeight?: number
   className?: string
+  autoFocus?: boolean
 }

 export const Textarea = ({
   value,
   setValue,
   placeholder = '내용을 입력해주세요',
   maxHeight = 300,
   maxLength = 1000,
   className,
+  autoFocus = false,
 }: Props) => {
   const textareaRef = useRef<HTMLTextAreaElement>(null)

   useLayoutEffect(() => {
-    textareaRef.current?.focus()
+    if (autoFocus) {
+      textareaRef.current?.focus()
+    }
-  }, [])
+  }, [autoFocus])
📝 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
useLayoutEffect(() => {
textareaRef.current?.focus()
}, [])
type Props = {
value: string
setValue: (value: React.SetStateAction<string>) => void
placeholder?: string
maxLength?: number
maxHeight?: number
className?: string
autoFocus?: boolean
}
export const Textarea = ({
value,
setValue,
placeholder = '내용을 입력해주세요',
maxHeight = 300,
maxLength = 1000,
className,
autoFocus = false,
}: Props) => {
const textareaRef = useRef<HTMLTextAreaElement>(null)
useLayoutEffect(() => {
if (autoFocus) {
textareaRef.current?.focus()
}
}, [autoFocus])
// …rest of the component
}
🤖 Prompt for AI Agents
In packages/ui/src/components/Textarea/Textarea.tsx around lines 41 to 43, the
component currently always calls textareaRef.current?.focus() on mount which
forces focus for every instance; change this to be opt-in by adding an
autoFocus?: boolean prop (default false) and only call focus if that prop is
true, e.g. guard the effect with if (!autoFocus) return; and ensure you use an
effect that runs on the client (useEffect or keep useLayoutEffect if
intentional) and check textareaRef.current before focusing; update prop types
and any story/tests/docs to reflect the new optional autoFocus behavior.

'ui:caret-blue',
'ui:resize-none',
'ui:h-fit',
`ui:max-h-[${maxHeight}px]`,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동적 클래스 생성의 안정성 문제

템플릿 리터럴을 사용한 동적 클래스 생성 ui:max-h-[${maxHeight}px]은 Tailwind의 JIT 컴파일러가 해당 클래스를 인식하지 못할 수 있습니다.

인라인 스타일 또는 사전 정의된 클래스 사용을 권장:

 <textarea
   ref={textareaRef}
   value={value}
   maxLength={maxLength}
   onChange={(e) => setValue(e.target.value)}
   placeholder={placeholder}
+  style={{ maxHeight: `${maxHeight}px` }}
   className={cn(
     'ui:pt-2.5 ui:px-0.5',
     'ui:border-t ui:border-gray-100',
     'ui:outline-none',
     'ui:text-sm ui:font-regular',
     'ui:text-gray-500',
     'ui:placeholder:text-gray-200',
     'ui:caret-blue',
     'ui:resize-none',
     'ui:h-fit',
-    `ui:max-h-[${maxHeight}px]`,
     'scrollbar-hide',
     className,
   )}
 />
📝 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
`ui:max-h-[${maxHeight}px]`,
<textarea
ref={textareaRef}
value={value}
maxLength={maxLength}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
style={{ maxHeight: `${maxHeight}px` }}
className={cn(
'ui:pt-2.5 ui:px-0.5',
'ui:border-t ui:border-gray-100',
'ui:outline-none',
'ui:text-sm ui:font-regular',
'ui:text-gray-500',
'ui:placeholder:text-gray-200',
'ui:caret-blue',
'ui:resize-none',
'ui:h-fit',
'scrollbar-hide',
className,
)}
/>
🤖 Prompt for AI Agents
In packages/ui/src/components/Textarea/Textarea.tsx around line 72, the dynamic
Tailwind class `ui:max-h-[${maxHeight}px]` may not be recognized by Tailwind
JIT; replace the dynamic class with a safe solution: apply the maxHeight via a
style prop (e.g., style={{ maxHeight: `${maxHeight}px` }}) or map known height
values to predefined Tailwind classes and use that mapping, ensuring no
runtime-generated Tailwind token is emitted.

Comment on lines +1 to +12
import { ComponentProps, ElementType } from 'react'

type AsProp<C extends ElementType> = {
as?: C
}

type KeyWithAs<C extends ElementType, Props> = keyof (AsProp<C> & Props)

export type PolymorphicComponentProps<
C extends ElementType,
Props = object,
> = (Props & AsProp<C>) & Omit<ComponentProps<C>, KeyWithAs<C, Props>>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

기본 제네릭 Props=object 때문에 ComponentProps가 통째로 Omit됨 — 타입 붕괴 버그

keyof objectstring | number | symbol입니다. 현재 구현은 KeyWithAs<C, object>가 사실상 모든 키가 되어 Omit<ComponentProps<C>, ...>가 전부 제거됩니다. 그 결과, 기본 사용 시(추가 Props 없이) 기본 DOM/JSX 속성들이 사라지는 심각한 타입 문제를 유발합니다. 또한 일반적으로 ref가 없는 컴포넌트엔 ComponentPropsWithoutRef가 더 안전합니다.

아래와 같이 기본 제네릭을 {}로 바꾸고, ComponentPropsWithoutRef를 사용해주세요.

-import { ComponentProps, ElementType } from 'react'
+import { ComponentPropsWithoutRef, ElementType } from 'react'

 type AsProp<C extends ElementType> = {
   as?: C
 }

 type KeyWithAs<C extends ElementType, Props> = keyof (AsProp<C> & Props)

 export type PolymorphicComponentProps<
-  C extends ElementType,
-  Props = object,
-> = (Props & AsProp<C>) & Omit<ComponentProps<C>, KeyWithAs<C, Props>>
+  C extends ElementType,
+  Props extends object = {},
+> = (Props & AsProp<C>) &
+  Omit<ComponentPropsWithoutRef<C>, KeyWithAs<C, 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
import { ComponentProps, ElementType } from 'react'
type AsProp<C extends ElementType> = {
as?: C
}
type KeyWithAs<C extends ElementType, Props> = keyof (AsProp<C> & Props)
export type PolymorphicComponentProps<
C extends ElementType,
Props = object,
> = (Props & AsProp<C>) & Omit<ComponentProps<C>, KeyWithAs<C, Props>>
import { ComponentPropsWithoutRef, ElementType } from 'react'
type AsProp<C extends ElementType> = {
as?: C
}
type KeyWithAs<C extends ElementType, Props> = keyof (AsProp<C> & Props)
export type PolymorphicComponentProps<
C extends ElementType,
Props extends object = {},
> = (Props & AsProp<C>) &
Omit<ComponentPropsWithoutRef<C>, KeyWithAs<C, Props>>
🤖 Prompt for AI Agents
In packages/ui/src/polymorphics.ts around lines 1 to 12, the default generic
Props=object causes keyof object to be string|number|symbol which Omit's away
all ComponentProps; change the default generic to {} and switch from
ComponentProps to ComponentPropsWithoutRef to preserve DOM/JSX props and refs
for typical use; keep KeyWithAs computed from AsProp<C> & Props and ensure the
exported PolymorphicComponentProps uses the updated default and
ComponentPropsWithoutRef.

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

Labels

⚙️ Chore 환경 셋팅 ✨ Feature 기능 구현 🎨 Style 스타일 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 공통 컴포넌트 구현

2 participants