🎯v2.0 업데이트 내용
웹 사이트 주소 : https://www.ys2ys2.com/bbangmap/
💡회원가입 기능 추가
✅ 회원가입 기능을 추가하였습니다.
✅ 회원가입은 이메일 인증을 통해 가입할 수 있습니다.
💡가게 데이터 입력 권한 변경
✅ 로그인한 회원만 가게 데이터를 입력할 수 있게 변경하였습니다.
✅ 비로그인 시 붕어빵 가게 정보 조회만 가능합니다.
💡반응형 CSS 추가
✅ 헤더와 회원가입, 로그인에 대한 반응형 CSS를 추가하였습니다.
📒업데이트 공부하기
❗ Spring Boot와 JPA를 활용한 회원가입 로직
붕어빵 가게 정보 입력은 회원만 가능하도록 하여 무분별한 등록을 방지하기 위해 회원가입 기능을 구현했다.
비회원은 붕어빵 가게 정보 조회만 할 수 있고, 회원은 조회와 함께 가게 정보를 등록할 수 있다.
✏️이용 약관 동의 로직
먼저 회원가입 시 약관동의를 관리할 컴포넌트를 만들었다.
필수 항목에 대한 체크가 이루어지지 않으면 다음 페이지로 넘어갈 수 없게 구현했고,
선택약관에 대한 동의 여부를 확인할 수 있게 테이블에 marcketing과 location이라는 컬럼을 추가했고
동의 = Y, 비동의 = N 이라는 값으로 저장되게 했다.
선택 약관에 대한 동의 상태는 React의 상태 관리와 라우팅(React Router)을 활용하여 처리했다.
선택 약관에 대한 상태는 checkedItems 객체에 저장되고 마케팅과 위치기반 서비스를 관리하게 했다.
const [checkedItems, setCheckedItems] = useState({
termsOfService: false, // 이용약관 (필수)
personalInfo: false, // 개인정보 수집 및 이용 (필수)
identityAuth: false, // 광고 및 마케팅 활용 동의 (선택)
locationService: false // 위치기반서비스 이용 동의 (선택)
});
사용자가 약관 동의 후 다음 버튼을 클릭하면 navigate를 통해 회원가입 페이지로 이동하게 되고
이때 선택 약관의 동의 여부를 React Router의 location.state를 통해 상태로 전달한다.
const handleNext = () => {
if (!checkedItems.termsOfService || !checkedItems.personalInfo) {
alert("필수 항목에 동의해야 합니다.");
return;
}
// 선택 약관 동의 여부 전달
navigate("/register", {
state: {
marketing: checkedItems.identityAuth ? "Y" : "N", // 광고 및 마케팅
location: checkedItems.locationService ? "Y" : "N", // 위치 기반 서비스
},
});
};
React Router의 location.state를 통해 상태로 전달받은 값들은 회원가입 요청인 post /register의 데이터에 포함되어 서버로 전송된다.
const { marketing, location: locationConsent } = location.state || {}; // Terms에서 전달된 동의 정보
const [formData, setFormData] = useState({
nickname: "",
email: "",
password: "",
confirmPassword: "",
verificationCode: "", // 인증 코드 추가
});
✏️이메일 인증 로직
회원 가입에 필요한 요소는 사이트 목적상 간단하게 구성했다.
닉네임, 이메일, 비밀번호 로 구성했으며 이메일 같은 경우는 무분별한 가입을 막기 위해 이메일 인증 기능을 추가했다.
이메일 인증은 JavaMailSender를 사용했으며 spring-boot-starter-mail 의존성을 추가해서 사용했다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
그 후 application.properties에서 SMTP관련 설정을 추가해서 사용했다.
# 메일 서버 설정
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com # Gmail 계정
spring.mail.password=your-email-password # Gmail 앱 비밀번호
spring.mail.protocol=smtp
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.debug=true
사용자가 이메일을 입력하고 인증하기 버튼을 누르면 알럿과 함께 인증 코드 부분이 나타난다.
이메일 인증 코드를 받고 인증 코드에 입력 후 인증확인 버튼을 누르면 인증이 마무리 된다.
이메일도 마찬가지로 상태로 관리하게 하였다.
인증 코드는 5분간 유효하고, 새로 인증 코드를 받으면 기존 인증 코드는 폐기되게 구현했다.
또한 이메일로 로그인을 하기 때문에 중복이메일 방지 기능도 추가했다.
const handleEmailVerification = async () => {
if (!formData.email) {
alert("이메일을 입력하세요.");
return;
}
try {
const response = await apiClient.post("/email/verify", { email: formData.email });
if (response.status === 200) {
alert("인증 이메일이 발송되었습니다. 이메일을 확인해주세요.");
setShowVerificationCodeInput(true); // 인증 코드 입력 폼 표시
}
} catch (error) {
console.error(error);
alert("이메일 인증 요청 중 문제가 발생했습니다.");
}
};
const handleCodeVerification = async () => {
if (!formData.verificationCode) {
alert("인증 코드를 입력하세요.");
return;
}
try {
const response = await apiClient.post("/email/verify-code", {
email: formData.email,
code: formData.verificationCode,
});
if (response.status === 200) {
alert("이메일 인증이 완료되었습니다.");
setIsEmailVerified(true); // 이메일 인증 성공 상태 업데이트
}
} catch (error) {
console.error(error);
alert("인증 코드가 올바르지 않습니다.");
}
};
@PostMapping("/verify-code")
public ResponseEntity<?> verifyCode(@RequestBody Map<String, String> request) {
String email = request.get("email");
String code = request.get("code");
boolean isValid = emailService.verifyCode(email, code);
if (isValid) {
return ResponseEntity.ok("인증 코드가 확인되었습니다.");
} else {
return ResponseEntity.status(400).body("인증 코드가 올바르지 않습니다.");
}
}
const handleSubmit = async (e) => {
e.preventDefault();
const requestData = {
nickname: formData.nickname,
email: formData.email,
password: formData.password,
marketing,
location: locationConsent,
};
try {
const response = await apiClient.post("/register", requestData);
if (response.status === 200) {
alert("회원가입이 완료되었습니다!");
}
} catch (error) {
if (error.response && error.response.status === 400 && error.response.data === "duplicateEmail") {
alert("이미 사용 중인 이메일입니다. 다시 확인해주세요.");
} else {
alert("회원가입 중 문제가 발생했습니다.");
}
}
};
✏️비밀번호 유효성 검사
비밀번호는 최소 6자리 이상 문자와 특수문자 1개 이상 포함의 유효성 검사로 하였다.
비밀번호 입력 폼 옆에 span을 두고 상태를 체크하면서 통과 시 초록색으로 변하게 구성했다.
passwordValid라는 상태로 비밀번호 유효성을 2가지 조건으로 나눠서 관리하게 했다. (length, specialChar)
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
if (name === "password") {
setPasswordValid({
length: value.length >= 6,
specialChar: /[!@#$%^&*(),.?":{}|<>]/.test(value),
});
}
};
그리고 삼항연산자를 통해 조건 충족시 valid, 조건 불충족시 invalid로 관리했다. 그 후 css를 추가했다.
<label>
<span>비밀번호</span>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="비밀번호를 입력하세요"
required
/>
<div className="password-validation">
<span className={passwordValid.length ? "valid" : "invalid"}>
최소 6자리 이상
</span>
<br />
<span className={passwordValid.specialChar ? "valid" : "invalid"}>
특수문자 1개 이상 포함
</span>
</div>
</label>
.password-validation span.valid {
color: green;
}
.password-validation span.invalid {
color: red;
}
✏️회원 가입 백엔드
그 후 회원가입 버튼을 누르면 폼에 담겨져 있는 데이터를 검증한다.
1. 이메일 인증 여부 확인
2. 비밀번호 유효성 검사
3. 비밀번호 확인
if (!isEmailVerified) {
alert("이메일 인증이 완료되지 않았습니다.");
return;
}
if (!passwordValid.length || !passwordValid.specialChar) {
alert("비밀번호는 최소 6자리 이상, 특수문자 1개 이상 포함해야 합니다.");
return;
}
if (!passwordMatch) {
alert("비밀번호가 일치하지 않습니다.");
return;
}
검증이 끝나면 닉네임, 이메일, 비밀번호, 선택 약관 동의 정보를 담아서 서버에 /register 경로로 axios 요청을 한다.
const requestData = {
nickname: formData.nickname,
email: formData.email,
password: formData.password,
marketing,
location: locationConsent,
};
try {
const response = await apiClient.post("/register", requestData);
if (response.status === 200) {
alert("회원가입이 완료되었습니다!");
}
} catch (error) {
if (error.response && error.response.status === 400 && error.response.data === "duplicateEmail") {
alert("이미 사용 중인 이메일입니다. 다시 확인해주세요.");
} else {
alert("회원가입 중 문제가 발생했습니다.");
}
}
클라이언트에서 전달된 회원가입 데이터를 서버가 받아 데이터 검증 및 처리 후 JPA를 이용해 데이터베이스에 저장한다.
서버에서의 처리 흐름
Controller
@RestController
@RequestMapping("/register")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<?> registerUser(@RequestBody UserEntity userEntity) {
boolean success = userService.registerUser(userEntity);
if (!success) {
// 이메일 중복 오류 발생 시 400 상태 코드와 "duplicateEmail" 메시지 반환
return ResponseEntity.badRequest().body("duplicateEmail");
}
return ResponseEntity.ok("회원가입이 완료되었습니다.");
}
}
Service(interface)
public interface UserService {
boolean registerUser(UserEntity userEntity);
}
ServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public boolean registerUser(UserEntity userEntity) {
if (userRepository.existsByEmail(userEntity.getEmail())) {
return false;
}
userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword()));
userRepository.save(userEntity);
return true;
}
Entity
@Entity
@Table(name = "bbangmapUser")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String nickname;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, length = 255)
private String password;
@Column(nullable = false, length = 1)
private String marketing;
@Column(nullable = false, length = 1)
private String location;
}
Repository
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
boolean existsByEmail(String email);
Optional<UserEntity> findByEmail(String email);
}
✏️비밀번호 암호화
비밀번호가 DB에 저장될 때 암호화돼서 저장되게 하였다.
Spring Security에서 제공하는 PasswordEncoder 인터페이스인 BCryptPasswordEncoder를 사용했다.
💡BCryptPasswordEncoder?
BCryptPasswordEncoder는 Spring Security에서 제공하는 비밀번호 암호화 도구로
내부적으로 bcrypt 해시 알고리즘을 사용하여 비밀번호를 암호화한다.
동일한 입력값이어도 항상 다른 해시값을 생성하고 해시 알고리즘은 복호화가 불가능하며, 입력값과 저장된 해시값을 비교하는 방식으로 검증이 이루어진다고 한다.
먼저 build.gradle에 Spring Security 의존성을 추가했다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 의존성
}
Spring Boot에서 비밀번호 암호화는 BCryptPasswordEncoder 클래스를 사용하는데 SecurityConfig클래스를 추가 후
bean 어노테이션을 사용해 BCryptPasswordEncoder를 Spring 컨텍스트에 등록했다.
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
비밀번호 저장 시 암호화 해서 데이터베이스에 저장이 된다.
암호화된 비밀번호는 복호화할 수 없고 이후 검증은 입력값을 암호화한 결과와 비교하는 방식으로 이루어진다.
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public boolean registerUser(UserEntity userEntity) {
if (userRepository.existsByEmail(userEntity.getEmail())) {
return false;
}
// 비밀번호 암호화
userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword()));
userRepository.save(userEntity);
return true;
}
}
'Develop > React' 카테고리의 다른 글
useEffect (0) | 2025.05.25 |
---|---|
webX (0) | 2025.05.17 |
붕어빵 프로젝트 업데이트 v1.1 (0) | 2025.01.09 |
붕어빵 프로젝트 업데이트 v1.0 (0) | 2024.12.26 |
붕어빵 프로젝트 업데이트 v0.3 (0) | 2024.12.22 |