Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/shared/ui/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,178 @@ export const ArrowRightIcon: React.FC<IconProps> = ({ size = 20, ...props }) =>
/>
</svg>
);

// Eye (Password Show) 아이콘
export const EyeIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M1 10C1 10 4.6 3.7 10 3.7C15.4 3.7 19 10 19 10C19 10 15.4 16.3 10 16.3C4.6 16.3 1 10 1 10Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10 12.7C11.4912 12.7 12.7 11.4912 12.7 10C12.7 8.50883 11.4912 7.3 10 7.3C8.50883 7.3 7.3 8.50883 7.3 10C7.3 11.4912 8.50883 12.7 10 12.7Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// Eye Off (Password Hide) 아이콘
export const EyeOffIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M15.3083 15.3083C13.8248 16.4526 11.9675 16.969 10 16.3C4.6 16.3 1 10 1 10C1 10 2.21398 7.34848 4.20849 5.38148M8.31885 3.9015C8.86877 3.76697 9.43174 3.7 10 3.7C15.4 3.7 19 10 19 10C19 10 18.0664 12.0624 16.5165 13.9248M8.28636 8.28636C7.94273 8.63 7.72728 9.09636 7.68378 9.58045C7.64028 10.0645 7.77227 10.5367 8.0506 10.8924C8.32893 11.2481 8.73099 11.4583 9.16738 11.4764C9.60377 11.4944 10.0392 11.3188 10.3776 10.9882M1 1L19 19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// Chevron Down (Select) 아이콘
export const ChevronDownIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// X Circle (Clear Search) 아이콘
export const XCircleIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<circle cx="10" cy="10" r="8.33333" stroke="currentColor" strokeWidth="2" />
<path
d="M12.5 7.5L7.5 12.5M7.5 7.5L12.5 12.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// User 아이콘 (예제용)
export const UserIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M16.6667 17.5V15.8333C16.6667 14.9493 16.3155 14.1014 15.6904 13.4763C15.0653 12.8512 14.2174 12.5 13.3333 12.5H6.66667C5.78261 12.5 4.93477 12.8512 4.30965 13.4763C3.68452 14.1014 3.33334 14.9493 3.33334 15.8333V17.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle
cx="10"
cy="5.83333"
r="3.33333"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// Mail 아이콘 (예제용)
export const MailIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.33334 3.33334H16.6667C17.5833 3.33334 18.3333 4.08334 18.3333 5.00001V15C18.3333 15.9167 17.5833 16.6667 16.6667 16.6667H3.33334C2.41667 16.6667 1.66667 15.9167 1.66667 15V5.00001C1.66667 4.08334 2.41667 3.33334 3.33334 3.33334Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18.3333 5L10 10.8333L1.66667 5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

// Lock 아이콘 (예제용)
export const LockIcon: React.FC<IconProps> = ({ size = 20, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
x="3.33333"
y="9.16667"
width="13.3333"
height="9.16667"
rx="1.66667"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.83333 9.16667V5.83333C5.83333 3.53215 7.69881 1.66667 10 1.66667C12.3012 1.66667 14.1667 3.53215 14.1667 5.83333V9.16667"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
162 changes: 162 additions & 0 deletions src/shared/ui/inputBox/inputBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Input, Textarea, Select } from "./inputBox";
// 아이콘 임포트
import { UserIcon, MailIcon, LockIcon, XCircleIcon } from "@/shared/ui/icons";

const meta = {
title: "Shared/UI/InputBox",
component: Input,
parameters: {
layout: "padded",
docs: {
description: {
component: "공통 입력 컴포넌트입니다. Input, Textarea, Select를 포함합니다.",
},
},
},
tags: ["autodocs"],
argTypes: {
// 1. Size
size: {
control: "radio",
options: ["medium", "large"],
description: "입력창 크기 조절",
table: { defaultValue: { summary: "large" } },
},
// 2. Boolean States
hasError: {
control: "boolean",
description: "에러 상태 표시 (Red Border)",
},
isFullWidth: {
control: "boolean",
description: "부모 컨테이너 너비 100% 차지 여부",
table: { defaultValue: { summary: "true" } },
},
disabled: {
control: "boolean",
description: "비활성화 상태",
},
// 3. Icons
leftIcon: { control: false, description: "좌측 아이콘 (ReactNode)" },
rightIcon: { control: false, description: "우측 아이콘 (ReactNode)" },
},
} satisfies Meta<typeof Input>;

export default meta;
type Story = StoryObj<typeof meta>;

// 기본 텍스트 입력
export const Default: Story = {
args: {
placeholder: "텍스트를 입력해주세요",
size: "large",
isFullWidth: true,
},
};

// 아이콘 포함 (User, Mail 아이콘 예시)
export const WithIcons: Story = {
args: {
placeholder: "이메일 입력",
leftIcon: <UserIcon width={20} />, // 아이콘 크기 지정 권장
rightIcon: <MailIcon width={20} />,
isFullWidth: true,
},
};

// 비밀번호 입력 (강도 표시 포함)
export const Password: Story = {
args: {
type: "password",
placeholder: "비밀번호를 입력하세요",
leftIcon: <LockIcon width={20} />,
passwordStrength: 2, // 0~3 단계
isFullWidth: true,
},
};

// 검색 입력 (Rounded Pill Shape)
export const Search: Story = {
args: {
type: "search",
placeholder: "검색어를 입력하세요...",
size: "medium",
isFullWidth: true,
// onClear가 전달되면 값이 있을 때 X 버튼이 자동으로 노출됨
onClear: () => alert("Clear Clicked!"),
},
};
Comment on lines +80 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear button won't be visible without a value prop.

The onClear callback is provided but the clear button only renders when props.value && onClear is truthy (per component logic at line 188 in inputBox.tsx). Without a value, users won't see the clear functionality in this story.

🔧 Proposed fix to demonstrate clear functionality
 export const Search: Story = {
   args: {
     type: "search",
     placeholder: "검색어를 입력하세요...",
     size: "medium",
     isFullWidth: true,
+    value: "검색어 예시",
     // onClear가 전달되면 값이 있을 때 X 버튼이 자동으로 노출됨
     onClear: () => alert("Clear Clicked!"),
   },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 검색 입력 (Rounded Pill Shape)
export const Search: Story = {
args: {
type: "search",
placeholder: "검색어를 입력하세요...",
size: "medium",
isFullWidth: true,
// onClear가 전달되면 값이 있을 때 X 버튼이 자동으로 노출됨
onClear: () => alert("Clear Clicked!"),
},
};
// 검색 입력 (Rounded Pill Shape)
export const Search: Story = {
args: {
type: "search",
placeholder: "검색어를 입력하세요...",
size: "medium",
isFullWidth: true,
value: "검색어 예시",
// onClear가 전달되면 값이 있을 때 X 버튼이 자동으로 노출됨
onClear: () => alert("Clear Clicked!"),
},
};
🤖 Prompt for AI Agents
In `@src/shared/ui/inputBox/inputBox.stories.tsx` around lines 80 - 90, The Search
story provides onClear but doesn't set a value so the clear button won't render;
update the Search Story (export const Search) to include a non-empty value (or
defaultValue) in args (e.g., value: "example") so the component's condition that
checks props.value && onClear becomes truthy and the clear button is visible;
keep the existing onClear handler to demonstrate the clear behavior.

Comment on lines +81 to +90
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The Search story demonstrates the onClear functionality with an alert(), but this doesn't show users how to properly implement the clear functionality. The value prop should be demonstrated with state management to show that onClear should update the input value. Consider using a wrapper component with useState to demonstrate the complete pattern, similar to how interactive stories are typically implemented in Storybook.

Copilot uses AI. Check for mistakes.

// 에러 상태
export const ErrorState: Story = {
args: {
placeholder: "잘못된 입력",
defaultValue: "유효하지 않은 값",
hasError: true,
isFullWidth: true,
rightIcon: <XCircleIcon width={20} className="text-[color:var(--color-error-500,#EF4444)]" />,
},
};

export const TextareaField: StoryObj<typeof Textarea> = {
render: (args) => <Textarea {...args} />,
args: {
placeholder: "내용을 자세히 적어주세요.",
maxLength: 200, // 글자수 카운터 표시
isFullWidth: true,
rows: 4,
},
argTypes: {
hasError: { control: "boolean" },
},
};

export const SelectField: StoryObj<typeof Select> = {
render: (args) => <Select {...args} />,
args: {
placeholder: "옵션을 선택하세요",
isFullWidth: true,
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
{ label: "옵션 3", value: "option3" },
],
},
};

// all states 모음
export const States: Story = {
render: () => (
<div className="flex flex-col gap-5 w-[360px] p-4 border rounded-xl bg-gray-50">
<h3 className="text-sm font-bold text-gray-500 mb-2">Input States</h3>

{/* 1. 기본 */}
<Input placeholder="기본 (Default)" isFullWidth />

{/* 2. 포커스 (autoFocus) */}
<Input placeholder="포커스 (Focus)" autoFocus isFullWidth />

{/* 3. 에러 */}
<Input
placeholder="에러 (Error)"
defaultValue="유효하지 않은 값"
hasError
isFullWidth
rightIcon={<XCircleIcon className="text-red-500" width={20} />}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

In the States story, className="text-red-500" is used for the error icon, but this doesn't follow the CSS variable pattern used throughout the component. For consistency with the design system, this should use className="text-[color:var(--color-error-500,#EF4444)]" to match the error color pattern used in the component itself (line 19).

Suggested change
rightIcon={<XCircleIcon className="text-red-500" width={20} />}
rightIcon={<XCircleIcon className="text-[color:var(--color-error-500,#EF4444)]" width={20} />}

Copilot uses AI. Check for mistakes.
/>

{/* 4. 비활성 */}
<Input placeholder="비활성 (Disabled)" defaultValue="수정 불가능한 값" disabled isFullWidth />

{/* 5. 읽기 전용 */}
<Input
placeholder="읽기 전용 (Read Only)"
defaultValue="읽기 전용 모드"
readOnly
isFullWidth
/>
</div>
),
};
Loading