Skip to content

Commit

Permalink
[BE] 팀플레이스 공지 등록시 실시간 SSE 알림 추가 (#925)
Browse files Browse the repository at this point in the history
* refactor: remove unused field

* feat: add Notice Creation domain event

* refactor: add generic to TeamPlaceSseConverter

* feat: Implement NoticeCreatedEventConverter
  • Loading branch information
pilyang authored Feb 16, 2024
1 parent c2ab050 commit 939ff2b
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public class FeedWriteService {
private final FeedRepository feedRepository;
private final FeedThreadImageRepository feedThreadImageRepository;
private final MemberRepository memberRepository;
private final MemberTeamPlaceRepository memberTeamPlaceRepository;
private final FileStorageManager fileStorageManager;

@Value("${aws.s3.image-directory}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -27,6 +28,7 @@
import team.teamby.teambyteam.notice.application.dto.NoticeResponse;
import team.teamby.teambyteam.notice.domain.Notice;
import team.teamby.teambyteam.notice.domain.NoticeRepository;
import team.teamby.teambyteam.notice.domain.event.NoticeCreationEvent;
import team.teamby.teambyteam.notice.domain.image.NoticeImage;
import team.teamby.teambyteam.notice.domain.image.NoticeImageRepository;
import team.teamby.teambyteam.notice.domain.image.vo.ImageName;
Expand All @@ -52,6 +54,7 @@ public class NoticeService {

private final Clock clock;

private final ApplicationEventPublisher applicationEventPublisher;
private final NoticeRepository noticeRepository;
private final TeamPlaceRepository teamPlaceRepository;
private final MemberRepository memberRepository;
Expand All @@ -75,9 +78,10 @@ public Long register(final NoticeRegisterRequest noticeRegisterRequest,
final Notice savedNotice = noticeRepository.save(new Notice(contentVo, teamPlaceId, memberId.id()));
saveImages(images, savedNotice);

Long savedNoticeId = savedNotice.getId();
final Long savedNoticeId = savedNotice.getId();
log.info("공지 등록 - 등록자 이메일 : {}, 팀플레이스 아이디 : {}, 공지 아이디 : {}", memberEmailDto.email(), teamPlaceId,
savedNoticeId);
applicationEventPublisher.publishEvent(new NoticeCreationEvent(savedNotice));
return savedNoticeId;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public interface NoticeRepository extends JpaRepository<Notice, Long> {
"LIMIT 1"
)
Optional<Notice> findMostRecentByTeamPlaceId(Long teamPlaceId);

@Query("SELECT n.teamPlaceId FROM Notice n WHERE n.id = :id")
Optional<Long> findTeamPlaceIdByNoticeId(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package team.teamby.teambyteam.notice.domain.event;

import team.teamby.teambyteam.common.domain.DomainEvent;
import team.teamby.teambyteam.notice.domain.Notice;

public class NoticeCreationEvent implements DomainEvent<Long> {

public static final String NOTICE_NOT_CREATED_MESSAGE_FORMAT = "아직 생성되지 않은 공지입니다. teamplaceId: %d";
private final Long id;

public NoticeCreationEvent(final Notice notice) {
validate(notice);
this.id = notice.getId();
}

private static void validate(Notice notice) {
if(notice.getId() == null) {
throw new RuntimeException(String.format(NOTICE_NOT_CREATED_MESSAGE_FORMAT, notice.getTeamPlaceId()));
}
}

@Override
public Long getDomainId() {
return id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
@Slf4j
@Component
@RequiredArgsConstructor
public class FeedEventConverter implements TeamPlaceSseConverter {
public class FeedEventConverter implements TeamPlaceSseConverter<Long> {

private final FeedRepository feedRepository;
private final MemberTeamPlaceRepository memberTeamPlaceRepository;

@Override
@Transactional(readOnly = true)
public TeamPlaceSseEvent convert(final DomainEvent event) {
final Long feedId = (Long) event.getDomainId();
public TeamPlaceSseEvent convert(final DomainEvent<Long> event) {
final Long feedId = event.getDomainId();
final Feed feed = feedRepository.findById(feedId)
.orElseThrow(() -> {
final String message = "No FeedFound ID : " + feedId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package team.teamby.teambyteam.sse.domain.converter;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import team.teamby.teambyteam.common.domain.DomainEvent;
import team.teamby.teambyteam.notice.domain.NoticeRepository;
import team.teamby.teambyteam.notice.domain.event.NoticeCreationEvent;
import team.teamby.teambyteam.sse.domain.TeamPlaceSseEvent;
import team.teamby.teambyteam.sse.domain.emitter.TeamPlaceEmitterId;

@Component
@RequiredArgsConstructor
public class NoticeCreatedEventConverter implements TeamPlaceSseConverter<Long> {

private final NoticeRepository noticeRepository;

@Override
public TeamPlaceSseEvent convert(DomainEvent<Long> event) {
final Long noticeId = event.getDomainId();
final Long teamPlaceId = noticeRepository.findTeamPlaceIdByNoticeId(noticeId)
.orElseThrow(() -> new RuntimeException(String.format("팀플레이스 공지를 찾을 수 없습니다. id : %d", noticeId)));
return new NoticeCreatedSse(noticeId, teamPlaceId);
}

@Override
public String supportEventName() {
return NoticeCreationEvent.class.getName();
}

private static class NoticeCreatedSse implements TeamPlaceSseEvent {

private static final String EVENT_NAME = "new_notice";

private final NoticeSse event;

public NoticeCreatedSse(final Long id, final Long teamPlaceId) {
this.event = new NoticeSse(id, teamPlaceId);
}

@Override
public Long getTeamPlaceId() {
return event.teamPlaceId;
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public Object getEvent(TeamPlaceEmitterId emitterId) {
return event;
}

private record NoticeSse(Long id, Long teamPlaceId) {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import team.teamby.teambyteam.common.domain.DomainEvent;
import team.teamby.teambyteam.sse.domain.TeamPlaceSseEvent;

public interface TeamPlaceSseConverter {
TeamPlaceSseEvent convert(DomainEvent event);
public interface TeamPlaceSseConverter<T> {
TeamPlaceSseEvent convert(DomainEvent<T> event);

String supportEventName();
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import team.teamby.teambyteam.notice.application.dto.NoticeResponse;
import team.teamby.teambyteam.notice.domain.Notice;
import team.teamby.teambyteam.notice.domain.NoticeRepository;
import team.teamby.teambyteam.notice.domain.event.NoticeCreationEvent;
import team.teamby.teambyteam.teamplace.domain.TeamPlace;
import team.teamby.teambyteam.teamplace.exception.TeamPlaceException.NotFoundException;

Expand Down Expand Up @@ -93,6 +94,23 @@ void success() {
assertThat(registeredId).isNotNull();
}

@Test
@DisplayName("공지 등록 이벤트를 발행한다")
void publishCreatedEvent() {
// given

// when
final Long registeredId = noticeService.register(request, teamPlace.getId(), memberEmailDto);

// then
final Optional<NoticeCreationEvent> event = applicationEvents.stream(NoticeCreationEvent.class).findAny();
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(event).isNotEmpty();
softly.assertThat(event.get().getDomainId()).isEqualTo(registeredId);
});

}

@Test
@DisplayName("공지 등록 시 팀 플레이스 ID에 해당하는 팀 플레이스가 존재하지 않으면 예외가 발생한다.")
void failTeamPlaceNotExistById() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ public TeamPlaceSseConverter testConvertor() {
return new TestSseConverter();
}

public static class TestSseConverter implements TeamPlaceSseConverter {
public static class TestSseConverter implements TeamPlaceSseConverter<Long> {

@Override
public TeamPlaceSseEvent convert(DomainEvent event) {
public TeamPlaceSseEvent convert(DomainEvent<Long> event) {
return ((TestDomainEvent) event).getTestSseEvent();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package team.teamby.teambyteam.sse.domain.converter;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import team.teamby.teambyteam.common.builder.TestFixtureBuilder;
import team.teamby.teambyteam.common.fixtures.MemberFixtures;
import team.teamby.teambyteam.common.fixtures.NoticeFixtures;
import team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures;
import team.teamby.teambyteam.member.domain.Member;
import team.teamby.teambyteam.notice.domain.Notice;
import team.teamby.teambyteam.notice.domain.event.NoticeCreationEvent;
import team.teamby.teambyteam.sse.domain.TeamPlaceSseEvent;
import team.teamby.teambyteam.teamplace.domain.TeamPlace;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Sql({"/h2-truncate.sql"})
class NoticeCreatedEventConverterTest {

@Autowired
private NoticeCreatedEventConverter noticeCreatedEventConverter;

@Autowired
private TestFixtureBuilder testFixtureBuilder;

@Test
@DisplayName("NoticeCreatedEvent 지원 확인")
void isSupportNoticeCreatedEvent() {
// given
final String expected = NoticeCreationEvent.class.getName();

// when
final String actual = noticeCreatedEventConverter.supportEventName();

// then
assertThat(actual).isEqualTo(expected);
}

@Test
@DisplayName("Notice 이벤트 변환 테스트")
void convert() {
// given
final Member member = testFixtureBuilder.buildMember(MemberFixtures.ROY());
final TeamPlace teamPlace = testFixtureBuilder.buildTeamPlace(TeamPlaceFixtures.CONTROLS_TEAM_PLACE());
testFixtureBuilder.buildMemberTeamPlace(member, teamPlace);
final Notice createdNotice = testFixtureBuilder.buildNotice(NoticeFixtures.NOTICE_1ST(teamPlace.getId(), member.getId()));

final NoticeCreationEvent noticeCreationEvent = new NoticeCreationEvent(createdNotice);

// when
final TeamPlaceSseEvent convertedEvent = noticeCreatedEventConverter.convert(noticeCreationEvent);

// then
assertThat(convertedEvent.getTeamPlaceId()).isEqualTo(teamPlace.getId());

}
}

0 comments on commit 939ff2b

Please sign in to comment.