Repository navigation
[FBE-15] Memebr Domain Entity 및 닉네임 처리 추가 #5
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
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
e6effac
feat: Member 엔티티 추가
nimuseel 27b6599
feat: nickname 검증 로직 추가
nimuseel 9117364
test: Member 닉네임 관련 테스트 추가
nimuseel 33dac5e
refactor: 성별, 생일 정보 제외
nimuseel f805181
feat: 닉네임 중복 시 사용할 generator 추가
nimuseel e55cc3d
feat: NicknameGenerator 개선 및 테스트 추가
nimuseel 0d8fd23
refactor: 폴더 아키텍처 맞추기
nimuseel a11bae8
feat: email, providerId, provider 컬럼 NotNull 추가
nimuseel 4649f5a
refactor: 닉네임 검증 개선
nimuseel 4cb8a98
refactor: unicode 공백 처리 포함
nimuseel 789ab45
refactor: 줄바꿈 없는 공백 제거
nimuseel 51d5001
refactor: normalize에 중복 strip() 처리 제거
nimuseel e0be3dc
refactor: 이모지 글자 검증 주석 개선
nimuseel 106ec7f
refactor: 테스트 중복 검증 제거
nimuseel 79fdbd7
refactor: NicknameConstant 상속 방지 및 인스턴스 생성 제한, 변수명 변경
nimuseel 557e315
refactor: ThreadLocalRandom 사용으로 객체 반복 생성 방지
nimuseel a1ea94a
refactor: Member entity 객체 생성, 초기화 책임 분리
nimuseel af58c54
refactor: @NotBlank 및 @Email 어노테이션 적용
nimuseel 7a44f99
refactor: Provider 컬럼 @NotNull 적용
nimuseel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
...lication/member-service/src/main/java/framey/member/domain/constant/NicknameConstant.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package framey.member.domain.constant; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.regex.Pattern; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public final class NicknameConstant { | ||
| public static final int MAX_NICKNAME_LENGTH = 10; | ||
| public static final Pattern EDGE_WHITESPACE = Pattern.compile( | ||
| "^[\\p{javaWhitespace}\\p{IsWhite_Space}]+|[\\p{javaWhitespace}\\p{IsWhite_Space}]+$" | ||
| ); | ||
| public static final String BASE_NICKNAME = "framey"; | ||
| public static final int SUFFIX_DIGITS = 4; | ||
| } | ||
7 changes: 7 additions & 0 deletions
7
framey-application/member-service/src/main/java/framey/member/domain/constant/Provider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package framey.member.domain.constant; | ||
|
|
||
| public enum Provider { | ||
| GOOGLE, | ||
| KAKAO, | ||
| APPLE | ||
| } |
17 changes: 17 additions & 0 deletions
17
...lication/member-service/src/main/java/framey/member/domain/exception/MemberErrorCode.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package framey.member.domain.exception; | ||
|
|
||
| import framey.common.response.ErrorCode; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum MemberErrorCode implements ErrorCode { | ||
| NICKNAME_REQUIRED("MB-001", "닉네임을 입력해 주세요.", HttpStatus.BAD_REQUEST), | ||
| MAX_NICKNAME_LENGTH("MB-002", "닉네임은 10자 이내로 입력해 주세요.", HttpStatus.BAD_REQUEST); | ||
|
|
||
| private final String value; | ||
| private final String message; | ||
| private final HttpStatus httpStatus; | ||
| } |
112 changes: 112 additions & 0 deletions
112
framey-application/member-service/src/main/java/framey/member/domain/model/Member.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package framey.member.domain.model; | ||
|
|
||
| import framey.common.exception.BusinessException; | ||
| import framey.common.jpa.BaseEntity; | ||
| import framey.member.domain.constant.Provider; | ||
| import framey.member.domain.exception.MemberErrorCode; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| import static framey.member.domain.constant.NicknameConstant.EDGE_WHITESPACE; | ||
| import static framey.member.domain.constant.NicknameConstant.MAX_NICKNAME_LENGTH; | ||
|
|
||
| @Entity | ||
| @Table( | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uk_member_provider", columnNames = {"provider", "provider_id"}), | ||
| @UniqueConstraint(name = "uk_member_nickname", columnNames = {"nickname"}), | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
| @UniqueConstraint(name = "uk_member_email", columnNames = {"email"}), | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Getter | ||
| public class Member extends BaseEntity { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long memberId; | ||
|
|
||
| @Column(updatable = false, nullable = false) | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
| @NotBlank | ||
| private String email; | ||
|
|
||
| @Column(length = MAX_NICKNAME_LENGTH, nullable = false) | ||
| @NotBlank | ||
| private String nickname; | ||
|
|
||
| @Column(updatable = false, unique = true, nullable = false) | ||
| private UUID entityId; | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
|
|
||
| @Column(length = 2048, nullable = false) | ||
|
nimuseel marked this conversation as resolved.
Resolved
|
||
| @NotBlank | ||
| private String profileImageUrl; | ||
|
|
||
| @Column(updatable = false, nullable = false) | ||
| @NotBlank | ||
| private String providerId; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(updatable = false, nullable = false) | ||
| @NotNull | ||
| private Provider provider; | ||
|
|
||
| private Member( | ||
| String email, | ||
| String nickname, | ||
| String profileImageUrl, | ||
| String providerId, | ||
| Provider provider | ||
| ) { | ||
| this.email = email; | ||
| this.nickname = validateNickname(nickname); | ||
| this.profileImageUrl = profileImageUrl; | ||
| this.providerId = providerId; | ||
| this.provider = provider; | ||
| this.entityId = UUID.randomUUID(); | ||
| } | ||
|
|
||
| public static Member signUp( | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
| String email, | ||
| String nickname, | ||
| String profileImageUrl, | ||
| String providerId, | ||
| Provider provider | ||
| ) { | ||
| return new Member(email, nickname, profileImageUrl, providerId, provider); | ||
| } | ||
|
|
||
| private static String validateNickname(String nickname) { | ||
| if (nickname == null) { | ||
| throw new BusinessException(MemberErrorCode.NICKNAME_REQUIRED); | ||
| } | ||
|
|
||
| String stripedNickname = EDGE_WHITESPACE.matcher(nickname).replaceAll(""); | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
|
|
||
| if (stripedNickname.isEmpty()) { | ||
| throw new BusinessException(MemberErrorCode.NICKNAME_REQUIRED); | ||
| } | ||
|
|
||
|
|
||
| // NOTE: 유니코드 코드 포인트 기준으로 길이 검증 | ||
| if (stripedNickname.codePointCount(0, stripedNickname.length()) > MAX_NICKNAME_LENGTH) { | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
| throw new BusinessException(MemberErrorCode.MAX_NICKNAME_LENGTH); | ||
| } | ||
|
|
||
| return stripedNickname; | ||
| } | ||
| } | ||
47 changes: 47 additions & 0 deletions
47
...pplication/member-service/src/main/java/framey/member/domain/model/NicknameGenerator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package framey.member.domain.model; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.Random; | ||
| import java.util.concurrent.ThreadLocalRandom; | ||
|
|
||
| import static framey.member.domain.constant.NicknameConstant.BASE_NICKNAME; | ||
| import static framey.member.domain.constant.NicknameConstant.EDGE_WHITESPACE; | ||
| import static framey.member.domain.constant.NicknameConstant.MAX_NICKNAME_LENGTH; | ||
| import static framey.member.domain.constant.NicknameConstant.SUFFIX_DIGITS; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class NicknameGenerator { | ||
| public static String normalize(String nickname) { | ||
| if (nickname == null) { | ||
| return BASE_NICKNAME; | ||
| } | ||
|
|
||
| String stripedNickname = EDGE_WHITESPACE.matcher(nickname).replaceAll(""); | ||
|
|
||
| if (stripedNickname.isEmpty()) { | ||
| return BASE_NICKNAME; | ||
| } | ||
|
|
||
| return truncate(stripedNickname, MAX_NICKNAME_LENGTH); | ||
| } | ||
|
|
||
| public static String appendSuffix(String base) { | ||
|
nimuseel marked this conversation as resolved.
Resolved
|
||
| int baseLength = base.codePointCount(0, base.length()); | ||
| int suffixLength = Math.max(SUFFIX_DIGITS, MAX_NICKNAME_LENGTH - baseLength); | ||
|
|
||
| StringBuilder nickname = new StringBuilder(truncate(base, MAX_NICKNAME_LENGTH - suffixLength)); | ||
| for (int i = 0; i < suffixLength; i++) { | ||
| nickname.append(ThreadLocalRandom.current().nextInt(10)); | ||
| } | ||
| return nickname.toString(); | ||
| } | ||
|
|
||
| private static String truncate(String value, int maxLength) { | ||
| if (value.codePointCount(0, value.length()) <= maxLength) { | ||
| return value; | ||
| } | ||
| return value.substring(0, value.offsetByCodePoints(0, maxLength)); | ||
| } | ||
| } | ||
73 changes: 73 additions & 0 deletions
73
framey-application/member-service/src/test/java/framey/member/domain/member/MemberTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package framey.member.domain.member; | ||
|
|
||
| import framey.common.exception.BusinessException; | ||
| import framey.member.domain.constant.Provider; | ||
| import framey.member.domain.model.Member; | ||
| import framey.member.domain.exception.MemberErrorCode; | ||
| import org.assertj.core.api.Assertions; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.NullAndEmptySource; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| class MemberTest { | ||
|
nimuseel marked this conversation as resolved.
Resolved
|
||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {" ", " ", "\u3000"}) | ||
| @NullAndEmptySource | ||
| @DisplayName("닉네임이 비어 있거나 null이면 가입할 수 없다") | ||
| void signUpFailWhenNicknameIsBlank(String nickname) { | ||
| Assertions.assertThatThrownBy(() -> | ||
| Member.signUp("framey@test.com", nickname, "", "test-1", Provider.GOOGLE) | ||
| ) | ||
| .isInstanceOf(BusinessException.class) | ||
| .extracting("errorCode") | ||
| .isEqualTo(MemberErrorCode.NICKNAME_REQUIRED); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {" Framey ", "\u3000Framey\u3000"}) | ||
| @DisplayName("닉네임에 공백(유니코드 공백 포함)이 있으면 제거된다") | ||
| void signUpStripsNicknameWhitespace(String nickname) { | ||
| Member member = Member.signUp("framey@test.com", nickname, "", "test-1", Provider.GOOGLE); | ||
|
|
||
| Assertions.assertThat(member.getEntityId()).isNotNull(); | ||
| Assertions.assertThat(member.getNickname()).isEqualTo(nickname.strip()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("닉네임이 10자 이내면 가입할 수 있다") | ||
| void signUpSuccessWhenNicknameIsWithinMaxLength() { | ||
| String nickname = "0123456789"; | ||
| Member member = Member.signUp("framey@test.com", nickname, "", "test-1", Provider.GOOGLE); | ||
|
|
||
| Assertions.assertThat(nickname.length()).isEqualTo(10); | ||
| Assertions.assertThat(member.getNickname()).isEqualTo(nickname); | ||
| Assertions.assertThat(member.getEntityId()).isNotNull(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("닉네임의 이모지는 1글자로 센다") | ||
| void signUpCountsEmojiAsOneChar() { | ||
| String nickname = "🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂"; | ||
| Member member = Member.signUp("framey@test.com", nickname, "", "test-1", Provider.GOOGLE); | ||
|
|
||
| Assertions.assertThat(nickname.codePointCount(0, nickname.length())).isEqualTo(10); | ||
| Assertions.assertThat(member.getNickname()).isEqualTo(nickname); | ||
| Assertions.assertThat(member.getEntityId()).isNotNull(); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {"01234567890", "🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂"}) | ||
| @DisplayName("닉네임 길이는 10자를 초과할 수 없다") | ||
| void signUpFailWhenNicknameOverMaxLength(String nickname) { | ||
| Assertions.assertThat(nickname.codePointCount(0, nickname.length())).isEqualTo(11); | ||
| Assertions.assertThatThrownBy(() -> | ||
| Member.signUp("framey@test.com", nickname, "", "test-1", Provider.GOOGLE) | ||
| ) | ||
| .isInstanceOf(BusinessException.class) | ||
| .extracting("errorCode") | ||
| .isEqualTo(MemberErrorCode.MAX_NICKNAME_LENGTH); | ||
| } | ||
| } | ||
34 changes: 34 additions & 0 deletions
34
...ation/member-service/src/test/java/framey/member/domain/member/NicknameGeneratorTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package framey.member.domain.member; | ||
|
|
||
| import org.assertj.core.api.Assertions; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.NullAndEmptySource; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| import static framey.member.domain.model.NicknameGenerator.appendSuffix; | ||
| import static framey.member.domain.model.NicknameGenerator.normalize; | ||
|
|
||
| class NicknameGeneratorTest { | ||
|
young0311 marked this conversation as resolved.
Resolved
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {" ", "\u3000", "\u00A0"}) | ||
| @NullAndEmptySource | ||
| @DisplayName("닉네임이 비어있을 경우 기본 닉네임을 사용한다") | ||
| void useDefaultNicknameWhenNicknameIsBlank(String nickname) { | ||
| String generatedNickname = appendSuffix(normalize(nickname)); | ||
|
|
||
| Assertions.assertThat(generatedNickname).isNotBlank(); | ||
| Assertions.assertThat(generatedNickname).matches("^framey\\d{4}$"); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = { " 프레이미 ", " 프레이미", "프레이미 ", "\u3000프레이미\u3000"}) | ||
| @DisplayName("닉네임의 공백을 제거한다.") | ||
| void stripsNicknameWhiteSpaces(String nickname) { | ||
| String generatedNickname = normalize(nickname); | ||
|
|
||
| Assertions.assertThat(generatedNickname).isNotBlank(); | ||
| Assertions.assertThat(generatedNickname).isEqualTo("프레이미"); | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.