Feat(design-system): textarea 공통 컴포넌트 구현#23
Conversation
Walkthrough디자인 시스템에 신규 Textarea 컴포넌트를 추가하고 이를 공개 재익스포트했으며, Textarea용 Storybook 스토리(인터랙션 플레이 포함)와 공용 스크롤바 유틸리티(.ds-scrollbar)를 도입했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant T as Textarea 컴포넌트
participant CSS as .ds-scrollbar 스타일
U->>T: 입력(타이핑)
T->>T: maxLength 검사 및 적용
alt 입력 길이 초과
T-->>U: 추가 입력 차단(브라우저 기본 동작)
end
U->>T: 내용이 영역 초과
T->>CSS: overflow 발생 → 스크롤바 렌더링
CSS-->>U: 스크롤 표시 (ds-scrollbar 스타일)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
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
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 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
Status, Documentation and Community
|
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (15)
packages/tailwind-config/scrollbar-styles.css (3)
2-5: Firefox 얇은 스크롤바와 토큰화 지원 추가 제안
- Firefox 전용 얇은 스크롤바(
scrollbar-width: thin)를 함께 지정하면 WebKit과 두께가 더 일치합니다.- 색상을 바로
--gray100에 의존하기보다 전용 토큰(--scrollbar-thumb,--scrollbar-track)으로 감싸두면 다크모드/테마별 조정이 수월합니다.적용 예시:
:where(.ds-scrollbar) { - /* Firefox: thumb 색, track 투명 */ - scrollbar-color: var(--gray100, #e5e7eb) transparent; + /* Firefox: thumb 색, track 투명 (전용 토큰 → gray100 fallback) */ + scrollbar-color: var(--scrollbar-thumb, var(--gray100, #e5e7eb)) + var(--scrollbar-track, transparent); + /* Firefox 얇은 스크롤바 */ + scrollbar-width: thin; }
15-17: WebKit hover/active 상태 및 모서리 반경 부여로 사용성 개선Thumb에 hover/active 시 색 변화를 주고 약간의 radius를 주면 조작성이 올라갑니다.
적용 예시:
:where(.ds-scrollbar)::-webkit-scrollbar-thumb { - background: var(--gray100, #e5e7eb); /* 토큰 없으면 fallback */ + background: var(--scrollbar-thumb, var(--gray100, #e5e7eb)); /* 토큰 → fallback */ + border-radius: 0.4rem; } + +:where(.ds-scrollbar)::-webkit-scrollbar-thumb:hover { + /* color-mix가 미지원이어도 기본색으로 표시되어 안전 */ + background: color-mix(in oklab, + var(--scrollbar-thumb, var(--gray100, #e5e7eb)) 85%, black); +} +:where(.ds-scrollbar)::-webkit-scrollbar-thumb:active { + background: color-mix(in oklab, + var(--scrollbar-thumb, var(--gray100, #e5e7eb)) 70%, black); +}
1-17: Windows 고대비 모드 호환성 보완고대비(Forced Colors) 환경에서는 시스템 색을 우선하도록 두는 것이 안전합니다.
적용 예시(파일 하단에 추가):
+:@media (forced-colors: active) { + :where(.ds-scrollbar) { + /* OS가 지정한 대비 색을 사용하도록 위임 */ + scrollbar-color: auto; + } + :where(.ds-scrollbar)::-webkit-scrollbar { + width: auto; + height: auto; + } +}packages/design-system/src/components/index.ts (1)
4-4: Textarea 컴포넌트 파일명 및 export 방식 일관화 권장현재
packages/design-system/src/components/textarea/textarea.tsx는 소문자 파일명·named export를 사용하고 있어, 다른 컴포넌트(Button/Input)의 default export·PascalCase 파일명과 불일치합니다. 일관된 DX를 위해 아래와 같이 옵션 A(파일명 PascalCase + default export)로 통일을 권장드립니다.– 변경 요약
• 파일명:textarea.tsx→Textarea.tsx
• 컴포넌트:export { Textarea };→export default Textarea;
•packages/design-system/src/components/index.ts: named → default as export
• 스토리 파일 등 import 경로: named import → default import– 예시 diff
- 컴포넌트 파일
packages/design-system/src/components/textarea/textarea.tsx→…/Textarea.tsx-export { Textarea }; +export default Textarea;
- 디렉토리 내 파일명 변경
git mv packages/design-system/src/components/textarea/textarea.tsx \ packages/design-system/src/components/textarea/Textarea.tsx
packages/design-system/src/components/index.ts-export { Textarea } from './textarea/textarea'; +export { default as Textarea } from './textarea/Textarea';
- 스토리 파일(
testarea.stories.tsx) 등 기존 사용처-import { Textarea } from './textarea'; +import Textarea from './textarea/Textarea';위 수정 후에는 lint·테스트를 통해 경로·import가 모두 올바르게 변경되었는지 확인 부탁드립니다.
packages/tailwind-config/package.json (1)
8-10: 서브패스 export 추가는 적절합니다. CSS tree-shaking 방지용 sideEffects 설정을 함께 고려일부 번들러 환경에서 CSS가 사이드이펙트로 간주되지 않아 제거될 위험을 줄이기 위해
sideEffects에*.css를 명시해두면 안전합니다(패키지가 private여도 워크스페이스 빌드에서 유효).예시:
{ "name": "@pinback/tailwind-config", "version": "0.0.0", "type": "module", "private": true, + "sideEffects": [ + "*.css" + ], "exports": { ".": "./shared-styles.css", "./shared-styles.css": "./shared-styles.css", "./scrollbar-styles.css": "./scrollbar-styles.css" },packages/design-system/styles.css (1)
4-4: 공유 CSS 중복 import 확인 및 정리 권장앱별로 아래 파일에서
@pinback/tailwind-config/shared-styles.css를 직접 import 하고 있습니다. 디자인 시스템(packages/design-system/styles.css)에서만 한 번 import 되도록 중복을 제거해주세요.
- apps/landing/src/App.css:1
- apps/extension/src/App.css:1
- apps/client/src/App.css:1
위 import 구문을 삭제하거나, 디자인 시스템의
styles.css를 통해서만 불러오도록 수정해 주세요.packages/design-system/src/components/textarea/textarea.tsx (4)
5-5: MAX_TEXTAREA_LENGTH 상수의 공용화 제안컴포넌트 내부 하드코딩 대신 디자인시스템 차원의 상수/토큰 파일로 분리하면 재사용과 변경 추적이 용이합니다. 예:
packages/design-system/src/foundations/constants.ts또는tokens.ts.원하시면 상수 파일 신설과 참조 변경 패치를 제안드릴게요.
9-13: forwardRef 적용 및 style 병합 방식 보완
- 외부에서 focus 제어 등을 위해 ref 전달이 필요한 경우가 많아
forwardRef가 표준적입니다.- 현재
style={{ scrollbarGutter: 'stable' }}뒤에{...props}를 배치해 사용자가 넘긴style이 컴포넌트의 기본 스타일을 덮어쓰는 구조입니다. 의도라면 괜찮지만, 보통은 병합이 안전합니다.적용 예시:
-type TextareaProps = React.ComponentProps<'textarea'>; +export type TextareaProps = React.ComponentProps<'textarea'>; -function Textarea({ - className, - maxLength = MAX_TEXTAREA_LENGTH, - ...props -}: TextareaProps) { +const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( + function Textarea( + { className, maxLength = MAX_TEXTAREA_LENGTH, style, ...props }, + ref + ) {그리고 렌더 부분(style 병합 및 ref 전달은 아래 코멘트 diff 참고).
15-27: style 병합, 고정 크기 완화, 비활성화 상태 스타일 추가 제안
scrollbarGutter기본값을 유지하면서 사용자style과 병합합니다.w-[24.8rem]같은 고정 폭은 재사용성을 떨어뜨립니다. DS 기본은w-full을 추천하고, 너비 제어는 컨테이너에서 맡기면 조합성이 좋아집니다.- 높이는 최소 높이로 두고 컨텐츠에 따라 커지게 하거나,
rows속성으로 제어할 수 있게 하는 편이 유연합니다.- disabled 상태 시각 피드백을 추가합니다.
적용 예시:
return ( <textarea data-slot="textarea" maxLength={maxLength} className={cn( - 'h-[12rem] w-[24.8rem]', - 'resize-none overflow-y-auto', - 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', + 'min-h-[12rem] w-full', + 'resize-none overflow-y-auto', + 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', + 'disabled:cursor-not-allowed disabled:opacity-50', 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', 'ds-scrollbar', className )} - style={{ scrollbarGutter: 'stable' }} - {...props} + ref={ref} + style={{ scrollbarGutter: 'stable', ...style }} + {...props} /> );
16-24: ds-scrollbar 적용 범위를 data-slot과도 매핑하는 대안이미 컴포넌트에
ds-scrollbar를 포함해두신 점 아주 좋습니다. 추가로 전역 CSS에서[data-slot="textarea"]를 함께 대상으로 잡아두면, 추후 실수로ds-scrollbar를 제거해도 안전합니다.관련 CSS(패키지: tailwind-config/scrollbar-styles.css)에 다음을 추가하는 방안:
-:where(.ds-scrollbar) { ... } +:where(.ds-scrollbar, [data-slot="textarea"]) { ... }이렇게 하면 Textarea는 data-slot만으로도 일관된 스크롤바가 적용됩니다.
packages/design-system/src/components/textarea/testarea.stories.tsx (5)
1-1: 파일명/주석 불일치 정리 권장상단 주석은 Textarea.stories.tsx인데 실제 파일명은 testarea.stories.tsx로 보입니다. 팀 컨벤션과 스토리 자동 탐색/정렬 시 혼선을 줄이기 위해 파일명을 Textarea.stories.tsx로 통일하는 것을 권장합니다.
32-35: 중복 기본값 제거: meta.args의 maxLength(500)컴포넌트 내부 기본값이 500이라면 스토리 meta.args에서 동일 값을 다시 지정할 필요는 없습니다. 단일 소스화로 유지보수 비용을 낮추세요.
args: { placeholder: '나중에 내가 꺼내줄 수 있게 살짝 적어줘!', - maxLength: 500, },
64-78: maxLength 동작 테스트 보강 제안현재도 충분히 동작 검증이 되지만, 아래 두 가지를 추가하면 회귀를 더 잘 잡아낼 수 있습니다.
- DOM 속성(ta.maxLength)이 args와 일치하는지 확인
- 제한 길이에 도달한 뒤 추가 타이핑이 무시되는지 확인
export const PreventOverflowByMaxLength: Story = { name: '입력 길이 제한 동작', args: { maxLength: 50, placeholder: '최대 50자' }, render: (args) => <Textarea {...args} />, play: async ({ canvasElement, args }) => { const limit = Number(args.maxLength ?? 500); const canvas = within(canvasElement); const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement; + // 속성 자체가 올바르게 전달되었는지 확인 + expect(ta.maxLength).toBe(limit); + await userEvent.click(ta); await userEvent.type(ta, 'a'.repeat(limit + 10)); // 제한 초과 입력 시도 await expect(ta.value.length).toBe(limit); + + // 추가 입력이 무시되는지 재확인 + const prev = ta.value.length; + await userEvent.type(ta, 'b'); + expect(ta.value.length).toBe(prev); }, };
1-79: 상수 관리 및 스크롤바 적용 편의성에 대한 제안
- maxLength 기본값(500)의 단일 소스화: 컴포넌트 내부의 MAX_TEXTAREA_LENGTH(500)를 기준으로, 스토리/문서에서는 상수를 직접 적기보다 import하여 재사용하거나(디자인 시스템에서 공개가 가능하다면) 주석으로만 언급하는 방식을 권장합니다. 추후 값 변경 시 누락 위험을 줄입니다.
- ds-scrollbar 적용 편의성: 매 소비자마다 클래스를 직접 붙여야 하는 비용이 있으니, Textarea에 기본 적용하고 필요 시 비활성화할 수 있는 prop(예: useScrollbarStyle = true)을 제공하는 패턴도 고려해볼 만합니다. 또는 디자인 시스템 공용 글로벌 스타일을 앱 루트에서 import하는 컨벤션(learned: shared-styles.css 활용 계획)과 함께 “스크롤 가능한 컴포넌트는 기본적으로 ds-scrollbar를 포함한다”는 규칙을 문서화하는 것도 방법입니다.
51-62: ScrollableOverflow 스토리에 커스텀 스크롤바 클래스 추가 및await제거 검토
- 스토리 args 수정 (packages/design-system/src/components/textarea/testarea.stories.tsx:51-62)
커스텀 스크롤바 스타일은ds-scrollbar클래스가 붙은 요소에만 적용되므로, 데모용 스토리에서 해당 클래스를 명시적으로 부여합니다.- 불필요한
await제거
expect는 동기 호출이므로await없이 바로 사용해도 무방합니다.- CSS 로드 위치 확인
packages/design-system/styles.css4행에서@pinback/tailwind-config/scrollbar-styles.css가 import됨을 확인했습니다.packages/design-system/.storybook/preview.ts에서../styles.css가 전역으로 import되어 있어 스토리북 환경에도 스크롤바 스타일이 적용됩니다.제안하는 변경사항:
export const ScrollableOverflow: Story = { name: '넘치면 스크롤', args: { - defaultValue: 'o '.repeat(400), + defaultValue: 'o '.repeat(400), + className: 'ds-scrollbar', }, render: (args) => <Textarea {...args} />, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement; - await expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight); + expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight); }, };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/components/textarea/testarea.stories.tsx(1 hunks)packages/design-system/src/components/textarea/textarea.tsx(1 hunks)packages/design-system/styles.css(1 hunks)packages/tailwind-config/package.json(1 hunks)packages/tailwind-config/scrollbar-styles.css(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.657Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
📚 Learning: 2025-08-19T17:18:57.657Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.657Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Applied to files:
packages/design-system/styles.css
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This single import pulls in all of Tailwind's preflight, components, and utilities in one step and is used with the tailwindcss/vite plugin.
Applied to files:
packages/design-system/styles.css
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This is used with the tailwindcss/vite plugin.
Applied to files:
packages/design-system/styles.css
🧬 Code graph analysis (2)
packages/design-system/src/components/textarea/testarea.stories.tsx (2)
packages/design-system/src/components/textarea/textarea.tsx (1)
Textarea(32-32)packages/design-system/src/components/index.ts (1)
Textarea(4-4)
packages/design-system/src/components/textarea/textarea.tsx (3)
packages/design-system/src/components/index.ts (1)
Textarea(4-4)packages/design-system/src/lib/utils.ts (1)
cn(108-110)packages/design-system/src/components/Textarea.tsx (1)
Textarea(8-28)
🔇 Additional comments (1)
packages/design-system/src/components/textarea/testarea.stories.tsx (1)
2-2: 공식 Storybook 문서에 따른 타입 import 유지 요청
현재@storybook/react-vite(v9.1.3) 기반 Vite 빌더를 사용 중이므로,Meta와StoryObj타입은 문서에서 안내하듯이@storybook/react-vite에서 가져와야 합니다. 별도@storybook/react패키지 설치나 import 경로 변경은 불필요하며, 현 상태를 유지해 주시기 바랍니다. (storybook.js.org)Likely an incorrect or invalid review comment.
| export const WithMaxLength100: Story = { | ||
| name: 'MaxLength=500', | ||
| args: { maxLength: 500, placeholder: '최대 500자' }, | ||
| render: (args) => <Textarea {...args} />, | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
스토리 명/내용 불일치 수정 (WithMaxLength100)
식별자명은 100을 가리키는데 표시 이름(name)과 args는 500으로 지정되어 있습니다. 혼란을 피하기 위해 실제로 100 제한 시나리오를 보여주도록 정정 바랍니다.
export const WithMaxLength100: Story = {
- name: 'MaxLength=500',
- args: { maxLength: 500, placeholder: '최대 500자' },
+ name: 'MaxLength=100',
+ args: { maxLength: 100, placeholder: '최대 100자' },
render: (args) => <Textarea {...args} />,
};📝 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 const WithMaxLength100: Story = { | |
| name: 'MaxLength=500', | |
| args: { maxLength: 500, placeholder: '최대 500자' }, | |
| render: (args) => <Textarea {...args} />, | |
| }; | |
| export const WithMaxLength100: Story = { | |
| name: 'MaxLength=100', | |
| args: { maxLength: 100, placeholder: '최대 100자' }, | |
| render: (args) => <Textarea {...args} />, | |
| }; |
🤖 Prompt for AI Agents
In packages/design-system/src/components/textarea/testarea.stories.tsx around
lines 45 to 49, the story identifier WithMaxLength100 is inconsistent with the
name and args which use 500; change the story to reflect a 100-character
scenario by updating the name to 'MaxLength=100', set args.maxLength to 100, and
update the placeholder to '최대 100자' so identifier, display name, and args all
match.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
packages/design-system/src/components/textarea/testarea.stories.tsx (5)
5-29: Meta 선언에satisfies사용으로 타입 안정성 강화
const meta = {...} satisfies Meta<typeof Textarea>패턴은 불필요한 속성 유입을 막고, 제네릭 narowing을 개선합니다. 스토리 args 추론도 더 안정적입니다.-const meta: Meta<typeof Textarea> = { +const meta = { title: 'Components/Textarea', component: Textarea, tags: ['autodocs'], parameters: { layout: 'centered', docs: { description: { - component: - '고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' + - '`maxLength`로 글자수 제한을 변경할 수 있습니다.', + component: + '고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' + + '`maxLength`로 글자수 제한을 변경할 수 있습니다.', }, }, }, argTypes: { className: { table: { disable: true } }, maxLength: { control: { type: 'number', min: 1 } }, placeholder: { control: 'text' }, defaultValue: { control: 'text' }, }, args: { placeholder: '나중에 내가 꺼내줄 수 있게 살짝 적어줘!', maxLength: 500, }, -}; +} satisfies Meta<typeof Textarea>;
14-15: 기본 글자수(500) 하드코딩 제거 — 컴포넌트 상수와 싱크 유지Textarea 컴포넌트 내부에
MAX_TEXTAREA_LENGTH = 500이 선언되어 있다면(또는 앞으로 중앙화 예정이라면), 스토리의 설명/args에도 같은 상수를 사용해 드리프트를 방지하는 것이 좋습니다.
- 옵션 A:
textarea.tsx에서export const MAX_TEXTAREA_LENGTH = 500로 노출- 옵션 B:
packages/design-system/src/constants/textarea.ts등으로 분리하여 양쪽에서 import적용 예시(diff는 상수 export가 가능하다는 가정):
@@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MAX_TEXTAREA_LENGTH } from './textarea'; @@ - component: - '고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' + - '`maxLength`로 글자수 제한을 변경할 수 있습니다.', + component: + `고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **${MAX_TEXTAREA_LENGTH}자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ` + + '`maxLength`로 글자수 제한을 변경할 수 있습니다.', @@ - maxLength: 500, + maxLength: MAX_TEXTAREA_LENGTH,PR 설명의 “상수 중앙 관리” 논의와도 맞물립니다. 디자인 토큰/컴포넌트 상수의 소스오브트루스를 단일화해 주세요.
Also applies to: 25-28
69-69:expect는 Promise가 아닙니다 —await제거 권장간결성과 테스트 실행 성능 측면에서
await제거를 추천합니다.- await expect(ta.value.length).toBe(limit); + expect(ta.value.length).toBe(limit);
1-1: 파일명 오타 의심:testarea.stories.tsx→textarea.stories.tsx컴포넌트명 및 디렉터리명과의 일관성을 위해 파일명을
textarea.stories.tsx로 변경하는 것이 검색/탐색성 측면에서 유리합니다. 스토리북은 파일명에 의존하지 않지만, 레포 관리 관점에서는 혼동을 줄일 수 있습니다.
44-55: ScrollableOverflow 스토리:ds-scrollbar클래스 지정 불필요 및await제거 제안Textarea 컴포넌트 내부에서 이미
ds-scrollbar클래스를 적용하고 있으므로 스토리의args에 중복 지정할 필요가 없습니다. 또한expect는 Promise가 아니므로await를 제거하여 코드를 더 간결하게 유지할 수 있습니다.
- 파일:
packages/design-system/src/components/textarea/testarea.stories.tsx
args에서className: 'ds-scrollbar'제거play함수에서- await expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight);
- expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight);
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/design-system/src/components/textarea/testarea.stories.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.657Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
🧬 Code graph analysis (1)
packages/design-system/src/components/textarea/testarea.stories.tsx (2)
packages/design-system/src/components/textarea/textarea.tsx (1)
Textarea(32-32)packages/design-system/src/components/index.ts (1)
Textarea(4-4)
🔇 Additional comments (2)
packages/design-system/src/components/textarea/testarea.stories.tsx (2)
38-42: WithMaxLength500 스토리: 명칭과 args 일치 — OK이전 커밋에서 지적되었던 명칭/args 불일치가 해소되었습니다. 현재 식별자·표시 이름·args가 모두 500으로 일관됩니다.
1-1: 스토리북 타입 import 경로 유지:@storybook/react-vite가 올바릅니다.검토 결과,
packages/design-system/package.json내에는@storybook/react-vite@9.1.3만이 의존성으로 등록되어 있고,@storybook/react는 포함되어 있지 않습니다.
또한 Vite 환경에서 Storybook을 사용하는 경우 공식 가이드에 따라@storybook/react-vite에서 타입을 가져오는 것이 권장됩니다.
따라서 기존 import 구문을 그대로 유지해 주세요.—
Likely an incorrect or invalid review comment.
constantly-dev
left a comment
There was a problem hiding this comment.
수고하셨습니다 몇 개 코멘트 남겼으니 확인부탁드려요~
| args: { | ||
| defaultValue: 'o '.repeat(400), | ||
| }, |
There was a problem hiding this comment.
o 반복보다 글이나 시라도 넣는 것은 어떨까요?
There was a problem hiding this comment.
코드래빗 시 넣는것도 재밌을것 같아요
|
|
||
| const MAX_TEXTAREA_LENGTH = 500; | ||
|
|
||
| type TextareaProps = React.ComponentProps<'textarea'>; |
There was a problem hiding this comment.
shadcn을 사용하셔서 그렇기는 하지만 저희의 컨벤션 상 object는 interface를 사용하기로 했기 때문에 이 부분 interface로 반영하고, textarea 기본 속성 extends하면 될 것 같아요!
shadcn이 단순 라이브러리가 아니라 코드 베이스니까 자유롭게 커스텀이 가능하다는 장점을 가지고 있으니, 컨벤션을 지켜주세요!
| type TextareaProps = React.ComponentProps<'textarea'>; | |
| interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> { |
| data-slot="textarea" | ||
| maxLength={maxLength} | ||
| className={cn( | ||
| 'h-[12rem] w-[24.8rem]', |
There was a problem hiding this comment.
하나 질문이 있습니다!
현재 GUI로는 메모 부분에 textarea를 사용하기는 하는데, 이후 확장성을 고려하면 width값이 매번 같지 않아 고정이 안될 것 같은데 어떻게 생각하시나요? 그런 관점에서는 width를 고정하는 것이 아닌, w-full을 주고 외부 container의 width값을 조정해서 크기를 맞추는 것은 어떨까요??
높이는 조금 더 고민해봐야 할 것 같지만, 일단 width 먼저 생각해봐야 할 것 같아요.
정민님은 이에 대해서 어떻게 생각하시나요?
There was a problem hiding this comment.
width는 확장성을 고려해서 w-full을 주고 외부 container의 width값을 조정해서 크기를 맞추는 것이 좋을것 같다고 생각합니다 반응형이나 다른 사이즈가 추가 된다면 w-full주는 방식이 좋아보여요.
높이는 아직 고정해서 사용하는게 좋릏 것 같다고 생각합니다-!!
There was a problem hiding this comment.
shared-styles.css가 있는 입장에서 스크롤 바에만 해당되는 scrollbar-styles.css 패키지를 분리하신 이유가 궁금합니다!
특히 안에 스크롤 폭/높이가 고정 값으로 담겨있는데, 이렇게 되면 다른 스타일이 있는 스크롤바에서는 재사용이 불가능할 것 같아서 패키지의 의미가 크게 없다고 생각해요.
스타일을 분리할 목적이라면 기본 스크롤 없애는 스타일만 유틸리티로 shared-styles.css에 만들고 사용하는 곳에서 className에 만든 유틸리티 넣어서 기본 스타일 제거하고, 추가 스타일은 사용하는 곳에서 정의하는 것은 어떤가요??
물론 기본 스타일 높이/폭 등도 적용한 유틸리티를 만들어서 재사용하고 싶으시다면 디자이너 분들께 스크롤바를 사용하는 곳에서 스타일을 통일하는 방향으로 디자인을 해주시면 감사하겠다고 전달해야 할 것 같아요.
정민님 생각이 궁금합니다!
There was a problem hiding this comment.
스타일 초기화하기위해 scrollbar-styles.css 패키지 만들었는데 생각해보니 shared-styles.css 안에 같이 정의하는게 좋을것같다고 판단되네요 이 부분은 수정하겠습니다-!
디자이너 분들께 전달해서 스크롤바를 사용하는 곳에서 스타일을 통일하는 방향으로 디자인 통일하기로했습니다-!
| import * as React from 'react'; | ||
| import { cn } from '@/lib/utils'; | ||
|
|
||
| const MAX_TEXTAREA_LENGTH = 500; |
There was a problem hiding this comment.
MAX_TEXTAREA_LENGTH는 모든 textarea에서 고정인가요? 해당 값을 props로 받는 것에 대해서 어떻게 생각하시나요?
There was a problem hiding this comment.
기능 명세서에 500자 제한으로 되어있어 textarea에서 선언했던건데 사용처에서 props로 받는 방식이 안헷갈리고 더 좋을것같다는 생각이드네요 수정하겠습니다-!
스크롤은 기본적으로 요소가 height값을 넘어설 때 자동으로 생기게 하면 될 것 같아요. |
일단 제 생각은 컨벤션대로 PacalCase를 사용하는 것이 맞다고 생각합니다! shadcn-ui/ui#962 따라서 두 가지 방법이 있을 것 같은데,
그냥 바꾸는 건 어렵지 않으니 1번으로 해도 될 것 같은데, 다만 add하고 파일명 수정할 때 OS에 따라 아마 윈도우..? 파일 이름 변경하고 대/소문자 관련 문제가 있던 경우가 있어서, 한번 바꾸고 체크해봐야겠네요 👍 |
이 부분은 리뷰로 달았으니 참고해주세요 👍 |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (8)
packages/design-system/src/components/textarea/Textarea.tsx (3)
23-30: 고정 폭(w-[24.8rem]) 대신 w-full(+max-w) 기본값 권장컴포넌트 기본 폭을 고정하면 레이아웃 재사용성이 떨어집니다. 기본을 w-full로 두고, Figma 스펙은 max-w로 제한하면 사용성이 좋아집니다. twMerge 덕에 외부 className이 우선이지만, 기본값 자체를 더 범용적으로 설정하는 편이 안전합니다.
- 'h-[12rem] w-[24.8rem]', + 'h-[12rem] w-full max-w-[24.8rem]',
26-29: scrollbar-gutter + pr 동시 적용 시 우측 여백 과도 가능성 → 조건부 padding으로 정리현재 'scrollbarGutter: stable'로 스크롤바 거터가 항상 예약되는데, 동시에 'pr-[1.4rem]'을 고정 적용하면 환경에 따라(오버레이 스크롤바, 플랫폼별 차이) 우측 여백이 과도해질 수 있습니다. stableScrollbarGutter가 true일 때는 pr 제거, false일 때에만 pr을 주는 형태를 권장합니다.
- 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', + 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem]', 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', - 'ds-scrollbar', + 'ds-scrollbar', + stableScrollbarGutter ? undefined : 'pr-[1.4rem]', className
5-5: MAX_TEXTAREA_LENGTH 상수 중앙화 제안여러 컴포넌트에서 재사용될 여지가 있으므로 상수를 별도 모듈로 이동해 단일 출처로 관리하는 것을 권장합니다. 현 파일에서는 import 후 재-exports 하여 기존 퍼블릭 API도 유지하세요.
예시:
- 새 파일 생성: packages/design-system/src/constants/form.ts
// packages/design-system/src/constants/form.ts export const MAX_TEXTAREA_LENGTH = 500 as const;
- 본 컴포넌트 수정:
-import { cn } from '@/lib/utils'; - -export const MAX_TEXTAREA_LENGTH = 500; +import { cn } from '@/lib/utils'; +import { MAX_TEXTAREA_LENGTH } from '@/constants/form';
- 파일 하단에 재-exports 추가(퍼블릭 API 유지 목적):
export { MAX_TEXTAREA_LENGTH };원하시면 관련 파일(Index barrel 포함)까지 일괄 리팩터링 패치 드리겠습니다.
packages/design-system/src/components/textarea/Textarea.stories.tsx (5)
1-1: Storybook 타입 import를 '@storybook/react'로 교체 권장프레임워크/빌더에 덜 결합된 공식 권장 import는 '@storybook/react'입니다. v8 기준으로도 해당 경로 사용을 권장합니다.
-import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from '@storybook/react';
19-28: stableScrollbarGutter를 컨트롤러에 노출해 시나리오 검증 확대컴포넌트가 제공하는 핵심 옵션(stableScrollbarGutter)을 스토리 argTypes/args에 노출하면 QA/디자인 리뷰가 용이합니다.
argTypes: { className: { table: { disable: true } }, maxLength: { control: { type: 'number', min: 1 } }, placeholder: { control: 'text' }, defaultValue: { control: 'text' }, + stableScrollbarGutter: { control: 'boolean' }, }, args: { placeholder: '나중에 내가 꺼내줄 수 있게 살짝 적어줘!', maxLength: 500, + stableScrollbarGutter: true, },
38-42: WithMaxLength500 스토리는 Basic과 중복됨기본 args가 이미 500이므로 중복 케이스입니다. 유지하려면 1000 등 차별화된 값으로 바꾸거나 삭제를 제안합니다.
export const WithMaxLength500: Story = { - name: 'MaxLength=500', - args: { maxLength: 500, placeholder: '최대 500자' }, + name: 'MaxLength=1000', + args: { maxLength: 1000, placeholder: '최대 1000자' }, render: (args) => <Textarea {...args} />, };
44-55: 오버플로 스토리의 maxLength 상호작용 제거로 테스트 안정성 강화브라우저마다 defaultValue와 maxlength 상호작용이 다를 수 있어(일반적으로 defaultValue는 잘려나가지 않지만) 명시적으로 큰 maxLength를 지정해 테스트 안정성을 높이는 것을 권장합니다.
export const ScrollableOverflow: Story = { name: '넘치면 스크롤', args: { - defaultValue: 'o '.repeat(400), + defaultValue: 'o '.repeat(400), + maxLength: 1000, }, render: (args) => <Textarea {...args} />,
57-71: maxLength 플레이라이트 테스트 적절 — 추가로 이모지(서로게이트) 케이스 문서화 고려현재 테스트는 영문자 기준이며, 브라우저의 maxLength는 UTF-16 코드 유닛 기준이라 이모지 카운트가 사용자 기대와 다를 수 있습니다. 문서에 주석을 추가하거나 별도 스토리/노트로 알려주면 혼선을 줄일 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/components/textarea/Textarea.stories.tsx(1 hunks)packages/design-system/src/components/textarea/Textarea.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/index.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
🧬 Code graph analysis (2)
packages/design-system/src/components/textarea/Textarea.tsx (2)
packages/design-system/src/lib/utils.ts (1)
cn(108-110)packages/design-system/src/components/Textarea.tsx (1)
Textarea(8-28)
packages/design-system/src/components/textarea/Textarea.stories.tsx (1)
packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(12-38)
🔇 Additional comments (5)
packages/design-system/src/components/textarea/Textarea.tsx (4)
31-34: 인라인 style 병합 우선순위 적절합니다사용자 style이 마지막에 병합되어 최종 우선권을 갖는 설계가 올바릅니다.
21-29: 클래스 구성(cn + twMerge) 순서 적절기본 클래스 → 유틸리티 → 사용자 className 순으로 병합되어 외부 오버라이드가 자연스럽습니다.
12-18: forwardRef 적용이 누락되어 폼 라이브러리/포커스 제어에 제약이 있습니다디자인 시스템의 입력 컴포넌트는 ref 전달을 기본으로(react-hook-form, focus 관리, 스크롤-into-view 등) 지원하는 게 일반적입니다. forwardRef로 전환을 권장합니다.
아래처럼 변경 제안드립니다:
-export function Textarea({ - className, - maxLength = MAX_TEXTAREA_LENGTH, - stableScrollbarGutter = true, - style, - ...props -}: TextareaProps) { - return ( - <textarea - data-slot="textarea" +export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( + ({ className, maxLength = MAX_TEXTAREA_LENGTH, stableScrollbarGutter = true, style, ...props }, ref) => { + return ( + <textarea + data-slot="textarea" + ref={ref} maxLength={maxLength} className={cn( 'h-[12rem] w-[24.8rem]', 'resize-none overflow-y-auto', 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', 'ds-scrollbar', className )} style={{ ...(stableScrollbarGutter ? { scrollbarGutter: 'stable' } : {}), ...style, // 사용자가 넘긴 style이 최종 우선 }} {...props} /> - ); -} + ); + } +); + +Textarea.displayName = 'Textarea';Also applies to: 20-21, 37-38
⛔ Skipped due to learnings
Learnt from: jjangminii PR: Pinback-Team/pinback-client#22 File: packages/design-system/src/components/input/Input.tsx:5-9 Timestamp: 2025-08-23T18:37:59.985Z Learning: React 19에서는 함수 컴포넌트가 ref를 일반 prop으로 받을 수 있다. interface에서 ref: Ref<HTMLInputElement>로 정의하고 함수 컴포넌트에서 ({ ref, ...props }) => 형태로 받을 수 있어 forwardRef가 불필요하다. React 19.1.1+ 프로젝트에서는 이 방식을 사용해야 한다.Learnt from: jjangminii PR: Pinback-Team/pinback-client#22 File: packages/design-system/src/components/input/Input.tsx:5-9 Timestamp: 2025-08-23T18:37:59.985Z Learning: React 19부터 함수 컴포넌트에서 ref를 일반 prop으로 받을 수 있어 forwardRef가 더 이상 필요하지 않다. InputProps interface에서 ref: Ref<HTMLInputElement>로 정의하고 함수 컴포넌트에서 ({ ref, ...props }) => 형태로 받을 수 있다.
1-4: 폴더/파일 네이밍 컨벤션 검토 필요
packages/design-system/src/components/textarea/Textarea.tsx내부에export function Textarea정의가 단일로 존재하므로 중복 정의 이슈는 없습니다.packages/design-system/src/components/index.ts에서export { Textarea } from './textarea/Textarea';로 정상 노출되고 있습니다.packages/design-system/styles.css에서도shared-styles.css,scrollbar-styles.css를 임포트 중이어서 전역 스타일 로딩에는 문제가 없습니다.- 다만, 현재 컴포넌트 디렉토리(
textarea)는 소문자, 컴포넌트 파일(Textarea.tsx)은 PascalCase로 분리되어 있습니다.
- 팀 내 컨벤션이 “컴포넌트 폴더도 PascalCase”라면, 다른 컴포넌트 디렉토리(예:
input,switch, …)와 동일하게 일괄 리네임이 필요합니다.- 리네임 시 Windows 등 OS별 대소문자 민감도 환경에서 의도치 않은 파일 누락/충돌이 발생하지 않도록 사전 테스트를 권장합니다.
위 내용을 바탕으로 팀 컨벤션을 재확인하시고, 필요 시 디렉토리명을 조정해주세요.
packages/design-system/src/components/textarea/Textarea.stories.tsx (1)
5-18: 문서(description)와 기본 args가 컴포넌트 기본값과 일치합니다고정 크기 및 기본 500자 제한 설명이 실제 기본값과 맞습니다. 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (8)
packages/tailwind-config/shared-styles.css (2)
242-251: Firefox 대응 보강: scrollbar-width 추가
scrollbar-color는 Firefox에서만 적용되고, 크기 제어도 Firefox에서는scrollbar-width(auto/thin/none)만 지원합니다. 현재 크기 변수(--ds-scroll-size)는 Chromium/Safari에서만 의미가 있으므로 Firefox에서는 폭이 기본값으로 보일 수 있습니다.아래 한 줄을 추가해 시각적 일관성을 높이세요.
:where(.ds-scrollbar) { --ds-scroll-size: 0.4rem; --ds-scroll-thumb: var(--color-gray100); --ds-scroll-track: transparent; scrollbar-color: var(--ds-scroll-thumb) var(--ds-scroll-track); + scrollbar-width: thin; /* Firefox */ - scrollbar-gutter: stable; }참고:
scrollbar-width는 구체 px 지정이 불가합니다. 필요 시 Firefox 전용 대안 레이아웃(내부 패딩 조정 등)도 검토해보세요.
264-270: 디자인 미세 조정 제안: Thumb 시각적 품질현재 thumb는 단색+큰 라운딩입니다. 트랙과의 대비가 약할 수 있으니, 필요 시 다음을 고려할 수 있습니다.
- hover 상태에만 약한 명도 변화 도입(모션은 유지하지 않음)
- 고대비 모드 대응:
@media (prefers-contrast: more)에서--ds-scroll-thumb상향원칙적으로 괜찮지만, 테마/다크모드가 들어오면 CSS 변수만 교체해도 되도록 변수 전략만 유지하면 충분합니다.
packages/design-system/src/components/textarea/Textarea.stories.tsx (6)
14-16: 문서 설명의 사이즈 정보가 컴포넌트와 불일치설명에는 고정 너비
w-[24.8rem]으로 표기돼 있으나, 실제Textarea는w-full(부모 컨테이너 너비)에 맞춰집니다. 문서-구현 간 혼선을 줄이기 위해 정정이 필요합니다.아래처럼 서술을 맞추는 것을 제안합니다.
- '고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' + + '고정 높이 **h-[12rem]**, 너비는 **w-full(컨테이너 기준)** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' +
25-28: 중복 기본값은 제거 가능컴포넌트가 이미
maxLength기본값(500)을 제공한다면, 스토리의args에서 동일 값을 중복 지정할 필요는 없습니다. 제거하면 오히려 “컴포넌트 기본값”을 검증하는 의도도 드러납니다.args: { placeholder: '나중에 내가 꺼내줄 수 있게 살짝 적어줘!', - maxLength: 500, },
44-94: 오버플로우 검증의 안정성 향상 포인트
scrollHeight > clientHeight검증은 타이밍/폰트 로딩에 영향을 받을 수 있어 드물게 플래키할 수 있습니다. 테스트 신뢰도를 높이려면 페인트 이후로 한 프레임을 양보하거나(raf),requestIdleCallback대체 대기 등을 추가하는 방법이 있습니다.예시:
await new Promise<void>((r) => requestAnimationFrame(() => r())); const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement; await expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight);또한,
stableScrollbarGutter토글 스토리를 별도로 두고 “레이아웃 시프트가 없는지”를 시각/상호작용 테스트로 보강하면 회귀를 잡는 데 도움이 됩니다.
96-110: 입력 길이 제한 테스트: 속성 자체도 단정 추가 권장현재는 결과 값의 길이로만 검증합니다. 추가로 DOM 속성(
maxlength)이 기대대로 설정됐는지도 단정하면 회귀 방지가 강화됩니다.const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement; + // 속성 자체가 올바르게 반영되는지 먼저 확인 + await expect(ta).toHaveAttribute('maxlength', String(limit)); + await userEvent.click(ta); await userEvent.type(ta, 'a'.repeat(limit + 10)); await expect(ta.value.length).toBe(limit);
34-42: 중복 렌더 패턴 간소화(선택)여러 스토리에서 동일한
render: (args) => <Textarea {...args} />가 반복됩니다. 기본 렌더는 생략 가능하며, 스토리는args만 제공해도 됩니다. 불필요한 보일러플레이트를 줄일 수 있습니다.보정 예:
Basic,WithMaxLength500,PreventOverflowByMaxLength에서render제거- 커스텀 렌더가 필요한 스토리만 유지
38-41: 이름/표기 일관성
WithMaxLength500스토리는name: 'MaxLength=500'으로 표기되어 있습니다. 한글/영문 혼용 중인 다른 스토리들과 네이밍 규칙을 맞추면 탐색성이 좋아집니다. 예:'최대 길이 500'또는'Max length: 500'.원하시면 전체 Story 네이밍 규칙(포맷) 가이드 초안도 드리겠습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/design-system/src/components/textarea/Textarea.stories.tsx(1 hunks)packages/design-system/src/components/textarea/Textarea.tsx(1 hunks)packages/tailwind-config/shared-styles.css(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/textarea/Textarea.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/design-system/src/components/textarea/Textarea.stories.tsx (1)
packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(11-37)
🔇 Additional comments (1)
packages/design-system/src/components/textarea/Textarea.stories.tsx (1)
1-1: Storybook CSF: Vite 환경에서는@storybook/react-vite에서 타입을 임포트해야 합니다
공식 문서에 따르면 CSF 파일에서import type { Meta, StoryObj } from '@storybook/your-framework'을 사용하며, Vite 환경에서는@storybook/react-vite를 지정해야 합니다(storybook.js.org). 따라서 현재 상태 그대로 유지해 주시면 됩니다.Likely an incorrect or invalid review comment.
| scrollbar-gutter: stable; | ||
| } |
There was a problem hiding this comment.
Textarea의 stableScrollbarGutter 속성이 비활성화되지 않는 버그
.ds-scrollbar에 scrollbar-gutter: stable;가 기본 적용되어 있어, 컴포넌트에서 stableScrollbarGutter={false}로 내려도 실제로는 비활성화되지 않습니다. 현재 구현(Textarea.tsx)은 true일 때만 인라인 스타일을 추가하므로 false일 때 CSS 규칙이 그대로 남습니다.
- 옵션을 신뢰할 수 있게 하려면 인라인 스타일에서 항상 값을 명시해 우선순위로 덮어쓰는 방식이 안전합니다.
컴포넌트 쪽 수정 제안(Textarea.tsx 내 style 속성만 교체):
- style={{
- ...(stableScrollbarGutter ? { scrollbarGutter: 'stable' } : {}),
- ...style,
- }}
+ style={{
+ scrollbarGutter: stableScrollbarGutter ? 'stable' : 'auto',
+ ...style,
+ }}또는 CSS에서 기본값을 제거하고(아래 라인 삭제) 컴포넌트에서만 제어하는 것도 방법입니다.
- scrollbar-gutter: stable;🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 250-251 the rule
"scrollbar-gutter: stable;" applied to .ds-scrollbar forces the gutter on all
textareas and prevents stableScrollbarGutter={false} from taking effect; either
remove this default CSS line so the component controls the gutter, or
(preferred) update Textarea.tsx to always set an inline scrollbar-gutter style
that overrides the stylesheet — e.g. in the style prop always include
scrollbarGutter: stableScrollbarGutter ? 'stable' : 'unset' (or 'auto') so the
prop reliably enables or disables the gutter via inline priority.
| :where(.ds-scrollbar)::-webkit-scrollbar, | ||
| :where(.ds-scrollbar)::-webkit-scrollbar:hover, | ||
| :where(.ds-scrollbar)::-webkit-scrollbar:active { | ||
| width: var(--ds-scroll-size); | ||
| height: var(--ds-scroll-size); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
불필요/무효한 선택자 정리: ::-webkit-scrollbar:hover, :active
::-webkit-scrollbar 자체에는 :hover/:active 상태가 적용되지 않습니다. 현재 셀렉터 병합은 의미가 없고 규칙 파싱만 늘립니다. 기본 ::-webkit-scrollbar만 남기세요.
아래처럼 간소화:
- :where(.ds-scrollbar)::-webkit-scrollbar,
- :where(.ds-scrollbar)::-webkit-scrollbar:hover,
- :where(.ds-scrollbar)::-webkit-scrollbar:active {
+ :where(.ds-scrollbar)::-webkit-scrollbar {
width: var(--ds-scroll-size);
height: var(--ds-scroll-size);
}📝 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.
| :where(.ds-scrollbar)::-webkit-scrollbar, | |
| :where(.ds-scrollbar)::-webkit-scrollbar:hover, | |
| :where(.ds-scrollbar)::-webkit-scrollbar:active { | |
| width: var(--ds-scroll-size); | |
| height: var(--ds-scroll-size); | |
| } | |
| :where(.ds-scrollbar)::-webkit-scrollbar { | |
| width: var(--ds-scroll-size); | |
| height: var(--ds-scroll-size); | |
| } |
🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 253 to 258, the
selector group includes invalid/unnecessary pseudo-classes
(::-webkit-scrollbar:hover and ::-webkit-scrollbar:active) which have no effect
on the pseudo-element; remove the :hover and :active variants and keep only the
base ::-webkit-scrollbar selector (and its width/height declarations) to
simplify and avoid meaningless selector duplication.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/design-system/src/components/textarea/Textarea.tsx (3)
5-8: maxLength를 필수로 강제할지 재확인 + 기본값(예: 500)과 상수 추출 제안현 구현은 모든 사용처에서
maxLength를 매번 지정해야 합니다. DS의 사용성을 고려하면 기본값을 제공하고, 프로젝트 전역에서 재사용할 수 있도록 상수로 추출하는 편이 실용적입니다. PR 코멘트에서도 중앙 관리 필요성이 언급되어 있어 다음과 같이 개선을 제안합니다.import { cn } from '@/lib/utils'; +// TODO: 별도 constants 모듈로 이동을 검토하세요. +const DEFAULT_TEXTAREA_MAX_LENGTH = 500; + export interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength'> { - maxLength: number; + maxLength?: number; }아울러 JSX에서도 기본값을 사용하도록 다음과 같이 변경합니다(아래 diff는 본문 JSX 일부):
- maxLength={maxLength} + maxLength={maxLength ?? DEFAULT_TEXTAREA_MAX_LENGTH}필수로 유지하고 싶은 명확한 가이드(예: 서비스 정책상 모든 입력에 상한 필요)가 있다면 그대로 두되, 최소한 디자인 시스템 레벨에서 합의된 상수로 관리하는 것을 권장합니다.
22-22: 리사이즈 정책 고정(resize-none) — 옵션화 제안현재 사용자는 텍스트 입력이 길어도 크기를 조절할 수 없습니다. 디자인 요구에 따라 기본은 비리사이즈로 두되,
resizable같은 불리언 prop을 제공해 컨텍스트에 따라 활성화할 수 있도록 열어두는 것을 제안합니다.예시:
-export interface TextareaProps +export interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength'> { maxLength?: number; + resizable?: boolean; } - ({ className, maxLength, style, ...props }, ref) => { + ({ className, maxLength, style, resizable = false, ...props }, ref) => { ... - 'resize-none overflow-y-auto', + (resizable ? 'resize-y' : 'resize-none'), + 'overflow-y-auto',
25-25: ds-scrollbar를 컴포넌트 내부에서 상시 적용할지 여부지금은 내부적으로 항상
ds-scrollbar를 붙입니다. 디자인 시스템 전역 정책이라면 괜찮지만, 특정 페이지/컨테이너에서만 커스텀 스크롤바가 필요하다면 과도한 적용이 될 수 있습니다.옵션:
- 기본 적용 유지 +
withScrollbar(기본값 true) prop로 제어폭 제공.- 반대로 기본 미적용 + 소비자가 opt-in으로
ds-scrollbar를 넘기도록 문서화.- Tailwind 플러그인/전역 스타일 레이어에서 특정 슬롯(
data-slot="textarea") 기준으로 일괄 적용.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/design-system/src/components/textarea/Textarea.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/design-system/src/components/textarea/Textarea.tsx (1)
packages/design-system/src/lib/utils.ts (1)
cn(108-110)
🔇 Additional comments (2)
packages/design-system/src/components/textarea/Textarea.tsx (2)
28-28: 스타일 병합 순서(LTR) 적절합니다
{ scrollbarGutter: 'stable', ...style }로 사용자style이 우선 적용됩니다. 의도에 부합하며, 예외 케이스 대응도 용이합니다.
10-15: ref 포워딩 누락 — 폼 라이브러리, 포커스 관리, 스크롤-투-에러 등과의 호환을 위해 forwardRef 권장Textarea는 포커스 제어, 커서 위치, 유효성 검증 스크롤 등에서 ref 접근이 빈번합니다. DS 기본 프리미티브는
React.forwardRef패턴을 따르는 것이 일반적입니다.-export function Textarea({ - className, - maxLength, - style, - ...props -}: TextareaProps) { - return ( - <textarea +export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( + ({ className, maxLength, style, ...props }, ref) => { + return ( + <textarea + ref={ref} data-slot="textarea" - maxLength={maxLength} + maxLength={maxLength ?? DEFAULT_TEXTAREA_MAX_LENGTH} className={cn( 'h-[12rem] w-full', 'resize-none overflow-y-auto', 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', - 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', + 'focus:border-input outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2', 'ds-scrollbar', className )} style={{ scrollbarGutter: 'stable', ...style }} {...props} /> - ); -} + ); + } +); + +Textarea.displayName = 'Textarea';참고: 위 diff엔 접근성 포커스 개선도 함께 반영했습니다. 포커스 스타일 변경은 아래 별도 코멘트에서도 다룹니다.
Also applies to: 17-31
⛔ Skipped due to learnings
Learnt from: jjangminii PR: Pinback-Team/pinback-client#22 File: packages/design-system/src/components/input/Input.tsx:5-9 Timestamp: 2025-08-23T18:37:59.985Z Learning: React 19에서는 함수 컴포넌트가 ref를 일반 prop으로 받을 수 있다. interface에서 ref: Ref<HTMLInputElement>로 정의하고 함수 컴포넌트에서 ({ ref, ...props }) => 형태로 받을 수 있어 forwardRef가 불필요하다. React 19.1.1+ 프로젝트에서는 이 방식을 사용해야 한다.Learnt from: jjangminii PR: Pinback-Team/pinback-client#22 File: packages/design-system/src/components/input/Input.tsx:5-9 Timestamp: 2025-08-23T18:37:59.985Z Learning: React 19부터 함수 컴포넌트에서 ref를 일반 prop으로 받을 수 있어 forwardRef가 더 이상 필요하지 않다. InputProps interface에서 ref: Ref<HTMLInputElement>로 정의하고 함수 컴포넌트에서 ({ ref, ...props }) => 형태로 받을 수 있다.
| 'h-[12rem] w-full', | ||
| 'resize-none overflow-y-auto', |
There was a problem hiding this comment.
🛠️ Refactor suggestion
rows/height 충돌: 고정 height가 소비자 props(예: rows, style.height)를 무력화합니다
h-[12rem]가 명시되어 있어 rows나 인라인 style={{ height }}를 전달해도 효과가 없습니다. 디자인 시스템 컴포넌트는 합리적인 기본값을 주되, 소비자 제어를 막지 않는 쪽이 좋습니다. 최소 높이만 보장하고 나머지는 소비자/브라우저 기본 동작에 맡기는 형태로 변경을 권장합니다.
아래처럼 최소 높이로 완화하면 rows/style.height와도 잘 공존합니다.
- 'h-[12rem] w-full',
+ 'min-h-[12rem] h-auto w-full',📝 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.
| 'h-[12rem] w-full', | |
| 'resize-none overflow-y-auto', | |
| 'min-h-[12rem] h-auto w-full', | |
| 'resize-none overflow-y-auto', |
🤖 Prompt for AI Agents
In packages/design-system/src/components/textarea/Textarea.tsx around lines
21-22, the textarea uses a fixed height class 'h-[12rem]' which prevents
consumer props like rows or inline style={{ height }} from taking effect;
replace the fixed height with a minimum height (e.g., 'min-h-[12rem]') so the
component provides a sensible default while allowing consumers or the browser to
override height via rows/style; ensure the class list retains 'w-full' and any
overflow/resize classes and that the min-h class appears where the fixed height
was.
| 'h-[12rem] w-full', | ||
| 'resize-none overflow-y-auto', | ||
| 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', | ||
| 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', | ||
| 'ds-scrollbar', | ||
| className | ||
| )} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
접근성: 포커스 인디케이터가 충분히 눈에 띄지 않을 수 있습니다
현재 클래스는 outline/ring을 제거하고 border 색만 바꾸는데, 이는 키보드 사용자에게 포커스가 잘 보이지 않을 수 있습니다. 최소 focus-visible 상태에서 두께 있는 ring과 대비를 주길 권장합니다.
권장 수정(토큰 네이밍은 디자인 토큰에 맞춰 조정하세요):
- 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
+ 'focus:border-input outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',추가로 배경색 토큰이 있다면 focus-visible:ring-offset-2 focus-visible:ring-offset-[bg-token]도 고려해 주세요.
📝 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.
| 'h-[12rem] w-full', | |
| 'resize-none overflow-y-auto', | |
| 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', | |
| 'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0', | |
| 'ds-scrollbar', | |
| className | |
| )} | |
| // packages/design-system/src/components/textarea/Textarea.tsx | |
| className={clsx( | |
| 'h-[12rem] w-full', | |
| 'resize-none overflow-y-auto', | |
| 'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]', | |
| 'focus:border-input outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2', | |
| 'ds-scrollbar', | |
| className | |
| )} |
🤖 Prompt for AI Agents
In packages/design-system/src/components/textarea/Textarea.tsx around lines 21
to 27, the current classes remove outline/ring making keyboard focus hard to
see; update the focus-visible styles to add a thicker, high-contrast ring and
optional offset instead of fully suppressing focus indicators (e.g., add classes
like focus-visible:ring-2 focus-visible:ring-[token-foreground]
focus-visible:ring-offset-2 focus-visible:ring-offset-[token-background] and
keep focus:outline-none only for non-visible focus while ensuring focus-visible
shows the ring); use your design token names for the ring color and offset
background.
| 가슴속에 하나둘 새겨지는 별을 | ||
| 이제 다 못 헤는 것은 | ||
| 쉬이 아침이 오는 까닭이요, | ||
| 내일 밤이 남은 까닭이요, | ||
| 아직 나의 청춘이 다하지 않은 까닭입니다. | ||
|
|
||
| 별 하나에 추억과 | ||
| 별 하나에 사랑과 | ||
| 별 하나에 쓸쓸함과 | ||
| 별 하나에 동경과 | ||
| 별 하나에 시와 |
There was a problem hiding this comment.
엌,, 삭막한 테스트용 글귀에 윤동주씨 시라니,,, 감성 좋아여어 짱
| data-slot="textarea" | ||
| maxLength={maxLength} | ||
| className={cn( |
There was a problem hiding this comment.
maxLength 상수화에 대한 의견은 좋은 것 같습니다!
근데 just 질문이 있는데, 혹시 이 maxLength를 별도로 받는 이유가 있을까요?? 이 length로 글자수 제한이나 입력창 높이가 달라지는 걸까요? figma상에서는 옛날 앱잼 UI때처럼 0/500 이런 텍스트 처리 멘트가 없어져서요!
There was a problem hiding this comment.
원래 textarea에서 고정으로 지정할까싶었지만 추후 확장성을 고려해서 사용처에서 내려주는게 좋다고 판단했습니다-!
이 length로 글자수 제한은 달라지지만 ui상에서 달라지는건 없습니다
| style={{ scrollbarGutter: 'stable', ...style }} | ||
| {...props} |
There was a problem hiding this comment.
오왕 scrollbarGutter는 스크롤바 커스텀해주는 속성인건가요?
There was a problem hiding this comment.
아뇽-!! 스크롤바가 생길 때를 대비해 콘텐츠 옆에 공간을 미리 확보해서 스크롤바 등장/사라짐으로 인한 레이아웃 흔들림을 막아줍니다-!


📌 Related Issues
✅ 체크 리스트
📄 Tasks
⭐ PR Point (To Reviewer)
스크롤바 커스텀 적용 방식 결정 필요
현재 ds-scrollbar 클래스를 붙인 요소에만 적용됩니다. 피그마에있는 너비, 컬러로 공통적인 디자인 적용해뒀습니다.
스크롤이 생기는 컴포넌트에 매번 적용하기에는 번거로울거같아 다른 방법있을지 궁금합니다.
maxLength 매직 넘버 분리
Textarea.tsx 내부에 MAX_TEXTAREA_LENGTH = 500 상수로 선언해 사용했습니다. 추후 상수만 관리하는 파일을 만들어 따로 분리하여 사용하는 것에 대해 어떻게 생각하시는지 얘기해보면 좋을거같아요-!!
파일 제목 컨벤션
저희가 현재 컴포넌트에는
PascalCase사용하기로했는데 shadecn으로 생성한 파일들은carmelCase로 자동생성됩니다. 현재는 그냥 자동생성된 형식으로 파일 이름 지정해뒀는데PascalCase로변경하는게 좋을까요?📷 Screenshot
Summary by CodeRabbit
New Features
Style
Documentation