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
126 changes: 126 additions & 0 deletions src/entities/user/ui/Input/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./Input";
import React, {useState} from "react";

const meta: Meta<typeof Input> = {
title: "USER/Input",
component: Input,
tags: ["autodocs"],
argTypes: {
size: {
control: { type: "radio" },
options: ["sm", "md", "lg"],
description: "Input 크기 (label, text, message 동시 조정)",
},
widthSize: {
control: { type: "radio" },
options: ["long", "short"],
description: "TextField 너비",
},
label: { control: "text", description: "Input label 텍스트" },
value: { control: "text", description: "Controlled value (직접 입력 가능)" },
placeholder: { control: "text", description: "Input placeholder" },
isHiddenable: { control: "boolean", description: "비밀번호 토글 버튼 활성화" },
isError: { control: "boolean", description: "에러 상태 여부" },
message: { control: "text", description: "가이드 메시지 (value 없을 때 표시)" },
errorMessage: { control: "text", description: "에러 메시지 (isError=true일 때 표시)" },
onChange: { action: "changed", description: "값 변경 이벤트" },
},
};
export default meta;

type Story = StoryObj<typeof Input>;

//Size 관련 스토리
export const Small: Story = {
args: {
label: "Small input",
size: "sm",
widthSize: "long",
placeholder: "Small input",
message: "가이드 메시지",
},
};

export const Medium: Story = {
args: {
label: "Medium input",
size: "md",
widthSize: "long",
placeholder: "Medium input",
message: "가이드 메시지",
},
};

export const Large: Story = {
args: {
label: "Large input",
size: "lg",
widthSize: "long",
placeholder: "Large input",
message: "가이드 메시지",
},
};

// Width 관련 스토리
export const LongWidth: Story = {
args: {
label: "Long width input",
size: "lg",
widthSize: "long",
placeholder: "Long input",
message: "가이드 메시지",
},
};

export const ShortWidth: Story = {
args: {
label: "Short width input",
size: "lg",
widthSize: "short",
placeholder: "Short input",
message: "가이드 메시지",
},
};

// Password (isHiddenable = true)
export const Password: Story = {
args: {
label: "비밀번호",
size: "lg",
widthSize: "long",
placeholder: "비밀번호를 입력하세요",
isHiddenable: true,
message: "최소 8자 이상 입력하세요",
},
};

// Error (isError = true)
export const Error: Story = {
args: {
label: "닉네임",
size: "lg",
widthSize: "long",
placeholder: "닉네임을 입력하세요",
isError: true,
errorMessage: "이미 사용 중인 닉네임입니다",
},
};

// ✅ Controlled (value를 state로 제어)
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 체크 아이콘은 지워도 될 것 같아요!!

export const Controlled: Story = {
render: (args) => {
const [value, setValue] = useState("초기값");
return (
<Input
label="Controlled input"
size="lg"
widthSize="long"
value={value}
placeholder="값을 입력하세요"
onChange={(e) => setValue(e.target.value)}
message="가이드 메시지"
/>
);
},
};
85 changes: 85 additions & 0 deletions src/entities/user/ui/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import React, { useState } from "react";
import cn from "@/shared/lib/cn";
import { TextField } from "@/shared/ui/TextField/TextField";

type InputSize = "sm" | "md" | "lg";
type InputWidthSize = "long" | "short"

interface InputProps {
size?: InputSize;
widthSize? : InputWidthSize
label?: string;
value?: string;
placeholder?: string;
isHiddenable?: boolean;
isError?: boolean;
message?: string; // 가이드 메시지
errorMessage?: string; // 에러 메시지
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const labelSizeMap: Record<InputSize, string> = {
sm: "text-[14px]",
md: "text-[16px]",
lg: "text-[16px]",
};

const messageSizeMap: Record<InputSize, string> = {
sm: "text-[12px]",
md: "text-[12px]",
lg: "text-[14px]",
};

export const Input = ({
size = "lg",
widthSize = "long",
label,
value,
placeholder,
isHiddenable = false,
isError = false,
message,
errorMessage,
onChange,
}: InputProps) => {

// 메시지 우선순위 로직
let helperText: string | null = null;
let helperClass = "text-gray-400";

if (isError) {
helperText = errorMessage ?? "";
helperClass = "text-red";
} else if (!isError && !value) {
helperText = message ?? "";
helperClass = "text-gray-400";
}

return (
<div className="flex flex-col gap-[6px]">
{label && (
<label className={cn("font-medium text-white", labelSizeMap[size])}>
{label}
</label>
)}

<TextField
fieldSize={size}
widthSize={widthSize}
value={value}
placeholder={placeholder}
isHiddenable={isHiddenable}
isError={isError}
onChange={onChange}
/>

{helperText && (
<span className={cn("mt-1", messageSizeMap[size], helperClass)}>
{helperText}
</span>
)}
</div>
);
};