Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 사용자는 회원가입을 할 수 있다. #6

Merged
merged 5 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.seong.shoutlink.domain.auth;

public interface PasswordEncoder {

String encode(String rawPassword);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.seong.shoutlink.domain.auth;

import org.springframework.stereotype.Service;

@Service
public class SimplePasswordEncoder implements PasswordEncoder {

@Override
public String encode(String rawPassword) {
return new StringBuilder(rawPassword).reverse().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.seong.shoutlink.domain.auth.controller;

import com.seong.shoutlink.domain.auth.controller.request.CreateMemberRequest;
import com.seong.shoutlink.domain.auth.service.AuthService;
import com.seong.shoutlink.domain.auth.service.request.CreateMemberCommand;
import com.seong.shoutlink.domain.auth.service.response.CreateMemberResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RestController;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/members")
public ResponseEntity<CreateMemberResponse> createMember(
@Valid @RequestBody CreateMemberRequest request) {
CreateMemberResponse response = authService.createMember(new CreateMemberCommand(
request.email(),
request.password(),
request.nickname()
));
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.seong.shoutlink.domain.auth.controller.request;

import jakarta.validation.constraints.NotBlank;

public record CreateMemberRequest(
@NotBlank(message = "이메일은 공백일 수 없습니다.")
String email,
@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
String password,
@NotBlank(message = "닉네임은 공백일 수 없습니다.")
String nickname) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.seong.shoutlink.domain.auth.service;

import com.seong.shoutlink.domain.auth.PasswordEncoder;
import com.seong.shoutlink.domain.auth.service.request.CreateMemberCommand;
import com.seong.shoutlink.domain.auth.service.response.CreateMemberResponse;
import com.seong.shoutlink.domain.member.Member;
import com.seong.shoutlink.domain.member.MemberRole;
import com.seong.shoutlink.domain.member.service.MemberRepository;
import com.seong.shoutlink.global.exception.ErrorCode;
import com.seong.shoutlink.global.exception.ShoutLinkException;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthService {

private static final Pattern PASSWORD_PATTEN = Pattern.compile(
"^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,20}$");

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

private void validatePassword(CreateMemberCommand command) {
if(!PASSWORD_PATTEN.matcher(command.password()).matches()) {
throw new ShoutLinkException("비밀번호는 영어 소문자, 특수문자, 숫자 8자 이상, 20자 이하여야 합니다.",
ErrorCode.ILLEGAL_ARGUMENT);
}
}

public CreateMemberResponse createMember(CreateMemberCommand command) {
validatePassword(command);
memberRepository.findByEmail(command.email())
.ifPresent(member -> {
throw new ShoutLinkException("중복된 이메일입니다.", ErrorCode.DUPLICATE_EMAIL);
});
memberRepository.findByNickname(command.nickname())
.ifPresent(member -> {
throw new ShoutLinkException("중복된 닉네임입니다.", ErrorCode.DUPLICATE_NICKNAME);
});
Member member = new Member(
command.email(),
passwordEncoder.encode(command.password()),
command.nickname(),
MemberRole.USER);
return new CreateMemberResponse(memberRepository.save(member));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.auth.service.request;

public record CreateMemberCommand(String email, String password, String nickname) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.auth.service.response;

public record CreateMemberResponse(Long memberId) {

}
72 changes: 72 additions & 0 deletions src/main/java/com/seong/shoutlink/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.seong.shoutlink.domain.member;

import com.seong.shoutlink.global.exception.ErrorCode;
import com.seong.shoutlink.global.exception.ShoutLinkException;
import java.util.Objects;
import java.util.regex.Pattern;
import lombok.Getter;

@Getter
public class Member {

private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]{1,64}@([\\w-]+\\.)+[\\w-]{2,4}$");
private static final Pattern NICKNAME_PATTERN = Pattern.compile("^[a-zA-Z가-힣0-9]{1,20}$");

private Long memberId;
private String email;
private String password;
private String nickname;
private MemberRole memberRole;

public Member(String email, String password, String nickname, MemberRole memberRole) {
this(null, email, password, nickname, memberRole);
}

public Member(Long memberId, String email, String password, String nickname,
MemberRole memberRole) {
validateEmail(email);
validatePassword(password);
validateNickname(nickname);
validateMemberRole(memberRole);
this.memberId = memberId;
this.email = email;
this.password = password;
this.nickname = nickname;
this.memberRole = memberRole;
}

private void validateEmail(String email) {
if(Objects.isNull(email)) {
throw new ShoutLinkException("이메일은 필수입니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
if(!EMAIL_PATTERN.matcher(email).matches()) {
throw new ShoutLinkException("잘못된 이메일 형식입니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
}

private void validatePassword(String password) {
if(Objects.isNull(password)) {
throw new ShoutLinkException("비밀번호는 필수입니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
if(password.isBlank()) {
throw new ShoutLinkException("비밀번호는 공백일 수 없습니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
}

private void validateNickname(String nickname) {
if(Objects.isNull(nickname)) {
throw new ShoutLinkException("닉네임은 필수입니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
if(!NICKNAME_PATTERN.matcher(nickname).matches()) {
throw new ShoutLinkException("닉네임은 영문 대소문자, 한글, 숫자 1자 이상, 20자 이하여야 합니다.",
ErrorCode.ILLEGAL_ARGUMENT);
}
}

private void validateMemberRole(MemberRole memberRole) {
if(Objects.isNull(memberRole)) {
throw new ShoutLinkException("회원 역할은 필수입니다.", ErrorCode.ILLEGAL_ARGUMENT);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.member;

public enum MemberRole {
USER, ADMIN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.seong.shoutlink.domain.member.repository;

import com.seong.shoutlink.domain.member.Member;
import com.seong.shoutlink.domain.member.MemberRole;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class MemberEntity {

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

private String email;
private String password;
private String nickname;

@Enumerated(EnumType.STRING)
private MemberRole memberRole;

private MemberEntity(String email, String password, String nickname, MemberRole memberRole) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.memberRole = memberRole;
}

public static MemberEntity create(final Member member) {
return new MemberEntity(
member.getEmail(),
member.getPassword(),
member.getNickname(),
member.getMemberRole()
);
}

public Member toDomain() {
return new Member(
memberId,
email,
password,
nickname,
memberRole
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.seong.shoutlink.domain.member.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberJpaRepository extends JpaRepository<MemberEntity, Long> {

Optional<MemberEntity> findByEmail(String email);

Optional<MemberEntity> findByNickname(String nickname);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.seong.shoutlink.domain.member.repository;

import com.seong.shoutlink.domain.member.Member;
import com.seong.shoutlink.domain.member.service.MemberRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepository {

private final MemberJpaRepository memberJpaRepository;

@Override
public Optional<Member> findByEmail(String email) {
return memberJpaRepository.findByEmail(email)
.map(MemberEntity::toDomain);
}

@Override
public Optional<Member> findByNickname(String nickname) {
return memberJpaRepository.findByNickname(nickname)
.map(MemberEntity::toDomain);
}

@Override
public Long save(Member member) {
MemberEntity memberEntity = memberJpaRepository.save(MemberEntity.create(member));
return memberEntity.getMemberId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.seong.shoutlink.domain.member.service;

import com.seong.shoutlink.domain.member.Member;
import java.util.Optional;

public interface MemberRepository {

Optional<Member> findByEmail(String email);

Optional<Member> findByNickname(String nickname);

Long save(Member member);
}
12 changes: 12 additions & 0 deletions src/main/java/com/seong/shoutlink/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.seong.shoutlink.global.exception;

import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum ErrorCode {
ILLEGAL_ARGUMENT("SL001"),
DUPLICATE_EMAIL("SL901"),
DUPLICATE_NICKNAME("SL902");

private final String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.seong.shoutlink.global.exception;

import lombok.Getter;

@Getter
public class ShoutLinkException extends RuntimeException {

private final ErrorCode errorCode;

public ShoutLinkException(String message, ErrorCode errorCode) {
super(message);
this.errorCode = errorCode;
}
}
Loading