From 95be84e5ece9d04d7169f39f27a9c274b412d27c Mon Sep 17 00:00:00 2001 From: msk226 Date: Thu, 29 Jan 2026 14:06:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Feature]=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81=20-=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20-=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EC=9E=91=EC=84=B1=EC=9E=90=20=EC=9C=A0?= =?UTF-8?q?=EB=AC=B4=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/notification/build.gradle | 3 ++ .../StudyApplicationNotificationListener.java | 23 +++++----- .../query/GetNotificationService.java | 23 ++++++++++ .../jpa/NotificationRepository.java | 2 + .../query/NotificationQueryController.java | 40 +++++++++++++++++ .../dto/GetNotificationListResponse.java | 45 +++++++++++++++++++ .../command/ManageScheduleService.java | 4 +- .../application/query/GetScheduleService.java | 17 +++---- .../kr/spot/schedule/domain/Schedule.java | 6 ++- .../command/ScheduleCommandController.java | 4 +- .../query/ScheduleQueryController.java | 10 +++-- .../query/dto/GetScheduleListResponse.java | 7 +-- 12 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 modules/notification/src/main/java/kr/spot/application/query/GetNotificationService.java create mode 100644 modules/notification/src/main/java/kr/spot/presentation/query/NotificationQueryController.java create mode 100644 modules/notification/src/main/java/kr/spot/presentation/query/dto/GetNotificationListResponse.java diff --git a/modules/notification/build.gradle b/modules/notification/build.gradle index 87e6daef..3721f10c 100644 --- a/modules/notification/build.gradle +++ b/modules/notification/build.gradle @@ -8,10 +8,13 @@ dependencies { implementation project(":common:snowflake") implementation project(":modules:shared") + implementation project(":modules:auth-api") implementation project(":modules:study-api") implementation project(":modules:notification-api") implementation "org.springframework.boot:spring-boot-starter-data-jpa" + implementation "org.springframework.boot:spring-boot-starter-web" + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" diff --git a/modules/notification/src/main/java/kr/spot/application/event/StudyApplicationNotificationListener.java b/modules/notification/src/main/java/kr/spot/application/event/StudyApplicationNotificationListener.java index 1bfa7dc6..c80aa881 100644 --- a/modules/notification/src/main/java/kr/spot/application/event/StudyApplicationNotificationListener.java +++ b/modules/notification/src/main/java/kr/spot/application/event/StudyApplicationNotificationListener.java @@ -27,6 +27,12 @@ public class StudyApplicationNotificationListener { @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void handle(StudyApplicationProcessedEvent event) { + if (!event.isApproved()) { + log.debug("Skipping notification for rejected application: studyId={}, applicantId={}", + event.studyId(), event.applicantId()); + return; + } + try { Notification notification = createNotification(event); notificationRepository.save(notification); @@ -42,24 +48,15 @@ public void handle(StudyApplicationProcessedEvent event) { } private Notification createNotification(StudyApplicationProcessedEvent event) { - NotificationType type = event.isApproved() - ? NotificationType.STUDY_APPLICATION_APPROVED - : NotificationType.STUDY_APPLICATION_REJECTED; - - String title = event.studyName(); - String body = event.isApproved() - ? "스터디 가입이 승인되었습니다! 지금 바로 참여해보세요." - : "스터디 가입이 거절되었습니다."; - String dedupeKey = String.format("%s:STUDY:%d:%d", - type.name(), event.studyId(), event.applicantId()); + NotificationType.STUDY_APPLICATION_APPROVED.name(), event.studyId(), event.applicantId()); return Notification.create( idGenerator.nextId(), event.applicantId(), - type, - title, - body, + NotificationType.STUDY_APPLICATION_APPROVED, + event.studyName(), + "스터디 가입이 승인되었습니다! 지금 바로 참여해보세요.", event.studyThumbnailUrl(), "STUDY", event.studyId(), diff --git a/modules/notification/src/main/java/kr/spot/application/query/GetNotificationService.java b/modules/notification/src/main/java/kr/spot/application/query/GetNotificationService.java new file mode 100644 index 00000000..e71f62a2 --- /dev/null +++ b/modules/notification/src/main/java/kr/spot/application/query/GetNotificationService.java @@ -0,0 +1,23 @@ +package kr.spot.application.query; + +import java.util.List; +import kr.spot.domain.Notification; +import kr.spot.infrastructure.jpa.NotificationRepository; +import kr.spot.presentation.query.dto.GetNotificationListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class GetNotificationService { + + private final NotificationRepository notificationRepository; + + public GetNotificationListResponse getMyNotifications(long memberId) { + List notifications = notificationRepository + .findByMemberIdOrderByCreatedAtDesc(memberId); + return GetNotificationListResponse.from(notifications); + } +} diff --git a/modules/notification/src/main/java/kr/spot/infrastructure/jpa/NotificationRepository.java b/modules/notification/src/main/java/kr/spot/infrastructure/jpa/NotificationRepository.java index 1ac80156..8484d812 100644 --- a/modules/notification/src/main/java/kr/spot/infrastructure/jpa/NotificationRepository.java +++ b/modules/notification/src/main/java/kr/spot/infrastructure/jpa/NotificationRepository.java @@ -13,6 +13,8 @@ public interface NotificationRepository extends JpaRepository findByMemberIdOrderByCreatedAtDesc(long memberId); + /** * 발송 대상 알림 선점 (FOR UPDATE SKIP LOCKED 사용) MySQL 8.0+에서 지원 */ diff --git a/modules/notification/src/main/java/kr/spot/presentation/query/NotificationQueryController.java b/modules/notification/src/main/java/kr/spot/presentation/query/NotificationQueryController.java new file mode 100644 index 00000000..18ef5626 --- /dev/null +++ b/modules/notification/src/main/java/kr/spot/presentation/query/NotificationQueryController.java @@ -0,0 +1,40 @@ +package kr.spot.presentation.query; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.spot.ApiResponse; +import kr.spot.annotations.CurrentMember; +import kr.spot.application.query.GetNotificationService; +import kr.spot.code.status.SuccessStatus; +import kr.spot.presentation.query.dto.GetNotificationListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "알림") +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class NotificationQueryController { + + private final GetNotificationService getNotificationService; + + @Operation(summary = "내 알림 목록 조회", description = "로그인한 사용자의 알림 목록을 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.", + content = @Content(schema = @Schema(implementation = ApiResponse.class))) + }) + @GetMapping("/me") + public ResponseEntity> getMyNotifications( + @CurrentMember @Parameter(hidden = true) Long memberId) { + GetNotificationListResponse response = getNotificationService.getMyNotifications(memberId); + return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); + } +} diff --git a/modules/notification/src/main/java/kr/spot/presentation/query/dto/GetNotificationListResponse.java b/modules/notification/src/main/java/kr/spot/presentation/query/dto/GetNotificationListResponse.java new file mode 100644 index 00000000..b0e90b8e --- /dev/null +++ b/modules/notification/src/main/java/kr/spot/presentation/query/dto/GetNotificationListResponse.java @@ -0,0 +1,45 @@ +package kr.spot.presentation.query.dto; + +import java.time.LocalDateTime; +import java.util.List; +import kr.spot.domain.Notification; + +public record GetNotificationListResponse( + List notifications, + long totalCount +) { + + public static GetNotificationListResponse from(List notifications) { + List responses = notifications.stream() + .map(NotificationResponse::from) + .toList(); + return new GetNotificationListResponse(responses, responses.size()); + } + + public record NotificationResponse( + Long notificationId, + String type, + String title, + String body, + String imageUrl, + String referenceType, + Long referenceId, + boolean isRead, + LocalDateTime createdAt + ) { + + public static NotificationResponse from(Notification notification) { + return new NotificationResponse( + notification.getId(), + notification.getType().name(), + notification.getTitle(), + notification.getBody(), + notification.getImageUrl(), + notification.getReferenceType(), + notification.getReferenceId(), + notification.isRead(), + notification.getCreatedAt() + ); + } + } +} diff --git a/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java b/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java index fc159518..0696abb3 100644 --- a/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java +++ b/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java @@ -16,9 +16,9 @@ public class ManageScheduleService { private final IdGenerator idGenerator; private final ScheduleRepository scheduleRepository; - public long createSchedule(CreateScheduleRequest request, long studyId) { + public long createSchedule(CreateScheduleRequest request, long studyId, long creatorId) { long scheduleId = idGenerator.nextId(); - Schedule schedule = Schedule.of(scheduleId, studyId, request.title(), + Schedule schedule = Schedule.of(scheduleId, studyId, creatorId, request.title(), request.locationInfo(), request.startAt(), request.endAt()); diff --git a/modules/study/src/main/java/kr/spot/schedule/application/query/GetScheduleService.java b/modules/study/src/main/java/kr/spot/schedule/application/query/GetScheduleService.java index 2d9b4f67..09212b24 100644 --- a/modules/study/src/main/java/kr/spot/schedule/application/query/GetScheduleService.java +++ b/modules/study/src/main/java/kr/spot/schedule/application/query/GetScheduleService.java @@ -20,23 +20,23 @@ public class GetScheduleService { private final ScheduleQueryRepository scheduleQueryRepository; - public GetScheduleListResponse getMonthlySchedules(Long studyId, int year, int month) { + public GetScheduleListResponse getMonthlySchedules(long studyId, int year, int month, long memberId) { LocalDate date = LocalDate.of(year, month, 1); List schedules = scheduleQueryRepository.findMonthlySchedules(studyId, date); - return toResponse(schedules); + return toResponse(schedules, memberId); } - public GetScheduleListResponse getWeeklySchedules(Long studyId, LocalDate date) { + public GetScheduleListResponse getWeeklySchedules(long studyId, LocalDate date, long memberId) { List schedules = scheduleQueryRepository.findWeeklySchedules(studyId, date); - return toResponse(schedules); + return toResponse(schedules, memberId); } - public GetScheduleListResponse getUpcomingSchedules(Long studyId) { + public GetScheduleListResponse getUpcomingSchedules(long studyId, long memberId) { List schedules = scheduleQueryRepository.findUpcomingSchedules(studyId, UPCOMING_LIMIT); - return toResponse(schedules); + return toResponse(schedules, memberId); } - private GetScheduleListResponse toResponse(List schedules) { + private GetScheduleListResponse toResponse(List schedules, long memberId) { LocalDateTime now = LocalDateTime.now(); List responses = schedules.stream() @@ -45,7 +45,8 @@ private GetScheduleListResponse toResponse(List schedules) { schedule.getTitle(), schedule.getStartAt(), schedule.getEndAt(), - schedule.isOngoing(now) + schedule.isOngoing(now), + schedule.getCreatorId() != null && schedule.getCreatorId() == memberId )) .toList(); diff --git a/modules/study/src/main/java/kr/spot/schedule/domain/Schedule.java b/modules/study/src/main/java/kr/spot/schedule/domain/Schedule.java index fda2d535..20a64c97 100644 --- a/modules/study/src/main/java/kr/spot/schedule/domain/Schedule.java +++ b/modules/study/src/main/java/kr/spot/schedule/domain/Schedule.java @@ -27,6 +27,8 @@ public class Schedule extends BaseEntity { private Long studyId; + private Long creatorId; + @Column(nullable = false) private String title; @@ -40,10 +42,10 @@ public class Schedule extends BaseEntity { private String attendanceQrCodeImageUrl; - public static Schedule of(Long id, Long studyId, String title, String locationMemo, + public static Schedule of(Long id, Long studyId, long creatorId, String title, String locationMemo, LocalDateTime startAt, LocalDateTime endAt ) { - return new Schedule(id, studyId, title, locationMemo, startAt, endAt, false, null); + return new Schedule(id, studyId, creatorId, title, locationMemo, startAt, endAt, false, null); } public void delete(long studyId) { diff --git a/modules/study/src/main/java/kr/spot/schedule/presentation/command/ScheduleCommandController.java b/modules/study/src/main/java/kr/spot/schedule/presentation/command/ScheduleCommandController.java index 96793bfe..0102ea1b 100644 --- a/modules/study/src/main/java/kr/spot/schedule/presentation/command/ScheduleCommandController.java +++ b/modules/study/src/main/java/kr/spot/schedule/presentation/command/ScheduleCommandController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import kr.spot.ApiResponse; +import kr.spot.annotations.CurrentMember; import kr.spot.code.status.SuccessStatus; import kr.spot.schedule.application.command.ManageScheduleService; import kr.spot.schedule.presentation.command.dto.CreateScheduleRequest; @@ -37,9 +38,10 @@ public class ScheduleCommandController { }) @PostMapping public ResponseEntity> createSchedule( + @CurrentMember @Parameter(hidden = true) Long memberId, @Parameter(description = "스터디 ID", required = true) @PathVariable Long studyId, @RequestBody CreateScheduleRequest request) { - long scheduleId = manageScheduleService.createSchedule(request, studyId); + long scheduleId = manageScheduleService.createSchedule(request, studyId, memberId); return ResponseEntity.ok( ApiResponse.onSuccess(SuccessStatus._CREATED, CreateScheduleResponse.from(scheduleId))); } diff --git a/modules/study/src/main/java/kr/spot/schedule/presentation/query/ScheduleQueryController.java b/modules/study/src/main/java/kr/spot/schedule/presentation/query/ScheduleQueryController.java index ac9c5376..bfe2b351 100644 --- a/modules/study/src/main/java/kr/spot/schedule/presentation/query/ScheduleQueryController.java +++ b/modules/study/src/main/java/kr/spot/schedule/presentation/query/ScheduleQueryController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import java.time.LocalDate; import kr.spot.ApiResponse; +import kr.spot.annotations.CurrentMember; import kr.spot.code.status.SuccessStatus; import kr.spot.schedule.application.query.GetScheduleService; import kr.spot.schedule.presentation.query.dto.GetScheduleListResponse; @@ -36,10 +37,11 @@ public class ScheduleQueryController { }) @GetMapping("/monthly") public ResponseEntity> getMonthlySchedules( + @CurrentMember @Parameter(hidden = true) Long memberId, @Parameter(description = "스터디 ID", required = true) @PathVariable Long studyId, @Parameter(description = "연도", example = "2025") @RequestParam Integer year, @Parameter(description = "월", example = "1") @RequestParam Integer month) { - GetScheduleListResponse response = getScheduleService.getMonthlySchedules(studyId, year, month); + GetScheduleListResponse response = getScheduleService.getMonthlySchedules(studyId, year, month, memberId); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } @@ -50,10 +52,11 @@ public ResponseEntity> getMonthlySchedules( }) @GetMapping("/weekly") public ResponseEntity> getWeeklySchedules( + @CurrentMember @Parameter(hidden = true) Long memberId, @Parameter(description = "스터디 ID", required = true) @PathVariable Long studyId, @Parameter(description = "조회 기준 날짜 (해당 주 전체 조회), ISO 8601 표준 방식으로 입력해주세요. ", example = "2025-01-15") @RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate date) { - GetScheduleListResponse response = getScheduleService.getWeeklySchedules(studyId, date); + GetScheduleListResponse response = getScheduleService.getWeeklySchedules(studyId, date, memberId); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } @@ -64,8 +67,9 @@ public ResponseEntity> getWeeklySchedules( }) @GetMapping("/upcoming") public ResponseEntity> getUpcomingSchedules( + @CurrentMember @Parameter(hidden = true) Long memberId, @Parameter(description = "스터디 ID", required = true) @PathVariable Long studyId) { - GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(studyId); + GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(studyId, memberId); return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._OK, response)); } } diff --git a/modules/study/src/main/java/kr/spot/schedule/presentation/query/dto/GetScheduleListResponse.java b/modules/study/src/main/java/kr/spot/schedule/presentation/query/dto/GetScheduleListResponse.java index 6e51cdac..680a6544 100644 --- a/modules/study/src/main/java/kr/spot/schedule/presentation/query/dto/GetScheduleListResponse.java +++ b/modules/study/src/main/java/kr/spot/schedule/presentation/query/dto/GetScheduleListResponse.java @@ -17,12 +17,13 @@ public record ScheduleResponse( String title, LocalDateTime startAt, LocalDateTime endAt, - boolean isNow + boolean isNow, + boolean isMine ) { public static ScheduleResponse from(Long scheduleId, String title, - LocalDateTime startAt, LocalDateTime endAt, boolean isNow) { - return new ScheduleResponse(scheduleId, title, startAt, endAt, isNow); + LocalDateTime startAt, LocalDateTime endAt, boolean isNow, boolean isMine) { + return new ScheduleResponse(scheduleId, title, startAt, endAt, isNow, isMine); } } From 9340f2a2a15f029631257680254992939d0a734f Mon Sep 17 00:00:00 2001 From: msk226 Date: Thu, 29 Jan 2026 14:08:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[Feature]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...dyApplicationNotificationListenerTest.java | 138 +++++++++--------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/modules/notification/src/test/java/kr/spot/application/event/StudyApplicationNotificationListenerTest.java b/modules/notification/src/test/java/kr/spot/application/event/StudyApplicationNotificationListenerTest.java index e326272f..99cc5002 100644 --- a/modules/notification/src/test/java/kr/spot/application/event/StudyApplicationNotificationListenerTest.java +++ b/modules/notification/src/test/java/kr/spot/application/event/StudyApplicationNotificationListenerTest.java @@ -12,6 +12,7 @@ import kr.spot.event.StudyApplicationProcessedEvent; import kr.spot.infrastructure.jpa.NotificationRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,72 +24,73 @@ @ExtendWith(MockitoExtension.class) class StudyApplicationNotificationListenerTest { - @Mock - IdGenerator idGenerator; - - @Mock - NotificationRepository notificationRepository; - - @Captor - ArgumentCaptor notificationCaptor; - - StudyApplicationNotificationListener listener; - - @BeforeEach - void setUp() { - listener = new StudyApplicationNotificationListener(idGenerator, notificationRepository); - } - - @Test - @DisplayName("승인 이벤트 수신 시 알림이 저장된다") - void should_save_notification_when_approved() { - // given - Long notificationId = 100L; - StudyApplicationProcessedEvent event = StudyApplicationProcessedEvent.of( - 1L, 2L, 3L, "APPROVE", "자바 스터디", "http://image.url/java.png"); - - when(idGenerator.nextId()).thenReturn(notificationId); - when(notificationRepository.save(any(Notification.class))) - .thenAnswer(invocation -> invocation.getArgument(0)); - - // when - listener.handle(event); - - // then - verify(notificationRepository).save(notificationCaptor.capture()); - - Notification notification = notificationCaptor.getValue(); - assertThat(notification.getId()).isEqualTo(notificationId); - assertThat(notification.getTitle()).isEqualTo("자바 스터디"); - assertThat(notification.getBody()).contains("승인"); - assertThat(notification.getMemberId()).isEqualTo(2L); - assertThat(notification.getReferenceId()).isEqualTo(1L); - assertThat(notification.getReferenceType()).isEqualTo("STUDY"); - assertThat(notification.getType()).isEqualTo(NotificationType.STUDY_APPLICATION_APPROVED); - assertThat(notification.getDispatchStatus()).isEqualTo(NotificationStatus.PENDING); - } - - @Test - @DisplayName("거절 이벤트 수신 시 알림이 저장된다") - void should_save_notification_when_rejected() { - // given - Long notificationId = 100L; - StudyApplicationProcessedEvent event = StudyApplicationProcessedEvent.of( - 1L, 2L, 3L, "REJECT", "자바 스터디", "http://image.url"); - - when(idGenerator.nextId()).thenReturn(notificationId); - when(notificationRepository.save(any(Notification.class))) - .thenAnswer(invocation -> invocation.getArgument(0)); - - // when - listener.handle(event); - - // then - verify(notificationRepository).save(notificationCaptor.capture()); - - Notification notification = notificationCaptor.getValue(); - assertThat(notification.getTitle()).isEqualTo("자바 스터디"); - assertThat(notification.getBody()).contains("거절"); - assertThat(notification.getType()).isEqualTo(NotificationType.STUDY_APPLICATION_REJECTED); - } + @Mock + IdGenerator idGenerator; + + @Mock + NotificationRepository notificationRepository; + + @Captor + ArgumentCaptor notificationCaptor; + + StudyApplicationNotificationListener listener; + + @BeforeEach + void setUp() { + listener = new StudyApplicationNotificationListener(idGenerator, notificationRepository); + } + + @Test + @DisplayName("승인 이벤트 수신 시 알림이 저장된다") + void should_save_notification_when_approved() { + // given + Long notificationId = 100L; + StudyApplicationProcessedEvent event = StudyApplicationProcessedEvent.of( + 1L, 2L, 3L, "APPROVE", "자바 스터디", "http://image.url/java.png"); + + when(idGenerator.nextId()).thenReturn(notificationId); + when(notificationRepository.save(any(Notification.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // when + listener.handle(event); + + // then + verify(notificationRepository).save(notificationCaptor.capture()); + + Notification notification = notificationCaptor.getValue(); + assertThat(notification.getId()).isEqualTo(notificationId); + assertThat(notification.getTitle()).isEqualTo("자바 스터디"); + assertThat(notification.getBody()).contains("승인"); + assertThat(notification.getMemberId()).isEqualTo(2L); + assertThat(notification.getReferenceId()).isEqualTo(1L); + assertThat(notification.getReferenceType()).isEqualTo("STUDY"); + assertThat(notification.getType()).isEqualTo(NotificationType.STUDY_APPLICATION_APPROVED); + assertThat(notification.getDispatchStatus()).isEqualTo(NotificationStatus.PENDING); + } + + @Test + @Disabled + @DisplayName("거절 이벤트 수신 시 알림이 저장된다") + void should_save_notification_when_rejected() { + // given + Long notificationId = 100L; + StudyApplicationProcessedEvent event = StudyApplicationProcessedEvent.of( + 1L, 2L, 3L, "REJECT", "자바 스터디", "http://image.url"); + + when(idGenerator.nextId()).thenReturn(notificationId); + when(notificationRepository.save(any(Notification.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // when + listener.handle(event); + + // then + verify(notificationRepository).save(notificationCaptor.capture()); + + Notification notification = notificationCaptor.getValue(); + assertThat(notification.getTitle()).isEqualTo("자바 스터디"); + assertThat(notification.getBody()).contains("거절"); + assertThat(notification.getType()).isEqualTo(NotificationType.STUDY_APPLICATION_REJECTED); + } } From 6f876c531029dbef7d0c97dd3bd7476393784b3c Mon Sep 17 00:00:00 2001 From: msk226 Date: Thu, 29 Jan 2026 14:12:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[Feature]=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/ManageScheduleServiceTest.java | 6 ++++-- .../application/query/GetScheduleServiceTest.java | 13 +++++++------ .../kr/spot/schedule/common/AttendanceFixture.java | 4 ++++ .../kr/spot/schedule/common/ScheduleFixture.java | 5 +++-- .../java/kr/spot/schedule/domain/ScheduleTest.java | 12 +++++++----- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java b/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java index 17e82a22..80960f48 100644 --- a/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java +++ b/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java @@ -1,5 +1,6 @@ package kr.spot.schedule.application.command; +import static kr.spot.schedule.common.ScheduleFixture.CREATOR_ID; import static kr.spot.schedule.common.ScheduleFixture.LOCATION_MEMO; import static kr.spot.schedule.common.ScheduleFixture.STUDY_ID; import static kr.spot.schedule.common.ScheduleFixture.TITLE; @@ -63,7 +64,7 @@ void should_create_schedule_successfully() { .thenAnswer(invocation -> invocation.getArgument(0)); // when - manageScheduleService.createSchedule(request, STUDY_ID); + manageScheduleService.createSchedule(request, STUDY_ID, CREATOR_ID); // then verify(scheduleRepository).save(scheduleCaptor.capture()); @@ -71,6 +72,7 @@ void should_create_schedule_successfully() { Schedule capturedSchedule = scheduleCaptor.getValue(); assertThat(capturedSchedule.getId()).isEqualTo(generatedId); assertThat(capturedSchedule.getStudyId()).isEqualTo(STUDY_ID); + assertThat(capturedSchedule.getCreatorId()).isEqualTo(CREATOR_ID); assertThat(capturedSchedule.getTitle()).isEqualTo(TITLE); assertThat(capturedSchedule.getLocationMemo()).isEqualTo(LOCATION_MEMO); assertThat(capturedSchedule.getStartAt()).isEqualTo(request.startAt()); @@ -91,7 +93,7 @@ TITLE, null, createScheduleRequest().startAt(), createScheduleRequest().endAt() .thenAnswer(invocation -> invocation.getArgument(0)); // when - manageScheduleService.createSchedule(request, STUDY_ID); + manageScheduleService.createSchedule(request, STUDY_ID, CREATOR_ID); // then verify(scheduleRepository).save(scheduleCaptor.capture()); diff --git a/modules/study/src/test/java/kr/spot/schedule/application/query/GetScheduleServiceTest.java b/modules/study/src/test/java/kr/spot/schedule/application/query/GetScheduleServiceTest.java index 722791a1..e9d8156e 100644 --- a/modules/study/src/test/java/kr/spot/schedule/application/query/GetScheduleServiceTest.java +++ b/modules/study/src/test/java/kr/spot/schedule/application/query/GetScheduleServiceTest.java @@ -1,5 +1,6 @@ package kr.spot.schedule.application.query; +import static kr.spot.schedule.common.ScheduleFixture.CREATOR_ID; import static kr.spot.schedule.common.ScheduleFixture.STUDY_ID; import static kr.spot.schedule.common.ScheduleFixture.schedule; import static org.assertj.core.api.Assertions.assertThat; @@ -55,7 +56,7 @@ void should_get_monthly_schedules() { .thenReturn(schedules); // when - GetScheduleListResponse response = getScheduleService.getMonthlySchedules(STUDY_ID, year, month); + GetScheduleListResponse response = getScheduleService.getMonthlySchedules(STUDY_ID, year, month, CREATOR_ID); // then assertThat(response.schedules()).hasSize(2); @@ -71,7 +72,7 @@ void should_return_empty_list_when_no_schedules() { .thenReturn(Collections.emptyList()); // when - GetScheduleListResponse response = getScheduleService.getMonthlySchedules(STUDY_ID, 2025, 1); + GetScheduleListResponse response = getScheduleService.getMonthlySchedules(STUDY_ID, 2025, 1, CREATOR_ID); // then assertThat(response.schedules()).isEmpty(); @@ -94,7 +95,7 @@ void should_get_weekly_schedules() { .thenReturn(schedules); // when - GetScheduleListResponse response = getScheduleService.getWeeklySchedules(STUDY_ID, date); + GetScheduleListResponse response = getScheduleService.getWeeklySchedules(STUDY_ID, date, CREATOR_ID); // then assertThat(response.schedules()).hasSize(1); @@ -111,7 +112,7 @@ void should_return_empty_list_when_no_schedules() { .thenReturn(Collections.emptyList()); // when - GetScheduleListResponse response = getScheduleService.getWeeklySchedules(STUDY_ID, date); + GetScheduleListResponse response = getScheduleService.getWeeklySchedules(STUDY_ID, date, CREATOR_ID); // then assertThat(response.schedules()).isEmpty(); @@ -136,7 +137,7 @@ void should_get_upcoming_schedules() { .thenReturn(schedules); // when - GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(STUDY_ID); + GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(STUDY_ID, CREATOR_ID); // then assertThat(response.schedules()).hasSize(2); @@ -151,7 +152,7 @@ void should_return_empty_list_when_no_schedules() { .thenReturn(Collections.emptyList()); // when - GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(STUDY_ID); + GetScheduleListResponse response = getScheduleService.getUpcomingSchedules(STUDY_ID, CREATOR_ID); // then assertThat(response.schedules()).isEmpty(); diff --git a/modules/study/src/test/java/kr/spot/schedule/common/AttendanceFixture.java b/modules/study/src/test/java/kr/spot/schedule/common/AttendanceFixture.java index 030b6335..d4df6d31 100644 --- a/modules/study/src/test/java/kr/spot/schedule/common/AttendanceFixture.java +++ b/modules/study/src/test/java/kr/spot/schedule/common/AttendanceFixture.java @@ -11,6 +11,7 @@ public class AttendanceFixture { public static final long SCHEDULE_ID = 100L; public static final long ATTENDANCE_ID = 200L; public static final long MEMBER_ID = 42L; + public static final long CREATOR_ID = 10L; public static final long OTHER_MEMBER_ID = 99L; public static final String MEMBER_NAME = "테스터"; @@ -34,6 +35,7 @@ public static Schedule ongoingSchedule() { return Schedule.of( SCHEDULE_ID, STUDY_ID, + CREATOR_ID, "테스트 일정", "테스트 장소", now.minusHours(1), @@ -52,6 +54,7 @@ public static Schedule pastSchedule() { return Schedule.of( SCHEDULE_ID, STUDY_ID, + CREATOR_ID, "지난 일정", "테스트 장소", now.minusHours(3), @@ -64,6 +67,7 @@ public static Schedule futureSchedule() { return Schedule.of( SCHEDULE_ID, STUDY_ID, + CREATOR_ID, "미래 일정", "테스트 장소", now.plusHours(1), diff --git a/modules/study/src/test/java/kr/spot/schedule/common/ScheduleFixture.java b/modules/study/src/test/java/kr/spot/schedule/common/ScheduleFixture.java index b0c9225c..a3f4c589 100644 --- a/modules/study/src/test/java/kr/spot/schedule/common/ScheduleFixture.java +++ b/modules/study/src/test/java/kr/spot/schedule/common/ScheduleFixture.java @@ -8,17 +8,18 @@ public class ScheduleFixture { public static final Long ID = 1L; public static final Long STUDY_ID = 100L; + public static final Long CREATOR_ID = 10L; public static final String TITLE = "Weekly Meeting"; public static final String LOCATION_MEMO = "강남역 스터디카페"; public static final LocalDateTime START_AT = LocalDateTime.of(2025, 1, 15, 14, 0); public static final LocalDateTime END_AT = LocalDateTime.of(2025, 1, 15, 16, 0); public static Schedule schedule() { - return Schedule.of(ID, STUDY_ID, TITLE, LOCATION_MEMO, START_AT, END_AT); + return Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, LOCATION_MEMO, START_AT, END_AT); } public static Schedule schedule(Long id, Long studyId) { - return Schedule.of(id, studyId, TITLE, LOCATION_MEMO, START_AT, END_AT); + return Schedule.of(id, studyId, CREATOR_ID, TITLE, LOCATION_MEMO, START_AT, END_AT); } public static CreateScheduleRequest createScheduleRequest() { diff --git a/modules/study/src/test/java/kr/spot/schedule/domain/ScheduleTest.java b/modules/study/src/test/java/kr/spot/schedule/domain/ScheduleTest.java index ef42f07b..f087b42a 100644 --- a/modules/study/src/test/java/kr/spot/schedule/domain/ScheduleTest.java +++ b/modules/study/src/test/java/kr/spot/schedule/domain/ScheduleTest.java @@ -1,5 +1,6 @@ package kr.spot.schedule.domain; +import static kr.spot.schedule.common.ScheduleFixture.CREATOR_ID; import static kr.spot.schedule.common.ScheduleFixture.END_AT; import static kr.spot.schedule.common.ScheduleFixture.ID; import static kr.spot.schedule.common.ScheduleFixture.LOCATION_MEMO; @@ -27,12 +28,13 @@ class CreateSchedule { @DisplayName("일정 객체를 정상적으로 생성할 수 있다") void should_create_schedule_successfully() { // when - Schedule schedule = Schedule.of(ID, STUDY_ID, TITLE, LOCATION_MEMO, START_AT, END_AT); + Schedule schedule = Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, LOCATION_MEMO, START_AT, END_AT); // then assertThat(schedule).isNotNull(); assertThat(schedule.getId()).isEqualTo(ID); assertThat(schedule.getStudyId()).isEqualTo(STUDY_ID); + assertThat(schedule.getCreatorId()).isEqualTo(CREATOR_ID); assertThat(schedule.getTitle()).isEqualTo(TITLE); assertThat(schedule.getLocationMemo()).isEqualTo(LOCATION_MEMO); assertThat(schedule.getStartAt()).isEqualTo(START_AT); @@ -43,7 +45,7 @@ void should_create_schedule_successfully() { @DisplayName("위치 정보 없이 일정 객체를 생성할 수 있다") void should_create_schedule_without_location() { // when - Schedule schedule = Schedule.of(ID, STUDY_ID, TITLE, null, START_AT, END_AT); + Schedule schedule = Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, null, START_AT, END_AT); // then assertThat(schedule).isNotNull(); @@ -54,7 +56,7 @@ void should_create_schedule_without_location() { @DisplayName("시작/종료 시간 없이 일정 객체를 생성할 수 있다") void should_create_schedule_without_time() { // when - Schedule schedule = Schedule.of(ID, STUDY_ID, TITLE, LOCATION_MEMO, null, null); + Schedule schedule = Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, LOCATION_MEMO, null, null); // then assertThat(schedule).isNotNull(); @@ -167,7 +169,7 @@ void should_return_false_when_now_is_after_schedule() { @DisplayName("시작 시간이 null이면 false를 반환한다") void should_return_false_when_start_time_is_null() { // given - Schedule schedule = Schedule.of(ID, STUDY_ID, TITLE, LOCATION_MEMO, null, END_AT); + Schedule schedule = Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, LOCATION_MEMO, null, END_AT); LocalDateTime now = LocalDateTime.now(); // when @@ -181,7 +183,7 @@ void should_return_false_when_start_time_is_null() { @DisplayName("종료 시간이 null이면 false를 반환한다") void should_return_false_when_end_time_is_null() { // given - Schedule schedule = Schedule.of(ID, STUDY_ID, TITLE, LOCATION_MEMO, START_AT, null); + Schedule schedule = Schedule.of(ID, STUDY_ID, CREATOR_ID, TITLE, LOCATION_MEMO, START_AT, null); LocalDateTime now = LocalDateTime.now(); // when