Skip to content

Commit b01ceac

Browse files
jun-0411young-52
andauthored
✨ Sign up pages (#9)
### πŸ“ μž‘μ—… λ‚΄μš© - νšŒμ›κ°€μž… νŽ˜μ΄μ§€λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. - νšŒμ›κ°€μž… 방식 선택 νŽ˜μ΄μ§€μ™€ μ΄λ©”μΌλ‘œ νšŒμ›κ°€μž… νŽ˜μ΄μ§€λ₯Ό λ‚˜λˆ΄μŠ΅λ‹ˆλ‹€. - μ†Œμ…œλ‘œ νšŒμ›κ°€μž…μ„ μ„ νƒν•˜λ©΄, 둜그인 νŽ˜μ΄μ§€μ—μ„œ μ†Œμ…œλ‘œ 둜그인 ν•œ 것과 같은 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•©λ‹ˆλ‹€. μ†Œμ…œ 둜그인 νŽ˜μ΄μ§€μ—μ„œλŠ” 각 ν”Œλž«νΌμ—μ„œ μΈκ°€μ½”λ“œλ₯Ό λ°›μ•„μ™€μ„œ λ°±μ—”λ“œλ‘œ λ„˜κ²¨μ€λ‹ˆλ‹€. - λ°±μ—”λ“œμ—μ„œ μΈκ°€μ½”λ“œλ₯Ό μ΄μš©ν•΄μ„œ 아직 λͺ¨μ΄μƒ€μ— νšŒμ›κ°€μž…μ΄ μ•ˆ 된 μœ μ €λΌλ©΄ νšŒμ›κ°€μž… μ‹œν‚€λŠ” 둜직이 ν•„μš”ν•©λ‹ˆλ‹€ - λ§Œμ•½ ν”„λŸ°νŠΈμ—μ„œ κ·Έ 과정을 μ²˜λ¦¬ν•΄μ•Όν•œλ‹€λ©΄ μΆ”ν›„ 보강이 ν•„μš”ν•©λ‹ˆλ‹€. - νšŒμ›κ°€μž…μ—μ„œ 이름/이메일/λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜λ‘œ μž…λ ₯ν•˜κ²Œ λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. - λΉ„λ°€λ²ˆν˜ΈλŠ” 8μžμ΄μƒ/숫자/νŠΉμˆ˜λ¬Έμžκ°€ ν•„μš”ν•©λ‹ˆλ‹€. ### πŸ“Έ μŠ€ν¬λ¦°μƒ· (선택) <img width="2507" height="1325" alt="μŠ€ν¬λ¦°μƒ· 2026-01-01 210112" src="https://github.com/user-attachments/assets/c94d2e84-b57b-4694-b321-63f0fe2a1e4c" /> ### πŸš€ 리뷰 μš”κ΅¬μ‚¬ν•­ (선택) - λΉ„λ°€λ²ˆν˜Έμ— 더 κ°•ν•œ μ œν•œμ„ κ±Έμ–΄μ•Ό ν• κΉŒμš”? (e.g. λŒ€μ†Œλ¬Έμž 포함 λ“±) --------- Co-authored-by: Park Junyoung <bloomwayz@snu.ac.kr>
1 parent 0a72cf7 commit b01ceac

File tree

4 files changed

+330
-16
lines changed

4 files changed

+330
-16
lines changed

β€Žsrc/routes.tsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { createBrowserRouter } from 'react-router';
22
import RootLayout from './layouts/RootLayout';
33
import Home from './routes/Home';
44
import Login from './routes/Login';
5-
import Register from './routes/Register';
5+
import RegisterChoice from './routes/RegisterChoice';
6+
import RegisterForm from './routes/RegisterForm';
67
import SocialCallback from './routes/SocialCallback';
78

89
export const router = createBrowserRouter([
@@ -12,7 +13,8 @@ export const router = createBrowserRouter([
1213
children: [
1314
{ index: true, Component: Home },
1415
{ path: 'login', Component: Login },
15-
{ path: 'register', Component: Register },
16+
{ path: 'register', Component: RegisterChoice },
17+
{ path: 'register/email', Component: RegisterForm },
1618
{ path: 'auth/callback/:provider', Component: SocialCallback },
1719
],
1820
},

β€Žsrc/routes/Register.tsxβ€Ž

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useNavigate } from 'react-router';
2+
import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '../constants/auth';
3+
4+
export default function RegisterChoice() {
5+
const navigate = useNavigate();
6+
7+
return (
8+
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
9+
<div className="max-w-md w-full space-y-8 bg-white p-10 rounded-xl shadow-lg text-center border border-gray-100">
10+
<div>
11+
<h2 className="text-2xl font-bold text-gray-900 mb-2 font-title">
12+
νšŒμ›κ°€μž…
13+
</h2>
14+
<p className="text-gray-500">
15+
λͺ¨μ΄μƒ€μ™€ ν•¨κ»˜ λͺ¨μž„ ν™œλ™μ„ μ‹œμž‘ν•΄ λ³΄μ„Έμš”!
16+
</p>
17+
</div>
18+
19+
<div className="flex justify-center gap-6">
20+
<a
21+
href={GOOGLE_AUTH_URL}
22+
className="w-14 h-14 flex items-center justify-center border border-gray-200 rounded-full hover:bg-gray-50 transition-all shadow-sm bg-white"
23+
aria-label="Google둜 νšŒμ›κ°€μž…"
24+
>
25+
<svg width="24" height="24" viewBox="0 0 24 24">
26+
<path
27+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
28+
fill="#4285F4"
29+
/>
30+
<path
31+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-1 .67-2.28 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
32+
fill="#34A853"
33+
/>
34+
<path
35+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
36+
fill="#FBBC05"
37+
/>
38+
<path
39+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
40+
fill="#EA4335"
41+
/>
42+
</svg>
43+
</a>
44+
45+
<a
46+
href={KAKAO_AUTH_URL}
47+
className="w-14 h-14 flex items-center justify-center bg-[#FEE500] rounded-full hover:bg-[#FDD835] transition-all shadow-sm"
48+
aria-label="카카였둜 νšŒμ›κ°€μž…"
49+
>
50+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
51+
<path
52+
d="M12 3C7.02944 3 3 6.13401 3 10C3 12.5 4.5 14.5 7 15.5L6 19L10 16.5C10.5 16.8 11.2 17 12 17C16.9706 17 21 13.866 21 10C21 6.13401 16.9706 3 12 3Z"
53+
fill="#3A1D1D"
54+
/>
55+
</svg>
56+
</a>
57+
</div>
58+
59+
<div className="relative">
60+
<div className="absolute inset-0 flex items-center">
61+
<span className="w-full border-t border-gray-200" />
62+
</div>
63+
<div className="relative flex justify-center text-xs uppercase">
64+
<span className="bg-white px-3 text-gray-400 font-medium">
65+
λ˜λŠ”
66+
</span>
67+
</div>
68+
</div>
69+
70+
<button
71+
onClick={() => navigate('/register/email')}
72+
className="w-full py-3.5 px-4 rounded-md bg-blue-600 text-white hover:bg-blue-700 font-bold shadow-md transition-all active:scale-[0.98]"
73+
>
74+
μ΄λ©”μΌλ‘œ κ°€μž…ν•˜κΈ°
75+
</button>
76+
</div>
77+
</div>
78+
);
79+
}

β€Ž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 '../hooks/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)