diff --git a/src/main/java/cmf/commitField/CommitFieldApplication.java b/src/main/java/cmf/commitField/CommitFieldApplication.java index f807de8..530e927 100644 --- a/src/main/java/cmf/commitField/CommitFieldApplication.java +++ b/src/main/java/cmf/commitField/CommitFieldApplication.java @@ -3,9 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJpaAuditing +// 스케쥴링 활성화 +// 테스트시에만 주석 풀기 +@EnableScheduling public class CommitFieldApplication { public static void main(String[] args) { diff --git a/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java b/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java index d527e66..6679b06 100644 --- a/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java +++ b/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java @@ -23,12 +23,17 @@ public class CommitScheduler { @Scheduled(fixedRate = 60000) // 1분마다 실행 public void updateUserCommits() { + log.info("🔍 updateUserCommits 실행중"); List activeUsers = userRepository.findAll(); // 💫 변경 필요, 차후 active 상태인 user만 찾게끔 변경해야 함. + log.info("🔍 Active User Count: {}", activeUsers.size()); + for (User user : activeUsers) { Integer cachedCount = commitCacheService.getCachedCommitCount(user.getUsername()); int newCommitCount = githubService.getUserCommitCount(user.getUsername()); + log.info("🔍 User: {}, Commit Count: {}", user.getUsername(), newCommitCount); + if (cachedCount == null || cachedCount != newCommitCount) { // 변화가 있을 때만 처리 commitCacheService.updateCachedCommitCount(user.getUsername(), newCommitCount); redpandaProducer.sendCommitUpdate(user.getUsername(), newCommitCount); diff --git a/src/main/java/cmf/commitField/domain/commit/sinceCommit/entity/CommitHistory.java b/src/main/java/cmf/commitField/domain/commit/sinceCommit/entity/CommitHistory.java new file mode 100644 index 0000000..8e5854b --- /dev/null +++ b/src/main/java/cmf/commitField/domain/commit/sinceCommit/entity/CommitHistory.java @@ -0,0 +1,27 @@ +package cmf.commitField.domain.commit.sinceCommit.entity; + +import cmf.commitField.global.jpa.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Entity +@Table(name = "commit_history") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CommitHistory extends BaseEntity { + + @Column(nullable = false) + private String username; // GitHub 사용자명 + + @Column(nullable = false) + private int streak; // 연속 커밋 수 + + @Column(nullable = false) + private LocalDate commitDate; // 커밋한 날짜 +} + diff --git a/src/main/java/cmf/commitField/domain/commit/sinceCommit/repositoty/CommitHistoryRepository.java b/src/main/java/cmf/commitField/domain/commit/sinceCommit/repositoty/CommitHistoryRepository.java new file mode 100644 index 0000000..083a80a --- /dev/null +++ b/src/main/java/cmf/commitField/domain/commit/sinceCommit/repositoty/CommitHistoryRepository.java @@ -0,0 +1,13 @@ +package cmf.commitField.domain.commit.sinceCommit.repositoty; + +import cmf.commitField.domain.commit.sinceCommit.entity.CommitHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CommitHistoryRepository extends JpaRepository { + // 특정 유저의 최신 커밋 기록 조회 + Optional findTopByUsernameOrderByCommitDateDesc(String username); +} diff --git a/src/main/java/cmf/commitField/domain/commit/sinceCommit/service/CommitCacheService.java b/src/main/java/cmf/commitField/domain/commit/sinceCommit/service/CommitCacheService.java index 4e6a193..c94d3e5 100644 --- a/src/main/java/cmf/commitField/domain/commit/sinceCommit/service/CommitCacheService.java +++ b/src/main/java/cmf/commitField/domain/commit/sinceCommit/service/CommitCacheService.java @@ -14,9 +14,10 @@ public class CommitCacheService { private final StringRedisTemplate redisTemplate; public Integer getCachedCommitCount(String username) { - String key = "commit:" + username; - String value = redisTemplate.opsForValue().get(key); - return value != null ? Integer.parseInt(value) : null; + log.info("Redis Template: {}", redisTemplate); + String key = "commit:" + username; // Redis 키 생성 (ex: commit:hongildong) + String value = redisTemplate.opsForValue().get(key); // Redis에서 값 가져오기 + return value != null ? Integer.parseInt(value) : null; // 값이 있으면 정수 변환, 없으면 null 반환 } public void updateCachedCommitCount(String username, int count) { diff --git a/src/main/java/cmf/commitField/domain/noti/noti/entity/Noti.java b/src/main/java/cmf/commitField/domain/noti/noti/entity/Noti.java new file mode 100644 index 0000000..3825472 --- /dev/null +++ b/src/main/java/cmf/commitField/domain/noti/noti/entity/Noti.java @@ -0,0 +1,31 @@ +package cmf.commitField.domain.noti.noti.entity; + +import cmf.commitField.domain.user.entity.User; +import cmf.commitField.global.jpa.BaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import static lombok.AccessLevel.PROTECTED; + +@Entity +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor(access = PROTECTED) +@SuperBuilder +@Getter +@Setter +public class Noti extends BaseEntity { + @ManyToOne + private User actor; + @ManyToOne + private User receiver; + private String relTypeCode; + private long relId; + private String typeCode; + private String type2Code; + private boolean read; +} diff --git a/src/main/java/cmf/commitField/domain/noti/noti/eventListener/NotiEventListener.java b/src/main/java/cmf/commitField/domain/noti/noti/eventListener/NotiEventListener.java new file mode 100644 index 0000000..0eb3ba2 --- /dev/null +++ b/src/main/java/cmf/commitField/domain/noti/noti/eventListener/NotiEventListener.java @@ -0,0 +1,24 @@ +package cmf.commitField.domain.noti.noti.eventListener; + +import cmf.commitField.domain.noti.noti.service.NotiService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotiEventListener { + private final NotiService notiService; + +// public void listenPost(PostCreatedEvent event){ +// notiService.postCreated(event.getPost()); +// } +// +// public void consume(ChatMessageDto message){ +// System.out.println("Consumed message: " + message); +// } +// +// public void consumeChatRoom1DLT(byte[] in){ +// String message = new String(in); +// System.out.println("Failed message: " + message); +// } +} diff --git a/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java new file mode 100644 index 0000000..ffd26c6 --- /dev/null +++ b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java @@ -0,0 +1,9 @@ +package cmf.commitField.domain.noti.noti.repository; + +import cmf.commitField.domain.noti.noti.entity.Noti; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotiRepository extends JpaRepository { +} diff --git a/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java new file mode 100644 index 0000000..fda1ccb --- /dev/null +++ b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java @@ -0,0 +1,31 @@ +package cmf.commitField.domain.noti.noti.service; + +import cmf.commitField.domain.noti.noti.repository.NotiRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class NotiService { + private final NotiRepository notiRepository; + + public void sendCommitStreakNotification(String username, int streakCount) { + log.info("🎉 {}님의 연속 커밋이 {}일로 증가했습니다!", username, streakCount); + // 알림을 DB 저장 또는 웹소켓 / 이메일 / 푸시 알림 전송 가능 + } + +// public CommitAnalysisResponseDto getCommitAnalysis(String owner, String repo, String username, LocalDateTime since, LocalDateTime until) { +// List commits = getSinceCommits(owner, repo, since, until); +// StreakResult streakResult = calculateStreaks(commits); +// +// // 연속 커밋 수 Redis 업데이트 및 알림 +// streakService.updateStreak(username, streakResult.currentStreak, streakResult.maxStreak); +// +// return new CommitAnalysisResponseDto(commits, streakResult.currentStreak, streakResult.maxStreak); +// } + +} diff --git a/src/main/java/cmf/commitField/domain/noti/streak/service/StreakService.java b/src/main/java/cmf/commitField/domain/noti/streak/service/StreakService.java new file mode 100644 index 0000000..7ac2cea --- /dev/null +++ b/src/main/java/cmf/commitField/domain/noti/streak/service/StreakService.java @@ -0,0 +1,36 @@ +package cmf.commitField.domain.noti.streak.service; + +import cmf.commitField.domain.noti.noti.service.NotiService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StreakService { + private final RedisTemplate redisTemplate; + + private final NotiService notiService; + + public void updateStreak(String username, int newCurrentStreak, int newMaxStreak) { + String currentStreakKey = "user:" + username + ":current_streak"; + String maxStreakKey = "user:" + username + ":max_streak"; + + int prevCurrentStreak = getStreak(currentStreakKey); + int prevMaxStreak = getStreak(maxStreakKey); + + // 연속 커밋이 증가했으면 Redis 업데이트 및 알림 발송 + if (newCurrentStreak > prevCurrentStreak) { + // redis 업데이트 + redisTemplate.opsForValue().set(currentStreakKey, String.valueOf(newCurrentStreak)); + + // 알림 발송 +// notiService.sendCommitStreakNotification(username, newCurrentStreak); + } + } + + private int getStreak(String key) { + String value = redisTemplate.opsForValue().get(key); + return (value != null) ? Integer.parseInt(value) : 0; + } +} diff --git a/src/main/java/cmf/commitField/domain/season/service/UserSeasonService.java b/src/main/java/cmf/commitField/domain/season/service/UserSeasonService.java index 51425b8..402223a 100644 --- a/src/main/java/cmf/commitField/domain/season/service/UserSeasonService.java +++ b/src/main/java/cmf/commitField/domain/season/service/UserSeasonService.java @@ -7,6 +7,8 @@ import cmf.commitField.domain.season.repository.UserSeasonRepository; import cmf.commitField.domain.user.entity.User; import cmf.commitField.domain.user.repository.UserRepository; +import cmf.commitField.global.error.ErrorCode; +import cmf.commitField.global.exception.CustomException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,7 +25,7 @@ public class UserSeasonService { // 현재 시즌에 유저 랭크 추가하기 (SEED 등급) public UserSeason addUserRank(Long userId) { User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); Season season = seasonService.getActiveSeason(); @@ -38,17 +40,17 @@ public UserSeason addUserRank(Long userId) { // 유저의 모든 시즌 랭크 조회 public List getUserRanks(Long userId) { User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); return userSeasonRepository.findByUser(user); } // 특정 시즌의 유저 랭크 조회 public UserSeason getUserRankBySeason(Long userId, Long seasonId) { User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); Season season = seasonRepository.findById(seasonId) - .orElseThrow(() -> new RuntimeException("Season not found")); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SEASON)); return userSeasonRepository.findByUserAndSeason(user, season) - .orElseThrow(() -> new RuntimeException("Rank not found for this season")); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SEASON_RANK)); } } diff --git a/src/main/java/cmf/commitField/global/config/RedisConfig.java b/src/main/java/cmf/commitField/global/config/RedisConfig.java index b2a25ae..e8d568c 100644 --- a/src/main/java/cmf/commitField/global/config/RedisConfig.java +++ b/src/main/java/cmf/commitField/global/config/RedisConfig.java @@ -36,6 +36,7 @@ public RedisConnectionFactory redisConnectionFactory() { configuration.setPort(port); return new LettuceConnectionFactory(configuration); } + @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); diff --git a/src/main/java/cmf/commitField/global/error/ErrorCode.java b/src/main/java/cmf/commitField/global/error/ErrorCode.java index cac124c..f842e25 100644 --- a/src/main/java/cmf/commitField/global/error/ErrorCode.java +++ b/src/main/java/cmf/commitField/global/error/ErrorCode.java @@ -35,6 +35,10 @@ public enum ErrorCode { // member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다"), + // season + NOT_FOUND_SEASON(HttpStatus.NOT_FOUND, "시즌을 찾을 수 없습니다."), + NOT_FOUND_SEASON_RANK(HttpStatus.NOT_FOUND, "해당 시즌의 랭킹을 찾을 수 없습니다."), + //chatroom NOT_FOUND_ROOM(HttpStatus.NOT_FOUND, "이미 삭제된 방이거나 방을 찾을 수 없습니다."), ROOM_USER_FULL(HttpStatus.BAD_REQUEST, "방에 사용자가 다 차 있습니다."),