diff --git a/src/main/java/daybyquest/group/application/CheckGroupNameService.java b/src/main/java/daybyquest/group/application/CheckGroupNameService.java new file mode 100644 index 0000000..502c6d4 --- /dev/null +++ b/src/main/java/daybyquest/group/application/CheckGroupNameService.java @@ -0,0 +1,20 @@ +package daybyquest.group.application; + +import daybyquest.group.domain.Groups; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CheckGroupNameService { + + private final Groups groups; + + public CheckGroupNameService(final Groups groups) { + this.groups = groups; + } + + @Transactional(readOnly = true) + public void invoke(final String name) { + groups.validateNotExistentByName(name); + } +} diff --git a/src/main/java/daybyquest/group/application/RecommendGroupsService.java b/src/main/java/daybyquest/group/application/RecommendGroupsService.java new file mode 100644 index 0000000..884fb38 --- /dev/null +++ b/src/main/java/daybyquest/group/application/RecommendGroupsService.java @@ -0,0 +1,41 @@ +package daybyquest.group.application; + +import daybyquest.group.dto.response.GroupResponse; +import daybyquest.group.dto.response.MultipleGroupsResponse; +import daybyquest.group.query.GroupDao; +import daybyquest.group.query.GroupData; +import daybyquest.group.query.GroupRecommendDao; +import daybyquest.user.domain.User; +import daybyquest.user.domain.Users; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class RecommendGroupsService { + + private static final int MAX_RECOMMENDATION_COUNT = 5; + + private final Users users; + + private final GroupRecommendDao recommendDao; + + private final GroupDao groupDao; + + public RecommendGroupsService(final Users users, final GroupRecommendDao recommendDao, + final GroupDao groupDao) { + this.users = users; + this.recommendDao = recommendDao; + this.groupDao = groupDao; + } + + @Transactional(readOnly = true) + public MultipleGroupsResponse invoke(final Long loginId) { + final User user = users.getById(loginId); + final List ids = recommendDao.getRecommendIds(MAX_RECOMMENDATION_COUNT, + user.getInterests()); + final List groupData = groupDao.findAllByIdsIn(loginId, ids); + final List responses = groupData.stream().map(GroupResponse::of).toList(); + return new MultipleGroupsResponse(responses); + } +} diff --git a/src/main/java/daybyquest/group/domain/Group.java b/src/main/java/daybyquest/group/domain/Group.java index 13293a8..d2e5d59 100644 --- a/src/main/java/daybyquest/group/domain/Group.java +++ b/src/main/java/daybyquest/group/domain/Group.java @@ -7,7 +7,6 @@ import daybyquest.global.error.exception.InvalidDomainException; import daybyquest.image.domain.Image; -import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -31,7 +30,7 @@ public class Group { private String interest; - @Column(nullable = false, length = MAX_NAME_LENGTH) + @Column(nullable = false, length = MAX_NAME_LENGTH, unique = true) private String name; @Column(length = MAX_DESCRIPTION_LENGTH) @@ -40,7 +39,6 @@ public class Group { private boolean deleted; @Embedded - @AttributeOverride(name = "imageUrl", column = @Column(name = "image_url")) private Image image; public Group(String interest, String name, String description, Image image) { diff --git a/src/main/java/daybyquest/group/domain/GroupRepository.java b/src/main/java/daybyquest/group/domain/GroupRepository.java index f1f39e8..f8b236c 100644 --- a/src/main/java/daybyquest/group/domain/GroupRepository.java +++ b/src/main/java/daybyquest/group/domain/GroupRepository.java @@ -10,4 +10,6 @@ interface GroupRepository extends Repository { Optional findById(final Long id); boolean existsById(final Long id); + + boolean existsByName(final String name); } \ No newline at end of file diff --git a/src/main/java/daybyquest/group/domain/Groups.java b/src/main/java/daybyquest/group/domain/Groups.java index a858483..02b5bf7 100644 --- a/src/main/java/daybyquest/group/domain/Groups.java +++ b/src/main/java/daybyquest/group/domain/Groups.java @@ -1,6 +1,7 @@ package daybyquest.group.domain; import static daybyquest.global.error.ExceptionCode.ALREADY_MEMBER; +import static daybyquest.global.error.ExceptionCode.DUPLICATED_GROUP_NAME; import daybyquest.global.error.exception.InvalidDomainException; import daybyquest.global.error.exception.NotExistGroupException; @@ -31,11 +32,18 @@ public class Groups { public Long save(final Long userId, final Group group) { users.validateModeratorById(userId); interests.validateInterest(group.getInterest()); + validateNotExistentByName(group.getName()); final Group savedGroup = groupRepository.save(group); groupUserRepository.save(GroupUser.createGroupManager(userId, savedGroup)); return savedGroup.getId(); } + public void validateNotExistentByName(final String name) { + if (groupRepository.existsByName(name)) { + throw new InvalidDomainException(DUPLICATED_GROUP_NAME); + } + } + public void addUser(final GroupUser groupUser) { validateExistentById(groupUser.getGroupId()); if (groupUser.isManager()) { @@ -68,7 +76,7 @@ private void validateNotMember(final Long userId, final Long groupId) { throw new InvalidDomainException(ALREADY_MEMBER); } } - + public Group getById(final Long id) { return groupRepository.findById(id).orElseThrow(NotExistGroupException::new); } diff --git a/src/main/java/daybyquest/group/presentation/GroupQueryApi.java b/src/main/java/daybyquest/group/presentation/GroupQueryApi.java index c75ff2a..9a5cc75 100644 --- a/src/main/java/daybyquest/group/presentation/GroupQueryApi.java +++ b/src/main/java/daybyquest/group/presentation/GroupQueryApi.java @@ -3,9 +3,11 @@ import daybyquest.auth.Authorization; import daybyquest.auth.domain.AccessUser; import daybyquest.global.query.NoOffsetIdPage; +import daybyquest.group.application.CheckGroupNameService; import daybyquest.group.application.GetGroupProfileService; import daybyquest.group.application.GetGroupUsersService; import daybyquest.group.application.GetGroupsService; +import daybyquest.group.application.RecommendGroupsService; import daybyquest.group.application.SearchGroupService; import daybyquest.group.dto.response.GroupResponse; import daybyquest.group.dto.response.MultipleGroupsResponse; @@ -20,23 +22,37 @@ @RestController public class GroupQueryApi { + private final CheckGroupNameService checkGroupNameService; + private final GetGroupProfileService getGroupProfileService; private final GetGroupUsersService getGroupUsersService; private final GetGroupsService getGroupsService; + private final RecommendGroupsService recommendGroupsService; + private final SearchGroupService searchGroupService; - public GroupQueryApi(final GetGroupProfileService getGroupProfileService, + public GroupQueryApi(final CheckGroupNameService checkGroupNameService, + final GetGroupProfileService getGroupProfileService, final GetGroupUsersService getGroupUsersService, final GetGroupsService getGroupsService, + final RecommendGroupsService recommendGroupsService, final SearchGroupService searchGroupService) { + this.checkGroupNameService = checkGroupNameService; this.getGroupProfileService = getGroupProfileService; this.getGroupUsersService = getGroupUsersService; this.getGroupsService = getGroupsService; + this.recommendGroupsService = recommendGroupsService; this.searchGroupService = searchGroupService; } + @GetMapping("/group/{groupName}/check") + public ResponseEntity checkGroupName(@PathVariable final String groupName) { + checkGroupNameService.invoke(groupName); + return ResponseEntity.ok().build(); + } + @GetMapping("/group/{groupId}") @Authorization public ResponseEntity getGroupProfile(final AccessUser accessUser, @@ -59,7 +75,14 @@ public ResponseEntity getGroups(final AccessUser accessU final MultipleGroupsResponse response = getGroupsService.invoke(accessUser.getId()); return ResponseEntity.ok(response); } - + + @GetMapping("/group/recommendation") + @Authorization + public ResponseEntity recommendGroups(final AccessUser accessUser) { + final MultipleGroupsResponse response = recommendGroupsService.invoke(accessUser.getId()); + return ResponseEntity.ok(response); + } + @GetMapping("/search/group") @Authorization public ResponseEntity searchGroup(final AccessUser accessUser, diff --git a/src/main/java/daybyquest/group/query/GroupRecommendDao.java b/src/main/java/daybyquest/group/query/GroupRecommendDao.java new file mode 100644 index 0000000..36259c5 --- /dev/null +++ b/src/main/java/daybyquest/group/query/GroupRecommendDao.java @@ -0,0 +1,16 @@ +package daybyquest.group.query; + +import daybyquest.group.domain.Group; +import java.util.Collection; +import java.util.List; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +public interface GroupRecommendDao extends Repository { + + @Query(value = "select g.id from `group` g where g.interest in :interests " + + "and g.id >= FLOOR(1 + RAND() * (select MAX(`group`.id) from `group`)) " + + "ORDER BY g.id limit :topN", + nativeQuery = true) + List getRecommendIds(final int topN, final Collection interests); +} diff --git a/src/main/java/daybyquest/quest/application/RecommendQuestsService.java b/src/main/java/daybyquest/quest/application/RecommendQuestsService.java index c7a4745..f3f6317 100644 --- a/src/main/java/daybyquest/quest/application/RecommendQuestsService.java +++ b/src/main/java/daybyquest/quest/application/RecommendQuestsService.java @@ -32,7 +32,7 @@ public RecommendQuestsService(final Users users, final QuestRecommendDao recomme @Transactional(readOnly = true) public MultipleQuestsResponse invoke(final Long loginId) { final User user = users.getById(loginId); - final List ids = recommendDao.findTopNNormalQuestIdsByInterestIn(MAX_RECOMMENDATION_COUNT, + final List ids = recommendDao.getRecommendIds(MAX_RECOMMENDATION_COUNT, user.getInterests()); final List questData = questDao.findAllByIdIn(loginId, ids); final List responses = questData.stream().map(QuestResponse::of).toList(); diff --git a/src/main/java/daybyquest/quest/query/QuestRecommendDao.java b/src/main/java/daybyquest/quest/query/QuestRecommendDao.java index cb92ef0..bcaaa93 100644 --- a/src/main/java/daybyquest/quest/query/QuestRecommendDao.java +++ b/src/main/java/daybyquest/quest/query/QuestRecommendDao.java @@ -12,5 +12,5 @@ public interface QuestRecommendDao extends Repository { + "and q.id >= FLOOR(1 + RAND() * (select MAX(quest.id) from quest)) " + "ORDER BY q.id limit :topN", nativeQuery = true) - List findTopNNormalQuestIdsByInterestIn(final int topN, final Collection interests); + List getRecommendIds(final int topN, final Collection interests); } diff --git a/src/main/resources/db/migration/V7__modify_group.sql b/src/main/resources/db/migration/V7__modify_group.sql new file mode 100644 index 0000000..b31d77e --- /dev/null +++ b/src/main/resources/db/migration/V7__modify_group.sql @@ -0,0 +1,2 @@ +ALTER table `group` + add constraint `GROUP_UNIQUE_NAME` unique (`name`); \ No newline at end of file diff --git a/src/test/java/daybyquest/group/domain/GroupsTest.java b/src/test/java/daybyquest/group/domain/GroupsTest.java index 63183d5..b187634 100644 --- a/src/test/java/daybyquest/group/domain/GroupsTest.java +++ b/src/test/java/daybyquest/group/domain/GroupsTest.java @@ -9,6 +9,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import daybyquest.global.error.exception.InvalidDomainException; import daybyquest.global.error.exception.NotExistGroupException; import daybyquest.interest.domain.Interests; import daybyquest.user.domain.Users; @@ -56,6 +57,36 @@ public class GroupsTest { }); } + @Test + void 그룹을_저장할_때_중복_이름이_있다면_에외를_던진다() { + // given + final Long userId = 1L; + given(groupRepository.existsByName(GROUP_1.name)).willReturn(true); + + // when & then + assertThatThrownBy(() -> groups.save(userId, GROUP_1.생성())) + .isInstanceOf(InvalidDomainException.class); + } + + @Test + void 그룹_이름_유일성을_검증한다() { + // given & when + groups.validateNotExistentByName(GROUP_1.name); + + // then + then(groupRepository).should().existsByName(GROUP_1.name); + } + + @Test + void 그룹_이름_유일성을_검증_시_이미_있다면_예외를_던진다() { + // given + given(groupRepository.existsByName(GROUP_1.name)).willReturn(true); + + // when + assertThatThrownBy(() -> groups.validateNotExistentByName(GROUP_1.name)) + .isInstanceOf(InvalidDomainException.class); + } + @Test void 그룹을_저장할_땐_사용자를_함께_저장한다() { // given