diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupController.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupController.java index 9c642df..ae09ab0 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupController.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupController.java @@ -59,28 +59,11 @@ public ApiResponse getCha @PathVariable Long challengeGroupId, @Valid PagingRequest pagingRequest ) { - return ApiResponse.success( - new ChallengeGroupRes.ChallengeGroupRankingPagingResponse( - List.of( - new ChallengeGroupRes.ChallengeGroupRankingDto(1, 12, - new UserRes.UserDto( - 1L, "nickname", "https://picsum.photos/200/300", new UserRes.TierInfoDto( - "tier", 100, 50 - - ) - ) - ) - ), - 1, - new ChallengeGroupRes.ChallengeGroupRankingDto( - 1, 12, - new UserRes.UserDto( - 1L, "nickname", "https://picsum.photos/200/300", new UserRes.TierInfoDto( - "tier", 100, 50 - ) - ) - ) - )); + var rankingPage = challengeGroupQueryService.getChallengeGroupRankingsPaging(challengeGroupId, pagingRequest.toPageable()); + var rankingModel = challengeGroupQueryService.getChallengeGroupRanking(challengeGroupId, jwtUser.getId()); + var response = ChallengeGroupRes.ChallengeGroupRankingPagingResponse + .from(rankingPage, rankingModel); + return ApiResponse.success(response); } //숏폼 조회 diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupRes.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupRes.java index 30bcb88..7c376a7 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupRes.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/ChallengeGroupRes.java @@ -6,6 +6,7 @@ import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupModel; import org.haedal.zzansuni.domain.challengegroup.DayType; import org.haedal.zzansuni.domain.challengegroup.challenge.ChallengeModel; +import org.springframework.data.domain.Page; import java.time.LocalDate; import java.util.List; @@ -135,6 +136,19 @@ public record ChallengeGroupRankingPagingResponse( Integer totalPage, ChallengeGroupRankingDto myRanking //null이면 랭킹이 없는 것 ) { + public static ChallengeGroupRankingPagingResponse from( + Page rankingPage, + ChallengeGroupModel.Ranking myRanking + ){ + var data = rankingPage.getContent().stream() + .map(ChallengeGroupRankingDto::from) + .toList(); + return ChallengeGroupRankingPagingResponse.builder() + .data(data) + .totalPage(rankingPage.getTotalPages()) + .myRanking(ChallengeGroupRankingDto.from(myRanking)) + .build(); + } } @@ -146,6 +160,16 @@ public record ChallengeGroupRankingDto( Integer acquiredPoint, UserRes.UserDto user ) { + public static ChallengeGroupRankingDto from( + ChallengeGroupModel.Ranking model + ){ + var user = UserRes.UserDto.from(model.getUser()); + return ChallengeGroupRankingDto.builder() + .ranking(model.getRank()) + .acquiredPoint(model.getAccumulatedPoint()) + .user(user) + .build(); + } } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/challenge/ChallengeController.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/challenge/ChallengeController.java index cbc5c93..7f9126a 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/challenge/ChallengeController.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/controller/challengegroup/challenge/ChallengeController.java @@ -47,6 +47,7 @@ public ApiResponse challengeParticipation( @Operation(summary = "챌린지 인증", description = "챌린지에 인증한다.") @PostMapping("/api/challenges/{userChallengeId}/verification") public ApiResponse challengeVerification( + @AuthenticationPrincipal JwtUser jwtUser, @PathVariable Long userChallengeId, @RequestPart("body") ChallengeReq.ChallengeVerificationRequest request, @RequestPart("image") MultipartFile image @@ -54,7 +55,7 @@ public ApiResponse challengeVerifica ChallengeCommand.Verificate command = request.toCommand(image); String imageUrl = imageUploader.upload(command.getImage()); ChallengeCommand.VerificationCreate afterUpload = command.afterUpload(imageUrl); - var model = userChallengeService.verification(userChallengeId, afterUpload); + var model = userChallengeService.verification(userChallengeId, jwtUser.getId(), afterUpload); var response = ChallengeRes.ChallengeVerificationResponse.from(model); return ApiResponse.success(response, "챌린지 인증에 성공하였습니다."); } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupModel.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupModel.java index 68ff7d0..c745e25 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupModel.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupModel.java @@ -3,6 +3,7 @@ import lombok.Builder; import lombok.Getter; import org.haedal.zzansuni.domain.challengegroup.challenge.ChallengeModel; +import org.haedal.zzansuni.domain.user.UserModel; import java.time.LocalDate; import java.util.List; @@ -116,4 +117,12 @@ public static Detail from(ChallengeGroup challengeGroup, List getChallengeGroupsShortsPaging(Pageable pa Page challengeGroups = challengeGroupReader.getChallengeGroupsShortsPaging(pageable, userId); return challengeGroups.map(ChallengeGroupModel.Info::from); } + + public Page getChallengeGroupRankingsPaging(Long challengeGroupId, Pageable pageable) { + Page challengeGroupUserExps + = challengeGroupReader.getByChallengeGroupId(challengeGroupId, pageable); + + return getRankingPage(pageable, challengeGroupUserExps); + } + + private static PageImpl getRankingPage(Pageable pageable, Page challengeGroupUserExps) { + List rankings = new ArrayList<>(); + for(int i = 0; i < challengeGroupUserExps.getContent().size(); i++) { + Integer rank = challengeGroupUserExps.getNumber() * challengeGroupUserExps.getSize() + 1 + i; + ChallengeGroupUserExp challengeGroupUserExp = challengeGroupUserExps.getContent().get(i); + var rankingModel = ChallengeGroupModel.Ranking.builder() + .user(UserModel.from(challengeGroupUserExp.getUser())) + .accumulatedPoint(challengeGroupUserExp.getTotalExp()) + .rank(rank) + .build(); + rankings.add(rankingModel); + } + return new PageImpl<>(rankings, pageable, challengeGroupUserExps.getTotalElements()); + } + + public ChallengeGroupModel.Ranking getChallengeGroupRanking(Long challengeGroupId, Long id) { + return challengeGroupReader.getRanking(challengeGroupId, id); + } + + } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupReader.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupReader.java index 562a56e..4652b6d 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupReader.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupReader.java @@ -3,6 +3,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.Optional; + public interface ChallengeGroupReader { ChallengeGroup getById(Long challengeGroupId); @@ -11,4 +13,10 @@ public interface ChallengeGroupReader { Page getChallengeGroupsPagingByCategory(Pageable pageable, ChallengeCategory category); Page getChallengeGroupsShortsPaging(Pageable pageable, Long userId); + + Optional findByChallengeGroupIdAndUserId(Long challengeGroupId, Long userId); + + Page getByChallengeGroupId(Long challengeGroupId, Pageable pageable); + + ChallengeGroupModel.Ranking getRanking(Long challengeGroupId, Long userId); } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExp.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExp.java new file mode 100644 index 0000000..e701111 --- /dev/null +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExp.java @@ -0,0 +1,37 @@ +package org.haedal.zzansuni.domain.challengegroup; + +import jakarta.persistence.*; +import lombok.*; +import org.haedal.zzansuni.domain.user.User; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class ChallengeGroupUserExp { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "challenge_group_id") + private ChallengeGroup challengeGroup; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private Integer totalExp; + + public static ChallengeGroupUserExp create(ChallengeGroup challengeGroup, User user) { + return ChallengeGroupUserExp.builder() + .challengeGroup(challengeGroup) + .user(user) + .totalExp(0) + .build(); + } + + public void addExp(Integer exp) { + this.totalExp += exp; + } +} diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExpStore.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExpStore.java new file mode 100644 index 0000000..6310049 --- /dev/null +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/ChallengeGroupUserExpStore.java @@ -0,0 +1,5 @@ +package org.haedal.zzansuni.domain.challengegroup; + +public interface ChallengeGroupUserExpStore { + ChallengeGroupUserExp store(ChallengeGroupUserExp challengeGroupUserExp); +} diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallenge.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallenge.java index e3d38f5..92aad2c 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallenge.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallenge.java @@ -52,9 +52,11 @@ public static UserChallenge create(Challenge challenge, User user) { public void addChallengeVerification(ChallengeCommand.VerificationCreate command) { ChallengeVerification challengeVerification = ChallengeVerification.create(command, this); this.challengeVerifications.add(challengeVerification); + user.addExp(challenge.getOnceExp()); // 만약 챌린지 인증 참여횟수와 필요참여획수가 같으면 챌린지 완료로 변경 if(this.challengeVerifications.size() == this.challenge.getRequiredCount()) { + user.addExp(challenge.getSuccessExp()); complete(); } } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallengeService.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallengeService.java index 8aa5d07..1edf611 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallengeService.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/challengegroup/userchallenge/UserChallengeService.java @@ -4,6 +4,9 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupReader; +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupUserExp; +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupUserExpStore; import org.haedal.zzansuni.domain.challengegroup.challenge.Challenge; import org.haedal.zzansuni.domain.challengegroup.challenge.ChallengeCommand; import org.haedal.zzansuni.domain.challengegroup.challenge.ChallengeModel; @@ -26,7 +29,8 @@ public class UserChallengeService { private final ChallengeReviewReader challengeReviewReader; private final UserReader userReader; private final ChallengeReader challengeReader; - + private final ChallengeGroupReader challengeGroupReader; + private final ChallengeGroupUserExpStore challengeGroupUserExpStore; /** * 챌린지 참여하기 1. 유저와 챌린지 정보를 받아서 UserChallenge 테이블에 데이터 추가 */ @@ -56,11 +60,29 @@ public void participateChallenge(Long userId, Long challengeId) { @Transactional public ChallengeModel.ChallengeVerificationResult verification( Long userChallengeId, + Long userId, ChallengeCommand.VerificationCreate command ) { UserChallenge userChallenge = userChallengeReader.getByIdWithVerificationAndChallenge( userChallengeId); + if(!userChallenge.getUser().getId().equals(userId)) { + throw new IllegalArgumentException("해당 챌린지에 참여한 유저가 아닙니다."); + } + + Integer beforeExp = userChallenge.getUser().getExp(); userChallenge.addChallengeVerification(command); + Integer afterExp = userChallenge.getUser().getExp(); + + // 챌린지 경험치 획득 로직 + ChallengeGroupUserExp challengeGroupUserExp = challengeGroupReader + .findByChallengeGroupIdAndUserId(userChallenge.getChallenge().getChallengeGroup().getId(), + userId) + .orElseGet(() -> { + ChallengeGroupUserExp entity = ChallengeGroupUserExp + .create(userChallenge.getChallenge().getChallengeGroup(), userChallenge.getUser()); + return challengeGroupUserExpStore.store(entity); + }); + challengeGroupUserExp.addExp(afterExp - beforeExp); // 챌린지 RequiredCount 가져오기 위해 챌린지 정보 가져온다 Challenge challenge = userChallenge.getChallenge(); diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/user/User.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/user/User.java index 1db9398..09efff2 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/user/User.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/domain/user/User.java @@ -80,4 +80,8 @@ public static User createManager(UserCommand.Create command){ public void update(UserCommand.Update userUpdate) { this.nickname = userUpdate.getNickname(); } + + public void addExp(Integer exp){ + this.exp += exp; + } } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupReaderImpl.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupReaderImpl.java index 2bb67c8..93c87f6 100644 --- a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupReaderImpl.java +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupReaderImpl.java @@ -6,22 +6,25 @@ import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import org.haedal.zzansuni.domain.challengegroup.ChallengeCategory; -import org.haedal.zzansuni.domain.challengegroup.ChallengeGroup; -import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupReader; -import org.haedal.zzansuni.domain.challengegroup.QChallengeGroup; +import org.haedal.zzansuni.domain.challengegroup.*; +import org.haedal.zzansuni.domain.user.QUser; +import org.haedal.zzansuni.domain.user.User; +import org.haedal.zzansuni.domain.user.UserModel; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; @Component @RequiredArgsConstructor public class ChallengeGroupReaderImpl implements ChallengeGroupReader { private final JPAQueryFactory queryFactory; + private final JdbcTemplate jdbcTemplate; private final ChallengeGroupRepository challengeGroupRepository; @Override public ChallengeGroup getById(Long challengeGroupId) { @@ -58,12 +61,9 @@ public Page getChallengeGroupsPagingByCategory(Pageable pageable @Override public Page getChallengeGroupsShortsPaging(Pageable pageable, Long userId) { - Double seed = Double.valueOf(userId); - - // 해시 값을 기준으로 정렬 - NumberTemplate hashOrder = Expressions - .numberTemplate(Double.class, "SHA1(CONCAT({0}, id))", seed); - + OrderSpecifier orderSpecifier = Expressions + .numberTemplate(Double.class, "RAND({0})", userId) + .asc(); Long count = queryFactory .select(QChallengeGroup.challengeGroup.count()) @@ -72,7 +72,7 @@ public Page getChallengeGroupsShortsPaging(Pageable pageable, Lo List page = queryFactory .selectFrom(QChallengeGroup.challengeGroup) .leftJoin(QChallengeGroup.challengeGroup.challenges).fetchJoin() - .orderBy(Expressions.numberTemplate(Double.class, "RAND({0})", userId).asc()) + .orderBy(orderSpecifier) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -80,4 +80,67 @@ public Page getChallengeGroupsShortsPaging(Pageable pageable, Lo return new PageImpl<>(page, pageable, count == null ? 0 : count); } + @Override + public Optional findByChallengeGroupIdAndUserId(Long challengeGroupId, Long userId) { + ChallengeGroupUserExp result = queryFactory + .selectFrom(QChallengeGroupUserExp.challengeGroupUserExp) + .where(QChallengeGroupUserExp.challengeGroupUserExp.challengeGroup.id.eq(challengeGroupId) + .and(QChallengeGroupUserExp.challengeGroupUserExp.user.id.eq(userId))) + .fetchOne(); + return Optional.ofNullable(result); + } + + @Override + public Page getByChallengeGroupId(Long challengeGroupId, Pageable pageable) { + Long count = queryFactory + .select(QChallengeGroupUserExp.challengeGroupUserExp.count()) + .from(QChallengeGroupUserExp.challengeGroupUserExp) + .where(QChallengeGroupUserExp.challengeGroupUserExp.challengeGroup.id.eq(challengeGroupId)) + .fetchOne(); + + List page = queryFactory + .selectFrom(QChallengeGroupUserExp.challengeGroupUserExp) + .where(QChallengeGroupUserExp.challengeGroupUserExp.challengeGroup.id.eq(challengeGroupId)) + .orderBy(QChallengeGroupUserExp.challengeGroupUserExp.totalExp.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(page, pageable, count == null ? 0 : count); + } + + @Override + public ChallengeGroupModel.Ranking getRanking(Long challengeGroupId, Long userId) { + User user = queryFactory + .selectFrom(QUser.user) + .where(QUser.user.id.eq(userId)) + .fetchOne(); + if(user == null) { + throw new NoSuchElementException(); + } + + String sql = "SELECT ranked.rank1 FROM (" + + " SELECT user_id, RANK() OVER (ORDER BY total_exp DESC) as rank1 " + + " FROM challenge_group_user_exp " + + " WHERE challenge_group_id = ?" + + ") as ranked " + + "WHERE ranked.user_id = ?"; + + Integer rank = jdbcTemplate.queryForObject(sql, Integer.class, challengeGroupId, userId); + + + ChallengeGroupUserExp challengeGroupUserExp = queryFactory + .select(QChallengeGroupUserExp.challengeGroupUserExp) + .from(QChallengeGroupUserExp.challengeGroupUserExp) + .where(QChallengeGroupUserExp.challengeGroupUserExp.challengeGroup.id.eq(challengeGroupId) + .and(QChallengeGroupUserExp.challengeGroupUserExp.user.id.eq(userId))) + .fetchOne(); + + return ChallengeGroupModel.Ranking.builder() + .rank(rank==null ? 0 : rank) + .accumulatedPoint(challengeGroupUserExp != null ? challengeGroupUserExp.getTotalExp() : 0) + .user(UserModel.from(user)) + .build(); + } + } diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpRepository.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpRepository.java new file mode 100644 index 0000000..c7f7723 --- /dev/null +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpRepository.java @@ -0,0 +1,7 @@ +package org.haedal.zzansuni.infrastructure.challengegroup; + +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupUserExp; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChallengeGroupUserExpRepository extends JpaRepository { +} diff --git a/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpStoreImpl.java b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpStoreImpl.java new file mode 100644 index 0000000..4216be9 --- /dev/null +++ b/zzansuni-api-server/app/src/main/java/org/haedal/zzansuni/infrastructure/challengegroup/ChallengeGroupUserExpStoreImpl.java @@ -0,0 +1,17 @@ +package org.haedal.zzansuni.infrastructure.challengegroup; + +import lombok.RequiredArgsConstructor; +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupUserExp; +import org.haedal.zzansuni.domain.challengegroup.ChallengeGroupUserExpStore; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ChallengeGroupUserExpStoreImpl implements ChallengeGroupUserExpStore { + private final ChallengeGroupUserExpRepository challengeGroupUserExpRepository; + + @Override + public ChallengeGroupUserExp store(ChallengeGroupUserExp challengeGroupUserExp) { + return challengeGroupUserExpRepository.save(challengeGroupUserExp); + } +}