diff --git a/frontend/src/api/user/userAPI.ts b/frontend/src/api/user/userAPI.ts new file mode 100644 index 00000000..20b14d56 --- /dev/null +++ b/frontend/src/api/user/userAPI.ts @@ -0,0 +1,6 @@ +import apiClient from "@_api/apiClient"; +import type { UserCreateRequest, UserCreateResponse } from "./userTypes"; + +export const signup = async (data: UserCreateRequest): Promise => { + return await apiClient.post('/users', data, false); +}; \ No newline at end of file diff --git a/frontend/src/api/user/userTypes.ts b/frontend/src/api/user/userTypes.ts new file mode 100644 index 00000000..94804f51 --- /dev/null +++ b/frontend/src/api/user/userTypes.ts @@ -0,0 +1,27 @@ +export interface UserCreateRequest { + /** not empty */ + name: string; + + /** not empty, email format */ + email: string; + + /** length >= 8 */ + password: string; + + /** not empty, pattern: ^010([0-9]{3,4})([0-9]{4})$ */ + phone: string; + + /** nullable */ + regionCode?: string | null; +} + +export interface UserCreateResponse { + id: number; + name: string; +} + +export interface UserContactRetrieveResponse { + id: number; + name: string; + phone: string; +} diff --git a/frontend/src/css/signup-page-v2.css b/frontend/src/css/signup-page-v2.css index 1fc04326..7918185b 100644 --- a/frontend/src/css/signup-page-v2.css +++ b/frontend/src/css/signup-page-v2.css @@ -63,3 +63,9 @@ .signup-form-v2 .btn-primary:hover { background-color: #1B64DA; } + +.error-text { + color: #E53E3E; + font-size: 12px; + margin-top: 4px; +} \ No newline at end of file diff --git a/frontend/src/pages/SignupPage.tsx b/frontend/src/pages/SignupPage.tsx index ed2c208d..0ac27557 100644 --- a/frontend/src/pages/SignupPage.tsx +++ b/frontend/src/pages/SignupPage.tsx @@ -1,42 +1,146 @@ -import React, { useState } from 'react'; +import { signup } from '@_api/user/userAPI'; +import type { UserCreateRequest, UserCreateResponse } from '@_api/user/userTypes'; +import '@_css/signup-page-v2.css'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { signup } from '@_api/member/memberAPI'; -import type { SignupRequest } from '@_api/member/memberTypes'; + +const MIN_PASSWORD_LENGTH = 8; const SignupPage: React.FC = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); + const [phone, setPhone] = useState(''); + const [errors, setErrors] = useState>({}); + const [hasSubmitted, setHasSubmitted] = useState(false); + const navigate = useNavigate(); - const handleSignup = async () => { - const request: SignupRequest = { email, password, name }; - await signup(request) - .then((response) => { - alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`); - navigate('/login') - }) - .catch(error => { - console.error(error); - }); + const validate = () => { + const newErrors: Record = {}; + + if (!name.trim()) { + newErrors.name = '이름을 입력해주세요.'; + } + if (!email.trim()) { + newErrors.email = '이메일을 입력해주세요.'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + newErrors.email = '올바른 이메일 형식을 입력해주세요.'; + } + if (password.length < MIN_PASSWORD_LENGTH) { + newErrors.password = `비밀번호는 최소 ${MIN_PASSWORD_LENGTH}자리 이상이어야 합니다.`; + } + if (!phone.trim()) { + newErrors.phone = '전화번호를 입력해주세요.'; + } else if (!/^010([0-9]{3,4})([0-9]{4})$/.test(phone)) { + newErrors.phone = '올바른 휴대폰 번호 형식이 아닙니다. (예: 01012345678)'; + } + + return newErrors; + }; + + // 제출 이후에는 입력값이 바뀔 때마다 다시 validate 실행 + useEffect(() => { + if (hasSubmitted) { + setErrors(validate()); + } + }, [email, password, name, phone, hasSubmitted]); + + const handleSignup = async (e: React.FormEvent) => { + e.preventDefault(); + setHasSubmitted(true); + + const newErrors = validate(); + setErrors(newErrors); + + if (Object.keys(newErrors).length > 0) return; + + const request: UserCreateRequest = { email, password, name, phone, regionCode: null }; + try { + const response: UserCreateResponse = await signup(request); + alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`); + navigate('/login'); + } catch (error: any) { + const message = + error.response?.data?.message || + '회원가입에 실패했어요. 입력 정보를 확인해주세요.'; + alert(message); + console.error(error); + } }; return ( -
-

Signup

-
- - setEmail(e.target.value)} /> -
-
- - setPassword(e.target.value)} /> -
-
- - setName(e.target.value)} /> -
- +
+

회원가입

+
+
+ + setEmail(e.target.value)} + required + /> + {hasSubmitted && errors.email && ( +

{errors.email}

+ )} +
+ +
+ + setPassword(e.target.value)} + required + /> + {hasSubmitted && errors.password && ( +

{errors.password}

+ )} +
+ +
+ + setName(e.target.value)} + required + /> + {hasSubmitted && errors.name && ( +

{errors.name}

+ )} +
+ +
+ + setPhone(e.target.value)} + required + /> + {hasSubmitted && errors.phone && ( +

{errors.phone}

+ )} +
+ + +
); };