Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cc71cff
feature(temperature): Top10 조회 쿼리 추가 DP-379
sunsetkk Sep 11, 2025
f17ec49
feature(temperature): ReputationQueryController 추가 - 온도 랭킹 TOP10 조회 엔…
sunsetkk Sep 11, 2025
f3cedcf
feature(temperature): ReputationQueryService 추가 - 랭킹 조회 비즈니스 로직 구현 DP…
sunsetkk Sep 11, 2025
509cec4
feature(temperature): TemperatureRankResponse DTO 추가 - 응답 전용 객체 정의 DP…
sunsetkk Sep 11, 2025
19ee243
refactor: 불필요한 주석 제거
sunsetkk Sep 11, 2025
7b219fc
feature(temperature): TemperatureMeResponse DTO 추가 - 응답 전용 객체 정의 DP-384
sunsetkk Sep 12, 2025
0411b27
feature(temperature): ReputationQueryController 내 온도 조회 API 엔드포인트 추가…
sunsetkk Sep 12, 2025
b691007
feature(temperature): ReputationQueryService 내 온도 조회 비즈니스 로직 추가 DP-384
sunsetkk Sep 12, 2025
1d171e1
feature(badge): Abandon 배지 부여 로직 추가 DP-354
sunsetkk Sep 12, 2025
a91e1eb
feature(badge): 삭제된 멤버 제외 조회 메서드 추가 DP-354
sunsetkk Sep 12, 2025
60190c5
feature(badge): 중도 하차/추방 시 Abandon 배지 부여 DP-354
sunsetkk Sep 12, 2025
48146ba
feature(badge): 팀 종료 시 배지 부여 대상 필터링 DP-354
sunsetkk Sep 12, 2025
c5b3fcd
feature(scheduler): 전체 온도 랭킹 TOP1 캐싱 스케줄러 추가 DP-385
sunsetkk Sep 12, 2025
77183c0
refactor(exception): TOP1 캐시 미갱신 예외 코드 추가 DP-385
sunsetkk Sep 12, 2025
b096838
feature(temperature): 전체 온도 랭킹 TOP1 조회 API 추가 DP-385
sunsetkk Sep 12, 2025
1dd591f
feature(temperature): TemperatureRankResponse updatedAt 및 toBuilder 추…
sunsetkk Sep 12, 2025
1e8233a
feature(temperature): TOP1 랭킹 조회 메서드 추가 DP-385
sunsetkk Sep 12, 2025
37846f8
feature(temperature): TOP1 랭킹 조회 비즈니스 로직 추가 DP-385
sunsetkk Sep 12, 2025
2484d9a
Update ReputationRankingScheduler.java
sunsetkk Sep 12, 2025
3f8babc
feature(reputation): TOP10 조회 메서드 캐싱 전용 구조로 개선 (DP-379)
sunsetkk Sep 12, 2025
7d2cc60
feature(reputation): TOP10 조회 시 캐싱 데이터 사용 및 isMine 동적 처리 (DP-379)
sunsetkk Sep 12, 2025
a1e1a68
feature(reputation): TOP10 캐싱 로직 추가 (DP-379)
sunsetkk Sep 12, 2025
97b9be5
feature(temperature): TemperatureRegionResponse DTO 추가 (DP-389)
sunsetkk Sep 12, 2025
37e9ef7
feature(temperature): 지역별 TOP1 랭킹 캐싱 로직 추가 (DP-389)
sunsetkk Sep 12, 2025
613128f
feature(temperature): 지역별 TOP1 랭킹 조회 API 구현 (DP-389)
sunsetkk Sep 12, 2025
61156df
feature(temperature): 지역별 TOP1 조회 쿼리 추가 (DP-389)
sunsetkk Sep 12, 2025
6449dea
feature(temperature): 지역별 TOP1 서비스 로직 구현 (DP-389)
sunsetkk Sep 12, 2025
da5d539
Merge pull request #196 from DeepDirect/feature/DP-379-temperature-ra…
projectmiluju Sep 12, 2025
d32e89f
Merge pull request #205 from DeepDirect/feature/DP-354-badge-system-a…
projectmiluju Sep 12, 2025
d23b625
Merge pull request #206 from DeepDirect/feature/DP-385-temperature-ra…
projectmiluju Sep 12, 2025
08f9572
Merge pull request #208 from DeepDirect/feature/DP-389-region-rank-top1
projectmiluju Sep 12, 2025
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
13 changes: 13 additions & 0 deletions src/main/java/goorm/ddok/badge/service/BadgeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ public AbandonBadgeDto getAbandonBadge(User user) {
.build());
}

/**
* 프로젝트/스터디 중도 하차 배지 부여
*
* - 중도 하차 시 abandon 배지를 +1 증가.
*
* @param user 중도 하차한 사용자
*/
@Transactional
public void grantAbandonBadge(User user) {
increaseBadge(user, BadgeType.abandon);
}


/**
* 착한 배지 변환
*
Expand Down
1 change: 1 addition & 0 deletions src/main/java/goorm/ddok/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public enum ErrorCode {
REPUTATION_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자 온도 정보를 찾을 수 없습니다."),
APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "참여 신청 내역을 찾을 수 없습니다."),
TEAM_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "팀원을 찾을 수 없습니다."),
RANKING_NOT_READY(HttpStatus.NOT_FOUND, "아직 랭킹 데이터가 준비되지 않았습니다."),


// 409 CONFLICT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package goorm.ddok.global.scheduler;

import goorm.ddok.global.exception.ErrorCode;
import goorm.ddok.global.exception.GlobalException;
import goorm.ddok.member.domain.User;
import goorm.ddok.reputation.dto.response.TemperatureRankResponse;
import goorm.ddok.reputation.dto.response.TemperatureRegionResponse;
import goorm.ddok.reputation.service.ReputationQueryService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Slf4j
@Component
@RequiredArgsConstructor
public class ReputationRankingScheduler {

private final ReputationQueryService reputationQueryService;

private final AtomicReference<TemperatureRankResponse> top1Cache = new AtomicReference<>();
private final AtomicReference<List<TemperatureRankResponse>> top10Cache = new AtomicReference<>();
private final AtomicReference<List<TemperatureRegionResponse>> regionTop1Cache = new AtomicReference<>();

private final AtomicReference<Instant> lastUpdated = new AtomicReference<>();

/** 서버 시작 시 바로 1회 실행 */
@PostConstruct
public void init() {
updateTop1Ranking();
updateTop10Ranking();
updateRegionTop1Ranking();
}

/** 매 시간마다 TOP1 + TOP10 갱신 */
@Scheduled(cron = "0 0 * * * *")
public void updateRankings() {
updateTop1Ranking();
updateTop10Ranking();
updateRegionTop1Ranking();
}

public void updateTop1Ranking() {
try {
// currentUser == null
TemperatureRankResponse top1 = reputationQueryService.getTop1TemperatureRank(null);
Instant now = Instant.now();

// 캐시에 저장 (updatedAt 덮어쓰기)
TemperatureRankResponse withUpdatedAt = TemperatureRankResponse.builder()
.rank(top1.getRank())
.userId(top1.getUserId())
.nickname(top1.getNickname())
.temperature(top1.getTemperature())
.mainPosition(top1.getMainPosition())
.profileImageUrl(top1.getProfileImageUrl())
.chatRoomId(top1.getChatRoomId())
.dmRequestPending(top1.isDmRequestPending())
.IsMine(false)
.mainBadge(top1.getMainBadge())
.abandonBadge(top1.getAbandonBadge())
.updatedAt(now)
.build();

top1Cache.set(withUpdatedAt);
lastUpdated.set(now);

log.info("✅ TOP1 랭킹 갱신 완료: userId={}, temp={}, updatedAt={}",
top1.getUserId(), top1.getTemperature(), lastUpdated.get());
} catch (Exception e) {
log.error("❌ TOP1 랭킹 갱신 실패", e);
}
}

private void updateTop10Ranking() {
try {
List<TemperatureRankResponse> top10 = reputationQueryService.getTop10TemperatureRank(null);
Instant now = Instant.now();

// 각 유저 순위, updatedAt 덮어쓰기
List<TemperatureRankResponse> withUpdatedAt =
IntStream.range(0, top10.size())
.mapToObj(i -> top10.get(i).toBuilder()
.rank(i + 1)
.IsMine(false)
.dmRequestPending(false)
.chatRoomId(null)
.updatedAt(now)
.build()
)
.collect(Collectors.toList());

top10Cache.set(withUpdatedAt);
lastUpdated.set(now);

log.info("✅ TOP10 랭킹 갱신 완료, updatedAt={}", now);
} catch (Exception e) {
log.error("❌ TOP10 랭킹 갱신 실패", e);
}
}

private void updateRegionTop1Ranking() {
try {
List<TemperatureRegionResponse> regionTop1 = reputationQueryService.getRegionTop1Rank(null);
Instant now = Instant.now();

// updatedAt 덮어쓰기
List<TemperatureRegionResponse> withUpdatedAt = regionTop1.stream()
.map(r -> r.toBuilder()
.dmRequestPending(false)
.chatRoomId(null)
.IsMine(false)
.updatedAt(now)
.build())
.toList();

regionTop1Cache.set(withUpdatedAt);
lastUpdated.set(now);

log.info("✅ 지역별 TOP1 랭킹 갱신 완료, updatedAt={}", now);
} catch (Exception e) {
log.error("❌ 지역별 TOP1 랭킹 갱신 실패", e);
}
}

/** 컨트롤러에서 캐시 조회용 */
public TemperatureRankResponse getCachedTop1() {
TemperatureRankResponse cached = top1Cache.get();
if (cached == null) {
throw new GlobalException(ErrorCode.RANKING_NOT_READY);
}
return cached;
}

public List<TemperatureRankResponse> getCachedTop10() {
List<TemperatureRankResponse> cached = top10Cache.get();
if (cached == null) {
throw new GlobalException(ErrorCode.RANKING_NOT_READY);
}
return cached;
}

public List<TemperatureRegionResponse> getCachedRegionTop1() {
List<TemperatureRegionResponse> cached = regionTop1Cache.get();
if (cached == null) {
throw new GlobalException(ErrorCode.RANKING_NOT_READY);
}
return cached;
}
}
Loading