Skip to content

Commit

Permalink
[SAMBAD-271] 알림 목록 조회 및 알림 메시지 템플릿 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
kkjsw17 committed Aug 21, 2024
1 parent 039dbd3 commit 14f5591
Show file tree
Hide file tree
Showing 20 changed files with 407 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.depromeet.sambad.moring.common.config;

import java.util.List;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@EnableCaching
@Configuration
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
cacheManager.setAllowNullValues(false);
cacheManager.setCacheNames(List.of("eventTemplates"));
return cacheManager;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class ExecutionLoggingAdvice {
+ "!execution(* org.depromeet.sambad.moring.*.infrastructure.*Properties.*(..)) && "
+ "!execution(* org.depromeet.sambad.moring.*.*.infrastructure.*Properties.*(..)) && "
+ "!execution(* org.depromeet.sambad.moring.common..*(..)) && "
+ "!execution(* org.depromeet.sambad.moring.*.*.annotation..*(..))"
+ "!execution(* org.depromeet.sambad.moring.*.*.annotation..*(..)) && "
+ "!@annotation(org.depromeet.sambad.moring.common.logging.NoLogging)"
)
private void logPointcut() {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.depromeet.sambad.moring.common.logging;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 메서드에 부착 시, ExecutionLoggingAdvice 내 로깅 대상에서 제외합니다.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogging {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.depromeet.sambad.moring.event.application;

import java.util.Optional;

import org.depromeet.sambad.moring.event.domain.EventMessageTemplate;
import org.depromeet.sambad.moring.event.domain.EventType;

public interface EventMessageTemplateRepository {

Optional<EventMessageTemplate> findByType(EventType type);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.depromeet.sambad.moring.event.application;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand All @@ -19,4 +20,7 @@ Optional<Event> findFirstByUserIdAndMeetingIdAndStatusAndType(
);

List<Event> findByMeetingIdAndStatusAndType(Long meetingId, EventStatus eventStatus, EventType eventType);

List<Event> findByUserIdAndMeetingIdAndCreatedAtAfterOrderByCreatedAtDesc(
Long userId, Long meetingId, LocalDateTime keepDays);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import static org.depromeet.sambad.moring.event.domain.EventStatus.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

import org.depromeet.sambad.moring.event.domain.Event;
import org.depromeet.sambad.moring.event.domain.EventType;
import org.depromeet.sambad.moring.event.infrastructure.EventProperties;
import org.depromeet.sambad.moring.event.presentation.excepiton.NotFoundEventException;
import org.depromeet.sambad.moring.event.presentation.response.PollingEventListResponse;
import org.depromeet.sambad.moring.meeting.member.domain.MeetingMemberValidator;
Expand All @@ -18,35 +21,53 @@
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class EventService {

private final EventRepository eventRepository;
private final EventMessageTemplateRepository eventMessageTemplateRepository;
private final MeetingMemberValidator meetingMemberValidator;

@Transactional
private final EventProperties eventProperties;

public void publish(Long userId, Long meetingId, EventType type) {
this.publish(userId, meetingId, type, Map.of(), Map.of());
}

public void publish(
Long userId, Long meetingId, EventType type, Map<String, String> contentsMap, Map<String, Object> additionalData
) {
if (meetingMemberValidator.isNotUserOfMeeting(userId, meetingId)) {
log.warn("User is not member of meeting. userId: {}, meetingId: {}", userId, meetingId);
return;
}

Event event = Event.publish(userId, meetingId, type);
String message = constructEventMessage(type, contentsMap);

Event event = Event.publish(userId, meetingId, type, message, additionalData);
eventRepository.save(event);
}

@Transactional
public void inactivate(Long eventId) {
Event event = getEventById(eventId);
event.inactivate();
eventRepository.save(event);
}

@Transactional
@Transactional(readOnly = true)
public List<Event> getEvents(Long userId, Long meetingId) {
meetingMemberValidator.validateUserIsMemberOfMeeting(userId, meetingId);
LocalDateTime keepDays = LocalDateTime.now().minusDays(eventProperties.keepDays());

return eventRepository.findByUserIdAndMeetingIdAndCreatedAtAfterOrderByCreatedAtDesc(
userId, meetingId, keepDays);
}

public void inactivateLastEventByType(Long userId, Long meetingId, EventType type) {
eventRepository.findFirstByUserIdAndMeetingIdAndStatusAndType(userId, meetingId, ACTIVE, type)
.ifPresent(Event::inactivate);
}

@Transactional
public PollingEventListResponse getActiveEvents(Long userId, Long meetingId) {
meetingMemberValidator.validateUserIsMemberOfMeeting(userId, meetingId);
List<Event> events = eventRepository.findByUserIdAndMeetingIdAndStatus(userId, meetingId, ACTIVE);
Expand All @@ -59,12 +80,21 @@ public PollingEventListResponse getActiveEvents(Long userId, Long meetingId) {
return PollingEventListResponse.from(notExpiredEvents);
}

@Transactional
public void inactivateLastEventsOfAllMemberByType(Long meetingId, EventType eventType) {
List<Event> events = eventRepository.findByMeetingIdAndStatusAndType(meetingId, ACTIVE, eventType);
events.forEach(Event::inactivate);
}

private String constructEventMessage(EventType type, Map<String, String> contentsMap) {
if (contentsMap.isEmpty()) {
return null;
}

return eventMessageTemplateRepository.findByType(type)
.map(template -> template.replaceTemplateVariables(contentsMap))
.orElse(null);
}

private Event getEventById(Long eventId) {
return eventRepository.findById(eventId)
.orElseThrow(NotFoundEventException::new);
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/org/depromeet/sambad/moring/event/domain/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import static org.depromeet.sambad.moring.meeting.question.domain.MeetingQuestion.*;

import java.time.LocalDateTime;
import java.util.Map;

import org.depromeet.sambad.moring.common.domain.BaseTimeEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -38,19 +40,32 @@ public class Event extends BaseTimeEntity {
@Enumerated(STRING)
private EventStatus status;

private String message;

private LocalDateTime expiredAt;

private Event(Long userId, Long meetingId, EventType type, EventStatus status, LocalDateTime expiredAt) {
@Column(columnDefinition = "text")
@Convert(converter = MapToJsonConverter.class)
private Map<String, Object> additionalData = Map.of();

private Event(
Long userId, Long meetingId, EventType type, EventStatus status, String message, LocalDateTime expiredAt,
Map<String, Object> additionalData
) {
this.userId = userId;
this.meetingId = meetingId;
this.type = type;
this.status = status;
this.message = message;
this.expiredAt = expiredAt;
this.additionalData = additionalData;
}

public static Event publish(Long userId, Long meetingId, EventType type) {
public static Event publish(
Long userId, Long meetingId, EventType type, String message, Map<String, Object> additionalData
) {
LocalDateTime expiredAt = LocalDateTime.now().plusSeconds(RESPONSE_TIME_LIMIT_SECONDS);
return new Event(userId, meetingId, type, ACTIVE, expiredAt);
return new Event(userId, meetingId, type, ACTIVE, message, expiredAt, additionalData);
}

public void inactivate() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.depromeet.sambad.moring.event.domain;

import static jakarta.persistence.EnumType.*;
import static jakarta.persistence.GenerationType.*;
import static lombok.AccessLevel.*;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = PROTECTED)
public class EventMessageTemplate {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "event_template_id")
private Long id;

@Enumerated(STRING)
@Column(columnDefinition = "varchar(50)")
private EventType type;

private String template;

public String replaceTemplateVariables(Map<String, String> contentsMap) {
String message = template;
for (String key : contentsMap.keySet()) {
String regex = Pattern.quote("#{" + key + "}");
message = message.replaceAll(regex, Matcher.quoteReplacement(contentsMap.get(key)));
}
return message;
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
package org.depromeet.sambad.moring.event.domain;

import java.util.HashSet;
import java.util.Set;

import org.depromeet.sambad.moring.meeting.member.domain.MeetingMember;
import org.depromeet.sambad.moring.meeting.question.domain.MeetingQuestion;

public enum EventType {
QUESTION_REGISTERED,
TARGET_MEMBER,
HAND_WAVING_REQUESTED,
;

public static Set<EventType> of(MeetingQuestion meetingQuestion, MeetingMember loginMember) {
Set<EventType> eventTypes = new HashSet<>();
if (meetingQuestion.getQuestion() != null) {
eventTypes.add(EventType.QUESTION_REGISTERED);
}
if (meetingQuestion.getTargetMember().equals(loginMember)) {
eventTypes.add(EventType.TARGET_MEMBER);
}
return eventTypes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.depromeet.sambad.moring.event.domain;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class MapToJsonConverter implements AttributeConverter<Map<String, Object>, String> {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public String convertToDatabaseColumn(Map<String, Object> attribute) {
if (attribute == null) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Could not convert map to JSON", e);
}
}

@Override
public Map<String, Object> convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
try {
return objectMapper.readValue(dbData, HashMap.class);
} catch (IOException e) {
throw new IllegalArgumentException("Could not convert JSON to map", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.depromeet.sambad.moring.event.infrastructure;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand All @@ -16,4 +17,7 @@ Optional<Event> findFirstByUserIdAndMeetingIdAndStatusAndTypeOrderByIdDesc(
);

List<Event> findByMeetingIdAndStatusAndType(Long meetingId, EventStatus eventStatus, EventType eventType);

List<Event> findByUserIdAndMeetingIdAndCreatedAtAfterOrderByCreatedAtDesc(
Long userId, Long meetingId, LocalDateTime keepDays);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.depromeet.sambad.moring.event.infrastructure;

import java.util.Optional;

import org.depromeet.sambad.moring.event.domain.EventMessageTemplate;
import org.depromeet.sambad.moring.event.domain.EventType;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EventMessageTemplateJpaRepository extends JpaRepository<EventMessageTemplate, Long> {

Optional<EventMessageTemplate> findByType(EventType type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.depromeet.sambad.moring.event.infrastructure;

import java.util.Optional;

import org.depromeet.sambad.moring.common.logging.NoLogging;
import org.depromeet.sambad.moring.event.application.EventMessageTemplateRepository;
import org.depromeet.sambad.moring.event.domain.EventMessageTemplate;
import org.depromeet.sambad.moring.event.domain.EventType;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Repository
public class EventMessageTemplateRepositoryImpl implements EventMessageTemplateRepository {

private final EventMessageTemplateJpaRepository eventMessageTemplateJpaRepository;

@Override
@Cacheable(value = "eventTemplates", key = "#type", unless = "#result == null")
public Optional<EventMessageTemplate> findByType(EventType type) {
return eventMessageTemplateJpaRepository.findByType(type);
}

@NoLogging
@CacheEvict(value = "eventTemplates", allEntries = true)
@Scheduled(fixedRateString = "${caching.spring.event-templates-ttl}")
public void evictAllCaches() {
}
}
Loading

0 comments on commit 14f5591

Please sign in to comment.