Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
36 changes: 36 additions & 0 deletions components/DayPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
import { ko } from 'date-fns/locale';

interface CustomDayPickerProps {
selected?: Date;
onSelect?: (date: Date | undefined) => void;
}

function CustomDayPicker({ selected, onSelect }: CustomDayPickerProps) {
return (
<div className="relative">
<DayPicker
mode="single"
selected={selected}
onSelect={onSelect}
locale={ko}
captionLayout="dropdown"
fromYear={2000}
toYear={new Date().getFullYear()}
modifiersStyles={{
selected: {
backgroundColor: '#32A68A',
color: 'white',
},
today: {
color: '#32A68A',
fontWeight: 'bold',
},
}}
/>
</div>
);
}

export default CustomDayPicker;
119 changes: 119 additions & 0 deletions components/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import { useValidation } from 'hooks/useValidation';
import CustomDayPicker from './DayPicker';

interface InputFieldProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => 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<HTMLInputElement>) => {
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 (
<div
className={
layout === 'horizontal'
? `${variantClass.containerHorizontal} relative`
: `${variantClass.containerVertical} relative`
}
>
{label && <label className={labelClass}>{label}</label>}
<input
type={getInputType()}
value={value}
onChange={handleChange}
placeholder={placeholder}
onBlur={handleBlur}
onFocus={handleFocus}
className={inputClass}
/>
{layout === 'vertical' && errorMessage && (
<span className={variantClass.errorText}>{errorMessage}</span>
)}
{showDayPicker && (
<div className="absolute left-0 top-full z-50 mt-2 rounded bg-white p-4 shadow-md">
<p>
<CustomDayPicker />
</p>
<button onClick={closeDayPicker} className="mt-2 text-gray-500">
닫기
</button>
</div>
)}
</div>
);
}

export default InputField;
Empty file added components/SearchInput.tsx
Empty file.
59 changes: 59 additions & 0 deletions hooks/useValidation.tsx
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(
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,
};
}
72 changes: 72 additions & 0 deletions pages/signup/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
setEmail(e.target.value);
};

const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
};

const handlePasswordConfirmChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setPasswordConfirm(e.target.value);
};

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert('가입이 완료되었습니다');
};

return (
<form onSubmit={handleSubmit}>
<InputField
label="이름"
type="name"
value={name}
onChange={handleNameChange}
placeholder="이름을 입력해 주세요"
/>

<InputField
label="이메일"
type="email"
value={email}
onChange={handleEmailChange}
placeholder="이메일을 입력해 주세요"
/>

<InputField
label="비밀번호"
type="password"
value={password}
onChange={handlePasswordChange}
placeholder="비밀번호를 입력해 주세요"
/>

<InputField
label="비밀번호 확인"
type="passwordConfirm"
value={passwordConfirm}
onChange={handlePasswordConfirmChange}
placeholder="비밀번호를 다시 입력해 주세요"
compareValue={password}
/>
</form>
);
};

export default SignUp;
12 changes: 12 additions & 0 deletions pages/test/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Signup from '@/pages/signUp';
import Button from '@/components/Button';
import LinkBar from '@/components/LinkBar';

Expand All @@ -15,6 +16,12 @@ export default function Test() {
</tr>
</thead>
<tbody>
<tr>
<td className={commonCellClass}>inputfield</td>
<td className={commonRowClass}>
<Signup />
</td>
</tr>
<tr className="border-b border-gray-300">
<td className={commonCellClass}>Button</td>
<td className={commonRowClass}>
Expand All @@ -37,6 +44,11 @@ export default function Test() {
<td className={commonCellClass}>LinkBar</td>
<td className={commonRowClass}>
<LinkBar link="https://www.google.com" />

<td className={commonCellClass}>inputfield</td>
<td className={commonRowClass}>
<Signup />
</td>
</td>
</tr>
</tbody>
Expand Down