diff --git a/docs/task-async-api.yaml b/docs/task-async-api.yaml index 28555eb..a1dedbf 100644 --- a/docs/task-async-api.yaml +++ b/docs/task-async-api.yaml @@ -3,6 +3,9 @@ info: title: Pinit task events over RabbitMQ version: '1.0.0' description: 핀잇의 task 서버가 발행/구독 하는 이벤트 정리 + x-notes: | + Task/Schedule 분리 이후로 일정 이벤트는 V1 스케줄(작업과 독립)에서도 발행됩니다. + Task 연동 여부와 무관하게 일정 상태 이벤트는 동일한 라우팅 키를 사용합니다. servers: rabbitmq: @@ -78,6 +81,32 @@ channels: type: direct durable: true autoDelete: false + task.completed: + address: task.completed + messages: + taskCompleted: + $ref: '#/components/messages/TaskCompleted' + bindings: + amqp: + is: routingKey + exchange: + name: task.task.direct + type: direct + durable: true + autoDelete: false + task.canceled: + address: task.canceled + messages: + taskCanceled: + $ref: '#/components/messages/TaskCanceled' + bindings: + amqp: + is: routingKey + exchange: + name: task.task.direct + type: direct + durable: true + autoDelete: false operations: publishScheduleTimeUpcomingUpdated: # 발행하는 방법의 정의, 여기서 발행을 처리함을 명시 @@ -160,6 +189,38 @@ operations: messages: - $ref: '#/channels/schedule.state.completed/messages/scheduleStateCompleted' + publishTaskCompleted: + action: send + channel: + $ref: '#/channels/task.completed' + summary: 작업 완료 이벤트 발행 + messages: + - $ref: '#/channels/task.completed/messages/taskCompleted' + + subscribeTaskCompleted: + action: receive + channel: + $ref: '#/channels/task.completed' + summary: 작업 완료 이벤트 구독 + messages: + - $ref: '#/channels/task.completed/messages/taskCompleted' + + publishTaskCanceled: + action: send + channel: + $ref: '#/channels/task.canceled' + summary: 작업 취소 이벤트 발행 + messages: + - $ref: '#/channels/task.canceled/messages/taskCanceled' + + subscribeTaskCanceled: + action: receive + channel: + $ref: '#/channels/task.canceled' + summary: 작업 취소 이벤트 구독 + messages: + - $ref: '#/channels/task.canceled/messages/taskCanceled' + components: messages: ScheduleTimeUpcomingState: @@ -176,10 +237,10 @@ components: format: int64 newUpcomingTime: type: string - format: date-time + format: designatedStartTime-time occurredAt: type: string - format: date-time + format: designatedStartTime-time idempotentKey: type: string required: [ ownerId, scheduleId, newUpcomingTime, occurredAt, idempotentKey ] @@ -197,7 +258,7 @@ components: format: int64 occurredAt: type: string - format: date-time + format: designatedStartTime-time idempotentKey: type: string required: [ ownerId, scheduleId, occurredAt, idempotentKey ] @@ -218,7 +279,43 @@ components: description: 상태 전환 전 상태 occurredAt: type: string - format: date-time + format: designatedStartTime-time idempotentKey: type: string required: [ ownerId, scheduleId, beforeState, occurredAt, idempotentKey ] + TaskCompleted: + name: TaskCompleted + title: 작업 완료 정보 + payload: + type: object + properties: + ownerId: + type: integer + format: int64 + taskId: + type: integer + format: int64 + occurredAt: + type: string + format: designatedStartTime-time + idempotentKey: + type: string + required: [ ownerId, taskId, occurredAt, idempotentKey ] + TaskCanceled: + name: TaskCanceled + title: 작업 취소 정보(미완료로 되돌림 포함) + payload: + type: object + properties: + ownerId: + type: integer + format: int64 + taskId: + type: integer + format: int64 + occurredAt: + type: string + format: designatedStartTime-time + idempotentKey: + type: string + required: [ ownerId, taskId, occurredAt, idempotentKey ] diff --git a/src/main/java/me/pinitnotification/application/notification/ScheduleNotificationService.java b/src/main/java/me/pinitnotification/application/notification/ScheduleNotificationService.java index b4faece..7e03cdb 100644 --- a/src/main/java/me/pinitnotification/application/notification/ScheduleNotificationService.java +++ b/src/main/java/me/pinitnotification/application/notification/ScheduleNotificationService.java @@ -33,7 +33,12 @@ public ScheduleNotificationService(UpcomingScheduleNotificationRepository notifi public void handleUpcomingUpdated(UpcomingUpdatedCommand command) { notificationRepository.findByScheduleIdAndOwnerId(command.scheduleId(), command.ownerId()) .ifPresentOrElse( - existing -> existing.updateScheduleStartTime(resolveScheduleStartTime(command, existing), command.idempotentKey()), + existing -> notificationRepository.updateScheduleStartTimeAndIdempotentKey( + command.scheduleId(), + command.ownerId(), + resolveScheduleStartTime(command, existing), + command.idempotentKey() + ), () -> notificationRepository.save(buildNotification(command.ownerId(), command.scheduleId(), command.idempotentKey(), toStringValue(command.newUpcomingTime()))) ); } diff --git a/src/main/java/me/pinitnotification/domain/notification/UpcomingScheduleNotificationRepository.java b/src/main/java/me/pinitnotification/domain/notification/UpcomingScheduleNotificationRepository.java index f262433..1f929c1 100644 --- a/src/main/java/me/pinitnotification/domain/notification/UpcomingScheduleNotificationRepository.java +++ b/src/main/java/me/pinitnotification/domain/notification/UpcomingScheduleNotificationRepository.java @@ -8,6 +8,8 @@ public interface UpcomingScheduleNotificationRepository { Optional findByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); boolean existsByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); UpcomingScheduleNotification save(UpcomingScheduleNotification notification); + + void updateScheduleStartTimeAndIdempotentKey(Long scheduleId, Long ownerId, String scheduleStartTime, String idempotentKey); void deleteByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); void deleteAllInBatch(List notifications); } diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepository.java b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepository.java index 9140ee7..59c3013 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepository.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepository.java @@ -1,11 +1,31 @@ package me.pinitnotification.infrastructure.persistence.notification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; +import java.util.UUID; -public interface UpcomingScheduleNotificationJpaRepository extends JpaRepository { +public interface UpcomingScheduleNotificationJpaRepository extends JpaRepository { Optional findByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); boolean existsByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query(""" + UPDATE UpcomingScheduleNotificationEntity n + SET n.scheduleStartTime = :scheduleStartTime, + n.idempotentKey = :idempotentKey + WHERE n.scheduleId = :scheduleId + AND n.ownerId = :ownerId + """) + int updateScheduleStartTimeAndIdempotentKey( + @Param("scheduleId") Long scheduleId, + @Param("ownerId") Long ownerId, + @Param("scheduleStartTime") String scheduleStartTime, + @Param("idempotentKey") String idempotentKey + ); + void deleteByScheduleIdAndOwnerId(Long scheduleId, Long ownerId); } diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapter.java b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapter.java index 3797396..443ffaa 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapter.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapter.java @@ -2,6 +2,8 @@ import me.pinitnotification.domain.notification.UpcomingScheduleNotification; import me.pinitnotification.domain.notification.UpcomingScheduleNotificationRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import java.util.List; @@ -9,6 +11,8 @@ @Repository public class UpcomingScheduleNotificationRepositoryAdapter implements UpcomingScheduleNotificationRepository { + private static final Logger log = LoggerFactory.getLogger(UpcomingScheduleNotificationRepositoryAdapter.class); + private final UpcomingScheduleNotificationJpaRepository jpaRepository; public UpcomingScheduleNotificationRepositoryAdapter(UpcomingScheduleNotificationJpaRepository jpaRepository) { @@ -39,6 +43,14 @@ public UpcomingScheduleNotification save(UpcomingScheduleNotification notificati return toDomain(saved); } + @Override + public void updateScheduleStartTimeAndIdempotentKey(Long scheduleId, Long ownerId, String scheduleStartTime, String idempotentKey) { + int updatedRows = jpaRepository.updateScheduleStartTimeAndIdempotentKey(scheduleId, ownerId, scheduleStartTime, idempotentKey); + if (updatedRows == 0) { + log.debug("Skip updating notification. scheduleId={}, ownerId={} not found", scheduleId, ownerId); + } + } + @Override public void deleteByScheduleIdAndOwnerId(Long scheduleId, Long ownerId) { jpaRepository.deleteByScheduleIdAndOwnerId(scheduleId, ownerId); diff --git a/src/test/java/me/pinitnotification/application/notification/ScheduleNotificationServiceTest.java b/src/test/java/me/pinitnotification/application/notification/ScheduleNotificationServiceTest.java index bbd53a4..cefa177 100644 --- a/src/test/java/me/pinitnotification/application/notification/ScheduleNotificationServiceTest.java +++ b/src/test/java/me/pinitnotification/application/notification/ScheduleNotificationServiceTest.java @@ -59,8 +59,12 @@ void handleUpcomingUpdated_updatesExistingNotification() { scheduleNotificationService.handleUpcomingUpdated(command); - assertThat(existing.getScheduleStartTime()).isEqualTo("2024-02-01T12:00Z"); - assertThat(existing.getIdempotentKey()).isEqualTo("new-key"); + verify(notificationRepository).updateScheduleStartTimeAndIdempotentKey( + scheduleId, + ownerId, + "2024-02-01T12:00Z", + "new-key" + ); verify(notificationRepository, never()).save(any()); verifyNoInteractions(scheduleQueryPort); } diff --git a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepositoryTest.java b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepositoryTest.java index 2a32158..afacd3a 100644 --- a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepositoryTest.java +++ b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationJpaRepositoryTest.java @@ -32,4 +32,31 @@ void savesAndFindsByScheduleAndOwner() { assertThat(loaded).isPresent(); assertThat(loaded.get().getPublicId()).isNotNull(); } + + @Test + void updatesScheduleStartTimeAndIdempotentKey() { + UpcomingScheduleNotificationEntity entity = new UpcomingScheduleNotificationEntity(); + entity.setOwnerId(10L); + entity.setScheduleId(20L); + entity.setScheduleTitle("title"); + entity.setScheduleStartTime("2025-01-01T00:00:00Z"); + entity.setIdempotentKey("key-1"); + repository.save(entity); + + int updatedRows = repository.updateScheduleStartTimeAndIdempotentKey( + 20L, + 10L, + "2025-01-03T10:30:00Z", + "key-2" + ); + + assertThat(updatedRows).isEqualTo(1); + + Optional loaded = + repository.findByScheduleIdAndOwnerId(20L, 10L); + + assertThat(loaded).isPresent(); + assertThat(loaded.get().getScheduleStartTime()).isEqualTo("2025-01-03T10:30:00Z"); + assertThat(loaded.get().getIdempotentKey()).isEqualTo("key-2"); + } } diff --git a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapterTest.java b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapterTest.java index 38c8f73..f82d980 100644 --- a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapterTest.java +++ b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationRepositoryAdapterTest.java @@ -40,4 +40,26 @@ void savesAndLoadsDomainWithPublicId() { assertThat(loaded).isPresent(); assertThat(loaded.get().getId()).isEqualTo(publicId); } + + @Test + void updatesScheduleStartTimeAndIdempotentKey() { + UUID publicId = UUID.randomUUID(); + repository.save(new UpcomingScheduleNotification( + publicId, + 1L, + 2L, + "title", + "2025-01-01T00:00:00Z", + "key-1" + )); + + repository.updateScheduleStartTimeAndIdempotentKey(2L, 1L, "2025-01-02T01:00:00Z", "key-2"); + + Optional loaded = + repository.findByScheduleIdAndOwnerId(2L, 1L); + + assertThat(loaded).isPresent(); + assertThat(loaded.get().getScheduleStartTime()).isEqualTo("2025-01-02T01:00:00Z"); + assertThat(loaded.get().getIdempotentKey()).isEqualTo("key-2"); + } }