✔️ username, password, email, name을 입력 받고 회원 가입을 진행하도록 하겠음!
📌 User 엔티티 구현
package yerong.InstagramCloneCoding.domain.user;
import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.BaseTimeEntity;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userId")
private Long id;
@Column(nullable = false, unique = true, length = 20)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
private String bio; //자기소현
private String website;
private String phone;
private String gender;
}
- BaseTimeEntity를 통해 생성 시간 및 수정 시간을 받아올 것임!
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- 번호 증가 전략이 데이터베이스를 따라가는 것!
- @Column(unique = true)
- 중복 불가 제약 조건 설정
- 중복되면 오류 발생 ! (이 오류를 처리하는 방법은 밑에서 다시 설명하겠음)
✅ BaseTimeEntity코드
package yerong.InstagramCloneCoding.domain;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
➡️ JPAAuditing 활성화 시켜줘야 함!
@EnableJpaAuditing
@SpringBootApplication
public class InstagramCloneCodingApplication {
public static void main(String[] args) {
SpringApplication.run(InstagramCloneCodingApplication.class, args);
}
}
📌 UserRepository
package yerong.InstagramCloneCoding.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import yerong.InstagramCloneCoding.domain.user.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
➡️ JPA Repository 사용
📌 AuthService
package yerong.InstagramCloneCoding.service;
import yerong.InstagramCloneCoding.web.dto.SignupDto;
public interface AuthService {
void join(SignupDto signupDto);
}
package yerong.InstagramCloneCoding.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.repository.UserRepository;
import yerong.InstagramCloneCoding.service.AuthService;
import yerong.InstagramCloneCoding.web.dto.SignupDto;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
@Override
@Transactional
public void join(SignupDto signupDto){
User user = signupDto.toEntity();
userRepository.save(user);
}
}
➡️ 회원가입 서비스 기본적인 구현
(비밀번호 해시 등 구체적인 작업은 밑에서 다시 설명!)
📌 AuthController
- csrf 토큰 때문에 회원 가입을 완료한 후 로그인으로 이동하는 것이 실행 안됨
- 서버는 시큐리티가 감싸고 있기 때문에 서버에 도착하기 전에 시큐리티가 검사를 진행함
- 비활성화 시키는 것이 편함
- SecurityConfig 코드 중 일부
return http
.csrf(csrfConfig ->
csrfConfig.disable())
.build();
- signup.html 코드에 해당 코드를 구현해주면 해당 url을 post로 요청 시 아래의 코드 실행됨
<form class="login__input" action="/auth/signup" method="post">
✅ AuthController 코드
package yerong.InstagramCloneCoding.web.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.service.AuthService;
import yerong.InstagramCloneCoding.web.dto.SignupDto;
@Controller
@Slf4j
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signin")
public String signinForm(){
return "views/auth/signin";
}
@GetMapping("/auth/signup")
public String signupForm(){
return "views/auth/signup";
}
@PostMapping("/auth/signup")
public String signup(SignupDto signupDto){
authService.join(signupDto);
return "views/auth/signin";
}
}
📌 비밀번호 해시
✅ SecurityConfig
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
➡️ 빈으로 등록
✅ AuthServiceImpl 코드에 비밀번호 encode 추가 및 Role 설정
private final BCryptPasswordEncoder passwordEncoder;
@Override
@Transactional
public void join(SignupDto signupDto){
String rawPassword = signupDto.getPassword();
String encPassword = passwordEncoder.encode(rawPassword);
signupDto.setPassword(encPassword);
User user = signupDto.toEntity();
user.setRole(Role.USER);
userRepository.save(user);
}
📌 유효성 검증
✅ build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
✅ SignupDto
- Dto로 요청받을 것이기 때문에 여기에 조건 걸어줌
package yerong.InstagramCloneCoding.web.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import yerong.InstagramCloneCoding.domain.user.User;
@Getter
@Setter
public class SignupDto {
@Size(min = 2, max = 20)
@NotBlank
String username;
@NotBlank
String password;
@NotBlank
String email;
@NotBlank
String name;
public User toEntity(){
return User.builder()
.username(username)
.password(password)
.email(email)
.name(name)
.build();
}
}
- @Max() : 문자열/배열의 길이 제한
- @NotBlank : 빈칸이면 안됨
✅ 엔티티에도 적용
package yerong.InstagramCloneCoding.domain.user;
import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.BaseTimeEntity;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userId")
private Long id;
@Column(nullable = false, unique = true, length = 20)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
private String bio; //자기소개
private String website;
private String phone;
private String gender;
public void setRole(Role role){
this.role = role;
}
}
✅ Controller에도 적용
@PostMapping("/auth/signup")
public String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()){
errorMap.put(error.getField(), error.getDefaultMessage());
log.info(error.getDefaultMessage());
}
}
authService.join(signupDto);
return "views/auth/signin";
}
- Dto 앞에 @Valid 설정해두면 앞서 설정한 조건들에 맞게 유효성 검사를 자동으로 해줌
- 검증할 객체 바로 옆에 BindingResult 생성
- 자동으로 안에 에러들이 담김
- Map을 하나 만들어서 생성된 에러들을 담아줌
➡️ 20 이하여야 합니다 로그가 자동으로 생성됨!
✔️ @ResponseBody 적용 ver
@PostMapping("/auth/signup")
public @ResponseBody String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()){
errorMap.put(error.getField(), error.getDefaultMessage());
log.info(error.getDefaultMessage());
}
}
authService.join(signupDto);
return "views/auth/signin";
}
- @ResponseBody + @Controller = @RestController➡️ 즉, 데이터를 반환함
✅ Handler 적용
- 예외가 터지면 Handler가 가로챔
➡️ @ControllerAdvice 사용 - 따로 handler 패키지에 ControllerExceptionHandler 만들어서 사용
- @RestController 사용해서 데이터 전달
✔️ 공통응답DTO
package yerong.InstagramCloneCoding.web.dto;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class CMRespDto<T> {
private String message;
private T data;
}
➡️ 왜 T로 하는가? : 그때그때 리턴하고 싶은 형태가 다를 수도 있기 때문!
✔️ CustomValidationException
package yerong.InstagramCloneCoding.handler.exception;
import java.util.Map;
public class CustomValidationException extends RuntimeException{
private String message;
private Map<String, String> errorMap;
public CustomValidationException(String message, Map<String, String> errorMap){
super(message);
this.message = message;
this.errorMap = errorMap;
}
public Map<String, String> getErrorMap(){
return errorMap;
}
}
✔️ ControllerExceptionHandler
package yerong.InstagramCloneCoding.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationException;
import yerong.InstagramCloneCoding.web.dto.CMRespDto;
import java.util.Map;
@RestController
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {
@ExceptionHandler(CustomValidationException.class)
public CMRespDto<Map<String, String>> validationException(CustomValidationException e){
return new CMRespDto(-1, e.getMessage(), e.getErrorMap());
}
}
➡️ 뭘 리턴할 지 모를 때는 <> 사이의 값을 <?>로 해도 됨! (괜히 없는 타입을 해서 오류가 날 수도 있음)
✔️ AuthController
@PostMapping("/auth/signup")
public String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()){
errorMap.put(error.getField(), error.getDefaultMessage());
log.info(error.getDefaultMessage());
}
throw new CustomValidationException("유효성 검사 실패", errorMap);
}
else {
authService.join(signupDto);
return "views/auth/signin";
}
}
➡️ 일단 이렇게 예외 처리를 해놓으면 예외가 터지면(username의 길이가 20 이상, 나머지 값들이 빈칸) CustomValidationException 실행
❗️JavaScript 사용해서 예외 처리
✅ util/Script
- 에러가 발생해도 해당 페이지에서 에러를 처리해야지 다른 페이지로 넘어가면 안됨
- 프론트에서 에러를 처리하면 되는 것 같지만 postman으로 실행하면 적용이 안됨
➡️ 즉, 서버에서도 에러처리를 해줘야 함
package yerong.InstagramCloneCoding.util;
public class Script {
public static String back(String message){
StringBuffer sb = new StringBuffer();
sb.append("<script>");
sb.append("alert('"+message+"');");
sb.append("history.back();");
sb.append("</script>");
return sb.toString();
}
}
✔️ ControllerExceptionHandler
package yerong.InstagramCloneCoding.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationException;
import yerong.InstagramCloneCoding.util.Script;
import yerong.InstagramCloneCoding.web.dto.CMRespDto;
import java.util.Map;
@RestController
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {
@ExceptionHandler(CustomValidationException.class)
public String validationException(CustomValidationException e){
return Script.back(e.getErrorMap().toString());
}
}
➡️ 보기 좋게 에러 처리
➡️ 확인 누르면 다시 회원가입 창으로 넘어옴!
💡CMRespDto vs Script
- 클라이언트에게 응답할 때는 Script 가 좋음
- Ajax 통신할 때는 CMRespDto
- Android 통신 : CMRespDto
PREV
NEXT
'프로젝트 > 인스타그램 클론 코딩' 카테고리의 다른 글
[인스타그램 클론 코딩][Spring boot] 6. 구독하기 (0) | 2024.02.19 |
---|---|
[인스타그램 클론 코딩][Spring boot] 5. 회원 정보 수정 (1) | 2024.02.19 |
[인스타그램 클론 코딩][Spring boot] 4. 로그인 구현 (1) | 2024.02.18 |
[인스타그램 클론 코딩][Spring boot] 2. Security 설정 (0) | 2024.02.16 |
[인스타그램 클론 코딩][Spring boot] 1. 프로젝트 설정 (0) | 2024.02.14 |