11package 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 ;
311import java .net .URL ;
412import java .time .LocalDate ;
13+ import java .time .LocalDateTime ;
14+ import java .util .Optional ;
515import java .util .UUID ;
616import lombok .RequiredArgsConstructor ;
717import ongi .exception .EntityNotFoundException ;
1727import ongi .pill .repository .PillIntakeRecordRepository ;
1828import ongi .pill .repository .PillRepository ;
1929import ongi .user .entity .User ;
30+ import ongi .user .entity .UserFcmToken ;
31+ import ongi .user .repository .UserFcmTokenRepository ;
2032import ongi .user .repository .UserRepository ;
2133import ongi .util .S3FileService ;
34+ import org .springframework .scheduling .annotation .Scheduled ;
2235import org .springframework .stereotype .Service ;
2336import 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