Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SAMBAD-271] 알림 목록 조회 및 알림 메시지 템플릿 기능 추가 #122

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading