Skip to content

Commit

Permalink
Merge pull request #48 from Yanol-Market/feature/39
Browse files Browse the repository at this point in the history
Feature/39 회원가입
  • Loading branch information
cyPark95 authored Jan 13, 2024
2 parents 27d8e31 + d3baf45 commit e2febd3
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public enum ErrorCode {
EMPTY_FAILURE_HANDLER(INTERNAL_SERVER_ERROR, "FailureHandler 필수 값 입니다."),
LOGIN_FAIL(BAD_REQUEST, "이메일, 비밀번호를 확인해주세요."),
INVALID_TOKEN(UNAUTHORIZED, "유효하지 않은 토큰 입니다."),
SAVE_REFRESH_TOKEN_FAILED(UNAUTHORIZED, "Token 저장 중 오류가 발생 했습니다.")
SAVE_REFRESH_TOKEN_FAILED(UNAUTHORIZED, "Token 저장 중 오류가 발생 했습니다."),

// User
ALREADY_EXIST_EMAIL(BAD_REQUEST, "이미 사용중인 이메일입니다. 이미 가입하신 적이 있다면 로그인을 시도해주세요"),
ALREADY_EXIST_NICKNAME(BAD_REQUEST, "이미 사용중인 아이디입니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@
@RequiredArgsConstructor
public class SecurityConfiguration {

private static final String[] PERMIT_ALL_GET_URLS = new String[]{
private static final String[] PERMIT_ALL_GET_URLS = new String[] {
"/favicon.ico",
"/docs/**"
"/docs/**",
"/users/check/**"
};

private static final String[] PERMIT_ALL_POST_URLS = new String[] {
"/users"
};

private final ObjectMapper objectMapper;
Expand All @@ -63,6 +68,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(GET, PERMIT_ALL_GET_URLS).permitAll()
.requestMatchers(POST, PERMIT_ALL_POST_URLS).permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package site.goldenticket.domain.user.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import site.goldenticket.common.response.CommonResponse;
import site.goldenticket.domain.user.dto.JoinRequest;
import site.goldenticket.domain.user.dto.JoinResponse;
import site.goldenticket.domain.user.service.UserService;

import static org.springframework.http.HttpStatus.CREATED;

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

private final UserService userService;

@GetMapping("/check/email")
public ResponseEntity<CommonResponse<Boolean>> duplicateEmail(@RequestParam String email) {
return ResponseEntity.ok(CommonResponse.ok(userService.isExistEmail(email)));
}

@GetMapping("/check/nickname")
public ResponseEntity<CommonResponse<Boolean>> duplicateNickname(@RequestParam String nickname) {
return ResponseEntity.ok(CommonResponse.ok(userService.isExistNickname(nickname)));
}

@PostMapping
public ResponseEntity<CommonResponse<JoinResponse>> join(@RequestBody @Validated JoinRequest joinRequest) {
JoinResponse response = userService.join(joinRequest);
return new ResponseEntity<>(CommonResponse.ok(response), CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package site.goldenticket.domain.user.dto;

import jakarta.validation.constraints.NotEmpty;
import site.goldenticket.domain.user.entity.Agreement;
import site.goldenticket.domain.user.entity.User;

public record AgreementRequest(
@NotEmpty(message = "마케팅 동의는 필수 선택 사항입니다.")
Boolean isMarketing
) {

public Agreement toEntity(User user) {
Agreement agreement = Agreement.builder()
.marketing(isMarketing)
.build();
agreement.registerUser(user);
return agreement;
}
}
41 changes: 41 additions & 0 deletions src/main/java/site/goldenticket/domain/user/dto/JoinRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package site.goldenticket.domain.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import site.goldenticket.domain.user.entity.User;

public record JoinRequest(
@NotEmpty(message = "비밀번호는 필수 입력 항목입니다.")
@Size(min = 2, message = "이름은 두 글자 이상의 한글이어야 합니다.")
String name,
@NotEmpty(message = "닉네임은 필수 입력 항목입니다.")
@Size(max = 15, message = "닉네임은 1글자 이상, 15자 이하여야 합니다.")
String nickname,
@NotEmpty(message = "이메일은 필수 입력 항목입니다.")
@Email(message = "이메일은 유효한 형식이어야 합니다.")
String email,
@NotEmpty(message = "비밀번호는 필수 입력 항목입니다.")
@Size(min = 6, max = 20, message = "비밀번호는 6자 이상, 20자 이하여야 합니다.")
String password,
@NotEmpty(message = "휴대폰 번호는 필수 입력 항목입니다.")
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "000-0000-0000 형식이여야 합니다.")
String phoneNumber,
Long yanoljaId,
AgreementRequest agreement
) {

public User toEntity(String encodePassword) {
User user = User.builder()
.name(name)
.nickname(nickname)
.email(email)
.password(encodePassword)
.phoneNumber(phoneNumber)
.yanoljaId(yanoljaId)
.build();
agreement.toEntity(user);
return user;
}
}
10 changes: 10 additions & 0 deletions src/main/java/site/goldenticket/domain/user/dto/JoinResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package site.goldenticket.domain.user.dto;

import site.goldenticket.domain.user.entity.User;

public record JoinResponse(Long id) {

public static JoinResponse from(User user) {
return new JoinResponse(user.getId());
}
}
34 changes: 34 additions & 0 deletions src/main/java/site/goldenticket/domain/user/entity/Agreement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package site.goldenticket.domain.user.entity;

import jakarta.persistence.*;
import lombok.*;
import site.goldenticket.common.entiy.BaseTimeEntity;

import static jakarta.persistence.FetchType.LAZY;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(exclude = {"user"})
public class Agreement extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = LAZY)
@JoinColumn(name = "user_id")
private User user;

private Boolean marketing;

@Builder
private Agreement(Boolean marketing) {
this.marketing = marketing;
}

public void registerUser(User user) {
this.user = user;
user.registerAlertSetting(this);
}
}
33 changes: 22 additions & 11 deletions src/main/java/site/goldenticket/domain/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package site.goldenticket.domain.user.entity;

import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;
import site.goldenticket.common.entiy.BaseTimeEntity;

import static jakarta.persistence.CascadeType.ALL;
import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;
import static site.goldenticket.domain.user.entity.RoleType.ROLE_USER;

@Getter
@Entity
Expand All @@ -34,27 +38,34 @@ public class User extends BaseTimeEntity {
@Enumerated(STRING)
private RoleType role;

private String yanoljaId;
private Long yanoljaId;
private boolean deleted;

@OneToOne(
mappedBy = "user", fetch = LAZY,
cascade = ALL, orphanRemoval = true
)
private Agreement agreement;

@Builder
private User(
String name,
String nickname,
String email,
String password,
String phoneNumber,
String imageUrl,
String yanoljaId,
RoleType role
@Nullable String name,
@Nullable String nickname,
@Nullable String email,
@Nullable String password,
@Nullable String phoneNumber,
Long yanoljaId
) {
this.role = ROLE_USER;
this.name = name;
this.nickname = nickname;
this.email = email;
this.password = password;
this.phoneNumber = phoneNumber;
this.imageUrl = imageUrl;
this.yanoljaId = yanoljaId;
this.role = role;
}

public void registerAlertSetting(Agreement agreement) {
this.agreement = agreement;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);

boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package site.goldenticket.domain.user.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import site.goldenticket.common.exception.CustomException;
import site.goldenticket.domain.user.dto.JoinRequest;
import site.goldenticket.domain.user.dto.JoinResponse;
import site.goldenticket.domain.user.entity.User;
import site.goldenticket.domain.user.repository.UserRepository;

import static site.goldenticket.common.response.ErrorCode.ALREADY_EXIST_EMAIL;
import static site.goldenticket.common.response.ErrorCode.ALREADY_EXIST_NICKNAME;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public boolean isExistEmail(String email) {
log.info("Duplicated Check Email = {}", email);
return userRepository.findByEmail(email).isPresent();
}

public boolean isExistNickname(String nickname) {
log.info("Duplicated Check Nickname = {}", nickname);
return userRepository.existsByNickname(nickname);
}

@Transactional
public JoinResponse join(JoinRequest joinRequest) {
joinValidate(joinRequest);
log.info("Join User Info = {}", joinRequest);

String encodePassword = passwordEncoder.encode(joinRequest.password());
User user = joinRequest.toEntity(encodePassword);
userRepository.save(user);
return JoinResponse.from(user);
}

private void joinValidate(JoinRequest joinRequest) {
if (isExistEmail(joinRequest.email())) {
throw new CustomException(ALREADY_EXIST_EMAIL);
}

if (isExistNickname(joinRequest.nickname())) {
throw new CustomException(ALREADY_EXIST_NICKNAME);
}
}
}
7 changes: 2 additions & 5 deletions src/test/java/site/goldenticket/common/utils/UserUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@

import site.goldenticket.domain.user.entity.User;

import static site.goldenticket.domain.user.entity.RoleType.ROLE_USER;

public final class UserUtils {

public static String EMAIL = "[email protected]";
public static String PASSWORD = "password";
public static String NAME = "name";
public static String NICKNAME = "nickname";
public static String PHONENUMBER = "010-0000-0000";
public static String PHONE_NUMBER = "010-0000-0000";

public static User createUser(String encodePassword) {
return User.builder()
.email(EMAIL)
.password(encodePassword)
.name(NAME)
.nickname(NICKNAME)
.phoneNumber(PHONENUMBER)
.role(ROLE_USER)
.phoneNumber(PHONE_NUMBER)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package site.goldenticket.domain.user.controller;

import io.restassured.RestAssured;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import site.goldenticket.common.config.ApiTest;
import site.goldenticket.domain.user.dto.AgreementRequest;
import site.goldenticket.domain.user.dto.JoinRequest;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static site.goldenticket.common.utils.UserUtils.*;

@DisplayName("UserController 검증")
class UserControllerTest extends ApiTest {

@Test
@DisplayName("회원가입 검증")
void join() {
// given
JoinRequest request = new JoinRequest(
NAME,
NICKNAME,
EMAIL,
PASSWORD,
PHONE_NUMBER,
null,
new AgreementRequest(true)
);

String url = "/users";

// when
ExtractableResponse<Response> result = RestAssured
.given().log().all()
.contentType(APPLICATION_JSON_VALUE)
.body(request)
.when()
.post(url)
.then().log().all()
.extract();

// then
assertThat(result.statusCode()).isEqualTo(CREATED.value());
assertThat(result.jsonPath().getLong("data.id")).isEqualTo(1L);
}
}

0 comments on commit e2febd3

Please sign in to comment.