-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/#7 공통 컴포넌트 구현 #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- stories 위치 경로 수정 (packages/ui/src) - staticDirs 경로 수정(통일) (/packages/ui/public) - storybook 에서 tailwind 사용 설정
- globals.css 의 사용하지 않는 로직 삭제
WalkthroughStorybook 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
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 unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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(또는 배럴)에서 노출해 주세요.
선호 순서:
- 앱에서 import '@repo/ui' 경로만 사용 (최선)
- 불가피하면 범위를 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.tsx→index.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.tsx→index.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 configpackages/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 Previewapps/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} >추가로, 여러 아이콘에서 유사한
clipPathid를 사용하고 있습니다. 일반적으로 각<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: 컴포넌트 확장성 향상: 공용 앵커 속성/클래스 전달 지원현재
SearchBar는href만 받도록 제한되어 있습니다. 외부에서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 constpackages/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: 키 이름 오타 의심:kakkoLogo→kakaoLogo(+ 하위 호환 보장).의도된 별칭이 아니라면 오타로 보입니다. 기존 사용처가 있을 수 있으므로 당장은 둘 다 제공하고, 다음 메이저에
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.Elementpackages/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, 900Also applies to: 16-25
| "@/*": ["./src/*"], | ||
| "@repo/styles" : ["../../packages/ui/src/styles.css"] | ||
| } |
There was a problem hiding this comment.
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 (신규)
– 변경 내용
- apps/storybook/tsconfig.json
{
"compilerOptions": {
// …
+ "baseUrl": ".",
"incremental": true,
"paths": {
"@/*": ["./src/*"],
"@repo/styles": ["../../packages/ui/src/styles.css"]
}
}
}
- 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.
| export type ChipProps<C extends ElementType> = PolymorphicComponentProps< | ||
| C, | ||
| { | ||
| type: ChipTagKey | ||
| onToggle?: () => void | ||
| } | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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
}
>위 변경 후 스토리/사용처에서 type → chipType로 교체가 필요합니다. 또한 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.
| 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.
| useLayoutEffect(() => { | ||
| textareaRef.current?.focus() | ||
| }, []) |
There was a problem hiding this comment.
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.
| 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]`, |
There was a problem hiding this comment.
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.
| `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.
| 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>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기본 제네릭 Props=object 때문에 ComponentProps가 통째로 Omit됨 — 타입 붕괴 버그
keyof object는 string | 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.
| 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.
#️⃣연관된 이슈
📝작업 내용
Storybook configuration 수정 5d578fc
다형성 컴포넌트 타입 정의 추가 713f198
공통 Flex 컴포넌트 & 스토리북 구현 e030c72
Typography 컴포넌트 & 스토리북 구현 994d56a
Icon 컴포넌트 & 스토리북 추가 f8dcc6a
SearchBar 컴포넌트 & 스토리북 구현 57f31a9
Header 컴포넌트 & 스토리북 구현 36f9d3f
Textarea 컴포넌트 & 스토리북 구현 82dd54d
스크린샷 (선택)
🚀storybook: https://688ef65dab8c9268d9218107-lhmbhqcevi.chromatic.com/
💬리뷰 요구사항(선택)
Summary by CodeRabbit