Skip to content
Open
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,17 @@
package framey.member.application.dto;

import framey.member.domain.model.Member;

public record CreateMember(
String nickname,
String email,
String profileImageUrl
) {
public static CreateMember from(final Member member) {
return new CreateMember(
member.getNickname(),
member.getEmail(),
member.getProfileImageUrl()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package framey.member.application.service;

import framey.common.exception.BusinessException;
import framey.member.application.dto.CreateMember;
import framey.member.config.MemberProperties;
import framey.member.domain.constant.NicknameConstant;
import framey.member.domain.constant.Provider;
import framey.member.domain.exception.MemberErrorCode;
import framey.member.domain.model.Member;
import framey.member.domain.model.NicknameGenerator;
import framey.member.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
private final MemberProperties memberProperties;

@Transactional
public Member createMember(CreateMember createMember) {
// TODO: 추후 AuthService와 같은 createMember를 처리하는 쪽에서 Transaction 재시도 로직 추가

Member member = Member.signUp(
createMember.email(),
resolveNickname(createMember.nickname()),
resolveProfileImageUrl(createMember.profileImageUrl()),
"123", // TODO: 임시 providerId
Provider.GOOGLE // TODO: 임시 provider
Comment thread
nimuseel marked this conversation as resolved.
Resolved
);

return memberRepository.save(member);
}

private String resolveNickname(String nickname) {
String baseNickname = NicknameGenerator.normalize(nickname);

if (!baseNickname.equals(NicknameConstant.BASE_NICKNAME) && !memberRepository.existsByNickname(baseNickname)) {
return baseNickname;
}

for (int i = 0; i < NicknameConstant.MAX_RETRY_COUNT; i++) {
String generatedNickname = NicknameGenerator.appendSuffix(baseNickname);

if (!memberRepository.existsByNickname(generatedNickname)) {
return generatedNickname;
}
}

throw new BusinessException(MemberErrorCode.NICKNAME_GENERATION_FAILED);
}
Comment thread
nimuseel marked this conversation as resolved.
Resolved

private String resolveProfileImageUrl(String profileImageUrl) {
if (profileImageUrl == null || profileImageUrl.isBlank()) {
return memberProperties.defaultProfileImageUrl();
}

return profileImageUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package framey.member.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "member")
public record MemberProperties(String defaultProfileImageUrl) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public final class NicknameConstant {
);
public static final String BASE_NICKNAME = "framey";
public static final int SUFFIX_DIGITS = 4;
public static final int MAX_RETRY_COUNT = 5;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
@RequiredArgsConstructor
public enum MemberErrorCode implements ErrorCode {
NICKNAME_REQUIRED("MB-001", "닉네임을 입력해 주세요.", HttpStatus.BAD_REQUEST),
MAX_NICKNAME_LENGTH("MB-002", "닉네임은 10자 이내로 입력해 주세요.", HttpStatus.BAD_REQUEST);
MAX_NICKNAME_LENGTH("MB-002", "닉네임은 10자 이내로 입력해 주세요.", HttpStatus.BAD_REQUEST),
NICKNAME_GENERATION_FAILED("MB-003", "닉네임 생성에 실패했습니다. 다시 시도해 주세요.", HttpStatus.INTERNAL_SERVER_ERROR);

private final String value;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

import static framey.member.domain.constant.NicknameConstant.BASE_NICKNAME;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package framey.member.domain.repository;

import framey.member.domain.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ management:
health:
probes:
enabled: true

member:
default-profile-image-url: https://dev-swagger.framey.kr/assets/members/default-profile-image.png
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ server:
springdoc:
api-docs:
enabled: true
member:
default-profile-image-url: http://localhost:8081/assets/members/default-profile-image.png
Comment thread
nimuseel marked this conversation as resolved.
Resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package framey.member.application.service;

import framey.common.exception.BusinessException;
import framey.member.application.dto.CreateMember;
import framey.member.config.MemberProperties;
import framey.member.domain.constant.NicknameConstant;
import framey.member.domain.exception.MemberErrorCode;
import framey.member.domain.model.Member;
import framey.member.domain.repository.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;

@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
private static final String DEFAULT_PROFILE_IMAGE_URL = "https://test.framey/default-profile-image.png";

@Mock
private MemberRepository memberRepository;

private MemberService memberService;

@BeforeEach
void setUp() {
memberService = new MemberService(memberRepository, new MemberProperties(DEFAULT_PROFILE_IMAGE_URL));
}

@Test
@DisplayName("소셜 닉네임이 사용 중이 아니면 해당 닉네임을 그대로 사용한다")
void useSocialNicknameWhenNotDuplicated() {
given(memberRepository.existsByNickname("프레이미")).willReturn(false);
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

CreateMember member = new CreateMember("프레이미", "framey@exam.com", "");

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getNickname()).isEqualTo("프레이미");
}

@Test
@DisplayName("소셜 닉네임이 사용 중이라면, 기존 닉네임 뒤에 10자리를 맞춰 임의 숫자를 넣어 사용한다")
void useAppendSuffixNicknameWhenDuplicated() {
given(memberRepository.existsByNickname("프레이미")).willReturn(true);
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

CreateMember member = new CreateMember("프레이미", "framey@exam.com", "");

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getNickname()).matches("^프레이미\\d{6}");
}

@Test
@DisplayName("소셜 닉네임이 기본 닉네임과 같으면 뒤에 임의 숫자를 붙인다")
void appendSuffixWhenNicknameEqualsBaseNickname() {
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

CreateMember member = new CreateMember("framey", "framey@exam.com", "");

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getNickname()).matches("^framey\\d{4}");
then(memberRepository).should(never()).existsByNickname("framey");
}

@ParameterizedTest
@ValueSource(strings = {" ", "\u3000", "\u00A0"})
@NullAndEmptySource
@DisplayName("소셜 닉네임이 없으면 기본 닉네임에 임의 숫자를 붙인다")
void useBaseNicknameWithSuffixWhenNicknameIsBlank(String nickname) {
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));
CreateMember member = new CreateMember(nickname, "framey@exam.com", "");

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getNickname()).matches("^framey\\d{4}");
then(memberRepository).should(never()).existsByNickname("framey");
}

@Test
@DisplayName("임의 숫자를 붙인 닉네임도 모두 사용 중이면 닉네임 생성에 실패한다")
void failWhenAllGeneratedNicknamesAreDuplicated() {
given(memberRepository.existsByNickname(anyString())).willReturn(true);

CreateMember member = new CreateMember("프레이미", "framey@exam.com", "");

Assertions.assertThatThrownBy(() -> memberService.createMember(member))
.isInstanceOf(BusinessException.class)
.extracting("errorCode")
.isEqualTo(MemberErrorCode.NICKNAME_GENERATION_FAILED);

then(memberRepository).should(never()).save(any());
then(memberRepository).should(times(1 + NicknameConstant.MAX_RETRY_COUNT)).existsByNickname(anyString());
}

@Test
@DisplayName("임의 숫자를 붙인 닉네임도 사용 중이라면 새 숫자를 붙여 다시 시도한다")
void retryWithNewSuffixWhenGeneratedNicknameIsDuplicated() {
given(memberRepository.existsByNickname(anyString())).willReturn(true, true, false);
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

CreateMember member = new CreateMember("프레이미", "framey@exam.com", "");

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getNickname()).matches("^프레이미\\d{6}");
then(memberRepository).should(times(3)).existsByNickname(anyString());
}

@ParameterizedTest
@ValueSource(strings = " ")
@NullAndEmptySource
@DisplayName("프로필 이미지가 없으면 기본 프로필 이미지를 사용한다")
void useDefaultProfileImageWhenProfileImageIsBlank(String profileImageUrl) {
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

CreateMember member = new CreateMember("프레이미", "framey@exam.com", profileImageUrl);

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getProfileImageUrl()).isEqualTo(DEFAULT_PROFILE_IMAGE_URL);
}

@Test
@DisplayName("프로필 이미지가 있으면 그대로 사용한다")
void useSocialProfileImageWhenPresent() {
given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0));

String profileImageUrl = "https://test.framey/profile.png";

CreateMember member = new CreateMember("프레이미", "framey@exam.com", profileImageUrl);

Member newMember = memberService.createMember(member);

Assertions.assertThat(newMember.getProfileImageUrl()).isEqualTo(profileImageUrl);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package framey.member.domain.member;
package framey.member.domain.model.member;

import framey.common.exception.BusinessException;
import framey.member.domain.constant.Provider;
Expand Down
Loading
Loading