Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9895341
feat: 연속 읽기 랭킹용 flat 추가
kysub99 Mar 21, 2026
5753940
feat: flat 기준 뱃지 응답 변환 추가
kysub99 Mar 21, 2026
d87451b
feat: 연속 읽기 랭킹 응답 DTO 추가
kysub99 Mar 21, 2026
5dba4ab
feat: 연속 읽기 랭킹 쿼리 추가
kysub99 Mar 21, 2026
bfdbd74
feat: 연속 읽기 랭킹 서비스 및 API 연동
kysub99 Mar 21, 2026
fa8cf3e
test: 연속 읽기 랭킹 테스트
kysub99 Mar 21, 2026
24fc57c
fix: 나의 연속 읽기 순위에서 dayCount 0을 최하위 공동 순위로 반환
kysub99 Mar 24, 2026
6278d51
refactor: 연속 읽기 순위 Optional 사용
kysub99 Mar 25, 2026
c5da72f
feat: 연속 읽기 랭킹 스냅샷/메타 테이블 및 레파지토리 추가
kysub99 Mar 25, 2026
b71d264
feat: 연속 읽기 랭킹을 스냅샷 기반으로 조회
kysub99 Mar 25, 2026
a2056d2
feat: 연속 읽기 랭킹 DTO 수정
kysub99 Mar 25, 2026
cde89a8
test: 조회 구조 변경에 따른 테스트 수정
kysub99 Mar 25, 2026
58c72e8
refactor: required를 requiredMode로 변경
kysub99 Mar 25, 2026
73f034c
style: DTO 공백 추가
kysub99 Mar 25, 2026
b616413
refactor: cron 별도로 분리
kysub99 Mar 25, 2026
5fe908c
refactor: ASC 정렬 조건 명시 제거
kysub99 Mar 27, 2026
18d1245
refactor: 메서드 네이밍 변경
kysub99 Mar 27, 2026
b826935
refactor: 연속 읽기 실시간, 스냅샨 도메인 네이밍 수정
kysub99 Mar 27, 2026
5d6c9fb
feat: 연속 읽기 스냅샷을 월간 스냅샷과 동일한 구조로 변경
kysub99 Mar 27, 2026
1b1fc26
refactor: readOnly 트랜잭션 적용
kysub99 Mar 27, 2026
1cf7ef5
refactor: 불필요한 Schema 어노테이션 제거
kysub99 Mar 28, 2026
c5efb25
chore: flyway 버전 수정
kysub99 Mar 28, 2026
691fbfd
chore: 엔티티명 변경에 맞춰 테이블명 수정
kysub99 Mar 28, 2026
842868e
refactor: meta 테이블 통합
kysub99 Apr 1, 2026
267218f
feat: meta 테이블 통합에 따른 스키마 수정
kysub99 Apr 1, 2026
889d3ea
chore: flyway 버전 변경
kysub99 Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.bombom.api.v1.badge.dto.response;

import me.bombom.api.v1.reading.dto.MonthlyReadingRankFlat;
import me.bombom.api.v1.reading.dto.ContinueReadingRankFlat;

public record BadgesResponse(
RankingBadgeResponse ranking,
Expand All @@ -15,4 +16,13 @@ public static BadgesResponse from(MonthlyReadingRankFlat flat) {
}
return new BadgesResponse(ranking, challenge);
}

public static BadgesResponse from(ContinueReadingRankFlat flat) {
RankingBadgeResponse ranking = RankingBadgeResponse.from(flat);
ChallengeBadgeResponse challenge = ChallengeBadgeResponse.from(flat);
if (ranking == null && challenge == null) {
return null;
}
return new BadgesResponse(ranking, challenge);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.validation.constraints.NotNull;
import me.bombom.api.v1.badge.domain.BadgeGrade;
import me.bombom.api.v1.reading.dto.MonthlyReadingRankFlat;
import me.bombom.api.v1.reading.dto.ContinueReadingRankFlat;

public record ChallengeBadgeResponse(

Expand All @@ -27,4 +28,15 @@ public static ChallengeBadgeResponse from(MonthlyReadingRankFlat flat) {
}
return null;
}

public static ChallengeBadgeResponse from(ContinueReadingRankFlat flat) {
if (flat.hasChallengeBadge()) {
return new ChallengeBadgeResponse(
BadgeGrade.valueOf(flat.challengeBadgeGrade()),
flat.challengeBadgeName(),
flat.challengeBadgeGeneration()
);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.validation.constraints.NotNull;
import me.bombom.api.v1.badge.domain.BadgeGrade;
import me.bombom.api.v1.reading.dto.MonthlyReadingRankFlat;
import me.bombom.api.v1.reading.dto.ContinueReadingRankFlat;

public record RankingBadgeResponse(

Expand All @@ -27,4 +28,15 @@ public static RankingBadgeResponse from(MonthlyReadingRankFlat flat) {
}
return null;
}

public static RankingBadgeResponse from(ContinueReadingRankFlat flat) {
if (flat.hasRankingBadge()) {
return new RankingBadgeResponse(
BadgeGrade.valueOf(flat.rankingBadgeGrade()),
flat.rankingBadgeYear(),
flat.rankingBadgeMonth()
);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package me.bombom.api.v1.common;

import jakarta.validation.constraints.NotBlank;
import java.time.ZoneId;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.validation.annotation.Validated;

@Getter
@Setter
@Validated
@ConfigurationProperties("ranking.reading.continue")
public class ContinueReadingRankingScheduleProperties {

@NotBlank
private String cron;

@NotBlank
private String zone;

public ZoneId zoneId() {
return ZoneId.of(zone);
}

public CronExpression cronExpression() {
return CronExpression.parse(cron);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package me.bombom.api.v1.common.config;

import me.bombom.api.v1.common.ContinueReadingRankingScheduleProperties;
import me.bombom.api.v1.common.MonthlyRankingScheduleProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(MonthlyRankingScheduleProperties.class)
@EnableConfigurationProperties({
MonthlyRankingScheduleProperties.class,
ContinueReadingRankingScheduleProperties.class
})
public class RankingScheduleConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import me.bombom.api.v1.reading.dto.response.MemberMonthlyReadingRankResponse;
import me.bombom.api.v1.reading.dto.response.MonthlyReadingRankingResponse;
import me.bombom.api.v1.reading.dto.response.ReadingInformationResponse;
import me.bombom.api.v1.reading.dto.response.ContinueReadingRankingResponse;
import me.bombom.api.v1.reading.dto.response.MemberContinueReadingRankResponse;
import me.bombom.api.v1.reading.dto.response.WeeklyGoalCountResponse;
import me.bombom.api.v1.reading.service.ReadingService;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -46,12 +48,26 @@ public MonthlyReadingRankingResponse getMonthlyReadingRank(@RequestParam @Positi
return readingService.getMonthlyReadingRank(limit);
}

@Override
@GetMapping("/streak/rank")
public ContinueReadingRankingResponse getContinueReadingRank(
@RequestParam @Positive(message = "limit는 1 이상의 값이어야 합니다.") int limit
) {
return readingService.getContinueReadingRank(limit);
}

@Override
@GetMapping("/month/rank/me")
public MemberMonthlyReadingRankResponse getMemberMonthlyRank(@LoginMember Member member) {
return readingService.getMemberMonthlyReadingRank(member);
}

@Override
@GetMapping("/streak/rank/me")
public MemberContinueReadingRankResponse getMemberContinueReadingRank(@LoginMember Member member) {
return readingService.getMemberContinueReadingRank(member);
}

@Override
@GetMapping("/month")
public MemberMonthlyReadingCountResponse getMemberMonthlyReadingCount(@LoginMember Member member) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import me.bombom.api.v1.reading.dto.response.MemberMonthlyReadingRankResponse;
import me.bombom.api.v1.reading.dto.response.MonthlyReadingRankingResponse;
import me.bombom.api.v1.reading.dto.response.ReadingInformationResponse;
import me.bombom.api.v1.reading.dto.response.ContinueReadingRankingResponse;
import me.bombom.api.v1.reading.dto.response.MemberContinueReadingRankResponse;
import me.bombom.api.v1.reading.dto.response.WeeklyGoalCountResponse;
import org.springframework.web.bind.annotation.RequestParam;

Expand Down Expand Up @@ -50,13 +52,36 @@ WeeklyGoalCountResponse updateWeeklyGoalCount(
MonthlyReadingRankingResponse getMonthlyReadingRank(
@Parameter(description = "최대 조회 개수 (예: ?limit=10)") @RequestParam @Positive(message = "limit는 1 이상의 값이어야 합니다.") int limit);

@Operation(
summary = "스트릭 랭킹 조회",
description = "연속 읽기 일수(continue_reading_realtime.day_count) 기준 내림차순 순위를 조회합니다. day_count가 0인 회원도 목록에 포함됩니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "스트릭 랭킹 조회 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 값 (limit는 1 이상의 값이어야 함)", content = @Content),
})
ContinueReadingRankingResponse getContinueReadingRank(
@Parameter(description = "최대 조회 개수 (예: ?limit=10)") @RequestParam @Positive(message = "limit는 1 이상의 값이어야 합니다.") int limit
);

@Operation(summary = "나의 월간 순위 조회", description = "저장된 rank 기반으로 나의 순위와 총 랭킹 참여자 수를 반환합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "나의 월간 순위 조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패 (로그인 필요)", content = @Content),
})
MemberMonthlyReadingRankResponse getMemberMonthlyRank(@Parameter(hidden = true) Member member);

@Operation(
summary = "나의 스트릭 순위 조회",
description = "실시간 연속 읽기 일수 기준 나의 순위를 반환합니다. day_count가 0이면 월간 순위와 같이 최하위 구간의 공동 순위로 포함됩니다. continue_reading_snapshot 행이 없으면 404입니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "나의 스트릭 순위 조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패 (로그인 필요)", content = @Content),
@ApiResponse(responseCode = "404", description = "연속 읽기 랭킹 스냅샷 정보 없음", content = @Content),
})
MemberContinueReadingRankResponse getMemberContinueReadingRank(@Parameter(hidden = true) Member member);

@Operation(summary = "나의 월간 읽기 개수 조회", description = "현재 로그인한 사용자의 이번 달 아티클 읽기 개수를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "월간 읽기 개수 조회 성공"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ContinueReading extends BaseEntity {
public class ContinueReadingRealtime extends BaseEntity {

private static final int INITIAL_DAY_COUNT = 0;
private static final int RESET_DAY_COUNT = 0;
Expand All @@ -32,7 +32,7 @@ public class ContinueReading extends BaseEntity {
private int dayCount;

@Builder
public ContinueReading(
public ContinueReadingRealtime(
Long id,
@NonNull Long memberId,
int dayCount
Expand All @@ -42,8 +42,8 @@ public ContinueReading(
this.dayCount = dayCount;
}

public static ContinueReading create(Long memberId) {
return ContinueReading.builder()
public static ContinueReadingRealtime create(Long memberId) {
return ContinueReadingRealtime.builder()
.memberId(memberId)
.dayCount(INITIAL_DAY_COUNT)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package me.bombom.api.v1.reading.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import me.bombom.api.v1.common.BaseEntity;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ContinueReadingSnapshot extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private Long memberId;

@Column(nullable = false, columnDefinition = "SMALLINT")
private int dayCount;

@Column(nullable = false)
private long rankOrder;

@Builder
public ContinueReadingSnapshot(
Long id,
@NonNull Long memberId,
int dayCount,
long rankOrder
) {
this.id = id;
this.memberId = memberId;
this.dayCount = dayCount;
this.rankOrder = rankOrder;
}

public static ContinueReadingSnapshot create(Long memberId, int dayCount, long rankOrder) {
return ContinueReadingSnapshot.builder()
.memberId(memberId)
.dayCount(dayCount)
.rankOrder(rankOrder)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
Expand All @@ -13,21 +15,22 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MonthlyReadingSnapshotMeta {
public class ReadingSnapshotMeta {

@Id
@Column(nullable = false)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private ReadingSnapshotType snapshotType;

@Column(nullable= false)
@Column(nullable = false)
private LocalDateTime snapshotAt;

@Builder
public MonthlyReadingSnapshotMeta(
@NotNull Long id,
public ReadingSnapshotMeta(
@NotNull ReadingSnapshotType snapshotType,
@NotNull LocalDateTime snapshotAt
) {
this.id = id;
this.snapshotType = snapshotType;
this.snapshotAt = snapshotAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.bombom.api.v1.reading.domain;

public enum ReadingSnapshotType {

MONTHLY,
CONTINUE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package me.bombom.api.v1.reading.dto;

public record ContinueReadingRankFlat(

String nickname,
Comment on lines +3 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1

이 사이에 공백 추가해주세요!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다

style: DTO 공백 추가

long rank,
int dayCount,
String rankingBadgeGrade,
Integer rankingBadgeYear,
Integer rankingBadgeMonth,
String challengeBadgeGrade,
String challengeBadgeName,
Integer challengeBadgeGeneration
) {

public boolean hasRankingBadge() {
return rankingBadgeGrade != null && rankingBadgeYear != null && rankingBadgeMonth != null;
}

public boolean hasChallengeBadge() {
return challengeBadgeGrade != null && challengeBadgeName != null && challengeBadgeGeneration != null;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.bombom.api.v1.reading.dto;

public record MonthlyReadingRankFlat(

String nickname,
long rank,
int monthlyReadCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.bombom.api.v1.reading.dto;

public record RankerInfo(

Long memberId,
long rankOrder
) {
Expand Down
Loading
Loading