Repository navigation
[FEAT] 온보딩 및 프로필 태그 선택 개수 정책 변경 #212
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
Changes from all commits
3fa4352
c309b26
d009d2e
d5625ec
f683a90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.sopt.buddys.domain.user.service; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
| import org.sopt.buddys.domain.tag.entity.Tag; | ||
| import org.sopt.buddys.domain.tag.entity.TagType; | ||
| import org.sopt.buddys.domain.user.code.UserErrorCode; | ||
| import org.sopt.buddys.global.exception.BaseException; | ||
|
|
||
| public final class UserTagSelectionValidator { | ||
|
|
||
| private static final int MIN_ACTIVITY_TAG_COUNT = 1; | ||
| private static final int MAX_ACTIVITY_TAG_COUNT = 3; | ||
| private static final int MIN_INTEREST_TAG_COUNT = 1; | ||
| private static final int MAX_INTEREST_TAG_COUNT = 2; | ||
| private static final int MIN_TRAVEL_STYLE_TAG_COUNT = 1; | ||
| private static final int MAX_TRAVEL_STYLE_TAG_COUNT = 2; | ||
|
|
||
| private UserTagSelectionValidator() { | ||
| } | ||
|
|
||
| public static void validate(Collection<Tag> tags) { | ||
| Map<TagType, Long> tagCounts = tags.stream() | ||
| .collect(Collectors.groupingBy(Tag::getTagType, Collectors.counting())); | ||
| if (!isBetween(tagCounts.getOrDefault(TagType.ACTIVITY, 0L), | ||
| MIN_ACTIVITY_TAG_COUNT, MAX_ACTIVITY_TAG_COUNT) | ||
| || !isBetween(tagCounts.getOrDefault(TagType.INTEREST, 0L), | ||
| MIN_INTEREST_TAG_COUNT, MAX_INTEREST_TAG_COUNT) | ||
| || !isBetween(tagCounts.getOrDefault(TagType.TRAVEL_STYLE, 0L), | ||
| MIN_TRAVEL_STYLE_TAG_COUNT, MAX_TRAVEL_STYLE_TAG_COUNT)) { | ||
| throw new BaseException(UserErrorCode.INVALID_TAG_SELECTION_COUNT); | ||
| } | ||
| } | ||
|
|
||
| private static boolean isBetween(long value, int minimum, int maximum) { | ||
| return value >= minimum && value <= maximum; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,9 +12,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.LocalDate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.stream.LongStream; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.stream.Stream; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.Arguments; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.params.provider.MethodSource; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.mockito.InjectMocks; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.mockito.Mock; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.mockito.junit.jupiter.MockitoExtension; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +40,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @ExtendWith(MockitoExtension.class) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class UserProfileEditServiceTest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final long ACTIVITY_TAG_START_ID = 1L; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final long INTEREST_TAG_START_ID = 13L; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final long TRAVEL_STYLE_TAG_START_ID = 27L; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @InjectMocks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private UserProfileEditService userProfileEditService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -208,6 +217,37 @@ void updateProfile_withoutActivityTag_rejectsBeforeMutation() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| verify(userTagRepository, never()).deleteAllByUserId(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @DisplayName("전체 개수와 무관하게 카테고리별 최대 선택 개수를 하나라도 초과하면 프로필을 수정하지 않는다") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @ParameterizedTest(name = "활동 {0}개, 관심사 {1}개, 여행 스타일 {2}개") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @MethodSource("tagSelectionCountsExceedingLimit") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void updateProfile_withTagSelectionCountExceeded_rejectsBeforeMutation( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+220
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
7개 태그 성공 경계를 추가로 검증하세요. 현재 성공 테스트는 활동 1개, 관심사 2개, 여행 스타일 2개인 총 5개 태그만 사용합니다. 활동 3개, 관심사 2개, 여행 스타일 2개인 총 7개 태그로 권장 테스트 추가
Committable suggestion
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int activityCount, int interestCount, int travelStyleCount | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // given | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long userId = 1L; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| User user = createUser(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Tag> tags = Stream.of( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createTags(ACTIVITY_TAG_START_ID, activityCount, TagType.ACTIVITY), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createTags(INTEREST_TAG_START_ID, interestCount, TagType.INTEREST), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createTags(TRAVEL_STYLE_TAG_START_ID, travelStyleCount, TagType.TRAVEL_STYLE)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .flatMap(List::stream) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Long> orderedTagIds = tags.stream().map(Tag::getId).toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UpdateProfileCommand command = createCommandWithTagIds(orderedTagIds); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| given(userRepository.findActiveByIdForUpdate(userId)).willReturn(Optional.of(user)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| given(tagRepository.findAllById(orderedTagIds)).willReturn(tags); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // when & then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertThatThrownBy(() -> userProfileEditService.updateProfile(userId, command)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .isInstanceOf(BaseException.class) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .satisfies(exception -> assertThat(((BaseException) exception).getErrorCode()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .isEqualTo(UserErrorCode.INVALID_TAG_SELECTION_COUNT)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertThat(user.getNickname()).isEqualTo("기존닉네임"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| verify(userRepository, never()).flush(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| verify(userTagRepository, never()).deleteAllByUserId(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @DisplayName("orderedTagIds에 중복 태그가 있으면 프로필을 수정하지 않는다") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void updateProfile_withDuplicateTagIds_rejectsBeforeMutation() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -252,6 +292,13 @@ void isNicknameAvailable_excludesCurrentUser() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertThat(available).isTrue(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static Stream<Arguments> tagSelectionCountsExceedingLimit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Stream.of( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Arguments.of(1, 3, 1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Arguments.of(1, 1, 3) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private User createUser(Long id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return User.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .id(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -266,6 +313,23 @@ private User createUser(Long id) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private UpdateProfileCommand createCommandWithTagIds(List<Long> orderedTagIds) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new UpdateProfileCommand( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "새닉네임", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Gender.MALE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LocalDate.of(2001, 2, 3), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orderedTagIds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private List<Tag> createTags(long startId, int count, TagType tagType) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return LongStream.range(startId, startId + count) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .mapToObj(id -> createTag(id, "태그" + id, tagType)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private Tag createTag(Long id, String name, TagType tagType) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Tag tag = mock(Tag.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| given(tag.getId()).willReturn(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔎 Supported by static analysis
Script executed:
Repository: TEAM-BUDDYS/BUDDYS-SERVER
Length of output: 19643
Script executed:
Repository: TEAM-BUDDYS/BUDDYS-SERVER
Length of output: 27532
최대 허용 조합의 온보딩 성공 경계를 추가하세요.
현재 추가된 테스트는 관심사 또는 여행 스타일이 3개인 실패 경로만 검증합니다.
activity=3,interest=2,travel=2는 유효한 최대 조합입니다. 기존 성공 테스트를 확장하거나 별도 테스트를 추가하여 온보딩 성공과saveAll에 전달되는 태그 7개를 검증하세요. 이 검증은 상한 경계의 회귀를 방지합니다.