Skip to content

Commit 05029fd

Browse files
Merge pull request #30 from codeitFE11-part3-team7/feature/#19_인풋필드-컴포넌트
Feature/#19 인풋필드 컴포넌트
2 parents 3229ac1 + a4d2b29 commit 05029fd

File tree

5 files changed

+299
-2
lines changed

5 files changed

+299
-2
lines changed

components/DayPicker.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ko } from 'date-fns/locale';
2+
import { DayPicker } from 'react-day-picker';
3+
4+
import 'react-day-picker/dist/style.css';
5+
6+
interface CustomDayPickerProps {
7+
selected?: Date;
8+
onSelect?: (date: Date | undefined) => void;
9+
}
10+
11+
function CustomDayPicker({ selected, onSelect }: CustomDayPickerProps) {
12+
return (
13+
<div className="relative">
14+
<DayPicker
15+
mode="single"
16+
selected={selected}
17+
onSelect={onSelect}
18+
locale={ko}
19+
captionLayout="dropdown"
20+
fromYear={2000}
21+
toYear={new Date().getFullYear()}
22+
modifiersStyles={{
23+
selected: {
24+
backgroundColor: '#32A68A',
25+
color: 'white',
26+
},
27+
today: {
28+
color: '#32A68A',
29+
fontWeight: 'bold',
30+
},
31+
}}
32+
/>
33+
</div>
34+
);
35+
}
36+
37+
export default CustomDayPicker;

components/Input.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useValidation } from 'hooks/useValidation';
2+
import React, { useState } from 'react';
3+
4+
import CustomDayPicker from './DayPicker';
5+
6+
interface InputFieldProps {
7+
value: string;
8+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
9+
type?: 'text' | 'email' | 'password' | 'name' | 'passwordConfirm';
10+
placeholder?: string;
11+
label?: string;
12+
compareValue?: string;
13+
layout?: 'vertical' | 'horizontal';
14+
}
15+
16+
function InputField({
17+
value,
18+
onChange,
19+
type = 'text',
20+
placeholder,
21+
label,
22+
compareValue,
23+
layout = 'vertical',
24+
}: InputFieldProps) {
25+
const { errorMessage, validate } = useValidation({
26+
type,
27+
compareValue,
28+
});
29+
30+
const [showDayPicker, setShowDayPicker] = useState(false);
31+
32+
const handleFocus = () => {
33+
if (layout === 'horizontal' && label === '생일') {
34+
setShowDayPicker(true);
35+
}
36+
};
37+
38+
const closeDayPicker = () => {
39+
setShowDayPicker(false);
40+
};
41+
42+
const handleBlur = () => {
43+
if (layout === 'vertical')
44+
// 가로모드 에러 확인 비활성화
45+
validate(value);
46+
};
47+
48+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
49+
onChange(e);
50+
if (layout === 'vertical' && errorMessage) {
51+
validate(e.target.value);
52+
}
53+
};
54+
const getInputType = () => {
55+
if (type === 'name') {
56+
return 'text';
57+
} else if (type === 'passwordConfirm') {
58+
return 'password';
59+
}
60+
return type;
61+
};
62+
//스타일에 따른 클래스
63+
const variantClass = {
64+
containerVertical: 'mb-[24px] flex flex-col gap-[10px]',
65+
containerHorizontal: 'mb-[24px] w-[239px] flex items-center gap-[10px]',
66+
labelVertical: 'text-14 text-gray-500',
67+
labelHorizontal: 'text-14 text-gray-400 w-[60px] flex-shrink-0',
68+
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]',
69+
error: 'border border-red-100 bg-red-50',
70+
normal:
71+
'bg-gray-100 focus:border-green-200 focus:ring-1 focus:ring-green-200',
72+
errorText: 'text-12 text-red-100',
73+
};
74+
const labelClass =
75+
layout === 'horizontal'
76+
? variantClass.labelHorizontal
77+
: variantClass.labelVertical;
78+
79+
const inputClass = `${variantClass.base} ${
80+
layout === 'vertical' && errorMessage
81+
? variantClass.error
82+
: variantClass.normal
83+
}`;
84+
85+
return (
86+
<div
87+
className={
88+
layout === 'horizontal'
89+
? `${variantClass.containerHorizontal} relative`
90+
: `${variantClass.containerVertical} relative`
91+
}
92+
>
93+
{label && <label className={labelClass}>{label}</label>}
94+
<input
95+
type={getInputType()}
96+
value={value}
97+
onChange={handleChange}
98+
placeholder={placeholder}
99+
onBlur={handleBlur}
100+
onFocus={handleFocus}
101+
className={inputClass}
102+
/>
103+
{layout === 'vertical' && errorMessage && (
104+
<span className={variantClass.errorText}>{errorMessage}</span>
105+
)}
106+
{showDayPicker && (
107+
<div className="absolute left-0 top-full z-50 mt-2 rounded bg-white p-4 shadow-md">
108+
<div>
109+
<CustomDayPicker />
110+
</div>
111+
<button onClick={closeDayPicker} className="mt-2 text-gray-500">
112+
닫기
113+
</button>
114+
</div>
115+
)}
116+
</div>
117+
);
118+
}
119+
120+
export default InputField;

hooks/useValidation.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useState } from 'react';
2+
3+
type ValidationType =
4+
| 'text'
5+
| 'email'
6+
| 'password'
7+
| 'name'
8+
| 'passwordConfirm';
9+
10+
interface UseValidationProps {
11+
type: ValidationType;
12+
compareValue?: string;
13+
}
14+
15+
export function useValidation({ type, compareValue }: UseValidationProps) {
16+
const [errorMessage, setErrorMessage] = useState<string | undefined>(
17+
undefined
18+
);
19+
20+
const validateInput = (value: string) => {
21+
if (!value) return undefined;
22+
23+
switch (type) {
24+
case 'email':
25+
if (!/\S+@\S+\.\S+/.test(value)) {
26+
return '이메일 형식으로 작성해 주세요.';
27+
}
28+
break;
29+
case 'password':
30+
if (value.length < 8) {
31+
return '8자 이상 입력해주세요.';
32+
}
33+
break;
34+
case 'name':
35+
if (value.length > 10) {
36+
return '열 자 이하로 작성해주세요.';
37+
}
38+
break;
39+
case 'passwordConfirm':
40+
if (compareValue !== undefined && value !== compareValue) {
41+
return '비밀번호가 일치하지 않습니다.';
42+
}
43+
break;
44+
}
45+
return undefined;
46+
};
47+
48+
const validate = (value: string) => {
49+
const error = validateInput(value);
50+
setErrorMessage(error);
51+
return error;
52+
};
53+
54+
return {
55+
errorMessage,
56+
validate,
57+
setErrorMessage,
58+
};
59+
}

pages/signup/index.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useState } from 'react';
2+
3+
import InputField from '@/components/Input';
4+
5+
const SignUp: React.FC = () => {
6+
const [email, setEmail] = useState('');
7+
const [password, setPassword] = useState('');
8+
const [passwordConfirm, setPasswordConfirm] = useState('');
9+
const [name, setName] = useState('');
10+
11+
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
12+
setEmail(e.target.value);
13+
};
14+
15+
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
16+
setPassword(e.target.value);
17+
};
18+
19+
const handlePasswordConfirmChange = (
20+
e: React.ChangeEvent<HTMLInputElement>
21+
) => {
22+
setPasswordConfirm(e.target.value);
23+
};
24+
25+
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
26+
setName(e.target.value);
27+
};
28+
29+
const handleSubmit = (e: React.FormEvent) => {
30+
e.preventDefault();
31+
alert('가입이 완료되었습니다');
32+
};
33+
34+
return (
35+
<form onSubmit={handleSubmit}>
36+
<InputField
37+
label="이름"
38+
type="name"
39+
value={name}
40+
onChange={handleNameChange}
41+
placeholder="이름을 입력해 주세요"
42+
/>
43+
44+
<InputField
45+
label="이메일"
46+
type="email"
47+
value={email}
48+
onChange={handleEmailChange}
49+
placeholder="이메일을 입력해 주세요"
50+
/>
51+
52+
<InputField
53+
label="비밀번호"
54+
type="password"
55+
value={password}
56+
onChange={handlePasswordChange}
57+
placeholder="비밀번호를 입력해 주세요"
58+
/>
59+
60+
<InputField
61+
label="비밀번호 확인"
62+
type="passwordConfirm"
63+
value={passwordConfirm}
64+
onChange={handlePasswordConfirmChange}
65+
placeholder="비밀번호를 다시 입력해 주세요"
66+
compareValue={password}
67+
/>
68+
</form>
69+
);
70+
};
71+
72+
export default SignUp;

pages/test/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { useState } from 'react';
2-
31
import Button from '@/components/Button';
42
import Dropdown from '@/components/Dropdown';
53
import LinkBar from '@/components/LinkBar';
@@ -61,6 +59,12 @@ export default function Test() {
6159
</tr>
6260
</thead>
6361
<tbody>
62+
<tr>
63+
<td className={commonCellClass}>inputfield</td>
64+
<td className={commonRowClass}>
65+
<Signup />
66+
</td>
67+
</tr>
6468
<tr className="border-b border-gray-300">
6569
<td className={commonCellClass}>Button</td>
6670
<td className={commonRowClass}>
@@ -92,6 +96,11 @@ export default function Test() {
9296
<td className={commonCellClass}>LinkBar</td>
9397
<td className={commonRowClass}>
9498
<LinkBar link="https://www.google.com" />
99+
100+
<td className={commonCellClass}>inputfield</td>
101+
<td className={commonRowClass}>
102+
<Signup />
103+
</td>
95104
</td>
96105
</tr>
97106
<tr className="border-b border-gray-300">

0 commit comments

Comments
 (0)