generated from pricelees/issue-pr-template
[#34] 회원 / 인증 도메인 재정의 #43
6
frontend/src/api/user/userAPI.ts
Normal file
6
frontend/src/api/user/userAPI.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import apiClient from "@_api/apiClient";
|
||||||
|
import type { UserCreateRequest, UserCreateResponse } from "./userTypes";
|
||||||
|
|
||||||
|
export const signup = async (data: UserCreateRequest): Promise<UserCreateResponse> => {
|
||||||
|
return await apiClient.post('/users', data, false);
|
||||||
|
};
|
||||||
27
frontend/src/api/user/userTypes.ts
Normal file
27
frontend/src/api/user/userTypes.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -63,3 +63,9 @@
|
|||||||
.signup-form-v2 .btn-primary:hover {
|
.signup-form-v2 .btn-primary:hover {
|
||||||
background-color: #1B64DA;
|
background-color: #1B64DA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #E53E3E;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
@ -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 { 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 SignupPage: React.FC = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSignup = async () => {
|
const validate = () => {
|
||||||
const request: SignupRequest = { email, password, name };
|
const newErrors: Record<string, string> = {};
|
||||||
await signup(request)
|
|
||||||
.then((response) => {
|
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}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
|
alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
|
||||||
navigate('/login')
|
navigate('/login');
|
||||||
})
|
} catch (error: any) {
|
||||||
.catch(error => {
|
const message =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
'회원가입에 실패했어요. 입력 정보를 확인해주세요.';
|
||||||
|
alert(message);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-container" style={{ width: '400px' }}>
|
<div className="signup-container-v2">
|
||||||
<h2 className="content-container-title">Signup</h2>
|
<h2 className="page-title">회원가입</h2>
|
||||||
|
<form className="signup-form-v2" onSubmit={handleSignup}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Email address</label>
|
<label className="form-label">이메일</label>
|
||||||
<input type="email" className="form-control" placeholder="Enter email" value={email} onChange={e => setEmail(e.target.value)} />
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="이메일을 입력하세요"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{hasSubmitted && errors.email && (
|
||||||
|
<p className="error-text">{errors.email}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Password</label>
|
<label className="form-label">비밀번호</label>
|
||||||
<input type="password" className="form-control" placeholder="Enter password" value={password} onChange={e => setPassword(e.target.value)} />
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="비밀번호를 입력하세요"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{hasSubmitted && errors.password && (
|
||||||
|
<p className="error-text">{errors.password}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Name</label>
|
<label className="form-label">이름</label>
|
||||||
<input type="text" className="form-control" placeholder="Enter name" value={name} onChange={e => setName(e.target.value)} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="이름을 입력하세요"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{hasSubmitted && errors.name && (
|
||||||
|
<p className="error-text">{errors.name}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-custom" onClick={handleSignup}>Register</button>
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">전화번호</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="전화번호를 입력하세요('-' 제외)"
|
||||||
|
value={phone}
|
||||||
|
onChange={e => setPhone(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{hasSubmitted && errors.phone && (
|
||||||
|
<p className="error-text">{errors.phone}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn-primary"
|
||||||
|
disabled={hasSubmitted && Object.keys(errors).length > 0}
|
||||||
|
>
|
||||||
|
가입하기
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user