Skip to content

Commit ae35ff7

Browse files
authored
feat: 약 복용 알림 기능 추가 (#176)
1 parent 8ddd03f commit ae35ff7

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed

backend/ongi/src/main/java/ongi/pill/repository/PillIntakeRecordRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@
1414
public interface PillIntakeRecordRepository extends JpaRepository<PillIntakeRecord, Long> {
1515

1616
List<PillIntakeRecord> findByPillInAndIntakeDate(List<Pill> pills, LocalDate intakeDate);
17+
List<PillIntakeRecord> findByIntakeDate(LocalDate intakeDate);
1718
Optional<PillIntakeRecord> findByPillAndIntakeDateAndIntakeTime(Pill pill, LocalDate intakeDate, LocalTime intakeTime);
19+
20+
void deleteByPill(Pill pill);
1821
}

backend/ongi/src/main/java/ongi/pill/service/PillService.java

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
package ongi.pill.service;
22

3+
import com.google.firebase.messaging.AndroidConfig;
4+
import com.google.firebase.messaging.AndroidNotification;
5+
import com.google.firebase.messaging.ApnsConfig;
6+
import com.google.firebase.messaging.Aps;
7+
import com.google.firebase.messaging.FirebaseMessaging;
8+
import com.google.firebase.messaging.FirebaseMessagingException;
9+
import com.google.firebase.messaging.Message;
10+
import com.google.firebase.messaging.Notification;
311
import java.net.URL;
412
import java.time.LocalDate;
13+
import java.time.LocalDateTime;
14+
import java.util.Optional;
515
import java.util.UUID;
616
import lombok.RequiredArgsConstructor;
717
import ongi.exception.EntityNotFoundException;
@@ -17,8 +27,11 @@
1727
import ongi.pill.repository.PillIntakeRecordRepository;
1828
import ongi.pill.repository.PillRepository;
1929
import ongi.user.entity.User;
30+
import ongi.user.entity.UserFcmToken;
31+
import ongi.user.repository.UserFcmTokenRepository;
2032
import ongi.user.repository.UserRepository;
2133
import ongi.util.S3FileService;
34+
import org.springframework.scheduling.annotation.Scheduled;
2235
import org.springframework.stereotype.Service;
2336
import org.springframework.transaction.annotation.Transactional;
2437

@@ -38,6 +51,7 @@ public class PillService {
3851
private final S3FileService s3FileService;
3952

4053
private static final String DIR_NAME = "pill-photos";
54+
private final UserFcmTokenRepository userFcmTokenRepository;
4155

4256
@Transactional
4357
public PillInfo createPill(User child, PillCreateRequest request) {
@@ -55,7 +69,7 @@ public PillInfo createPill(User child, PillCreateRequest request) {
5569
throw new IllegalArgumentException("가족에 속하지 않은 사용자입니다.");
5670
}
5771

58-
if(request.fileName() != null) {
72+
if (request.fileName() != null) {
5973
if (!s3FileService.objectExists(DIR_NAME, request.fileName())) {
6074
throw new IllegalArgumentException("S3에 파일이 존재하지 않습니다.");
6175
}
@@ -80,6 +94,8 @@ public void deletePill(User user, Long pillId) {
8094
Pill pill = pillRepository.findById(pillId)
8195
.orElseThrow(() -> new EntityNotFoundException("약 정보를 찾을 수 없습니다."));
8296

97+
pillIntakeRecordRepository.deleteByPill(pill);
98+
8399
Family family = familyRepository.findByMembersContains(user.getUuid())
84100
.orElseThrow(() -> new EntityNotFoundException("가족 정보를 찾을 수 없습니다."));
85101

@@ -157,7 +173,8 @@ public List<PillInfoWithIntakeStatus> getFamilyPills(User user, UUID parentUuid,
157173

158174
return pills.stream()
159175
.map(pill -> new PillInfoWithIntakeStatus(pill,
160-
pill.getFileName() != null ? s3FileService.createSignedGetUrl(DIR_NAME, pill.getFileName()) : null,
176+
pill.getFileName() != null ? s3FileService.createSignedGetUrl(DIR_NAME,
177+
pill.getFileName()) : null,
161178
intakeRecordsMap.getOrDefault(pill.getId(), List.of())))
162179
.toList();
163180
}
@@ -168,4 +185,88 @@ public PillPresignedResponseDto getPresignedPutUrl(User user) {
168185

169186
return new PillPresignedResponseDto(signedGetUrl, fileName);
170187
}
188+
189+
@Scheduled(cron = "0 * * * * *")
190+
public void checkAndSendMedicationAlarms() {
191+
List<Pill> targets = findMedicationsNeedAlarm();
192+
193+
if (targets.isEmpty()) {
194+
return;
195+
}
196+
197+
for (Pill pill : targets) {
198+
try {
199+
sendPillAlarmNotification(pill);
200+
} catch (Exception e) {
201+
System.err.println("약 알람 발송 실패: " + pill.getName() + ", 오류: " + e.getMessage());
202+
}
203+
}
204+
}
205+
206+
private void sendPillAlarmNotification(Pill pill) throws FirebaseMessagingException {
207+
Optional<UserFcmToken> userFcmTokenOptional = userFcmTokenRepository.findByUser(pill.getOwner());
208+
if (userFcmTokenOptional.isEmpty()) {
209+
return;
210+
}
211+
212+
Message message = Message.builder()
213+
.setNotification(Notification.builder()
214+
.setTitle("'" + pill.getName() + "' 약을 복용할 시간입니다!")
215+
.setBody("복용 후 알림을 길게 눌러 복용 여부를 체크하세요.")
216+
.build())
217+
.setAndroidConfig(AndroidConfig.builder()
218+
.setTtl(3600 * 1000)
219+
.setNotification(AndroidNotification.builder()
220+
.setSound("default")
221+
.build())
222+
.build())
223+
.setApnsConfig(ApnsConfig.builder()
224+
.putHeader("apns-push-type", "alert")
225+
.putHeader("apns-priority", "10")
226+
.setAps(Aps.builder()
227+
.setCategory("PILL_TAKE_REMINDER")
228+
.setBadge(1)
229+
.setSound("default")
230+
.putCustomData("interruption-level", "time-sensitive")
231+
.build())
232+
.build())
233+
.setToken(userFcmTokenOptional.get().getToken())
234+
.build();
235+
236+
FirebaseMessaging.getInstance().send(message);
237+
}
238+
239+
private List<Pill> findMedicationsNeedAlarm() {
240+
LocalDateTime now = LocalDateTime.now();
241+
LocalDate today = now.toLocalDate();
242+
243+
List<Pill> allPills = pillRepository.findAll();
244+
List<PillIntakeRecord> todayRecords = pillIntakeRecordRepository.findByIntakeDate(today);
245+
Map<Long, List<PillIntakeRecord>> todayRecordsMap = todayRecords.stream()
246+
.collect(Collectors.groupingBy(record -> record.getPill().getId()));
247+
248+
return allPills.stream()
249+
.filter(pill -> {
250+
if (!pill.getIntakeDays().contains(today.getDayOfWeek())) {
251+
return false;
252+
}
253+
254+
List<PillIntakeRecord> pillTodayRecords = todayRecordsMap.getOrDefault(
255+
pill.getId(), List.of());
256+
257+
return pill.getIntakeTimes().stream()
258+
.anyMatch(intakeTime -> {
259+
boolean isTimePassed =
260+
now.toLocalTime().isAfter(intakeTime) || now.toLocalTime()
261+
.equals(intakeTime);
262+
263+
boolean hasRecord = pillTodayRecords.stream()
264+
.anyMatch(record -> record.getIntakeTime()
265+
.equals(intakeTime));
266+
267+
return isTimePassed && !hasRecord;
268+
});
269+
})
270+
.toList();
271+
}
171272
}

0 commit comments

Comments
 (0)