Skip to content

Commit

Permalink
Merge pull request #34 from 2024-TEAM-05/feat/signup
Browse files Browse the repository at this point in the history
feat: 회원가입 비즈니스 로직 구현
  • Loading branch information
uijin-j authored Aug 26, 2024
2 parents 0e2d8f7 + 350460a commit 84f7922
Show file tree
Hide file tree
Showing 29 changed files with 528 additions and 40 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'

// Spring Cloud
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public enum StatusCode {
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "요청하신 리소스를 찾을 수 없습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "요청 경로가 지원되지 않습니다."),
INVALID_EMAIL_FORMAT(HttpStatus.BAD_REQUEST, "이메일 형식이 올바르지 않습니다."),
SHORT_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호는 최소 10자 이상이어야 합니다."),
SIMPLE_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호는 숫자, 문자, 특수문자 중 2가지 이상을 포함해야 합니다."),
PASSWORD_HAS_REPEATING_CHARACTER(HttpStatus.BAD_REQUEST, "비밀번호에 3회 이상 연속되는 문자 사용이 불가합니다."),
DUPLICATE_ACCOUNT(HttpStatus.CONFLICT, "이미 사용 중인 계정입니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 사용 중인 이메일입니다."),
POST_NOT_EXIST(HttpStatus.NOT_FOUND, "존재하지 않는 게시물입니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "요청된 사용자를 찾을 수 없습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증 오류가 발생했습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package team05.integrated_feed_backend.common.util;

import java.security.SecureRandom;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class VerificationCodeGenerator {
private static final int CODE_LENGTH = 6;
private static final SecureRandom random = new SecureRandom();

public static String generateCode() {
StringBuilder codeBuilder = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
int digit = random.nextInt(10); // 0부터 9까지 랜덤한 숫자 선택
codeBuilder.append(digit);
}
return codeBuilder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand All @@ -24,7 +24,6 @@
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtManager jwtManager;
private final UserDetailsService userDetailsService;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
Expand Down Expand Up @@ -64,7 +63,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 비밀번호 인코딩을 사용하지 않음
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public class BadRequestException extends BusinessException {
public BadRequestException(StatusCode statusCode) {
super(statusCode);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ public BusinessException(StatusCode statusCode) {
this.statusCode = statusCode;
this.message = statusCode.getMessage();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public DataNotFoundException(StatusCode statusCode) {
super(statusCode);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public DuplicateResourceException(StatusCode statusCode) {
super(statusCode);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public ForbiddenException(StatusCode statusCode) {
super(statusCode);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package team05.integrated_feed_backend.infra.mail;

public interface MailService {

void sendVerificationCode(String sendTo, String code);

void send(String sendTo, String subject, String content);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package team05.integrated_feed_backend.infra.mail;

import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class SimpleMailService implements MailService {
private static final String VERIFICATION_EMAIL_TITLE = "Integrated Feed 회원가입 인증코드";

@Override
public void sendVerificationCode(String sendTo, String code) {
String content = buildVerificationEmailContent(code);
send(sendTo, VERIFICATION_EMAIL_TITLE, content);
}

@Override
public void send(String sendTo, String subject, String content) {
log.info("Send mail to: {}\nSubject: {}\nContent: {}", sendTo, subject, content);
}

private String buildVerificationEmailContent(String code) {
return "안녕하세요.\nIntegrated Feed의 이메일 등록을 위한 인증 메일입니다.\n\n인증번호는 " + code + " 입니다.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Builder(access = AccessLevel.PRIVATE)
public class VerificationCode extends BaseEntity {
private static final int EXPIRE_IN_DAYS = 1;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long verificationCodeId;
Expand All @@ -37,4 +38,16 @@ public class VerificationCode extends BaseEntity {

@Column(nullable = false)
private LocalDateTime expiredAt;
}

public static VerificationCode of(Member member, String code, LocalDateTime issuedAt) {
return VerificationCode.builder()
.member(member)
.code(code)
.expiredAt(calculateExpiredAt(issuedAt))
.build();
}

private static LocalDateTime calculateExpiredAt(LocalDateTime issuedAt) {
return issuedAt.plusDays(EXPIRE_IN_DAYS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package team05.integrated_feed_backend.module.auth.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import team05.integrated_feed_backend.module.auth.entity.VerificationCode;

@Repository
public interface VerificationCodeRepository extends JpaRepository<VerificationCode, Long> {

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package team05.integrated_feed_backend.module.user.controller;
package team05.integrated_feed_backend.module.member.controller;

import static org.springframework.http.HttpStatus.*;
import static team05.integrated_feed_backend.common.code.StatusCode.*;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import team05.integrated_feed_backend.common.BaseApiResponse;
import team05.integrated_feed_backend.module.user.dto.request.MemberSignupReq;
import team05.integrated_feed_backend.module.user.service.MemberService;
import team05.integrated_feed_backend.module.member.dto.request.MemberSignupReq;
import team05.integrated_feed_backend.module.member.service.MemberService;

@RestController
@RequestMapping("/api/members")
Expand All @@ -22,7 +23,7 @@ public class MemberController implements MemberControllerDocs {

@PostMapping
@ResponseStatus(ACCEPTED)
public BaseApiResponse<Void> signUp(@Valid MemberSignupReq request) {
public BaseApiResponse<Void> signUp(@RequestBody @Valid MemberSignupReq request) {
memberService.signUp(request);
return BaseApiResponse.of(SIGN_UP_ACCEPTED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package team05.integrated_feed_backend.module.user.controller;
package team05.integrated_feed_backend.module.member.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import team05.integrated_feed_backend.common.BaseApiResponse;
import team05.integrated_feed_backend.module.user.dto.request.MemberSignupReq;
import team05.integrated_feed_backend.module.member.dto.request.MemberSignupReq;

@Tag(name = "Member", description = "멤버 관련 API")
public interface MemberControllerDocs {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package team05.integrated_feed_backend.module.user.dto.request;
package team05.integrated_feed_backend.module.member.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package team05.integrated_feed_backend.module.member.entity;

import static team05.integrated_feed_backend.module.member.entity.enums.MemberStatus.*;

import org.springframework.security.crypto.password.PasswordEncoder;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -11,26 +18,52 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import team05.integrated_feed_backend.common.entity.BaseEntity;
import team05.integrated_feed_backend.module.member.entity.enums.MemberStatus;
import team05.integrated_feed_backend.module.member.entity.vo.Email;
import team05.integrated_feed_backend.module.member.entity.vo.Password;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Builder(access = AccessLevel.PRIVATE)
public class Member extends BaseEntity {
public static final MemberStatus DEFAULT_MEMBER_STATUS = UNVERIFIED;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;

@Column(nullable = false, unique = true)
private String account;

@Embedded
@Column(nullable = false)
private String password;
@Getter(AccessLevel.NONE)
private Password password;

@Embedded
@Column(nullable = false, unique = true)
private String email;
@Getter(AccessLevel.NONE)
private Email email;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private String status;
private MemberStatus status;

public static Member signUp(String account, String password, String email, PasswordEncoder passwordEncoder) {
return Member.builder()
.account(account)
.password(Password.of(password, passwordEncoder))
.email(Email.from(email))
.status(DEFAULT_MEMBER_STATUS)
.build();
}

public String getPassword() {
return password.getValue();
}

public String getEmail() {
return email.getValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package team05.integrated_feed_backend.module.member.entity.enums;

public enum MemberStatus {
UNVERIFIED, VERIFIED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package team05.integrated_feed_backend.module.member.entity.vo;

import static team05.integrated_feed_backend.common.code.StatusCode.*;

import java.util.regex.Pattern;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import team05.integrated_feed_backend.exception.custom.BadRequestException;

@Embeddable
@Getter
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public class Email {
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$");

@Column(name = "email", nullable = false, unique = true)
private final String value;

private Email(String value) {
validate(value);
this.value = value;
}

public static Email from(String email) {
return new Email(email);
}

private void validate(String value) {
if (!isValidEmail(value)) {
throw new BadRequestException(INVALID_EMAIL_FORMAT);
}
}

private boolean isValidEmail(String value) {
return EMAIL_PATTERN.matcher(value).matches();
}
}
Loading

0 comments on commit 84f7922

Please sign in to comment.