Skip to content

Commit c399651

Browse files
jun-0411young-52
authored andcommitted
✨Add register form page(email login)
1 parent 9f53eb6 commit c399651

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

src/routes/RegisterForm.tsx

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { useEffect, useMemo, useRef, useState } from 'react';
2+
import { useNavigate } from 'react-router';
3+
import useAuth from '../utils/useAuth';
4+
5+
export default function RegisterForm() {
6+
const [name, setName] = useState('');
7+
const [email, setEmail] = useState('');
8+
const [password, setPassword] = useState('');
9+
const [confirmPassword, setConfirmPassword] = useState('');
10+
const [photo, setPhoto] = useState<File | null>(null);
11+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
12+
13+
const [showErrors, setShowErrors] = useState(false);
14+
const emailRef = useRef<HTMLInputElement>(null);
15+
const nameRef = useRef<HTMLInputElement>(null);
16+
const passwordRef = useRef<HTMLInputElement>(null);
17+
const confirmPasswordRef = useRef<HTMLInputElement>(null);
18+
19+
const navigate = useNavigate();
20+
const { handleSignUp } = useAuth();
21+
22+
// 사진이 선택될 때마다 미리보기 URL 생성
23+
useEffect(() => {
24+
if (!photo) {
25+
setPreviewUrl(null);
26+
return;
27+
}
28+
29+
const objectUrl = URL.createObjectURL(photo);
30+
setPreviewUrl(objectUrl);
31+
32+
// 컴포넌트 언마운트 시 메모리 누수 방지를 위해 URL 해제
33+
return () => URL.revokeObjectURL(objectUrl);
34+
}, [photo]);
35+
36+
const validations = useMemo(() => {
37+
// 이메일 형식 체크 정규표현식
38+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
39+
40+
return {
41+
isEmailValid: emailRegex.test(email),
42+
isNameValid: name.trim().length > 0, // 공백 제외 한 글자라도 있으면 성공
43+
password: {
44+
isLongEnough: password.length >= 8,
45+
hasNumber: /[0-9]/.test(password),
46+
hasSpecial: /[!@#$%^&*()]/.test(password),
47+
},
48+
isPasswordMatch:
49+
password === confirmPassword && confirmPassword.length > 0,
50+
};
51+
}, [email, name, password, confirmPassword]);
52+
53+
const isPasswordValid = Object.values(validations.password).every(Boolean);
54+
55+
const onRegisterSubmit = (e: React.FormEvent) => {
56+
e.preventDefault();
57+
setShowErrors(true);
58+
59+
if (!validations.isNameValid) {
60+
nameRef.current?.focus();
61+
return;
62+
}
63+
64+
if (!validations.isEmailValid) {
65+
emailRef.current?.focus();
66+
return;
67+
}
68+
69+
if (!isPasswordValid) {
70+
passwordRef.current?.focus();
71+
return;
72+
}
73+
74+
if (!validations.isPasswordMatch) {
75+
confirmPasswordRef.current?.focus();
76+
return;
77+
}
78+
79+
handleSignUp({ username: email, password, name, photo });
80+
};
81+
82+
const errorTextStyle = 'mt-1 text-xs text-red-500 font-medium';
83+
84+
return (
85+
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4 py-12">
86+
<div className="max-w-md w-full space-y-6 bg-white p-10 rounded-xl shadow-lg border border-gray-100">
87+
<h2 className="text-2xl font-bold text-gray-900 text-center">
88+
회원 정보 입력
89+
</h2>
90+
91+
<form className="space-y-5" onSubmit={onRegisterSubmit}>
92+
<div className="flex flex-col items-center mb-6">
93+
<div className="w-24 h-24 rounded-full overflow-hidden bg-gray-100 border-2 border-gray-200 mb-3 flex items-center justify-center">
94+
{previewUrl ? (
95+
<img
96+
src={previewUrl}
97+
alt="Preview"
98+
className="w-full h-full object-cover"
99+
/>
100+
) : (
101+
<span className="text-gray-400 text-xs">사진 없음</span>
102+
)}
103+
</div>
104+
<label className="text-sm text-blue-600 font-medium cursor-pointer hover:underline">
105+
사진 선택
106+
<input
107+
type="file"
108+
accept="image/*"
109+
className="hidden"
110+
onChange={(e) =>
111+
setPhoto(e.target.files ? e.target.files[0] : null)
112+
}
113+
/>
114+
</label>
115+
</div>
116+
117+
<div>
118+
<label className="block text-sm font-medium text-gray-700 mb-1">
119+
이름
120+
</label>
121+
<input
122+
ref={nameRef}
123+
type="text"
124+
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none transition-all"
125+
value={name}
126+
onChange={(e) => setName(e.target.value)}
127+
placeholder="이름을 입력하세요"
128+
/>
129+
130+
{showErrors && !name && (
131+
<p className={errorTextStyle}>이름을 입력해주세요.</p>
132+
)}
133+
</div>
134+
135+
<div>
136+
<label className="block text-sm font-medium text-gray-700 mb-1">
137+
이메일
138+
</label>
139+
<input
140+
ref={emailRef}
141+
type="email"
142+
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none transition-all ${
143+
email.length > 0 && !validations.isEmailValid
144+
? 'border-red-400 focus:ring-red-100'
145+
: 'border-gray-300 focus:ring-blue-500'
146+
}`}
147+
value={email}
148+
onChange={(e) => setEmail(e.target.value)}
149+
placeholder="example@moisha.com"
150+
/>
151+
152+
{showErrors && !email && (
153+
<p className={errorTextStyle}>이메일을 입력해주세요.</p>
154+
)}
155+
{email.length > 0 && !validations.isEmailValid && (
156+
<p className={errorTextStyle}>
157+
<span>유효한 이메일 형식을 입력해주세요.</span>
158+
</p>
159+
)}
160+
</div>
161+
162+
<div>
163+
<label className="block text-sm font-medium text-gray-700 mb-1">
164+
비밀번호
165+
</label>
166+
<input
167+
ref={passwordRef}
168+
type="password"
169+
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none transition-all ${
170+
password.length > 0 && !isPasswordValid
171+
? 'border-red-400 focus:ring-red-100'
172+
: 'border-gray-300 focus:ring-blue-500'
173+
}`}
174+
value={password}
175+
onChange={(e) => setPassword(e.target.value)}
176+
placeholder="8자 이상, 숫자, 특수문자 포함"
177+
/>
178+
179+
{showErrors && !password && (
180+
<p className={errorTextStyle}>비밀번호를 입력해주세요.</p>
181+
)}
182+
{password.length > 0 && (
183+
<ul className="mt-2 space-y-1">
184+
<li
185+
className={`text-xs flex items-center ${validations.password.isLongEnough ? 'text-green-600' : 'text-gray-400'}`}
186+
>
187+
{validations.password.isLongEnough ? '✓' : '○'} 8자 이상
188+
</li>
189+
<li
190+
className={`text-xs flex items-center ${validations.password.hasNumber ? 'text-green-600' : 'text-gray-400'}`}
191+
>
192+
{validations.password.hasNumber ? '✓' : '○'} 숫자 포함
193+
</li>
194+
<li
195+
className={`text-xs flex items-center ${validations.password.hasSpecial ? 'text-green-600' : 'text-gray-400'}`}
196+
>
197+
{validations.password.hasSpecial ? '✓' : '○'} 특수문자 포함
198+
</li>
199+
</ul>
200+
)}
201+
</div>
202+
203+
<div>
204+
<label className="block text-sm font-medium text-gray-700 mb-1">
205+
비밀번호 확인
206+
</label>
207+
<input
208+
ref={confirmPasswordRef}
209+
type="password"
210+
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none transition-all ${
211+
confirmPassword.length > 0 && !validations.isPasswordMatch
212+
? 'border-red-400 focus:ring-red-100'
213+
: 'border-gray-300 focus:ring-blue-500'
214+
}`}
215+
value={confirmPassword}
216+
onChange={(e) => setConfirmPassword(e.target.value)}
217+
placeholder="비밀번호를 확인해주세요"
218+
/>
219+
220+
{showErrors && !confirmPassword && (
221+
<p className={errorTextStyle}>비밀번호를 확인해주세요.</p>
222+
)}
223+
{confirmPassword.length > 0 && !validations.isPasswordMatch && (
224+
<p className={errorTextStyle}>비밀번호가 일치하지 않습니다.</p>
225+
)}
226+
</div>
227+
228+
<div className="pt-4 space-y-3">
229+
<button
230+
type="submit"
231+
className="w-full py-3.5 px-4 rounded-md bg-blue-600 text-white font-bold hover:bg-blue-700 transition-all shadow-md active:scale-[0.98]"
232+
>
233+
회원가입
234+
</button>
235+
<button
236+
type="button"
237+
onClick={() => navigate(-1)}
238+
className="w-full py-2 text-sm text-gray-500 hover:text-gray-700 font-medium transition-colors"
239+
>
240+
이전 단계로
241+
</button>
242+
</div>
243+
</form>
244+
</div>
245+
</div>
246+
);
247+
}

0 commit comments

Comments
 (0)