-
Notifications
You must be signed in to change notification settings - Fork 4
✨feat: 공통 컴포넌트 인풋 구현 #26
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
Changes from 6 commits
8a11dd9
d079858
242f7f8
2d9229c
5c8dc7a
3f78e48
f21f2c6
d0a0b14
793e61e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // src/components/ui/input/input.stories.tsx | ||
| import Button from '@/components/ui/button/button'; | ||
| import type { Meta, StoryObj } from '@storybook/nextjs'; | ||
| import { useState } from 'react'; | ||
| import Input from './input'; | ||
|
|
||
| const meta: Meta<typeof Input> = { | ||
| title: 'Form/Input', | ||
| component: Input, | ||
| tags: ['autodocs'], | ||
| }; | ||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Input>; | ||
|
|
||
| export const Email: Story = { | ||
| args: { id: 'email', label: '이메일', placeholder: '입력', type: 'email' }, | ||
| }; | ||
|
|
||
| export const Password: Story = { | ||
| args: { id: 'pw', label: '비밀번호', type: 'password', placeholder: '••••••••' }, | ||
| }; | ||
|
|
||
| export const PasswordConfirm_Error: Story = { | ||
| args: { | ||
| id: 'pw2', | ||
| label: '비밀번호 확인', | ||
| type: 'password', | ||
| placeholder: '••••••••', | ||
| error: '비밀번호가 일치하지 않습니다.', | ||
| }, | ||
| }; | ||
|
|
||
| /** 시급(원) — 스토리 내부에서 숫자만 허용(컴포넌트 변경 없음) */ | ||
| export const WageWithSuffix: Story = { | ||
| render: () => { | ||
| const [v, setV] = useState(''); | ||
| return ( | ||
| <Input | ||
| id='wage' | ||
| label='시급*' | ||
| requiredMark | ||
| placeholder='입력' | ||
| inputMode='numeric' | ||
| suffix='원' | ||
| value={v} | ||
| onChange={e => setV(e.currentTarget.value.replace(/\D+/g, ''))} // 숫자만 | ||
| /> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| /** 미니 폼 데모 */ | ||
| export const MiniFormDemo: Story = { | ||
| render: () => { | ||
| const [wage2, setWage2] = useState(''); | ||
| return ( | ||
| <div className='max-w-md space-y-5'> | ||
| <Input id='email2' label='이메일' placeholder='입력' type='email' /> | ||
| <Input id='pw3' label='비밀번호' type='password' placeholder='••••••••' /> | ||
| <Input | ||
| id='pw4' | ||
| label='비밀번호 확인' | ||
| type='password' | ||
| placeholder='••••••••' | ||
| error='비밀번호가 일치하지 않습니다.' | ||
| /> | ||
| <Input | ||
| id='wage2' | ||
| label='시급*' | ||
| requiredMark | ||
| inputMode='numeric' | ||
| placeholder='입력' | ||
| suffix='원' | ||
| value={wage2} | ||
| onChange={e => setWage2(e.currentTarget.value.replace(/\D+/g, ''))} // 숫자만 | ||
| /> | ||
| <Button variant='primary' size='md' full> | ||
| 제출 | ||
| </Button> | ||
| </div> | ||
| ); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { InputHTMLAttributes, ReactNode } from 'react'; | ||
|
|
||
| type Props = { | ||
| label?: string; // 라벨 텍스트 | ||
| requiredMark?: boolean; // 라벨 옆 * 표시 | ||
| error?: string; // 에러 문구(있으면 빨간 테두리/문구) | ||
| suffix?: ReactNode; // 우측 단위/아이콘(예: '원') | ||
| className?: string; // 외부 커스텀 클래스 | ||
| } & InputHTMLAttributes<HTMLInputElement>; | ||
|
|
||
| export default function Input({ | ||
| label, | ||
| requiredMark, | ||
| error, | ||
| suffix, | ||
| className = '', | ||
| id, | ||
| disabled, | ||
| ...rest // (type, placeholder, value, onChange, inputMode 등) | ||
| }: Props) { | ||
| const hasError = Boolean(error); | ||
| const isDisabled = Boolean(disabled); | ||
| const errorId = id && hasError ? `${id}-error` : undefined; | ||
|
|
||
| return ( | ||
| <div className={`flex w-full flex-col gap-1 ${className}`}> | ||
| {label && ( | ||
| <label htmlFor={id} className='text-[var(--black)]/80 text-sm'> | ||
| {label} | ||
| {requiredMark && <span className='ml-0.5 text-[var(--red-500)]'>*</span>} | ||
| </label> | ||
| )} | ||
|
|
||
| <div className='relative'> | ||
| <input | ||
| id={id} | ||
| disabled={isDisabled} | ||
| aria-invalid={hasError || undefined} | ||
| aria-describedby={errorId} | ||
| {...rest} | ||
| className={[ | ||
| 'base-input', // ← 전역 공통 | ||
| 'w-full outline-none transition', | ||
| hasError ? 'border-[var(--red-500)]' : '', | ||
| hasError ? '' : 'focus:border-[var(--red-500)]', | ||
| isDisabled ? 'cursor-not-allowed bg-[var(--gray-100)]' : '', | ||
| suffix ? 'pr-10' : '', // md 드롭다운과 동일한 오른쪽 여백 | ||
| ].join(' ')} | ||
| /> | ||
|
||
|
|
||
| {suffix && ( | ||
| <span className='pointer-events-none absolute right-5 top-1/2 -translate-y-1/2 text-sm text-[var(--gray-500)]'> | ||
|
||
| {suffix} | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {hasError && ( | ||
| <p id={errorId} className='text-xs text-[var(--red-500)]'> | ||
|
||
| {error} | ||
| </p> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
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.
text-sm 는 테일윈드에서 자체적으로 사용하는 값으로 보여지는데요!
이것을 사용하셔도 괜찮지만 line-height , letter-space(있는경우) 적용 같이 부탁드립니다.
현재 폰트셋은 globals.css + tailwind.config 에 선언되어있어서 참고해서 사용해주시면 일관성있는 텍스트가 보여질것 같습니다
(참고) 현재 base-input 은 tailwind config 에 있는 폰트로 선언되어있습니다
또한 컬러는 text-black 으로 확인되는데 /80을 주신 이유가 있으실까요!?