diff --git a/src/components/Input/FileInput.tsx b/src/components/Input/FileInput.tsx new file mode 100644 index 00000000..f5dbfcb7 --- /dev/null +++ b/src/components/Input/FileInput.tsx @@ -0,0 +1,102 @@ +import { ChangeEvent, useId } from 'react'; + +import styled from '@emotion/styled'; + +import Icon from '../Icon'; +import { B3 } from '../Text'; +import { + BaseInputType, + type InputBaseProps, + InputContainer, + baseInputStyles, +} from './InputContainer'; + +export type FileInputProps = Omit & { + uploadType: 'audio' | 'image' | 'video' | 'csv' | 'txt' | 'pdf' | 'default'; + file: File | null; + onUpload: (file: File) => void; +}; + +const getMimeType = (uploadType?: FileInputProps['uploadType']) => { + switch (uploadType) { + case 'audio': + case 'image': + case 'video': + return `${uploadType}/*`; + case 'csv': + return 'text/csv'; + case 'txt': + return 'text/plain'; + case 'pdf': + return '.pdf'; + default: + return '*/*'; + } +}; + +export function FileInput({ + label, + placeholder, + description, + name, + file, + error, + disabled, + onUpload, + defaultValue, + required = false, + width, + tooltip, + onClick, + height, + uploadType = 'default', + ...props +}: FileInputProps) { + const id = useId(); + return ( + } + > + <> + ) => { + if (event.target.files) { + onUpload(event.target.files[0]); + } + }} + type="file" + accept={getMimeType(uploadType)} + id={`file-input-${id}`} + /> + + {file ? {file.name} : {placeholder}} + + + + ); +} + +const FileUploadInput = styled.input` + display: none; +`; + +export const UploadButton = styled.label` + ${props => baseInputStyles(props)} + display: flex; + align-items: center; +`; diff --git a/src/components/Input/InputContainer.tsx b/src/components/Input/InputContainer.tsx index 31cb1de6..69f5d198 100644 --- a/src/components/Input/InputContainer.tsx +++ b/src/components/Input/InputContainer.tsx @@ -6,6 +6,7 @@ import Icon from '../Icon'; import { B3, B5, B7 } from '../Text'; import { Tooltip, type TooltipProps } from '../Tooltip'; import { color, typography } from '../styles'; +import { css } from '@emotion/react'; export type InputTooltipProps = Omit; @@ -121,18 +122,27 @@ export const ErrorContainer = styled.div` gap: 4px; `; -export const BaseInput = styled.input<{ +export type BaseInputType = { error?: string; isRightSection?: boolean; isLeftSection?: boolean; height?: string | number; cursorPointer?: boolean; -}>` +}; + +export const baseInputStyles = ({ + error, + isRightSection, + isLeftSection, + height, + cursorPointer, +}: BaseInputType) => css` width: 100%; - padding: ${({ isRightSection, isLeftSection }) => { - if (isLeftSection) return '6px 12px 6px 34px'; - return isRightSection ? '6px 36px 6px 12px' : '6px 12px'; - }}; + padding: ${isLeftSection + ? '6px 12px 6px 34px' + : isRightSection + ? '6px 36px 6px 12px' + : '6px 12px'}; outline: none; border: 1px solid ${color['grey-50']}; font-style: normal; @@ -141,7 +151,7 @@ export const BaseInput = styled.input<{ color: ${color['grey-80']}; background: ${color.white}; border-radius: 8px; - height: ${p => (typeof p.height === 'number' ? `${p.height}px` : p.height || '34px')}; + height: ${typeof height === 'number' ? `${height}px` : height || '34px'}; &:disabled { border: none; @@ -155,19 +165,22 @@ export const BaseInput = styled.input<{ font-size: ${typography.size.xs}px; } - ${({ error }) => - error - ? ` + ${error + ? ` border: 1px solid ${color['error-30']}; background: ${color['error-10']}; ` - : ` + : ` &:focus-visible, &:focus { border: 1px solid ${color['grey-80']}; } `} - cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'default')}; + cursor: ${cursorPointer ? 'pointer' : 'default'}; +`; + +export const BaseInput = styled.input` + ${props => baseInputStyles(props)} `; const QuestionIconWrapper = styled.i` diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 317c9902..269009cc 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -3,12 +3,14 @@ import { NumberInput, NumberInputProps } from './NumberInput'; import { PasswordInput, PasswordInputProps } from './PasswordInput'; import { TextInput, TextInputProps } from './TextInput'; import { ColorPickerInput, ColorPickerInputProps } from './ColorPickerInput'; +import { FileInput, FileInputProps } from './FileInput'; -export { InputContainer, TextInput, PasswordInput, NumberInput, ColorPickerInput }; +export { InputContainer, TextInput, PasswordInput, NumberInput, ColorPickerInput, FileInput }; export type { InputBaseProps, TextInputProps, PasswordInputProps, NumberInputProps, ColorPickerInputProps, + FileInputProps, }; diff --git a/src/stories/Input.stories.tsx b/src/stories/Input.stories.tsx index e461b6cb..7c4ed0f0 100644 --- a/src/stories/Input.stories.tsx +++ b/src/stories/Input.stories.tsx @@ -15,7 +15,8 @@ import { import Icon from '../components/Icon'; import { BaseInput, type InputTooltipProps } from '../components/Input/InputContainer'; import { NumberInput } from '../components/Input/NumberInput'; -import { useState } from 'react'; +import { ChangeEvent, useState } from 'react'; +import { FileInput, FileInputProps } from '../components/Input/FileInput'; export default { title: 'Components/Input', @@ -93,6 +94,98 @@ export function NumberInputDefault() { ); } +export const FileInputDefault = () => { + const [files, setFiles] = useState<{ + [key in FileInputProps['uploadType']]: File | null; + }>({ + audio: null, + image: null, + video: null, + csv: null, + txt: null, + pdf: null, + default: null, + }); + + const handleFileChange = (type: FileInputProps['uploadType'], file: File | null) => { + setFiles(prevFiles => ({ + ...prevFiles, + [type]: file, + })); + }; + + const fileTypes: FileInputProps['uploadType'][] = [ + 'audio', + 'image', + 'video', + 'csv', + 'txt', + 'pdf', + 'default', + ]; + + return ( + <> + {fileTypes.map(type => ( + handleFileChange(type, file)} + file={files[type as FileInputProps['uploadType'] | 'default']} + uploadType={type as FileInputProps['uploadType']} + /> + ))} + + ); +}; + +const FILE_TYPE_CONFIGS: { + [key in FileInputProps['uploadType']]: { + explain: string; + description: string; + }; +} = { + audio: { + explain: 'MP3, WAV, FLAC 파일 추가', + description: '오디오 파일만 입력 가능한 입력창입니다.', + }, + image: { + explain: 'JPG, JPEG, PNG, SVG 파일 추가', + description: '이미지 파일만 입력 가능한 입력창입니다.', + }, + video: { + explain: 'MP4, AVI, MOV 파일 추가', + description: '비디오 파일만 입력 가능한 입력창입니다.', + }, + csv: { + explain: 'CSV 파일 추가', + description: 'CSV 파일만 입력 가능한 입력창입니다.', + }, + txt: { + explain: 'TXT 파일 추가', + description: 'TXT 파일만 입력 가능한 입력창입니다.', + }, + pdf: { + explain: 'PDF 파일 추가', + description: 'PDF 파일만 입력 가능한 입력창입니다.', + }, + default: { + explain: '전체 유형의 파일 추가', + description: '전체 유형의 파일을 입력할 수 있는 입력창입니다.', + }, +}; + +const getFileInputExplain = (uploadType?: FileInputProps['uploadType']) => { + return (FILE_TYPE_CONFIGS[uploadType || 'default'] || FILE_TYPE_CONFIGS.default).explain; +}; + +const getFileInputDescription = (uploadType?: FileInputProps['uploadType']) => { + return (FILE_TYPE_CONFIGS[uploadType || 'default'] || FILE_TYPE_CONFIGS.default).description; +}; + export function WithIcon() { return ( <>