From 78ec3fdab0be1a8324d14f0328304ff162ce1655 Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 21:58:26 +0900 Subject: [PATCH 01/19] =?UTF-8?q?chore(shipment):=20application.yaml=20red?= =?UTF-8?q?is=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/shipment-service/src/main/resources/application.yaml b/shipment-service/src/main/resources/application.yaml index 8cc5056..246f601 100644 --- a/shipment-service/src/main/resources/application.yaml +++ b/shipment-service/src/main/resources/application.yaml @@ -16,3 +16,21 @@ spring: show_sql: true format_sql: true default_schema: shipment + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + + rabbitmq: + host: ${RABBITMQ_HOST:localhost} + port: 5672 + username: ${RABBITMQ_USERNAME:guest} + password: ${RABBITMQ_PASSWORD:guest} + listener: + simple: + retry: + enabled: true + max-attempts: 3 + initial-interval: 1000ms + multiplier: 2.0 From 4cc4833da7f87f26655efc5e05a0cd50f05401ca Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 21:58:45 +0900 Subject: [PATCH 02/19] =?UTF-8?q?chore(shipment):=20redis,=20feign=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shipment-service/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shipment-service/build.gradle b/shipment-service/build.gradle index c34e915..78811fd 100644 --- a/shipment-service/build.gradle +++ b/shipment-service/build.gradle @@ -45,6 +45,12 @@ dependencies { // RabbitMQ implementation 'org.springframework.boot:spring-boot-starter-amqp' + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Feign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' From f37b5caa4cfa745df75469fd8b5b4b4c8fd50c96 Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 22:13:26 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat(shipment):=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EB=8F=85/=EB=B0=9C=ED=96=89=20=EC=9D=B8?= =?UTF-8?q?=ED=94=84=EB=9D=BC=20=EA=B5=AC=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ShipmentRabbitConfig: order.created 큐 및 DLQ 설정 - OrderCreatedEvent, ShipmentCreatedEvent, ShipmentCreationFailedEvent 정의 - ProcessedSagaEventJpaEntity, Repository: DB 기반 멱등성 저장 - IdempotentSagaExecutor, IdempotentSagaHandler: Redis+DB 이중 멱등성 처리 - ProcessedSagaEventCleanupScheduler: 24시간 경과 이벤트 주기적 정리 --- .../config/ShipmentRabbitConfig.java | 66 ++++++++++++++++++- .../event/consume/OrderCreatedEvent.java | 28 ++++++++ .../event/publish/ShipmentCreatedEvent.java | 43 ++++++++++++ .../publish/ShipmentCreationFailedEvent.java | 20 ++++++ .../handler/IdempotentSagaExecutor.java | 50 ++++++++++++++ .../handler/IdempotentSagaHandler.java | 38 +++++++++++ .../ProcessedSagaEventCleanupScheduler.java | 28 ++++++++ .../ProcessedSagaEventJpaEntity.java | 36 ++++++++++ .../ProcessedSagaEventRepository.java | 9 +++ 9 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCreatedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaExecutor.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaHandler.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ProcessedSagaEventCleanupScheduler.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventJpaEntity.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventRepository.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/config/ShipmentRabbitConfig.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/config/ShipmentRabbitConfig.java index e4f3eb7..eff7b8d 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/config/ShipmentRabbitConfig.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/config/ShipmentRabbitConfig.java @@ -13,11 +13,23 @@ @Configuration public class ShipmentRabbitConfig { + // Routing Keys private static final String ROUTING_PRODUCT_STOCK_DECREASED = "product.stock.decreased"; + private static final String ROUTING_ORDER_CREATED = "order.created"; + private static final String ROUTING_ORDER_CANCELED = "order.canceled"; + // Queue Names public static final String QUEUE_SHIPMENT_PRODUCT_STOCK_DECREASED = "shipment.product.stock.decreased"; - public static final String QUEUE_SHIPMENT_PRODUCT_STOCK_DECREASED_DLQ = + public static final String QUEUE_SHIPMENT_ORDER_CREATED = "shipment.order.created"; + public static final String QUEUE_SHIPMENT_ORDER_CANCELED = "shipment.order.canceled"; + + // DLQ Names + private static final String QUEUE_SHIPMENT_PRODUCT_STOCK_DECREASED_DLQ = QUEUE_SHIPMENT_PRODUCT_STOCK_DECREASED + ".dlq"; + private static final String QUEUE_SHIPMENT_ORDER_CREATED_DLQ = QUEUE_SHIPMENT_ORDER_CREATED + ".dlq"; + private static final String QUEUE_SHIPMENT_ORDER_CANCELED_DLQ = QUEUE_SHIPMENT_ORDER_CANCELED + ".dlq"; + + // ---- product.stock.decreased ---- @Bean public Queue queueShipmentProductStockDecreased() { @@ -42,4 +54,56 @@ public Binding bindShipmentProductStockDecreasedDlq(DirectExchange sagaDlx) { .to(sagaDlx) .with(QUEUE_SHIPMENT_PRODUCT_STOCK_DECREASED_DLQ); } + + // ---- order.created ---- + + @Bean + public Queue queueShipmentOrderCreated() { + return RabbitMqConfig.durableQueue(QUEUE_SHIPMENT_ORDER_CREATED); + } + + @Bean + public Queue queueShipmentOrderCreatedDlq() { + return RabbitMqConfig.dlqQueue(QUEUE_SHIPMENT_ORDER_CREATED_DLQ); + } + + @Bean + public Binding bindShipmentOrderCreated(TopicExchange sagaExchange) { + return BindingBuilder.bind(queueShipmentOrderCreated()) + .to(sagaExchange) + .with(ROUTING_ORDER_CREATED); + } + + @Bean + public Binding bindShipmentOrderCreatedDlq(DirectExchange sagaDlx) { + return BindingBuilder.bind(queueShipmentOrderCreatedDlq()) + .to(sagaDlx) + .with(QUEUE_SHIPMENT_ORDER_CREATED_DLQ); + } + + // ---- order.canceled ---- + + @Bean + public Queue queueShipmentOrderCanceled() { + return RabbitMqConfig.durableQueue(QUEUE_SHIPMENT_ORDER_CANCELED); + } + + @Bean + public Queue queueShipmentOrderCanceledDlq() { + return RabbitMqConfig.dlqQueue(QUEUE_SHIPMENT_ORDER_CANCELED_DLQ); + } + + @Bean + public Binding bindShipmentOrderCanceled(TopicExchange sagaExchange) { + return BindingBuilder.bind(queueShipmentOrderCanceled()) + .to(sagaExchange) + .with(ROUTING_ORDER_CANCELED); + } + + @Bean + public Binding bindShipmentOrderCanceledDlq(DirectExchange sagaDlx) { + return BindingBuilder.bind(queueShipmentOrderCanceledDlq()) + .to(sagaDlx) + .with(QUEUE_SHIPMENT_ORDER_CANCELED_DLQ); + } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCreatedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCreatedEvent.java new file mode 100644 index 0000000..795bff4 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCreatedEvent.java @@ -0,0 +1,28 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.consume; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class OrderCreatedEvent extends SagaEvent { + + private UUID orderId; + private UUID ordererId; + private UUID supplierCompanyId; + private UUID receiverCompanyId; + private UUID productId; + private int quantity; + private UUID departureHubId; + private UUID arrivalHubId; + private LocalDateTime requestDeadline; + private String requestNote; + private String shipmentAddress; +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java new file mode 100644 index 0000000..8d97330 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java @@ -0,0 +1,43 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; + +@Getter +public class ShipmentCreatedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "shipment.created"; + + private UUID orderId; + private UUID shipmentId; + private UUID productId; + private int quantity; + private UUID departureHubId; + private UUID arrivalHubId; + private LocalDateTime requestDeadline; + private String requestNote; + private List routes; + + public ShipmentCreatedEvent(UUID orderId, UUID shipmentId, UUID productId, int quantity, + UUID departureHubId, UUID arrivalHubId, LocalDateTime requestDeadline, String requestNote, + List routes) { + super(EVENT_TYPE); + this.orderId = orderId; + this.shipmentId = shipmentId; + this.productId = productId; + this.quantity = quantity; + this.departureHubId = departureHubId; + this.arrivalHubId = arrivalHubId; + this.requestDeadline = requestDeadline; + this.requestNote = requestNote; + this.routes = routes; + } + + public record Route(int sequence, UUID departureHubId, UUID arrivalHubId) { + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java new file mode 100644 index 0000000..1843873 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java @@ -0,0 +1,20 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; + +import java.util.UUID; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; + +@Getter +public class ShipmentCreationFailedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "shipment.creation.failed"; + + private UUID orderId; + + public ShipmentCreationFailedEvent(UUID orderId) { + super(EVENT_TYPE); + this.orderId = orderId; + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaExecutor.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaExecutor.java new file mode 100644 index 0000000..e59d372 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaExecutor.java @@ -0,0 +1,50 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.handler; + +import java.time.Duration; +import java.util.function.Consumer; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import com.shipflow.common.messaging.event.SagaEvent; +import com.shipflow.shipmentservice.application.client.CacheClient; +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventJpaEntity; +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class IdempotentSagaExecutor { + + private static final Duration REDIS_TTL = Duration.ofHours(24); + + private final ProcessedSagaEventRepository processedSagaEventRepository; + private final CacheClient cacheClient; + + public boolean hasProcessed(String redisKey) { + return cacheClient.hasKey(redisKey); + } + + public void rewarmCache(String redisKey) { + cacheClient.set(redisKey, "1", REDIS_TTL); + } + + @Transactional + public void executeWithIdempotency(T event, String redisKey, Consumer doProcess) { + doProcess.accept(event); + + processedSagaEventRepository.save( + ProcessedSagaEventJpaEntity.of(event.getEventId(), event.getEventType()) + ); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + cacheClient.set(redisKey, "1", REDIS_TTL); + } + }); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaHandler.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaHandler.java new file mode 100644 index 0000000..24a7d24 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/IdempotentSagaHandler.java @@ -0,0 +1,38 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.handler; + +import com.shipflow.common.messaging.event.SagaEvent; +import com.shipflow.common.messaging.handler.AbstractSagaHandler; +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public abstract class IdempotentSagaHandler extends AbstractSagaHandler { + + private static final String REDIS_KEY_PREFIX = "idempotent:saga:"; + + private final ProcessedSagaEventRepository processedSagaEventRepository; + private final IdempotentSagaExecutor sagaExecutor; + + @Override + protected final void process(T event) { + String redisKey = REDIS_KEY_PREFIX + event.getEventId(); + + if (sagaExecutor.hasProcessed(redisKey)) { + log.info("[Idempotent] Skip - already processed (Redis) | eventId={}", event.getEventId()); + return; + } + + if (processedSagaEventRepository.existsById(event.getEventId())) { + log.info("[Idempotent] Skip - already processed (DB) | eventId={}", event.getEventId()); + sagaExecutor.rewarmCache(redisKey); + return; + } + + sagaExecutor.executeWithIdempotency(event, redisKey, this::doProcess); + } + + protected abstract void doProcess(T event); +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ProcessedSagaEventCleanupScheduler.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ProcessedSagaEventCleanupScheduler.java new file mode 100644 index 0000000..6da6b83 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ProcessedSagaEventCleanupScheduler.java @@ -0,0 +1,28 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.scheduler; + +import java.time.LocalDateTime; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ProcessedSagaEventCleanupScheduler { + + private final ProcessedSagaEventRepository processedSagaEventRepository; + + @Scheduled(cron = "0 0 * * * *") + @Transactional + public void cleanUpExpiredEvents() { + LocalDateTime cutoff = LocalDateTime.now().minusHours(24); + processedSagaEventRepository.deleteByProcessedAtBefore(cutoff); + log.info("[Idempotent] Cleanup expired saga events | cutoff={}", cutoff); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventJpaEntity.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventJpaEntity.java new file mode 100644 index 0000000..9344950 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventJpaEntity.java @@ -0,0 +1,36 @@ +package com.shipflow.shipmentservice.infrastructure.persistence; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "p_processed_saga_events") +public class ProcessedSagaEventJpaEntity { + + @Id + @Column(name = "event_id", length = 36, updatable = false) + private String eventId; + + @Column(name = "event_type", length = 100, nullable = false, updatable = false) + private String eventType; + + @Column(name = "processed_at", nullable = false, updatable = false) + private LocalDateTime processedAt; + + public static ProcessedSagaEventJpaEntity of(String eventId, String eventType) { + ProcessedSagaEventJpaEntity entity = new ProcessedSagaEventJpaEntity(); + entity.eventId = eventId; + entity.eventType = eventType; + entity.processedAt = LocalDateTime.now(); + return entity; + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventRepository.java new file mode 100644 index 0000000..c4c01b7 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ProcessedSagaEventRepository.java @@ -0,0 +1,9 @@ +package com.shipflow.shipmentservice.infrastructure.persistence; + +import java.time.LocalDateTime; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProcessedSagaEventRepository extends JpaRepository { + void deleteByProcessedAtBefore(LocalDateTime cutoff); +} From 0e929c8219917ce81db35690d49319b88f1bfdb6 Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 22:17:48 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat(shipment):=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HubClient 인터페이스 및 HubFeignClient/HubClientImpl 구현 - CacheClient 인터페이스 및 RedisClient 구현 - @EnableFeignClients, @EnableScheduling 활성화 - ShipmentManagerRepository: findFirstAvailableByType, findAllByType 추가 --- .../ShipmentserviceApplication.java | 2 ++ .../application/client/CacheClient.java | 11 +++++++ .../application/client/HubClient.java | 10 ++++++ .../client/dto/HubRouteResult.java | 17 ++++++++++ .../repository/ShipmentManagerRepository.java | 4 +++ .../infrastructure/client/HubClient.java | 4 --- .../infrastructure/client/HubClientImpl.java | 23 +++++++++++++ .../infrastructure/client/HubFeignClient.java | 20 +++++++++++ .../infrastructure/client/RedisClient.java | 33 +++++++++++++++++++ .../ShipmentManagerJpaRepository.java | 18 ++++++++++ .../ShipmentManagerRepositoryImpl.java | 10 ++++++ 11 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/CacheClient.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/HubClient.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/dto/HubRouteResult.java delete mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClient.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubFeignClient.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/RedisClient.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/ShipmentserviceApplication.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/ShipmentserviceApplication.java index 6ac891d..39ae2b9 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/ShipmentserviceApplication.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/ShipmentserviceApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableFeignClients +@EnableScheduling @SpringBootApplication public class ShipmentserviceApplication { diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/CacheClient.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/CacheClient.java new file mode 100644 index 0000000..c4486ed --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/CacheClient.java @@ -0,0 +1,11 @@ +package com.shipflow.shipmentservice.application.client; + +import java.time.Duration; + +public interface CacheClient { + long increment(String key); + + boolean hasKey(String key); + + void set(String key, String value, Duration ttl); +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/HubClient.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/HubClient.java new file mode 100644 index 0000000..1b25571 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/HubClient.java @@ -0,0 +1,10 @@ +package com.shipflow.shipmentservice.application.client; + +import java.util.List; +import java.util.UUID; + +import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; + +public interface HubClient { + List getHubRoutes(UUID departureHubId, UUID arrivalHubId); +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/dto/HubRouteResult.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/dto/HubRouteResult.java new file mode 100644 index 0000000..72a3b8f --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/client/dto/HubRouteResult.java @@ -0,0 +1,17 @@ +package com.shipflow.shipmentservice.application.client.dto; + +import java.math.BigDecimal; +import java.util.UUID; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class HubRouteResult { + private Integer sequence; + private UUID departureHubId; + private UUID arrivalHubId; + private BigDecimal estimatedDistance; + private Integer estimatedDuration; +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java index b02778d..d596757 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java @@ -21,4 +21,8 @@ public interface ShipmentManagerRepository { int findMaxSequenceByTypeAndHubId(ShipmentManagerType type, UUID hubId); Optional findById(UUID managerId); + + Optional findFirstAvailableByType(ShipmentManagerType type); + + List findAllByType(ShipmentManagerType type); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClient.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClient.java deleted file mode 100644 index 76cabf1..0000000 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClient.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.shipflow.shipmentservice.infrastructure.client; - -public interface HubClient { -} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java new file mode 100644 index 0000000..3b5d7ff --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java @@ -0,0 +1,23 @@ +package com.shipflow.shipmentservice.infrastructure.client; + +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Component; + +import com.shipflow.shipmentservice.application.client.HubClient; +import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class HubClientImpl implements HubClient { + + private final HubFeignClient hubFeignClient; + + @Override + public List getHubRoutes(UUID departureHubId, UUID arrivalHubId) { + return hubFeignClient.getHubRoutes(departureHubId, arrivalHubId).getData(); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubFeignClient.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubFeignClient.java new file mode 100644 index 0000000..193d4b7 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubFeignClient.java @@ -0,0 +1,20 @@ +package com.shipflow.shipmentservice.infrastructure.client; + +import java.util.List; +import java.util.UUID; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; + +@FeignClient(name = "hub-service", path = "/internal") +public interface HubFeignClient { + @GetMapping("/hub-routes") + ApiResponse> getHubRoutes( + @RequestParam UUID departureHubId, + @RequestParam UUID arrivalHubId + ); +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/RedisClient.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/RedisClient.java new file mode 100644 index 0000000..7f39e5e --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/RedisClient.java @@ -0,0 +1,33 @@ +package com.shipflow.shipmentservice.infrastructure.client; + +import java.time.Duration; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import com.shipflow.shipmentservice.application.client.CacheClient; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class RedisClient implements CacheClient { + + private final RedisTemplate redisTemplate; + + @Override + public long increment(String key) { + Long value = redisTemplate.opsForValue().increment(key); + return value != null ? value : 0L; + } + + @Override + public boolean hasKey(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + @Override + public void set(String key, String value, Duration ttl) { + redisTemplate.opsForValue().set(key, value, ttl); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java index ce1d3bc..d6d076c 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java @@ -1,5 +1,6 @@ package com.shipflow.shipmentservice.infrastructure.persistence; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -33,4 +34,21 @@ int findMaxSequenceByTypeAndHubId( ); Optional findByIdAndDeletedAtIsNull(UUID managerId); + + @Query(""" + select m from ShipmentManager m + where m.type = :type + and m.deletedAt is null + order by m.shipmentSequence asc + limit 1 + """) + Optional findFirstAvailableByType(@Param("type") ShipmentManagerType type); + + @Query(""" + select m from ShipmentManager m + where m.type = :type + and m.deletedAt is null + order by m.shipmentSequence asc + """) + List findAllByType(@Param("type") ShipmentManagerType type); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java index 0291114..1e098bc 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java @@ -45,4 +45,14 @@ public int findMaxSequenceByType(ShipmentManagerType type) { public int findMaxSequenceByTypeAndHubId(ShipmentManagerType type, UUID hubId) { return shipmentManagerJpaRepository.findMaxSequenceByTypeAndHubId(type, hubId); } + + @Override + public Optional findFirstAvailableByType(ShipmentManagerType type) { + return shipmentManagerJpaRepository.findFirstAvailableByType(type); + } + + @Override + public List findAllByType(ShipmentManagerType type) { + return shipmentManagerJpaRepository.findAllByType(type); + } } From 3327ed8b2d5211e79564658da0e69d8b22938c50 Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 22:18:53 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat(shipment):=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=B0=B0=EC=86=A1=20=EC=83=9D=EC=84=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserClient로 수신자 조회, Redis 커서 기반 허브 담당자 순번 배정 - 이벤트 수신 후 createShipment 호출 --- .../application/ShipmentService.java | 123 +++++++++++++++--- .../dto/command/CreateShipmentCommand.java | 17 +++ .../domain/repository/ShipmentRepository.java | 2 + .../handler/OrderCreatedHandler.java | 45 +++++++ .../persistence/ShipmentRepositoryImpl.java | 5 + 5 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/command/CreateShipmentCommand.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCreatedHandler.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java index d247e3d..29d94d0 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java @@ -1,5 +1,6 @@ package com.shipflow.shipmentservice.application; +import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -8,26 +9,96 @@ import org.springframework.transaction.annotation.Transactional; import com.shipflow.common.exception.BusinessException; +import com.shipflow.common.messaging.publisher.EventPublisher; +import com.shipflow.shipmentservice.application.client.CacheClient; +import com.shipflow.shipmentservice.application.client.HubClient; +import com.shipflow.shipmentservice.application.client.UserClient; +import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; +import com.shipflow.shipmentservice.application.client.dto.UserInfo; +import com.shipflow.shipmentservice.application.dto.command.CreateShipmentCommand; +import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; +import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; import com.shipflow.shipmentservice.application.dto.result.ShipmentResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteResult; -import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteUpdateResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentSearchResult; -import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; import com.shipflow.shipmentservice.application.dto.result.ShipmentUpdateResult; import com.shipflow.shipmentservice.domain.Shipment; +import com.shipflow.shipmentservice.domain.ShipmentManager; +import com.shipflow.shipmentservice.domain.ShipmentManagerType; import com.shipflow.shipmentservice.domain.ShipmentRoute; import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; +import com.shipflow.shipmentservice.domain.repository.ShipmentManagerRepository; import com.shipflow.shipmentservice.domain.repository.ShipmentRepository; +import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreatedEvent; +import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreationFailedEvent; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ShipmentService { + private static final String HUB_MANAGER_CURSOR_KEY = "shipment:hub_manager:cursor"; + private final ShipmentRepository shipmentRepository; + private final ShipmentManagerRepository shipmentManagerRepository; + private final HubClient hubClient; + private final UserClient userClient; + private final CacheClient cacheClient; + private final EventPublisher eventPublisher; + + @Transactional + public void createShipment(CreateShipmentCommand command) { + try { + ShipmentManager companyManager = findCompanyManager(); + List hubRoutes = hubClient.getHubRoutes(command.departureHubId(), command.arrivalHubId()); + List hubManagers = findHubManagers(); + + UserInfo recipient = userClient.getUser(command.ordererId()); + + List routes = buildRoutes(hubRoutes, hubManagers); + + Shipment shipment = Shipment.create( + command.orderId(), + command.departureHubId(), + command.arrivalHubId(), + command.shipmentAddress(), + recipient.getName(), + recipient.getSlackId(), + companyManager, + routes + ); + + Shipment saved = shipmentRepository.save(shipment); + + List eventRoutes = saved.getRoutes().stream() + .map(r -> new ShipmentCreatedEvent.Route(r.getSequence(), r.getDepartureHubId(), r.getArrivalHubId())) + .toList(); + + eventPublisher.publish(new ShipmentCreatedEvent( + saved.getOrderId(), + saved.getId(), + command.productId(), + command.quantity(), + saved.getDepartureHubId(), + saved.getArrivalHubId(), + command.requestDeadline(), + command.requestNote(), + eventRoutes + )); + + log.info("[Shipment] Created | shipmentId={} | orderId={}", saved.getId(), saved.getOrderId()); + + } catch (Exception e) { + log.error("[Shipment] Creation failed | orderId={} | error={}", command.orderId(), e.getMessage(), e); + eventPublisher.publish(new ShipmentCreationFailedEvent(command.orderId())); + throw e; + } + } public List searchShipment(Pageable pageable) { return shipmentRepository.findAll(pageable).stream() @@ -70,23 +141,45 @@ public ShipmentRouteUpdateResult updateShipmentRoute(UUID shipmentId, UUID route ShipmentRoute route; switch (command.getStatus()) { + case MOVING_TO_HUB -> route = shipment.markRouteMovingToHub(routeId); + case ARRIVED_AT_HUB -> route = shipment.markRouteArrivedAtHub(routeId, command.getActualDistance()); + default -> throw new BusinessException(ShipmentErrorCode.INVALID_SHIPMENT_ROUTE_STATUS); + } - case MOVING_TO_HUB -> { - route = shipment.markRouteMovingToHub(routeId); - } + return ShipmentRouteUpdateResult.fromEntity(route); + } - case ARRIVED_AT_HUB -> { - route = shipment.markRouteArrivedAtHub( - routeId, - command.getActualDistance() - ); - } + private ShipmentManager findCompanyManager() { + return shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY) + .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND)); + } - default -> { - throw new BusinessException(ShipmentErrorCode.INVALID_SHIPMENT_ROUTE_STATUS); - } + private List findHubManagers() { + List managers = shipmentManagerRepository.findAllByType(ShipmentManagerType.HUB); + if (managers.isEmpty()) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND); } + return managers; + } - return ShipmentRouteUpdateResult.fromEntity(route); + private List buildRoutes(List hubRoutes, List hubManagers) { + long cursor = cacheClient.increment(HUB_MANAGER_CURSOR_KEY); + int startIndex = (int)((cursor - 1) % hubManagers.size()); + + return hubRoutes.stream() + .sorted(Comparator.comparingInt(HubRouteResult::getSequence)) + .map(route -> { + int managerIndex = (startIndex + route.getSequence() - 1) % hubManagers.size(); + ShipmentManager assignedManager = hubManagers.get(managerIndex); + return ShipmentRoute.create( + route.getSequence(), + route.getDepartureHubId(), + route.getArrivalHubId(), + route.getEstimatedDistance(), + route.getEstimatedDuration(), + assignedManager + ); + }) + .toList(); } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/command/CreateShipmentCommand.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/command/CreateShipmentCommand.java new file mode 100644 index 0000000..630102d --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/command/CreateShipmentCommand.java @@ -0,0 +1,17 @@ +package com.shipflow.shipmentservice.application.dto.command; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record CreateShipmentCommand( + UUID orderId, + UUID ordererId, + UUID productId, + int quantity, + UUID departureHubId, + UUID arrivalHubId, + LocalDateTime requestDeadline, + String requestNote, + String shipmentAddress +) { +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java index cae2728..46cb138 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java @@ -9,6 +9,8 @@ import com.shipflow.shipmentservice.domain.Shipment; public interface ShipmentRepository { + Shipment save(Shipment shipment); + Optional findById(UUID shipmentId); Optional findByIdWithManager(UUID shipmentId); diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCreatedHandler.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCreatedHandler.java new file mode 100644 index 0000000..f3928aa --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCreatedHandler.java @@ -0,0 +1,45 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.handler; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.shipmentservice.application.ShipmentService; +import com.shipflow.shipmentservice.application.dto.command.CreateShipmentCommand; +import com.shipflow.shipmentservice.infrastructure.messaging.config.ShipmentRabbitConfig; +import com.shipflow.shipmentservice.infrastructure.messaging.event.consume.OrderCreatedEvent; +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventRepository; + +@Component +public class OrderCreatedHandler extends IdempotentSagaHandler { + + private final ShipmentService shipmentService; + + public OrderCreatedHandler( + ProcessedSagaEventRepository processedSagaEventRepository, + IdempotentSagaExecutor sagaExecutor, + ShipmentService shipmentService + ) { + super(processedSagaEventRepository, sagaExecutor); + this.shipmentService = shipmentService; + } + + @RabbitListener(queues = ShipmentRabbitConfig.QUEUE_SHIPMENT_ORDER_CREATED) + public void receive(OrderCreatedEvent event) { + handle(event); + } + + @Override + protected void doProcess(OrderCreatedEvent event) { + shipmentService.createShipment(new CreateShipmentCommand( + event.getOrderId(), + event.getOrdererId(), + event.getProductId(), + event.getQuantity(), + event.getDepartureHubId(), + event.getArrivalHubId(), + event.getRequestDeadline(), + event.getRequestNote(), + event.getShipmentAddress() + )); + } +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java index ec65120..36f90ec 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java @@ -18,6 +18,11 @@ public class ShipmentRepositoryImpl implements ShipmentRepository { private final ShipmentJpaRepository shipmentJpaRepository; + @Override + public Shipment save(Shipment shipment) { + return shipmentJpaRepository.save(shipment); + } + @Override public Optional findById(UUID shipmentId) { return shipmentJpaRepository.findById(shipmentId); From 39db1e63da40649e2481f66f495e8f1fb3d8e158 Mon Sep 17 00:00:00 2001 From: soo Date: Mon, 6 Apr 2026 22:26:47 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat(shipment):=20HubClient=20=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=EA=B2=BD=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exception/ShipmentErrorCode.java | 2 ++ .../infrastructure/client/HubClientImpl.java | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java index 65a7a9d..87ce28d 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java @@ -29,6 +29,8 @@ public enum ShipmentErrorCode implements ErrorCode { // External Service USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), USER_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "사용자 서비스에 연결할 수 없습니다."), + HUB_ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "허브 경로를 찾을 수 없습니다."), + HUB_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "허브 서비스에 연결할 수 없습니다."), // Validation INVALID_ACTUAL_DISTANCE(HttpStatus.BAD_REQUEST, "실제 이동 거리는 0보다 커야 합니다."), diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java index 3b5d7ff..f54b41c 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/client/HubClientImpl.java @@ -5,11 +5,17 @@ import org.springframework.stereotype.Component; +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.common.exception.BusinessException; import com.shipflow.shipmentservice.application.client.HubClient; import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; +import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; +import feign.FeignException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @RequiredArgsConstructor public class HubClientImpl implements HubClient { @@ -18,6 +24,23 @@ public class HubClientImpl implements HubClient { @Override public List getHubRoutes(UUID departureHubId, UUID arrivalHubId) { - return hubFeignClient.getHubRoutes(departureHubId, arrivalHubId).getData(); + try { + ApiResponse> response = hubFeignClient.getHubRoutes(departureHubId, arrivalHubId); + + List routes = response.getData(); + if (routes == null || routes.isEmpty()) { + throw new BusinessException(ShipmentErrorCode.HUB_ROUTE_NOT_FOUND); + } + + return routes; + } catch (BusinessException e) { + throw e; + } catch (FeignException.NotFound e) { + log.warn("[HubClient] 허브 경로 없음 | departureHubId={} | arrivalHubId={}", departureHubId, arrivalHubId); + throw new BusinessException(ShipmentErrorCode.HUB_ROUTE_NOT_FOUND); + } catch (FeignException e) { + log.error("[HubClient] 허브 서비스 호출 실패 | status={} | message={}", e.status(), e.getMessage()); + throw new BusinessException(ShipmentErrorCode.HUB_SERVICE_UNAVAILABLE); + } } } From 16c92fbf0eb3df3c1432ef66e9b5012e98164e9f Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 00:10:19 +0900 Subject: [PATCH 07/19] =?UTF-8?q?refactor(shipment):=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=9D=98=20infra=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A7=81=EC=A0=91=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - domain/event에 ShipmentCreatedEvent, ShipmentCreationFailedEvent 레코드 정의 - ShipmentEventPublisher 인터페이스를 application 레이어에 추가 - ShipmentEventPublisherImpl: domain 레코드를 SagaEvent로 변환 후 발행 --- .../application/ShipmentEventPublisher.java | 11 +++++ .../application/ShipmentService.java | 11 +++-- .../domain/event/ShipmentCreatedEvent.java | 20 +++++++++ .../event/ShipmentCreationFailedEvent.java | 6 +++ .../messaging/ShipmentEventPublisherImpl.java | 29 ++++++++++++ .../event/publish/ShipmentCreatedEvent.java | 43 ------------------ .../publish/ShipmentCreatedSagaEvent.java | 44 +++++++++++++++++++ ...a => ShipmentCreationFailedSagaEvent.java} | 8 ++-- 8 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreationFailedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java delete mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java rename shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/{ShipmentCreationFailedEvent.java => ShipmentCreationFailedSagaEvent.java} (67%) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java new file mode 100644 index 0000000..a2029eb --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java @@ -0,0 +1,11 @@ +package com.shipflow.shipmentservice.application; + +import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; + +public interface ShipmentEventPublisher { + + void publishCreated(ShipmentCreatedEvent event); + + void publishCreationFailed(ShipmentCreationFailedEvent event); +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java index 29d94d0..0fb7667 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java @@ -9,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import com.shipflow.common.exception.BusinessException; -import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.shipmentservice.application.client.CacheClient; import com.shipflow.shipmentservice.application.client.HubClient; import com.shipflow.shipmentservice.application.client.UserClient; @@ -27,11 +26,11 @@ import com.shipflow.shipmentservice.domain.ShipmentManager; import com.shipflow.shipmentservice.domain.ShipmentManagerType; import com.shipflow.shipmentservice.domain.ShipmentRoute; +import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; import com.shipflow.shipmentservice.domain.repository.ShipmentManagerRepository; import com.shipflow.shipmentservice.domain.repository.ShipmentRepository; -import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreatedEvent; -import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreationFailedEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -49,7 +48,7 @@ public class ShipmentService { private final HubClient hubClient; private final UserClient userClient; private final CacheClient cacheClient; - private final EventPublisher eventPublisher; + private final ShipmentEventPublisher eventPublisher; @Transactional public void createShipment(CreateShipmentCommand command) { @@ -79,7 +78,7 @@ public void createShipment(CreateShipmentCommand command) { .map(r -> new ShipmentCreatedEvent.Route(r.getSequence(), r.getDepartureHubId(), r.getArrivalHubId())) .toList(); - eventPublisher.publish(new ShipmentCreatedEvent( + eventPublisher.publishCreated(new ShipmentCreatedEvent( saved.getOrderId(), saved.getId(), command.productId(), @@ -95,7 +94,7 @@ public void createShipment(CreateShipmentCommand command) { } catch (Exception e) { log.error("[Shipment] Creation failed | orderId={} | error={}", command.orderId(), e.getMessage(), e); - eventPublisher.publish(new ShipmentCreationFailedEvent(command.orderId())); + eventPublisher.publishCreationFailed(new ShipmentCreationFailedEvent(command.orderId())); throw e; } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java new file mode 100644 index 0000000..cf54d61 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java @@ -0,0 +1,20 @@ +package com.shipflow.shipmentservice.domain.event; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public record ShipmentCreatedEvent( + UUID orderId, + UUID shipmentId, + UUID productId, + int quantity, + UUID departureHubId, + UUID arrivalHubId, + LocalDateTime requestDeadline, + String requestNote, + List routes +) { + public record Route(int sequence, UUID departureHubId, UUID arrivalHubId) { + } +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreationFailedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreationFailedEvent.java new file mode 100644 index 0000000..2fc5183 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreationFailedEvent.java @@ -0,0 +1,6 @@ +package com.shipflow.shipmentservice.domain.event; + +import java.util.UUID; + +public record ShipmentCreationFailedEvent(UUID orderId) { +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java new file mode 100644 index 0000000..73745ca --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java @@ -0,0 +1,29 @@ +package com.shipflow.shipmentservice.infrastructure.messaging; + +import org.springframework.stereotype.Component; + +import com.shipflow.common.messaging.publisher.EventPublisher; +import com.shipflow.shipmentservice.application.ShipmentEventPublisher; +import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; +import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreatedSagaEvent; +import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreationFailedSagaEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ShipmentEventPublisherImpl implements ShipmentEventPublisher { + + private final EventPublisher eventPublisher; + + @Override + public void publishCreated(ShipmentCreatedEvent event) { + eventPublisher.publish(new ShipmentCreatedSagaEvent(event)); + } + + @Override + public void publishCreationFailed(ShipmentCreationFailedEvent event) { + eventPublisher.publish(new ShipmentCreationFailedSagaEvent(event.orderId())); + } +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java deleted file mode 100644 index 8d97330..0000000 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; - -import com.shipflow.common.messaging.event.SagaEvent; - -import lombok.Getter; - -@Getter -public class ShipmentCreatedEvent extends SagaEvent { - - private static final String EVENT_TYPE = "shipment.created"; - - private UUID orderId; - private UUID shipmentId; - private UUID productId; - private int quantity; - private UUID departureHubId; - private UUID arrivalHubId; - private LocalDateTime requestDeadline; - private String requestNote; - private List routes; - - public ShipmentCreatedEvent(UUID orderId, UUID shipmentId, UUID productId, int quantity, - UUID departureHubId, UUID arrivalHubId, LocalDateTime requestDeadline, String requestNote, - List routes) { - super(EVENT_TYPE); - this.orderId = orderId; - this.shipmentId = shipmentId; - this.productId = productId; - this.quantity = quantity; - this.departureHubId = departureHubId; - this.arrivalHubId = arrivalHubId; - this.requestDeadline = requestDeadline; - this.requestNote = requestNote; - this.routes = routes; - } - - public record Route(int sequence, UUID departureHubId, UUID arrivalHubId) { - } -} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java new file mode 100644 index 0000000..c3b3af3 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java @@ -0,0 +1,44 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import com.shipflow.common.messaging.event.SagaEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; + +import lombok.Getter; + +@Getter +public class ShipmentCreatedSagaEvent extends SagaEvent { + + private static final String EVENT_TYPE = "shipment.created"; + + private final UUID orderId; + private final UUID shipmentId; + private final UUID productId; + private final int quantity; + private final UUID departureHubId; + private final UUID arrivalHubId; + private final LocalDateTime requestDeadline; + private final String requestNote; + private final List routes; + + public ShipmentCreatedSagaEvent(ShipmentCreatedEvent event) { + super(EVENT_TYPE); + this.orderId = event.orderId(); + this.shipmentId = event.shipmentId(); + this.productId = event.productId(); + this.quantity = event.quantity(); + this.departureHubId = event.departureHubId(); + this.arrivalHubId = event.arrivalHubId(); + this.requestDeadline = event.requestDeadline(); + this.requestNote = event.requestNote(); + this.routes = event.routes().stream() + .map(r -> new Route(r.sequence(), r.departureHubId(), r.arrivalHubId())) + .toList(); + } + + public record Route(int sequence, UUID departureHubId, UUID arrivalHubId) { + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedSagaEvent.java similarity index 67% rename from shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java rename to shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedSagaEvent.java index 1843873..b735d1f 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedEvent.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreationFailedSagaEvent.java @@ -7,14 +7,14 @@ import lombok.Getter; @Getter -public class ShipmentCreationFailedEvent extends SagaEvent { +public class ShipmentCreationFailedSagaEvent extends SagaEvent { private static final String EVENT_TYPE = "shipment.creation.failed"; - private UUID orderId; + private final UUID orderId; - public ShipmentCreationFailedEvent(UUID orderId) { + public ShipmentCreationFailedSagaEvent(UUID orderId) { super(EVENT_TYPE); this.orderId = orderId; } -} +} \ No newline at end of file From 1a06883f5cff023a514766f4dc5d2992a4eb3a90 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 01:30:18 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat(shipment):=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/shipments/{shipmentId}/complete api 추가 - ShipmentCompletedEvent 발행 --- .../application/ShipmentEventPublisher.java | 5 +++- .../application/ShipmentService.java | 17 +++++++++++ .../dto/result/ShipmentCompleteResult.java | 29 +++++++++++++++++++ .../shipmentservice/domain/Shipment.java | 22 ++++++++++++++ .../domain/event/ShipmentCompletedEvent.java | 6 ++++ .../domain/exception/ShipmentErrorCode.java | 3 ++ .../messaging/ShipmentEventPublisherImpl.java | 9 +++++- .../publish/ShipmentCompletedSagaEvent.java | 26 +++++++++++++++++ .../presentation/ShipmentController.java | 10 +++++++ .../dto/response/ShipmentCompleteResDto.java | 29 +++++++++++++++++++ 10 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCompleteResult.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCompletedEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/dto/response/ShipmentCompleteResDto.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java index a2029eb..8cad29c 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentEventPublisher.java @@ -1,5 +1,6 @@ package com.shipflow.shipmentservice.application; +import com.shipflow.shipmentservice.domain.event.ShipmentCompletedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; @@ -8,4 +9,6 @@ public interface ShipmentEventPublisher { void publishCreated(ShipmentCreatedEvent event); void publishCreationFailed(ShipmentCreationFailedEvent event); -} \ No newline at end of file + + void publishCompleted(ShipmentCompletedEvent event); +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java index 0fb7667..87abb60 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java @@ -17,6 +17,7 @@ import com.shipflow.shipmentservice.application.dto.command.CreateShipmentCommand; import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; +import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteUpdateResult; @@ -26,6 +27,7 @@ import com.shipflow.shipmentservice.domain.ShipmentManager; import com.shipflow.shipmentservice.domain.ShipmentManagerType; import com.shipflow.shipmentservice.domain.ShipmentRoute; +import com.shipflow.shipmentservice.domain.event.ShipmentCompletedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; @@ -148,6 +150,21 @@ public ShipmentRouteUpdateResult updateShipmentRoute(UUID shipmentId, UUID route return ShipmentRouteUpdateResult.fromEntity(route); } + @Transactional + public ShipmentCompleteResult completeShipment(UUID shipmentId) { + Shipment shipment = shipmentRepository.findByIdWithRoutes(shipmentId) + .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_NOT_FOUND)); + + shipment.markCompleted(); + + eventPublisher.publishCompleted(new ShipmentCompletedEvent( + shipment.getOrderId(), + shipment.getId() + )); + + return ShipmentCompleteResult.fromEntity(shipment); + } + private ShipmentManager findCompanyManager() { return shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY) .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND)); diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCompleteResult.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCompleteResult.java new file mode 100644 index 0000000..76a28a4 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCompleteResult.java @@ -0,0 +1,29 @@ +package com.shipflow.shipmentservice.application.dto.result; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.shipmentservice.domain.Shipment; +import com.shipflow.shipmentservice.domain.ShipmentStatus; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ShipmentCompleteResult { + + private UUID shipmentId; + private UUID orderId; + private ShipmentStatus status; + private LocalDateTime completedAt; + + public static ShipmentCompleteResult fromEntity(Shipment shipment) { + return ShipmentCompleteResult.builder() + .shipmentId(shipment.getId()) + .orderId(shipment.getOrderId()) + .status(shipment.getStatus()) + .completedAt(shipment.getUpdatedAt()) + .build(); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java index b1d767e..990f3d5 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java @@ -114,6 +114,28 @@ public static Shipment create( return shipment; } + public void markCompleted() { + validateCompletable(); + this.status = ShipmentStatus.COMPLETED; + } + + private void validateCompletable() { + if (this.status == ShipmentStatus.COMPLETED) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_ALREADY_COMPLETED); + } + + if (this.status == ShipmentStatus.CANCELLED) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_ALREADY_CANCELLED); + } + + boolean allRoutesCompleted = routes.stream() + .allMatch(r -> r.getStatus() == ShipmentRouteStatus.ARRIVED_AT_HUB); + + if (!allRoutesCompleted) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_ROUTES_NOT_ALL_COMPLETED); + } + } + public void addRoute(ShipmentRoute route) { this.routes.add(route); route.assignShipment(this); diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCompletedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCompletedEvent.java new file mode 100644 index 0000000..4e46c5c --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCompletedEvent.java @@ -0,0 +1,6 @@ +package com.shipflow.shipmentservice.domain.event; + +import java.util.UUID; + +public record ShipmentCompletedEvent(UUID orderId, UUID shipmentId) { +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java index 87ce28d..82e14bf 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java @@ -9,6 +9,9 @@ public enum ShipmentErrorCode implements ErrorCode { // Shipment SHIPMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "배송을 찾을 수 없습니다."), INVALID_SHIPMENT_STATUS(HttpStatus.BAD_REQUEST, "잘못된 배송 상태입니다."), + SHIPMENT_ALREADY_COMPLETED(HttpStatus.CONFLICT, "이미 완료된 배송입니다."), + SHIPMENT_ALREADY_CANCELLED(HttpStatus.CONFLICT, "이미 취소된 배송입니다."), + SHIPMENT_ROUTES_NOT_ALL_COMPLETED(HttpStatus.BAD_REQUEST, "완료되지 않은 배송 경로가 존재합니다."), // ShipmentRoute SHIPMENT_ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "배송 경로를 찾을 수 없습니다."), diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java index 73745ca..6d912fc 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/ShipmentEventPublisherImpl.java @@ -4,8 +4,10 @@ import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.shipmentservice.application.ShipmentEventPublisher; +import com.shipflow.shipmentservice.domain.event.ShipmentCompletedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCreationFailedEvent; +import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCompletedSagaEvent; import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreatedSagaEvent; import com.shipflow.shipmentservice.infrastructure.messaging.event.publish.ShipmentCreationFailedSagaEvent; @@ -26,4 +28,9 @@ public void publishCreated(ShipmentCreatedEvent event) { public void publishCreationFailed(ShipmentCreationFailedEvent event) { eventPublisher.publish(new ShipmentCreationFailedSagaEvent(event.orderId())); } -} \ No newline at end of file + + @Override + public void publishCompleted(ShipmentCompletedEvent event) { + eventPublisher.publish(new ShipmentCompletedSagaEvent(event)); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java new file mode 100644 index 0000000..cc8964d --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java @@ -0,0 +1,26 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import com.shipflow.common.messaging.event.SagaEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCompletedEvent; +import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; + +import lombok.Getter; + +@Getter +public class ShipmentCompletedSagaEvent extends SagaEvent { + + private static final String EVENT_TYPE = "shipment.completed"; + + private final UUID orderId; + private final UUID shipmentId; + + public ShipmentCompletedSagaEvent(ShipmentCompletedEvent event) { + super(EVENT_TYPE); + this.orderId = event.orderId(); + this.shipmentId = event.shipmentId(); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java index 0dab227..95570e0 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java @@ -7,11 +7,13 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; import com.shipflow.shipmentservice.application.ShipmentService; +import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteUpdateResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentUpdateResult; import com.shipflow.shipmentservice.presentation.dto.response.GetShipmentResDto; @@ -20,6 +22,7 @@ import com.shipflow.shipmentservice.presentation.dto.response.PatchShipmentResDto; import com.shipflow.shipmentservice.presentation.dto.request.PatchShipmentRouteReqDto; import com.shipflow.shipmentservice.presentation.dto.response.PatchShipmentRouteResDto; +import com.shipflow.shipmentservice.presentation.dto.response.ShipmentCompleteResDto; import com.shipflow.shipmentservice.presentation.dto.response.ShipmentSearchResDto; import jakarta.validation.Valid; @@ -97,4 +100,11 @@ public ApiResponse updateShipmentRoute( return ApiResponse.ok(PatchShipmentRouteResDto.fromResult(result)); } + @PostMapping("/api/shipments/{shipmentId}/complete") + public ApiResponse completeShipment( + @PathVariable UUID shipmentId + ) { + ShipmentCompleteResult result = shipmentService.completeShipment(shipmentId); + return ApiResponse.ok(ShipmentCompleteResDto.fromResult(result)); + } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/dto/response/ShipmentCompleteResDto.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/dto/response/ShipmentCompleteResDto.java new file mode 100644 index 0000000..2498e41 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/dto/response/ShipmentCompleteResDto.java @@ -0,0 +1,29 @@ +package com.shipflow.shipmentservice.presentation.dto.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; +import com.shipflow.shipmentservice.domain.ShipmentStatus; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ShipmentCompleteResDto { + + private UUID shipmentId; + private UUID orderId; + private ShipmentStatus status; + private LocalDateTime completedAt; + + public static ShipmentCompleteResDto fromResult(ShipmentCompleteResult result) { + return ShipmentCompleteResDto.builder() + .shipmentId(result.getShipmentId()) + .orderId(result.getOrderId()) + .status(result.getStatus()) + .completedAt(result.getCompletedAt()) + .build(); + } +} From e774ed0fd0b35d1dfccb20d900516fb294be1eae Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 07:19:26 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat(shipment):=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrderCanceledEvent 구독 --- .../event/consume/ShipmentCompletedEvent.java | 15 ++++--- .../event/consume/ShipmentCreatedEvent.java | 43 ++++++++++--------- .../event/publish/OrderCanceledEvent.java | 25 +++++------ .../application/ShipmentService.java | 11 +++++ .../dto/result/ShipmentCanceledResult.java | 29 +++++++++++++ .../shipmentservice/domain/Shipment.java | 23 +++++++++- .../shipmentservice/domain/ShipmentRoute.java | 4 ++ .../domain/ShipmentRouteStatus.java | 3 +- .../domain/exception/ShipmentErrorCode.java | 1 + .../domain/repository/ShipmentRepository.java | 2 + .../event/consume/OrderCanceledEvent.java | 18 ++++++++ .../publish/ShipmentCompletedSagaEvent.java | 3 -- .../handler/OrderCanceledHandler.java | 34 +++++++++++++++ .../persistence/ShipmentJpaRepository.java | 8 ++++ .../persistence/ShipmentRepositoryImpl.java | 5 +++ 15 files changed, 179 insertions(+), 45 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCanceledResult.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCanceledEvent.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCanceledHandler.java diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCompletedEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCompletedEvent.java index 660f235..212716c 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCompletedEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCompletedEvent.java @@ -1,21 +1,22 @@ package com.shipflow.orderservice.infrastructure.messaging.event.consume; +import java.util.UUID; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.shipflow.common.messaging.event.SagaEvent; + import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Getter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class ShipmentCompletedEvent extends SagaEvent { - private UUID orderId; + private UUID orderId; - public ShipmentCompletedEvent(UUID orderId) { - super("shipment.completed"); - this.orderId = orderId; - } + public ShipmentCompletedEvent(UUID orderId) { + super("shipment.completed"); + this.orderId = orderId; + } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCreatedEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCreatedEvent.java index a211d91..3db46f8 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCreatedEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/consume/ShipmentCreatedEvent.java @@ -1,36 +1,37 @@ package com.shipflow.orderservice.infrastructure.messaging.event.consume; +import java.util.UUID; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.shipflow.common.messaging.event.SagaEvent; import com.shipflow.orderservice.domain.model.ShipmentStatus; + import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Getter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class ShipmentCreatedEvent extends SagaEvent { - private UUID orderId; - private UUID shipmentId; - private ShipmentStatus shipmentStatus; - private UUID departureHubId; - private String departureHubName; - private UUID arrivalHubId; - private String arrivalHubName; + private UUID orderId; + private UUID shipmentId; + private ShipmentStatus shipmentStatus; + private UUID departureHubId; + private String departureHubName; + private UUID arrivalHubId; + private String arrivalHubName; - public ShipmentCreatedEvent(UUID orderId, UUID shipmentId, ShipmentStatus shipmentStatus, - UUID departureHubId, String departureHubName, - UUID arrivalHubId, String arrivalHubName) { - super("shipment.created"); - this.orderId = orderId; - this.shipmentId = shipmentId; - this.shipmentStatus = shipmentStatus; - this.departureHubId = departureHubId; - this.departureHubName = departureHubName; - this.arrivalHubId = arrivalHubId; - this.arrivalHubName = arrivalHubName; - } + public ShipmentCreatedEvent(UUID orderId, UUID shipmentId, ShipmentStatus shipmentStatus, + UUID departureHubId, String departureHubName, + UUID arrivalHubId, String arrivalHubName) { + super("shipment.created"); + this.orderId = orderId; + this.shipmentId = shipmentId; + this.shipmentStatus = shipmentStatus; + this.departureHubId = departureHubId; + this.departureHubName = departureHubName; + this.arrivalHubId = arrivalHubId; + this.arrivalHubName = arrivalHubName; + } } diff --git a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCanceledEvent.java b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCanceledEvent.java index 38599d8..dd5a7eb 100644 --- a/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCanceledEvent.java +++ b/order-service/src/main/java/com/shipflow/orderservice/infrastructure/messaging/event/publish/OrderCanceledEvent.java @@ -1,25 +1,26 @@ package com.shipflow.orderservice.infrastructure.messaging.event.publish; +import java.util.UUID; + import com.shipflow.common.messaging.event.SagaEvent; + import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Getter @NoArgsConstructor public class OrderCanceledEvent extends SagaEvent { - private static final String EVENT_TYPE = "order.canceled"; + private static final String EVENT_TYPE = "order.canceled"; - private UUID orderId; - private UUID productId; - private int quantity; + private UUID orderId; + private UUID productId; + private int quantity; - public OrderCanceledEvent(UUID orderId, UUID productId, int quantity) { - super(EVENT_TYPE); - this.orderId = orderId; - this.productId = productId; - this.quantity = quantity; - } + public OrderCanceledEvent(UUID orderId, UUID productId, int quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java index 87abb60..d8fa3b5 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java @@ -17,6 +17,7 @@ import com.shipflow.shipmentservice.application.dto.command.CreateShipmentCommand; import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; +import com.shipflow.shipmentservice.application.dto.result.ShipmentCanceledResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteResult; @@ -165,6 +166,16 @@ public ShipmentCompleteResult completeShipment(UUID shipmentId) { return ShipmentCompleteResult.fromEntity(shipment); } + @Transactional + public ShipmentCanceledResult cancelShipment(UUID orderId) { + Shipment shipment = shipmentRepository.findByOrderIdWithRoutes(orderId) + .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_NOT_FOUND)); + + shipment.markCanceled(); + + return ShipmentCanceledResult.fromEntity(shipment); + } + private ShipmentManager findCompanyManager() { return shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY) .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND)); diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCanceledResult.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCanceledResult.java new file mode 100644 index 0000000..76157e0 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/dto/result/ShipmentCanceledResult.java @@ -0,0 +1,29 @@ +package com.shipflow.shipmentservice.application.dto.result; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.shipmentservice.domain.Shipment; +import com.shipflow.shipmentservice.domain.ShipmentStatus; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ShipmentCanceledResult { + + private UUID shipmentId; + private UUID orderId; + private ShipmentStatus status; + private LocalDateTime canceledAt; + + public static ShipmentCanceledResult fromEntity(Shipment shipment) { + return ShipmentCanceledResult.builder() + .shipmentId(shipment.getId()) + .orderId(shipment.getOrderId()) + .status(shipment.getStatus()) + .canceledAt(shipment.getUpdatedAt()) + .build(); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java index 990f3d5..885326a 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -119,6 +120,12 @@ public void markCompleted() { this.status = ShipmentStatus.COMPLETED; } + public void markCanceled() { + validateCancelable(); + this.status = ShipmentStatus.CANCELLED; + markAllRoutesCanceled(); + } + private void validateCompletable() { if (this.status == ShipmentStatus.COMPLETED) { throw new BusinessException(ShipmentErrorCode.SHIPMENT_ALREADY_COMPLETED); @@ -136,6 +143,16 @@ private void validateCompletable() { } } + private void validateCancelable() { + if (this.status == ShipmentStatus.CANCELLED) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_ALREADY_CANCELLED); + } + + if (this.status != ShipmentStatus.WAITING_AT_HUB) { + throw new BusinessException(ShipmentErrorCode.SHIPMENT_NOT_CANCELABLE_STATUS); + } + } + public void addRoute(ShipmentRoute route) { this.routes.add(route); route.assignShipment(this); @@ -160,10 +177,14 @@ public ShipmentRoute markRouteArrivedAtHub(UUID routeId, BigDecimal actualDistan return route; } + private void markAllRoutesCanceled() { + routes.forEach(ShipmentRoute::markCanceled); + } + private LocalDateTime getArrivalBaseTime(ShipmentRoute currentRoute) { ShipmentRoute previousRoute = routes.stream() .filter(r -> r.getSequence() < currentRoute.getSequence()) - .max((a, b) -> Integer.compare(a.getSequence(), b.getSequence())) + .max(Comparator.comparingInt(ShipmentRoute::getSequence)) .orElse(null); // 첫 번째 경로인 경우: 현재 경로가 MOVING_TO_HUB 로 변경된 시점 기준 diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRoute.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRoute.java index c90baf3..83d1ab0 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRoute.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRoute.java @@ -118,6 +118,10 @@ void assignShipment(Shipment shipment) { this.shipment = shipment; } + public void markCanceled() { + this.status = ShipmentRouteStatus.CANCELED; + } + public void markMovingToHub() { validateMovableToHub(); this.status = ShipmentRouteStatus.MOVING_TO_HUB; diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRouteStatus.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRouteStatus.java index d8a6c46..dfa07d1 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRouteStatus.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentRouteStatus.java @@ -3,5 +3,6 @@ public enum ShipmentRouteStatus { WAITING_AT_HUB, MOVING_TO_HUB, - ARRIVED_AT_HUB + ARRIVED_AT_HUB, + CANCELED } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java index 82e14bf..c258e56 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java @@ -11,6 +11,7 @@ public enum ShipmentErrorCode implements ErrorCode { INVALID_SHIPMENT_STATUS(HttpStatus.BAD_REQUEST, "잘못된 배송 상태입니다."), SHIPMENT_ALREADY_COMPLETED(HttpStatus.CONFLICT, "이미 완료된 배송입니다."), SHIPMENT_ALREADY_CANCELLED(HttpStatus.CONFLICT, "이미 취소된 배송입니다."), + SHIPMENT_NOT_CANCELABLE_STATUS(HttpStatus.BAD_REQUEST, "현재 배송이 시작되어 취소가 불가능합니다."), SHIPMENT_ROUTES_NOT_ALL_COMPLETED(HttpStatus.BAD_REQUEST, "완료되지 않은 배송 경로가 존재합니다."), // ShipmentRoute diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java index 46cb138..702ee01 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentRepository.java @@ -18,4 +18,6 @@ public interface ShipmentRepository { List findAll(Pageable pageable); Optional findByIdWithRoutes(UUID shipmentId); + + Optional findByOrderIdWithRoutes(UUID orderId); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCanceledEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCanceledEvent.java new file mode 100644 index 0000000..692babb --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/consume/OrderCanceledEvent.java @@ -0,0 +1,18 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.event.consume; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class OrderCanceledEvent extends SagaEvent { + + private UUID orderId; +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java index cc8964d..2387971 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCompletedSagaEvent.java @@ -1,12 +1,9 @@ package com.shipflow.shipmentservice.infrastructure.messaging.event.publish; -import java.time.LocalDateTime; -import java.util.List; import java.util.UUID; import com.shipflow.common.messaging.event.SagaEvent; import com.shipflow.shipmentservice.domain.event.ShipmentCompletedEvent; -import com.shipflow.shipmentservice.domain.event.ShipmentCreatedEvent; import lombok.Getter; diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCanceledHandler.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCanceledHandler.java new file mode 100644 index 0000000..d69365e --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/handler/OrderCanceledHandler.java @@ -0,0 +1,34 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.handler; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.shipmentservice.application.ShipmentService; +import com.shipflow.shipmentservice.infrastructure.messaging.config.ShipmentRabbitConfig; +import com.shipflow.shipmentservice.infrastructure.messaging.event.consume.OrderCanceledEvent; +import com.shipflow.shipmentservice.infrastructure.persistence.ProcessedSagaEventRepository; + +@Component +public class OrderCanceledHandler extends IdempotentSagaHandler { + + private final ShipmentService shipmentService; + + public OrderCanceledHandler( + ProcessedSagaEventRepository processedSagaEventRepository, + IdempotentSagaExecutor sagaExecutor, + ShipmentService shipmentService + ) { + super(processedSagaEventRepository, sagaExecutor); + this.shipmentService = shipmentService; + } + + @RabbitListener(queues = ShipmentRabbitConfig.QUEUE_SHIPMENT_ORDER_CANCELED) + public void receive(OrderCanceledEvent event) { + handle(event); + } + + @Override + protected void doProcess(OrderCanceledEvent event) { + shipmentService.cancelShipment(event.getOrderId()); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentJpaRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentJpaRepository.java index 75e4341..c0c2665 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentJpaRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentJpaRepository.java @@ -27,4 +27,12 @@ public interface ShipmentJpaRepository extends JpaRepository { and s.deletedAt is null """) Optional findByIdWithRoutesAndManager(@Param("shipmentId") UUID shipmentId); + + @Query(""" + select distinct s from Shipment s + left join fetch s.routes r + where s.orderId = :orderId + and s.deletedAt is null + """) + Optional findByOrderIdWithRoutes(@Param("orderId") UUID orderId); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java index 36f90ec..1f458f8 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentRepositoryImpl.java @@ -42,4 +42,9 @@ public List findAll(Pageable pageable) { public Optional findByIdWithRoutes(UUID shipmentId) { return shipmentJpaRepository.findByIdWithRoutesAndManager(shipmentId); } + + @Override + public Optional findByOrderIdWithRoutes(UUID orderId) { + return shipmentJpaRepository.findByOrderIdWithRoutes(orderId); + } } From 430becd8d61cfc7b3f5c099f66ea100ec33e565e Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 08:34:45 +0900 Subject: [PATCH 10/19] =?UTF-8?q?test(shipment):=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=EC=B7=A8=EC=86=8C,=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ShipmentServiceTest.java | 296 +++++++++++++++++- 1 file changed, 294 insertions(+), 2 deletions(-) diff --git a/shipment-service/src/test/java/com/shipflow/shipmentservice/application/ShipmentServiceTest.java b/shipment-service/src/test/java/com/shipflow/shipmentservice/application/ShipmentServiceTest.java index 7cf5829..57e5586 100644 --- a/shipment-service/src/test/java/com/shipflow/shipmentservice/application/ShipmentServiceTest.java +++ b/shipment-service/src/test/java/com/shipflow/shipmentservice/application/ShipmentServiceTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -16,22 +19,35 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; import com.shipflow.common.exception.BusinessException; +import com.shipflow.shipmentservice.application.client.CacheClient; +import com.shipflow.shipmentservice.application.client.HubClient; +import com.shipflow.shipmentservice.application.client.UserClient; +import com.shipflow.shipmentservice.application.client.dto.HubRouteResult; +import com.shipflow.shipmentservice.application.client.dto.UserInfo; +import com.shipflow.shipmentservice.application.dto.command.CreateShipmentCommand; +import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; +import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; +import com.shipflow.shipmentservice.application.dto.result.ShipmentCanceledResult; +import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteResult; -import com.shipflow.shipmentservice.application.dto.command.ShipmentRouteUpdateCommand; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteUpdateResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentSearchResult; -import com.shipflow.shipmentservice.application.dto.command.ShipmentUpdateCommand; import com.shipflow.shipmentservice.application.dto.result.ShipmentUpdateResult; import com.shipflow.shipmentservice.domain.Shipment; +import com.shipflow.shipmentservice.domain.ShipmentManager; +import com.shipflow.shipmentservice.domain.ShipmentManagerType; import com.shipflow.shipmentservice.domain.ShipmentRoute; import com.shipflow.shipmentservice.domain.ShipmentRouteStatus; import com.shipflow.shipmentservice.domain.ShipmentStatus; import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; +import com.shipflow.shipmentservice.domain.repository.ShipmentManagerRepository; import com.shipflow.shipmentservice.domain.repository.ShipmentRepository; import com.shipflow.shipmentservice.fixture.ShipmentFixture; +import com.shipflow.shipmentservice.fixture.ShipmentManagerFixture; import com.shipflow.shipmentservice.fixture.ShipmentRouteFixture; @ExtendWith(MockitoExtension.class) @@ -39,10 +55,286 @@ class ShipmentServiceTest { @Mock private ShipmentRepository shipmentRepository; + @Mock + private ShipmentManagerRepository shipmentManagerRepository; + @Mock + private HubClient hubClient; + @Mock + private UserClient userClient; + @Mock + private CacheClient cacheClient; + @Mock + private ShipmentEventPublisher eventPublisher; @InjectMocks private ShipmentService shipmentService; + private HubRouteResult createHubRouteResult(int sequence, UUID departureHubId, UUID arrivalHubId) { + HubRouteResult result = new HubRouteResult(); + ReflectionTestUtils.setField(result, "sequence", sequence); + ReflectionTestUtils.setField(result, "departureHubId", departureHubId); + ReflectionTestUtils.setField(result, "arrivalHubId", arrivalHubId); + ReflectionTestUtils.setField(result, "estimatedDistance", new BigDecimal("12.50")); + ReflectionTestUtils.setField(result, "estimatedDuration", 30); + return result; + } + + @Nested + @DisplayName("배송 생성") + class CreateShipmentTest { + + @Test + @DisplayName("배송 생성 성공") + void createShipment_success() { + // given + UUID orderId = UUID.randomUUID(); + UUID ordererId = UUID.randomUUID(); + UUID departureHubId = UUID.randomUUID(); + UUID arrivalHubId = UUID.randomUUID(); + + CreateShipmentCommand command = new CreateShipmentCommand( + orderId, ordererId, UUID.randomUUID(), 5, + departureHubId, arrivalHubId, + LocalDateTime.now().plusDays(7), "요청사항", "서울시 강남구 테헤란로 123" + ); + + ShipmentManager companyManager = ShipmentManagerFixture.createCompanyManager(); + ShipmentManager hubManager = ShipmentManagerFixture.createHubManager(); + HubRouteResult hubRoute = createHubRouteResult(1, departureHubId, arrivalHubId); + UserInfo userInfo = new UserInfo(ordererId, "홍길동", "hong123"); + Shipment savedShipment = ShipmentFixture.createShipment(); + + given(shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY)) + .willReturn(Optional.of(companyManager)); + given(hubClient.getHubRoutes(departureHubId, arrivalHubId)) + .willReturn(List.of(hubRoute)); + given(shipmentManagerRepository.findAllByType(ShipmentManagerType.HUB)) + .willReturn(List.of(hubManager)); + given(userClient.getUser(ordererId)) + .willReturn(userInfo); + given(cacheClient.increment(anyString())) + .willReturn(1L); + given(shipmentRepository.save(any())) + .willReturn(savedShipment); + + // when & then + assertThatNoException().isThrownBy(() -> shipmentService.createShipment(command)); + then(eventPublisher).should().publishCreated(any()); + } + + @Test + @DisplayName("업체 배송 담당자가 없으면 생성에 실패하고 실패 이벤트를 발행한다") + void createShipment_companyManagerNotFound() { + // given + CreateShipmentCommand command = new CreateShipmentCommand( + UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), 5, + UUID.randomUUID(), UUID.randomUUID(), + LocalDateTime.now().plusDays(7), "요청사항", "서울시 강남구 테헤란로 123" + ); + + given(shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> shipmentService.createShipment(command)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND); + + then(eventPublisher).should().publishCreationFailed(any()); + } + + @Test + @DisplayName("허브 배송 담당자가 없으면 생성에 실패하고 실패 이벤트를 발행한다") + void createShipment_hubManagerNotFound() { + // given + UUID departureHubId = UUID.randomUUID(); + UUID arrivalHubId = UUID.randomUUID(); + + CreateShipmentCommand command = new CreateShipmentCommand( + UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), 5, + departureHubId, arrivalHubId, + LocalDateTime.now().plusDays(7), "요청사항", "서울시 강남구 테헤란로 123" + ); + + given(shipmentManagerRepository.findFirstAvailableByType(ShipmentManagerType.COMPANY)) + .willReturn(Optional.of(ShipmentManagerFixture.createCompanyManager())); + given(hubClient.getHubRoutes(departureHubId, arrivalHubId)) + .willReturn(List.of(createHubRouteResult(1, departureHubId, arrivalHubId))); + given(shipmentManagerRepository.findAllByType(ShipmentManagerType.HUB)) + .willReturn(Collections.emptyList()); + + // when & then + assertThatThrownBy(() -> shipmentService.createShipment(command)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND); + + then(eventPublisher).should().publishCreationFailed(any()); + } + } + + @Nested + @DisplayName("배송 완료") + class CompleteShipmentTest { + + @Test + @DisplayName("모든 경로가 완료되면 배송 완료 성공") + void completeShipment_success() { + // given + UUID shipmentId = UUID.randomUUID(); + List arrivedRoutes = List.of( + ShipmentRouteFixture.createArrivedRoute(UUID.randomUUID(), 1, LocalDateTime.now().minusHours(2)), + ShipmentRouteFixture.createArrivedRoute(UUID.randomUUID(), 2, LocalDateTime.now().minusHours(1)) + ); + Shipment shipment = ShipmentFixture.createShipmentWithRoutes(shipmentId, arrivedRoutes); + + given(shipmentRepository.findByIdWithRoutes(shipmentId)) + .willReturn(Optional.of(shipment)); + + // when + ShipmentCompleteResult result = shipmentService.completeShipment(shipmentId); + + // then + assertThat(result).isNotNull(); + assertThat(result.getShipmentId()).isEqualTo(shipmentId); + then(eventPublisher).should().publishCompleted(any()); + } + + @Test + @DisplayName("배송 정보가 없으면 실패한다") + void completeShipment_notFound() { + // given + UUID shipmentId = UUID.randomUUID(); + given(shipmentRepository.findByIdWithRoutes(shipmentId)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> shipmentService.completeShipment(shipmentId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_NOT_FOUND); + } + + @Test + @DisplayName("이미 완료된 배송이면 실패한다") + void completeShipment_alreadyCompleted() { + // given + UUID shipmentId = UUID.randomUUID(); + Shipment shipment = ShipmentFixture.createShipmentWithStatus(shipmentId, ShipmentStatus.COMPLETED); + + given(shipmentRepository.findByIdWithRoutes(shipmentId)) + .willReturn(Optional.of(shipment)); + + // when & then + assertThatThrownBy(() -> shipmentService.completeShipment(shipmentId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_ALREADY_COMPLETED); + } + + @Test + @DisplayName("완료되지 않은 경로가 있으면 실패한다") + void completeShipment_routesNotAllCompleted() { + // given + UUID shipmentId = UUID.randomUUID(); + List routes = List.of( + ShipmentRouteFixture.createArrivedRoute(UUID.randomUUID(), 1, LocalDateTime.now().minusHours(1)), + ShipmentRouteFixture.createRoute(UUID.randomUUID(), 2) // WAITING_AT_HUB + ); + Shipment shipment = ShipmentFixture.createShipmentWithRoutes(shipmentId, routes); + + given(shipmentRepository.findByIdWithRoutes(shipmentId)) + .willReturn(Optional.of(shipment)); + + // when & then + assertThatThrownBy(() -> shipmentService.completeShipment(shipmentId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_ROUTES_NOT_ALL_COMPLETED); + } + } + + @Nested + @DisplayName("배송 취소") + class CancelShipmentTest { + + @Test + @DisplayName("WAITING_AT_HUB 상태의 배송은 취소 성공") + void cancelShipment_success() { + // given + UUID orderId = UUID.randomUUID(); + UUID shipmentId = UUID.randomUUID(); + List routes = List.of( + ShipmentRouteFixture.createRoute(UUID.randomUUID(), 1), + ShipmentRouteFixture.createRoute(UUID.randomUUID(), 2) + ); + Shipment shipment = ShipmentFixture.createShipmentWithRoutes(shipmentId, routes); + + given(shipmentRepository.findByOrderIdWithRoutes(orderId)) + .willReturn(Optional.of(shipment)); + + // when + ShipmentCanceledResult result = shipmentService.cancelShipment(orderId); + + // then + assertThat(result).isNotNull(); + assertThat(result.getShipmentId()).isEqualTo(shipmentId); + assertThat(result.getStatus()).isEqualTo(ShipmentStatus.CANCELLED); + } + + @Test + @DisplayName("배송 정보가 없으면 실패한다") + void cancelShipment_notFound() { + // given + UUID orderId = UUID.randomUUID(); + given(shipmentRepository.findByOrderIdWithRoutes(orderId)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> shipmentService.cancelShipment(orderId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_NOT_FOUND); + } + + @Test + @DisplayName("이미 취소된 배송이면 실패한다") + void cancelShipment_alreadyCancelled() { + // given + UUID orderId = UUID.randomUUID(); + UUID shipmentId = UUID.randomUUID(); + Shipment shipment = ShipmentFixture.createShipmentWithStatus(shipmentId, ShipmentStatus.CANCELLED); + + given(shipmentRepository.findByOrderIdWithRoutes(orderId)) + .willReturn(Optional.of(shipment)); + + // when & then + assertThatThrownBy(() -> shipmentService.cancelShipment(orderId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_ALREADY_CANCELLED); + } + + @Test + @DisplayName("배송이 이미 시작된 경우 취소 실패") + void cancelShipment_notCancelableStatus() { + // given + UUID orderId = UUID.randomUUID(); + UUID shipmentId = UUID.randomUUID(); + Shipment shipment = ShipmentFixture.createShipmentWithStatus(shipmentId, ShipmentStatus.MOVING_TO_HUB); + + given(shipmentRepository.findByOrderIdWithRoutes(orderId)) + .willReturn(Optional.of(shipment)); + + // when & then + assertThatThrownBy(() -> shipmentService.cancelShipment(orderId)) + .isInstanceOf(BusinessException.class) + .extracting("errorCode") + .isEqualTo(ShipmentErrorCode.SHIPMENT_NOT_CANCELABLE_STATUS); + } + } + @Nested @DisplayName("배송 단건 조회") class GetShipmentTest { From 9fae04fe52b7a46138fcb0fc87706943a164874d Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 09:08:16 +0900 Subject: [PATCH 11/19] =?UTF-8?q?refactor(shipment):=20=EB=B0=B0=EC=86=A1?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20-=20shipm?= =?UTF-8?q?entManagerSlackId=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipflow/shipmentservice/application/ShipmentService.java | 1 + .../shipmentservice/domain/event/ShipmentCreatedEvent.java | 3 ++- .../messaging/event/publish/ShipmentCreatedSagaEvent.java | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java index d8fa3b5..aa8486f 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentService.java @@ -90,6 +90,7 @@ public void createShipment(CreateShipmentCommand command) { saved.getArrivalHubId(), command.requestDeadline(), command.requestNote(), + companyManager.getSlackId(), eventRoutes )); diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java index cf54d61..1305b7a 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/event/ShipmentCreatedEvent.java @@ -13,8 +13,9 @@ public record ShipmentCreatedEvent( UUID arrivalHubId, LocalDateTime requestDeadline, String requestNote, + String shipmentManagerSlackId, List routes ) { public record Route(int sequence, UUID departureHubId, UUID arrivalHubId) { } -} \ No newline at end of file +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java index c3b3af3..b8f5e54 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/event/publish/ShipmentCreatedSagaEvent.java @@ -22,6 +22,7 @@ public class ShipmentCreatedSagaEvent extends SagaEvent { private final UUID arrivalHubId; private final LocalDateTime requestDeadline; private final String requestNote; + private final String shipmentManagerSlackId; private final List routes; public ShipmentCreatedSagaEvent(ShipmentCreatedEvent event) { @@ -34,6 +35,7 @@ public ShipmentCreatedSagaEvent(ShipmentCreatedEvent event) { this.arrivalHubId = event.arrivalHubId(); this.requestDeadline = event.requestDeadline(); this.requestNote = event.requestNote(); + this.shipmentManagerSlackId = event.shipmentManagerSlackId(); this.routes = event.routes().stream() .map(r -> new Route(r.sequence(), r.departureHubId(), r.arrivalHubId())) .toList(); From 8b8db81cb9e3b2917941cd8f2eba491e8009af58 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 09:32:35 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat(shipment):=20Gateway=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=ED=97=A4=EB=8D=94=EB=A5=BC=20UserContext?= =?UTF-8?q?=EB=A1=9C=20=EC=A3=BC=EC=9E=85=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exception/ShipmentErrorCode.java | 4 ++ .../presentation/ShipmentController.java | 25 ++++++----- .../ShipmentManagerController.java | 18 +++++--- .../presentation/UserContext.java | 6 +++ .../UserContextArgumentResolver.java | 45 +++++++++++++++++++ .../presentation/WebMvcConfig.java | 16 +++++++ 6 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContext.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContextArgumentResolver.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/WebMvcConfig.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java index c258e56..9dc88b9 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/exception/ShipmentErrorCode.java @@ -30,6 +30,10 @@ public enum ShipmentErrorCode implements ErrorCode { SHIPMENT_MANAGER_SLACK_ID_REQUIRED(HttpStatus.BAD_REQUEST, "배송 담당자 slackId는 필수입니다."), INVALID_SHIPMENT_SEQUENCE(HttpStatus.BAD_REQUEST, "배송 순번은 0 이상이어야 합니다."), + // Auth + MISSING_USER_ID(HttpStatus.UNAUTHORIZED, "인증 정보가 없습니다."), + MISSING_USER_ROLE(HttpStatus.FORBIDDEN, "권한 정보가 없습니다."), + // External Service USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), USER_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "사용자 서비스에 연결할 수 없습니다."), diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java index 95570e0..c2b7afc 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentController.java @@ -16,11 +16,11 @@ import com.shipflow.shipmentservice.application.dto.result.ShipmentCompleteResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentRouteUpdateResult; import com.shipflow.shipmentservice.application.dto.result.ShipmentUpdateResult; +import com.shipflow.shipmentservice.presentation.dto.request.PatchShipmentReqDto; +import com.shipflow.shipmentservice.presentation.dto.request.PatchShipmentRouteReqDto; import com.shipflow.shipmentservice.presentation.dto.response.GetShipmentResDto; import com.shipflow.shipmentservice.presentation.dto.response.GetShipmentRouteListResDto; -import com.shipflow.shipmentservice.presentation.dto.request.PatchShipmentReqDto; import com.shipflow.shipmentservice.presentation.dto.response.PatchShipmentResDto; -import com.shipflow.shipmentservice.presentation.dto.request.PatchShipmentRouteReqDto; import com.shipflow.shipmentservice.presentation.dto.response.PatchShipmentRouteResDto; import com.shipflow.shipmentservice.presentation.dto.response.ShipmentCompleteResDto; import com.shipflow.shipmentservice.presentation.dto.response.ShipmentSearchResDto; @@ -39,7 +39,8 @@ public class ShipmentController { */ @GetMapping("/api/shipments") public ApiResponse> searchShipment( - Pageable pageable + Pageable pageable, + UserContext userContext ) { List shipmentList = shipmentService.searchShipment(pageable).stream() .map(ShipmentSearchResDto::fromResult) @@ -52,7 +53,8 @@ public ApiResponse> searchShipment( */ @GetMapping("/api/shipments/{shipmentId}") public ApiResponse getShipment( - @PathVariable UUID shipmentId + @PathVariable UUID shipmentId, + UserContext userContext ) { return ApiResponse.ok(GetShipmentResDto.fromResult(shipmentService.getShipment(shipmentId))); } @@ -62,7 +64,8 @@ public ApiResponse getShipment( */ @GetMapping("/api/shipments/{shipmentId}/routes") public ApiResponse> getShipmentRoutes( - @PathVariable UUID shipmentId + @PathVariable UUID shipmentId, + UserContext userContext ) { List routes = shipmentService.getShipmentRoutes(shipmentId).stream() .map(GetShipmentRouteListResDto::fromResult) @@ -76,7 +79,8 @@ public ApiResponse> getShipmentRoutes( @PatchMapping("/api/shipments/{shipmentId}") public ApiResponse updateShipment( @PathVariable UUID shipmentId, - @Valid @RequestBody PatchShipmentReqDto request + @Valid @RequestBody PatchShipmentReqDto request, + UserContext userContext ) { ShipmentUpdateResult result = shipmentService.updateShipment(shipmentId, request.toCommand()); return ApiResponse.ok(PatchShipmentResDto.fromResult(result)); @@ -89,22 +93,23 @@ public ApiResponse updateShipment( public ApiResponse updateShipmentRoute( @PathVariable UUID shipmentId, @PathVariable UUID routeId, - @Valid @RequestBody PatchShipmentRouteReqDto request + @Valid @RequestBody PatchShipmentRouteReqDto request, + UserContext userContext ) { ShipmentRouteUpdateResult result = shipmentService.updateShipmentRoute( shipmentId, routeId, request.toCommand() ); - return ApiResponse.ok(PatchShipmentRouteResDto.fromResult(result)); } @PostMapping("/api/shipments/{shipmentId}/complete") public ApiResponse completeShipment( - @PathVariable UUID shipmentId + @PathVariable UUID shipmentId, + UserContext userContext ) { ShipmentCompleteResult result = shipmentService.completeShipment(shipmentId); return ApiResponse.ok(ShipmentCompleteResDto.fromResult(result)); } -} +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerController.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerController.java index c9cc69b..64152c7 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerController.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerController.java @@ -5,7 +5,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -37,14 +36,18 @@ public class ShipmentManagerController { @PostMapping public ApiResponse createShipmentManager( - @Valid @RequestBody PostShipmentManagerReqDto request + @Valid @RequestBody PostShipmentManagerReqDto request, + UserContext userContext ) { ShipmentManagerCreateResult result = shipmentManagerService.createShipmentManager(request.toCommand()); return ApiResponse.ok(PostShipmentManagerResDto.fromResult(result)); } @GetMapping("/{managerId}") - public ApiResponse getShipmentManager(@PathVariable UUID managerId) { + public ApiResponse getShipmentManager( + @PathVariable UUID managerId, + UserContext userContext + ) { ShipmentManagerResult result = shipmentManagerService.getShipmentManager(managerId); return ApiResponse.ok(GetShipmentManagerResDto.fromResult(result)); } @@ -53,7 +56,8 @@ public ApiResponse getShipmentManager(@PathVariable UU public ApiResponse> searchShipmentManager( @RequestParam(required = false) ShipmentManagerType type, @RequestParam(required = false) UUID hubId, - Pageable pageable + Pageable pageable, + UserContext userContext ) { ShipmentManagerSearchQuery query = ShipmentManagerSearchQuery.builder() .type(type) @@ -69,9 +73,9 @@ public ApiResponse> searchShipmentManager( @DeleteMapping("/{managerId}") public ApiResponse deleteShipmentManager( @PathVariable UUID managerId, - @RequestHeader("X-User-Id") UUID userId + UserContext userContext ) { - shipmentManagerService.deleteShipmentManager(managerId, userId); + shipmentManagerService.deleteShipmentManager(managerId, userContext.userId()); return ApiResponse.ok(null); } -} +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContext.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContext.java new file mode 100644 index 0000000..10e0467 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContext.java @@ -0,0 +1,6 @@ +package com.shipflow.shipmentservice.presentation; + +import java.util.UUID; + +public record UserContext(UUID userId, String role) { +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContextArgumentResolver.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContextArgumentResolver.java new file mode 100644 index 0000000..f45c3a5 --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/UserContextArgumentResolver.java @@ -0,0 +1,45 @@ +package com.shipflow.shipmentservice.presentation; + +import java.util.UUID; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.shipmentservice.domain.exception.ShipmentErrorCode; + +import jakarta.servlet.http.HttpServletRequest; + +public class UserContextArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String USER_ID_HEADER = "X-User-Id"; + private static final String USER_ROLE_HEADER = "X-User-Role"; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(UserContext.class); + } + + @Override + public UserContext resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + String userId = request.getHeader(USER_ID_HEADER); + String role = request.getHeader(USER_ROLE_HEADER); + + if (userId == null || userId.isBlank()) { + throw new BusinessException(ShipmentErrorCode.MISSING_USER_ID); + } + if (role == null || role.isBlank()) { + throw new BusinessException(ShipmentErrorCode.MISSING_USER_ROLE); + } + + return new UserContext(UUID.fromString(userId), role); + } +} \ No newline at end of file diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/WebMvcConfig.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/WebMvcConfig.java new file mode 100644 index 0000000..530b16a --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/WebMvcConfig.java @@ -0,0 +1,16 @@ +package com.shipflow.shipmentservice.presentation; + +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new UserContextArgumentResolver()); + } +} \ No newline at end of file From db9580222b42c286777c67ddcc65d05ec91b2798 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 09:56:37 +0900 Subject: [PATCH 13/19] =?UTF-8?q?feat(shipment):=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EB=8B=B4=EB=8B=B9=EC=9E=90=20=EC=9D=BC=EA=B4=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=82=B4=EB=B6=80=20API=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=EB=A7=81=20=EA=B5=AC=ED=98=84=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hub 삭제 시 업체 배송 담당자 일괄삭제 - User 삭제시 배송 담당자 삭제 - 매일 자정에 일괄 삭제 처리 --- .../application/ShipmentManagerService.java | 21 ++++++++++++ .../domain/ShipmentManager.java | 7 ++++ .../repository/ShipmentManagerRepository.java | 6 ++++ .../ShipmentManagerCleanupScheduler.java | 24 ++++++++++++++ .../ShipmentManagerJpaRepository.java | 21 ++++++++++++ .../ShipmentManagerRepositoryImpl.java | 15 +++++++++ .../ShipmentManagerInternalController.java | 33 +++++++++++++++++++ 7 files changed, 127 insertions(+) create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ShipmentManagerCleanupScheduler.java create mode 100644 shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerInternalController.java diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java index 0107727..7b78fba 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java @@ -27,6 +27,8 @@ @Transactional(readOnly = true) public class ShipmentManagerService { + private static final UUID SYSTEM_ID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + private final ShipmentManagerRepository shipmentManagerRepository; private final UserClient userClient; @@ -107,4 +109,23 @@ public void deleteShipmentManager(UUID managerId, UUID userId) { shipmentManager.delete(userId); } + + @Transactional + public void markPendingDeletionByHubId(UUID hubId) { + List managers = shipmentManagerRepository.findAllByHubId(hubId); + managers.forEach(ShipmentManager::markPendingDeletion); + } + + @Transactional + public void markPendingDeletionByUserId(UUID userId) { + ShipmentManager manager = shipmentManagerRepository.findByUserId(userId) + .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND)); + manager.markPendingDeletion(); + } + + @Transactional + public void deleteAllPending() { + List pending = shipmentManagerRepository.findAllPendingDeletion(); + pending.forEach(m -> m.delete(SYSTEM_ID)); + } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentManager.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentManager.java index e3c36b2..f6e46af 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentManager.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/ShipmentManager.java @@ -51,10 +51,17 @@ public class ShipmentManager extends BaseEntity { @Column(name = "shipment_sequence", nullable = false) private Integer shipmentSequence; + @Column(name = "pending_deletion", nullable = false) + private boolean pendingDeletion = false; + public void updateSequence(int sequence) { this.shipmentSequence = sequence; } + public void markPendingDeletion() { + this.pendingDeletion = true; + } + public void delete(UUID deletedBy) { softDelete(deletedBy); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java index d596757..0ae61ff 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/repository/ShipmentManagerRepository.java @@ -25,4 +25,10 @@ public interface ShipmentManagerRepository { Optional findFirstAvailableByType(ShipmentManagerType type); List findAllByType(ShipmentManagerType type); + + List findAllByHubId(UUID hubId); + + Optional findByUserId(UUID userId); + + List findAllPendingDeletion(); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ShipmentManagerCleanupScheduler.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ShipmentManagerCleanupScheduler.java new file mode 100644 index 0000000..b9e8e3f --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/messaging/scheduler/ShipmentManagerCleanupScheduler.java @@ -0,0 +1,24 @@ +package com.shipflow.shipmentservice.infrastructure.messaging.scheduler; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.shipflow.shipmentservice.application.ShipmentManagerService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ShipmentManagerCleanupScheduler { + + private final ShipmentManagerService shipmentManagerService; + + @Scheduled(cron = "0 0 0 * * *") + public void deletePendingManagers() { + log.info("[Scheduler] 삭제 예정 배송 담당자 일괄 삭제 시작"); + shipmentManagerService.deleteAllPending(); + log.info("[Scheduler] 삭제 예정 배송 담당자 일괄 삭제 완료"); + } +} diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java index d6d076c..2502e86 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java @@ -51,4 +51,25 @@ int findMaxSequenceByTypeAndHubId( order by m.shipmentSequence asc """) List findAllByType(@Param("type") ShipmentManagerType type); + + @Query(""" + select m from ShipmentManager m + where m.hubId = :hubId + and m.deletedAt is null + """) + List findAllByHubId(@Param("hubId") UUID hubId); + + @Query(""" + select m from ShipmentManager m + where m.userId = :userId + and m.deletedAt is null + """) + Optional findByUserId(@Param("userId") UUID userId); + + @Query(""" + select m from ShipmentManager m + where m.pendingDeletion = true + and m.deletedAt is null + """) + List findAllPendingDeletion(); } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java index 1e098bc..af664c6 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerRepositoryImpl.java @@ -55,4 +55,19 @@ public Optional findFirstAvailableByType(ShipmentManagerType ty public List findAllByType(ShipmentManagerType type) { return shipmentManagerJpaRepository.findAllByType(type); } + + @Override + public List findAllByHubId(UUID hubId) { + return shipmentManagerJpaRepository.findAllByHubId(hubId); + } + + @Override + public Optional findByUserId(UUID userId) { + return shipmentManagerJpaRepository.findByUserId(userId); + } + + @Override + public List findAllPendingDeletion() { + return shipmentManagerJpaRepository.findAllPendingDeletion(); + } } diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerInternalController.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerInternalController.java new file mode 100644 index 0000000..8f9e4cb --- /dev/null +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/presentation/ShipmentManagerInternalController.java @@ -0,0 +1,33 @@ +package com.shipflow.shipmentservice.presentation; + +import java.util.UUID; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.shipmentservice.application.ShipmentManagerService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/internal/shipment-managers") +public class ShipmentManagerInternalController { + + private final ShipmentManagerService shipmentManagerService; + + @DeleteMapping("/hubs/{hubId}") + public ApiResponse markPendingDeletionByHub(@PathVariable UUID hubId) { + shipmentManagerService.markPendingDeletionByHubId(hubId); + return ApiResponse.ok(null); + } + + @DeleteMapping("/users/{userId}") + public ApiResponse markPendingDeletionByUser(@PathVariable UUID userId) { + shipmentManagerService.markPendingDeletionByUserId(userId); + return ApiResponse.ok(null); + } +} From 36f2873a7405c57ae83479f957250b6c106e14d6 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 10:11:58 +0900 Subject: [PATCH 14/19] =?UTF-8?q?fix(shipment):=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EB=8B=B4=EB=8B=B9=EC=9E=90=20=EB=B0=B0=EC=A0=95=20=EC=8B=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=98=88=EC=A0=95=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=9C=EC=99=B8=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - findFirstAvailableByType, findAllByType 쿼리에 pendingDeletion = false 조건 추가 --- .../persistence/ShipmentManagerJpaRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java index 2502e86..5d4b00f 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/infrastructure/persistence/ShipmentManagerJpaRepository.java @@ -39,6 +39,7 @@ int findMaxSequenceByTypeAndHubId( select m from ShipmentManager m where m.type = :type and m.deletedAt is null + and m.pendingDeletion = false order by m.shipmentSequence asc limit 1 """) @@ -48,6 +49,7 @@ int findMaxSequenceByTypeAndHubId( select m from ShipmentManager m where m.type = :type and m.deletedAt is null + and m.pendingDeletion = false order by m.shipmentSequence asc """) List findAllByType(@Param("type") ShipmentManagerType type); From ca72fa5c3c01b7af7d114d5744ac3683f5cba964 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 10:39:41 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix(hub):=20DeliveryClient=20=EC=97=85?= =?UTF-8?q?=EC=B2=B4=EB=B0=B0=EC=86=A1=EB=8B=B4=EB=8B=B9=EC=9E=90=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=9A=94=EC=B2=AD=20URL=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hubservice/infrastructure/client/DeliveryClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub-service/src/main/java/com/shipflow/hubservice/infrastructure/client/DeliveryClient.java b/hub-service/src/main/java/com/shipflow/hubservice/infrastructure/client/DeliveryClient.java index 5f61cde..c65a819 100644 --- a/hub-service/src/main/java/com/shipflow/hubservice/infrastructure/client/DeliveryClient.java +++ b/hub-service/src/main/java/com/shipflow/hubservice/infrastructure/client/DeliveryClient.java @@ -10,7 +10,7 @@ @FeignClient(name = "deliveryservice") public interface DeliveryClient { - @DeleteMapping("/internal/delivery-managers/{hubId}") + @DeleteMapping("/internal/delivery-managers/hubs/{hubId}") void deleteCompanyDeliveryManagers( @RequestHeader("X-Internal-Request") String internalRequest, @RequestHeader("X-User-Id") String requestUserId, From 21c3aca90e62ba0891bd8dfc7011c1f16480e706 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 11:28:21 +0900 Subject: [PATCH 16/19] =?UTF-8?q?refactor(hub):=20Shipment=EC=9D=98=20orde?= =?UTF-8?q?rId=20Unique=20=EC=84=A4=EC=A0=95=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/shipflow/shipmentservice/domain/Shipment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java index 885326a..503d449 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/domain/Shipment.java @@ -38,7 +38,7 @@ public class Shipment extends BaseEntity { @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @Column(name = "order_id", nullable = false) + @Column(name = "order_id", nullable = false, unique = true) private UUID orderId; @Enumerated(EnumType.STRING) From 60465da44e2b4531a7c23e711b74b2b4867dcf85 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 12:23:36 +0900 Subject: [PATCH 17/19] =?UTF-8?q?chore(hub):=20JacksonConfig=20setVisibili?= =?UTF-8?q?ty=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/shipflow/config/message/JacksonConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/src/main/java/com/shipflow/config/message/JacksonConfig.java b/common/src/main/java/com/shipflow/config/message/JacksonConfig.java index 25e613f..6c87acf 100644 --- a/common/src/main/java/com/shipflow/config/message/JacksonConfig.java +++ b/common/src/main/java/com/shipflow/config/message/JacksonConfig.java @@ -1,5 +1,7 @@ package com.shipflow.config.message; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -17,6 +19,7 @@ public ObjectMapper objectMapper() { mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); return mapper; } } From b534f5c61dc2aa261acbec32b4c228e2fe58c709 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 12:24:39 +0900 Subject: [PATCH 18/19] =?UTF-8?q?chore(shipment):=20application.yaml,=20bu?= =?UTF-8?q?ild.gradle=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shipment-service/build.gradle | 7 ++-- .../src/main/resources/application.yaml | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/shipment-service/build.gradle b/shipment-service/build.gradle index 78811fd..131ea16 100644 --- a/shipment-service/build.gradle +++ b/shipment-service/build.gradle @@ -29,6 +29,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + // Service Discovery + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + // Feign implementation "org.springframework.cloud:spring-cloud-starter-openfeign" @@ -48,9 +52,6 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' - // Feign - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/shipment-service/src/main/resources/application.yaml b/shipment-service/src/main/resources/application.yaml index 246f601..723a567 100644 --- a/shipment-service/src/main/resources/application.yaml +++ b/shipment-service/src/main/resources/application.yaml @@ -1,13 +1,15 @@ spring: application: - name: shipment-service + name: shipmentservice + #DB datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:shipflow}?currentSchema=shipment username: ${DB_USER:shipflow} password: ${DB_PASSWORD:1234} + #JPA jpa: hibernate: ddl-auto: update @@ -17,11 +19,13 @@ spring: format_sql: true default_schema: shipment + #Redis data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} + # RabbitMQ rabbitmq: host: ${RABBITMQ_HOST:localhost} port: 5672 @@ -34,3 +38,35 @@ spring: max-attempts: 3 initial-interval: 1000ms multiplier: 2.0 +server: + port: 8080 + +# Service Discovery +eureka: + client: + service-url: + defaultZone: http://discoveryserver:8761/eureka/ + registry-fetch-interval-seconds: 5 + instance: + prefer-ip-address: true + lease-renewal-interval-in-seconds: 10 + lease-expiration-duration-in-seconds: 30 + +# Actuator +management: + endpoints: + web: + exposure: + include: health, info + endpoint: + health: + show-details: always + +# Feign +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: full From 84f4b0895d1fa87ef67570048b0ca4a2b62b1d10 Mon Sep 17 00:00:00 2001 From: soo Date: Tue, 7 Apr 2026 12:29:48 +0900 Subject: [PATCH 19/19] =?UTF-8?q?refactor(shipment):=20markPendingDeletion?= =?UTF-8?q?ByUserId=20=EB=A9=B1=EB=93=B1=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipmentservice/application/ShipmentManagerService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java index 7b78fba..7cbde44 100644 --- a/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java +++ b/shipment-service/src/main/java/com/shipflow/shipmentservice/application/ShipmentManagerService.java @@ -118,9 +118,8 @@ public void markPendingDeletionByHubId(UUID hubId) { @Transactional public void markPendingDeletionByUserId(UUID userId) { - ShipmentManager manager = shipmentManagerRepository.findByUserId(userId) - .orElseThrow(() -> new BusinessException(ShipmentErrorCode.SHIPMENT_MANAGER_NOT_FOUND)); - manager.markPendingDeletion(); + shipmentManagerRepository.findByUserId(userId) + .ifPresent(ShipmentManager::markPendingDeletion); } @Transactional