Skip to content
Merged
84 changes: 84 additions & 0 deletions src/components/ui/input/input.stories.tsx
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>
);
},
};
65 changes: 65 additions & 0 deletions src/components/ui/input/input.tsx
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}
Copy link
Contributor

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을 주신 이유가 있으실까요!?

{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(' ')}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

이런경우 cn 라이브러리를 사용하면 조금더 편하게 사용할 수 있습니다 cn() 함수 안에 , 로 구분해서 넣게되면 .join('') 과 같은 문자열 조합을 하지 않아도 됩니다.

또한 foo ? value : "" 조합 에서 foo && value 조합으로 변경하여 쉽게 사용할 수 있습니다 ( 빈값을 사용하지 않아도 됩니다)

          className={cn(
            'base-input w-full outline-none transition',
            hasError && 'border-red-500',
            !hasError && 'focus:border-red-500',
            isDisabled && 'cursor-not-allowed bg-gray-100',
            suffix && 'pr-10'
          )}


{suffix && (
<span className='pointer-events-none absolute right-5 top-1/2 -translate-y-1/2 text-sm text-[var(--gray-500)]'>
Copy link
Contributor

Choose a reason for hiding this comment

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

text-[var(--gray-500)] 컬러 사용할때 tailwind config 에 전부 정의가 되어 있으므로 text-gray-500 으로 사용하셔도 동일하게 나오기 떄문에 불편하게 var 작성 안하셔도 될것 같습니다 :)

{suffix}
</span>
)}
</div>

{hasError && (
<p id={errorId} className='text-xs text-[var(--red-500)]'>
Copy link
Contributor

Choose a reason for hiding this comment

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

이부분도 text-red-500 으로 사용 가능할것 같습니다 :) 그리고 text-xs 또한 위의 코멘트 참고 부탁드립니다

{error}
</p>
)}
</div>
);
}
Loading