Skip to content

Commit

Permalink
feat: 애플리케이션 재시작 시 PENDING 상태 알림 스케줄링 및 새벽 4시마다 지난 모임 삭제 기능 추가 (#410)
Browse files Browse the repository at this point in the history
* feat: 시스템 타임 존 설정 추가

* feat: 알림 테이블에 fcmTopic 컬럼 추가

* feat: 애플리케이션 시작 시 PENDING 상태 알림 스케줄링 적용

* feat: 지난 약속방 논리 삭제 스케줄링 추가

* test: 접근제어자 protected로 변경 및 getter 제거

* feat: 오늘 약속의 기한이 지난 약속 리스트 조회 기능 추가 및 이벤트 발행 기능 추가

* feat: fcm topic 구독 해제 기능 추가

* test: 테스트 설명 추가

* refactor: Device Token getter 디미터 법칙 적용

* test: Base 추상 클래스 접근 제어자 수정

* refactor: 개행 적용

* style: 벌크 쿼리 메서드명 수정

* feat: 약속 참여 시간이 지난 약속방 참여 검증 추가

* refactor: 새벽 4시 스케줄링 코드 이벤트 리스너 제거 및 트랜잭션 제거

* refactor: 머지 충돌 작업 해결

* refactor: 조회 메서드에 약속 기간이 지나지 않은 조건 추가

* feat: 기간이 지나지 않은 약속 단건 조회 메서드 추가

* refactor: findFetchedMateById() 메서드 사용 시 약속 기한이 지난 약속 처리 로직을 service에서 처리

* test: 기한이 지난 약속 조회 테스트 어떤 약속인지 명확하게 변수명 네이밍

* style: 약속방 -> 약속으로 텍스트 변경

* style: 기간이 지나지 않은 약속방 전체 Mate 조회 메서드 네이밍 수정

* test: 2가지 검증 구문을 assertAll로 래핑
  • Loading branch information
hyeon0208 committed Sep 9, 2024
1 parent c18c32e commit 0b3c379
Show file tree
Hide file tree
Showing 24 changed files with 489 additions and 74 deletions.
19 changes: 19 additions & 0 deletions backend/src/main/java/com/ody/common/config/ClockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ody.common.config;

import java.time.Clock;
import java.time.ZoneId;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ClockConfig {

@Bean
public Clock clock() {
String zoneId = "Asia/Seoul";
log.info("시스템 타임 존 설정 : {}", zoneId);
return Clock.system(ZoneId.of(zoneId));
}
}
7 changes: 6 additions & 1 deletion backend/src/main/java/com/ody/common/domain/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseEntity {

@CreatedDate
@Column(updatable = false)
@NotNull
@CreatedDate
private LocalDateTime createdAt;

@NotNull
@LastModifiedDate
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ public interface MateRepository extends JpaRepository<Mate, Long> {
select mate
from Mate mate
join fetch mate.member
where mate.meeting.id = :meetingId
where mate.meeting.id = :meetingId
and mate.meeting.overdue = false
""")
List<Mate> findAllByMeetingId(Long meetingId);
List<Mate> findAllByOverdueFalseMeetingId(Long meetingId);

@Query("""
select mate
Expand Down
22 changes: 12 additions & 10 deletions backend/src/main/java/com/ody/mate/service/MateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
import com.ody.eta.dto.request.MateEtaRequest;
import com.ody.eta.service.EtaService;
import com.ody.mate.domain.Mate;
import com.ody.mate.dto.request.MateSaveRequest;
import com.ody.mate.dto.request.MateSaveRequestV2;
import com.ody.mate.dto.request.NudgeRequest;
import com.ody.mate.dto.response.MateSaveResponse;
import com.ody.mate.dto.response.MateSaveResponseV2;
import com.ody.mate.repository.MateRepository;
import com.ody.meeting.domain.Coordinates;
import com.ody.meeting.domain.Meeting;
import com.ody.meeting.dto.response.MateEtaResponsesV2;
import com.ody.member.domain.Member;
Expand All @@ -34,15 +31,16 @@ public class MateService {
private final NotificationService notificationService;
private final RouteService routeService;


@Transactional
public MateSaveResponseV2 saveAndSendNotifications(
MateSaveRequestV2 mateSaveRequest,
Member member,
Meeting meeting
) {
public MateSaveResponseV2 saveAndSendNotifications(MateSaveRequestV2 mateSaveRequest, Member member, Meeting meeting) {
if (mateRepository.existsByMeetingIdAndMemberId(meeting.getId(), member.getId())) {
throw new OdyBadRequestException("약속에 이미 참여한 회원입니다.");
}
if (meeting.isOverdue()) {
throw new OdyBadRequestException("참여 가능한 시간이 지난 약속에 참여할 수 없습니다.");
}

Mate mate = saveMateAndEta(mateSaveRequest, member, meeting);
notificationService.saveAndSendNotifications(meeting, mate, member.getDeviceToken());
return MateSaveResponseV2.from(meeting);
Expand All @@ -60,7 +58,7 @@ private Mate saveMateAndEta(MateSaveRequestV2 mateSaveRequest, Member member, Me

public List<Mate> findAllByMeetingIdIfMate(Member member, long meetingId) {
findByMeetingIdAndMemberId(meetingId, member.getId());
return mateRepository.findAllByMeetingId(meetingId);
return mateRepository.findAllByOverdueFalseMeetingId(meetingId);
}

@Transactional
Expand All @@ -76,8 +74,12 @@ public void nudge(NudgeRequest nudgeRequest) {
}

private Mate findFetchedMate(Long mateId) {
return mateRepository.findFetchedMateById(mateId)
Mate mate = mateRepository.findFetchedMateById(mateId)
.orElseThrow(() -> new OdyBadRequestException("존재하지 않는 약속 참여자입니다."));
if (mate.getMeeting().isOverdue()) {
throw new OdyBadRequestException("기한이 지난 약속입니다.");
}
return mate;
}

private boolean canNudge(Mate mate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public interface MeetingControllerSwagger {
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않은 약속방이거나 약속방 일원이 아닌 경우",
description = "존재하지 않은 약속이거나 약속 일원이 아닌 경우",
content = @Content(schema = @Schema(implementation = ProblemDetail.class))
)
}
Expand Down
7 changes: 5 additions & 2 deletions backend/src/main/java/com/ody/meeting/domain/Meeting.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Meeting extends BaseEntity {

@Id
Expand All @@ -42,8 +42,11 @@ public class Meeting extends BaseEntity {
@NotNull
private String inviteCode;

@NotNull
private boolean overdue;

public Meeting(String name, LocalDate date, LocalTime time, Location target, String inviteCode) {
this(null, name, date, TimeUtil.trimSecondsAndNanos(time), target, inviteCode);
this(null, name, date, TimeUtil.trimSecondsAndNanos(time), target, inviteCode, false);
}

public void updateInviteCode(String inviteCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.ody.meeting.domain.Meeting;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface MeetingRepository extends JpaRepository<Meeting, Long> {
Expand All @@ -12,7 +14,17 @@ public interface MeetingRepository extends JpaRepository<Meeting, Long> {
from Meeting meeting
right join Mate mate on meeting.id = mate.meeting.id
where mate.member.id = :memberId
and meeting.overdue = false
"""
)
List<Meeting> findAllByMemberId(Long memberId);

@Modifying(clearAutomatically = true)
@Query("update Meeting m set m.overdue = true where m.overdue = false and m.date < CURRENT_DATE")
void updateAllByNotOverdueMeetings();

@Query("select m from Meeting m where m.overdue = true and CAST(m.updatedAt AS LOCALDATE) = CURRENT_DATE")
List<Meeting> findAllByUpdatedTodayAndOverdue();

Optional<Meeting> findByIdAndOverdueFalse(Long id);
}
17 changes: 15 additions & 2 deletions backend/src/main/java/com/ody/meeting/service/MeetingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@
import com.ody.meeting.dto.response.MeetingWithMatesResponse;
import com.ody.meeting.repository.MeetingRepository;
import com.ody.member.domain.Member;
import com.ody.notification.service.NotificationService;
import com.ody.util.InviteCodeGenerator;
import com.ody.util.TimeUtil;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -35,6 +39,7 @@ public class MeetingService {
private final MateService mateService;
private final MeetingRepository meetingRepository;
private final MateRepository mateRepository;
private final NotificationService notificationService;

@Transactional
public MeetingSaveResponseV1 saveV1(MeetingSaveRequestV1 meetingSaveRequestV1) {
Expand All @@ -58,7 +63,7 @@ public Meeting findByInviteCode(String inviteCode) {
}

public Meeting findById(Long meetingId) {
return meetingRepository.findById(meetingId)
return meetingRepository.findByIdAndOverdueFalse(meetingId)
.orElseThrow(() -> new OdyNotFoundException("존재하지 않는 모임입니다."));
}

Expand All @@ -75,7 +80,7 @@ public MeetingFindByMemberResponses findAllByMember(Member member) {
private MeetingFindByMemberResponse makeMeetingFindByMemberResponse(Member member, Meeting meeting) {
int mateCount = mateRepository.countByMeetingId(meeting.getId());
Mate mate = mateRepository.findByMeetingIdAndMemberId(meeting.getId(), member.getId())
.orElseThrow(() -> new OdyNotFoundException("참여하고 있지 않는 약속방입니다"));
.orElseThrow(() -> new OdyNotFoundException("참여하고 있지 않는 약속입니다"));
return MeetingFindByMemberResponse.of(meeting, mateCount, mate);
}

Expand All @@ -93,4 +98,12 @@ public MateSaveResponseV2 saveMateAndSendNotifications(MateSaveRequestV2 mateSav
}
return mateService.saveAndSendNotifications(mateSaveRequest, member, meeting);
}

@Scheduled(cron = "0 0 4 * * *", zone = "Asia/Seoul")
public void scheduleOverdueMeetings() {
meetingRepository.updateAllByNotOverdueMeetings();
List<Meeting> meetings = meetingRepository.findAllByUpdatedTodayAndOverdue();
log.info("약속 시간이 지난 약속들 overdue = true로 update 쿼리 실행");
notificationService.unSubscribeTopic(meetings);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.ody.notification.domain;

import com.ody.meeting.domain.Meeting;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class FcmTopic {

@Column(name = "fcm_topic")
private String value;

public FcmTopic(Meeting meeting) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.ody.common.domain.BaseEntity;
import com.ody.mate.domain.Mate;
import com.ody.member.domain.DeviceToken;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand Down Expand Up @@ -44,23 +46,46 @@ public class Notification extends BaseEntity {
@NotNull
private NotificationStatus status;

public Notification(Mate mate, NotificationType type, LocalDateTime sendAt, NotificationStatus status) {
this(null, mate, type, sendAt, status);
@Embedded
private FcmTopic fcmTopic;

public Notification(
Mate mate,
NotificationType type,
LocalDateTime sendAt,
NotificationStatus status,
FcmTopic fcmTopic
) {
this(null, mate, type, sendAt, status, fcmTopic);
}

public static Notification createEntry(Mate mate) {
return new Notification(mate, NotificationType.ENTRY, LocalDateTime.now(), NotificationStatus.PENDING);
return new Notification(mate, NotificationType.ENTRY, LocalDateTime.now(), NotificationStatus.PENDING, null);
}

public static Notification createDepartureReminder(Mate mate, LocalDateTime sendAt) {
return new Notification(mate, NotificationType.DEPARTURE_REMINDER, sendAt, NotificationStatus.PENDING);
public static Notification createDepartureReminder(Mate mate, LocalDateTime sendAt, FcmTopic fcmTopic) {
return new Notification(
mate,
NotificationType.DEPARTURE_REMINDER,
sendAt,
NotificationStatus.PENDING,
fcmTopic
);
}

public static Notification createNudge(Mate mate) {
return new Notification(mate, NotificationType.NUDGE, LocalDateTime.now(), NotificationStatus.PENDING);
return new Notification(mate, NotificationType.NUDGE, LocalDateTime.now(), NotificationStatus.PENDING, null);
}

public void updateStatusToDone() {
this.status = NotificationStatus.DONE;
}

public String getFcmTopicValue() {
return fcmTopic.getValue();
}

public DeviceToken getMateDeviceToken() {
return mate.getMemberDeviceToken();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.ody.notification.domain.message;

import com.google.firebase.messaging.Message;
import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.Notification;
import lombok.Getter;

Expand All @@ -10,11 +9,11 @@ public class PushMessage {

private final Message message;

public PushMessage(FcmTopic topic, Notification notification) {
public PushMessage(Notification notification) {
this.message = Message.builder()
.putData("type", notification.getType().toString())
.putData("nickname", notification.getMate().getNicknameValue())
.setTopic(topic.getValue())
.setTopic(notification.getFcmTopicValue())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.ody.notification.dto.request;

import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.Notification;

public record FcmSendRequest(FcmTopic fcmTopic, Notification notification) {
public record FcmSendRequest(Notification notification) {

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.ody.notification.repository;

import com.ody.notification.domain.Notification;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -16,4 +18,15 @@ public interface NotificationRepository extends JpaRepository<Notification, Long
order by noti.sendAt asc
""")
List<Notification> findAllMeetingLogs(Long meetingId);

List<Notification> findAllByTypeAndStatus(NotificationType type, NotificationStatus status);

@Query("""
select noti
from Notification noti
join fetch Mate mate on noti.mate.id = mate.id and mate.meeting.id = :meetingId
join fetch Member member on mate.member.id = member.id
where noti.type = :type
""")
List<Notification> findAllMeetingIdAndType(Long meetingId, NotificationType type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class FcmPushSender {
@Transactional
public void sendPushNotification(FcmSendRequest fcmSendRequest) {
Notification notification = fcmSendRequest.notification();
PushMessage pushMessage = new PushMessage(fcmSendRequest.fcmTopic(), notification);
PushMessage pushMessage = new PushMessage(notification);
sendMessage(pushMessage.getMessage(), notification);
}

Expand Down
Loading

0 comments on commit 0b3c379

Please sign in to comment.