Skip to content

Commit 1379182

Browse files
authored
Merge pull request #102 from Daily-Step/feat/87
[Feat/87] 알림 발송 스케줄러 및 알림 내역 저장 기능 구현
2 parents 8ccfb3c + 9820e2a commit 1379182

7 files changed

Lines changed: 719 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.challenge.api.service.notification;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Builder
10+
public class AchieveChallengeDTO {
11+
12+
Long memberId;
13+
String nickname;
14+
List<String> challengeTitles;
15+
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.challenge.api.service.notification;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class NewChallengeDTO {
9+
10+
Long memberId;
11+
String nickname;
12+
13+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.challenge.api.service.notification;
2+
3+
import com.challenge.domain.member.Member;
4+
import com.challenge.domain.member.MemberRepository;
5+
import com.challenge.domain.notification.Notification;
6+
import com.challenge.domain.notification.NotificationQueryRepository;
7+
import com.challenge.domain.notification.NotificationRepository;
8+
import com.challenge.exception.ErrorCode;
9+
import com.challenge.exception.GlobalException;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
import java.time.LocalDate;
15+
import java.time.LocalDateTime;
16+
import java.util.Map;
17+
18+
@Service
19+
@RequiredArgsConstructor
20+
@Transactional(readOnly = true)
21+
public class NotificationService {
22+
23+
private final NotificationQueryRepository notificationQueryRepository;
24+
private final NotificationRepository notificationRepository;
25+
private final MemberRepository memberRepository;
26+
27+
/**
28+
* 진행중인 챌린지가 없는 회원 token, 닉네임, id 조회
29+
*
30+
* @return token, NewChallengeDTO
31+
*/
32+
public Map<String, NewChallengeDTO> getNewChallengeTargets() {
33+
return notificationQueryRepository.getNewChallengeTargets();
34+
}
35+
36+
/**
37+
* 현재 시각 기준 달성할 챌린지가 있는 회원 token, id, 닉네임, 챌린지 제목 리스트 조회
38+
*
39+
* @return token, AchieveChallengeCountDTO
40+
*/
41+
public Map<String, AchieveChallengeDTO> getAchieveTargetsAndChallenge() {
42+
LocalDate today = LocalDate.now();
43+
return notificationQueryRepository.getAchieveTargetsAndChallenge(today);
44+
}
45+
46+
/**
47+
* 알림 내역 생성 및 저장
48+
*
49+
* @param memberId
50+
* @param title
51+
* @param content
52+
* @return
53+
*/
54+
@Transactional
55+
public Notification createAndSave(Long memberId, String title, String content) {
56+
Member member = memberRepository.findById(memberId)
57+
.orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND));
58+
59+
return notificationRepository.save(Notification.of(title, content, LocalDateTime.now(), member));
60+
}
61+
62+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.challenge.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.scheduling.annotation.EnableScheduling;
5+
6+
@EnableScheduling
7+
@Configuration
8+
public class SchedulerConfig {
9+
10+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.challenge.domain.notification;
2+
3+
import com.challenge.api.service.notification.AchieveChallengeDTO;
4+
import com.challenge.api.service.notification.NewChallengeDTO;
5+
import com.challenge.domain.challenge.ChallengeStatus;
6+
import com.querydsl.core.Tuple;
7+
import com.querydsl.core.types.Expression;
8+
import com.querydsl.core.types.dsl.BooleanExpression;
9+
import com.querydsl.core.types.dsl.Expressions;
10+
import com.querydsl.jpa.JPAExpressions;
11+
import com.querydsl.jpa.JPQLQuery;
12+
import com.querydsl.jpa.impl.JPAQueryFactory;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Repository;
15+
16+
import java.time.LocalDate;
17+
import java.time.LocalDateTime;
18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import static com.challenge.domain.challenge.QChallenge.challenge;
24+
import static com.challenge.domain.challengeRecord.QChallengeRecord.challengeRecord;
25+
import static com.challenge.domain.member.QMember.member;
26+
27+
@RequiredArgsConstructor
28+
@Repository
29+
public class NotificationQueryRepository {
30+
31+
private final JPAQueryFactory queryFactory;
32+
33+
/**
34+
* 진행중인 챌린지가 없는 회원 token 및 닉네임 조회
35+
*
36+
* @return
37+
*/
38+
public Map<String, NewChallengeDTO> getNewChallengeTargets() {
39+
// ONGOING 상태인 challenge 개수가 0개인 member의 token, nickname 조회
40+
List<Tuple> result = queryFactory
41+
.select(member.id,
42+
member.fcmToken,
43+
member.nickname)
44+
.from(member)
45+
.leftJoin(challenge)
46+
.on(challenge.member.id.eq(member.id)
47+
.and(challenge.status.eq(ChallengeStatus.ONGOING)))
48+
.where(member.isNotificationReceived.eq(true))
49+
.groupBy(member.id, member.fcmToken, member.nickname)
50+
.having(challenge.id.count().eq(0L))
51+
.fetch();
52+
53+
// 결과를 Map<String, NewChallengeDTO> 형태로 변환
54+
Map<String, NewChallengeDTO> resultMap = new HashMap<>();
55+
for (Tuple tuple : result) {
56+
String token = tuple.get(member.fcmToken);
57+
Long memberId = tuple.get(member.id);
58+
String nickname = tuple.get(member.nickname);
59+
60+
NewChallengeDTO dto = NewChallengeDTO.builder()
61+
.memberId(memberId)
62+
.nickname(nickname)
63+
.build();
64+
resultMap.put(token, dto);
65+
}
66+
67+
return resultMap;
68+
}
69+
70+
/**
71+
* 현재 시각 기준 달성할 챌린지가 있는 회원 token, 닉네임, 챌린지 제목 리스트 조회
72+
*
73+
* @param day
74+
* @return
75+
*/
76+
public Map<String, AchieveChallengeDTO> getAchieveTargetsAndChallenge(LocalDate day) {
77+
// status=ONGOING -> 진행중
78+
// 해당 챌린지의 마지막 기록이 없거나 isSucceed=false -> 달성 가능
79+
// 그 챌린지의 title, member.fcmToken, member.nickname 조회
80+
List<Tuple> result = queryFactory
81+
.select(member.id,
82+
member.fcmToken,
83+
member.nickname,
84+
challenge.title)
85+
.from(challenge)
86+
.join(member).on(challenge.member.id.eq(member.id))
87+
.where(challenge.status.eq(ChallengeStatus.ONGOING),
88+
lastRecordSucceed(day).eq(false),
89+
member.isNotificationReceived.eq(true))
90+
.fetch();
91+
92+
// 결과를 Map<String, AchieveChallengeDTO> 형태로 변환
93+
Map<String, AchieveChallengeDTO> resultMap = new HashMap<>();
94+
for (Tuple tuple : result) {
95+
String token = tuple.get(member.fcmToken);
96+
Long memberId = tuple.get(member.id);
97+
String nickname = tuple.get(member.nickname);
98+
String title = tuple.get(challenge.title);
99+
100+
AchieveChallengeDTO dto = resultMap.getOrDefault(
101+
token,
102+
AchieveChallengeDTO.builder()
103+
.memberId(memberId)
104+
.nickname(nickname)
105+
.challengeTitles(new ArrayList<>())
106+
.build());
107+
108+
dto.getChallengeTitles().add(title);
109+
resultMap.put(token, dto);
110+
}
111+
112+
return resultMap;
113+
}
114+
115+
/**
116+
* 해당 일자의 마지막 ChallengeRecord.isSucceeds를 반환, ChallengeRecord가 없는 경우 false를 반환
117+
*
118+
* @param day 일자
119+
* @return
120+
*/
121+
private BooleanExpression lastRecordSucceed(LocalDate day) {
122+
// 가징 최신 challengeRecord의 createdAt 조회
123+
Expression<LocalDateTime> maxCreatedAtSubquery =
124+
JPAExpressions.select(challengeRecord.createdAt.max())
125+
.from(challengeRecord)
126+
.where(challengeRecord.challenge.id.eq(challenge.id),
127+
challengeRecord.recordDate.eq(day));
128+
129+
// 가장 최신 challengeRecord의 isSucceed 값 조회
130+
JPQLQuery<Boolean> lastIsSucceedQuery =
131+
JPAExpressions.select(challengeRecord.isSucceed)
132+
.from(challengeRecord)
133+
.where(
134+
challengeRecord.challenge.id.eq(challenge.id),
135+
challengeRecord.recordDate.eq(day),
136+
challengeRecord.createdAt.eq(maxCreatedAtSubquery) // 가장 최신 createdAt
137+
);
138+
139+
// null을 false로 처리
140+
return Expressions.booleanTemplate(
141+
"COALESCE(({0}), FALSE)",
142+
lastIsSucceedQuery
143+
);
144+
}
145+
146+
}

0 commit comments

Comments
 (0)