From 61f5daa2fcf20237ee078e9b6025ef0222ab8afe Mon Sep 17 00:00:00 2001 From: DongGeon0908 Date: Wed, 21 Aug 2024 10:39:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20group=20user=20score=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/DDL.sql | 15 ++++++++ .../domain/group/application/GroupFacade.kt | 34 ++++++++++++++++--- .../application/GroupUserScoreService.kt | 18 ++++++++++ .../group/application/GroupUserService.kt | 8 ++++- .../domain/group/domain/GroupUserScore.kt | 24 +++++++++++++ .../infrastructure/GroupUserRepository.kt | 2 ++ .../GroupUserScoreRepository.kt | 12 +++++++ .../model/response/GetGroupRanksResponse.kt | 13 +++++++ .../group/resource/GroupUserScoreResource.kt | 26 ++++++++++++++ .../com/hero/alignlab/exception/ErrorCode.kt | 1 + .../alignlab/ws/model/ConcurrentMessage.kt | 7 +++- 11 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserScoreService.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/group/domain/GroupUserScore.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserScoreRepository.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/group/model/response/GetGroupRanksResponse.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/group/resource/GroupUserScoreResource.kt diff --git a/sql/DDL.sql b/sql/DDL.sql index 8660961..319a6fb 100644 --- a/sql/DDL.sql +++ b/sql/DDL.sql @@ -74,6 +74,21 @@ CREATE TABLE `group_user` CREATE UNIQUE INDEX uidx__group_id__uid ON group_user (group_id, uid); CREATE INDEX idx__uid ON group_user (uid); +-- 그룹 유저 스코어 +CREATE TABLE `group_user_score` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'group user score id', + `group_id` bigint NOT NULL COMMENT 'group id', + `group_user_id` bigint NOT NULL COMMENT 'group user id', + `uid` bigint NOT NULL COMMENT 'uid', + `score` int DEFAULT NULL COMMENT '스코어', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + `modified_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=200000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='그룹 유저 스코어'; +CREATE UNIQUE INDEX uidx__group_user_id ON group_user_score (group_user_id); +CREATE INDEX idx__group_id__group_user_id ON group_user_score (group_id, group_user_id); + -- 포즈 스냅샵 CREATE TABLE `pose_snapshot` ( diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupFacade.kt b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupFacade.kt index f7af5f5..da26a46 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupFacade.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupFacade.kt @@ -10,10 +10,8 @@ import com.hero.alignlab.domain.group.domain.Group import com.hero.alignlab.domain.group.model.CreateGroupContext import com.hero.alignlab.domain.group.model.request.CheckGroupRegisterRequest import com.hero.alignlab.domain.group.model.request.CreateGroupRequest -import com.hero.alignlab.domain.group.model.response.CreateGroupResponse -import com.hero.alignlab.domain.group.model.response.GetGroupResponse -import com.hero.alignlab.domain.group.model.response.JoinGroupResponse -import com.hero.alignlab.domain.group.model.response.SearchGroupResponse +import com.hero.alignlab.domain.group.model.response.* +import com.hero.alignlab.domain.user.application.UserInfoService import com.hero.alignlab.event.model.CreateGroupEvent import com.hero.alignlab.exception.ErrorCode import com.hero.alignlab.exception.InvalidRequestException @@ -21,11 +19,14 @@ import com.hero.alignlab.exception.NotFoundException import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Page import org.springframework.stereotype.Service +import java.util.concurrent.atomic.AtomicInteger @Service class GroupFacade( private val groupService: GroupService, private val groupUserService: GroupUserService, + private val groupUserScoreService: GroupUserScoreService, + private val userInfoService: UserInfoService, private val txTemplates: TransactionTemplates, private val publisher: ApplicationEventPublisher, ) { @@ -187,4 +188,29 @@ class GroupFacade( throw InvalidRequestException(ErrorCode.DUPLICATE_GROUP_NAME_ERROR) } } + + suspend fun getGroupRank(user: AuthUser, groupId: Long): GetGroupRanksResponse { + val groupUser = groupUserService.findByGroupIdAndUid(groupId, user.uid) + ?: throw InvalidRequestException(ErrorCode.NOT_CONTAINS_GROUP_USER_ERROR) + + val groupUserScores = groupUserScoreService.findAllByGroupId(groupId) + .filterNot { groupUserScore -> groupUserScore.score == null } + .sortedBy { groupUserScore -> groupUserScore.score } + + val userbyId = userInfoService.findAllByIds(groupUserScores.map { it.uid }).associateBy { it.id } + + val rank = AtomicInteger(1) + + return GetGroupRanksResponse( + groupId = groupUser.groupId, + ranks = groupUserScores.mapNotNull { groupUserScore -> + GetGroupRankResponse( + groupUserId = groupUserScore.groupUserId, + name = userbyId[groupUserScore.uid]?.nickname ?: return@mapNotNull null, + rank = rank.getAndIncrement(), + score = groupUserScore.score ?: return@mapNotNull null, + ) + } + ) + } } diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserScoreService.kt b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserScoreService.kt new file mode 100644 index 0000000..137c783 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserScoreService.kt @@ -0,0 +1,18 @@ +package com.hero.alignlab.domain.group.application + +import com.hero.alignlab.domain.group.domain.GroupUserScore +import com.hero.alignlab.domain.group.infrastructure.GroupUserScoreRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Service + +@Service +class GroupUserScoreService( + private val groupUserScoreRepository: GroupUserScoreRepository, +) { + suspend fun findAllByGroupId(groupId: Long): List { + return withContext(Dispatchers.IO) { + groupUserScoreRepository.findAllByGroupId(groupId) + } + } +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserService.kt b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserService.kt index b94c275..bfd1c8f 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserService.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/group/application/GroupUserService.kt @@ -64,6 +64,12 @@ class GroupUserService( } } + suspend fun findByGroupIdAndUid(groupId: Long, uid: Long): GroupUser? { + return withContext(Dispatchers.IO) { + groupUserRepository.findByGroupIdAndUid(groupId, uid) + } + } + suspend fun getGroupUsers(user: AuthUser, groupId: Long, pageable: Pageable): Page { val exists = existsByGroupIdAndUid(groupId, user.uid) @@ -75,7 +81,7 @@ class GroupUserService( .map { groupUser -> GroupUserResponse(groupUser.id, groupUser.uid) } } - private suspend fun findAllByGroupId(groupId: Long, pageable: Pageable): Page { + suspend fun findAllByGroupId(groupId: Long, pageable: Pageable): Page { return withContext(Dispatchers.IO) { groupUserRepository.findAllByGroupId(groupId, pageable) } diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/domain/GroupUserScore.kt b/src/main/kotlin/com/hero/alignlab/domain/group/domain/GroupUserScore.kt new file mode 100644 index 0000000..074d82d --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/group/domain/GroupUserScore.kt @@ -0,0 +1,24 @@ +package com.hero.alignlab.domain.group.domain + +import com.hero.alignlab.domain.common.domain.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(name = "`group_user_score`") +class GroupUserScore( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = -1, + + @Column(name = "group_id") + val groupId: Long, + + @Column(name = "group_user_id") + val groupUserId: Long, + + @Column(name = "uid") + val uid: Long, + + @Column(name = "score") + var score: Int?, +) : BaseEntity() diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserRepository.kt b/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserRepository.kt index dbcf100..3730e7b 100644 --- a/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserRepository.kt +++ b/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserRepository.kt @@ -21,6 +21,8 @@ interface GroupUserRepository : JpaRepository { fun existsByGroupIdAndUid(groupId: Long, uid: Long): Boolean + fun findByGroupIdAndUid(groupId: Long, uid: Long): GroupUser? + fun findAllByGroupId(groupId: Long, pageable: Pageable): Page fun existsByUid(uid: Long): Boolean diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserScoreRepository.kt b/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserScoreRepository.kt new file mode 100644 index 0000000..c630f38 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/group/infrastructure/GroupUserScoreRepository.kt @@ -0,0 +1,12 @@ +package com.hero.alignlab.domain.group.infrastructure + +import com.hero.alignlab.domain.group.domain.GroupUserScore +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Transactional(readOnly = true) +@Repository +interface GroupUserScoreRepository : JpaRepository { + fun findAllByGroupId(groupId: Long): List +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/model/response/GetGroupRanksResponse.kt b/src/main/kotlin/com/hero/alignlab/domain/group/model/response/GetGroupRanksResponse.kt new file mode 100644 index 0000000..702521a --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/group/model/response/GetGroupRanksResponse.kt @@ -0,0 +1,13 @@ +package com.hero.alignlab.domain.group.model.response + +data class GetGroupRanksResponse( + val groupId: Long, + val ranks: List +) + +data class GetGroupRankResponse( + val groupUserId: Long, + val name: String, + val rank: Int, + val score: Int, +) diff --git a/src/main/kotlin/com/hero/alignlab/domain/group/resource/GroupUserScoreResource.kt b/src/main/kotlin/com/hero/alignlab/domain/group/resource/GroupUserScoreResource.kt new file mode 100644 index 0000000..e65797b --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/group/resource/GroupUserScoreResource.kt @@ -0,0 +1,26 @@ +package com.hero.alignlab.domain.group.resource + +import com.hero.alignlab.common.extension.wrapOk +import com.hero.alignlab.domain.auth.model.AuthUser +import com.hero.alignlab.domain.group.application.GroupFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Group User Score API") +@RestController +@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) +class GroupUserScoreResource( + private val groupFacade: GroupFacade, +) { + @Operation(summary = "바른 자세 랭킹") + @GetMapping("/api/v1/group-scores") + suspend fun getScores( + user: AuthUser, + @RequestParam groupId: Long, + ) = groupFacade.getGroupRank(user, groupId).wrapOk() +} diff --git a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt index 78109e2..a700b67 100644 --- a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt +++ b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt @@ -41,6 +41,7 @@ enum class ErrorCode(val status: HttpStatus, val description: String) { ALREADY_JOIN_GROUP_ERROR(HttpStatus.BAD_REQUEST, "이미 그룹에 들어가 있습니다."), NOT_FOUND_JOIN_CODE_ERROR(HttpStatus.BAD_REQUEST, "비밀 그룹의 경우, 비밀번호가 필수 입니다."), INVALID_JOIN_CODE_ERROR(HttpStatus.BAD_REQUEST, "비밀번호의 조건이 부합하지 않습니다."), + NOT_CONTAINS_GROUP_USER_ERROR(HttpStatus.BAD_REQUEST, "그룹원이 아닙니다."), /** Group User Error Code */ DUPLICATE_GROUP_JOIN_ERROR(HttpStatus.BAD_REQUEST, "한개의 그룹만 참여 가능합니다."), diff --git a/src/main/kotlin/com/hero/alignlab/ws/model/ConcurrentMessage.kt b/src/main/kotlin/com/hero/alignlab/ws/model/ConcurrentMessage.kt index ef6a6ae..4dc7458 100644 --- a/src/main/kotlin/com/hero/alignlab/ws/model/ConcurrentMessage.kt +++ b/src/main/kotlin/com/hero/alignlab/ws/model/ConcurrentMessage.kt @@ -13,6 +13,8 @@ data class ConcurrentMessage( val groupUserId: Long, val uid: Long, val nickname: String, + val rank: Int, + val score: Int, ) companion object { @@ -29,7 +31,10 @@ data class ConcurrentMessage( ConcurrentUser( groupUserId = groupUSer.id, uid = uid, - nickname = info.nickname + nickname = info.nickname, + // 더미 데이터 + rank = 1, + score = 1 ) } )