Skip to content

Commit 84a1943

Browse files
authored
Merge pull request #140 from CommitField/feat/#81-1
feat : 랭킹 업 알림 및 커밋 부재 알림 기능 구현
2 parents d79764e + ca304f7 commit 84a1943

File tree

12 files changed

+158
-37
lines changed

12 files changed

+158
-37
lines changed

src/main/java/cmf/commitField/domain/admin/controller/ApiV1PetImgController.java renamed to src/main/java/cmf/commitField/domain/admin/admin/controller/ApiV1PetImgController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmf.commitField.domain.admin.controller;
1+
package cmf.commitField.domain.admin.admin.controller;
22

33
import cmf.commitField.global.aws.s3.S3Service;
44
import lombok.RequiredArgsConstructor;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package cmf.commitField.domain.admin.notice.entity;
2+
3+
4+
import cmf.commitField.global.jpa.BaseEntity;
5+
import jakarta.persistence.Entity;
6+
import lombok.*;
7+
import lombok.experimental.SuperBuilder;
8+
9+
@Entity
10+
@SuperBuilder
11+
@Getter
12+
@Setter
13+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
14+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
15+
public class Notice extends BaseEntity {
16+
private String title;
17+
private String content;
18+
}

src/main/java/cmf/commitField/domain/commit/scheduler/CommitUpdateService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmf.commitField.domain.commit.scheduler;
22

33
import cmf.commitField.domain.commit.totalCommit.service.TotalCommitService;
4+
import cmf.commitField.domain.noti.noti.service.NotiService;
45
import cmf.commitField.domain.user.dto.UserInfoDto;
56
import cmf.commitField.domain.user.entity.Tier;
67
import cmf.commitField.domain.user.entity.User;
@@ -16,9 +17,9 @@
1617
public class CommitUpdateService {
1718
private final TotalCommitService totalCommitService;
1819
private final UserRepository userRepository;
19-
private final SimpMessagingTemplate messagingTemplate;
20+
private final NotiService notiService;
2021

21-
// 유저 랭킹 상승 로짓
22+
// 유저 랭킹 상승 로직
2223
public UserInfoDto updateUserTier(String username){
2324
User user = userRepository.findByUsername(username).get();
2425
long seasonCommitCount;
@@ -27,6 +28,8 @@ public UserInfoDto updateUserTier(String username){
2728
System.out.println(username+"유저 레벨 업! 현재 티어: "+user.getTier());
2829
userRepository.save(user);
2930

31+
notiService.createRankUpNoti(user);
32+
3033
return UserInfoDto.builder()
3134
.username(user.getUsername())
3235
.email(user.getEmail())

src/main/java/cmf/commitField/domain/noti/noti/controller/ApiV1NotiController.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@
3030
public class ApiV1NotiController {
3131
private final NotiService notiService;
3232
private final UserRepository userRepository;
33-
private final NotiWebSocketHandler notiWebSocketHandler;
34-
private final ApplicationEventPublisher eventPublisher;
3533

3634
@GetMapping("")
3735
public GlobalResponse<List<NotiDto>> getNoti(@AuthenticationPrincipal OAuth2User oAuth2User) {
@@ -41,11 +39,6 @@ public GlobalResponse<List<NotiDto>> getNoti(@AuthenticationPrincipal OAuth2User
4139
return GlobalResponse.success(notis);
4240
}
4341

44-
@PostMapping("")
45-
public void createNoti() {
46-
47-
}
48-
4942
@PostMapping("/read")
5043
public GlobalResponse<Object> readNoti() {
5144
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

src/main/java/cmf/commitField/domain/noti/noti/entity/NotiDetailType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
public enum NotiDetailType {
44
// 업적
5+
RANK_UP, // 랭킹 상승
56
ACHIEVEMENT_COMPLETED, // 업적 달성
67

78
// 연속
89
STREAK_CONTINUED, // 연속 커밋 이어짐
910
STREAK_BROKEN, // 연속 커밋 끊김
1011

11-
// 시즌
12+
NOTICE_CREATED, // 시즌
1213
SEASON_START // 시즌 시작
1314
}

src/main/java/cmf/commitField/domain/noti/noti/entity/NotiMessageTemplates.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ public class NotiMessageTemplates {
88
NotiDetailType.ACHIEVEMENT_COMPLETED, "🎉 {0}님이 [{1}] 업적을 달성했습니다!",
99
NotiDetailType.STREAK_CONTINUED, "🔥 {0}님의 연속 커밋이 {1}일째 이어지고 있습니다!",
1010
NotiDetailType.STREAK_BROKEN, "😢 {0}님의 연속 커밋 기록이 끊겼습니다. 다음번엔 더 오래 유지해봐요!",
11-
NotiDetailType.SEASON_START, "🚀 새로운 [{0}] 시즌 이 시작되었습니다! 랭킹 경쟁을 준비하세요!"
11+
NotiDetailType.SEASON_START, "🚀 새로운 [{0}] 시즌 이 시작되었습니다! 랭킹 경쟁을 준비하세요!",
12+
NotiDetailType.RANK_UP, "📈 축하합니다! {0}님의 랭킹이 {1}(으)로 상승했습니다! 🎊",
13+
NotiDetailType.NOTICE_CREATED, "📢 공지사항이 있습니다: {0}"
1214
);
1315

1416
// 알림 메시지 템플릿을 반환하는 메서드

src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import cmf.commitField.domain.noti.noti.dto.NotiDto;
44
import cmf.commitField.domain.noti.noti.entity.Noti;
5+
import cmf.commitField.domain.noti.noti.entity.NotiDetailType;
6+
import cmf.commitField.domain.noti.noti.entity.NotiType;
57
import cmf.commitField.domain.user.entity.User;
68
import io.lettuce.core.dynamic.annotation.Param;
79
import org.springframework.data.jpa.repository.JpaRepository;
810
import org.springframework.data.jpa.repository.Query;
911
import org.springframework.stereotype.Repository;
1012

13+
import java.time.LocalDateTime;
1114
import java.util.List;
1215
import java.util.Optional;
1316

@@ -18,4 +21,7 @@ public interface NotiRepository extends JpaRepository<Noti, Long> {
1821
"FROM Noti n JOIN n.receiver u WHERE u.id = :receiverId AND n.isRead = :isRead")
1922
Optional<List<NotiDto>> findNotiDtoByReceiverId(@Param("receiverId") Long receiverId, @Param("isRead") boolean isRead);
2023
Optional<List<Noti>> findNotiByReceiver(User receiver);
24+
25+
// 최근 10일 내 동일한 커밋 부재 알림이 있는지 확인
26+
boolean existsByReceiverAndTypeCodeAndType2CodeAndCreatedAtAfter(User receiver, NotiType type, NotiDetailType detailType, LocalDateTime after);
2127
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cmf.commitField.domain.noti.noti.service;
2+
3+
import cmf.commitField.domain.noti.noti.entity.NotiDetailType;
4+
import cmf.commitField.domain.noti.noti.entity.NotiType;
5+
import cmf.commitField.domain.noti.noti.repository.NotiRepository;
6+
import cmf.commitField.domain.user.entity.User;
7+
import cmf.commitField.domain.user.repository.UserRepository;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.scheduling.annotation.Scheduled;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
import java.time.LocalDateTime;
15+
import java.time.LocalTime;
16+
import java.util.List;
17+
18+
@Service
19+
@RequiredArgsConstructor
20+
@Slf4j
21+
public class CommitAbsenceNotiService {
22+
private final UserRepository userRepository;
23+
private final NotiService notiService;
24+
private final NotiRepository notiRepository;
25+
26+
// 매일 10시 실행
27+
@Scheduled(cron = "0 0 10 * * *")
28+
@Transactional
29+
public void sendCommitAbsenceNoti() {
30+
log.info("커밋 부재 알림 전송 시작");
31+
LocalDateTime today = LocalDateTime.now();
32+
LocalDateTime thresholdDate = today.minusDays(10); // 10일 전 날짜 계산
33+
34+
// 마지막 커밋이 10일 이상 지난 사용자 찾기
35+
List<User> inactiveUsers = userRepository.findUsersWithLastCommitBefore(thresholdDate);
36+
37+
for (User user : inactiveUsers) {
38+
if (!hasRecentAbsenceNoti(user)) { // 최근 알림이 없는 경우에만 생성
39+
notiService.createStreakBrokenNoti(user);
40+
log.info("커밋 부재 알림 전송: {}", user.getUsername());
41+
}
42+
}
43+
}
44+
45+
// 최근 10일 내 커밋 부재 알림이 있었는지 확인
46+
private boolean hasRecentAbsenceNoti(User user) {
47+
log.info("커밋 부재 알림 확인: {}", user.getUsername());
48+
LocalDateTime checkDate = LocalDateTime.now().minusDays(10);
49+
return notiRepository.existsByReceiverAndTypeCodeAndType2CodeAndCreatedAtAfter(
50+
user, NotiType.STREAK, NotiDetailType.STREAK_BROKEN, checkDate.toLocalDate().atTime(LocalTime.MIN)
51+
);
52+
}
53+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cmf.commitField.domain.noti.noti.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.stereotype.Service;
6+
7+
@Service
8+
@RequiredArgsConstructor
9+
@Slf4j
10+
public class CommitSteakNotiService {
11+
private final NotiService notiService;
12+
13+
}

src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import cmf.commitField.domain.noti.noti.entity.NotiType;
88
import cmf.commitField.domain.noti.noti.event.NotiEvent;
99
import cmf.commitField.domain.noti.noti.repository.NotiRepository;
10+
import cmf.commitField.domain.season.entity.Rank;
1011
import cmf.commitField.domain.season.entity.Season;
1112
import cmf.commitField.domain.user.entity.User;
1213
import cmf.commitField.domain.user.repository.UserRepository;
@@ -38,34 +39,39 @@ public static String generateMessage(NotiDetailType type, Object... params) {
3839
return message;
3940
}
4041

42+
// 알림 생성
4143
@Transactional
42-
public void createNoti(User receiver) {
43-
System.out.println("알림 생성");
44-
String message = NotiService.generateMessage(NotiDetailType.STREAK_BROKEN, receiver.getNickname());
44+
public void createNoti(User receiver, NotiType notiType, NotiDetailType notiDetailType, Long relId, String relTypeCode, Object... params) {
45+
// 메시지 생성
46+
String message = NotiService.generateMessage(notiDetailType, params);
4547

48+
// 알림 엔티티 생성
4649
Noti noti = Noti.builder()
47-
.typeCode(NotiType.STREAK)
48-
.type2Code(NotiDetailType.STREAK_BROKEN)
50+
.typeCode(notiType)
51+
.type2Code(notiDetailType)
4952
.receiver(receiver)
5053
.isRead(false)
5154
.message(message)
55+
.relId(relId)
56+
.relTypeCode(relTypeCode)
5257
.build();
5358

59+
notiRepository.save(noti);
60+
61+
// WebSocket 이벤트 발생
5462
List<NotiDto> notis = new ArrayList<>();
55-
Noti savedNoti = notiRepository.save(noti);
56-
notis.add(new NotiDto(savedNoti.getId(), savedNoti.getMessage(), savedNoti.getCreatedAt()));
63+
notis.add(new NotiDto(noti.getId(), noti.getMessage(), noti.getCreatedAt()));
5764
NotiEvent event = new NotiEvent(this, receiver.getUsername(), notis, "새로운 알림이 생성되었습니다.");
5865
eventPublisher.publishEvent(event);
5966
}
6067

61-
68+
// 알림 조회
6269
public List<NotiDto> getNotReadNoti(User receiver) {
63-
System.out.println("알림 조회");
6470
List<NotiDto> notis = notiRepository.findNotiDtoByReceiverId(receiver.getId(), false).orElse(null);
65-
System.out.println("알림 조회 끝");
6671
return notis;
6772
}
6873

74+
// 시즌 알림 확인
6975
public List<Noti> getSeasonNotiCheck(User receiver, long seasonId) {
7076
return notiRepository.findNotiByReceiverAndRelId(receiver, seasonId)
7177
.orElseThrow(() -> new CustomException(ErrorCode.ERROR_CHECK)); // 알림이 없을 경우 예외 발생
@@ -74,23 +80,37 @@ public List<Noti> getSeasonNotiCheck(User receiver, long seasonId) {
7480
// 새 시즌 알림 생성
7581
@Transactional
7682
public void createNewSeasonNoti(Season season, User user) {
77-
System.out.println("새 시즌 알림 생성");
78-
// 메시지 생성
79-
String message = NotiService.generateMessage(NotiDetailType.SEASON_START, season.getName());
83+
createNoti(user, NotiType.SEASON, NotiDetailType.SEASON_START, season.getId(), season.getModelName(), season.getName());
84+
}
8085

81-
Noti noti = Noti.builder()
82-
.typeCode(NotiType.SEASON)
83-
.type2Code(NotiDetailType.SEASON_START)
84-
.receiver(user)
85-
.isRead(false)
86-
.message(message)
87-
.relId(season.getId())
88-
.relTypeCode(season.getModelName())
89-
.build();
86+
// 랭킹 업 알림 생성
87+
@Transactional
88+
public void createRankUpNoti(User user) {
89+
createNoti(user, NotiType.RANK, NotiDetailType.RANK_UP, 0L, null, getDisplayName(user), user.getTier().name());
90+
}
9091

91-
notiRepository.save(noti);
92+
// 연속 커밋 축하 알림 생성
93+
@Transactional
94+
public void createStreakCommitNoti(User user, String days) {
95+
createNoti(user, NotiType.STREAK, NotiDetailType.STREAK_CONTINUED, 0L, null, getDisplayName(user), days);
96+
}
97+
98+
// 커밋 부재 알림 생성
99+
@Transactional
100+
public void createStreakBrokenNoti(User user) {
101+
createNoti(user, NotiType.STREAK, NotiDetailType.STREAK_BROKEN, 0L, null, getDisplayName(user));
102+
}
92103

93-
System.out.println("새 시즌 알림 생성 끝");
104+
// 업적 알림 생성
105+
@Transactional
106+
public void createAchievementNoti(User user, String achievementName) {
107+
createNoti(user, NotiType.ACHIEVEMENT, NotiDetailType.ACHIEVEMENT_COMPLETED, 0L, null, getDisplayName(user), achievementName);
108+
}
109+
110+
// 공지사항 알림 생성
111+
@Transactional
112+
public void createNoticeNoti(User user, String noticeTitle) {
113+
createNoti(user, NotiType.NOTICE, NotiDetailType.NOTICE_CREATED, 0L, null, noticeTitle);
94114
}
95115

96116
// 읽음 처리
@@ -104,4 +124,8 @@ public List<Noti> read(User receiver) {
104124
System.out.println("알림 읽음 처리 끝");
105125
return notis;
106126
}
127+
128+
private String getDisplayName(User user) {
129+
return user.getNickname() != null ? user.getNickname() : user.getUsername();
130+
}
107131
}

src/main/java/cmf/commitField/domain/user/repository/UserRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import org.springframework.data.repository.query.Param;
88
import org.springframework.stereotype.Repository;
99

10+
import java.time.LocalDate;
11+
import java.time.LocalDateTime;
12+
import java.util.List;
1013
import java.util.Optional;
1114

1215
@Repository
@@ -18,4 +21,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
1821
@Modifying
1922
@Query("UPDATE User u SET u.status = :status WHERE u.username = :username")
2023
void updateStatus(@Param("username") String username, @Param("status") boolean status);
24+
25+
@Query("SELECT u FROM User u WHERE u.lastCommitted <= :date")
26+
List<User> findUsersWithLastCommitBefore(@Param("date") LocalDateTime date);
2127
}

src/main/java/cmf/commitField/global/scheduler/NotiTestScheduler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cmf.commitField.global.scheduler;
22

3+
import cmf.commitField.domain.noti.noti.entity.NotiDetailType;
4+
import cmf.commitField.domain.noti.noti.entity.NotiType;
35
import cmf.commitField.domain.noti.noti.service.NotiService;
46
import cmf.commitField.domain.user.entity.User;
57
import cmf.commitField.domain.user.repository.UserRepository;
@@ -22,6 +24,6 @@ public void test() {
2224
System.out.println("test 실행");
2325

2426
User user = userRepository.findById(1L).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER));
25-
notiService.createNoti(user);
27+
notiService.createStreakBrokenNoti(user);
2628
}
2729
}

0 commit comments

Comments
 (0)