Skip to content

Commit

Permalink
feat: 랭킹 API 구현 (#391)
Browse files Browse the repository at this point in the history
* feat: 랭킹 도메인 추가

* feat: 랭킹 갱신

* feat: 랭킹 갱신 푸시 알림

* fix: 갱신 시간 수정

* fix: 갱신 시간 수정

* fix: 랭킹 코드 정리

* fix: 랭킹 id name 지정

* fix: 랭킹 쿼리 수정

* fix: 갱신 시간 수정

* feat: 랭킹 등수 rank 추가

* fix: long casting 이슈

* fix: findAllMissionSymbolStack RankingService 변경

* fix: repository custom, impl 삭제

* test: 랭킹 테스트 코드

* test: 랭킹 테스트 코드

* fix: 안쓰는 로직 제거

* fix: spotlessApply
  • Loading branch information
char-yb authored Jun 15, 2024
1 parent 368d2a3 commit 07ba94d
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "8. [댓글]", description = "댓글 관련 API")
@Tag(name = "9. [댓글]", description = "댓글 관련 API")
@RestController
@RequestMapping("/comments")
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.depromeet.domain.image.domain.ImageFileExtension;
import com.depromeet.domain.member.dao.MemberRepository;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.member.domain.MemberStatus;
import com.depromeet.domain.member.domain.Profile;
import com.depromeet.domain.member.dto.request.NicknameCheckRequest;
import com.depromeet.domain.member.dto.request.NicknameUpdateRequest;
Expand Down Expand Up @@ -208,4 +209,9 @@ private String escapeSpecialCharacters(String nickname) {
// 여기서 특수문자를 '_'로 대체할 수 있도록 정규표현식을 활용하여 구현
return nickname == null ? "" : nickname.replaceAll("[^0-9a-zA-Z가-힣 ]", "_");
}

@Transactional(readOnly = true)
public List<Member> findAllNormalMember() {
return memberRepository.findAllByStatusIs(MemberStatus.NORMAL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ public interface MemberRepository extends JpaRepository<Member, Long>, MemberRep
"SELECT m FROM Member m WHERE m.profile.nickname LIKE CONCAT('%', :searchNickname, '%') escape '_' AND m.profile.nickname != :myNickname")
List<Member> nicknameSearch(
@Param("searchNickname") String searchNickname, @Param("myNickname") String myNickname);

List<Member> findAllByStatusIs(MemberStatus status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface MissionRepositoryCustom {

List<Mission> findAllFinishedMission(Long memberId);

List<Mission> findAllMissionWithRecords();

List<Mission> findMissionsWithRecordsByDate(LocalDate date, Long memberId);

List<Mission> findMissionsNonCompleteAndInProgress(LocalDateTime today);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ public List<Mission> findMissionsWithRecords(Long memberId) {
return query.fetch();
}

@Override
public List<Mission> findAllMissionWithRecords() {
JPAQuery<Mission> query =
jpaQueryFactory
.selectFrom(mission)
.leftJoin(mission.missionRecords, missionRecord)
.fetchJoin();
return query.fetch();
}

@Override
public List<Mission> findInProgressMissionsWithRecords(Long memberId) {
JPAQuery<Mission> query =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.depromeet.domain.comment.domain.Comment;
import com.depromeet.domain.common.model.BaseTimeEntity;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.domain.Mission;
import com.depromeet.domain.reaction.domain.Reaction;
import com.depromeet.global.error.exception.CustomException;
Expand Down Expand Up @@ -99,4 +100,8 @@ public void updateUploadStatusComplete(String remark, String imageUrl) {
public void updateMissionRecord(String remark) {
this.remark = remark;
}

public Member getMember() {
return mission.getMember();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.depromeet.domain.ranking.api;

import com.depromeet.domain.ranking.application.RankingService;
import com.depromeet.domain.ranking.dto.response.RankingResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "10. [랭킹]", description = "랭킹 관련 API")
@RestController
@RequestMapping("/ranking")
@RequiredArgsConstructor
public class RankingController {

private final RankingService rankingService;

@Operation(summary = "랭킹 조회", description = "랭킹을 조회합니다.")
@GetMapping
public List<RankingResponse> rankingFindAll() {
return rankingService.findAllRanking();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.depromeet.domain.ranking.application;

import com.depromeet.domain.mission.application.MissionService;
import com.depromeet.domain.mission.dao.MissionRepository;
import com.depromeet.domain.mission.domain.Mission;
import com.depromeet.domain.missionRecord.domain.MissionRecord;
import com.depromeet.domain.ranking.dao.RankingRepository;
import com.depromeet.domain.ranking.domain.Ranking;
import com.depromeet.domain.ranking.dto.RankingDto;
import com.depromeet.domain.ranking.dto.response.RankingResponse;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class RankingService {

private final RankingRepository rankingRepository;
private final MissionRepository missionRepository;
private final MissionService missionService;

@Transactional(readOnly = true)
public List<RankingResponse> findAllRanking() {
List<Ranking> rankings = rankingRepository.findTop50ByOrderBySymbolStackDesc();
return IntStream.range(0, rankings.size())
.mapToObj(i -> RankingResponse.of(rankings.get(i), (long) i + 1))
.collect(Collectors.toList());
}

public void updateSymbolStack(List<RankingDto> rankingDtos) {
for (RankingDto rankingDto : rankingDtos) {
Ranking ranking = Ranking.createRanking(rankingDto.symbolStack(), rankingDto.member());
rankingRepository.updateSymbolStackAndMemberId(
ranking.getSymbolStack(), ranking.getMember().getId());
}
}

@Transactional(readOnly = true)
public List<RankingDto> findAllMissionSymbolStack() {
List<Mission> missions = missionRepository.findAllMissionWithRecords();
List<MissionRecord> completedMissionRecords =
missionService.findCompletedMissionRecords(missions);

return completedMissionRecords.stream()
.collect(Collectors.groupingBy(MissionRecord::getMember))
.entrySet()
.stream()
.map(
entry ->
RankingDto.of(
entry.getKey(),
missionService.symbolStackCalculate(entry.getValue())))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.depromeet.domain.ranking.dao;

import com.depromeet.domain.ranking.domain.Ranking;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface RankingRepository extends JpaRepository<Ranking, Long> {

// 최대 50개의 랭킹을 조회한다.
List<Ranking> findTop50ByOrderBySymbolStackDesc();

@Modifying
@Query(
value =
"INSERT INTO Ranking (member_id, symbol_stack, created_at) "
+ "VALUES (:memberId, :symbolStack, NOW()) "
+ "ON DUPLICATE KEY UPDATE member_id = :memberId, symbol_stack = :symbolStack, updated_at = NOW()",
nativeQuery = true)
void updateSymbolStackAndMemberId(
@Param("symbolStack") long symbolStack, @Param("memberId") Long memberId);
}
44 changes: 44 additions & 0 deletions src/main/java/com/depromeet/domain/ranking/domain/Ranking.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.depromeet.domain.ranking.domain;

import com.depromeet.domain.common.model.BaseTimeEntity;
import com.depromeet.domain.member.domain.Member;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Ranking extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ranking_id")
private Long id;

@Comment("번개 스택")
private Long symbolStack;

@OneToOne
@JoinColumn(name = "member_id")
private Member member;

@Builder(access = AccessLevel.PRIVATE)
private Ranking(Long symbolStack, Member member) {
this.symbolStack = symbolStack;
this.member = member;
}

public static Ranking createRanking(Long symbolStack, Member member) {
return Ranking.builder().symbolStack(symbolStack).member(member).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.depromeet.domain.ranking.dto;

import com.depromeet.domain.member.domain.Member;

public record RankingDto(Member member, long symbolStack) {
public static RankingDto of(Member member, long symbolStack) {
return new RankingDto(member, symbolStack);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.depromeet.domain.ranking.dto.response;

import com.depromeet.domain.ranking.domain.Ranking;
import io.swagger.v3.oas.annotations.media.Schema;

public record RankingResponse(
@Schema(description = "사용자 ID", defaultValue = "1") Long memberId,
@Schema(description = "랭킹", defaultValue = "1") long rank,
@Schema(description = "번개 수", defaultValue = "1") long symbolStack,
@Schema(description = "사용자 nickname", defaultValue = "default nickname") String nickname,
@Schema(description = "프로필 이미지", defaultValue = "profile image url")
String profileImageUrl) {
public static RankingResponse of(Ranking ranking, long rank) {
return new RankingResponse(
ranking.getMember().getId(),
rank,
ranking.getSymbolStack(),
ranking.getMember().getProfile().getNickname(),
ranking.getMember().getProfile().getProfileImageUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ public class PushNotificationConstants {
public static final String PUSH_MISSION_REMIND_CONTENT = "지금부터 미션 인증을 할 수 있어요 🕑";
public static final String PUSH_MISSION_START_REMIND_TITLE = "미션을 시작할 시간이에요!";
public static final String PUSH_MISSION_START_REMIND_CONTENT = "10분만 투자해서 %s 미션을 완료해봐요 🥳";
public static final String PUSH_RANKING_CONTENT = "오늘의 랭킹이 업데이트 되었어요! 🎉";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import static com.depromeet.global.common.constants.PushNotificationConstants.*;

import com.depromeet.domain.member.application.MemberService;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.application.MissionService;
import com.depromeet.domain.mission.dto.response.MissionRemindPushResponse;
import com.depromeet.domain.notification.application.FcmService;
import com.depromeet.domain.ranking.application.RankingService;
import com.depromeet.domain.ranking.dto.RankingDto;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
Expand All @@ -18,6 +22,8 @@
@Slf4j
public class MissionBatchScheduler {
private final MissionService missionService;
private final MemberService memberService;
private final RankingService rankingService;
private final FcmService fcmService;

// 자정에 schedule 실행
Expand All @@ -27,6 +33,22 @@ public void updateFinishedDurationStatus() {
missionService.updateFinishedDurationStatus();
}

@Scheduled(cron = "0 0 21 * * *", zone = "Asia/Seoul")
public void updateRankingSymbolStack() {
log.info("Ranking Symbol Stack Update batch execute");
List<RankingDto> allMissionSymbolStack = rankingService.findAllMissionSymbolStack();
rankingService.updateSymbolStack(allMissionSymbolStack);

log.info("send All Member Ranking Notification");
List<Member> allNormalMember = memberService.findAllNormalMember();

List<String> tokenList =
allNormalMember.stream().map(member -> member.getFcmInfo().getFcmToken()).toList();
if (!tokenList.isEmpty()) {
fcmService.sendGroupMessageAsync(tokenList, PUSH_SERVICE_TITLE, PUSH_RANKING_CONTENT);
}
}

// 매 10분마다 schedule 실행
@Scheduled(cron = "0 */10 * * * *", zone = "Asia/Seoul")
public void missionRemindPushNotification() {
Expand Down
Loading

0 comments on commit 07ba94d

Please sign in to comment.