Skip to content

Commit

Permalink
Merge pull request #312 from JNU-econovation/feat/#294
Browse files Browse the repository at this point in the history
[BE/FEAT] 회원수정 7일동안 안할 시 알림 보내기 기능
  • Loading branch information
hwangdaesun committed Jun 8, 2024
2 parents 1b7a31e + bdccbd6 commit 28a469b
Show file tree
Hide file tree
Showing 24 changed files with 665 additions and 7 deletions.
3 changes: 3 additions & 0 deletions BE/exceed/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ dependencies {

// Prometheus
implementation 'io.micrometer:micrometer-registry-prometheus'

// quartz
implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2'
}

tasks.named('bootBuildImage') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,18 @@ CREATE TABLE `HISTORY_TB`
`MEMBER_FK` bigint(20) DEFAULT NULL,
PRIMARY KEY (`HISTORY_PK`),
FOREIGN KEY (`MEMBER_FK`) REFERENCES `MEMBER_TB` (`MEMBER_PK`)
) ENGINE=InnoDB;
) ENGINE=InnoDB;

CREATE TABLE `NOTIFY_TB`
(
`NOTIFY_PK` bigint(20) NOT NULL AUTO_INCREMENT,
`CREATED_DATE` datetime(6) NOT NULL,
`UPDATED_DATE` datetime(6) NOT NULL,
`NOTIFY_URL` varchar(255) NOT NULL,
`NOTIFY_IS_READ` bit(1) NOT NULL,
`NOTIFY_CONTENT` varchar(255) NOT NULL,
`NOTIFY_TYPE` varchar(255) NOT NULL,
`MEMBER_FK` bigint(20) DEFAULT NULL,
PRIMARY KEY (`NOTIFY_PK`),
FOREIGN KEY (`MEMBER_FK`) REFERENCES `MEMBER_TB` (`MEMBER_PK`)
) ENGINE=InnoDB;
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,18 @@ CREATE TABLE `HISTORY_TB`
`MEMBER_FK` bigint(20) DEFAULT NULL,
PRIMARY KEY (`HISTORY_PK`),
FOREIGN KEY (`MEMBER_FK`) REFERENCES `MEMBER_TB` (`MEMBER_PK`)
) ENGINE=InnoDB;
) ENGINE=InnoDB;

CREATE TABLE `NOTIFY_TB`
(
`NOTIFY_PK` bigint(20) NOT NULL AUTO_INCREMENT,
`CREATED_DATE` datetime(6) NOT NULL,
`UPDATED_DATE` datetime(6) NOT NULL,
`NOTIFY_URL` varchar(255) NOT NULL,
`NOTIFY_IS_READ` bit(1) NOT NULL,
`NOTIFY_CONTENT` varchar(255) NOT NULL,
`NOTIFY_TYPE` varchar(255) NOT NULL,
`MEMBER_FK` bigint(20) DEFAULT NULL,
PRIMARY KEY (`NOTIFY_PK`),
FOREIGN KEY (`MEMBER_FK`) REFERENCES `MEMBER_TB` (`MEMBER_PK`)
) ENGINE=InnoDB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.gaebaljip.exceed.common.event;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class UpdateWeightEvent extends InfraEvent {
private Long memberId;
private String url;
private LocalDateTime localDateTime;

public static UpdateWeightEvent from(Long memberId, String url, LocalDateTime localDateTime) {
return new UpdateWeightEvent(memberId, url, localDateTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.gaebaljip.exceed.common.event.DeleteMemberEvent;
import com.gaebaljip.exceed.food.adapter.out.FoodEntity;
import com.gaebaljip.exceed.food.application.port.out.FoodPort;
import com.gaebaljip.exceed.infrastructure.sse.adapter.out.NotifyEntity;
import com.gaebaljip.exceed.infrastructure.sse.application.port.out.NotifyPort;
import com.gaebaljip.exceed.meal.adapter.out.MealEntity;
import com.gaebaljip.exceed.meal.adapter.out.MealFoodEntity;
import com.gaebaljip.exceed.meal.application.port.out.MealFoodPort;
Expand All @@ -26,6 +28,7 @@ public class DeleteMemberEventListener {
private final FoodPort foodPort;
private final MealPort mealPort;
private final MealFoodPort mealFoodPort;
private final NotifyPort notifyPort;

@EventListener(classes = DeleteMemberEvent.class)
@Transactional
Expand All @@ -34,6 +37,12 @@ public void handle(DeleteMemberEvent event) {
mealPort.deleteByAllByIdInQuery(findMIdsByMemberEntity(event.getMemberEntity()));
foodPort.deleteByAllByIdInQuery(findFIdsByMemberEntity(event.getMemberEntity()));
historyPort.deleteByAllByIdInQuery(findHIdsByMemberEntity(event.getMemberEntity()));
notifyPort.deleteByAllByIdInQuery(findNIdsByMemberEntity(event.getMemberEntity()));
}

private List<Long> findNIdsByMemberEntity(MemberEntity memberEntity) {
List<NotifyEntity> notifyEntities = notifyPort.findByMemberEntity(memberEntity);
return notifyEntities.stream().map(NotifyEntity::getId).toList();
}

private List<Long> findMfIdsByMemberEntity(MemberEntity memberEntity) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.gaebaljip.exceed.infrastructure.quartz;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import com.gaebaljip.exceed.common.event.UpdateWeightEvent;
import com.gaebaljip.exceed.infrastructure.quartz.job.UpdateWeightEmitterJob;
import com.gaebaljip.exceed.infrastructure.sse.adapter.out.NotificationType;

import lombok.extern.log4j.Log4j2;

@Component
@Log4j2
public class UpdateWeightEventListener {
@TransactionalEventListener(
classes = UpdateWeightEvent.class,
phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
public void handle(UpdateWeightEvent event) {
Long memberId = event.getMemberId();
String url = event.getUrl();
JobDetail jobDetail = createJobDetail(memberId, url);
Trigger trigger = createTrigger(event, jobDetail);
createSchedule(jobDetail, trigger);
}

private void createSchedule(JobDetail jobDetail, Trigger trigger) {
try {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
log.info(">>>>>>>>>>>>>>>>> SSE 전송 스케줄링 시작");
scheduler.scheduleJob(jobDetail, trigger);
log.info(">>>>>>>>>>>>>>>>> SSE 전송 스케줄링 등록");
} catch (SchedulerException e) {
log.error(">>>>>>>>>>>>>>>>> SSE 전송 스케줄링 실패");
}
}

private JobDetail createJobDetail(Long memberId, String url) {
String content = "몸무게 수정하신지 7일이 지났습니다. 몸무게를 수정해주세요";
NotificationType type = NotificationType.NOTICE;
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("memberId", memberId);
jobDataMap.put("url", url);
jobDataMap.put("content", content);
jobDataMap.put("type", type);
return JobBuilder.newJob(UpdateWeightEmitterJob.class)
.withIdentity("UPDATE_WEIGHT_JOB", "group1")
.setJobData(jobDataMap)
.build();
}

private Trigger createTrigger(UpdateWeightEvent event, JobDetail jobDetail) {
LocalDateTime triggerDateTime = event.getLocalDateTime().plusDays(7);
Date triggerDate = Date.from(triggerDateTime.atZone(ZoneId.of("Asia/Seoul")).toInstant());
TriggerBuilder<Trigger> triggerTriggerBuilder = TriggerBuilder.newTrigger();
triggerTriggerBuilder.withIdentity("UPDATE_WEIGHT_TRIGGER", "group1");
triggerTriggerBuilder.startAt(triggerDate);
triggerTriggerBuilder.forJob(jobDetail);
return triggerTriggerBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.gaebaljip.exceed.infrastructure.quartz.job;

import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import com.gaebaljip.exceed.infrastructure.sse.adapter.out.NotificationType;
import com.gaebaljip.exceed.infrastructure.sse.application.port.in.SendEmitterUseCase;
import com.gaebaljip.exceed.member.adapter.out.persistence.MemberEntity;
import com.gaebaljip.exceed.member.application.port.out.MemberPort;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@RequiredArgsConstructor
// Application Context에 Bean 등록되있는 것을 DI 하기 위해 QuartzJobBean을 상속함
public class UpdateWeightEmitterJob extends QuartzJobBean {
private final SendEmitterUseCase sendEmitterUseCase;
private final MemberPort memberPort;

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getMergedJobDataMap();
Long memberId = jobDataMap.getLong("memberId");
MemberEntity receiver = memberPort.query(memberId);
String url = jobDataMap.getString("url");
String content = jobDataMap.getString("content");
NotificationType notificationType = (NotificationType) jobDataMap.get("type");
sendEmitterUseCase.send(receiver, content, url, notificationType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.in;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import com.gaebaljip.exceed.common.annotation.AuthenticationMemberId;
import com.gaebaljip.exceed.infrastructure.sse.application.port.in.ConnectEmitterUseCase;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@SecurityRequirement(name = "access-token")
@Tag(name = "[SSE connection]")
public class EmitterController {
private final ConnectEmitterUseCase connectEmitterUseCase;

@GetMapping(value = "/emitter/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(
@RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "")
String lastEventId,
@Parameter(hidden = true) @AuthenticationMemberId Long memberId) {
return connectEmitterUseCase.execute(memberId.toString(), lastEventId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.out;

import java.util.Map;

import org.springframework.stereotype.Repository;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Repository
public interface EmitterRepository {
SseEmitter save(String emitterId, SseEmitter sseEmitter);

void saveEventCache(String eventCachedId, Object event);

Map<String, SseEmitter> findAllEmitterStartWithByMemberId(String memberId);

Map<String, Object> findAllEventCacheStartWithByMemberId(String memberId);

void deleteById(String emitterId);

void deleteAllEmitterStartWithMemberId(String memberId);

void deleteAllEventCacheStartWithMemberId(String memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.out;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class EmitterRepositoryImpl implements EmitterRepository {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<String, Object> eventCaches = new ConcurrentHashMap<>();

@Override
public SseEmitter save(String emitterId, SseEmitter sseEmitter) {
emitters.put(emitterId, sseEmitter);
return sseEmitter;
}

@Override
public void saveEventCache(String eventCacheId, Object event) {
eventCaches.put(eventCacheId, event);
}

@Override
public Map<String, SseEmitter> findAllEmitterStartWithByMemberId(String memberId) {
return emitters.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(memberId))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public Map<String, Object> findAllEventCacheStartWithByMemberId(String memberId) {
return eventCaches.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(memberId))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public void deleteById(String emitterId) {
emitters.remove(emitterId);
}

@Override
public void deleteAllEmitterStartWithMemberId(String memberId) {
emitters.forEach(
(key, emitter) -> {
if (key.startsWith(memberId)) {
emitters.remove(key);
}
});
}

@Override
public void deleteAllEventCacheStartWithMemberId(String memberId) {
eventCaches.forEach(
(key, emitter) -> {
if (key.startsWith(memberId)) {
eventCaches.remove(key);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.out;

public enum NotificationType {
// 알림
NOTICE,
// 채팅
CHAT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.out;

import lombok.*;

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class Notify {
private String content;
private String url;
private String type;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gaebaljip.exceed.infrastructure.sse.adapter.out;

import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class NotifyConverter {
public Notify toNotify(NotifyEntity notifyEntity) {
return Notify.builder()
.content(notifyEntity.getContent())
.url(notifyEntity.getUrl())
.type(notifyEntity.getType().name())
.build();
}
}
Loading

0 comments on commit 28a469b

Please sign in to comment.