diff --git a/components/DayPicker.tsx b/components/DayPicker.tsx new file mode 100644 index 0000000..eb4d285 --- /dev/null +++ b/components/DayPicker.tsx @@ -0,0 +1,37 @@ +import { ko } from 'date-fns/locale'; +import { DayPicker } from 'react-day-picker'; + +import 'react-day-picker/dist/style.css'; + +interface CustomDayPickerProps { + selected?: Date; + onSelect?: (date: Date | undefined) => void; +} + +function CustomDayPicker({ selected, onSelect }: CustomDayPickerProps) { + return ( +
+ +
+ ); +} + +export default CustomDayPicker; diff --git a/components/Input.tsx b/components/Input.tsx new file mode 100644 index 0000000..6c36ca8 --- /dev/null +++ b/components/Input.tsx @@ -0,0 +1,120 @@ +import { useValidation } from 'hooks/useValidation'; +import React, { useState } from 'react'; + +import CustomDayPicker from './DayPicker'; + +interface InputFieldProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + type?: 'text' | 'email' | 'password' | 'name' | 'passwordConfirm'; + placeholder?: string; + label?: string; + compareValue?: string; + layout?: 'vertical' | 'horizontal'; +} + +function InputField({ + value, + onChange, + type = 'text', + placeholder, + label, + compareValue, + layout = 'vertical', +}: InputFieldProps) { + const { errorMessage, validate } = useValidation({ + type, + compareValue, + }); + + const [showDayPicker, setShowDayPicker] = useState(false); + + const handleFocus = () => { + if (layout === 'horizontal' && label === '생일') { + setShowDayPicker(true); + } + }; + + const closeDayPicker = () => { + setShowDayPicker(false); + }; + + const handleBlur = () => { + if (layout === 'vertical') + // 가로모드 에러 확인 비활성화 + validate(value); + }; + + const handleChange = (e: React.ChangeEvent) => { + onChange(e); + if (layout === 'vertical' && errorMessage) { + validate(e.target.value); + } + }; + const getInputType = () => { + if (type === 'name') { + return 'text'; + } else if (type === 'passwordConfirm') { + return 'password'; + } + return type; + }; + //스타일에 따른 클래스 + const variantClass = { + containerVertical: 'mb-[24px] flex flex-col gap-[10px]', + containerHorizontal: 'mb-[24px] w-[239px] flex items-center gap-[10px]', + labelVertical: 'text-14 text-gray-500', + labelHorizontal: 'text-14 text-gray-400 w-[60px] flex-shrink-0', + base: 'px-[20px] py-[10px] h-[45px] w-[400px] rounded-md text-[14px] text-gray-500 placeholder:text-14 focus:outline-none mo:w-[355px]', + error: 'border border-red-100 bg-red-50', + normal: + 'bg-gray-100 focus:border-green-200 focus:ring-1 focus:ring-green-200', + errorText: 'text-12 text-red-100', + }; + const labelClass = + layout === 'horizontal' + ? variantClass.labelHorizontal + : variantClass.labelVertical; + + const inputClass = `${variantClass.base} ${ + layout === 'vertical' && errorMessage + ? variantClass.error + : variantClass.normal + }`; + + return ( +
+ {label && } + + {layout === 'vertical' && errorMessage && ( + {errorMessage} + )} + {showDayPicker && ( +
+
+ +
+ +
+ )} +
+ ); +} + +export default InputField; diff --git a/components/SearchInput.tsx b/components/SearchInput.tsx new file mode 100644 index 0000000..e69de29 diff --git a/hooks/useValidation.tsx b/hooks/useValidation.tsx new file mode 100644 index 0000000..63709f9 --- /dev/null +++ b/hooks/useValidation.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; + +type ValidationType = + | 'text' + | 'email' + | 'password' + | 'name' + | 'passwordConfirm'; + +interface UseValidationProps { + type: ValidationType; + compareValue?: string; +} + +export function useValidation({ type, compareValue }: UseValidationProps) { + const [errorMessage, setErrorMessage] = useState( + undefined + ); + + const validateInput = (value: string) => { + if (!value) return undefined; + + switch (type) { + case 'email': + if (!/\S+@\S+\.\S+/.test(value)) { + return '이메일 형식으로 작성해 주세요.'; + } + break; + case 'password': + if (value.length < 8) { + return '8자 이상 입력해주세요.'; + } + break; + case 'name': + if (value.length > 10) { + return '열 자 이하로 작성해주세요.'; + } + break; + case 'passwordConfirm': + if (compareValue !== undefined && value !== compareValue) { + return '비밀번호가 일치하지 않습니다.'; + } + break; + } + return undefined; + }; + + const validate = (value: string) => { + const error = validateInput(value); + setErrorMessage(error); + return error; + }; + + return { + errorMessage, + validate, + setErrorMessage, + }; +} diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx new file mode 100644 index 0000000..0e3432d --- /dev/null +++ b/pages/signup/index.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; + +import InputField from '@/components/Input'; + +const SignUp: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + const [name, setName] = useState(''); + + const handleEmailChange = (e: React.ChangeEvent) => { + setEmail(e.target.value); + }; + + const handlePasswordChange = (e: React.ChangeEvent) => { + setPassword(e.target.value); + }; + + const handlePasswordConfirmChange = ( + e: React.ChangeEvent + ) => { + setPasswordConfirm(e.target.value); + }; + + const handleNameChange = (e: React.ChangeEvent) => { + setName(e.target.value); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + alert('가입이 완료되었습니다'); + }; + + return ( +
+ + + + + + + + + ); +}; + +export default SignUp; diff --git a/pages/test/index.tsx b/pages/test/index.tsx index aac1243..29e84dc 100644 --- a/pages/test/index.tsx +++ b/pages/test/index.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import Button from '@/components/Button'; import LinkBar from '@/components/LinkBar'; import SnackBar from '@/components/SnackBar'; @@ -34,6 +32,12 @@ export default function Test() { + + inputfield + + + + Button @@ -65,6 +69,11 @@ export default function Test() { LinkBar + + inputfield + + + diff --git a/tsconfig.json b/tsconfig.json index 03d1b81..c32ac62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -20,15 +16,9 @@ "incremental": true, "baseUrl": ".", "paths": { - "@/components/*": [ - "components/*" - ], - "@/styles/*": [ - "styles/*" - ], - "@/pages/*": [ - "pages/*" - ] + "@/components/*": ["components/*"], + "@/styles/*": ["styles/*"], + "@/pages/*": ["pages/*"] } }, "include": [ @@ -37,9 +27,5 @@ "components/**/*.ts", "components/**/*.tsx" ], - "exclude": [ - "node_modules", - "dist", - ".next" - ] + "exclude": ["node_modules", "dist", ".next"] }