Skip to content

Commit c421605

Browse files
Ryan-Diakysub99
andauthored
DEV -> PROD (#780)
* feat: 공휴일 연속읽기 깨짐 방지 (#779) * [BOM-1148] feat: 마이페이지를 위한 엔티티 및 랭킹 저장 로직 추가 (5/31 오전까지 머지 필수) (#778) * [BOM-1148] feat: 아티클 읽음 이력 저장 구조 추가 * [BOM-1148] feat: 아티클 읽음 처리 시 읽음 이력 저장 * [BOM-1148] feat: 월말 확정 랭킹 이력 저장 구조 추가 * [BOM-1148] feat: 월간 초기화 전 확정 랭킹 이력 저장 * refactor: 아티클 읽음 이력 기간 컬럼 제거 * fix: 마이그레이션 버전 충돌 수정 * style: 엔티티 테이블명 명시 제거 * refactor: 랭킹 이력 기간을 날짜로 관리 --------- Co-authored-by: geonwoo kim <115832836+kysub99@users.noreply.github.com>
2 parents 54be6a3 + 1925d95 commit c421605

13 files changed

Lines changed: 549 additions & 1 deletion
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package me.bombom.api.v1.article.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import jakarta.persistence.UniqueConstraint;
10+
import java.time.LocalDateTime;
11+
import lombok.AccessLevel;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
import lombok.NonNull;
16+
import me.bombom.api.v1.common.BaseEntity;
17+
18+
@Entity
19+
@Getter
20+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
21+
@Table(
22+
uniqueConstraints = @UniqueConstraint(
23+
name = "uk_article_read_history_member_article",
24+
columnNames = {"member_id", "article_id"}
25+
)
26+
)
27+
public class ArticleReadHistory extends BaseEntity {
28+
29+
@Id
30+
@GeneratedValue(strategy = GenerationType.IDENTITY)
31+
private Long id;
32+
33+
@Column(nullable = false)
34+
private Long memberId;
35+
36+
@Column(nullable = false)
37+
private Long articleId;
38+
39+
@Column(nullable = false)
40+
private Long newsletterId;
41+
42+
@Column(nullable = false)
43+
private Long categoryId;
44+
45+
@Column(nullable = false)
46+
private LocalDateTime readAt;
47+
48+
@Builder
49+
public ArticleReadHistory(
50+
Long id,
51+
@NonNull Long memberId,
52+
@NonNull Long articleId,
53+
@NonNull Long newsletterId,
54+
@NonNull Long categoryId,
55+
@NonNull LocalDateTime readAt
56+
) {
57+
this.id = id;
58+
this.memberId = memberId;
59+
this.articleId = articleId;
60+
this.newsletterId = newsletterId;
61+
this.categoryId = categoryId;
62+
this.readAt = readAt;
63+
}
64+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package me.bombom.api.v1.article.repository;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.Optional;
5+
import me.bombom.api.v1.article.domain.ArticleReadHistory;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
10+
11+
public interface ArticleReadHistoryRepository extends JpaRepository<ArticleReadHistory, Long> {
12+
13+
Optional<ArticleReadHistory> findByMemberIdAndArticleId(Long memberId, Long articleId);
14+
15+
@Modifying
16+
@Query(value = """
17+
INSERT IGNORE INTO article_read_history (
18+
member_id,
19+
article_id,
20+
newsletter_id,
21+
category_id,
22+
read_at,
23+
created_at,
24+
updated_at
25+
)
26+
VALUES (
27+
:memberId,
28+
:articleId,
29+
:newsletterId,
30+
:categoryId,
31+
:readAt,
32+
NOW(6),
33+
NOW(6)
34+
)
35+
""", nativeQuery = true)
36+
int insertIfAbsent(
37+
@Param("memberId") Long memberId,
38+
@Param("articleId") Long articleId,
39+
@Param("newsletterId") Long newsletterId,
40+
@Param("categoryId") Long categoryId,
41+
@Param("readAt") LocalDateTime readAt
42+
);
43+
}

backend/bom-bom-server/src/main/java/me/bombom/api/v1/article/service/ArticleService.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package me.bombom.api.v1.article.service;
22

3+
import java.time.Clock;
34
import java.time.LocalDateTime;
45
import java.util.Comparator;
56
import java.util.List;
@@ -14,6 +15,7 @@
1415
import me.bombom.api.v1.article.dto.response.ArticleNewsletterStatisticsResponse;
1516
import me.bombom.api.v1.article.dto.response.ArticleResponse;
1617
import me.bombom.api.v1.article.event.MarkAsReadEvent;
18+
import me.bombom.api.v1.article.repository.ArticleReadHistoryRepository;
1719
import me.bombom.api.v1.article.repository.ArticleRepository;
1820
import me.bombom.api.v1.article.repository.RecentArticleRepository;
1921
import me.bombom.api.v1.bookmark.repository.BookmarkRepository;
@@ -47,13 +49,15 @@
4749
public class ArticleService {
4850

4951
private final ArticleRepository articleRepository;
52+
private final ArticleReadHistoryRepository articleReadHistoryRepository;
5053
private final RecentArticleRepository recentArticleRepository;
5154
private final TodayReadingRepository todayReadingRepository;
5255
private final CategoryRepository categoryRepository;
5356
private final NewsletterRepository newsletterRepository;
5457
private final HighlightRepository highlightRepository;
5558
private final ApplicationEventPublisher applicationEventPublisher;
5659
private final BookmarkRepository bookmarkRepository;
60+
private final Clock clock;
5761

5862
public Page<ArticleResponse> getArticles(
5963
Member member,
@@ -96,6 +100,19 @@ public void markAsRead(Long articleId, Member member) {
96100
return;
97101
}
98102
validateArticleOwner(article, member.getId());
103+
Newsletter newsletter = findNewsletterById(article.getNewsletterId(), member.getId());
104+
LocalDateTime readAt = LocalDateTime.now(clock);
105+
106+
int insertedRows = articleReadHistoryRepository.insertIfAbsent(
107+
member.getId(),
108+
article.getId(),
109+
article.getNewsletterId(),
110+
newsletter.getCategoryId(),
111+
readAt
112+
);
113+
if (insertedRows == 0) {
114+
return;
115+
}
99116
article.markAsRead();
100117

101118
applicationEventPublisher.publishEvent(new MarkAsReadEvent(member.getId(), articleId));
@@ -177,6 +194,14 @@ private Article findArticleById(Long articleId, Long memberId) {
177194
.addContext(ErrorContextKeys.ARTICLE_ID, articleId));
178195
}
179196

197+
private Newsletter findNewsletterById(Long newsletterId, Long memberId) {
198+
return newsletterRepository.findById(newsletterId)
199+
.orElseThrow(() -> new CIllegalArgumentException(ErrorDetail.ENTITY_NOT_FOUND)
200+
.addContext(ErrorContextKeys.ENTITY_TYPE, "newsletter")
201+
.addContext(ErrorContextKeys.MEMBER_ID, memberId)
202+
.addContext(ErrorContextKeys.NEWSLETTER_ID, newsletterId));
203+
}
204+
180205
private void validateNewsletterId(Long newsletterId) {
181206
if (newsletterId != null && !newsletterRepository.existsById(newsletterId)) {
182207
throw new CIllegalArgumentException(ErrorDetail.ENTITY_NOT_FOUND)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package me.bombom.api.v1.reading.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import jakarta.persistence.UniqueConstraint;
10+
import java.time.LocalDate;
11+
import lombok.AccessLevel;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
import lombok.NonNull;
16+
import me.bombom.api.v1.common.BaseEntity;
17+
18+
@Entity
19+
@Getter
20+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
21+
@Table(
22+
uniqueConstraints = @UniqueConstraint(
23+
name = "uk_continue_reading_rank_history_member_period",
24+
columnNames = {"member_id", "period"}
25+
)
26+
)
27+
public class ContinueReadingRankHistory extends BaseEntity {
28+
29+
@Id
30+
@GeneratedValue(strategy = GenerationType.IDENTITY)
31+
private Long id;
32+
33+
@Column(nullable = false)
34+
private Long memberId;
35+
36+
@Column(nullable = false)
37+
private LocalDate period;
38+
39+
@Column(nullable = false)
40+
private int dayCount;
41+
42+
@Column(nullable = false)
43+
private long rankOrder;
44+
45+
@Builder
46+
public ContinueReadingRankHistory(
47+
Long id,
48+
@NonNull Long memberId,
49+
@NonNull LocalDate period,
50+
int dayCount,
51+
long rankOrder
52+
) {
53+
this.id = id;
54+
this.memberId = memberId;
55+
this.period = period;
56+
this.dayCount = dayCount;
57+
this.rankOrder = rankOrder;
58+
}
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package me.bombom.api.v1.reading.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import jakarta.persistence.UniqueConstraint;
10+
import java.time.LocalDate;
11+
import lombok.AccessLevel;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
import lombok.NonNull;
16+
import me.bombom.api.v1.common.BaseEntity;
17+
18+
@Entity
19+
@Getter
20+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
21+
@Table(
22+
uniqueConstraints = @UniqueConstraint(
23+
name = "uk_monthly_reading_rank_history_member_period",
24+
columnNames = {"member_id", "period"}
25+
)
26+
)
27+
public class MonthlyReadingRankHistory extends BaseEntity {
28+
29+
@Id
30+
@GeneratedValue(strategy = GenerationType.IDENTITY)
31+
private Long id;
32+
33+
@Column(nullable = false)
34+
private Long memberId;
35+
36+
@Column(nullable = false)
37+
private LocalDate period;
38+
39+
@Column(nullable = false)
40+
private int readCount;
41+
42+
@Column(nullable = false)
43+
private long rankOrder;
44+
45+
@Builder
46+
public MonthlyReadingRankHistory(
47+
Long id,
48+
@NonNull Long memberId,
49+
@NonNull LocalDate period,
50+
int readCount,
51+
long rankOrder
52+
) {
53+
this.id = id;
54+
this.memberId = memberId;
55+
this.period = period;
56+
this.readCount = readCount;
57+
this.rankOrder = rankOrder;
58+
}
59+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package me.bombom.api.v1.reading.repository;
2+
3+
import java.time.LocalDate;
4+
import java.util.Optional;
5+
import me.bombom.api.v1.reading.domain.ContinueReadingRankHistory;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
10+
11+
public interface ContinueReadingRankHistoryRepository extends JpaRepository<ContinueReadingRankHistory, Long> {
12+
13+
Optional<ContinueReadingRankHistory> findByMemberIdAndPeriod(
14+
Long memberId,
15+
LocalDate period
16+
);
17+
18+
@Modifying
19+
@Query(value = """
20+
INSERT IGNORE INTO continue_reading_rank_history (
21+
member_id,
22+
period,
23+
day_count,
24+
rank_order,
25+
created_at,
26+
updated_at
27+
)
28+
SELECT
29+
cr.member_id,
30+
:period,
31+
cr.day_count,
32+
RANK() OVER (ORDER BY cr.day_count DESC),
33+
NOW(6),
34+
NOW(6)
35+
FROM continue_reading_realtime cr
36+
""", nativeQuery = true)
37+
void saveCurrentContinueReadingRanking(
38+
@Param("period") LocalDate period
39+
);
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package me.bombom.api.v1.reading.repository;
2+
3+
import java.time.LocalDate;
4+
import java.util.Optional;
5+
import me.bombom.api.v1.reading.domain.MonthlyReadingRankHistory;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
10+
11+
public interface MonthlyReadingRankHistoryRepository extends JpaRepository<MonthlyReadingRankHistory, Long> {
12+
13+
Optional<MonthlyReadingRankHistory> findByMemberIdAndPeriod(
14+
Long memberId,
15+
LocalDate period
16+
);
17+
18+
@Modifying
19+
@Query(value = """
20+
INSERT IGNORE INTO monthly_reading_rank_history (
21+
member_id,
22+
period,
23+
read_count,
24+
rank_order,
25+
created_at,
26+
updated_at
27+
)
28+
SELECT
29+
r.member_id,
30+
:period,
31+
r.current_count,
32+
RANK() OVER (ORDER BY r.current_count DESC),
33+
NOW(6),
34+
NOW(6)
35+
FROM monthly_reading_realtime r
36+
""", nativeQuery = true)
37+
void saveCurrentMonthlyRanking(
38+
@Param("period") LocalDate period
39+
);
40+
}

0 commit comments

Comments
 (0)