From d8eb41a225436bd4f35715ff23c97a7d9fa81f14 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Sat, 10 Feb 2024 15:02:44 +0900 Subject: [PATCH] release: 0.3.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 리뷰 등록시 회원 매너 온도 반영 기능 추가 및 리뷰 등록 리팩토링 (#158) * refactor: 회원 리뷰 등록 API 리팩토링 (#157) * feat: 회원 리뷰 등록시, 온도 반영 및 리뷰 피드백 반영 (#157) * feat: User 엔티티 메소드 추가 (#157) * feat: Review enum 필드 추가 (#157) * test: 리뷰 등록시 온도 업데이트에 대한 단위 테스트 (#157) * test: 리뷰 등록 기능 통합 테스트 (#157) * refactor: 회원 리뷰 등록 메소드 수정 (#157) * test: 불필요한 테스트 제거 및 CI 오류 수정 (#157) * test: MeetingRepository 테스트에서 시간과 id비교 비활서화 * refactor: 회원 탈퇴 URI 변경 (#162) * refactor: 회원 탈퇴 URI 수정 (#161) * test: 회원 탈퇴 URI 수정에 대한 테스트 수정 (#161) * fix: CI 에러 수정 (#161) * fix: 필드 값 비교를 위해 deprecated 된 메소드을 대체 (#161) * fix: 필드 값 비교를 위해 deprecated 된 메소드을 대체 (#161) * feat: fcm token 업데이트 api 추가 (#166) * fix: user_alert ddl의 pk에 auto_increment를 추가한다 * feat: token 업데이트 api를 추가한다 * fix: 모임 수정 시 이미지가 추가되는 버그 수정 (#169) * perf: upgrade GPT-3.5 to GPT-4 (#174) * refactor: 위치 기반 데이터 값 유효기간 변경 * refactor: 위치 기반 데이터 값 유효기간 변경 * refactor: 위치 기반 데이터 값 유효기간 변경 * feat: 모임 종료, 사용자 추천 알림을 추가 (#182) * feat: Alert Entity를 추가하고, 알림 전송 기능이 Entity를 파라미터로 받아 동작하게 수정한다 * refactor: MeetingAlerted를 BeforeMeetingAlerted로 직관적으로 변경한다 * feat: 모임 종료 알림을 추가한다 * feat: 유저가 추천받았을때 알림 전송 기능을 추가한다 * feat: 유저가 한달 동안 받은 알람을 조회하는 기능 개발 (#185) * fix: flyway schema에 _이 하나밖에 없는 버그를 수정한다 * feat: 유저가 한달동안 받은 알람을 조회하는 기능을 개발한다 * refactor: AlertRepository의 finalAll 메소드를 findAllByCreatedAt으로 변경한다 * feat: 북마크 추가, 취소 기능 구현 (#184) * chore: 북마크 스키마 추가 * feat: 북마크 추가, 삭제 기능 * test: 북마크 추가, 삭제 테스트 작성 - 북마크 추가 케이스 - 북마크 취소 케이스 - 모임 단일 응답에 북마크 여부 추 * feat: bookmarked -> isBookmarked, V1__ 언더바 누락 수정 --------- Co-authored-by: ChoiDongKuen Co-authored-by: ddingmin --- .../net/teumteum/alert/app/AlertHandler.java | 84 ++++++++++++++++ .../alert/app/BeforeMeetingAlertHandler.java | 33 ------- .../alert/controller/AlertController.java | 15 ++- .../java/net/teumteum/alert/domain/Alert.java | 40 ++++++++ .../teumteum/alert/domain/AlertPublisher.java | 6 +- .../alert/domain/AlertRepository.java | 17 +--- .../teumteum/alert/domain/AlertService.java | 35 +++---- .../net/teumteum/alert/domain/AlertType.java | 9 ++ .../net/teumteum/alert/domain/Alertable.java | 12 --- .../alert/domain/BeforeMeetingAlert.java | 25 ----- .../alert/domain/UserAlertRepository.java | 22 +++++ .../alert/domain/UserAlertService.java | 40 ++++++++ .../alert/domain/response/AlertsResponse.java | 25 +++++ .../alert/infra/FcmAlertPublisher.java | 43 +++++---- .../meeting/controller/MeetingController.java | 17 +++- ...Alerted.java => BeforeMeetingAlerted.java} | 2 +- .../meeting/domain/EndMeetingAlerted.java | 11 +++ .../net/teumteum/meeting/domain/Meeting.java | 16 ++++ .../meeting/domain/MeetingRepository.java | 2 +- .../domain/request/UpdateMeetingRequest.java | 4 +- .../domain/response/MeetingResponse.java | 9 +- .../service/MeetingAlertPublisher.java | 24 ++++- .../meeting/service/MeetingService.java | 32 ++++++- .../net/teumteum/user/UserRecommended.java | 8 ++ .../db/migration/V11__create_alert.sql | 10 ++ .../db/migration/V12__create_bookmark.sql | 6 ++ ...Test.java => UserAlertRepositoryTest.java} | 6 +- .../java/net/teumteum/integration/Api.java | 14 +++ .../integration/MeetingIntegrationTest.java | 96 ++++++++++++++++++- .../meeting/domain/MeetingFixture.java | 6 +- .../meeting/domain/MeetingRepositoryTest.java | 2 +- src/test/resources/schema.sql | 18 ++++ 32 files changed, 541 insertions(+), 148 deletions(-) create mode 100644 src/main/java/net/teumteum/alert/app/AlertHandler.java delete mode 100644 src/main/java/net/teumteum/alert/app/BeforeMeetingAlertHandler.java create mode 100644 src/main/java/net/teumteum/alert/domain/Alert.java create mode 100644 src/main/java/net/teumteum/alert/domain/AlertType.java delete mode 100644 src/main/java/net/teumteum/alert/domain/Alertable.java delete mode 100644 src/main/java/net/teumteum/alert/domain/BeforeMeetingAlert.java create mode 100644 src/main/java/net/teumteum/alert/domain/UserAlertRepository.java create mode 100644 src/main/java/net/teumteum/alert/domain/UserAlertService.java create mode 100644 src/main/java/net/teumteum/alert/domain/response/AlertsResponse.java rename src/main/java/net/teumteum/meeting/domain/{MeetingAlerted.java => BeforeMeetingAlerted.java} (53%) create mode 100644 src/main/java/net/teumteum/meeting/domain/EndMeetingAlerted.java create mode 100644 src/main/java/net/teumteum/user/UserRecommended.java create mode 100644 src/main/resources/db/migration/V11__create_alert.sql create mode 100644 src/main/resources/db/migration/V12__create_bookmark.sql rename src/test/java/net/teumteum/alert/domain/{AlertRepositoryTest.java => UserAlertRepositoryTest.java} (90%) diff --git a/src/main/java/net/teumteum/alert/app/AlertHandler.java b/src/main/java/net/teumteum/alert/app/AlertHandler.java new file mode 100644 index 00000000..2c18336c --- /dev/null +++ b/src/main/java/net/teumteum/alert/app/AlertHandler.java @@ -0,0 +1,84 @@ +package net.teumteum.alert.app; + +import static net.teumteum.alert.app.AlertExecutorConfigurer.ALERT_EXECUTOR; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import net.teumteum.alert.domain.Alert; +import net.teumteum.alert.domain.AlertPublisher; +import net.teumteum.alert.domain.AlertService; +import net.teumteum.alert.domain.AlertType; +import net.teumteum.alert.domain.UserAlertService; +import net.teumteum.meeting.domain.BeforeMeetingAlerted; +import net.teumteum.meeting.domain.EndMeetingAlerted; +import net.teumteum.user.UserRecommended; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.data.util.Pair; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@Profile("prod") +@RequiredArgsConstructor +public class AlertHandler { + + private final UserAlertService userAlertService; + private final AlertService alertService; + private final AlertPublisher alertPublisher; + + @Async(ALERT_EXECUTOR) + @EventListener(BeforeMeetingAlerted.class) + public void handleBeforeMeetingAlerts(BeforeMeetingAlerted alerted) { + userAlertService.findAllByUserId(alerted.userIds()) + .stream() + .map(userAlert -> Pair.of(userAlert.getToken(), + new Alert(null, userAlert.getUserId(), "5분 뒤에 모임이 시작돼요!", + "모임 장소로 가서 틈틈 모임을 준비해주세요.", AlertType.BEFORE_MEETING))) + .map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond()))) + .forEach( + tokenAndAlert -> alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(), Map.of()) + ); + } + + @Async(ALERT_EXECUTOR) + @EventListener(EndMeetingAlerted.class) + public void handleStartMeetingAlerts(EndMeetingAlerted alerted) { + userAlertService.findAllByUserId(alerted.userIds()) + .stream() + .map(userAlert -> Pair.of(userAlert.getToken(), + new Alert(null, userAlert.getUserId(), alerted.meetingTitle(), + "모임이 종료되었어요", AlertType.END_MEETING))) + .map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond()))) + .forEach(tokenAndAlert -> + alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(), + Map.of("meetingId", alerted.meetingId().toString(), "participants", + toCommaString(alerted.userIds().stream().toList()))) + ); + } + + private String toCommaString(List ids) { + var stringBuilder = new StringBuilder(); + for (int i = 0; i < ids.size() - 1; i++) { + stringBuilder.append(ids.get(i)).append(","); + } + stringBuilder.append(ids.getLast()); + return stringBuilder.toString(); + } + + @Async(ALERT_EXECUTOR) + @EventListener(UserRecommended.class) + public void handleUserRecommended(UserRecommended alerted) { + userAlertService.findAllByUserId(Set.of(alerted.userId())) + .stream() + .map(userAlert -> Pair.of(userAlert.getToken(), + new Alert(null, userAlert.getUserId(), "틈 채우기", + alerted.recommenderName() + "님이 당신을 추천했어요!", AlertType.RECOMMEND_USER))) + .map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond()))) + .forEach(tokenAndAlert -> + alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(), Map.of()) + ); + } +} diff --git a/src/main/java/net/teumteum/alert/app/BeforeMeetingAlertHandler.java b/src/main/java/net/teumteum/alert/app/BeforeMeetingAlertHandler.java deleted file mode 100644 index eb928553..00000000 --- a/src/main/java/net/teumteum/alert/app/BeforeMeetingAlertHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.teumteum.alert.app; - -import static net.teumteum.alert.app.AlertExecutorConfigurer.ALERT_EXECUTOR; - -import java.time.Instant; -import lombok.RequiredArgsConstructor; -import net.teumteum.alert.domain.AlertPublisher; -import net.teumteum.alert.domain.AlertService; -import net.teumteum.alert.domain.BeforeMeetingAlert; -import net.teumteum.meeting.domain.MeetingAlerted; -import org.springframework.context.annotation.Profile; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@Profile("prod") -@RequiredArgsConstructor -public class BeforeMeetingAlertHandler { - - private final AlertService alertService; - private final AlertPublisher alertPublisher; - - @Async(ALERT_EXECUTOR) - @EventListener(MeetingAlerted.class) - public void alert(MeetingAlerted alerted) { - alertService.findAllByUserId(alerted.userIds()) - .stream() - .map(userAlert -> new BeforeMeetingAlert(userAlert.getUserId(), userAlert.getToken(), Instant.now())) - .forEach(alertPublisher::publish); - } - -} diff --git a/src/main/java/net/teumteum/alert/controller/AlertController.java b/src/main/java/net/teumteum/alert/controller/AlertController.java index bf998ea9..47e5892c 100644 --- a/src/main/java/net/teumteum/alert/controller/AlertController.java +++ b/src/main/java/net/teumteum/alert/controller/AlertController.java @@ -4,12 +4,15 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import net.teumteum.alert.domain.AlertService; +import net.teumteum.alert.domain.UserAlertService; import net.teumteum.alert.domain.request.RegisterAlertRequest; import net.teumteum.alert.domain.request.UpdateAlertTokenRequest; +import net.teumteum.alert.domain.response.AlertsResponse; import net.teumteum.core.error.ErrorResponse; import net.teumteum.core.security.service.SecurityService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,20 +24,28 @@ public class AlertController { private final AlertService alertService; + private final UserAlertService userAlertService; private final SecurityService securityService; @PostMapping("/alerts") @ResponseStatus(HttpStatus.OK) public void registerAlert(@Valid @RequestBody RegisterAlertRequest registerAlertRequest) { var loginUserId = securityService.getCurrentUserId(); - alertService.registerAlert(loginUserId, registerAlertRequest); + userAlertService.registerAlert(loginUserId, registerAlertRequest); } @PatchMapping("/alerts") @ResponseStatus(HttpStatus.OK) public void updateAlert(@Valid @RequestBody UpdateAlertTokenRequest updateAlertTokenRequest) { var loginUserId = securityService.getCurrentUserId(); - alertService.updateAlertToken(loginUserId, updateAlertTokenRequest); + userAlertService.updateAlertToken(loginUserId, updateAlertTokenRequest); + } + + @GetMapping("/alerts") + @ResponseStatus(HttpStatus.OK) + public AlertsResponse getAlerts() { + var loginUserId = securityService.getCurrentUserId(); + return alertService.findAllByUserId(loginUserId); } @ExceptionHandler(IllegalArgumentException.class) diff --git a/src/main/java/net/teumteum/alert/domain/Alert.java b/src/main/java/net/teumteum/alert/domain/Alert.java new file mode 100644 index 00000000..dd488d73 --- /dev/null +++ b/src/main/java/net/teumteum/alert/domain/Alert.java @@ -0,0 +1,40 @@ +package net.teumteum.alert.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.teumteum.core.entity.TimeBaseEntity; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "alert") +@Entity(name = "alert") +public class Alert extends TimeBaseEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "userId", nullable = false) + private Long userId; + + @Column(name = "title", nullable = false, length = 20) + private String title; + + @Column(name = "body", nullable = false, length = 20) + private String body; + + @Column(name = "type") + @Enumerated(EnumType.STRING) + private AlertType type; +} diff --git a/src/main/java/net/teumteum/alert/domain/AlertPublisher.java b/src/main/java/net/teumteum/alert/domain/AlertPublisher.java index 8c69ac52..4321cd7c 100644 --- a/src/main/java/net/teumteum/alert/domain/AlertPublisher.java +++ b/src/main/java/net/teumteum/alert/domain/AlertPublisher.java @@ -1,8 +1,10 @@ package net.teumteum.alert.domain; +import java.util.Map; + @FunctionalInterface -public interface AlertPublisher { +public interface AlertPublisher { - void publish(T alertable); + void publish(String token, Alert alert, Map data); } diff --git a/src/main/java/net/teumteum/alert/domain/AlertRepository.java b/src/main/java/net/teumteum/alert/domain/AlertRepository.java index 33c34cf6..7c873b68 100644 --- a/src/main/java/net/teumteum/alert/domain/AlertRepository.java +++ b/src/main/java/net/teumteum/alert/domain/AlertRepository.java @@ -1,22 +1,15 @@ package net.teumteum.alert.domain; -import jakarta.persistence.LockModeType; +import java.time.Instant; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface AlertRepository extends JpaRepository { +public interface AlertRepository extends JpaRepository { - @Query("select u from user_alert as u where u.userId in :userIds") - List findAllByUserId(@Param("userIds") Iterable userIds); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("select u from user_alert as u where u.userId = :userId") - Optional findByUserIdWithLock(@Param("userId") Long userId); - - Optional findByUserId(@Param("userId") Long userId); + List findAllByUserId(Long userId); + @Query("select a from alert as a where a.createdAt <= :createdAt") + List findAllByCreatedAt(@Param("createdAt") Instant createdAt); } diff --git a/src/main/java/net/teumteum/alert/domain/AlertService.java b/src/main/java/net/teumteum/alert/domain/AlertService.java index 65d0b8e8..042524fb 100644 --- a/src/main/java/net/teumteum/alert/domain/AlertService.java +++ b/src/main/java/net/teumteum/alert/domain/AlertService.java @@ -1,10 +1,10 @@ package net.teumteum.alert.domain; -import java.util.List; -import java.util.Set; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; -import net.teumteum.alert.domain.request.RegisterAlertRequest; -import net.teumteum.alert.domain.request.UpdateAlertTokenRequest; +import net.teumteum.alert.domain.response.AlertsResponse; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,28 +13,23 @@ @Transactional(readOnly = true) public class AlertService { + private static final String EVERY_12AM = "0 0 0 * * *"; + private final AlertRepository alertRepository; @Transactional - public void registerAlert(Long userId, RegisterAlertRequest registerAlertRequest) { - alertRepository.findByUserId(userId) - .ifPresentOrElse(userAlert -> { - throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId +"\""); - }, () -> { - var alert = new UserAlert(null, userId, registerAlertRequest.token()); - alertRepository.save(alert); - }); + public Alert save(Alert alert) { + return alertRepository.save(alert); } - @Transactional - public void updateAlertToken(Long userId, UpdateAlertTokenRequest updateAlertTokenRequest) { - var userAlert = alertRepository.findByUserIdWithLock(userId) - .orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 토큰을 찾을 수 없습니다.")); - - userAlert.updateToken(updateAlertTokenRequest.token()); + public AlertsResponse findAllByUserId(Long userId) { + return AlertsResponse.of(alertRepository.findAllByUserId(userId)); } - public List findAllByUserId(Set userIds) { - return alertRepository.findAllByUserId(userIds); + @Transactional + @Scheduled(cron = EVERY_12AM) + public void deleteOneMonthBeforeAlert() { + var deleteTargets = alertRepository.findAllByCreatedAt(Instant.now().minus(1, ChronoUnit.MONTHS)); + alertRepository.deleteAllInBatch(deleteTargets); } } diff --git a/src/main/java/net/teumteum/alert/domain/AlertType.java b/src/main/java/net/teumteum/alert/domain/AlertType.java new file mode 100644 index 00000000..b32fea2c --- /dev/null +++ b/src/main/java/net/teumteum/alert/domain/AlertType.java @@ -0,0 +1,9 @@ +package net.teumteum.alert.domain; + +public enum AlertType { + + BEFORE_MEETING, + END_MEETING, + RECOMMEND_USER, + ; +} diff --git a/src/main/java/net/teumteum/alert/domain/Alertable.java b/src/main/java/net/teumteum/alert/domain/Alertable.java deleted file mode 100644 index a0cf7e1e..00000000 --- a/src/main/java/net/teumteum/alert/domain/Alertable.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.teumteum.alert.domain; - -public interface Alertable { - - String token(); - - String title(); - - String body(); - - String type(); -} diff --git a/src/main/java/net/teumteum/alert/domain/BeforeMeetingAlert.java b/src/main/java/net/teumteum/alert/domain/BeforeMeetingAlert.java deleted file mode 100644 index 7f863fd1..00000000 --- a/src/main/java/net/teumteum/alert/domain/BeforeMeetingAlert.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.teumteum.alert.domain; - -import java.time.Instant; - -public record BeforeMeetingAlert( - Long userId, - String token, - Instant publishedAt -) implements Alertable { - - @Override - public String title() { - return "5분 뒤에 모임이 시작돼요!"; - } - - @Override - public String body() { - return "모임 장소로 가서 틈틈 모임을 준비해주세요."; - } - - @Override - public String type() { - return "BEFORE_MEETING"; - } -} diff --git a/src/main/java/net/teumteum/alert/domain/UserAlertRepository.java b/src/main/java/net/teumteum/alert/domain/UserAlertRepository.java new file mode 100644 index 00000000..520e841e --- /dev/null +++ b/src/main/java/net/teumteum/alert/domain/UserAlertRepository.java @@ -0,0 +1,22 @@ +package net.teumteum.alert.domain; + +import jakarta.persistence.LockModeType; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserAlertRepository extends JpaRepository { + + @Query("select u from user_alert as u where u.userId in :userIds") + List findAllByUserId(@Param("userIds") Iterable userIds); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select u from user_alert as u where u.userId = :userId") + Optional findByUserIdWithLock(@Param("userId") Long userId); + + Optional findByUserId(@Param("userId") Long userId); + +} diff --git a/src/main/java/net/teumteum/alert/domain/UserAlertService.java b/src/main/java/net/teumteum/alert/domain/UserAlertService.java new file mode 100644 index 00000000..4c6cb185 --- /dev/null +++ b/src/main/java/net/teumteum/alert/domain/UserAlertService.java @@ -0,0 +1,40 @@ +package net.teumteum.alert.domain; + +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import net.teumteum.alert.domain.request.RegisterAlertRequest; +import net.teumteum.alert.domain.request.UpdateAlertTokenRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserAlertService { + + private final UserAlertRepository alertRepository; + + @Transactional + public void registerAlert(Long userId, RegisterAlertRequest registerAlertRequest) { + alertRepository.findByUserId(userId) + .ifPresentOrElse(userAlert -> { + throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId +"\""); + }, () -> { + var alert = new UserAlert(null, userId, registerAlertRequest.token()); + alertRepository.save(alert); + }); + } + + @Transactional + public void updateAlertToken(Long userId, UpdateAlertTokenRequest updateAlertTokenRequest) { + var userAlert = alertRepository.findByUserIdWithLock(userId) + .orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 토큰을 찾을 수 없습니다.")); + + userAlert.updateToken(updateAlertTokenRequest.token()); + } + + public List findAllByUserId(Set userIds) { + return alertRepository.findAllByUserId(userIds); + } +} diff --git a/src/main/java/net/teumteum/alert/domain/response/AlertsResponse.java b/src/main/java/net/teumteum/alert/domain/response/AlertsResponse.java new file mode 100644 index 00000000..c799ec56 --- /dev/null +++ b/src/main/java/net/teumteum/alert/domain/response/AlertsResponse.java @@ -0,0 +1,25 @@ +package net.teumteum.alert.domain.response; + +import java.util.List; +import net.teumteum.alert.domain.Alert; + +public record AlertsResponse( + List alerts +) { + + public static AlertsResponse of(List alerts) { + return new AlertsResponse( + alerts.stream() + .map(alert -> new AlertResponse(alert.getTitle(), alert.getBody(), alert.getType().name())) + .toList() + ); + } + + public record AlertResponse( + String title, + String body, + String type + ) { + + } +} diff --git a/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java b/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java index aabf074b..977731ac 100644 --- a/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java +++ b/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java @@ -14,8 +14,9 @@ import com.google.firebase.messaging.Notification; import jakarta.annotation.PostConstruct; import java.io.IOException; +import java.util.Map; +import net.teumteum.alert.domain.Alert; import net.teumteum.alert.domain.AlertPublisher; -import net.teumteum.alert.domain.BeforeMeetingAlert; import org.springframework.context.annotation.Profile; import org.springframework.core.io.ClassPathResource; import org.springframework.lang.Nullable; @@ -24,18 +25,30 @@ @Service @Profile("prod") -public class FcmAlertPublisher implements AlertPublisher { +public class FcmAlertPublisher implements AlertPublisher { private static final int MAX_RETRY_COUNT = 5; private static final String FCM_TOKEN_PATH = "teum-teum-12611-firebase-adminsdk-cjyx3-ea066f25ef.json"; @Override @Async(FCM_ALERT_EXECUTOR) - public void publish(BeforeMeetingAlert beforeMeetingAlert) { - var message = buildMessage(beforeMeetingAlert); + public void publish(String token, Alert alert, Map data) { + var message = buildMessage(token, alert, data); publishWithRetry(0, message, null); } + private Message buildMessage(String token, Alert alert, Map data) { + return Message.builder() + .setToken(token) + .setNotification(buildNotification(alert)) + .setAndroidConfig(buildAndroidConfig(alert)) + .putData("publishedAt", alert.getCreatedAt().toString()) + .putData("userId", alert.getUserId().toString()) + .putData("type", alert.getType().toString()) + .putAllData(data) + .build(); + } + private void publishWithRetry(int currentRetryCount, Message message, @Nullable ErrorCode errorCode) { if (MAX_RETRY_COUNT == currentRetryCount) { return; @@ -53,28 +66,18 @@ private void publishWithRetry(int currentRetryCount, Message message, @Nullable } } - private Message buildMessage(BeforeMeetingAlert beforeMeetingAlert) { - return Message.builder() - .setToken(beforeMeetingAlert.token()) - .setNotification(buildNotification(beforeMeetingAlert)) - .setAndroidConfig(buildAndroidConfig(beforeMeetingAlert)) - .putData("publishedAt", beforeMeetingAlert.publishedAt().toString()) - .putData("userId", beforeMeetingAlert.userId().toString()) - .build(); - } - - private Notification buildNotification(BeforeMeetingAlert beforeMeetingAlert) { + private Notification buildNotification(Alert alert) { return Notification.builder() - .setTitle(beforeMeetingAlert.title()) - .setBody(beforeMeetingAlert.body()) + .setTitle(alert.getTitle()) + .setBody(alert.getBody()) .build(); } - private AndroidConfig buildAndroidConfig(BeforeMeetingAlert beforeMeetingAlert) { + private AndroidConfig buildAndroidConfig(Alert alert) { return AndroidConfig.builder() .setNotification(AndroidNotification.builder() - .setTitle(beforeMeetingAlert.title()) - .setBody(beforeMeetingAlert.body()) + .setTitle(alert.getTitle()) + .setBody(alert.getBody()) .setClickAction("push_click") .build()) .build(); diff --git a/src/main/java/net/teumteum/meeting/controller/MeetingController.java b/src/main/java/net/teumteum/meeting/controller/MeetingController.java index 0fa661aa..267150b1 100644 --- a/src/main/java/net/teumteum/meeting/controller/MeetingController.java +++ b/src/main/java/net/teumteum/meeting/controller/MeetingController.java @@ -49,7 +49,8 @@ public MeetingResponse createMeeting( @GetMapping("/{meetingId}") @ResponseStatus(HttpStatus.OK) public MeetingResponse getMeetingById(@PathVariable("meetingId") Long meetingId) { - return meetingService.getMeetingById(meetingId); + Long userId = securityService.getCurrentUserId(); + return meetingService.getMeetingById(meetingId, userId); } @GetMapping @@ -103,6 +104,20 @@ public void reportMeeting(@PathVariable("meetingId") Long meetingId) { meetingService.reportMeeting(meetingId, userId); } + @PostMapping("/{meetingId}/bookmarks") + @ResponseStatus(HttpStatus.CREATED) + public void addBookmark(@PathVariable("meetingId") Long meetingId) { + Long userId = securityService.getCurrentUserId(); + meetingService.addBookmark(meetingId, userId); + } + + @DeleteMapping("/{meetingId}/bookmarks") + @ResponseStatus(HttpStatus.OK) + public void deleteBookmark(@PathVariable("meetingId") Long meetingId) { + Long userId = securityService.getCurrentUserId(); + meetingService.cancelBookmark(meetingId, userId); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResponse handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) { diff --git a/src/main/java/net/teumteum/meeting/domain/MeetingAlerted.java b/src/main/java/net/teumteum/meeting/domain/BeforeMeetingAlerted.java similarity index 53% rename from src/main/java/net/teumteum/meeting/domain/MeetingAlerted.java rename to src/main/java/net/teumteum/meeting/domain/BeforeMeetingAlerted.java index 7461ff0c..abbeeab7 100644 --- a/src/main/java/net/teumteum/meeting/domain/MeetingAlerted.java +++ b/src/main/java/net/teumteum/meeting/domain/BeforeMeetingAlerted.java @@ -2,6 +2,6 @@ import java.util.Set; -public record MeetingAlerted(Set userIds) { +public record BeforeMeetingAlerted(Set userIds) { } diff --git a/src/main/java/net/teumteum/meeting/domain/EndMeetingAlerted.java b/src/main/java/net/teumteum/meeting/domain/EndMeetingAlerted.java new file mode 100644 index 00000000..3562473a --- /dev/null +++ b/src/main/java/net/teumteum/meeting/domain/EndMeetingAlerted.java @@ -0,0 +1,11 @@ +package net.teumteum.meeting.domain; + +import java.util.Set; + +public record EndMeetingAlerted( + Long meetingId, + String meetingTitle, + Set userIds +) { + +} diff --git a/src/main/java/net/teumteum/meeting/domain/Meeting.java b/src/main/java/net/teumteum/meeting/domain/Meeting.java index da74a37a..9241922d 100644 --- a/src/main/java/net/teumteum/meeting/domain/Meeting.java +++ b/src/main/java/net/teumteum/meeting/domain/Meeting.java @@ -65,6 +65,10 @@ public class Meeting extends TimeBaseEntity { @ElementCollection(fetch = FetchType.EAGER) private Set imageUrls = new LinkedHashSet<>(); + @Builder.Default + @ElementCollection(fetch = FetchType.EAGER) + private Set bookmarkedUserIds = new HashSet<>(); + public void update(Meeting updateMeeting) { this.title = updateMeeting.title; this.topic = updateMeeting.topic; @@ -90,6 +94,18 @@ public boolean alreadyParticipant(Long userId) { return participantUserIds.contains(userId); } + public void addBookmark(Long userId) { + bookmarkedUserIds.add(userId); + } + + public void cancelBookmark(Long userId) { + bookmarkedUserIds.remove(userId); + } + + public boolean isBookmarked(Long userId) { + return bookmarkedUserIds.contains(userId); + } + public boolean isOpen() { return promiseDateTime.isAfter(LocalDateTime.now()); } diff --git a/src/main/java/net/teumteum/meeting/domain/MeetingRepository.java b/src/main/java/net/teumteum/meeting/domain/MeetingRepository.java index b12c0379..a427ac1f 100644 --- a/src/main/java/net/teumteum/meeting/domain/MeetingRepository.java +++ b/src/main/java/net/teumteum/meeting/domain/MeetingRepository.java @@ -13,7 +13,7 @@ public interface MeetingRepository extends JpaRepository, JpaSpec @Query("select m from meeting as m " + "where :startPromiseDate <= m.promiseDateTime and m.promiseDateTime < :endPromiseDate") - List findAlertMeetings(@Param("startPromiseDate") LocalDateTime currentTime, + List findAlertMeetings(@Param("startPromiseDate") LocalDateTime startPromiseDate, @Param("endPromiseDate") LocalDateTime endPromiseDate); boolean existsById(Long id); diff --git a/src/main/java/net/teumteum/meeting/domain/request/UpdateMeetingRequest.java b/src/main/java/net/teumteum/meeting/domain/request/UpdateMeetingRequest.java index c3313146..317004e0 100644 --- a/src/main/java/net/teumteum/meeting/domain/request/UpdateMeetingRequest.java +++ b/src/main/java/net/teumteum/meeting/domain/request/UpdateMeetingRequest.java @@ -32,6 +32,7 @@ public record UpdateMeetingRequest( public static final Long IGNORE_HOST_ID = null; public static final Set IGNORE_PARTICIPANT_USER_IDS = null; public static final Set IGNORE_IMAGE_URLS = null; + public static final Set IGNORE_BOOKMARKED_USER_IDS = null; public Meeting toMeeting() { return new Meeting( @@ -44,7 +45,8 @@ public Meeting toMeeting() { NewMeetingArea.of(meetingArea), numberOfRecruits, promiseDateTime, - IGNORE_IMAGE_URLS + IGNORE_IMAGE_URLS, + IGNORE_BOOKMARKED_USER_IDS ); } diff --git a/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java b/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java index 372638b5..9a09d428 100644 --- a/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java +++ b/src/main/java/net/teumteum/meeting/domain/response/MeetingResponse.java @@ -17,11 +17,13 @@ public record MeetingResponse( LocalDateTime promiseDateTime, int numberOfRecruits, MeetingArea meetingArea, - Set participantIds + Set participantIds, + Boolean isBookmarked ) { public static MeetingResponse of( - Meeting meeting + Meeting meeting, + Boolean isBookmarked ) { return new MeetingResponse( meeting.getId(), @@ -33,7 +35,8 @@ public static MeetingResponse of( meeting.getPromiseDateTime(), meeting.getNumberOfRecruits(), MeetingArea.of(meeting), - meeting.getParticipantUserIds() + meeting.getParticipantUserIds(), + isBookmarked ); } diff --git a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java index 3d162de7..f8d8192f 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java @@ -2,7 +2,8 @@ import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; -import net.teumteum.meeting.domain.MeetingAlerted; +import net.teumteum.meeting.domain.BeforeMeetingAlerted; +import net.teumteum.meeting.domain.EndMeetingAlerted; import net.teumteum.meeting.domain.MeetingRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; @@ -15,18 +16,35 @@ public class MeetingAlertPublisher { private static final String EVERY_ONE_MINUTES = "0 * * * * *"; + private static final String EVERY_12PM = "0 0 12 * * *"; private final MeetingRepository meetingRepository; private final ApplicationEventPublisher eventPublisher; @Scheduled(cron = EVERY_ONE_MINUTES) - public void alertMeeting() { + public void alertBeforeMeeting() { var alertStart = LocalDateTime.now().plusMinutes(5).withNano(0).withSecond(0); var alertEnd = alertStart.plusMinutes(1).withNano(0).withSecond(0); var alertTargets = meetingRepository.findAlertMeetings(alertStart, alertEnd); alertTargets.forEach(meeting -> eventPublisher.publishEvent( - new MeetingAlerted(meeting.getParticipantUserIds()) + new BeforeMeetingAlerted(meeting.getParticipantUserIds()) ) ); } + + @Scheduled(cron = EVERY_12PM) + public void alertEndMeeting() { + var today = LocalDateTime.now() + .withNano(0) + .withSecond(0) + .withMinute(0) + .withHour(0); + + var yesterday = today.minusDays(1); + + var alertTargets = meetingRepository.findAlertMeetings(yesterday, today); + alertTargets.forEach(meeting -> eventPublisher.publishEvent( + new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) + )); + } } diff --git a/src/main/java/net/teumteum/meeting/service/MeetingService.java b/src/main/java/net/teumteum/meeting/service/MeetingService.java index 602a5fb0..dca907d1 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingService.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingService.java @@ -48,14 +48,14 @@ public MeetingResponse createMeeting(List images, CreateMeetingRe uploadMeetingImages(images, meeting); - return MeetingResponse.of(meeting); + return MeetingResponse.of(meeting, meeting.isBookmarked(userId)); } @Transactional(readOnly = true) - public MeetingResponse getMeetingById(Long meetingId) { + public MeetingResponse getMeetingById(Long meetingId, Long userId) { var existMeeting = getMeeting(meetingId); - return MeetingResponse.of(existMeeting); + return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId)); } @Transactional @@ -72,7 +72,7 @@ public MeetingResponse updateMeeting(Long meetingId, List images, existMeeting.update(updateMeetingRequest.toMeeting()); uploadMeetingImages(images, existMeeting); - return MeetingResponse.of(existMeeting); + return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId)); } @Transactional @@ -126,7 +126,7 @@ public MeetingResponse addParticipant(Long meetingId, Long userId) { } existMeeting.addParticipant(userId); - return MeetingResponse.of(existMeeting); + return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId)); } @Transactional @@ -148,6 +148,28 @@ public void cancelParticipant(Long meetingId, Long userId) { existMeeting.cancelParticipant(userId); } + @Transactional + public void addBookmark(Long meetingId, Long userId) { + var existMeeting = getMeeting(meetingId); + + if (existMeeting.isBookmarked(userId)) { + throw new IllegalArgumentException("이미 북마크한 모임입니다."); + } + + existMeeting.addBookmark(userId); + } + + @Transactional + public void cancelBookmark(Long meetingId, Long userId) { + var existMeeting = getMeeting(meetingId); + + if (!existMeeting.isBookmarked(userId)) { + throw new IllegalArgumentException("북마크하지 않은 모임입니다."); + } + + existMeeting.cancelBookmark(userId); + } + private void uploadMeetingImages(List images, Meeting meeting) { Assert.isTrue(!images.isEmpty() && images.size() <= 5, "이미지는 1개 이상 5개 이하로 업로드해야 합니다."); meeting.getImageUrls().clear(); diff --git a/src/main/java/net/teumteum/user/UserRecommended.java b/src/main/java/net/teumteum/user/UserRecommended.java new file mode 100644 index 00000000..afb9064a --- /dev/null +++ b/src/main/java/net/teumteum/user/UserRecommended.java @@ -0,0 +1,8 @@ +package net.teumteum.user; + +public record UserRecommended( + Long userId, + String recommenderName +) { + +} diff --git a/src/main/resources/db/migration/V11__create_alert.sql b/src/main/resources/db/migration/V11__create_alert.sql new file mode 100644 index 00000000..195cc385 --- /dev/null +++ b/src/main/resources/db/migration/V11__create_alert.sql @@ -0,0 +1,10 @@ +create table if not exists alert( + id bigint not null auto_increment, + userId bigint not null, + title varchar(20) not null, + `body` varchar(20) not null, + type enum('BEFORE_MEETING'), + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) +); diff --git a/src/main/resources/db/migration/V12__create_bookmark.sql b/src/main/resources/db/migration/V12__create_bookmark.sql new file mode 100644 index 00000000..be6244e6 --- /dev/null +++ b/src/main/resources/db/migration/V12__create_bookmark.sql @@ -0,0 +1,6 @@ +create table if not exists meeting_bookmarked_user_ids +( + meeting_id bigint not null, + bookmarked_user_ids bigint null, + foreign key (meeting_id) references meeting (id) +); diff --git a/src/test/java/net/teumteum/alert/domain/AlertRepositoryTest.java b/src/test/java/net/teumteum/alert/domain/UserAlertRepositoryTest.java similarity index 90% rename from src/test/java/net/teumteum/alert/domain/AlertRepositoryTest.java rename to src/test/java/net/teumteum/alert/domain/UserAlertRepositoryTest.java index baff4e60..5d9cbb70 100644 --- a/src/test/java/net/teumteum/alert/domain/AlertRepositoryTest.java +++ b/src/test/java/net/teumteum/alert/domain/UserAlertRepositoryTest.java @@ -12,11 +12,11 @@ @DataJpaTest @ExtendWith(SpringExtension.class) -@DisplayName("AlertRepository 클래스의") -class AlertRepositoryTest { +@DisplayName("UserAlertRepository 클래스의") +class UserAlertRepositoryTest { @Autowired - private AlertRepository alertRepository; + private UserAlertRepository alertRepository; @Autowired private EntityManager entityManager; diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 1fd41c22..2b8d3b32 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -120,6 +120,20 @@ ResponseSpec cancelMeeting(String token, Long meetingId) { .exchange(); } + ResponseSpec addBookmark(String token, Long meetingId) { + return webTestClient.post() + .uri("/meetings/" + meetingId + "/bookmarks") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); + } + + ResponseSpec cancelBookmark(String token, Long meetingId) { + return webTestClient.delete() + .uri("/meetings/" + meetingId + "/bookmarks") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); + } + ResponseSpec getCommonInterests(String token, List userIds) { var param = new StringBuilder(); for (Long userId : userIds) { diff --git a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java index 806dff15..aea1c8ee 100644 --- a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java @@ -35,8 +35,10 @@ class Find_meeting_api { @DisplayName("존재하는 모임의 id가 주어지면, 모임 정보를 응답한다.") void Return_meeting_info_if_exist_meeting_id_received() { // given + var user = repository.saveAndGetUser(); + securityContextSetting.set(user.getId()); var meeting = repository.saveAndGetOpenMeeting(); - var expected = MeetingResponse.of(meeting); + var expected = MeetingResponse.of(meeting, false); // when var result = api.getMeetingById(VALID_TOKEN, meeting.getId()); // then @@ -52,6 +54,8 @@ void Return_meeting_info_if_exist_meeting_id_received() { @DisplayName("존재하지 않는 모임의 id가 주어지면, 400 Bad Request를 응답한다.") void Return_400_bad_request_if_not_exists_meeting_id_received() { // given + var user = repository.saveAndGetUser(); + securityContextSetting.set(user.getId()); var notExistMeetingId = 1L; // when var result = api.getMeetingById(VALID_TOKEN, notExistMeetingId); @@ -59,6 +63,25 @@ void Return_400_bad_request_if_not_exists_meeting_id_received() { result.expectStatus().isBadRequest() .expectBody(ErrorResponse.class); } + + @Test + @DisplayName("유저가 북마크한 모임이라면, isBookmarked를 true로 응답한다.") + void Return_is_bookmarked_true_if_user_bookmarked_meeting() { + // given + var user = repository.saveAndGetUser(); + securityContextSetting.set(user.getId()); + var meeting = repository.saveAndGetOpenMeeting(); + api.addBookmark(VALID_TOKEN, meeting.getId()); + // when + var result = api.getMeetingById(VALID_TOKEN, meeting.getId()); + // then + Assertions.assertThat( + result.expectStatus().isOk() + .expectBody(MeetingResponse.class) + .returnResult().getResponseBody()) + .extracting(MeetingResponse::isBookmarked) + .isEqualTo(true); + } } @Nested @@ -72,7 +95,6 @@ void Delete_meeting_if_exist_meeting_id_received() { var host = repository.saveAndGetUser(); securityContextSetting.set(host.getId()); - var meeting = repository.saveAndGetOpenMeetingWithHostId(host.getId()); // when var result = api.deleteMeeting(VALID_TOKEN, meeting.getId()); @@ -380,4 +402,74 @@ void Return_400_bad_request_if_closed_meeting_id_received() { .isEqualTo("종료된 모임에서 참여를 취소할 수 없습니다."); } } + + @Nested + @DisplayName("북마크 추가 API는") + class Add_bookmark_api { + + @Test + @DisplayName("존재하는 모임의 id가 주어지면, 모임을 북마크한다.") + void Add_bookmark_if_exist_meeting_id_received() { + // given + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetOpenMeeting(); + + securityContextSetting.set(me.getId()); + // when + var result = api.addBookmark(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isCreated(); + } + + @Test + @DisplayName("이미 북마크한 모임의 id가 주어지면, 400 Bad Request를 응답한다.") + void Return_400_bad_request_if_already_bookmarked_meeting_id_received() { + // given + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetOpenMeeting(); + + securityContextSetting.set(me.getId()); + api.addBookmark(VALID_TOKEN, meeting.getId()); + // when + var result = api.addBookmark(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class); + } + } + + @Nested + @DisplayName("북마크 취소 API는") + class Cancel_bookmark_api { + + @Test + @DisplayName("존재하는 모임의 id가 주어지면, 모임의 북마크를 취소한다.") + void Cancel_bookmark_if_exist_meeting_id_received() { + // given + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetOpenMeeting(); + + securityContextSetting.set(me.getId()); + api.addBookmark(VALID_TOKEN, meeting.getId()); + // when + var result = api.cancelBookmark(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isOk(); + } + + @Test + @DisplayName("북마크하지 않은 모임의 id가 주어지면, 400 Bad Request를 응답한다.") + void Return_400_bad_request_if_not_bookmarked_meeting_id_received() { + // given + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetOpenMeeting(); + + securityContextSetting.set(me.getId()); + // when + var result = api.cancelBookmark(VALID_TOKEN, meeting.getId()); + // then + result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class); + } + } } diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index dc91a3a3..93f1da27 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -141,7 +141,8 @@ public static Meeting newMeetingByBuilder(MeetingBuilder meetingBuilder) { meetingBuilder.meetingArea, meetingBuilder.numberOfRecruits, meetingBuilder.promiseDateTime, - meetingBuilder.imageUrls + meetingBuilder.imageUrls, + meetingBuilder.bookmarkedUserIds ); } @@ -177,6 +178,9 @@ public static class MeetingBuilder { @Builder.Default private Set imageUrls = new HashSet<>(List.of("/1/image.jpg", "/2/image.jpg")); + + @Builder.Default + private Set bookmarkedUserIds = new HashSet<>(List.of()); } } diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java index 18d30411..29f2f52f 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java @@ -97,7 +97,7 @@ void Delete_success_if_exists_meeting_input() { } @Nested - @DisplayName("JPA Specification을 이용한 findAll 메소드 중") + @DisplayName("JPA Specification을 이용한 findAllByCreatedAt 메소드 중") class FindAllWithSpecificationAndPageNation_method { @Test diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index dbf1fcb2..08b5a999 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -93,3 +93,21 @@ create table if not exists users_reviews foreign key (users_id) references users (id) on delete cascade ); + +create table if not exists alert( + id bigint not null auto_increment, + userId bigint not null, + title varchar(20) not null, + `body` varchar(20) not null, + type enum('BEFORE_MEETING'), + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) +); + +create table if not exists meeting_bookmarked_user_ids +( + meeting_id bigint not null, + bookmarked_user_ids bigint null, + foreign key (meeting_id) references meeting (id) +);