From 61086881da4b4f914bf78bce1bc89af702ca0cc5 Mon Sep 17 00:00:00 2001 From: jin Date: Wed, 1 Apr 2026 15:51:23 +0900 Subject: [PATCH 01/38] =?UTF-8?q?feature:=20domain=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: 공통 설정 사항 적용 --- product-service/build.gradle | 40 +++++++++++ .../productservice/domain/BasicEntity.java | 28 ++++++++ .../productservice/domain/Product.java | 66 +++++++++++++++++++ .../productservice/domain/ProductStatus.java | 5 ++ .../domain/repository/ProductRepository.java | 13 ++++ .../src/main/resources/application.yaml | 15 ++++- 6 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/Product.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java diff --git a/product-service/build.gradle b/product-service/build.gradle index 6a4cd12..c445fa1 100644 --- a/product-service/build.gradle +++ b/product-service/build.gradle @@ -21,16 +21,56 @@ configurations { repositories { mavenCentral() + maven {url = uri('https://repo.spring.io/milestone') } +} + +ext { + springCloudVersion = "2025.0.0" } dependencies { +// implementation project(':common') + + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + + implementation 'org.postgresql:postgresql' + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.amqp:spring-rabbit-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + tasks.named('test') { useJUnitPlatform() } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java new file mode 100644 index 0000000..b2e8bf4 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java @@ -0,0 +1,28 @@ +package com.shipflow.productservice.domain; + +import java.time.LocalDateTime; +import java.util.UUID; + +public class BasicEntity { + LocalDateTime createdAt; + UUID createdBy; + LocalDateTime updatedAt; + UUID updatedBy; + LocalDateTime deletedAt; + UUID deletedBy; + + public void create(UUID id) { + this.createdAt = LocalDateTime.now(); + this.createdBy = id; + } + + public void update(UUID id) { + this.updatedAt = LocalDateTime.now(); + this.updatedBy = id; + } + + public void delete(UUID id) { + this.deletedAt = LocalDateTime.now(); + this.deletedBy = id; + } +} \ No newline at end of file diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/Product.java new file mode 100644 index 0000000..0536c3d --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/Product.java @@ -0,0 +1,66 @@ +package com.shipflow.productservice.domain; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class Product extends BasicEntity{ + private UUID id; + private String name; + private BigDecimal price; + private Integer stock; + private ProductStatus status; + private UUID companyId; + private String companyName; + private UUID hubId; + private Boolean isHide; + + public static Product create(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, + UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy) { + Product product = new Product(id, name, price, stock, status, companyId, companyName, hubId, isHide); + product.create(createdBy); + return product; + } + + private Product(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, + UUID companyId, String companyName, UUID hubId, Boolean isHide){ + this.id = id; + this.name = Objects.requireNonNull(name, "name은 필수값입니다."); + this.price = Objects.requireNonNull(price, "price는 필수값입니다."); + this.stock = Objects.requireNonNullElse(stock, 0); + this.status = Objects.requireNonNullElse(status, ProductStatus.STOPPED); + this.companyId = Objects.requireNonNull(companyId, "companyId는 필수값입니다."); + this.companyName = companyName; + this.hubId = hubId; + this.isHide = Objects.requireNonNullElse(isHide, false); + } + + public static Product reconstruct(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, + UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy, LocalDateTime createdAt, + LocalDateTime updatedAt, UUID updatedBy, LocalDateTime deletedAt, UUID deletedBy) { + Product product = new Product(id, name, price, stock, status, companyId, companyName, hubId, isHide); + product.createdAt = createdAt; + product.createdBy = createdBy; + product.updatedAt = updatedAt; + product.updatedBy = updatedBy; + product.deletedAt = deletedAt; + product.deletedBy = deletedBy; + return product; + } + + public void updateInfo(String name, BigDecimal price, UUID updatedBy) { + if (name != null && !name.isBlank()) + this.name = name; + + if(price.compareTo(BigDecimal.ZERO) <=0) + throw new IllegalArgumentException("price는 0보다 커야 합니다."); + else + this.price = price; + this.update(updatedBy); + } + + public void delete(UUID deletedBy) { + super.delete(deletedBy); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java b/product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java new file mode 100644 index 0000000..3936153 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java @@ -0,0 +1,5 @@ +package com.shipflow.productservice.domain; + +public enum ProductStatus { + ON_SALE, STOPPED, OUT_OF_STOCK, DISCONTINUED +} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java new file mode 100644 index 0000000..fe851bb --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -0,0 +1,13 @@ +package com.shipflow.productservice.domain.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import com.shipflow.productservice.domain.Product; + +public interface ProductRepository { + Optional findById(UUID id); + void save(Product product); + List findAll(); +} diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index d52a666..10fadde 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -1,3 +1,14 @@ spring: - application: - name: productservice + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema={schema} + username: ${DB_USER} + password: ${DB_PASSWORD} + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + default_schema: products //본인 스키마명으로 변경 + show-sql: true \ No newline at end of file From d6ef8f73f3e2a02bab529e2cb4ab8f766c745ee9 Mon Sep 17 00:00:00 2001 From: jin Date: Wed, 1 Apr 2026 16:14:51 +0900 Subject: [PATCH 02/38] =?UTF-8?q?feature:=20infrastructure=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: Product, BaseEntity - getter 추가 --- product-service/settings.gradle | 1 - .../{BasicEntity.java => BaseEntity.java} | 5 +- .../productservice/domain/Product.java | 5 +- .../persistence/ProductJpaEntity.java | 90 +++++++++++++++++++ .../persistence/ProductJpaRepository.java | 7 ++ .../persistence/ProductRepositoryImpl.java | 40 +++++++++ 6 files changed, 145 insertions(+), 3 deletions(-) delete mode 100644 product-service/settings.gradle rename product-service/src/main/java/com/shipflow/productservice/domain/{BasicEntity.java => BaseEntity.java} (90%) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java diff --git a/product-service/settings.gradle b/product-service/settings.gradle deleted file mode 100644 index b489814..0000000 --- a/product-service/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'productservice' diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java similarity index 90% rename from product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java rename to product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java index b2e8bf4..92c54ae 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/BasicEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java @@ -3,7 +3,10 @@ import java.time.LocalDateTime; import java.util.UUID; -public class BasicEntity { +import lombok.Getter; + +@Getter +public class BaseEntity { LocalDateTime createdAt; UUID createdBy; LocalDateTime updatedAt; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/Product.java index 0536c3d..f8777c7 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/Product.java @@ -5,7 +5,10 @@ import java.util.Objects; import java.util.UUID; -public class Product extends BasicEntity{ +import lombok.Getter; + +@Getter +public class Product extends BaseEntity { private UUID id; private String name; private BigDecimal price; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java new file mode 100644 index 0000000..7e18e0c --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java @@ -0,0 +1,90 @@ +package com.shipflow.productservice.infrastructure.persistence; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.productservice.domain.Product; +import com.shipflow.productservice.domain.ProductStatus; + +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; + +@Entity +@Getter +@NoArgsConstructor(access= AccessLevel.PROTECTED) +@Table(name = "p_products") +public class ProductJpaEntity { + @Id + @Column(columnDefinition = "uuid") + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private BigDecimal price; + + @Column(nullable = false) + private Integer stock; + + @Column(nullable = false) + private ProductStatus status; + + @Column(columnDefinition = "uuid", nullable = false) + private UUID companyId; + + private String companyName; + + @Column(columnDefinition = "uuid") + private UUID hubId; + + private Boolean isHide; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(columnDefinition = "uuid") + private UUID createdBy; + + private LocalDateTime updatedAt; + + @Column(columnDefinition = "uuid") + private UUID updatedBy; + + private LocalDateTime deletedAt; + + @Column(columnDefinition = "uuid") + private UUID deletedBy; + + public static ProductJpaEntity from(Product product) { + ProductJpaEntity entity=new ProductJpaEntity(); + entity.id=product.getId(); + entity.name=product.getName(); + entity.price=product.getPrice(); + entity.stock=product.getStock(); + entity.status=product.getStatus(); + entity.companyId=product.getCompanyId(); + entity.companyName=product.getCompanyName(); + entity.hubId=product.getHubId(); + entity.isHide=product.getIsHide(); + entity.createdAt=product.getCreatedAt(); + entity.createdBy=product.getCreatedBy(); + entity.updatedAt=product.getUpdatedAt(); + entity.updatedBy=product.getUpdatedBy(); + entity.deletedAt=product.getDeletedAt(); + entity.deletedBy=product.getDeletedBy(); + return entity; + } + + public Product toDomain() { + return Product.reconstruct(id, name, price, stock, + status, companyId, companyName, hubId, isHide, + createdBy, createdAt, updatedAt, updatedBy, deletedAt, deletedBy); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java new file mode 100644 index 0000000..793543d --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java @@ -0,0 +1,7 @@ +package com.shipflow.productservice.infrastructure.persistence; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductJpaRepository extends JpaRepository { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java new file mode 100644 index 0000000..5678510 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.shipflow.productservice.infrastructure.persistence; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.stereotype.Repository; + +import com.shipflow.productservice.domain.Product; +import com.shipflow.productservice.domain.repository.ProductRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ProductRepositoryImpl implements ProductRepository { + private final ProductJpaRepository jpaRepository; + + @Override + public Optional findById(UUID id) { + ProductJpaEntity entity=jpaRepository.findById(id) + .orElseThrow(() -> new RuntimeException("해당 제품을 찾을 수 없습니다.")); + Product product=entity.toDomain(); + return Optional.of(product); + } + + @Override + public void save(Product product) { + ProductJpaEntity entity=ProductJpaEntity.from(product); + jpaRepository.save(entity); + } + + @Override + public List findAll() { + List entities=jpaRepository.findAll(); + return entities.stream() + .map(ProductJpaEntity::toDomain) + .toList(); + } +} From 41afdb76fa88ad970b565d93c3790401127ddf03 Mon Sep 17 00:00:00 2001 From: jin Date: Wed, 1 Apr 2026 20:48:04 +0900 Subject: [PATCH 03/38] =?UTF-8?q?feature:=20application,=20presentation=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: domain 계층 폴더 세분화 --- .../ProductserviceApplication.java | 2 + .../application/client/VendorFeignClient.java | 15 +++ .../dto/request/VendorInfoRequest.java | 10 ++ .../dto/response/VendorInfoResponse.java | 13 +++ .../application/mapper/ProductMapper.java | 15 +++ .../application/service/ProductService.java | 68 ++++++++++++++ .../productservice/domain/Product.java | 69 -------------- .../domain/{ => model}/BaseEntity.java | 2 +- .../productservice/domain/model/Product.java | 93 +++++++++++++++++++ .../domain/{ => model}/ProductStatus.java | 2 +- .../domain/repository/ProductRepository.java | 2 +- .../productservice/domain/vo/StockInfo.java | 31 +++++++ .../productservice/domain/vo/VendorInfo.java | 25 +++++ .../persistence/ProductJpaEntity.java | 40 ++++---- .../persistence/ProductRepositoryImpl.java | 2 +- .../ProductExternalController.java | 67 +++++++++++++ .../presentation/UserContext.java | 36 +++++++ .../dto/request/ProductCreateRequest.java | 16 ++++ .../dto/request/ProductUpdateInfoRequest.java | 8 ++ .../request/ProductUpdateStockRequest.java | 6 ++ .../dto/response/ProductCreateResponse.java | 14 +++ .../dto/response/ProductUpdateResponse.java | 14 +++ 22 files changed, 457 insertions(+), 93 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java delete mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/Product.java rename product-service/src/main/java/com/shipflow/productservice/domain/{ => model}/BaseEntity.java (91%) create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java rename product-service/src/main/java/com/shipflow/productservice/domain/{ => model}/ProductStatus.java (60%) create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/vo/VendorInfo.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductCreateRequest.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java diff --git a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java index 1acbbca..f7d4b5e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java +++ b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +@EnableFeignClients @SpringBootApplication public class ProductserviceApplication { diff --git a/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java b/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java new file mode 100644 index 0000000..11d32f0 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java @@ -0,0 +1,15 @@ +package com.shipflow.productservice.application.client; + +import java.util.UUID; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import com.shipflow.productservice.application.dto.response.VendorInfoResponse; + +@FeignClient(name = "company-service"/*,url="${}"*/)//todo: yaml 파일 설정 추가 후 url설정 +public interface VendorFeignClient { + @GetMapping("/internal/companies/{companyId}") + VendorInfoResponse getVendorInfo(@PathVariable("companyId") UUID companyId); +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java new file mode 100644 index 0000000..0c2a822 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java @@ -0,0 +1,10 @@ +package com.shipflow.productservice.application.dto.request; + +import java.util.UUID; + +import lombok.NonNull; + +public record VendorInfoRequest( + @NonNull UUID companyId +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java new file mode 100644 index 0000000..ab8ea0c --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java @@ -0,0 +1,13 @@ +package com.shipflow.productservice.application.dto.response; + +import java.util.UUID; + +import jakarta.validation.constraints.NotBlank; +import lombok.NonNull; + +public record VendorInfoResponse( + @NonNull UUID companyId, + @NotBlank String companyName, + @NonNull UUID hubId +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java new file mode 100644 index 0000000..e00e2d6 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -0,0 +1,15 @@ +package com.shipflow.productservice.application.mapper; + +import org.mapstruct.Mapper; + +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + //Entity->DTO + ProductCreateResponse toCreateResponse(Product product); + + ProductUpdateResponse toUpdateResponse(Product product); +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java new file mode 100644 index 0000000..42d6f0f --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -0,0 +1,68 @@ +package com.shipflow.productservice.application.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.shipflow.productservice.application.client.VendorFeignClient; +import com.shipflow.productservice.application.dto.response.VendorInfoResponse; +import com.shipflow.productservice.application.mapper.ProductMapper; +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.domain.repository.ProductRepository; +import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; +import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ProductService { + private ProductRepository productRepository; + private ProductMapper mapper; + private VendorFeignClient vendorClient; + + @Transactional + public ProductCreateResponse create(UUID companyId, ProductCreateRequest request, UUID createrId) { + VendorInfoResponse response = vendorClient.getVendorInfo(companyId); + Product product = Product.create( + request.name(), request.price(), request.stock(), + request.status(), companyId, response.companyName(), response.hubId(), + createrId); + productRepository.save(product); + return mapper.toCreateResponse(product); + } + + @Transactional + public void delete(UUID productId, UUID deleterId) { + Product product = findUserById(productId); + product.delete(deleterId); + productRepository.save(product); + } + + @Transactional + public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest request, UUID updaterId) { + Product product = findUserById(productId); + product.updateInfo( + request.productName(), request.price(), updaterId + ); + productRepository.save(product); + return mapper.toUpdateResponse(product); + } + + @Transactional + public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID updaterId) { + Product product = findUserById(productId); + product.updateStock(request.stock()); + return mapper.toUpdateResponse(product); + } + + private Product findUserById(UUID productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new IllegalArgumentException("해당 제품을 찾을 수 없습니다.")); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/Product.java deleted file mode 100644 index f8777c7..0000000 --- a/product-service/src/main/java/com/shipflow/productservice/domain/Product.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.shipflow.productservice.domain; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.Objects; -import java.util.UUID; - -import lombok.Getter; - -@Getter -public class Product extends BaseEntity { - private UUID id; - private String name; - private BigDecimal price; - private Integer stock; - private ProductStatus status; - private UUID companyId; - private String companyName; - private UUID hubId; - private Boolean isHide; - - public static Product create(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, - UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy) { - Product product = new Product(id, name, price, stock, status, companyId, companyName, hubId, isHide); - product.create(createdBy); - return product; - } - - private Product(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, - UUID companyId, String companyName, UUID hubId, Boolean isHide){ - this.id = id; - this.name = Objects.requireNonNull(name, "name은 필수값입니다."); - this.price = Objects.requireNonNull(price, "price는 필수값입니다."); - this.stock = Objects.requireNonNullElse(stock, 0); - this.status = Objects.requireNonNullElse(status, ProductStatus.STOPPED); - this.companyId = Objects.requireNonNull(companyId, "companyId는 필수값입니다."); - this.companyName = companyName; - this.hubId = hubId; - this.isHide = Objects.requireNonNullElse(isHide, false); - } - - public static Product reconstruct(UUID id, String name, BigDecimal price, Integer stock, ProductStatus status, - UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy, LocalDateTime createdAt, - LocalDateTime updatedAt, UUID updatedBy, LocalDateTime deletedAt, UUID deletedBy) { - Product product = new Product(id, name, price, stock, status, companyId, companyName, hubId, isHide); - product.createdAt = createdAt; - product.createdBy = createdBy; - product.updatedAt = updatedAt; - product.updatedBy = updatedBy; - product.deletedAt = deletedAt; - product.deletedBy = deletedBy; - return product; - } - - public void updateInfo(String name, BigDecimal price, UUID updatedBy) { - if (name != null && !name.isBlank()) - this.name = name; - - if(price.compareTo(BigDecimal.ZERO) <=0) - throw new IllegalArgumentException("price는 0보다 커야 합니다."); - else - this.price = price; - this.update(updatedBy); - } - - public void delete(UUID deletedBy) { - super.delete(deletedBy); - } -} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java similarity index 91% rename from product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java rename to product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java index 92c54ae..b7bffa4 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/BaseEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.domain; +package com.shipflow.productservice.domain.model; import java.time.LocalDateTime; import java.util.UUID; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java new file mode 100644 index 0000000..e711c6b --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -0,0 +1,93 @@ +package com.shipflow.productservice.domain.model; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +import com.shipflow.productservice.domain.vo.StockInfo; +import com.shipflow.productservice.domain.vo.VendorInfo; + +import lombok.Getter; + +@Getter +public class Product extends BaseEntity { + private UUID id; + private String name; + private BigDecimal price; + private ProductStatus status; + private Boolean isHide; + private StockInfo stockInfo; + private VendorInfo vendorInfo; + + private Product(String name, BigDecimal price, Integer stock, ProductStatus status, + UUID companyId, String companyName, UUID hubId) { + this.name = Objects.requireNonNull(name, "name은 필수값입니다."); + this.price = Objects.requireNonNull(price, "price는 필수값입니다."); + this.status = Objects.requireNonNullElse(status, ProductStatus.STOPPED); + this.isHide = this.status == ProductStatus.STOPPED || stock == 0; + this.stockInfo = new StockInfo(stock); + this.vendorInfo = new VendorInfo(companyId, companyName, hubId); + } + + public static Product create(String name, BigDecimal price, Integer stock, ProductStatus status, + UUID companyId, String companyName, UUID hubId, UUID createdBy) { + Product product = new Product(name, price, stock, status, companyId, companyName, hubId); + product.create(createdBy); + return product; + } + + public static Product reconstruct(String name, BigDecimal price, + Integer stock, ProductStatus status, UUID companyId, String companyName, + UUID hubId, Boolean isHide, UUID createdBy, LocalDateTime createdAt, + LocalDateTime updatedAt, UUID updatedBy, LocalDateTime deletedAt, UUID deletedBy) { + Product product = new Product(name, price, stock, status, companyId, companyName, hubId); + product.isHide = isHide; + product.createdAt = createdAt; + product.createdBy = createdBy; + product.updatedAt = updatedAt; + product.updatedBy = updatedBy; + product.deletedAt = deletedAt; + product.deletedBy = deletedBy; + return product; + } + + public void updateInfo(String name, BigDecimal price, UUID updatedBy) { + if (name != null && !name.isBlank()) + this.name = name; + + if (price.compareTo(BigDecimal.ZERO) <= 0) + throw new IllegalArgumentException("price는 0보다 커야 합니다."); + else + this.price = price; + this.update(updatedBy); + } + + public void updateVendorInfo(UUID companyId, String companyName, UUID hubId) { + this.vendorInfo = new VendorInfo(companyId, companyName, hubId); + } + + public void updateStatus(ProductStatus status) { + this.status = status; + if (status.equals(ProductStatus.STOPPED) || status.equals(ProductStatus.DISCONTINUED) + || status.equals(ProductStatus.OUT_OF_STOCK)) + this.isHide = true; + } + + public void updateStock(Integer stock) { + this.stockInfo.setStock(stock); + if (stock == 0) + this.isHide = true; + } + + public void decreaseStock(Integer quantity) { + this.stockInfo.decrease(quantity); + if (this.stockInfo.getStock() == 0) + this.isHide = true; + } + + public void delete(UUID deletedBy) { + super.delete(deletedBy); + this.isHide = true; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/ProductStatus.java similarity index 60% rename from product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java rename to product-service/src/main/java/com/shipflow/productservice/domain/model/ProductStatus.java index 3936153..46968b1 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/ProductStatus.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/ProductStatus.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.domain; +package com.shipflow.productservice.domain.model; public enum ProductStatus { ON_SALE, STOPPED, OUT_OF_STOCK, DISCONTINUED diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java index fe851bb..55613e3 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -4,7 +4,7 @@ import java.util.Optional; import java.util.UUID; -import com.shipflow.productservice.domain.Product; +import com.shipflow.productservice.domain.model.Product; public interface ProductRepository { Optional findById(UUID id); diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java new file mode 100644 index 0000000..19ababb --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java @@ -0,0 +1,31 @@ +package com.shipflow.productservice.domain.vo; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StockInfo { + private Integer stock; + + public StockInfo(Integer stock) { + if (stock == null || stock < 0) + throw new IllegalArgumentException("재고는 0보다 작을 수 없습니다."); + this.stock = stock; + } + + public void setStock(Integer stock) { + this.stock = stock; + } + + public void decrease(Integer quantity) { + if (quantity == null || quantity < 0) + throw new IllegalArgumentException("유효하지 않은 수량입니다."); + else if (quantity > stock) + throw new IllegalArgumentException("보유 재고보다 많은 요청입니다."); + this.stock -= quantity; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/vo/VendorInfo.java b/product-service/src/main/java/com/shipflow/productservice/domain/vo/VendorInfo.java new file mode 100644 index 0000000..ef15383 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/vo/VendorInfo.java @@ -0,0 +1,25 @@ +package com.shipflow.productservice.domain.vo; + +import java.util.Objects; +import java.util.UUID; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class VendorInfo { + private UUID companyId; + private String companyName; + private UUID hubId; + + public VendorInfo( + UUID companyId, String companyName, UUID hubId) { + this.companyId = Objects.requireNonNull(companyId, "ComapnyId는 필수값입니다."); + this.companyName = companyName; + this.hubId = hubId; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java index 7e18e0c..b5f3384 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java @@ -4,8 +4,8 @@ import java.time.LocalDateTime; import java.util.UUID; -import com.shipflow.productservice.domain.Product; -import com.shipflow.productservice.domain.ProductStatus; +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.domain.model.ProductStatus; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,7 +17,7 @@ @Entity @Getter -@NoArgsConstructor(access= AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "p_products") public class ProductJpaEntity { @Id @@ -63,27 +63,27 @@ public class ProductJpaEntity { private UUID deletedBy; public static ProductJpaEntity from(Product product) { - ProductJpaEntity entity=new ProductJpaEntity(); - entity.id=product.getId(); - entity.name=product.getName(); - entity.price=product.getPrice(); - entity.stock=product.getStock(); - entity.status=product.getStatus(); - entity.companyId=product.getCompanyId(); - entity.companyName=product.getCompanyName(); - entity.hubId=product.getHubId(); - entity.isHide=product.getIsHide(); - entity.createdAt=product.getCreatedAt(); - entity.createdBy=product.getCreatedBy(); - entity.updatedAt=product.getUpdatedAt(); - entity.updatedBy=product.getUpdatedBy(); - entity.deletedAt=product.getDeletedAt(); - entity.deletedBy=product.getDeletedBy(); + ProductJpaEntity entity = new ProductJpaEntity(); + entity.id = product.getId(); + entity.name = product.getName(); + entity.price = product.getPrice(); + entity.stock = product.getStockInfo().getStock(); + entity.status = product.getStatus(); + entity.companyId = product.getVendorInfo().getCompanyId(); + entity.companyName = product.getVendorInfo().getCompanyName(); + entity.hubId = product.getVendorInfo().getHubId(); + entity.isHide = product.getIsHide(); + entity.createdAt = product.getCreatedAt(); + entity.createdBy = product.getCreatedBy(); + entity.updatedAt = product.getUpdatedAt(); + entity.updatedBy = product.getUpdatedBy(); + entity.deletedAt = product.getDeletedAt(); + entity.deletedBy = product.getDeletedBy(); return entity; } public Product toDomain() { - return Product.reconstruct(id, name, price, stock, + return Product.reconstruct(name, price, stock, status, companyId, companyName, hubId, isHide, createdBy, createdAt, updatedAt, updatedBy, deletedAt, deletedBy); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index 5678510..d0e746b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -6,7 +6,7 @@ import org.springframework.stereotype.Repository; -import com.shipflow.productservice.domain.Product; +import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.repository.ProductRepository; import lombok.RequiredArgsConstructor; diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java new file mode 100644 index 0000000..e0760ab --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -0,0 +1,67 @@ +package com.shipflow.productservice.presentation; + +import java.util.UUID; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +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.productservice.application.service.ProductService; +import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; +import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; + +@RestController("/api/companies/{companyId}/products") +@RequiredArgsConstructor +public class ProductExternalController { + private final ProductService productService; + + @PostMapping("/") + public ResponseEntity addProduct(@PathVariable UUID companyId, + @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { + UUID createrId = getUserId(request); + ProductCreateResponse response = productService.create(companyId, productCreateRequest, createrId); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @DeleteMapping("/{productId}") + public ResponseEntity deleteProduct(@PathVariable UUID productId, + HttpServletRequest request) { + UUID deleterId = getUserId(request); + productService.delete(deleterId, productId); + return ResponseEntity.status(HttpStatus.OK).body("요청이 정상 처리되었습니다."); + } + + @PatchMapping("/{productId}") + public ResponseEntity updateProductInfo(@PathVariable UUID productId, + @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest, + HttpServletRequest request) { + UUID updaterId = getUserId(request); + ProductUpdateResponse response = productService.updateInfo(productId, productUpdateInfoRequest, updaterId); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + @PostMapping("/{productId}/stock") + public ResponseEntity updateStock(@PathVariable UUID productId, + ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { + UUID updaterId = getUserId(request); + ProductUpdateResponse response = productService.updateStock(productId, + productUpdateStockRequest, updaterId); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + private UUID getUserId(HttpServletRequest request) { + UserContext.setUserContext(request); + return UserContext.getUserId(); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java b/product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java new file mode 100644 index 0000000..f740491 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java @@ -0,0 +1,36 @@ +package com.shipflow.productservice.presentation; + +import java.util.UUID; + +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; + +@Component +public class UserContext { + private static final ThreadLocal USER_ID_HOLDER = new ThreadLocal<>(); + private static final ThreadLocal USER_ROLE_HOLDER = new ThreadLocal<>(); + + public static void setUserContext(HttpServletRequest request) { + String userId = request.getHeader("X-User-Id"); + String userRole = request.getHeader("X-User-Role"); + + if (userId != null) + USER_ID_HOLDER.set(UUID.fromString(userId)); + if (userRole != null) + USER_ROLE_HOLDER.set(userRole); + } + + public static UUID getUserId() { + return USER_ID_HOLDER.get(); + } + + public static String getUserRole() { + return USER_ROLE_HOLDER.get(); + } + + public static void clear() { + USER_ID_HOLDER.remove(); + USER_ROLE_HOLDER.remove(); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductCreateRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductCreateRequest.java new file mode 100644 index 0000000..83c902b --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductCreateRequest.java @@ -0,0 +1,16 @@ +package com.shipflow.productservice.presentation.dto.request; + +import java.math.BigDecimal; + +import com.shipflow.productservice.domain.model.ProductStatus; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ProductCreateRequest( + @NotBlank String name, + @NotNull BigDecimal price, + @NotNull Integer stock, + ProductStatus status +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java new file mode 100644 index 0000000..b4cb5a8 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java @@ -0,0 +1,8 @@ +package com.shipflow.productservice.presentation.dto.request; + +import java.math.BigDecimal; + +public record ProductUpdateInfoRequest( + String productName, BigDecimal price +) { +} \ No newline at end of file diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java new file mode 100644 index 0000000..09adde7 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java @@ -0,0 +1,6 @@ +package com.shipflow.productservice.presentation.dto.request; + +public record ProductUpdateStockRequest( + Integer stock +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java new file mode 100644 index 0000000..7049662 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java @@ -0,0 +1,14 @@ +package com.shipflow.productservice.presentation.dto.response; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.productservice.domain.model.ProductStatus; + +public record ProductCreateResponse( + String name, String description, BigDecimal price, Integer stock, + ProductStatus productStatus, UUID companyId, String companyName, + UUID hubId, Boolean isHide, LocalDateTime createdAt +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java new file mode 100644 index 0000000..c271a06 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java @@ -0,0 +1,14 @@ +package com.shipflow.productservice.presentation.dto.response; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.productservice.domain.model.ProductStatus; + +public record ProductUpdateResponse( + String name, String description, BigDecimal price, Integer stock, + ProductStatus productStatus, UUID companyId, String companyName, + UUID hubId, Boolean isHide, LocalDateTime updateAt +) { +} From 2b4c07ecffb5aafc5c957c1e7ca76fe0a6a3ecb4 Mon Sep 17 00:00:00 2001 From: jin Date: Wed, 1 Apr 2026 21:00:13 +0900 Subject: [PATCH 04/38] =?UTF-8?q?refactor:=20ProductService=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=A0=9C=ED=95=9C=EC=9E=90=20=EC=88=98=EC=A0=95,=20up?= =?UTF-8?q?dateby=20=EA=B8=B0=EB=A1=9D=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 8 ++++---- .../shipflow/productservice/domain/model/Product.java | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 42d6f0f..41df63a 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -22,9 +22,9 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class ProductService { - private ProductRepository productRepository; - private ProductMapper mapper; - private VendorFeignClient vendorClient; + private final ProductRepository productRepository; + private final ProductMapper mapper; + private final VendorFeignClient vendorClient; @Transactional public ProductCreateResponse create(UUID companyId, ProductCreateRequest request, UUID createrId) { @@ -57,7 +57,7 @@ public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest @Transactional public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID updaterId) { Product product = findUserById(productId); - product.updateStock(request.stock()); + product.updateStock(request.stock(), updaterId); return mapper.toUpdateResponse(product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index e711c6b..9c4bc15 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -63,21 +63,24 @@ public void updateInfo(String name, BigDecimal price, UUID updatedBy) { this.update(updatedBy); } - public void updateVendorInfo(UUID companyId, String companyName, UUID hubId) { + public void updateVendorInfo(UUID companyId, String companyName, UUID hubId, UUID updatedBy) { this.vendorInfo = new VendorInfo(companyId, companyName, hubId); + this.update(updatedBy); } - public void updateStatus(ProductStatus status) { + public void updateStatus(ProductStatus status, UUID updatedBy) { this.status = status; if (status.equals(ProductStatus.STOPPED) || status.equals(ProductStatus.DISCONTINUED) || status.equals(ProductStatus.OUT_OF_STOCK)) this.isHide = true; + this.update(updatedBy); } - public void updateStock(Integer stock) { + public void updateStock(Integer stock, UUID updatedBy) { this.stockInfo.setStock(stock); if (stock == 0) this.isHide = true; + this.update(updatedBy); } public void decreaseStock(Integer quantity) { From dff8af09235b560a35308f943895ac0d15f5a9bd Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 03:00:30 +0900 Subject: [PATCH 05/38] =?UTF-8?q?feat:=20product=20-=20read=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/ProductMapper.java | 6 ++++ .../application/service/ProductService.java | 23 ++++++++++++--- .../domain/repository/ProductRepository.java | 8 +++-- .../persistence/ProductJpaRepository.java | 4 +++ .../persistence/ProductRepositoryImpl.java | 17 +++++------ .../ProductExternalController.java | 21 ++++++++++++++ .../dto/response/ProductInfoResponse.java | 29 +++++++++++++++++++ .../dto/response/ProductListResponse.java | 12 ++++++++ 8 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index e00e2d6..113e93b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -4,6 +4,8 @@ import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductInfoResponse; +import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; @Mapper(componentModel = "spring") @@ -12,4 +14,8 @@ public interface ProductMapper { ProductCreateResponse toCreateResponse(Product product); ProductUpdateResponse toUpdateResponse(Product product); + + ProductInfoResponse toProductInfoResponse(Product product); + + ProductListResponse toProductListResponse(Product product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 41df63a..f46f0a1 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -2,6 +2,8 @@ import java.util.UUID; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +16,8 @@ import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductInfoResponse; +import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; import lombok.RequiredArgsConstructor; @@ -39,14 +43,14 @@ public ProductCreateResponse create(UUID companyId, ProductCreateRequest request @Transactional public void delete(UUID productId, UUID deleterId) { - Product product = findUserById(productId); + Product product = findProductById(productId); product.delete(deleterId); productRepository.save(product); } @Transactional public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest request, UUID updaterId) { - Product product = findUserById(productId); + Product product = findProductById(productId); product.updateInfo( request.productName(), request.price(), updaterId ); @@ -56,12 +60,23 @@ public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest @Transactional public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID updaterId) { - Product product = findUserById(productId); + Product product = findProductById(productId); product.updateStock(request.stock(), updaterId); return mapper.toUpdateResponse(product); } - private Product findUserById(UUID productId) { + public ProductInfoResponse getProductInfo(UUID productId) { + Product product = findProductById(productId); + return mapper.toProductInfoResponse(product); + } + + public Slice getProductList(UUID companyId, Pageable pageable) { + Slice products = productRepository.findAllByCompanyId(companyId, pageable); + return products.map(mapper::toProductListResponse); + } + + //util + private Product findProductById(UUID productId) { return productRepository.findById(productId) .orElseThrow(() -> new IllegalArgumentException("해당 제품을 찾을 수 없습니다.")); } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java index 55613e3..d4cfaa7 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -1,13 +1,17 @@ package com.shipflow.productservice.domain.repository; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + import com.shipflow.productservice.domain.model.Product; public interface ProductRepository { Optional findById(UUID id); + void save(Product product); - List findAll(); + + Slice findAllByCompanyId(UUID companyId, Pageable pageable); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java index 793543d..c3b0456 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java @@ -1,7 +1,11 @@ package com.shipflow.productservice.infrastructure.persistence; import java.util.UUID; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductJpaRepository extends JpaRepository { + Slice findAllByCompanyId(UUID companyId, Pageable pageable); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index d0e746b..6387f97 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -1,9 +1,10 @@ package com.shipflow.productservice.infrastructure.persistence; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; import com.shipflow.productservice.domain.model.Product; @@ -18,23 +19,21 @@ public class ProductRepositoryImpl implements ProductRepository { @Override public Optional findById(UUID id) { - ProductJpaEntity entity=jpaRepository.findById(id) + ProductJpaEntity entity = jpaRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 제품을 찾을 수 없습니다.")); - Product product=entity.toDomain(); + Product product = entity.toDomain(); return Optional.of(product); } @Override public void save(Product product) { - ProductJpaEntity entity=ProductJpaEntity.from(product); + ProductJpaEntity entity = ProductJpaEntity.from(product); jpaRepository.save(entity); } @Override - public List findAll() { - List entities=jpaRepository.findAll(); - return entities.stream() - .map(ProductJpaEntity::toDomain) - .toList(); + public Slice findAllByCompanyId(UUID companyId, Pageable pageable) { + Slice entities = jpaRepository.findAllByCompanyId(companyId, pageable); + return entities.map(ProductJpaEntity::toDomain); } } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java index e0760ab..9978241 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -2,9 +2,13 @@ import java.util.UUID; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +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; @@ -16,6 +20,8 @@ import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; +import com.shipflow.productservice.presentation.dto.response.ProductInfoResponse; +import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; import jakarta.servlet.http.HttpServletRequest; @@ -60,6 +66,21 @@ public ResponseEntity updateStock(@PathVariable UUID prod return ResponseEntity.status(HttpStatus.OK).body(response); } + @GetMapping("/{productId}") + public ResponseEntity getProductInfo(@PathVariable UUID productId) { + ProductInfoResponse response = productService.getProductInfo(productId); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + @GetMapping + public ResponseEntity> getProductList(@PathVariable UUID companyId, + @PageableDefault(size = 10, page = 0, sort = {"createdAt", + "deletedAt"}) Pageable pageable) { + Slice response = productService.getProductList(companyId, pageable); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + //util private UUID getUserId(HttpServletRequest request) { UserContext.setUserContext(request); return UserContext.getUserId(); diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java new file mode 100644 index 0000000..d7588a2 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java @@ -0,0 +1,29 @@ +package com.shipflow.productservice.presentation.dto.response; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.productservice.domain.model.ProductStatus; + +import jakarta.validation.constraints.NotBlank; +import lombok.NonNull; + +public record ProductInfoResponse( + @NonNull UUID productId, + @NotBlank String productName, + @NonNull BigDecimal price, + @NonNull Integer stock, + @NonNull ProductStatus status, // Enum 타입 가정 + @NonNull UUID companyId, + @NotBlank String companyName, + @NonNull UUID hubId, + @NonNull Boolean isHide, + @NonNull LocalDateTime createdAt, + @NonNull UUID createdBy, + LocalDateTime updatedAt, + UUID updatedBy, + LocalDateTime deletedAt, + UUID deletedBy +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java new file mode 100644 index 0000000..ec5baa4 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java @@ -0,0 +1,12 @@ +package com.shipflow.productservice.presentation.dto.response; + +import java.util.UUID; + +import jakarta.validation.constraints.NotBlank; +import lombok.NonNull; + +public record ProductListResponse( + @NonNull UUID productId, + @NotBlank String productName +) { +} From c01ae4bc6a9137f0cbbd19553ba0da8fcbf4b87a Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 11:32:17 +0900 Subject: [PATCH 06/38] =?UTF-8?q?refactor:=20common=20module=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9,=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C=EC=9D=80=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipflow/common/domain/BaseEntity.java | 55 ++++++------ product-service/build.gradle | 86 +++++++++---------- .../application/service/ProductService.java | 17 ++-- .../domain/exception/ProductErrorCode.java | 35 ++++++++ .../persistence/ProductJpaEntity.java | 22 +---- .../web}/UserContext.java | 2 +- .../ProductExternalController.java | 44 +++++----- 7 files changed, 144 insertions(+), 117 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java rename product-service/src/main/java/com/shipflow/productservice/{presentation => infrastructure/web}/UserContext.java (93%) diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index a28732d..cef72b8 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -1,15 +1,20 @@ package com.shipflow.common.domain; -import jakarta.persistence.*; -import lombok.Getter; +import java.time.LocalDateTime; +import java.util.UUID; + import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDateTime; -import java.util.UUID; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; @Getter @MappedSuperclass @@ -17,32 +22,32 @@ @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { - @Column(nullable = false, updatable = false) - @CreatedDate - private LocalDateTime createdAt; + @Column(nullable = false, updatable = false) + @CreatedDate + protected LocalDateTime createdAt; - @Column(nullable = false) - @CreatedBy - private UUID createdBy; + @Column(nullable = false) + @CreatedBy + protected UUID createdBy; - @LastModifiedDate - @Column(nullable = false) - private LocalDateTime updatedAt; + @LastModifiedDate + @Column(nullable = false) + protected LocalDateTime updatedAt; - @LastModifiedBy - @Column(nullable = false) - private UUID updatedBy; + @LastModifiedBy + @Column(nullable = false) + protected UUID updatedBy; - private LocalDateTime deletedAt; + protected LocalDateTime deletedAt; - private UUID deletedBy; + protected UUID deletedBy; - protected void softDelete(UUID userId) { - this.deletedAt = LocalDateTime.now(); - this.deletedBy = userId; - } + protected void softDelete(UUID userId) { + this.deletedAt = LocalDateTime.now(); + this.deletedBy = userId; + } - public boolean isDeleted() { - return deletedAt != null; - } + public boolean isDeleted() { + return deletedAt != null; + } } \ No newline at end of file diff --git a/product-service/build.gradle b/product-service/build.gradle index c445fa1..c68bd61 100644 --- a/product-service/build.gradle +++ b/product-service/build.gradle @@ -1,76 +1,76 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.13' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.5.13' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.flowship' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() - maven {url = uri('https://repo.spring.io/milestone') } + mavenCentral() + maven { url = uri('https://repo.spring.io/milestone') } } ext { - springCloudVersion = "2025.0.0" + springCloudVersion = "2025.0.0" } dependencies { -// implementation project(':common') + implementation project(':common') - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-amqp' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'io.micrometer:micrometer-tracing-bridge-brave' - implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.postgresql:postgresql' - implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' - annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' - annotationProcessor 'jakarta.annotation:jakarta.annotation-api' - annotationProcessor 'jakarta.persistence:jakarta.persistence-api' - implementation 'org.mapstruct:mapstruct:1.5.5.Final' - compileOnly 'org.projectlombok:lombok' + implementation 'org.postgresql:postgresql' + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' - annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.amqp:spring-rabbit-test' - testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.amqp:spring-rabbit-test' + testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index f46f0a1..ec27574 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -7,11 +7,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.shipflow.common.exception.BusinessException; import com.shipflow.productservice.application.client.VendorFeignClient; import com.shipflow.productservice.application.dto.response.VendorInfoResponse; import com.shipflow.productservice.application.mapper.ProductMapper; +import com.shipflow.productservice.domain.exception.ProductErrorCode; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.repository.ProductRepository; +import com.shipflow.productservice.infrastructure.web.UserContext; import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; @@ -31,7 +34,8 @@ public class ProductService { private final VendorFeignClient vendorClient; @Transactional - public ProductCreateResponse create(UUID companyId, ProductCreateRequest request, UUID createrId) { + public ProductCreateResponse create(UUID companyId, ProductCreateRequest request) { + UUID createrId = UserContext.getUserId(); VendorInfoResponse response = vendorClient.getVendorInfo(companyId); Product product = Product.create( request.name(), request.price(), request.stock(), @@ -42,14 +46,16 @@ public ProductCreateResponse create(UUID companyId, ProductCreateRequest request } @Transactional - public void delete(UUID productId, UUID deleterId) { + public void delete(UUID productId) { + UUID deleterId = UserContext.getUserId(); Product product = findProductById(productId); product.delete(deleterId); productRepository.save(product); } @Transactional - public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest request, UUID updaterId) { + public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest request) { + UUID updaterId = UserContext.getUserId(); Product product = findProductById(productId); product.updateInfo( request.productName(), request.price(), updaterId @@ -59,7 +65,8 @@ public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest } @Transactional - public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID updaterId) { + public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request) { + UUID updaterId = UserContext.getUserId(); Product product = findProductById(productId); product.updateStock(request.stock(), updaterId); return mapper.toUpdateResponse(product); @@ -78,6 +85,6 @@ public Slice getProductList(UUID companyId, Pageable pageab //util private Product findProductById(UUID productId) { return productRepository.findById(productId) - .orElseThrow(() -> new IllegalArgumentException("해당 제품을 찾을 수 없습니다.")); + .orElseThrow(() -> new BusinessException(ProductErrorCode.PRODUCT_NOT_FOUND, "해당 제품을 찾을 수 없습니다.")); } } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java new file mode 100644 index 0000000..f4945e5 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java @@ -0,0 +1,35 @@ +package com.shipflow.productservice.domain.exception; + +import org.springframework.http.HttpStatus; + +import com.shipflow.common.exception.ErrorCode; + +public enum ProductErrorCode implements ErrorCode { + + PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "Product not found"); + + private final String code; + private final HttpStatus status; + private final String message; + + ProductErrorCode(String code, HttpStatus status, String message) { + this.code = code; + this.status = status; + this.message = message; + } + + @Override + public String code() { + return code; + } + + @Override + public HttpStatus status() { + return status; + } + + @Override + public String message() { + return message; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java index b5f3384..e466e4d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java @@ -1,9 +1,9 @@ package com.shipflow.productservice.infrastructure.persistence; import java.math.BigDecimal; -import java.time.LocalDateTime; import java.util.UUID; +import com.shipflow.common.domain.BaseEntity; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.model.ProductStatus; @@ -18,8 +18,8 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "p_products") -public class ProductJpaEntity { +@Table(name = "p_product") +public class ProductJpaEntity extends BaseEntity { @Id @Column(columnDefinition = "uuid") private UUID id; @@ -46,22 +46,6 @@ public class ProductJpaEntity { private Boolean isHide; - @Column(nullable = false) - private LocalDateTime createdAt; - - @Column(columnDefinition = "uuid") - private UUID createdBy; - - private LocalDateTime updatedAt; - - @Column(columnDefinition = "uuid") - private UUID updatedBy; - - private LocalDateTime deletedAt; - - @Column(columnDefinition = "uuid") - private UUID deletedBy; - public static ProductJpaEntity from(Product product) { ProductJpaEntity entity = new ProductJpaEntity(); entity.id = product.getId(); diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java similarity index 93% rename from product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java index f740491..240bc1e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/UserContext.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.presentation; +package com.shipflow.productservice.infrastructure.web; import java.util.UUID; diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java index 9978241..9094f39 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -15,7 +15,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import com.shipflow.common.exception.ApiResponse; import com.shipflow.productservice.application.service.ProductService; +import com.shipflow.productservice.infrastructure.web.UserContext; import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; @@ -33,56 +35,50 @@ public class ProductExternalController { private final ProductService productService; @PostMapping("/") - public ResponseEntity addProduct(@PathVariable UUID companyId, + public ResponseEntity> addProduct(@PathVariable UUID companyId, @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { - UUID createrId = getUserId(request); - ProductCreateResponse response = productService.create(companyId, productCreateRequest, createrId); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + UserContext.setUserContext(request); + ProductCreateResponse response = productService.create(companyId, productCreateRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(response)); } @DeleteMapping("/{productId}") public ResponseEntity deleteProduct(@PathVariable UUID productId, HttpServletRequest request) { - UUID deleterId = getUserId(request); - productService.delete(deleterId, productId); + UserContext.setUserContext(request); + productService.delete(productId); return ResponseEntity.status(HttpStatus.OK).body("요청이 정상 처리되었습니다."); } @PatchMapping("/{productId}") - public ResponseEntity updateProductInfo(@PathVariable UUID productId, + public ResponseEntity> updateProductInfo(@PathVariable UUID productId, @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest, HttpServletRequest request) { - UUID updaterId = getUserId(request); - ProductUpdateResponse response = productService.updateInfo(productId, productUpdateInfoRequest, updaterId); - return ResponseEntity.status(HttpStatus.OK).body(response); + UserContext.setUserContext(request); + ProductUpdateResponse response = productService.updateInfo(productId, productUpdateInfoRequest); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @PostMapping("/{productId}/stock") - public ResponseEntity updateStock(@PathVariable UUID productId, + public ResponseEntity> updateStock(@PathVariable UUID productId, ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { - UUID updaterId = getUserId(request); + UserContext.setUserContext(request); ProductUpdateResponse response = productService.updateStock(productId, - productUpdateStockRequest, updaterId); - return ResponseEntity.status(HttpStatus.OK).body(response); + productUpdateStockRequest); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @GetMapping("/{productId}") - public ResponseEntity getProductInfo(@PathVariable UUID productId) { + public ResponseEntity> getProductInfo(@PathVariable UUID productId) { ProductInfoResponse response = productService.getProductInfo(productId); - return ResponseEntity.status(HttpStatus.OK).body(response); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @GetMapping - public ResponseEntity> getProductList(@PathVariable UUID companyId, + public ResponseEntity>> getProductList(@PathVariable UUID companyId, @PageableDefault(size = 10, page = 0, sort = {"createdAt", "deletedAt"}) Pageable pageable) { Slice response = productService.getProductList(companyId, pageable); - return ResponseEntity.status(HttpStatus.OK).body(response); - } - - //util - private UUID getUserId(HttpServletRequest request) { - UserContext.setUserContext(request); - return UserContext.getUserId(); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } } From 5e181ff7757756bf537dbcb3bd93fe73b4011836 Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 17:15:36 +0900 Subject: [PATCH 07/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/shipflow/common/domain/BaseEntity.java | 2 -- .../productservice/ProductserviceApplication.java | 2 ++ .../presentation/ProductExternalController.java | 15 +++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index cef72b8..9a2d3ac 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -31,11 +31,9 @@ public abstract class BaseEntity { protected UUID createdBy; @LastModifiedDate - @Column(nullable = false) protected LocalDateTime updatedAt; @LastModifiedBy - @Column(nullable = false) protected UUID updatedBy; protected LocalDateTime deletedAt; diff --git a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java index f7d4b5e..648e597 100644 --- a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java +++ b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.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.data.jpa.repository.config.EnableJpaAuditing; @EnableFeignClients +@EnableJpaAuditing @SpringBootApplication public class ProductserviceApplication { diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java index 9094f39..985009d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -13,6 +13,7 @@ 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; @@ -27,18 +28,21 @@ import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -@RestController("/api/companies/{companyId}/products") +@RestController +@RequestMapping("/api/companies/{companyId}/products") @RequiredArgsConstructor public class ProductExternalController { private final ProductService productService; - @PostMapping("/") + @PostMapping public ResponseEntity> addProduct(@PathVariable UUID companyId, - @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { + @Valid @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { UserContext.setUserContext(request); ProductCreateResponse response = productService.create(companyId, productCreateRequest); + UserContext.clear(); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(response)); } @@ -47,6 +51,7 @@ public ResponseEntity deleteProduct(@PathVariable UUID productId, HttpServletRequest request) { UserContext.setUserContext(request); productService.delete(productId); + UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body("요청이 정상 처리되었습니다."); } @@ -56,15 +61,17 @@ public ResponseEntity> updateProductInfo(@Pat HttpServletRequest request) { UserContext.setUserContext(request); ProductUpdateResponse response = productService.updateInfo(productId, productUpdateInfoRequest); + UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @PostMapping("/{productId}/stock") public ResponseEntity> updateStock(@PathVariable UUID productId, - ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { + @RequestBody ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { UserContext.setUserContext(request); ProductUpdateResponse response = productService.updateStock(productId, productUpdateStockRequest); + UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } From d5119c80015af7722a2ec54c355f9a121c496c8d Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 20:53:10 +0900 Subject: [PATCH 08/38] test: crud test --- .../dto/request/VendorInfoRequest.java | 2 +- .../dto/response/VendorInfoResponse.java | 4 +- .../application/service/ProductService.java | 9 +- .../domain/exception/ProductErrorCode.java | 5 +- .../productservice/domain/model/Product.java | 22 +- .../domain/repository/ProductRepository.java | 2 +- .../productservice/domain/vo/StockInfo.java | 11 +- .../persistence/ProductJpaEntity.java | 9 +- .../persistence/ProductRepositoryImpl.java | 5 +- .../dto/request/ProductUpdateInfoRequest.java | 2 +- .../dto/response/ProductInfoResponse.java | 4 +- .../dto/response/ProductListResponse.java | 4 +- .../service/ProductServiceTest.java | 214 ++++++++++++++++++ .../fixture/ProductFixture.java | 29 +++ 14 files changed, 299 insertions(+), 23 deletions(-) create mode 100644 product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java create mode 100644 product-service/src/test/java/com/shipflow/productservice/fixture/ProductFixture.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java index 0c2a822..e0aedf3 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/request/VendorInfoRequest.java @@ -5,6 +5,6 @@ import lombok.NonNull; public record VendorInfoRequest( - @NonNull UUID companyId + @NonNull UUID id ) { } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java index ab8ea0c..5e8838e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/VendorInfoResponse.java @@ -6,8 +6,8 @@ import lombok.NonNull; public record VendorInfoResponse( - @NonNull UUID companyId, - @NotBlank String companyName, + @NonNull UUID id, + @NotBlank String name, @NonNull UUID hubId ) { } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index ec27574..67f332d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -39,10 +39,10 @@ public ProductCreateResponse create(UUID companyId, ProductCreateRequest request VendorInfoResponse response = vendorClient.getVendorInfo(companyId); Product product = Product.create( request.name(), request.price(), request.stock(), - request.status(), companyId, response.companyName(), response.hubId(), + request.status(), companyId, response.name(), response.hubId(), createrId); - productRepository.save(product); - return mapper.toCreateResponse(product); + Product savedProduct =productRepository.save(product); + return mapper.toCreateResponse(savedProduct); } @Transactional @@ -58,7 +58,7 @@ public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest UUID updaterId = UserContext.getUserId(); Product product = findProductById(productId); product.updateInfo( - request.productName(), request.price(), updaterId + request.name(), request.price(), updaterId ); productRepository.save(product); return mapper.toUpdateResponse(product); @@ -69,6 +69,7 @@ public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockReque UUID updaterId = UserContext.getUserId(); Product product = findProductById(productId); product.updateStock(request.stock(), updaterId); + productRepository.save(product); return mapper.toUpdateResponse(product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java index f4945e5..ccbc50f 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java @@ -6,7 +6,10 @@ public enum ProductErrorCode implements ErrorCode { - PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "Product not found"); + PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "해당 상품을 찾을 수 없습니다."), + INVALID_STOCK_VALUE("INVALID_STOCK_VALUE", HttpStatus.BAD_REQUEST, "잘못된 재고값입니다."), + INVALID_ORDER_QUANTITY("INVALID_ORDER_QUANTITY", HttpStatus.BAD_REQUEST, "잘못된 주문량입니다."), + OUT_OF_STOCK("OUT_OF_STOCK",HttpStatus.BAD_REQUEST,"요청하신 주문량이 잔여 재고량보다 많습니다."); private final String code; private final HttpStatus status; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 9c4bc15..0144064 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -37,11 +37,12 @@ public static Product create(String name, BigDecimal price, Integer stock, Produ return product; } - public static Product reconstruct(String name, BigDecimal price, + public static Product reconstruct(UUID id,String name, BigDecimal price, Integer stock, ProductStatus status, UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy, LocalDateTime createdAt, LocalDateTime updatedAt, UUID updatedBy, LocalDateTime deletedAt, UUID deletedBy) { Product product = new Product(name, price, stock, status, companyId, companyName, hubId); + product.id=id; product.isHide = isHide; product.createdAt = createdAt; product.createdBy = createdBy; @@ -79,7 +80,8 @@ public void updateStatus(ProductStatus status, UUID updatedBy) { public void updateStock(Integer stock, UUID updatedBy) { this.stockInfo.setStock(stock); if (stock == 0) - this.isHide = true; + updateStatus(ProductStatus.OUT_OF_STOCK, updatedBy); + this.stockInfo.setStock(stock); this.update(updatedBy); } @@ -93,4 +95,20 @@ public void delete(UUID deletedBy) { super.delete(deletedBy); this.isHide = true; } + + public Integer getStock() { + return this.stockInfo.getStock(); + } + + public UUID getCompanyId () { + return this.vendorInfo.getCompanyId(); + } + + public String getCompanyName () { + return this.vendorInfo.getCompanyName(); + } + + public UUID getHubId () { + return this.vendorInfo.getHubId(); + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java index d4cfaa7..479ab60 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -11,7 +11,7 @@ public interface ProductRepository { Optional findById(UUID id); - void save(Product product); + Product save(Product product); Slice findAllByCompanyId(UUID companyId, Pageable pageable); } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java index 19ababb..5656b5a 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java @@ -1,5 +1,8 @@ package com.shipflow.productservice.domain.vo; +import com.shipflow.common.exception.BusinessException; +import com.shipflow.productservice.domain.exception.ProductErrorCode; + import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -13,19 +16,21 @@ public class StockInfo { public StockInfo(Integer stock) { if (stock == null || stock < 0) - throw new IllegalArgumentException("재고는 0보다 작을 수 없습니다."); + throw new BusinessException(ProductErrorCode.INVALID_STOCK_VALUE); this.stock = stock; } public void setStock(Integer stock) { + if (stock == null || stock < 0) + throw new BusinessException(ProductErrorCode.INVALID_STOCK_VALUE); this.stock = stock; } public void decrease(Integer quantity) { if (quantity == null || quantity < 0) - throw new IllegalArgumentException("유효하지 않은 수량입니다."); + throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); else if (quantity > stock) - throw new IllegalArgumentException("보유 재고보다 많은 요청입니다."); + throw new BusinessException(ProductErrorCode.OUT_OF_STOCK); this.stock -= quantity; } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java index e466e4d..5f58901 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java @@ -9,6 +9,9 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; @@ -17,10 +20,11 @@ @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @Table(name = "p_product") public class ProductJpaEntity extends BaseEntity { @Id + @GeneratedValue @Column(columnDefinition = "uuid") private UUID id; @@ -33,6 +37,7 @@ public class ProductJpaEntity extends BaseEntity { @Column(nullable = false) private Integer stock; + @Enumerated(value= EnumType.STRING) @Column(nullable = false) private ProductStatus status; @@ -67,7 +72,7 @@ public static ProductJpaEntity from(Product product) { } public Product toDomain() { - return Product.reconstruct(name, price, stock, + return Product.reconstruct(id,name, price, stock, status, companyId, companyName, hubId, isHide, createdBy, createdAt, updatedAt, updatedBy, deletedAt, deletedBy); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index 6387f97..c103a14 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -26,9 +26,10 @@ public Optional findById(UUID id) { } @Override - public void save(Product product) { + public Product save(Product product) { ProductJpaEntity entity = ProductJpaEntity.from(product); - jpaRepository.save(entity); + ProductJpaEntity savedEntity = jpaRepository.save(entity); + return savedEntity.toDomain(); } @Override diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java index b4cb5a8..e07d8da 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java @@ -3,6 +3,6 @@ import java.math.BigDecimal; public record ProductUpdateInfoRequest( - String productName, BigDecimal price + String name, BigDecimal price ) { } \ No newline at end of file diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java index d7588a2..00c13c0 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductInfoResponse.java @@ -10,8 +10,8 @@ import lombok.NonNull; public record ProductInfoResponse( - @NonNull UUID productId, - @NotBlank String productName, + @NonNull UUID id, + @NotBlank String name, @NonNull BigDecimal price, @NonNull Integer stock, @NonNull ProductStatus status, // Enum 타입 가정 diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java index ec5baa4..c32031f 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductListResponse.java @@ -6,7 +6,7 @@ import lombok.NonNull; public record ProductListResponse( - @NonNull UUID productId, - @NotBlank String productName + @NonNull UUID id, + @NotBlank String name ) { } diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java new file mode 100644 index 0000000..ebfee57 --- /dev/null +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -0,0 +1,214 @@ +package com.shipflow.productservice.application.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.mock.web.MockHttpServletRequest; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.productservice.application.client.VendorFeignClient; +import com.shipflow.productservice.application.dto.response.VendorInfoResponse; +import com.shipflow.productservice.application.mapper.ProductMapper; +import com.shipflow.productservice.domain.exception.ProductErrorCode; +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.domain.model.ProductStatus; +import com.shipflow.productservice.domain.repository.ProductRepository; +import com.shipflow.productservice.domain.vo.VendorInfo; +import com.shipflow.productservice.fixture.ProductFixture; +import com.shipflow.productservice.infrastructure.web.UserContext; +import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; +import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; +import com.shipflow.productservice.presentation.dto.response.ProductInfoResponse; +import com.shipflow.productservice.presentation.dto.response.ProductListResponse; + +@ExtendWith(MockitoExtension.class) +class ProductServiceTest { + + @Mock + private ProductRepository productRepository; + @Spy + ProductMapper mapper= Mappers.getMapper(ProductMapper.class); + @Mock + VendorFeignClient vendorClient; + @InjectMocks + ProductService productService; + + @Captor + private ArgumentCaptor productCaptor; + + @AfterEach + void tearDown() { + UserContext.clear(); + } + + @Test + void create() { + //given + setHttpHeaders(UUID.randomUUID().toString(), "Company_Manager"); + Product product=ProductFixture.create(); + ProductCreateRequest request=new ProductCreateRequest(product.getName(), product.getPrice(), + product.getStock(), product.getStatus()); + VendorInfoResponse vendorInfo=new VendorInfoResponse(product.getCompanyId(), product.getCompanyName(), product.getHubId()); + given(vendorClient.getVendorInfo(product.getCompanyId())).willReturn(vendorInfo); + + //when + productService.create(product.getCompanyId(), request); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct=productCaptor.getValue(); + assertThat(savedProduct.getName()).isEqualTo(product.getName()); + assertThat(savedProduct.getPrice()).isEqualTo(product.getPrice()); + assertThat(savedProduct.getStockInfo().getStock()).isEqualTo(product.getStock()); + assertThat(savedProduct.getStatus()).isEqualTo(product.getStatus()); + assertThat(savedProduct.getVendorInfo().getCompanyId()).isEqualTo(product.getCompanyId()); + assertThat(savedProduct.getVendorInfo().getCompanyName()).isEqualTo(product.getCompanyName()); + assertThat(savedProduct.getVendorInfo().getHubId()).isEqualTo(product.getHubId()); + } + + @Test + void delete() { + //given + UUID productId=UUID.randomUUID(); + Product product=ProductFixture.create(); + given(productRepository.findById(productId)).willReturn(Optional.of(product)); + + //when + productService.delete(productId); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct=productCaptor.getValue(); + assertThat(savedProduct.getDeletedAt()).isNotNull(); + } + + @Test + void updateInfo() { + //given + setHttpHeaders(UUID.randomUUID().toString(), "Company_Manager"); + Product product=ProductFixture.create(); + ProductUpdateInfoRequest request=new ProductUpdateInfoRequest(product.getName(), product.getPrice()); + given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); + + //when + productService.updateInfo(product.getId(), request); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct=productCaptor.getValue(); + assertThat(savedProduct.getName()).isEqualTo(request.name()); + assertThat(savedProduct.getPrice()).isEqualTo(request.price()); + } + + @Test + void updateStock_성공() { + //given + UUID productId=UUID.randomUUID(); + Product product=ProductFixture.create(); + ProductUpdateStockRequest request=new ProductUpdateStockRequest(100); + given(productRepository.findById(productId)).willReturn(Optional.of(product)); + + //when + productService.updateStock(productId, request); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct=productCaptor.getValue(); + assertThat(savedProduct.getStockInfo().getStock()).isEqualTo(100); + } + + @Test + void updateStock_실패_잘못된_재고값_입력() { + //given + UUID productId=UUID.randomUUID(); + Product product=ProductFixture.create(); + ProductUpdateStockRequest request=new ProductUpdateStockRequest(-1); + given(productRepository.findById(productId)).willReturn(Optional.of(product)); + + //when&then + assertThatThrownBy(() -> productService.updateStock(productId, request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("잘못된 재고값입니다."); + + } + + @Test + void updateStock_재고를_0으로_설정(){ + //given + UUID productId=UUID.randomUUID(); + Product product=ProductFixture.create(); + ProductUpdateStockRequest request=new ProductUpdateStockRequest(0); + given(productRepository.findById(productId)).willReturn(Optional.of(product)); + + //when + productService.updateStock(productId, request); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct=productCaptor.getValue(); + assertThat(savedProduct.getStatus()).isEqualTo(ProductStatus.OUT_OF_STOCK); + } + + @Test + void getProductInfo_success() { + //given + Product product=ProductFixture.create(); + given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); + + //when + ProductInfoResponse response= productService.getProductInfo(product.getId()); + + //then + assertThat(response.id()).isEqualTo(product.getId()); + assertThat(response.name()).isEqualTo(product.getName()); + assertThat(response.price()).isEqualTo(product.getPrice()); + assertThat(response.status()).isEqualTo(product.getStatus()); + } + + @Test + void getProductList() { + //given + UUID companyId=UUID.randomUUID(); + Product product=ProductFixture.create(); + List products=List.of(product); + Pageable pageable=Pageable.ofSize(10); + Sliceslice=new SliceImpl<>(products, pageable, false); + given(productRepository.findAllByCompanyId(companyId,pageable)).willReturn(slice); + + //when + Slice response=productService.getProductList(companyId, pageable); + + //then + assertThat(response.getContent().size()).isEqualTo(products.size()); + assertThat(response.hasNext()).isFalse(); + + } + + + private void setHttpHeaders(String userId, String role) { + MockHttpServletRequest httpRequest = new MockHttpServletRequest(); + httpRequest.addHeader("X-User-Id", userId); + httpRequest.addHeader("X-User-Role", role); + UserContext.setUserContext(httpRequest); + } +} \ No newline at end of file diff --git a/product-service/src/test/java/com/shipflow/productservice/fixture/ProductFixture.java b/product-service/src/test/java/com/shipflow/productservice/fixture/ProductFixture.java new file mode 100644 index 0000000..991862b --- /dev/null +++ b/product-service/src/test/java/com/shipflow/productservice/fixture/ProductFixture.java @@ -0,0 +1,29 @@ +package com.shipflow.productservice.fixture; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.domain.model.ProductStatus; +import com.shipflow.productservice.infrastructure.persistence.ProductJpaEntity; + +public class ProductFixture { + public static Product create() { + ProductJpaEntity entity = new ProductJpaEntity(); + ReflectionTestUtils.setField(entity, "id", UUID.randomUUID()); + ReflectionTestUtils.setField(entity, "name", "testName"); + ReflectionTestUtils.setField(entity, "price", BigDecimal.valueOf(1000000)); + ReflectionTestUtils.setField(entity, "stock", 100); + ReflectionTestUtils.setField(entity, "status", ProductStatus.OUT_OF_STOCK); + ReflectionTestUtils.setField(entity, "companyId", UUID.randomUUID()); + ReflectionTestUtils.setField(entity, "companyName", "testCompanyName"); + ReflectionTestUtils.setField(entity, "hubId", UUID.randomUUID()); + ReflectionTestUtils.setField(entity, "isHide", false); + ReflectionTestUtils.setField(entity, "createdAt", LocalDateTime.now()); + ReflectionTestUtils.setField(entity, "createdBy", UUID.randomUUID()); + return entity.toDomain(); + } +} From 6cdfef6ff21c48effc1f080021d70dd8bef4a3ae Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 20:57:50 +0900 Subject: [PATCH 09/38] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/persistence/ProductJpaEntity.java | 1 - .../application/service/ProductServiceTest.java | 4 ---- 2 files changed, 5 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java index 5f58901..3bf4421 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaEntity.java @@ -14,7 +14,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index ebfee57..f569653 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -1,7 +1,6 @@ package com.shipflow.productservice.application.service; import static org.assertj.core.api.AssertionsForClassTypes.*; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.util.List; @@ -18,7 +17,6 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -28,11 +26,9 @@ import com.shipflow.productservice.application.client.VendorFeignClient; import com.shipflow.productservice.application.dto.response.VendorInfoResponse; import com.shipflow.productservice.application.mapper.ProductMapper; -import com.shipflow.productservice.domain.exception.ProductErrorCode; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.model.ProductStatus; import com.shipflow.productservice.domain.repository.ProductRepository; -import com.shipflow.productservice.domain.vo.VendorInfo; import com.shipflow.productservice.fixture.ProductFixture; import com.shipflow.productservice.infrastructure.web.UserContext; import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; From 8ad61483d2aacd33068e660c57a016f633585081 Mon Sep 17 00:00:00 2001 From: JIN <126974009+Jin4041@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:43:05 +0900 Subject: [PATCH 10/38] Update product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../productservice/application/mapper/ProductMapper.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index 113e93b..afaafd7 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -13,7 +13,14 @@ public interface ProductMapper { //Entity->DTO ProductCreateResponse toCreateResponse(Product product); +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +`@Mapper`(componentModel = "spring") +public interface ProductMapper { + `@Mapping`(target = "updateAt", source = "updatedAt") ProductUpdateResponse toUpdateResponse(Product product); +} ProductInfoResponse toProductInfoResponse(Product product); From 06adbce2274f656142d663ebc25adc2e0effee5a Mon Sep 17 00:00:00 2001 From: JIN <126974009+Jin4041@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:08:55 +0900 Subject: [PATCH 11/38] Update product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../productservice/application/mapper/ProductMapper.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index afaafd7..150de4d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -1,6 +1,7 @@ package com.shipflow.productservice.application.mapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; @@ -8,19 +9,13 @@ import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; -@Mapper(componentModel = "spring") +`@Mapper`(componentModel = "spring") public interface ProductMapper { //Entity->DTO ProductCreateResponse toCreateResponse(Product product); -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -`@Mapper`(componentModel = "spring") -public interface ProductMapper { `@Mapping`(target = "updateAt", source = "updatedAt") ProductUpdateResponse toUpdateResponse(Product product); -} ProductInfoResponse toProductInfoResponse(Product product); From 14e4fec1fd6626592e4eab15425b09d39b3d0316 Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 22:20:35 +0900 Subject: [PATCH 12/38] =?UTF-8?q?refactor:=20build.gradle=20=EB=82=B4=20de?= =?UTF-8?q?pendencyManagement=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- product-service/build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/product-service/build.gradle b/product-service/build.gradle index c68bd61..8a39170 100644 --- a/product-service/build.gradle +++ b/product-service/build.gradle @@ -65,12 +65,6 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } -} - tasks.named('test') { useJUnitPlatform() } From 5c6643ede0a31057a1f40243bc5f5f4ea7018cc6 Mon Sep 17 00:00:00 2001 From: JIN <126974009+Jin4041@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:29:19 +0900 Subject: [PATCH 13/38] Update product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../com/shipflow/productservice/domain/model/BaseEntity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java index b7bffa4..8c053db 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java @@ -14,17 +14,22 @@ public class BaseEntity { LocalDateTime deletedAt; UUID deletedBy; +import java.util.Objects; + public void create(UUID id) { + Objects.requireNonNull(id, "createdBy id는 필수입니다."); this.createdAt = LocalDateTime.now(); this.createdBy = id; } public void update(UUID id) { + Objects.requireNonNull(id, "updatedBy id는 필수입니다."); this.updatedAt = LocalDateTime.now(); this.updatedBy = id; } public void delete(UUID id) { + Objects.requireNonNull(id, "deletedBy id는 필수입니다."); this.deletedAt = LocalDateTime.now(); this.deletedBy = id; } From 4215ee364cf052d5c3e24d7a044014fd471fe115 Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 22:33:16 +0900 Subject: [PATCH 14/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../productservice/domain/model/BaseEntity.java | 17 +++++++++++------ .../dto/response/ProductUpdateResponse.java | 2 +- .../src/main/resources/application.yaml | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java index b7bffa4..71a7e4f 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java @@ -3,18 +3,23 @@ import java.time.LocalDateTime; import java.util.UUID; +import com.shipflow.common.exception.BusinessException; +import com.shipflow.common.exception.CommonErrorCode; + import lombok.Getter; @Getter public class BaseEntity { - LocalDateTime createdAt; - UUID createdBy; - LocalDateTime updatedAt; - UUID updatedBy; - LocalDateTime deletedAt; - UUID deletedBy; + protected LocalDateTime createdAt; + protected UUID createdBy; + protected LocalDateTime updatedAt; + protected UUID updatedBy; + protected LocalDateTime deletedAt; + protected UUID deletedBy; public void create(UUID id) { + if (id == null) + throw new BusinessException(CommonErrorCode.VALIDATION_ERROR); this.createdAt = LocalDateTime.now(); this.createdBy = id; } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java index c271a06..ae63fee 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductUpdateResponse.java @@ -9,6 +9,6 @@ public record ProductUpdateResponse( String name, String description, BigDecimal price, Integer stock, ProductStatus productStatus, UUID companyId, String companyName, - UUID hubId, Boolean isHide, LocalDateTime updateAt + UUID hubId, Boolean isHide, LocalDateTime updatedAt ) { } diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index 10fadde..a53fa9e 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -1,7 +1,7 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema={schema} + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=p_product username: ${DB_USER} password: ${DB_PASSWORD} @@ -10,5 +10,5 @@ spring: ddl-auto: update properties: hibernate: - default_schema: products //본인 스키마명으로 변경 + default_schema: p_products show-sql: true \ No newline at end of file From 5f1bdcbb4970e4966e50f561fdce79c304327b62 Mon Sep 17 00:00:00 2001 From: JIN <126974009+Jin4041@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:38:45 +0900 Subject: [PATCH 15/38] Update product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../infrastructure/persistence/ProductRepositoryImpl.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index c103a14..a019e9b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -17,12 +17,10 @@ public class ProductRepositoryImpl implements ProductRepository { private final ProductJpaRepository jpaRepository; - @Override + `@Override` public Optional findById(UUID id) { - ProductJpaEntity entity = jpaRepository.findById(id) - .orElseThrow(() -> new RuntimeException("해당 제품을 찾을 수 없습니다.")); - Product product = entity.toDomain(); - return Optional.of(product); + return jpaRepository.findById(id) + .map(ProductJpaEntity::toDomain); } @Override From dae6ba6569db2b4542da52495c5d67ab27cccb58 Mon Sep 17 00:00:00 2001 From: jin Date: Thu, 2 Apr 2026 22:40:50 +0900 Subject: [PATCH 16/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/shipflow/productservice/domain/model/Product.java | 2 +- .../presentation/dto/response/ProductCreateResponse.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 0144064..89daf81 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -57,7 +57,7 @@ public void updateInfo(String name, BigDecimal price, UUID updatedBy) { if (name != null && !name.isBlank()) this.name = name; - if (price.compareTo(BigDecimal.ZERO) <= 0) + if (price!=null|| price.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("price는 0보다 커야 합니다."); else this.price = price; diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java index 7049662..0df1dc4 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/response/ProductCreateResponse.java @@ -8,7 +8,7 @@ public record ProductCreateResponse( String name, String description, BigDecimal price, Integer stock, - ProductStatus productStatus, UUID companyId, String companyName, + ProductStatus status, UUID companyId, String companyName, UUID hubId, Boolean isHide, LocalDateTime createdAt ) { } From 8e15cb7fdb9e05bb5c0fbd43bf0bd73d643a3852 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 11:18:43 +0900 Subject: [PATCH 17/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20-?= =?UTF-8?q?=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20audit=20=EA=B4=80=EB=A0=A8=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shipflow/common/domain/BaseEntity.java | 4 ++- .../application/service/ProductService.java | 8 ++--- .../domain/model/BaseEntity.java | 13 ------- .../productservice/domain/model/Product.java | 28 +++++++-------- .../infrastructure/config/JpaAuditConfig.java | 29 +++++++++++++++ .../infrastructure/web/UserContext.java | 14 ++++---- .../web/UserContextInterceptor.java | 35 +++++++++++++++++++ .../infrastructure/web/WebConfig.java | 19 ++++++++++ .../ProductExternalController.java | 4 +-- .../dto/request/ProductUpdateInfoRequest.java | 4 ++- .../request/ProductUpdateStockRequest.java | 5 +++ .../service/ProductServiceTest.java | 2 +- 12 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index 9a2d3ac..d261f8f 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -26,13 +26,15 @@ public abstract class BaseEntity { @CreatedDate protected LocalDateTime createdAt; - @Column(nullable = false) + @Column(nullable = false, updatable = false) @CreatedBy protected UUID createdBy; + @Column(nullable = false) @LastModifiedDate protected LocalDateTime updatedAt; + @Column(nullable = false) @LastModifiedBy protected UUID updatedBy; diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 67f332d..dc9a816 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -54,11 +54,10 @@ public void delete(UUID productId) { } @Transactional - public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest request) { - UUID updaterId = UserContext.getUserId(); + public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfoRequest request) { Product product = findProductById(productId); product.updateInfo( - request.name(), request.price(), updaterId + request.name(), request.price(), request.status() ); productRepository.save(product); return mapper.toUpdateResponse(product); @@ -66,9 +65,8 @@ public ProductUpdateResponse updateInfo(UUID productId, ProductUpdateInfoRequest @Transactional public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request) { - UUID updaterId = UserContext.getUserId(); Product product = findProductById(productId); - product.updateStock(request.stock(), updaterId); + product.updateStock(request.stock()); productRepository.save(product); return mapper.toUpdateResponse(product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java index 5560b5a..1d00efc 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java @@ -15,19 +15,6 @@ public class BaseEntity { protected UUID deletedBy; - - public void create(UUID id) { - Objects.requireNonNull(id, "createdBy id는 필수입니다."); - this.createdAt = LocalDateTime.now(); - this.createdBy = id; - } - - public void update(UUID id) { - Objects.requireNonNull(id, "updatedBy id는 필수입니다."); - this.updatedAt = LocalDateTime.now(); - this.updatedBy = id; - } - public void delete(UUID id) { Objects.requireNonNull(id, "deletedBy id는 필수입니다."); this.deletedAt = LocalDateTime.now(); diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 89daf81..0491f70 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -53,42 +53,42 @@ public static Product reconstruct(UUID id,String name, BigDecimal price, return product; } - public void updateInfo(String name, BigDecimal price, UUID updatedBy) { + public void updateInfo(String name, BigDecimal price, ProductStatus status) { if (name != null && !name.isBlank()) this.name = name; + this.price = validatePrice(price); + updateStatus(status); + } - if (price!=null|| price.compareTo(BigDecimal.ZERO) <= 0) - throw new IllegalArgumentException("price는 0보다 커야 합니다."); - else - this.price = price; - this.update(updatedBy); + private BigDecimal validatePrice(BigDecimal price) { + if (Objects.requireNonNull(price, "가격은 필수입니다.").compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("가격은 0보다 커야 합니다."); + } + return price; } - public void updateVendorInfo(UUID companyId, String companyName, UUID hubId, UUID updatedBy) { + public void updateVendorInfo(UUID companyId, String companyName, UUID hubId) { this.vendorInfo = new VendorInfo(companyId, companyName, hubId); - this.update(updatedBy); } - public void updateStatus(ProductStatus status, UUID updatedBy) { + public void updateStatus(ProductStatus status) { this.status = status; if (status.equals(ProductStatus.STOPPED) || status.equals(ProductStatus.DISCONTINUED) || status.equals(ProductStatus.OUT_OF_STOCK)) this.isHide = true; - this.update(updatedBy); } - public void updateStock(Integer stock, UUID updatedBy) { + public void updateStock(Integer stock) { this.stockInfo.setStock(stock); if (stock == 0) - updateStatus(ProductStatus.OUT_OF_STOCK, updatedBy); + updateStatus(ProductStatus.OUT_OF_STOCK); this.stockInfo.setStock(stock); - this.update(updatedBy); } public void decreaseStock(Integer quantity) { this.stockInfo.decrease(quantity); if (this.stockInfo.getStock() == 0) - this.isHide = true; + updateStatus(ProductStatus.OUT_OF_STOCK); } public void delete(UUID deletedBy) { diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java new file mode 100644 index 0000000..a03f103 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java @@ -0,0 +1,29 @@ +package com.shipflow.productservice.infrastructure.config; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; + +import com.shipflow.productservice.infrastructure.web.UserContext; + +@Configuration +public class JpaAuditConfig { + @Bean + public AuditorAware auditorAware() { + return () -> { + UUID userId=UserContext.getUserId(); + + if(userId==null) + return Optional.empty(); + + try { + return Optional.of(userId); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + }; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java index 240bc1e..6486427 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java @@ -11,14 +11,12 @@ public class UserContext { private static final ThreadLocal USER_ID_HOLDER = new ThreadLocal<>(); private static final ThreadLocal USER_ROLE_HOLDER = new ThreadLocal<>(); - public static void setUserContext(HttpServletRequest request) { - String userId = request.getHeader("X-User-Id"); - String userRole = request.getHeader("X-User-Role"); - - if (userId != null) - USER_ID_HOLDER.set(UUID.fromString(userId)); - if (userRole != null) - USER_ROLE_HOLDER.set(userRole); + public static void setUserId(UUID userId) { + USER_ID_HOLDER.set(userId); + } + + public static void setUserRole(String userRole) { + USER_ROLE_HOLDER.set(userRole); } public static UUID getUserId() { diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java new file mode 100644 index 0000000..14a7600 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java @@ -0,0 +1,35 @@ +package com.shipflow.productservice.infrastructure.web; + +import java.util.UUID; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class UserContextInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String userId = request.getHeader("X-User-Id"); + String userRole = request.getHeader("X-User-Role"); + + if (userId != null && !userId.isBlank()) { + try { + UserContext.setUserId(UUID.fromString(userId)); + UserContext.setUserRole(userRole); + } catch (IllegalArgumentException e) { + log.error("Invalid UUID format in X-User-Id header: {}", userId); + } + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + UserContext.clear(); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java new file mode 100644 index 0000000..21efe77 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java @@ -0,0 +1,19 @@ +package com.shipflow.productservice.infrastructure.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final UserContextInterceptor userContextInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(userContextInterceptor); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java index 985009d..9fd0da6 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -60,14 +60,14 @@ public ResponseEntity> updateProductInfo(@Pat @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest, HttpServletRequest request) { UserContext.setUserContext(request); - ProductUpdateResponse response = productService.updateInfo(productId, productUpdateInfoRequest); + ProductUpdateResponse response = productService.updateProductInfo(productId, productUpdateInfoRequest); UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @PostMapping("/{productId}/stock") public ResponseEntity> updateStock(@PathVariable UUID productId, - @RequestBody ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { + @Valid @RequestBody ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { UserContext.setUserContext(request); ProductUpdateResponse response = productService.updateStock(productId, productUpdateStockRequest); diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java index e07d8da..cfa83aa 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateInfoRequest.java @@ -2,7 +2,9 @@ import java.math.BigDecimal; +import com.shipflow.productservice.domain.model.ProductStatus; + public record ProductUpdateInfoRequest( - String name, BigDecimal price + String name, BigDecimal price, ProductStatus status ) { } \ No newline at end of file diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java index 09adde7..594dd91 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/dto/request/ProductUpdateStockRequest.java @@ -1,6 +1,11 @@ package com.shipflow.productservice.presentation.dto.request; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + public record ProductUpdateStockRequest( + @NotNull + @PositiveOrZero Integer stock ) { } diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index f569653..2b2504b 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -107,7 +107,7 @@ void updateInfo() { given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when - productService.updateInfo(product.getId(), request); + productService.updateProductInfo(product.getId(), request); //then verify(productRepository).save(productCaptor.capture()); From ab3e0750414643948695a3583c977c69b3ddc347 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 11:50:37 +0900 Subject: [PATCH 18/38] =?UTF-8?q?chore:=20eureka,=20feign=20client=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/client/VendorFeignClient.java | 2 +- .../application/mapper/ProductMapper.java | 3 +- .../productservice/domain/model/Product.java | 1 - .../persistence/ProductRepositoryImpl.java | 2 +- .../ProductExternalController.java | 16 ++------ .../src/main/resources/application.yaml | 37 ++++++++++++++++--- .../service/ProductServiceTest.java | 8 ++-- 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java b/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java index 11d32f0..2e83184 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/client/VendorFeignClient.java @@ -8,7 +8,7 @@ import com.shipflow.productservice.application.dto.response.VendorInfoResponse; -@FeignClient(name = "company-service"/*,url="${}"*/)//todo: yaml 파일 설정 추가 후 url설정 +@FeignClient(name = "company-service") public interface VendorFeignClient { @GetMapping("/internal/companies/{companyId}") VendorInfoResponse getVendorInfo(@PathVariable("companyId") UUID companyId); diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index 150de4d..9c2013b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -9,12 +9,11 @@ import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; -`@Mapper`(componentModel = "spring") +@Mapper(componentModel = "spring") public interface ProductMapper { //Entity->DTO ProductCreateResponse toCreateResponse(Product product); - `@Mapping`(target = "updateAt", source = "updatedAt") ProductUpdateResponse toUpdateResponse(Product product); ProductInfoResponse toProductInfoResponse(Product product); diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 0491f70..d9922ff 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -33,7 +33,6 @@ private Product(String name, BigDecimal price, Integer stock, ProductStatus stat public static Product create(String name, BigDecimal price, Integer stock, ProductStatus status, UUID companyId, String companyName, UUID hubId, UUID createdBy) { Product product = new Product(name, price, stock, status, companyId, companyName, hubId); - product.create(createdBy); return product; } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index a019e9b..5896e57 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -17,7 +17,7 @@ public class ProductRepositoryImpl implements ProductRepository { private final ProductJpaRepository jpaRepository; - `@Override` + @Override public Optional findById(UUID id) { return jpaRepository.findById(id) .map(ProductJpaEntity::toDomain); diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java index 9fd0da6..7e8dc64 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java @@ -40,38 +40,28 @@ public class ProductExternalController { @PostMapping public ResponseEntity> addProduct(@PathVariable UUID companyId, @Valid @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { - UserContext.setUserContext(request); ProductCreateResponse response = productService.create(companyId, productCreateRequest); - UserContext.clear(); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(response)); } @DeleteMapping("/{productId}") - public ResponseEntity deleteProduct(@PathVariable UUID productId, - HttpServletRequest request) { - UserContext.setUserContext(request); + public ResponseEntity deleteProduct(@PathVariable UUID productId) { productService.delete(productId); - UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body("요청이 정상 처리되었습니다."); } @PatchMapping("/{productId}") public ResponseEntity> updateProductInfo(@PathVariable UUID productId, - @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest, - HttpServletRequest request) { - UserContext.setUserContext(request); + @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest) { ProductUpdateResponse response = productService.updateProductInfo(productId, productUpdateInfoRequest); - UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @PostMapping("/{productId}/stock") public ResponseEntity> updateStock(@PathVariable UUID productId, - @Valid @RequestBody ProductUpdateStockRequest productUpdateStockRequest, HttpServletRequest request) { - UserContext.setUserContext(request); + @Valid @RequestBody ProductUpdateStockRequest productUpdateStockRequest) { ProductUpdateResponse response = productService.updateStock(productId, productUpdateStockRequest); - UserContext.clear(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index a53fa9e..a0d3047 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -1,14 +1,41 @@ spring: + application: + name: product-service + datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=p_product - username: ${DB_USER} - password: ${DB_PASSWORD} + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=product + username: ${DB_USER:shipflow} + password: ${DB_PASSWORD:1234} + + cloud: + openfeign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: full jpa: hibernate: ddl-auto: update properties: hibernate: - default_schema: p_products - show-sql: true \ No newline at end of file + default_schema: product + show-sql: true + +server: + port: 8090 + +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/} + +logging: + level: + com.shipflow.product.infrastructure.client: DEBUG + org.hibernate.orm.jdbc.bind: TRACE \ No newline at end of file diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index 2b2504b..1fb1e6b 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -103,7 +103,7 @@ void updateInfo() { //given setHttpHeaders(UUID.randomUUID().toString(), "Company_Manager"); Product product=ProductFixture.create(); - ProductUpdateInfoRequest request=new ProductUpdateInfoRequest(product.getName(), product.getPrice()); + ProductUpdateInfoRequest request=new ProductUpdateInfoRequest(product.getName(), product.getPrice(),null); given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when @@ -202,9 +202,7 @@ void getProductList() { private void setHttpHeaders(String userId, String role) { - MockHttpServletRequest httpRequest = new MockHttpServletRequest(); - httpRequest.addHeader("X-User-Id", userId); - httpRequest.addHeader("X-User-Role", role); - UserContext.setUserContext(httpRequest); + UserContext.setUserId(UUID.fromString(userId)); + UserContext.setUserRole(role); } } \ No newline at end of file From ba11b2c3f30e447a82a8c3a01fa3865533b54ef1 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 14:31:14 +0900 Subject: [PATCH 19/38] =?UTF-8?q?feat:=20order=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EA=B4=80=EB=A0=A8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: redis 설정 추가 --- .../dto/response/StockInfoResponse.java | 12 ++++ .../application/mapper/ProductMapper.java | 3 + .../application/service/ProductService.java | 56 ++++++++++++++++++- .../domain/exception/ProductErrorCode.java | 1 + .../domain/repository/ProductRepository.java | 5 ++ .../config/RedisInventoryLoader.java | 28 ++++++++++ .../{web => config}/WebConfig.java | 4 +- .../persistence/ProductJpaRepository.java | 4 ++ .../persistence/ProductRepositoryImpl.java | 12 ++++ .../ProductExternalController.java | 3 +- .../controller/ProductInternalController.java | 29 ++++++++++ .../src/main/resources/application.yaml | 13 +++++ 12 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java rename product-service/src/main/java/com/shipflow/productservice/infrastructure/{web => config}/WebConfig.java (79%) rename product-service/src/main/java/com/shipflow/productservice/presentation/{ => controller}/ProductExternalController.java (97%) create mode 100644 product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java new file mode 100644 index 0000000..450e9ac --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java @@ -0,0 +1,12 @@ +package com.shipflow.productservice.application.dto.response; + +import java.util.UUID; + +import jakarta.validation.constraints.Positive; +import lombok.NonNull; + +public record StockInfoResponse( + @NonNull UUID productId, + @NonNull @Positive Integer stock +){ +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index 9c2013b..cf5f2b0 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -3,6 +3,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import com.shipflow.productservice.application.dto.response.StockInfoResponse; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.presentation.dto.response.ProductCreateResponse; import com.shipflow.productservice.presentation.dto.response.ProductInfoResponse; @@ -19,4 +20,6 @@ public interface ProductMapper { ProductInfoResponse toProductInfoResponse(Product product); ProductListResponse toProductListResponse(Product product); + + StockInfoResponse toStockInfoResponse(Product product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index dc9a816..70d7974 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -1,14 +1,21 @@ package com.shipflow.productservice.application.service; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.shipflow.common.exception.BusinessException; +import com.shipflow.common.exception.CommonErrorCode; import com.shipflow.productservice.application.client.VendorFeignClient; +import com.shipflow.productservice.application.dto.response.StockInfoResponse; import com.shipflow.productservice.application.dto.response.VendorInfoResponse; import com.shipflow.productservice.application.mapper.ProductMapper; import com.shipflow.productservice.domain.exception.ProductErrorCode; @@ -32,7 +39,9 @@ public class ProductService { private final ProductRepository productRepository; private final ProductMapper mapper; private final VendorFeignClient vendorClient; + private final RedisTemplate redisTemplate; + //external @Transactional public ProductCreateResponse create(UUID companyId, ProductCreateRequest request) { UUID createrId = UserContext.getUserId(); @@ -81,9 +90,54 @@ public Slice getProductList(UUID companyId, Pageable pageab return products.map(mapper::toProductListResponse); } + + //internal + + /* + * 주문 시 product 측 흐름 : + * 1. 주문 전 재고 조회 요청 시) querystring으로 요청 재고 값 전달받음 + * 2. 재고 조회 시) redis에서 조회 후 없으면 db에서 조회 -> redis에 저장 -> 재고 조회 시 redis에서 조회 -> 없으면 db에서 조회 -> redis에 저장 / 없으면 예외 발생 + * 3. 조회 성공 시) 재고 감소 -> redis에 선점 정보 저장(ttl 5초) / 재고가 주문량보다 적을 경우 차감없이 재고 반환 + * 4. 선점 후 주문 이벤트 발생 시) redis 선점 정보 삭제 -> db에서 재고 차감 + * */ + + // 재고 조회 + public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productId, Integer quantity) { + String stockKey ="product:stock:"+productId; + String occupancyKey ="product:"+ UserContext.getUserId() +":"+productId; + + ensureStockInRedis(productId, stockKey); + + Long currentStock = Optional.ofNullable(redisTemplate.opsForValue().decrement(stockKey, quantity)) + .orElseThrow(() -> new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR)); + + if(currentStock<0){ + Long restoreStock=redisTemplate.opsForValue().increment(stockKey, (long)quantity); + return new StockInfoResponse(productId, restoreStock!=null?restoreStock.intValue():currentStock.intValue()); + } + + redisTemplate.opsForValue().set(occupancyKey, quantity, Duration.ofSeconds(5)); + + return new StockInfoResponse(productId, currentStock.intValue()); + } + + //util private Product findProductById(UUID productId) { return productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(ProductErrorCode.PRODUCT_NOT_FOUND, "해당 제품을 찾을 수 없습니다.")); + .orElseThrow(() -> new BusinessException(ProductErrorCode.PRODUCT_NOT_FOUND)); + } + + private void ensureStockInRedis(UUID productId, String stockKey) { + if (!redisTemplate.hasKey(stockKey)) { + + Integer dbStock = productRepository.findStockById(productId); + + if (dbStock == null) { + throw new BusinessException(ProductErrorCode.PRODUCT_NOT_FOUND); + } + + redisTemplate.opsForValue().setIfAbsent(stockKey, dbStock); + } } } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java index ccbc50f..1209a33 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java @@ -9,6 +9,7 @@ public enum ProductErrorCode implements ErrorCode { PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "해당 상품을 찾을 수 없습니다."), INVALID_STOCK_VALUE("INVALID_STOCK_VALUE", HttpStatus.BAD_REQUEST, "잘못된 재고값입니다."), INVALID_ORDER_QUANTITY("INVALID_ORDER_QUANTITY", HttpStatus.BAD_REQUEST, "잘못된 주문량입니다."), + INACTIVE_PRODUCT("INACTIVE_PRODUCT", HttpStatus.BAD_REQUEST, "현재 비활성화된 상품입니다."), OUT_OF_STOCK("OUT_OF_STOCK",HttpStatus.BAD_REQUEST,"요청하신 주문량이 잔여 재고량보다 많습니다."); private final String code; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java index 479ab60..eb626f8 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -1,5 +1,6 @@ package com.shipflow.productservice.domain.repository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -14,4 +15,8 @@ public interface ProductRepository { Product save(Product product); Slice findAllByCompanyId(UUID companyId, Pageable pageable); + + List findAll(); + + Integer findStockById(UUID productId); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java new file mode 100644 index 0000000..3956f50 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java @@ -0,0 +1,28 @@ +package com.shipflow.productservice.infrastructure.config; + +import java.util.List; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import com.shipflow.productservice.domain.model.Product; +import com.shipflow.productservice.domain.repository.ProductRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class RedisInventoryLoader { + + private final ProductRepository productRepository; + private final RedisTemplate redisTemplate; + + public void loadInventoryToRedis() { + List products = productRepository.findAll(); + + products.forEach(product -> { + String key="product:stock:"+product.getId(); + redisTemplate.opsForValue().set(key, product.getStock()); + }); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/WebConfig.java similarity index 79% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/config/WebConfig.java index 21efe77..523132d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/WebConfig.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/WebConfig.java @@ -1,9 +1,11 @@ -package com.shipflow.productservice.infrastructure.web; +package com.shipflow.productservice.infrastructure.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.shipflow.productservice.infrastructure.web.UserContextInterceptor; + import lombok.RequiredArgsConstructor; @Configuration diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java index c3b0456..620fa19 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java @@ -5,7 +5,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface ProductJpaRepository extends JpaRepository { Slice findAllByCompanyId(UUID companyId, Pageable pageable); + + @Query("select stock from ProductJpaEntity where id = :productId and isHide = false") + Integer findStockById(UUID productId); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index 5896e57..324de22 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -1,5 +1,6 @@ package com.shipflow.productservice.infrastructure.persistence; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -35,4 +36,15 @@ public Slice findAllByCompanyId(UUID companyId, Pageable pageable) { Slice entities = jpaRepository.findAllByCompanyId(companyId, pageable); return entities.map(ProductJpaEntity::toDomain); } + + @Override + public List findAll() { + List entities = jpaRepository.findAll(); + return entities.stream().map(ProductJpaEntity::toDomain).toList(); + } + + @Override + public Integer findStockById(UUID productId) { + return jpaRepository.findStockById(productId); + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java similarity index 97% rename from product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java rename to product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java index 7e8dc64..0f367ce 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.presentation; +package com.shipflow.productservice.presentation.controller; import java.util.UUID; @@ -18,7 +18,6 @@ import com.shipflow.common.exception.ApiResponse; import com.shipflow.productservice.application.service.ProductService; -import com.shipflow.productservice.infrastructure.web.UserContext; import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateStockRequest; diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java new file mode 100644 index 0000000..55a996b --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -0,0 +1,29 @@ +package com.shipflow.productservice.presentation.controller; + +import java.util.UUID; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; + +import com.shipflow.common.exception.ApiResponse; +import com.shipflow.productservice.application.dto.response.StockInfoResponse; +import com.shipflow.productservice.application.service.ProductService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/internal/products") +@RequiredArgsConstructor +public class ProductInternalController { + private final ProductService productService; + + @GetMapping("/{productId}") + public ApiResponse getStockInfo(@PathVariable UUID productId, + @RequestPart Integer quantity) { + StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); + return ApiResponse.ok(response); + } +} diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index a0d3047..155cb5c 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -17,6 +17,19 @@ spring: readTimeout: 5000 loggerLevel: full + data: + redis: + host: localhost + port: 6379 + database: 0 + timeout: 5000 + lettuce: + pool: + max-active: 10 + max-wait: -1 + max-idle: 8 + min-idle: 0 + jpa: hibernate: ddl-auto: update From 714c6f0c678a250da06d81d26fad2bb673a78bc3 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 15:50:00 +0900 Subject: [PATCH 20/38] =?UTF-8?q?feat:=20order=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=95=20=EA=B4=80=EB=A0=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/ProductMapper.java | 1 - .../application/service/ProductService.java | 18 +++++++- .../domain/model/BaseEntity.java | 3 +- .../productservice/domain/model/Product.java | 5 +++ .../productservice/domain/vo/StockInfo.java | 6 +++ .../messaging/OrderCanceledEvent.java | 23 ++++++++++ .../messaging/OrderCreationStartedEvent.java | 24 +++++++++++ .../messaging/ProductStockDecreasedEvent.java | 25 +++++++++++ .../ProductStockDecreasedFailedEvent.java | 24 +++++++++++ .../messaging/StockRestoredEvent.java | 24 +++++++++++ .../config/OrderCreationStatedHandler.java | 39 +++++++++++++++++ .../messaging/config/ProductRabbitConfig.java | 42 ++++++++++++++++++- .../messaging/config/ProductSagaListener.java | 28 +++++++++++++ .../messaging/config/StockRestoreHandler.java | 24 +++++++++++ .../infrastructure/web/UserContext.java | 2 - .../service/ProductServiceTest.java | 1 - 16 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index cf5f2b0..152cc3e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -1,7 +1,6 @@ package com.shipflow.productservice.application.mapper; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import com.shipflow.productservice.application.dto.response.StockInfoResponse; import com.shipflow.productservice.domain.model.Product; diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 70d7974..1bc5898 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -1,7 +1,6 @@ package com.shipflow.productservice.application.service; import java.time.Duration; -import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -121,6 +120,23 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI return new StockInfoResponse(productId, currentStock.intValue()); } + public void decreaseStock(String productId, Integer quantity) { + Product product = findProductById(UUID.fromString(productId)); + + if (product.getStock() < quantity) { + throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); + } + + product.decreaseStock(quantity); + productRepository.save(product); + } + + public void restoreStock(String productId, Integer quantity) { + Product product = findProductById(UUID.fromString(productId)); + product.restoreStock(quantity); + productRepository.save(product); + } + //util private Product findProductById(UUID productId) { diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java index 1d00efc..907b5dc 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/BaseEntity.java @@ -1,8 +1,9 @@ package com.shipflow.productservice.domain.model; import java.time.LocalDateTime; -import java.util.UUID; import java.util.Objects; +import java.util.UUID; + import lombok.Getter; @Getter diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index d9922ff..784f106 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -90,6 +90,10 @@ public void decreaseStock(Integer quantity) { updateStatus(ProductStatus.OUT_OF_STOCK); } + public void restoreStock(Integer quantity) { + this.stockInfo.restore(quantity); + } + public void delete(UUID deletedBy) { super.delete(deletedBy); this.isHide = true; @@ -110,4 +114,5 @@ public String getCompanyName () { public UUID getHubId () { return this.vendorInfo.getHubId(); } + } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java index 5656b5a..8c625fc 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java @@ -33,4 +33,10 @@ else if (quantity > stock) throw new BusinessException(ProductErrorCode.OUT_OF_STOCK); this.stock -= quantity; } + + public void restore(Integer quantity) { + if (quantity == null || quantity < 0) + throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); + this.stock += quantity; + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java new file mode 100644 index 0000000..3a1acf0 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java @@ -0,0 +1,23 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OrderCanceledEvent extends SagaEvent { + private static final String EVENT_TYPE = "order.canceled"; + + private String orderId; + private String productId; + private Integer quantity; + + public OrderCanceledEvent(String orderId, String productId, Integer quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java new file mode 100644 index 0000000..d99ea1b --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OrderCreationStartedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "order.creation.started"; + + private String orderId; + private String productId; + private Integer quantity; + + public OrderCreationStartedEvent(String orderId, String productId, Integer quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java new file mode 100644 index 0000000..ef78712 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java @@ -0,0 +1,25 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ProductStockDecreasedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "product.stock.decreased"; + + private String orderId; + private String productId; + private Integer quantity; + + public ProductStockDecreasedEvent(String orderId, String productId, Integer quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java new file mode 100644 index 0000000..488c174 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ProductStockDecreasedFailedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "product.stock.decreased.failed"; + + private String orderId; + private String productId; + private String message; + + public ProductStockDecreasedFailedEvent(String orderId, String productId, String message) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.message = message; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java new file mode 100644 index 0000000..f554433 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class StockRestoredEvent extends SagaEvent { + + private static final String EVENT_TYPE = "stock.restored"; + + private Object orderId; + private Object productId; + private Object quantity; + + public StockRestoredEvent(Object orderId, Object productId, Object quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java new file mode 100644 index 0000000..01ba565 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java @@ -0,0 +1,39 @@ +package com.shipflow.productservice.infrastructure.messaging.config; + +import org.springframework.stereotype.Component; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.common.messaging.handler.AbstractSagaHandler; +import com.shipflow.common.messaging.publisher.EventPublisher; +import com.shipflow.productservice.application.service.ProductService; +import com.shipflow.productservice.infrastructure.messaging.OrderCreationStartedEvent; +import com.shipflow.productservice.infrastructure.messaging.ProductStockDecreasedEvent; +import com.shipflow.productservice.infrastructure.messaging.ProductStockDecreasedFailedEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class OrderCreationStatedHandler extends AbstractSagaHandler { + private final ProductService productService; + private final EventPublisher eventPublisher; + + @Override + protected void process(OrderCreationStartedEvent event) { + try { + productService.decreaseStock(event.getProductId(), event.getQuantity()); + + eventPublisher.publish(new ProductStockDecreasedEvent( + event.getOrderId(), + event.getProductId(), + event.getQuantity() + )); + } catch (BusinessException e) { + eventPublisher.publish(new ProductStockDecreasedFailedEvent( + event.getOrderId(), + event.getProductId(), + e.getMessage() + )); + } + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductRabbitConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductRabbitConfig.java index 6b3e378..7541d95 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductRabbitConfig.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductRabbitConfig.java @@ -1,6 +1,5 @@ package com.shipflow.productservice.infrastructure.messaging.config; -import com.shipflow.config.message.RabbitMqConfig; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; @@ -9,6 +8,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.shipflow.config.message.RabbitMqConfig; + @Configuration public class ProductRabbitConfig { @@ -17,6 +18,14 @@ public class ProductRabbitConfig { public static final String QUEUE_PRODUCT_ORDER_CREATION_STARTED = "product.order.creation.started"; public static final String QUEUE_PRODUCT_ORDER_CREATION_STARTED_DLQ = QUEUE_PRODUCT_ORDER_CREATION_STARTED + ".dlq"; + public static final String ROUTING_ORDER_CANCELED = "order.canceled"; + public static final String ROUTING_ORDER_CREATION_FAILED = "order.creation.failed"; + + public static final String QUEUE_PRODUCT_STOCK_RESTORED = "product.stock.restored"; + public static final String QUEUE_PRODUCT_STOCK_RESTORED_DLQ = "product.stock.restored.dlq"; + + + @Bean public Queue queueProductOrderCreationStarted() { return RabbitMqConfig.durableQueue(QUEUE_PRODUCT_ORDER_CREATION_STARTED); @@ -27,6 +36,16 @@ public Queue queueProductOrderCreationStartedDlq() { return RabbitMqConfig.dlqQueue(QUEUE_PRODUCT_ORDER_CREATION_STARTED_DLQ); } + @Bean + public Queue queueProductStockRestored() { + return RabbitMqConfig.durableQueue(QUEUE_PRODUCT_STOCK_RESTORED); + } + + @Bean + public Queue queueProductStockRestoredDlq() { + return RabbitMqConfig.dlqQueue(QUEUE_PRODUCT_STOCK_RESTORED_DLQ); + } + @Bean public Binding bindProductOrderCreationStarted(TopicExchange sagaExchange) { return BindingBuilder.bind(queueProductOrderCreationStarted()) @@ -40,4 +59,25 @@ public Binding bindProductOrderCreationStartedDlq(DirectExchange sagaDlx) { .to(sagaDlx) .with(QUEUE_PRODUCT_ORDER_CREATION_STARTED_DLQ); } + + @Bean + public Binding bindProductStockRestored(TopicExchange sagaExchange) { + return BindingBuilder.bind(queueProductStockRestored()) + .to(sagaExchange) + .with(ROUTING_ORDER_CANCELED); + } + + @Bean + public Binding bindProductStockRestoredFailed(TopicExchange sagaExchange) { + return BindingBuilder.bind(queueProductStockRestored()) + .to(sagaExchange) + .with(ROUTING_ORDER_CREATION_FAILED); + } + + @Bean + public Binding bindProductStockRestoredDlq(DirectExchange sagaDlx) { + return BindingBuilder.bind(queueProductStockRestoredDlq()) + .to(sagaDlx) + .with(QUEUE_PRODUCT_STOCK_RESTORED_DLQ); + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java new file mode 100644 index 0000000..a611a88 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java @@ -0,0 +1,28 @@ +package com.shipflow.productservice.infrastructure.messaging.config; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.productservice.infrastructure.messaging.OrderCanceledEvent; +import com.shipflow.productservice.infrastructure.messaging.OrderCreationStartedEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ProductSagaListener { + + private final OrderCreationStatedHandler orderCreationStatedHandler; + private final StockRestoreHandler stockRestoreHandler; + + @RabbitListener(queues = ProductRabbitConfig.QUEUE_PRODUCT_ORDER_CREATION_STARTED) + public void handleOrderCreationStartedEvent(OrderCreationStartedEvent event) { + orderCreationStatedHandler.handle(event); + } + + @RabbitListener(queues = ProductRabbitConfig.QUEUE_PRODUCT_STOCK_RESTORED) + public void handleStockRestoredEvent(OrderCanceledEvent event) { + stockRestoreHandler.handle(event); + } + +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java new file mode 100644 index 0000000..03bac5b --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.messaging.config; + +import org.springframework.stereotype.Component; + +import com.shipflow.common.messaging.handler.AbstractSagaHandler; +import com.shipflow.common.messaging.publisher.EventPublisher; +import com.shipflow.productservice.application.service.ProductService; +import com.shipflow.productservice.infrastructure.messaging.OrderCanceledEvent; +import com.shipflow.productservice.infrastructure.messaging.StockRestoredEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class StockRestoreHandler extends AbstractSagaHandler { + private final ProductService productService; + private final EventPublisher eventPublisher; + + @Override + public void process(OrderCanceledEvent event) { + productService.restoreStock(event.getProductId(), event.getQuantity()); + eventPublisher.publish(new StockRestoredEvent(event.getOrderId(), event.getProductId(), event.getQuantity())); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java index 6486427..579613e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContext.java @@ -4,8 +4,6 @@ import org.springframework.stereotype.Component; -import jakarta.servlet.http.HttpServletRequest; - @Component public class UserContext { private static final ThreadLocal USER_ID_HOLDER = new ThreadLocal<>(); diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index 1fb1e6b..60c1be9 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -20,7 +20,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; -import org.springframework.mock.web.MockHttpServletRequest; import com.shipflow.common.exception.BusinessException; import com.shipflow.productservice.application.client.VendorFeignClient; From 940b9f7f76df45c4744cb0fda28f2de074c22478 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 16:16:23 +0900 Subject: [PATCH 21/38] =?UTF-8?q?test:=20=EC=A3=BC=EB=AC=B8=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20test=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 2 +- .../domain/exception/ProductErrorCode.java | 4 +- .../productservice/domain/model/Product.java | 4 -- .../productservice/domain/vo/StockInfo.java | 4 +- .../messaging/OrderCanceledEvent.java | 7 --- .../service/ProductServiceTest.java | 62 +++++++++++++++++++ 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 1bc5898..4a12f35 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -124,7 +124,7 @@ public void decreaseStock(String productId, Integer quantity) { Product product = findProductById(UUID.fromString(productId)); if (product.getStock() < quantity) { - throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); + throw new BusinessException(ProductErrorCode.EXCEEDS_STOCK_LEVEL); } product.decreaseStock(quantity); diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java index 1209a33..6fb5771 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java @@ -8,9 +8,9 @@ public enum ProductErrorCode implements ErrorCode { PRODUCT_NOT_FOUND("PRODUCT_NOT_FOUND", HttpStatus.NOT_FOUND, "해당 상품을 찾을 수 없습니다."), INVALID_STOCK_VALUE("INVALID_STOCK_VALUE", HttpStatus.BAD_REQUEST, "잘못된 재고값입니다."), - INVALID_ORDER_QUANTITY("INVALID_ORDER_QUANTITY", HttpStatus.BAD_REQUEST, "잘못된 주문량입니다."), + INVALID_ORDER_QUANTITY("INVALID_ORDER_QUANTITY", HttpStatus.BAD_REQUEST, "잘못된 수량입니다."), INACTIVE_PRODUCT("INACTIVE_PRODUCT", HttpStatus.BAD_REQUEST, "현재 비활성화된 상품입니다."), - OUT_OF_STOCK("OUT_OF_STOCK",HttpStatus.BAD_REQUEST,"요청하신 주문량이 잔여 재고량보다 많습니다."); + EXCEEDS_STOCK_LEVEL("EXCEEDS_STOCK_LEVEL", HttpStatus.BAD_REQUEST, "요청하신 주문량이 잔여 재고량보다 많습니다."); private final String code; private final HttpStatus status; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 784f106..20ba60b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -66,10 +66,6 @@ private BigDecimal validatePrice(BigDecimal price) { return price; } - public void updateVendorInfo(UUID companyId, String companyName, UUID hubId) { - this.vendorInfo = new VendorInfo(companyId, companyName, hubId); - } - public void updateStatus(ProductStatus status) { this.status = status; if (status.equals(ProductStatus.STOPPED) || status.equals(ProductStatus.DISCONTINUED) diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java index 8c625fc..6e63d80 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/vo/StockInfo.java @@ -29,8 +29,8 @@ public void setStock(Integer stock) { public void decrease(Integer quantity) { if (quantity == null || quantity < 0) throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); - else if (quantity > stock) - throw new BusinessException(ProductErrorCode.OUT_OF_STOCK); + else if (quantity > this.stock) + throw new BusinessException(ProductErrorCode.EXCEEDS_STOCK_LEVEL); this.stock -= quantity; } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java index 3a1acf0..918963a 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java @@ -13,11 +13,4 @@ public class OrderCanceledEvent extends SagaEvent { private String orderId; private String productId; private Integer quantity; - - public OrderCanceledEvent(String orderId, String productId, Integer quantity) { - super(EVENT_TYPE); - this.orderId = orderId; - this.productId = productId; - this.quantity = quantity; - } } diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index 60c1be9..0b88647 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -25,6 +25,7 @@ import com.shipflow.productservice.application.client.VendorFeignClient; import com.shipflow.productservice.application.dto.response.VendorInfoResponse; import com.shipflow.productservice.application.mapper.ProductMapper; +import com.shipflow.productservice.domain.exception.ProductErrorCode; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.model.ProductStatus; import com.shipflow.productservice.domain.repository.ProductRepository; @@ -199,7 +200,68 @@ void getProductList() { } + @Test + void getStockInfoAndOccupy() { + + } + + @Test + void decreaseStock_success() { + //given + Product product = ProductFixture.create(); + given(productRepository.findById(any())).willReturn(Optional.of(product)); + + //when + productService.decreaseStock(product.getId().toString(), 1); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct = productCaptor.getValue(); + assertThat(savedProduct.getStockInfo().getStock()) + .isEqualTo(99); + } + + @Test + void decreaseStock_올바르지_않은_차감요청() { + //given + Product product = ProductFixture.create(); + given(productRepository.findById(any())).willReturn(Optional.of(product)); + + //when&then + assertThatThrownBy(() -> productService.decreaseStock(product.getId().toString(), -1)) + .isInstanceOf(BusinessException.class) + .hasMessage(ProductErrorCode.INVALID_ORDER_QUANTITY.message()); + } + + @Test + void decreaseStock_재고보다_많은_차감요청() { + //given + Product product = ProductFixture.create(); + given(productRepository.findById(any())).willReturn(Optional.of(product)); + + //when&then + assertThatThrownBy(() -> productService.decreaseStock(product.getId().toString(), 101)) + .isInstanceOf(BusinessException.class) + .hasMessage(ProductErrorCode.EXCEEDS_STOCK_LEVEL.message()); + } + + @Test + void restoreStock() { + //given + Product product = ProductFixture.create(); + given(productRepository.findById(any())).willReturn(Optional.of(product)); + + //when + productService.restoreStock(product.getId().toString(), 1); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct = productCaptor.getValue(); + assertThat(savedProduct.getStockInfo().getStock()) + .isEqualTo(101); + } + //util private void setHttpHeaders(String userId, String role) { UserContext.setUserId(UUID.fromString(userId)); UserContext.setUserRole(role); From 6e1ef40177263ecdd9cf1027d8736918274e8902 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 16:56:59 +0900 Subject: [PATCH 22/38] =?UTF-8?q?feat:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EC=97=85=EC=B2=B4=20=EC=86=8C=EC=86=8D=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 7 +++++++ .../productservice/domain/model/Product.java | 10 +++++++++- .../domain/repository/ProductRepository.java | 2 ++ .../persistence/ProductJpaRepository.java | 3 +++ .../persistence/ProductRepositoryImpl.java | 6 ++++++ .../controller/ProductInternalController.java | 9 ++++++++- .../service/ProductServiceTest.java | 18 ++++++++++++++++++ 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 4a12f35..7b041e3 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -1,6 +1,7 @@ package com.shipflow.productservice.application.service; import java.time.Duration; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -91,7 +92,12 @@ public Slice getProductList(UUID companyId, Pageable pageab //internal + public void deleteByCompany(UUID companyId) { + List products = productRepository.findAllByCompanyId(companyId); + products.forEach(product -> delete(product.getId())); + } + //event /* * 주문 시 product 측 흐름 : * 1. 주문 전 재고 조회 요청 시) querystring으로 요청 재고 값 전달받음 @@ -156,4 +162,5 @@ private void ensureStockInRedis(UUID productId, String stockKey) { redisTemplate.opsForValue().setIfAbsent(stockKey, dbStock); } } + } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 20ba60b..d62d953 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -30,6 +30,9 @@ private Product(String name, BigDecimal price, Integer stock, ProductStatus stat this.vendorInfo = new VendorInfo(companyId, companyName, hubId); } + private Product() { + } + public static Product create(String name, BigDecimal price, Integer stock, ProductStatus status, UUID companyId, String companyName, UUID hubId, UUID createdBy) { Product product = new Product(name, price, stock, status, companyId, companyName, hubId); @@ -40,8 +43,13 @@ public static Product reconstruct(UUID id,String name, BigDecimal price, Integer stock, ProductStatus status, UUID companyId, String companyName, UUID hubId, Boolean isHide, UUID createdBy, LocalDateTime createdAt, LocalDateTime updatedAt, UUID updatedBy, LocalDateTime deletedAt, UUID deletedBy) { - Product product = new Product(name, price, stock, status, companyId, companyName, hubId); + Product product = new Product(); product.id=id; + product.name = name; + product.price = price; + product.status = status; + product.stockInfo = new StockInfo(stock); + product.vendorInfo = new VendorInfo(companyId, companyName, hubId); product.isHide = isHide; product.createdAt = createdAt; product.createdBy = createdBy; diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java index eb626f8..dac2cfb 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/repository/ProductRepository.java @@ -19,4 +19,6 @@ public interface ProductRepository { List findAll(); Integer findStockById(UUID productId); + + List findAllByCompanyId(UUID companyId); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java index 620fa19..2d16b49 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java @@ -1,5 +1,6 @@ package com.shipflow.productservice.infrastructure.persistence; +import java.util.List; import java.util.UUID; import org.springframework.data.domain.Pageable; @@ -12,4 +13,6 @@ public interface ProductJpaRepository extends JpaRepository findAllByCompanyId(UUID companyId); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java index 324de22..6093daf 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductRepositoryImpl.java @@ -47,4 +47,10 @@ public List findAll() { public Integer findStockById(UUID productId) { return jpaRepository.findStockById(productId); } + + @Override + public List findAllByCompanyId(UUID companyId) { + List entities = jpaRepository.findAllByCompanyId(companyId); + return entities.stream().map(ProductJpaEntity::toDomain).toList(); + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java index 55a996b..d195588 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -3,6 +3,7 @@ import java.util.UUID; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -15,7 +16,7 @@ import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/internal/products") +@RequestMapping("/internal/companies/{companyId}/products") @RequiredArgsConstructor public class ProductInternalController { private final ProductService productService; @@ -26,4 +27,10 @@ public ApiResponse getStockInfo(@PathVariable UUID productId, StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); return ApiResponse.ok(response); } + + @PatchMapping("/deactivate") + public ApiResponse deleteByCompany(@PathVariable UUID companyId) { + productService.deleteByCompany(companyId); + return ApiResponse.ok(null); + } } diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index 0b88647..8ceed97 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -261,6 +261,24 @@ void restoreStock() { .isEqualTo(101); } + @Test + void deleteByCompany() { + //given + setHttpHeaders(UUID.randomUUID().toString(), "Master"); + Product product = ProductFixture.create(); + List products = List.of(product); + given(productRepository.findById(any())).willReturn(Optional.of(product)); + given(productRepository.findAllByCompanyId(any())).willReturn(products); + + //when + productService.deleteByCompany(product.getId()); + + //then + verify(productRepository).save(productCaptor.capture()); + Product savedProduct = productCaptor.getValue(); + assertThat(savedProduct.getDeletedBy()).isNotNull(); + } + //util private void setHttpHeaders(String userId, String role) { UserContext.setUserId(UUID.fromString(userId)); From d5264e7199bb726567dce66b9c5bcdef1d908a8c Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 17:07:03 +0900 Subject: [PATCH 23/38] =?UTF-8?q?fix:=20pr=20=EC=A4=91=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=98=A4=EB=A5=98=EB=A1=9C=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 1da0933..f1702ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ARG MODULE WORKDIR /workspace COPY gradlew . COPY gradle gradle +RUN chmod +x gradlew COPY build.gradle . COPY settings.gradle . COPY common common From dfddb2d49496eddce34461050e2027e0ddbc93de Mon Sep 17 00:00:00 2001 From: JIN <126974009+Jin4041@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:22:17 +0900 Subject: [PATCH 24/38] Update product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../productservice/application/mapper/ProductMapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index 152cc3e..e6c6371 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -19,6 +19,7 @@ public interface ProductMapper { ProductInfoResponse toProductInfoResponse(Product product); ProductListResponse toProductListResponse(Product product); - + `@Mapping`(source = "id", target = "productId") + `@Mapping`(source = "stockInfo.stock", target = "stock") StockInfoResponse toStockInfoResponse(Product product); } From 5820303bf333e314979b579d56c8a8cb83de2f43 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 17:37:18 +0900 Subject: [PATCH 25/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 12 +++++++++ .../infrastructure/config/JpaAuditConfig.java | 5 +++- .../web/UserContextInterceptor.java | 25 +++++++++++++------ .../controller/ProductInternalController.java | 4 +-- .../src/main/resources/application.yaml | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 7b041e3..8361c8d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -92,6 +92,7 @@ public Slice getProductList(UUID companyId, Pageable pageab //internal + @Transactional public void deleteByCompany(UUID companyId) { List products = productRepository.findAllByCompanyId(companyId); products.forEach(product -> delete(product.getId())); @@ -107,7 +108,13 @@ public void deleteByCompany(UUID companyId) { * */ // 재고 조회 + @Transactional public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productId, Integer quantity) { + + if (quantity == null || quantity <= 0) { + throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); + } + String stockKey ="product:stock:"+productId; String occupancyKey ="product:"+ UserContext.getUserId() +":"+productId; @@ -126,6 +133,7 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI return new StockInfoResponse(productId, currentStock.intValue()); } + @Transactional public void decreaseStock(String productId, Integer quantity) { Product product = findProductById(UUID.fromString(productId)); @@ -137,7 +145,11 @@ public void decreaseStock(String productId, Integer quantity) { productRepository.save(product); } + @Transactional public void restoreStock(String productId, Integer quantity) { + String stockKey = "product:stock:" + productId; + redisTemplate.opsForValue().increment(stockKey, (long)quantity); + Product product = findProductById(UUID.fromString(productId)); product.restoreStock(quantity); productRepository.save(product); diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java index a03f103..b78df45 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/JpaAuditConfig.java @@ -11,13 +11,16 @@ @Configuration public class JpaAuditConfig { + + private static final UUID SYSTEM_ID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + @Bean public AuditorAware auditorAware() { return () -> { UUID userId=UserContext.getUserId(); if(userId==null) - return Optional.empty(); + return Optional.of(SYSTEM_ID); try { return Optional.of(userId); diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java index 14a7600..a8ea2d8 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/web/UserContextInterceptor.java @@ -13,21 +13,30 @@ @Component public class UserContextInterceptor implements HandlerInterceptor { @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws + Exception { + UserContext.clear(); String userId = request.getHeader("X-User-Id"); String userRole = request.getHeader("X-User-Role"); if (userId != null && !userId.isBlank()) { - try { - UserContext.setUserId(UUID.fromString(userId)); - UserContext.setUserRole(userRole); - } catch (IllegalArgumentException e) { - log.error("Invalid UUID format in X-User-Id header: {}", userId); - } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing X-User-Id"); + return false; + } + + try { + UserContext.setUserId(UUID.fromString(userId)); + UserContext.setUserRole(userRole); + return true; + } catch (IllegalArgumentException e) { + UserContext.clear(); + log.error("Invalid UUID format in X-User-Id header: {}", userId); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid UUID format in X-User-Id header"); + return true; } - return true; } + @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java index d195588..a5615cd 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; @@ -23,7 +23,7 @@ public class ProductInternalController { @GetMapping("/{productId}") public ApiResponse getStockInfo(@PathVariable UUID productId, - @RequestPart Integer quantity) { + @RequestParam Integer quantity) { StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); return ApiResponse.ok(response); } diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index 155cb5c..b89fa07 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -50,5 +50,5 @@ eureka: logging: level: - com.shipflow.product.infrastructure.client: DEBUG + com.shipflow.product.infrastructure.client: BASIC org.hibernate.orm.jdbc.bind: TRACE \ No newline at end of file From 6670706d5672584025c41ca1712002b09cacd477 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 17:54:22 +0900 Subject: [PATCH 26/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../productservice/domain/model/Product.java | 4 ++++ .../config/RedisInventoryLoader.java | 19 +++++++++++---- .../messaging/OrderCanceledEvent.java | 7 ++++++ .../ProductStockDecreasedFailedEvent.java | 6 ++--- .../messaging/StockRestoredEvent.java | 8 +++---- .../messaging/StockRestoredFailedEvent.java | 24 +++++++++++++++++++ .../config/OrderCreationStatedHandler.java | 3 +-- .../messaging/config/StockRestoreHandler.java | 11 +++++++-- .../persistence/ProductJpaRepository.java | 2 +- 9 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index d62d953..2c874d1 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -79,12 +79,16 @@ public void updateStatus(ProductStatus status) { if (status.equals(ProductStatus.STOPPED) || status.equals(ProductStatus.DISCONTINUED) || status.equals(ProductStatus.OUT_OF_STOCK)) this.isHide = true; + else if (status.equals(ProductStatus.ON_SALE)) + this.isHide = false; } public void updateStock(Integer stock) { this.stockInfo.setStock(stock); if (stock == 0) updateStatus(ProductStatus.OUT_OF_STOCK); + else if (stock > 0) + updateStatus(ProductStatus.ON_SALE); this.stockInfo.setStock(stock); } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java index 3956f50..6f3167a 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisInventoryLoader.java @@ -2,6 +2,9 @@ import java.util.List; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -12,17 +15,25 @@ @Component @RequiredArgsConstructor -public class RedisInventoryLoader { +public class RedisInventoryLoader implements ApplicationRunner { private final ProductRepository productRepository; private final RedisTemplate redisTemplate; + @Override + public void run(ApplicationArguments args) { + loadInventoryToRedis(); + } + public void loadInventoryToRedis() { List products = productRepository.findAll(); - products.forEach(product -> { - String key="product:stock:"+product.getId(); - redisTemplate.opsForValue().set(key, product.getStock()); + redisTemplate.executePipelined((RedisCallback)connection -> { + products.forEach(product -> { + String key = "product:stock:" + product.getId(); + redisTemplate.opsForValue().set(key, product.getStock()); + }); + return null; }); } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java index 918963a..3a1acf0 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java @@ -13,4 +13,11 @@ public class OrderCanceledEvent extends SagaEvent { private String orderId; private String productId; private Integer quantity; + + public OrderCanceledEvent(String orderId, String productId, Integer quantity) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.quantity = quantity; + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java index 488c174..f0f9bc8 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java @@ -13,12 +13,12 @@ public class ProductStockDecreasedFailedEvent extends SagaEvent { private String orderId; private String productId; - private String message; + private String reason; - public ProductStockDecreasedFailedEvent(String orderId, String productId, String message) { + public ProductStockDecreasedFailedEvent(String orderId, String productId, String reason) { super(EVENT_TYPE); this.orderId = orderId; this.productId = productId; - this.message = message; + this.reason = reason; } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java index f554433..bb1b9d3 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java @@ -11,11 +11,11 @@ public class StockRestoredEvent extends SagaEvent { private static final String EVENT_TYPE = "stock.restored"; - private Object orderId; - private Object productId; - private Object quantity; + private String orderId; + private String productId; + private Integer quantity; - public StockRestoredEvent(Object orderId, Object productId, Object quantity) { + public StockRestoredEvent(String orderId, String productId, Integer quantity) { super(EVENT_TYPE); this.orderId = orderId; this.productId = productId; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java new file mode 100644 index 0000000..65c483d --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.messaging; + +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class StockRestoredFailedEvent extends SagaEvent { + + private static final String EVENT_TYPE = "stock.restored"; + + private String orderId; + private String productId; + private String reason; + + public StockRestoredFailedEvent(String orderId, String productId, String reason) { + super(EVENT_TYPE); + this.orderId = orderId; + this.productId = productId; + this.reason = reason; + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java index 01ba565..d990d31 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java @@ -2,7 +2,6 @@ import org.springframework.stereotype.Component; -import com.shipflow.common.exception.BusinessException; import com.shipflow.common.messaging.handler.AbstractSagaHandler; import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.productservice.application.service.ProductService; @@ -28,7 +27,7 @@ protected void process(OrderCreationStartedEvent event) { event.getProductId(), event.getQuantity() )); - } catch (BusinessException e) { + } catch (Exception e) { eventPublisher.publish(new ProductStockDecreasedFailedEvent( event.getOrderId(), event.getProductId(), diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java index 03bac5b..09c6e47 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java @@ -7,6 +7,7 @@ import com.shipflow.productservice.application.service.ProductService; import com.shipflow.productservice.infrastructure.messaging.OrderCanceledEvent; import com.shipflow.productservice.infrastructure.messaging.StockRestoredEvent; +import com.shipflow.productservice.infrastructure.messaging.StockRestoredFailedEvent; import lombok.RequiredArgsConstructor; @@ -18,7 +19,13 @@ public class StockRestoreHandler extends AbstractSagaHandler @Override public void process(OrderCanceledEvent event) { - productService.restoreStock(event.getProductId(), event.getQuantity()); - eventPublisher.publish(new StockRestoredEvent(event.getOrderId(), event.getProductId(), event.getQuantity())); + try { + productService.restoreStock(event.getProductId(), event.getQuantity()); + eventPublisher.publish(new StockRestoredEvent( + event.getOrderId(), event.getProductId(), event.getQuantity())); + } catch (Exception e) { + eventPublisher.publish(new StockRestoredFailedEvent( + event.getOrderId(), event.getProductId(), e.getMessage())); + } } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java index 2d16b49..bb44b8b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/persistence/ProductJpaRepository.java @@ -11,7 +11,7 @@ public interface ProductJpaRepository extends JpaRepository { Slice findAllByCompanyId(UUID companyId, Pageable pageable); - @Query("select stock from ProductJpaEntity where id = :productId and isHide = false") + @Query("select p.stock from ProductJpaEntity p where p.id = :productId and p.isHide = false") Integer findStockById(UUID productId); List findAllByCompanyId(UUID companyId); From 2c5f67a2380dd7057023102e6cdcbde6cae6e246 Mon Sep 17 00:00:00 2001 From: jin Date: Fri, 3 Apr 2026 18:19:34 +0900 Subject: [PATCH 27/38] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 21 +++++++++++++++---- .../productservice/domain/model/Product.java | 2 +- .../config/OrderCreationStatedHandler.java | 17 ++++++++------- .../messaging/config/ProductSagaListener.java | 4 ++-- .../messaging/config/StockRestoreHandler.java | 12 ++++++----- .../{ => event}/OrderCanceledEvent.java | 2 +- .../OrderCreationStartedEvent.java | 2 +- .../ProductStockDecreasedEvent.java | 2 +- .../ProductStockDecreasedFailedEvent.java | 2 +- .../{ => event}/StockRestoredEvent.java | 2 +- .../{ => event}/StockRestoredFailedEvent.java | 4 ++-- 11 files changed, 43 insertions(+), 27 deletions(-) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/OrderCanceledEvent.java (88%) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/OrderCreationStartedEvent.java (88%) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/ProductStockDecreasedEvent.java (88%) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/ProductStockDecreasedFailedEvent.java (89%) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/StockRestoredEvent.java (88%) rename product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/{ => event}/StockRestoredFailedEvent.java (77%) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 8361c8d..74db148 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -95,7 +95,10 @@ public Slice getProductList(UUID companyId, Pageable pageab @Transactional public void deleteByCompany(UUID companyId) { List products = productRepository.findAllByCompanyId(companyId); - products.forEach(product -> delete(product.getId())); + products.forEach(product -> { + delete(product.getId()); + redisTemplate.delete("product:stock:" + product.getId()); + }); } //event @@ -111,9 +114,7 @@ public void deleteByCompany(UUID companyId) { @Transactional public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productId, Integer quantity) { - if (quantity == null || quantity <= 0) { - throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); - } + validateQuantitiy(quantity); String stockKey ="product:stock:"+productId; String occupancyKey ="product:"+ UserContext.getUserId() +":"+productId; @@ -135,6 +136,9 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI @Transactional public void decreaseStock(String productId, Integer quantity) { + + validateQuantitiy(quantity); + Product product = findProductById(UUID.fromString(productId)); if (product.getStock() < quantity) { @@ -147,6 +151,9 @@ public void decreaseStock(String productId, Integer quantity) { @Transactional public void restoreStock(String productId, Integer quantity) { + + validateQuantitiy(quantity); + String stockKey = "product:stock:" + productId; redisTemplate.opsForValue().increment(stockKey, (long)quantity); @@ -175,4 +182,10 @@ private void ensureStockInRedis(UUID productId, String stockKey) { } } + private void validateQuantitiy(Integer quantity) { + if (quantity == null || quantity <= 0) { + throw new BusinessException(ProductErrorCode.INVALID_ORDER_QUANTITY); + } + } + } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java index 2c874d1..5ec0056 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/model/Product.java @@ -68,7 +68,7 @@ public void updateInfo(String name, BigDecimal price, ProductStatus status) { } private BigDecimal validatePrice(BigDecimal price) { - if (Objects.requireNonNull(price, "가격은 필수입니다.").compareTo(BigDecimal.ZERO) < 0) { + if (Objects.requireNonNull(price, "가격은 필수입니다.").compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("가격은 0보다 커야 합니다."); } return price; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java index d990d31..f0c75b2 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/OrderCreationStatedHandler.java @@ -5,9 +5,9 @@ import com.shipflow.common.messaging.handler.AbstractSagaHandler; import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.productservice.application.service.ProductService; -import com.shipflow.productservice.infrastructure.messaging.OrderCreationStartedEvent; -import com.shipflow.productservice.infrastructure.messaging.ProductStockDecreasedEvent; -import com.shipflow.productservice.infrastructure.messaging.ProductStockDecreasedFailedEvent; +import com.shipflow.productservice.infrastructure.messaging.event.OrderCreationStartedEvent; +import com.shipflow.productservice.infrastructure.messaging.event.ProductStockDecreasedEvent; +import com.shipflow.productservice.infrastructure.messaging.event.ProductStockDecreasedFailedEvent; import lombok.RequiredArgsConstructor; @@ -22,11 +22,6 @@ protected void process(OrderCreationStartedEvent event) { try { productService.decreaseStock(event.getProductId(), event.getQuantity()); - eventPublisher.publish(new ProductStockDecreasedEvent( - event.getOrderId(), - event.getProductId(), - event.getQuantity() - )); } catch (Exception e) { eventPublisher.publish(new ProductStockDecreasedFailedEvent( event.getOrderId(), @@ -34,5 +29,11 @@ protected void process(OrderCreationStartedEvent event) { e.getMessage() )); } + + eventPublisher.publish(new ProductStockDecreasedEvent( + event.getOrderId(), + event.getProductId(), + event.getQuantity() + )); } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java index a611a88..58c6d4c 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductSagaListener.java @@ -3,8 +3,8 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; -import com.shipflow.productservice.infrastructure.messaging.OrderCanceledEvent; -import com.shipflow.productservice.infrastructure.messaging.OrderCreationStartedEvent; +import com.shipflow.productservice.infrastructure.messaging.event.OrderCanceledEvent; +import com.shipflow.productservice.infrastructure.messaging.event.OrderCreationStartedEvent; import lombok.RequiredArgsConstructor; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java index 09c6e47..88a23c6 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/StockRestoreHandler.java @@ -5,9 +5,9 @@ import com.shipflow.common.messaging.handler.AbstractSagaHandler; import com.shipflow.common.messaging.publisher.EventPublisher; import com.shipflow.productservice.application.service.ProductService; -import com.shipflow.productservice.infrastructure.messaging.OrderCanceledEvent; -import com.shipflow.productservice.infrastructure.messaging.StockRestoredEvent; -import com.shipflow.productservice.infrastructure.messaging.StockRestoredFailedEvent; +import com.shipflow.productservice.infrastructure.messaging.event.OrderCanceledEvent; +import com.shipflow.productservice.infrastructure.messaging.event.StockRestoredEvent; +import com.shipflow.productservice.infrastructure.messaging.event.StockRestoredFailedEvent; import lombok.RequiredArgsConstructor; @@ -21,11 +21,13 @@ public class StockRestoreHandler extends AbstractSagaHandler public void process(OrderCanceledEvent event) { try { productService.restoreStock(event.getProductId(), event.getQuantity()); - eventPublisher.publish(new StockRestoredEvent( - event.getOrderId(), event.getProductId(), event.getQuantity())); + } catch (Exception e) { eventPublisher.publish(new StockRestoredFailedEvent( event.getOrderId(), event.getProductId(), e.getMessage())); } + + eventPublisher.publish(new StockRestoredEvent( + event.getOrderId(), event.getProductId(), event.getQuantity())); } } diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCanceledEvent.java similarity index 88% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCanceledEvent.java index 3a1acf0..9b3c572 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCanceledEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCanceledEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCreationStartedEvent.java similarity index 88% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCreationStartedEvent.java index d99ea1b..e7fa174 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/OrderCreationStartedEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/OrderCreationStartedEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedEvent.java similarity index 88% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedEvent.java index ef78712..d011dc1 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedFailedEvent.java similarity index 89% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedFailedEvent.java index f0f9bc8..ec3370c 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/ProductStockDecreasedFailedEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/ProductStockDecreasedFailedEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredEvent.java similarity index 88% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredEvent.java index bb1b9d3..3abd97a 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredFailedEvent.java similarity index 77% rename from product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java rename to product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredFailedEvent.java index 65c483d..5b826b5 100644 --- a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/StockRestoredFailedEvent.java +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/StockRestoredFailedEvent.java @@ -1,4 +1,4 @@ -package com.shipflow.productservice.infrastructure.messaging; +package com.shipflow.productservice.infrastructure.messaging.event; import com.shipflow.common.messaging.event.SagaEvent; @@ -9,7 +9,7 @@ @NoArgsConstructor public class StockRestoredFailedEvent extends SagaEvent { - private static final String EVENT_TYPE = "stock.restored"; + private static final String EVENT_TYPE = "stock.restored.failed"; private String orderId; private String productId; From fc70eef538fd2db920b529208a40bfba827da966 Mon Sep 17 00:00:00 2001 From: jin Date: Sat, 4 Apr 2026 17:04:08 +0900 Subject: [PATCH 28/38] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=88=98=EC=A0=95=20=EC=BD=94=EB=93=9C=20=EB=B0=B1?= =?UTF-8?q?=ED=8B=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../productservice/application/mapper/ProductMapper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index e6c6371..b8660ca 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -1,6 +1,7 @@ package com.shipflow.productservice.application.mapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import com.shipflow.productservice.application.dto.response.StockInfoResponse; import com.shipflow.productservice.domain.model.Product; @@ -19,7 +20,8 @@ public interface ProductMapper { ProductInfoResponse toProductInfoResponse(Product product); ProductListResponse toProductListResponse(Product product); - `@Mapping`(source = "id", target = "productId") - `@Mapping`(source = "stockInfo.stock", target = "stock") + + @Mapping(source = "id", target = "productId") + @Mapping(source = "stockInfo.stock", target = "stock") StockInfoResponse toStockInfoResponse(Product product); } From 7e56a166d82a23d5e37cf51d60e4403945806344 Mon Sep 17 00:00:00 2001 From: jin Date: Sat, 4 Apr 2026 17:33:45 +0900 Subject: [PATCH 29/38] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=88=98=EC=A0=95=20=EC=BD=94=EB=93=9C=20=EB=B0=B1?= =?UTF-8?q?=ED=8B=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- product-service/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index b89fa07..155cb5c 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -50,5 +50,5 @@ eureka: logging: level: - com.shipflow.product.infrastructure.client: BASIC + com.shipflow.product.infrastructure.client: DEBUG org.hibernate.orm.jdbc.bind: TRACE \ No newline at end of file From 7afd44c52354fc7253b74ac4f31103410f97fe53 Mon Sep 17 00:00:00 2001 From: jin Date: Sat, 4 Apr 2026 19:18:28 +0900 Subject: [PATCH 30/38] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EC=A6=88?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=AC=EB=8F=99=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProductserviceApplication.java | 2 +- .../infrastructure/config/RedisConfig.java | 24 +++++++++++++++++++ .../src/main/resources/application.yaml | 22 +++++++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisConfig.java diff --git a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java index 648e597..f9ed76c 100644 --- a/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java +++ b/product-service/src/main/java/com/shipflow/productservice/ProductserviceApplication.java @@ -7,7 +7,7 @@ @EnableFeignClients @EnableJpaAuditing -@SpringBootApplication +@SpringBootApplication(scanBasePackages = "com.shipflow") public class ProductserviceApplication { public static void main(String[] args) { diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisConfig.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisConfig.java new file mode 100644 index 0000000..97062dd --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/config/RedisConfig.java @@ -0,0 +1,24 @@ +package com.shipflow.productservice.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // Key는 문자열로, Value는 숫자로 직렬화 설정 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericToStringSerializer<>(Integer.class)); + + return template; + } +} diff --git a/product-service/src/main/resources/application.yaml b/product-service/src/main/resources/application.yaml index 155cb5c..69cb43b 100644 --- a/product-service/src/main/resources/application.yaml +++ b/product-service/src/main/resources/application.yaml @@ -4,9 +4,9 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=product - username: ${DB_USER:shipflow} - password: ${DB_PASSWORD:1234} + url: jdbc:postgresql://postgres:5432/${POSTGRES_DB:shipflow}?currentSchema=product + username: ${POSTGRES_USER:shipflow} + password: ${POSTGRES_PASSWORD:1234} cloud: openfeign: @@ -19,7 +19,7 @@ spring: data: redis: - host: localhost + host: redis port: 6379 database: 0 timeout: 5000 @@ -38,6 +38,15 @@ spring: default_schema: product show-sql: true + rabbitmq: + host: rabbitmq + port: 5672 + + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration + + server: port: 8090 @@ -51,4 +60,7 @@ eureka: logging: level: com.shipflow.product.infrastructure.client: DEBUG - org.hibernate.orm.jdbc.bind: TRACE \ No newline at end of file + org.hibernate.orm.jdbc.bind: TRACE + + + From 98fc9995891d87d328919c9bff2200266ae040d0 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 11:56:39 +0900 Subject: [PATCH 31/38] =?UTF-8?q?refactor:=20order=EC=9D=98=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=A0=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/response/StockInfoResponse.java | 5 +++++ .../productservice/application/mapper/ProductMapper.java | 4 ++++ .../presentation/controller/ProductExternalController.java | 3 +-- .../presentation/controller/ProductInternalController.java | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java index 450e9ac..a679769 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/StockInfoResponse.java @@ -2,11 +2,16 @@ import java.util.UUID; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import lombok.NonNull; public record StockInfoResponse( @NonNull UUID productId, + @NotBlank String productName, + @NonNull UUID supplierCompanyId, + @NotBlank String supplierCompanyName, + @NonNull UUID departureHubId, @NonNull @Positive Integer stock ){ } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java index b8660ca..95143a9 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/mapper/ProductMapper.java @@ -23,5 +23,9 @@ public interface ProductMapper { @Mapping(source = "id", target = "productId") @Mapping(source = "stockInfo.stock", target = "stock") + @Mapping(source = "name", target = "productName") + @Mapping(source = "companyId", target = "supplierCompanyId") + @Mapping(source = "companyName", target = "supplierCompanyName") + @Mapping(source = "hubId", target = "departureHubId") StockInfoResponse toStockInfoResponse(Product product); } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java index 0f367ce..8d03e4b 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java @@ -26,7 +26,6 @@ import com.shipflow.productservice.presentation.dto.response.ProductListResponse; import com.shipflow.productservice.presentation.dto.response.ProductUpdateResponse; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -38,7 +37,7 @@ public class ProductExternalController { @PostMapping public ResponseEntity> addProduct(@PathVariable UUID companyId, - @Valid @RequestBody ProductCreateRequest productCreateRequest, HttpServletRequest request) { + @Valid @RequestBody ProductCreateRequest productCreateRequest) { ProductCreateResponse response = productService.create(companyId, productCreateRequest); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(response)); } diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java index a5615cd..ac1c47d 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -23,7 +23,7 @@ public class ProductInternalController { @GetMapping("/{productId}") public ApiResponse getStockInfo(@PathVariable UUID productId, - @RequestParam Integer quantity) { + @RequestParam Integer quantity, @PathVariable UUID companyId) { StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); return ApiResponse.ok(response); } From 49e50b0566cff75487edcd35d0d45453300a4a20 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 13:01:33 +0900 Subject: [PATCH 32/38] =?UTF-8?q?feat:=20api=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=20companyId=20=EA=B2=80=EC=A6=9D,=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=9D=B4=20=EC=97=85=EC=B2=B4=EB=82=98=20=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=EB=8B=B4=EB=8B=B9=EC=9E=90=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=8B=B4=EB=8B=B9=EC=97=AC=EB=B6=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/client/UserFeignClient.java | 15 ++++ .../dto/response/UserInfoResponse.java | 14 ++++ .../application/service/ProductService.java | 79 ++++++++++++++++--- .../domain/exception/ProductErrorCode.java | 5 +- .../controller/ProductExternalController.java | 18 +++-- .../service/ProductServiceTest.java | 12 +-- 6 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/client/UserFeignClient.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/application/dto/response/UserInfoResponse.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/client/UserFeignClient.java b/product-service/src/main/java/com/shipflow/productservice/application/client/UserFeignClient.java new file mode 100644 index 0000000..6bec2ea --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/client/UserFeignClient.java @@ -0,0 +1,15 @@ +package com.shipflow.productservice.application.client; + +import java.util.UUID; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import com.shipflow.productservice.application.dto.response.UserInfoResponse; + +@FeignClient(name = "user-service") +public interface UserFeignClient { + @GetMapping("/internal/users/{userId}") + UserInfoResponse getUserInfoById(@PathVariable("userId") UUID userId); +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/dto/response/UserInfoResponse.java b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/UserInfoResponse.java new file mode 100644 index 0000000..620926e --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/application/dto/response/UserInfoResponse.java @@ -0,0 +1,14 @@ +package com.shipflow.productservice.application.dto.response; + +import java.util.UUID; + +import jakarta.validation.constraints.NotBlank; +import lombok.NonNull; + +public record UserInfoResponse( + @NonNull UUID id, + @NotBlank String name, + @NonNull UUID hubId, + @NonNull UUID companyId +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 2e0cc65..ddbadf2 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -14,8 +14,10 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.common.exception.CommonErrorCode; +import com.shipflow.productservice.application.client.UserFeignClient; import com.shipflow.productservice.application.client.VendorFeignClient; import com.shipflow.productservice.application.dto.response.StockInfoResponse; +import com.shipflow.productservice.application.dto.response.UserInfoResponse; import com.shipflow.productservice.application.dto.response.VendorInfoResponse; import com.shipflow.productservice.application.mapper.ProductMapper; import com.shipflow.productservice.domain.exception.ProductErrorCode; @@ -40,30 +42,37 @@ public class ProductService { private final ProductMapper mapper; private final VendorFeignClient vendorClient; private final RedisTemplate redisTemplate; + private final UserFeignClient userClient; //external @Transactional public ProductCreateResponse create(UUID companyId, ProductCreateRequest request) { + validateAuth(companyId); UUID createrId = UserContext.getUserId(); VendorInfoResponse response = vendorClient.getVendorInfo(companyId); Product product = Product.create( request.name(), request.price(), request.stock(), - request.status(), companyId, response.name(), response.hubId(), - createrId); + request.status(), companyId, response.name(), response.hubId(), createrId); Product savedProduct = productRepository.save(product); return mapper.toCreateResponse(savedProduct); } @Transactional - public void delete(UUID productId) { + public void delete(UUID productId, UUID companyId) { + validateAuth(companyId); + UUID deleterId = UserContext.getUserId(); Product product = findProductById(productId); product.delete(deleterId); productRepository.save(product); + redisTemplate.delete("product:stock:" + productId); } @Transactional - public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfoRequest request) { + public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfoRequest request, UUID companyId) { + validateAuth(companyId); + validateProductOwnership(productId, companyId); + Product product = findProductById(productId); product.updateInfo( request.name(), request.price(), request.status() @@ -73,19 +82,30 @@ public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfo } @Transactional - public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request) { + public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID companyId) { + validateAuth(companyId); + validateProductOwnership(productId, companyId); + Product product = findProductById(productId); product.updateStock(request.stock()); + productRepository.save(product); + redisTemplate.delete("product:stock:" + productId); + redisTemplate.opsForValue().set("product:stock:" + productId, product.getStock()); + return mapper.toUpdateResponse(product); } - public ProductInfoResponse getProductInfo(UUID productId) { + public ProductInfoResponse getProductInfo(UUID productId, UUID companyId) { + validateAuth(companyId); + Product product = findProductById(productId); return mapper.toProductInfoResponse(product); } public Slice getProductList(UUID companyId, Pageable pageable) { + validateAuth(companyId); + Slice products = productRepository.findAllByCompanyId(companyId, pageable); return products.map(mapper::toProductListResponse); } @@ -96,7 +116,7 @@ public Slice getProductList(UUID companyId, Pageable pageab public void deleteByCompany(UUID companyId) { List products = productRepository.findAllByCompanyId(companyId); products.forEach(product -> { - delete(product.getId()); + delete(product.getId(), companyId); redisTemplate.delete("product:stock:" + product.getId()); }); } @@ -116,6 +136,8 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI validateQuantitiy(quantity); + Product product = findProductById(productId); + String stockKey ="product:stock:"+productId; String occupancyKey ="product:"+ UserContext.getUserId() +":"+productId; @@ -126,12 +148,15 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI if(currentStock<0){ Long restoreStock=redisTemplate.opsForValue().increment(stockKey, (long)quantity); - return new StockInfoResponse(productId, restoreStock!=null?restoreStock.intValue():currentStock.intValue()); + return new StockInfoResponse(productId, product.getName(), product.getCompanyId(), + product.getCompanyName(), product.getHubId(), + restoreStock != null ? restoreStock.intValue() : currentStock.intValue()); } redisTemplate.opsForValue().set(occupancyKey, quantity, Duration.ofSeconds(5)); - return new StockInfoResponse(productId, currentStock.intValue()); + return new StockInfoResponse(productId, product.getName(), product.getCompanyId(), + product.getCompanyName(), product.getHubId(), currentStock.intValue()); } @Transactional @@ -188,4 +213,40 @@ private void validateQuantitiy(Integer quantity) { } } + private void validateAuth(UUID companyId) { + validateCompanyId(companyId); + String role = UserContext.getUserRole(); + + if (role.equals("MASTER")) + return; + + UUID userId = UserContext.getUserId(); + UserInfoResponse userInfo = userClient.getUserInfoById(userId); + + if (role.equals("HUB_MANAGER")) { + if (vendorClient.getVendorInfo(companyId).hubId().equals(userInfo.hubId())) + return; + } else if (role.equals("COMPANY_MANAGER")) { + if (userInfo.companyId().equals(companyId)) + return; + } + + throw new BusinessException(ProductErrorCode.UNAUTHORIZED); + } + + private void validateCompanyId(UUID companyId) { + if (companyId == null) { + throw new BusinessException(ProductErrorCode.COMPANY_ID_REQUIRED); + } + } + + private Product validateProductOwnership(UUID productId, UUID companyId) { + validateCompanyId(companyId); + Product product = findProductById(productId); + if (!product.getCompanyId().equals(companyId)) { + throw new BusinessException(ProductErrorCode.PRODUCT_NOT_OWNED_BY_COMPANY); + } + return product; + } + } diff --git a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java index 6fb5771..f090099 100644 --- a/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java +++ b/product-service/src/main/java/com/shipflow/productservice/domain/exception/ProductErrorCode.java @@ -10,7 +10,10 @@ public enum ProductErrorCode implements ErrorCode { INVALID_STOCK_VALUE("INVALID_STOCK_VALUE", HttpStatus.BAD_REQUEST, "잘못된 재고값입니다."), INVALID_ORDER_QUANTITY("INVALID_ORDER_QUANTITY", HttpStatus.BAD_REQUEST, "잘못된 수량입니다."), INACTIVE_PRODUCT("INACTIVE_PRODUCT", HttpStatus.BAD_REQUEST, "현재 비활성화된 상품입니다."), - EXCEEDS_STOCK_LEVEL("EXCEEDS_STOCK_LEVEL", HttpStatus.BAD_REQUEST, "요청하신 주문량이 잔여 재고량보다 많습니다."); + EXCEEDS_STOCK_LEVEL("EXCEEDS_STOCK_LEVEL", HttpStatus.BAD_REQUEST, "요청하신 주문량이 잔여 재고량보다 많습니다."), + PRODUCT_NOT_OWNED_BY_COMPANY("PRODUCT_NOT_OWNED_BY_COMPANY", HttpStatus.BAD_REQUEST, "상품이 해당 회사에 소속되어 있지 않습니다."), + COMPANY_ID_REQUIRED("COMPANY_ID_REQUIRED", HttpStatus.BAD_REQUEST, "회사 ID가 필요합니다."), + UNAUTHORIZED("UNAUTHORIZED", HttpStatus.FORBIDDEN, "허가되지 않은 접근입니다."); private final String code; private final HttpStatus status; diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java index 8d03e4b..869b109 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductExternalController.java @@ -43,29 +43,31 @@ public ResponseEntity> addProduct(@PathVariab } @DeleteMapping("/{productId}") - public ResponseEntity deleteProduct(@PathVariable UUID productId) { - productService.delete(productId); + public ResponseEntity deleteProduct(@PathVariable UUID productId, @PathVariable UUID companyId) { + productService.delete(productId, companyId); return ResponseEntity.status(HttpStatus.OK).body("요청이 정상 처리되었습니다."); } @PatchMapping("/{productId}") public ResponseEntity> updateProductInfo(@PathVariable UUID productId, - @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest) { - ProductUpdateResponse response = productService.updateProductInfo(productId, productUpdateInfoRequest); + @RequestBody ProductUpdateInfoRequest productUpdateInfoRequest, @PathVariable UUID companyId) { + ProductUpdateResponse response = productService.updateProductInfo(productId, productUpdateInfoRequest, + companyId); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @PostMapping("/{productId}/stock") public ResponseEntity> updateStock(@PathVariable UUID productId, - @Valid @RequestBody ProductUpdateStockRequest productUpdateStockRequest) { + @Valid @RequestBody ProductUpdateStockRequest productUpdateStockRequest, @PathVariable UUID companyId) { ProductUpdateResponse response = productService.updateStock(productId, - productUpdateStockRequest); + productUpdateStockRequest, companyId); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } @GetMapping("/{productId}") - public ResponseEntity> getProductInfo(@PathVariable UUID productId) { - ProductInfoResponse response = productService.getProductInfo(productId); + public ResponseEntity> getProductInfo(@PathVariable UUID productId, + @PathVariable UUID companyId) { + ProductInfoResponse response = productService.getProductInfo(productId, companyId); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(response)); } diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index e619c88..ba79aa1 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -91,7 +91,7 @@ void delete() { given(productRepository.findById(productId)).willReturn(Optional.of(product)); //when - productService.delete(productId); + productService.delete(productId, companyId); //then verify(productRepository).save(productCaptor.capture()); @@ -108,7 +108,7 @@ void updateInfo() { given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when - productService.updateProductInfo(product.getId(), request); + productService.updateProductInfo(product.getId(), request, companyId); //then verify(productRepository).save(productCaptor.capture()); @@ -126,7 +126,7 @@ void updateInfo() { given(productRepository.findById(productId)).willReturn(Optional.of(product)); //when - productService.updateStock(productId, request); + productService.updateStock(productId, request, companyId); //then verify(productRepository).save(productCaptor.capture()); @@ -143,7 +143,7 @@ void updateInfo() { given(productRepository.findById(productId)).willReturn(Optional.of(product)); //when&then - assertThatThrownBy(() -> productService.updateStock(productId, request)) + assertThatThrownBy(() -> productService.updateStock(productId, request, companyId)) .isInstanceOf(BusinessException.class) .hasMessageContaining("잘못된 재고값입니다."); @@ -158,7 +158,7 @@ void updateInfo() { given(productRepository.findById(productId)).willReturn(Optional.of(product)); //when - productService.updateStock(productId, request); + productService.updateStock(productId, request, companyId); //then verify(productRepository).save(productCaptor.capture()); @@ -173,7 +173,7 @@ void getProductInfo_success() { given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when - ProductInfoResponse response = productService.getProductInfo(product.getId()); + ProductInfoResponse response = productService.getProductInfo(product.getId(), companyId); //then assertThat(response.id()).isEqualTo(product.getId()); From e1b6ac41bfffe05da2c7633b41f4514324e92a30 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 13:05:02 +0900 Subject: [PATCH 33/38] =?UTF-8?q?feat:=20cascading=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 10 ++++++++++ .../controller/ProductInternalController.java | 20 +++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index ddbadf2..cf923ff 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -48,12 +48,17 @@ public class ProductService { @Transactional public ProductCreateResponse create(UUID companyId, ProductCreateRequest request) { validateAuth(companyId); + UUID createrId = UserContext.getUserId(); VendorInfoResponse response = vendorClient.getVendorInfo(companyId); + Product product = Product.create( request.name(), request.price(), request.stock(), request.status(), companyId, response.name(), response.hubId(), createrId); + Product savedProduct = productRepository.save(product); + redisTemplate.opsForValue().set("product:stock:" + savedProduct.getId(), savedProduct.getStock()); + return mapper.toCreateResponse(savedProduct); } @@ -121,6 +126,11 @@ public void deleteByCompany(UUID companyId) { }); } + @Transactional + public void deleteByHub(List companyIds) { + companyIds.forEach(this::deleteByCompany); + } + //event /* * 주문 시 product 측 흐름 : diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java index ac1c47d..7fb7b2e 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -1,13 +1,13 @@ package com.shipflow.productservice.presentation.controller; +import java.util.List; import java.util.UUID; +import org.springframework.web.bind.annotation.DeleteMapping; 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.RequestMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; import com.shipflow.productservice.application.dto.response.StockInfoResponse; @@ -15,22 +15,26 @@ import lombok.RequiredArgsConstructor; -@RestController -@RequestMapping("/internal/companies/{companyId}/products") @RequiredArgsConstructor public class ProductInternalController { private final ProductService productService; - @GetMapping("/{productId}") + @GetMapping("/internal/companies/{companyId}/products/{productId}") public ApiResponse getStockInfo(@PathVariable UUID productId, - @RequestParam Integer quantity, @PathVariable UUID companyId) { + @RequestParam Integer quantity) { StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); return ApiResponse.ok(response); } - @PatchMapping("/deactivate") + @DeleteMapping("/internal/companies/{companyId}/products/deactivate") public ApiResponse deleteByCompany(@PathVariable UUID companyId) { productService.deleteByCompany(companyId); return ApiResponse.ok(null); } + + @DeleteMapping("/internal/companies/products/deactivate/bulk") + public ApiResponse deleteByHub(@RequestBody List companyIds) { + productService.deleteByHub(companyIds); + return ApiResponse.ok(null); + } } From c92b3c807e06195744282d66c76c05a9fb14630c Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 14:22:07 +0900 Subject: [PATCH 34/38] =?UTF-8?q?test:=20=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ProductServiceTest.java | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java index ba79aa1..fe52ebc 100644 --- a/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java +++ b/product-service/src/test/java/com/shipflow/productservice/application/service/ProductServiceTest.java @@ -20,6 +20,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import com.shipflow.common.exception.BusinessException; import com.shipflow.productservice.application.client.VendorFeignClient; @@ -45,7 +47,11 @@ class ProductServiceTest { @Spy ProductMapper mapper = Mappers.getMapper(ProductMapper.class); @Mock - VendorFeignClient vendorClient; + private VendorFeignClient vendorClient; + @Mock + private RedisTemplate redisTemplate; + @Mock + private ValueOperations valueOperations; @InjectMocks ProductService productService; @@ -60,13 +66,16 @@ void tearDown() { @Test void create() { //given - setHttpHeaders(UUID.randomUUID().toString(), "Company_Manager"); + setHttpHeaders(UUID.randomUUID().toString()); Product product = ProductFixture.create(); ProductCreateRequest request = new ProductCreateRequest(product.getName(), product.getPrice(), product.getStock(), product.getStatus()); VendorInfoResponse vendorInfo = new VendorInfoResponse(product.getCompanyId(), product.getCompanyName(), product.getHubId()); given(vendorClient.getVendorInfo(product.getCompanyId())).willReturn(vendorInfo); + given(productRepository.save(any(Product.class))).willAnswer(invocation -> invocation.getArgument(0)); + given(redisTemplate.opsForValue()).willReturn(valueOperations); + doNothing().when(valueOperations).set(anyString(), any()); //when productService.create(product.getCompanyId(), request); @@ -76,18 +85,21 @@ void create() { Product savedProduct = productCaptor.getValue(); assertThat(savedProduct.getName()).isEqualTo(product.getName()); assertThat(savedProduct.getPrice()).isEqualTo(product.getPrice()); - assertThat(savedProduct.getStockInfo().getStock()).isEqualTo(product.getStock()); + assertThat(savedProduct.getStock()).isEqualTo(product.getStock()); assertThat(savedProduct.getStatus()).isEqualTo(product.getStatus()); - assertThat(savedProduct.getVendorInfo().getCompanyId()).isEqualTo(product.getCompanyId()); - assertThat(savedProduct.getVendorInfo().getCompanyName()).isEqualTo(product.getCompanyName()); - assertThat(savedProduct.getVendorInfo().getHubId()).isEqualTo(product.getHubId()); + assertThat(savedProduct.getCompanyId()).isEqualTo(product.getCompanyId()); + assertThat(savedProduct.getCompanyName()).isEqualTo(product.getCompanyName()); + assertThat(savedProduct.getHubId()).isEqualTo(product.getHubId()); + ; } @Test void delete() { //given + setHttpHeaders(UUID.randomUUID().toString()); UUID productId = UUID.randomUUID(); Product product = ProductFixture.create(); + UUID companyId = product.getCompanyId(); given(productRepository.findById(productId)).willReturn(Optional.of(product)); //when @@ -102,9 +114,11 @@ void delete() { @Test void updateInfo() { //given - setHttpHeaders(UUID.randomUUID().toString(), "Company_Manager"); + setHttpHeaders(UUID.randomUUID().toString()); Product product=ProductFixture.create(); - ProductUpdateInfoRequest request=new ProductUpdateInfoRequest(product.getName(), product.getPrice(),null); + UUID companyId = product.getCompanyId(); + ProductUpdateInfoRequest request = new ProductUpdateInfoRequest(product.getName(), product.getPrice(), + ProductStatus.ON_SALE); given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when @@ -120,10 +134,13 @@ void updateInfo() { @Test void updateStock_성공() { //given + setHttpHeaders(UUID.randomUUID().toString()); UUID productId = UUID.randomUUID(); Product product = ProductFixture.create(); + UUID companyId = product.getCompanyId(); ProductUpdateStockRequest request = new ProductUpdateStockRequest(100); given(productRepository.findById(productId)).willReturn(Optional.of(product)); + given(redisTemplate.opsForValue()).willReturn(valueOperations); //when productService.updateStock(productId, request, companyId); @@ -137,8 +154,10 @@ void updateInfo() { @Test void updateStock_실패_잘못된_재고값_입력() { //given + setHttpHeaders(UUID.randomUUID().toString()); UUID productId = UUID.randomUUID(); Product product = ProductFixture.create(); + UUID companyId = product.getCompanyId(); ProductUpdateStockRequest request = new ProductUpdateStockRequest(-1); given(productRepository.findById(productId)).willReturn(Optional.of(product)); @@ -152,10 +171,13 @@ void updateInfo() { @Test void updateStock_재고를_0으로_설정() { //given + setHttpHeaders(UUID.randomUUID().toString()); UUID productId = UUID.randomUUID(); Product product = ProductFixture.create(); + UUID companyId = product.getCompanyId(); ProductUpdateStockRequest request = new ProductUpdateStockRequest(0); given(productRepository.findById(productId)).willReturn(Optional.of(product)); + given(redisTemplate.opsForValue()).willReturn(valueOperations); //when productService.updateStock(productId, request, companyId); @@ -169,7 +191,9 @@ void updateInfo() { @Test void getProductInfo_success() { //given + setHttpHeaders(UUID.randomUUID().toString()); Product product = ProductFixture.create(); + UUID companyId = product.getCompanyId(); given(productRepository.findById(product.getId())).willReturn(Optional.of(product)); //when @@ -185,6 +209,7 @@ void getProductInfo_success() { @Test void getProductList() { //given + setHttpHeaders(UUID.randomUUID().toString()); UUID companyId = UUID.randomUUID(); Product product = ProductFixture.create(); List products = List.of(product); @@ -226,7 +251,6 @@ void decreaseStock_success() { void decreaseStock_올바르지_않은_차감요청() { //given Product product = ProductFixture.create(); - given(productRepository.findById(any())).willReturn(Optional.of(product)); //when&then assertThatThrownBy(() -> productService.decreaseStock(product.getId().toString(), -1)) @@ -251,6 +275,7 @@ void restoreStock() { //given Product product = ProductFixture.create(); given(productRepository.findById(any())).willReturn(Optional.of(product)); + given(redisTemplate.opsForValue()).willReturn(valueOperations); //when productService.restoreStock(product.getId().toString(), 1); @@ -265,14 +290,14 @@ void restoreStock() { @Test void deleteByCompany() { //given - setHttpHeaders(UUID.randomUUID().toString(), "Master"); + setHttpHeaders(UUID.randomUUID().toString()); Product product = ProductFixture.create(); List products = List.of(product); - given(productRepository.findById(any())).willReturn(Optional.of(product)); - given(productRepository.findAllByCompanyId(any())).willReturn(products); + given(productRepository.findById(any(UUID.class))).willReturn(Optional.of(product)); + given(productRepository.findAllByCompanyId(any(UUID.class))).willReturn(products); //when - productService.deleteByCompany(product.getId()); + productService.deleteByCompany(product.getCompanyId()); //then verify(productRepository).save(productCaptor.capture()); @@ -281,8 +306,8 @@ void deleteByCompany() { } //util - private void setHttpHeaders(String userId, String role) { + private void setHttpHeaders(String userId) { UserContext.setUserId(UUID.fromString(userId)); - UserContext.setUserRole(role); + UserContext.setUserRole("MASTER"); } } From e09a05f1c4e3a659ae60fc73d1fdad04533df498 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 15:18:08 +0900 Subject: [PATCH 35/38] =?UTF-8?q?refactor:=20order=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=A3=BC=EC=86=8C=EC=97=90=EC=84=9C=20com?= =?UTF-8?q?pany=20=EC=A0=95=EB=B3=B4=20=EC=A0=9C=EC=99=B8,=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EC=82=AD=EC=A0=9C=20=EC=9A=94=EC=B2=AD=20=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20delete=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 20 ++++++++++++++----- .../controller/ProductInternalController.java | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index cf923ff..0beb2c5 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -76,9 +76,8 @@ public void delete(UUID productId, UUID companyId) { @Transactional public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfoRequest request, UUID companyId) { validateAuth(companyId); - validateProductOwnership(productId, companyId); + Product product = validateProductOwnership(productId, companyId); - Product product = findProductById(productId); product.updateInfo( request.name(), request.price(), request.status() ); @@ -89,9 +88,8 @@ public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfo @Transactional public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockRequest request, UUID companyId) { validateAuth(companyId); - validateProductOwnership(productId, companyId); + Product product = validateProductOwnership(productId, companyId); - Product product = findProductById(productId); product.updateStock(request.stock()); productRepository.save(product); @@ -121,7 +119,7 @@ public Slice getProductList(UUID companyId, Pageable pageab public void deleteByCompany(UUID companyId) { List products = productRepository.findAllByCompanyId(companyId); products.forEach(product -> { - delete(product.getId(), companyId); + internalDelete(product.getId(), companyId); redisTemplate.delete("product:stock:" + product.getId()); }); } @@ -131,6 +129,15 @@ public void deleteByHub(List companyIds) { companyIds.forEach(this::deleteByCompany); } + @Transactional + public void internalDelete(UUID productId, UUID companyId) { + UUID deleterId = UserContext.getUserId(); + Product product = findProductById(productId); + product.delete(deleterId); + productRepository.save(product); + redisTemplate.delete("product:stock:" + productId); + } + //event /* * 주문 시 product 측 흐름 : @@ -225,6 +232,9 @@ private void validateQuantitiy(Integer quantity) { private void validateAuth(UUID companyId) { validateCompanyId(companyId); + if (UserContext.getUserId() == null || UserContext.getUserRole() == null) + throw new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); + String role = UserContext.getUserRole(); if (role.equals("MASTER")) diff --git a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java index 7fb7b2e..e342e24 100644 --- a/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java +++ b/product-service/src/main/java/com/shipflow/productservice/presentation/controller/ProductInternalController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; import com.shipflow.productservice.application.dto.response.StockInfoResponse; @@ -15,11 +16,12 @@ import lombok.RequiredArgsConstructor; +@RestController @RequiredArgsConstructor public class ProductInternalController { private final ProductService productService; - @GetMapping("/internal/companies/{companyId}/products/{productId}") + @GetMapping("/internal/products/{productId}") public ApiResponse getStockInfo(@PathVariable UUID productId, @RequestParam Integer quantity) { StockInfoResponse response = productService.getStockInfoAndOccupy(productId,quantity); From 5f219ba9839ceed25ece115624d9c3f9a98e4501 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 16:34:39 +0900 Subject: [PATCH 36/38] =?UTF-8?q?refactor:=20redis=20stock=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=B2=98=EB=A6=AC=EB=A5=BC=20tra?= =?UTF-8?q?nsaction=20=EC=99=B8=EB=B6=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ProductService.java | 21 ++++++++++---- .../config/ProductCacheEventListener.java | 29 +++++++++++++++++++ .../messaging/event/DeleteStockEvent.java | 9 ++++++ .../messaging/event/UpdateStockEvent.java | 9 ++++++ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductCacheEventListener.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/DeleteStockEvent.java create mode 100644 product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/UpdateStockEvent.java diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 0beb2c5..6a782c1 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.UUID; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.redis.core.RedisTemplate; @@ -23,6 +24,9 @@ import com.shipflow.productservice.domain.exception.ProductErrorCode; import com.shipflow.productservice.domain.model.Product; import com.shipflow.productservice.domain.repository.ProductRepository; +import com.shipflow.productservice.infrastructure.messaging.config.ProductCacheEventListener; +import com.shipflow.productservice.infrastructure.messaging.event.DeleteStockEvent; +import com.shipflow.productservice.infrastructure.messaging.event.UpdateStockEvent; import com.shipflow.productservice.infrastructure.web.UserContext; import com.shipflow.productservice.presentation.dto.request.ProductCreateRequest; import com.shipflow.productservice.presentation.dto.request.ProductUpdateInfoRequest; @@ -43,6 +47,8 @@ public class ProductService { private final VendorFeignClient vendorClient; private final RedisTemplate redisTemplate; private final UserFeignClient userClient; + private final ProductCacheEventListener productCacheEventListener; + private final ApplicationEventPublisher eventPublisher; //external @Transactional @@ -57,7 +63,8 @@ public ProductCreateResponse create(UUID companyId, ProductCreateRequest request request.status(), companyId, response.name(), response.hubId(), createrId); Product savedProduct = productRepository.save(product); - redisTemplate.opsForValue().set("product:stock:" + savedProduct.getId(), savedProduct.getStock()); + + eventPublisher.publishEvent(new UpdateStockEvent(savedProduct.getId(), savedProduct.getStock())); return mapper.toCreateResponse(savedProduct); } @@ -70,7 +77,8 @@ public void delete(UUID productId, UUID companyId) { Product product = findProductById(productId); product.delete(deleterId); productRepository.save(product); - redisTemplate.delete("product:stock:" + productId); + + eventPublisher.publishEvent(new DeleteStockEvent(product.getId(), product.getStock())); } @Transactional @@ -82,6 +90,7 @@ public ProductUpdateResponse updateProductInfo(UUID productId, ProductUpdateInfo request.name(), request.price(), request.status() ); productRepository.save(product); + return mapper.toUpdateResponse(product); } @@ -93,8 +102,8 @@ public ProductUpdateResponse updateStock(UUID productId, ProductUpdateStockReque product.updateStock(request.stock()); productRepository.save(product); - redisTemplate.delete("product:stock:" + productId); - redisTemplate.opsForValue().set("product:stock:" + productId, product.getStock()); + + eventPublisher.publishEvent(new UpdateStockEvent(product.getId(), product.getStock())); return mapper.toUpdateResponse(product); } @@ -120,7 +129,6 @@ public void deleteByCompany(UUID companyId) { List products = productRepository.findAllByCompanyId(companyId); products.forEach(product -> { internalDelete(product.getId(), companyId); - redisTemplate.delete("product:stock:" + product.getId()); }); } @@ -135,7 +143,8 @@ public void internalDelete(UUID productId, UUID companyId) { Product product = findProductById(productId); product.delete(deleterId); productRepository.save(product); - redisTemplate.delete("product:stock:" + productId); + + eventPublisher.publishEvent(new DeleteStockEvent(product.getId(), product.getStock())); } //event diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductCacheEventListener.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductCacheEventListener.java new file mode 100644 index 0000000..ce5777d --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/config/ProductCacheEventListener.java @@ -0,0 +1,29 @@ +package com.shipflow.productservice.infrastructure.messaging.config; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import com.shipflow.productservice.infrastructure.messaging.event.DeleteStockEvent; +import com.shipflow.productservice.infrastructure.messaging.event.UpdateStockEvent; + +@Component +public class ProductCacheEventListener { + + private final RedisTemplate redisTemplate; + + public ProductCacheEventListener(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleStockUpdate(UpdateStockEvent event) { + redisTemplate.opsForValue().set("product:stock:" + event.productId(), event.stock()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleProductDelete(DeleteStockEvent event) { + redisTemplate.delete("product:stock:" + event.productId()); + } +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/DeleteStockEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/DeleteStockEvent.java new file mode 100644 index 0000000..0a301cd --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/DeleteStockEvent.java @@ -0,0 +1,9 @@ +package com.shipflow.productservice.infrastructure.messaging.event; + +import java.util.UUID; + +public record DeleteStockEvent( + UUID productId, + int stock +) { +} diff --git a/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/UpdateStockEvent.java b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/UpdateStockEvent.java new file mode 100644 index 0000000..56a4d83 --- /dev/null +++ b/product-service/src/main/java/com/shipflow/productservice/infrastructure/messaging/event/UpdateStockEvent.java @@ -0,0 +1,9 @@ +package com.shipflow.productservice.infrastructure.messaging.event; + +import java.util.UUID; + +public record UpdateStockEvent( + UUID productId, + int stock +) { +} From bf2573508748af62db65d26f6140a83dbecd5981 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 18:31:17 +0900 Subject: [PATCH 37/38] =?UTF-8?q?refactor:=20company-=EC=97=85=EC=B2=B4?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98=20=EC=8B=9C=20address?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/response/VendorInfoResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/company-service/src/main/java/com/shipflow/companyservice/application/dto/response/VendorInfoResponse.java b/company-service/src/main/java/com/shipflow/companyservice/application/dto/response/VendorInfoResponse.java index 92d8c82..9c64e8f 100644 --- a/company-service/src/main/java/com/shipflow/companyservice/application/dto/response/VendorInfoResponse.java +++ b/company-service/src/main/java/com/shipflow/companyservice/application/dto/response/VendorInfoResponse.java @@ -8,6 +8,7 @@ public record VendorInfoResponse( @NonNull UUID receiverCompanyId, @NotBlank String receiverCompanyName, - @NonNull UUID departureCompanyHubId + @NonNull UUID departureCompanyHubId, + @NotBlank String address ) { } From bcd7796454a31595cc79d13f844efc34216aff59 Mon Sep 17 00:00:00 2001 From: jin Date: Mon, 6 Apr 2026 22:07:03 +0900 Subject: [PATCH 38/38] =?UTF-8?q?feat:=20company)=20user=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20=EC=97=85=EC=B2=B4=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20user=20=EC=AA=BD=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=A3=BC=EC=86=8C=20=EC=88=98=EC=A0=95=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 재고 조회 중 주문량이 재고보다 많을 시 예외 발생 --- .../application/client/ProductFeignClient.java | 4 ++-- .../application/service/CompanyService.java | 12 ++++++++++-- .../controller/CompanyInternalController.java | 5 +++++ .../application/service/ProductService.java | 8 +++----- .../infrastructure/client/CompanyFeignClient.java | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/company-service/src/main/java/com/shipflow/companyservice/application/client/ProductFeignClient.java b/company-service/src/main/java/com/shipflow/companyservice/application/client/ProductFeignClient.java index d2498bf..00ab305 100644 --- a/company-service/src/main/java/com/shipflow/companyservice/application/client/ProductFeignClient.java +++ b/company-service/src/main/java/com/shipflow/companyservice/application/client/ProductFeignClient.java @@ -11,8 +11,8 @@ @FeignClient(name = "product-service") public interface ProductFeignClient { @DeleteMapping("/internal/companies/{companyId}/products/deactivate") - Void deleteProductByCompanyId(@PathVariable("companyId") UUID companyId); + Void deleteProductsByCompanyId(@PathVariable("companyId") UUID companyId); @DeleteMapping("/internal/companies/products/deactivate/bulk") - Void deleteProductsByCompanyIds(@RequestBody List companyIds); + Void deleteProductsByCompanyIdList(@RequestBody List companyIds); } diff --git a/company-service/src/main/java/com/shipflow/companyservice/application/service/CompanyService.java b/company-service/src/main/java/com/shipflow/companyservice/application/service/CompanyService.java index a8f9cc8..ce4e86f 100644 --- a/company-service/src/main/java/com/shipflow/companyservice/application/service/CompanyService.java +++ b/company-service/src/main/java/com/shipflow/companyservice/application/service/CompanyService.java @@ -61,7 +61,7 @@ public void deleteCompany(UUID companyId) { Company company = findCompanyById(companyId); company.delete(deleterId); companyRepository.save(company); - productFeignClient.deleteProductByCompanyId(companyId); + productFeignClient.deleteProductsByCompanyId(companyId); } @Transactional @@ -143,7 +143,15 @@ public void deleteProductsByHub(UUID hubId) { company.delete(UserContext.getUserId()); companyRepository.save(company); }); - productFeignClient.deleteProductsByCompanyIds(companyIds); + productFeignClient.deleteProductsByCompanyIdList(companyIds); + } + + @Transactional + public void deleteProductByUser(UUID userId) { + Company company = findCompanyByManagerId(userId); + company.delete(UserContext.getUserId()); + companyRepository.save(company); + productFeignClient.deleteProductsByCompanyId(company.getId()); } diff --git a/company-service/src/main/java/com/shipflow/companyservice/presentation/controller/CompanyInternalController.java b/company-service/src/main/java/com/shipflow/companyservice/presentation/controller/CompanyInternalController.java index 00ed07b..e62b8ce 100644 --- a/company-service/src/main/java/com/shipflow/companyservice/presentation/controller/CompanyInternalController.java +++ b/company-service/src/main/java/com/shipflow/companyservice/presentation/controller/CompanyInternalController.java @@ -29,4 +29,9 @@ public VendorInfoResponse getVendorById(@PathVariable("companyId") UUID companyI public void deleteByHub(@PathVariable("hubId") UUID hubId) { companyService.deleteProductsByHub(hubId); } + + @DeleteMapping("/user/{userId}") + public void deleteByUser(@PathVariable("userId") UUID userId) { + companyService.deleteProductByUser(userId); + } } diff --git a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java index 6a782c1..a61f5e8 100644 --- a/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java +++ b/product-service/src/main/java/com/shipflow/productservice/application/service/ProductService.java @@ -74,7 +74,7 @@ public void delete(UUID productId, UUID companyId) { validateAuth(companyId); UUID deleterId = UserContext.getUserId(); - Product product = findProductById(productId); + Product product = validateProductOwnership(productId, companyId); product.delete(deleterId); productRepository.save(product); @@ -173,10 +173,8 @@ public StockInfoResponse getStockInfoAndOccupy(@Param("productId") UUID productI .orElseThrow(() -> new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR)); if(currentStock<0){ - Long restoreStock=redisTemplate.opsForValue().increment(stockKey, (long)quantity); - return new StockInfoResponse(productId, product.getName(), product.getCompanyId(), - product.getCompanyName(), product.getHubId(), - restoreStock != null ? restoreStock.intValue() : currentStock.intValue()); + redisTemplate.opsForValue().increment(stockKey, (long)quantity); + throw new BusinessException(ProductErrorCode.EXCEEDS_STOCK_LEVEL); } redisTemplate.opsForValue().set(occupancyKey, quantity, Duration.ofSeconds(5)); diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/CompanyFeignClient.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/CompanyFeignClient.java index 6d010fa..8d46061 100644 --- a/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/CompanyFeignClient.java +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/CompanyFeignClient.java @@ -8,6 +8,6 @@ @FeignClient(name = "company-service") public interface CompanyFeignClient { - @DeleteMapping("/internal/companies/{userId}") + @DeleteMapping("/internal/companies/user/{userId}") ClientApiResponse deleteManager(@PathVariable UUID userId); }