Skip to content

Commit 230c89e

Browse files
committed
feat : 알림 데이터 저장 및 프론트 연동 테스트 (#18)
1 parent 451ad8c commit 230c89e

File tree

13 files changed

+249
-40
lines changed

13 files changed

+249
-40
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cmf.commitField.domain.noti.noti.controller;
2+
3+
import cmf.commitField.domain.noti.noti.dto.NotiDto;
4+
import cmf.commitField.domain.noti.noti.entity.Noti;
5+
import cmf.commitField.domain.noti.noti.service.NotiService;
6+
import cmf.commitField.domain.user.entity.User;
7+
import cmf.commitField.domain.user.repository.UserRepository;
8+
import cmf.commitField.domain.user.service.CustomOAuth2UserService;
9+
import cmf.commitField.global.error.ErrorCode;
10+
import cmf.commitField.global.exception.CustomException;
11+
import cmf.commitField.global.globalDto.GlobalResponse;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.security.core.Authentication;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
17+
import org.springframework.security.oauth2.core.user.OAuth2User;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.RequestMapping;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.stream.Collectors;
25+
26+
@RestController
27+
@RequestMapping("/api/notifications")
28+
@RequiredArgsConstructor
29+
@Slf4j
30+
public class ApiV1NotiController {
31+
private final NotiService notiService;
32+
private final CustomOAuth2UserService customOAuth2UserService;
33+
private final UserRepository userRepository;
34+
35+
@GetMapping("")
36+
public GlobalResponse<List<NotiDto>> getNoti() {
37+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
38+
log.info("getNoti - userRequest: {}", authentication);
39+
40+
if (authentication instanceof OAuth2AuthenticationToken) {
41+
OAuth2User principal = (OAuth2User) authentication.getPrincipal();
42+
Map<String, Object> attributes = principal.getAttributes();
43+
String username = (String) attributes.get("login"); // GitHub ID
44+
User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER));
45+
List<Noti> notis = notiService.getNotReadNoti(user);
46+
47+
List<NotiDto> notiDtos = notis.stream()
48+
.map(NotiDto::new)
49+
.collect(Collectors.toList());
50+
return GlobalResponse.success(notiDtos);
51+
}
52+
53+
return GlobalResponse.error(ErrorCode.LOGIN_REQUIRED);
54+
}
55+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmf.commitField.domain.noti.noti.dto;
2+
3+
import cmf.commitField.domain.noti.noti.entity.Noti;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
import java.time.format.DateTimeFormatter;
8+
import java.time.temporal.ChronoUnit;
9+
10+
@Getter
11+
public class NotiDto {
12+
private String message;
13+
private String formattedCreatedAt; // 변환된 날짜를 저장할 필드
14+
15+
public NotiDto(Noti noti) {
16+
this.message = noti.getMessage();
17+
this.formattedCreatedAt = formatCreatedAt(noti.getCreatedAt()); // 변환된 날짜 저장
18+
}
19+
20+
private String formatCreatedAt(LocalDateTime createdAt) {
21+
LocalDateTime today = LocalDateTime.now();
22+
long daysBetween = ChronoUnit.DAYS.between(createdAt, today);
23+
24+
if (daysBetween == 0) {
25+
return "오늘";
26+
} else if (daysBetween == 1) {
27+
return "어제";
28+
} else if (daysBetween == 2) {
29+
return "1일 전";
30+
} else if (daysBetween == 3) {
31+
return "2일 전";
32+
} else if (daysBetween == 4) {
33+
return "3일 전";
34+
} else {
35+
return createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
36+
}
37+
}
38+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
public class Noti extends BaseEntity {
2525
@Enumerated(EnumType.STRING)
2626
private NotiType typeCode; // 알림 타입
27+
@Enumerated(EnumType.STRING)
2728
private NotiDetailType type2Code; // 알림 세부 타입
2829
@ManyToOne
2930
private User receiver; // 알림을 받는 사람
@@ -32,7 +33,7 @@ public class Noti extends BaseEntity {
3233
private String message; // 알림 메시지
3334

3435
// TODO: 알림이 연결된 객체 어떻게 처리할지 고민 필요.
35-
// private String relTypeCode; // 알림이 연결된 실제 객체 유형
36-
// private long relId; // 알림 객체의 Id
36+
private String relTypeCode; // 알림이 연결된 실제 객체 유형
37+
private long relId; // 알림 객체의 Id
3738

3839
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
public class NotiMessageTemplates {
66
// 알림 메시지 템플릿을 저장하는 맵
77
private static final Map<NotiDetailType, String> TEMPLATES = Map.of(
8-
NotiDetailType.ACHIEVEMENT_COMPLETED, "🎉 {0}님이 '{1}' 업적을 달성했습니다!",
8+
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}] 시즌 이 시작되었습니다! 랭킹 경쟁을 준비하세요!"
1212
);
1313

1414
// 알림 메시지 템플릿을 반환하는 메서드
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package cmf.commitField.domain.noti.noti.repository;
22

33
import cmf.commitField.domain.noti.noti.entity.Noti;
4+
import cmf.commitField.domain.user.entity.User;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.stereotype.Repository;
67

8+
import java.util.List;
9+
import java.util.Optional;
10+
711
@Repository
812
public interface NotiRepository extends JpaRepository<Noti, Long> {
13+
Optional<List<Noti>> findNotiByReceiverAndRelId(User receiver, long season);
14+
Optional<List<Noti>> findNotiByReceiverAndIsRead(User receiver, boolean read);
915
}

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

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import cmf.commitField.domain.noti.noti.entity.NotiMessageTemplates;
66
import cmf.commitField.domain.noti.noti.entity.NotiType;
77
import cmf.commitField.domain.noti.noti.repository.NotiRepository;
8+
import cmf.commitField.domain.season.entity.Season;
9+
import cmf.commitField.domain.season.repository.SeasonRepository;
10+
import cmf.commitField.domain.season.service.SeasonService;
811
import cmf.commitField.domain.user.entity.User;
912
import cmf.commitField.domain.user.repository.UserRepository;
1013
import cmf.commitField.global.error.ErrorCode;
@@ -15,6 +18,7 @@
1518
import org.springframework.transaction.annotation.Transactional;
1619

1720
import java.text.MessageFormat;
21+
import java.util.List;
1822

1923
@Service
2024
@RequiredArgsConstructor
@@ -23,44 +27,57 @@
2327
public class NotiService {
2428
private final NotiRepository notiRepository;
2529
private final UserRepository userRepository;
30+
private final SeasonRepository seasonRepository;
31+
private final SeasonService seasonService;
2632

2733
// 알림 메시지 생성
2834
public static String generateMessage(NotiDetailType type, Object... params) {
2935
String template = NotiMessageTemplates.getTemplate(type);
30-
return MessageFormat.format(template, params);
36+
log.info("generateMessage - params: {}", params);
37+
log.info("generateMessage - template: {}", template); // template 자체를 출력
38+
String message = MessageFormat.format(template, params); // params 배열을 그대로 전달
39+
log.info("generateMessage - message: {}", message);
40+
return message;
3141
}
3242

33-
// 연속 커밋 알림 생성
34-
@Transactional
35-
public Noti createCommitStreak(String username, NotiType type, NotiDetailType detailType, Object... params) {
36-
// 메시지 생성
37-
String message = NotiService.generateMessage(detailType, params);
3843

39-
// 사용자 조회 (없으면 예외 처리)
40-
User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER));
41-
42-
// 알림 객체 생성 후 저장
43-
Noti noti = Noti.builder()
44-
.typeCode(type)
45-
.type2Code(detailType)
46-
.receiver(user)
47-
.isRead(false)
48-
.message(message)
49-
.build();
44+
public List<Noti> getNotReadNoti(User receiver) {
45+
log.info("getNotReadNoti - receiver: {}", receiver);
46+
List<Noti> notis = notiRepository.findNotiByReceiverAndIsRead(receiver, false).orElse(null);
47+
log.info("getNotReadNoti - notis: {}", notis);
48+
return notis;
49+
}
5050

51-
return notiRepository.save(noti);
51+
public List<Noti> getSeasonNotiCheck(User receiver, long seasonId) {
52+
log.info("getSeasonNotiCheck - receiver: {}, seasonId: {}", receiver, seasonId);
53+
return notiRepository.findNotiByReceiverAndRelId(receiver, seasonId)
54+
.orElseThrow(() -> new CustomException(ErrorCode.ERROR_CHECK)); // 알림이 없을 경우 예외 발생
5255
}
5356

57+
// 새 시즌 알림 생성
58+
@Transactional
59+
public void createNewSeason(Season season) {
60+
log.info("createNewSeason - season: {}", season.getName());
61+
// 메시지 생성
62+
String message = NotiService.generateMessage(NotiDetailType.SEASON_START, season.getName());
63+
log.info("createNewSeason - message: {}", message);
5464

65+
// 모든 사용자 조회
66+
Iterable<User> users = userRepository.findAll();
5567

56-
// public CommitAnalysisResponseDto getCommitAnalysis(String owner, String repo, String username, LocalDateTime since, LocalDateTime until) {
57-
// List<SinceCommitResponseDto> commits = getSinceCommits(owner, repo, since, until);
58-
// StreakResult streakResult = calculateStreaks(commits);
59-
//
60-
// // 연속 커밋 수 Redis 업데이트 및 알림
61-
// streakService.updateStreak(username, streakResult.currentStreak, streakResult.maxStreak);
62-
//
63-
// return new CommitAnalysisResponseDto(commits, streakResult.currentStreak, streakResult.maxStreak);
64-
// }
68+
// 모든 유저 알림 객체 생성
69+
users.forEach(user -> {
70+
Noti noti = Noti.builder()
71+
.typeCode(NotiType.SEASON)
72+
.type2Code(NotiDetailType.SEASON_START)
73+
.receiver(user)
74+
.isRead(false)
75+
.message(message)
76+
.relId(season.getId())
77+
.relTypeCode(season.getModelName())
78+
.build();
6579

80+
notiRepository.save(noti);
81+
});
82+
}
6683
}

src/main/java/cmf/commitField/domain/user/entity/User.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import cmf.commitField.domain.heart.entity.Heart;
77
import cmf.commitField.domain.pet.entity.Pet;
88
import cmf.commitField.global.jpa.BaseEntity;
9+
import com.fasterxml.jackson.annotation.JsonIgnore;
910
import jakarta.persistence.*;
1011
import lombok.AllArgsConstructor;
1112
import lombok.Getter;
@@ -40,18 +41,23 @@ public enum Role {
4041
}
4142

4243
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
44+
@JsonIgnore
4345
private List<ChatRoom> chatRooms = new ArrayList<>();
4446

4547
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
48+
@JsonIgnore
4649
private List<UserChatRoom> userChatRooms = new ArrayList<>();
4750

4851
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
52+
@JsonIgnore
4953
private List<ChatMsg> chatMsgs = new ArrayList<>();
5054

5155
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
56+
@JsonIgnore
5257
private List<Pet> pets = new ArrayList<>();
5358

5459
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
60+
@JsonIgnore
5561
private List<Heart> hearts;
5662

5763
public User(String username, String email, String nickname, String avatarUrl, Boolean status, List<ChatRoom> cr, List<UserChatRoom> ucr, List<ChatMsg> cmsg){

src/main/java/cmf/commitField/domain/user/service/CustomOAuth2UserService.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
import cmf.commitField.domain.commit.sinceCommit.service.CommitCacheService;
44
import cmf.commitField.domain.commit.totalCommit.service.TotalCommitService;
5+
import cmf.commitField.domain.noti.noti.service.NotiService;
56
import cmf.commitField.domain.pet.entity.Pet;
67
import cmf.commitField.domain.pet.repository.PetRepository;
8+
import cmf.commitField.domain.season.entity.Season;
9+
import cmf.commitField.domain.season.service.SeasonService;
710
import cmf.commitField.domain.user.entity.CustomOAuth2User;
811
import cmf.commitField.domain.user.entity.User;
912
import cmf.commitField.domain.user.repository.UserRepository;
1013
import jakarta.servlet.http.HttpServletRequest;
1114
import jakarta.servlet.http.HttpSession;
1215
import lombok.RequiredArgsConstructor;
16+
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.data.redis.core.StringRedisTemplate;
1318
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
1419
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
1520
import org.springframework.security.oauth2.core.user.OAuth2User;
@@ -21,12 +26,16 @@
2126

2227
@Service
2328
@RequiredArgsConstructor
29+
@Slf4j
2430
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
2531
private final UserRepository userRepository;
2632
private final PetRepository petRepository;
2733
private final HttpServletRequest request; // HttpServletRequest를 주입 받음.
2834
private final CommitCacheService commitCacheService;
2935
private final TotalCommitService totalCommitService;
36+
private final NotiService notiService;
37+
private final SeasonService seasonService;
38+
private final StringRedisTemplate redisTemplate;
3039

3140
@Override
3241
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
@@ -70,6 +79,17 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) {
7079
commitCacheService.updateCachedCommitCount(user.getUsername(),0);
7180
}
7281

82+
// 시즌 알림 처리
83+
// 알림 테이블에서 Active인 시즌의 알림을 해당 유저가 가지고 있는지 체크
84+
String season_key = "season_active:" + user.getUsername();
85+
Season season = seasonService.getActiveSeason();
86+
if(notiService.getSeasonNotiCheck(user, season.getId()).isEmpty()){
87+
log.info("User {} does not have season noti", user.getUsername());
88+
// 가지고 있지 않다면 알림을 추가
89+
notiService.createNewSeason(season);
90+
// redisTemplate.opsForValue().set(season_key, String.valueOf(count), Duration.ofHours(3)); // 3시간 캐싱
91+
}
92+
7393
// 세션에 사용자 정보 저장
7494
HttpSession session = request.getSession();
7595
session.setAttribute("username", user.getUsername());

src/main/java/cmf/commitField/global/error/ErrorCode.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public enum ErrorCode {
6464
// Lock
6565
FAILED_GET_LOCK(HttpStatus.LOCKED, "락을 획득하지 못했습니다."),
6666

67+
// Check
68+
ERROR_CHECK(HttpStatus.BAD_REQUEST, "에러 체크"),
69+
6770
//Heart
6871
NOT_EXIST_ROOM_HEART(HttpStatus.BAD_REQUEST, "해당 채팅방에 좋아요가 눌러져 있지 않습니다."),
6972
ALREADY_HEART_TO_ROOM(HttpStatus.BAD_REQUEST, "이미 해당 채팅방에 좋아요를 누르셨습니다."),

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// java/cmf/commitField/global/scheduler/SeasonScheduler.java
12
package cmf.commitField.global.scheduler;
23

34
import cmf.commitField.domain.season.entity.Rank;
@@ -29,8 +30,8 @@ public class SeasonScheduler {
2930
private final UserRepository userRepository;
3031
private final SeasonService seasonService;
3132

32-
// 매일 자정마다 시즌 확인 및 생성
33-
@Scheduled(cron = "0 0 0 * * *")
33+
// 매년 3, 6, 9, 12월 1일 자정마다 시즌 확인 및 생성
34+
@Scheduled(cron = "0 0 0 1 3,6,9,12 *")
3435
public void checkAndCreateNewSeason() {
3536
LocalDate today = LocalDate.now();
3637
String seasonName = today.getYear() + " " + getSeasonName(today.getMonthValue());
@@ -43,7 +44,7 @@ public void checkAndCreateNewSeason() {
4344
LocalDateTime startDate = getSeasonStartDate(today.getYear(), today.getMonth());
4445
LocalDateTime endDate = startDate.plusMonths(3).minusSeconds(1);
4546

46-
//현재 활성 시즌 종료
47+
// 현재 활성 시즌 종료
4748
if (activeSeason != null) {
4849
activeSeason.setStatus(SeasonStatus.INACTIVE);
4950
seasonRepository.save(activeSeason);
@@ -55,6 +56,9 @@ public void checkAndCreateNewSeason() {
5556
// 모든 유저의 랭크 초기화
5657
resetUserRanks(newSeason);
5758

59+
// 새 시즌 시작 알림 저장 및 알림 전송
60+
// String message = notiService.createNewSeason(newSeason.getName());
61+
// notiWebSocketHandler.sendNotificationToAllUsers(message);
5862
System.out.println("새 시즌 생성: " + newSeason.getName());
5963
} else {
6064
System.out.println("이미 활성화된 시즌: " + activeSeason.getName());
@@ -98,4 +102,4 @@ private LocalDateTime getSeasonStartDate(int year, Month month) {
98102
};
99103
return LocalDateTime.of(year, startMonth, 1, 0, 0);
100104
}
101-
}
105+
}

0 commit comments

Comments
 (0)