-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: 랭킹 API 구현 #391
feat: 랭킹 API 구현 #391
Changes from all commits
00b0bf6
3856c9b
750ff2a
5ede540
5ea0b46
a2ea219
be0c94a
d6193e0
e780447
b27e379
3256849
78a17c9
e1e62cf
9b6efd3
29aa6ee
53b630a
d444624
fa52041
f8c7f81
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,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); | ||
} |
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, | ||
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. 멤버 ID에서는 Long을 사용하고, 랭크 및 기타 필드에서는 long을 사용하신 이유가 있나요? 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. 현재 memberId는 일관성 있게 Long으로 사용하였고, 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. 음 그래도 박싱 / 언박싱 비용 생각해서 일관성 있게 전부 참조타입으로 맞춰주는게�좋지 않을까 싶네요 |
||
@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()); | ||
} | ||
} |
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.
NOW()를 실행하는 시점에 데이터가 있을때만 업데이트 되는거로 인지했어서 여쭤봅니당
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.
넵 데이터가 있을때만 업데이트되어 업데이트 시간만을 NOW()로 갱신해줍니당