Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,7 @@
package com.example.green.domain.certification.infra.projections;

public interface MemberCertifiedCountProjection {
Long getMemberId();

int getCertifiedCount();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.green.domain.certification.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.example.green.domain.certification.domain.ChallengeCertification;
import com.example.green.domain.certification.infra.projections.MemberCertifiedCountProjection;

public interface ChallengeCertificationRepository extends JpaRepository<ChallengeCertification, Long> {

//단일 회원 인증 수
int countChallengeCertificationByMemberMemberId(Long memberId);

//여러 회원 인증 수 (한번에 조회)
@Query("SELECT c.member.memberId AS memberId, COUNT(c) AS certifiedCount " +
"FROM ChallengeCertification c " +
"WHERE c.member.memberId IN :memberIds " +
"GROUP BY c.member.memberId")
List<MemberCertifiedCountProjection> findCertifiedCountByMemberIds(@Param("memberIds") List<Long> memberIds);
Comment on lines +17 to +22

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.

⚠️ Potential issue | 🟡 Minor

Handle empty memberIds list edge case.

If memberIds is empty, the IN :memberIds clause may produce unexpected behavior or SQL errors depending on the database. Consider adding a guard in the service layer or using @Query with conditional logic.

Service-layer guard example:

if (memberIds.isEmpty()) {
    return Collections.emptyList();
}
return repository.findCertifiedCountByMemberIds(memberIds);
🤖 Prompt for AI Agents
In
src/main/java/com/example/green/domain/certification/repository/ChallengeCertificationRepository.java
around lines 17 to 22, the repository query uses IN :memberIds which can fail or
behave unexpectedly when memberIds is empty; add a guard in the calling service
method to check if the memberIds list is empty and immediately return
Collections.emptyList() (or equivalent) instead of calling
findCertifiedCountByMemberIds, or alternatively ensure the repository call is
only invoked with a non-empty list to avoid SQL errors.

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@

public interface WeeklyRankingRepository extends JpaRepository<WeeklyRanking, Long>, WeeklyRankingRepositoryCustom {
List<Tuple> findByWeekStart(LocalDate weekStart);

void deleteByWeekStart(LocalDate weekStart);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.example.green.domain.dashboard.rankingmodule.service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneId;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

Expand All @@ -16,6 +20,12 @@ public class WeeklyRankingScheduler {
*/
@Scheduled(cron = "0 0 0 * * MON", zone = "Asia/Seoul")
public void calculateWeeklyRankingBatch() {
weeklyRankingService.updateWeeklyRanks();
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));

// 오늘이 월요일 → 지난주 계산
LocalDate weekStart = today.minusWeeks(1).with(DayOfWeek.MONDAY);
LocalDate weekEnd = today.minusWeeks(1).with(DayOfWeek.SUNDAY);

weeklyRankingService.updateWeeklyRanks(weekStart, weekEnd);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -12,6 +13,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.green.domain.certification.infra.projections.MemberCertifiedCountProjection;
import com.example.green.domain.certification.repository.ChallengeCertificationRepository;
import com.example.green.domain.dashboard.rankingmodule.dto.response.MemberPointResponse;
import com.example.green.domain.dashboard.rankingmodule.dto.response.TopMemberPointResponse;
import com.example.green.domain.dashboard.rankingmodule.entity.WeeklyRanking;
Expand All @@ -20,6 +23,7 @@
import com.example.green.domain.dashboard.rankingmodule.repository.WeeklyRankingRepository;
import com.example.green.domain.member.entity.Member;
import com.example.green.domain.member.repository.MemberRepository;
import com.example.green.domain.point.repository.PointTransactionQueryRepository;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,34 +36,78 @@ public class WeeklyRankingService {

private final WeeklyRankingRepository weeklyRankingRepository;
private final MemberRepository memberRepository;
private final PointTransactionQueryRepository pointTransactionQueryRepository;
private final ChallengeCertificationRepository challengeCertificationRepository;

//모든 주별 TopN 계산 로직 추가
@Transactional
public void updateWeeklyRanks() {
public void updateWeeklyRanks(LocalDate weekStart, LocalDate weekEnd) {

List<WeeklyRanking> allWeeklyRankings = weeklyRankingRepository.findAllRankings();
// 기존 주차 랭킹 삭제
weeklyRankingRepository.deleteByWeekStart(weekStart);

Map<LocalDate, List<WeeklyRanking>> rankingsByWeek = allWeeklyRankings.stream()
.collect(Collectors.groupingBy(WeeklyRanking::getWeekStart));

for (Map.Entry<LocalDate, List<WeeklyRanking>> entry : rankingsByWeek.entrySet()) {
// 모든 회원 조회
List<Member> allMembers = memberRepository.findAll();
List<Long> memberIds = allMembers.stream()
.map(Member::getId)
.toList();

List<WeeklyRanking> weeklyRankings = entry.getValue();
Map<Long, BigDecimal> memberPoints = Optional.ofNullable(
pointTransactionQueryRepository.findEarnedPointByMember(memberIds))
.orElse(new HashMap<>());

Map<Long, Integer> certifiedCounts = Optional.ofNullable(
challengeCertificationRepository.findCertifiedCountByMemberIds(memberIds))
.orElse(new ArrayList<>())
.stream()
.collect(Collectors.toMap(
MemberCertifiedCountProjection::getMemberId,
MemberCertifiedCountProjection::getCertifiedCount
));

List<WeeklyRanking> weeklyRankings = allMembers.stream()
.map(member -> {
BigDecimal point = memberPoints.getOrDefault(member.getId(), BigDecimal.ZERO);
int certificationCount = certifiedCounts.getOrDefault(member.getId(), 0);
String profileImageUrl = (member.getProfile() != null) ?
member.getProfile().getProfileImageUrl() : null;

return WeeklyRanking.builder()
.memberId(member.getId())
.memberName(member.getName())
.profileImageUrl(profileImageUrl)
.totalPoint(point) // DB 저장/정렬용
.certificationCount(certificationCount)
.weekStart(weekStart)
.weekEnd(weekEnd)
.rank(0)
.build();
})
.toList();

Comment on lines +68 to 87

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "pom.xml" -o -name "build.gradle" | head -5

Repository: GreenWiNit/backend

Length of output: 44


🏁 Script executed:

cat -n /tmp/src/main/java/com/example/green/domain/dashboard/rankingmodule/service/WeeklyRankingService.java | head -120

Repository: GreenWiNit/backend

Length of output: 195


🏁 Script executed:

git ls-files | grep -i "weeklyrankingservice"

Repository: GreenWiNit/backend

Length of output: 253


🏁 Script executed:

git ls-files | grep -E "(pom\.xml|build\.gradle)" | head -5

Repository: GreenWiNit/backend

Length of output: 74


🏁 Script executed:

cat build.gradle | grep -A 5 -B 5 "java\|source\|target\|version" | head -30

Repository: GreenWiNit/backend

Length of output: 895


🏁 Script executed:

cat -n src/main/java/com/example/green/domain/dashboard/rankingmodule/service/WeeklyRankingService.java | sed -n '50,110p'

Repository: GreenWiNit/backend

Length of output: 2574


🌐 Web query:

Java 16 stream().toList() unmodifiable list documentation

💡 Result:

Yes — Java 16 added Stream.toList(), and its Javadoc states the returned List is unmodifiable: "The returned List is unmodifiable; calls to any mutator method will always cause UnsupportedOperationException to be thrown." [1]. Note the difference from Collectors.toList() (no guarantees about mutability) and Collectors.toUnmodifiableList() (also unmodifiable but disallows nulls). [2]

Sources:

  • Stream.toList() Javadoc (Stream API). [1]
  • Collectors (toList / toUnmodifiableList) Javadoc. [2]

stream().toList() returns an unmodifiable list; calling sort() will throw UnsupportedOperationException at runtime

On Java 16+, stream().toList() returns an unmodifiable list. Line 89 calls weeklyRankings.sort(...) on this unmodifiable list, which will fail at runtime with UnsupportedOperationException.

Replace with a mutable list:

-		List<WeeklyRanking> weeklyRankings = allMembers.stream()
+		List<WeeklyRanking> weeklyRankings = allMembers.stream()
 			.map(member -> {
 				BigDecimal point = memberPoints.getOrDefault(member.getId(), BigDecimal.ZERO);
 				int certificationCount = certifiedCounts.getOrDefault(member.getId(), 0);
 				String profileImageUrl = (member.getProfile() != null) ?
 					member.getProfile().getProfileImageUrl() : null;
 
 				return WeeklyRanking.builder()
 					.memberId(member.getId())
 					.memberName(member.getName())
 					.profileImageUrl(profileImageUrl)
 					.totalPoint(point) // DB 저장/정렬용
 					.certificationCount(certificationCount)
 					.weekStart(weekStart)
 					.weekEnd(weekEnd)
 					.rank(0)
 					.build();
-			})
-			.toList();
+			})
+			.collect(Collectors.toCollection(ArrayList::new));
🤖 Prompt for AI Agents
In
src/main/java/com/example/green/domain/dashboard/rankingmodule/service/WeeklyRankingService.java
around lines 68-87, the code uses stream().toList() which returns an
unmodifiable list on Java 16+, causing weeklyRankings.sort(...) later to throw
UnsupportedOperationException; change the collection to a mutable list (for
example, collect into an ArrayList via
.collect(Collectors.toCollection(ArrayList::new)) or wrap the result with new
ArrayList<>(...)) so weeklyRankings can be sorted in place.

weeklyRankings.sort(
Comparator.comparing(WeeklyRanking::getTotalPoint, Comparator.reverseOrder())
.thenComparing(WeeklyRanking::getCertificationCount, Comparator.reverseOrder())
);
// 정렬 (포인트 내림차순, 동점이면 인증 수 내림차순)
weeklyRankings.sort(
Comparator.comparing(WeeklyRanking::getTotalPoint, Comparator.reverseOrder())
.thenComparing(WeeklyRanking::getCertificationCount, Comparator.reverseOrder())
);

for (int i = 0; i < weeklyRankings.size(); i++) {
WeeklyRanking weeklyRanking = weeklyRankings.get(i);
int newRank = i + 1;
if (weeklyRanking.getRank() != newRank) {
weeklyRanking.setRank(newRank);
}
int rankCounter = 1;
for (int i = 0; i < weeklyRankings.size(); i++) {
if (i > 0 &&
weeklyRankings.get(i).getTotalPoint().compareTo(weeklyRankings.get(i - 1).getTotalPoint()) == 0 &&
weeklyRankings.get(i).getCertificationCount() ==
weeklyRankings.get(i - 1).getCertificationCount()) {

// 완전 동점 → 이전 rank와 동일
weeklyRankings.get(i).setRank(weeklyRankings.get(i - 1).getRank());
} else {
weeklyRankings.get(i).setRank(rankCounter);
}
rankCounter++;
}

// DB 저장
weeklyRankingRepository.saveAll(weeklyRankings);
}

public List<TopMemberPointResponse> getAllRankData(LocalDate weekStart, int topN) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.example.green.domain.pointshop.item.dto.response.PointItemSearchResponse;
import com.example.green.domain.pointshop.item.entity.QPointItem;
import com.example.green.global.api.page.Pagination;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

Expand Down Expand Up @@ -50,7 +51,14 @@ public List<PointItemSearchResponse> findItemsForExcel(BooleanExpression express

public List<PointItemResponse> findItemByCursor(BooleanExpression expression, int cursorViewSize) {
return jpaQueryFactory
.select(PointItemProjections.toPointItemView(qPointItem))
.select(Projections.constructor(
PointItemResponse.class,
qPointItem.id,
qPointItem.itemBasicInfo.itemName,
qPointItem.itemMedia.itemThumbNailUrl,
qPointItem.itemPrice.itemPrice,
qPointItem.sellingStatus
))
.from(qPointItem)
.where(expression)
.orderBy(qPointItem.id.desc())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public interface CertificationClient {
int getTotalCertifiedCountByMember(Long memberId);

Map<Long, Long> getCertificationCountByMembers(List<Long> memberIds);

}