diff --git a/src/main/java/com/dailyon/auctionservice/controller/AuctionAdminController.java b/src/main/java/com/dailyon/auctionservice/controller/AuctionAdminController.java index 53f8aee..ff5fd21 100644 --- a/src/main/java/com/dailyon/auctionservice/controller/AuctionAdminController.java +++ b/src/main/java/com/dailyon/auctionservice/controller/AuctionAdminController.java @@ -2,11 +2,17 @@ import com.dailyon.auctionservice.dto.request.CreateAuctionRequest; import com.dailyon.auctionservice.dto.response.CreateAuctionResponse; +import com.dailyon.auctionservice.dto.response.ReadAuctionPageResponse; import com.dailyon.auctionservice.facade.AuctionFacade; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -22,4 +28,9 @@ public class AuctionAdminController { public ResponseEntity createAuction(@Valid @RequestBody CreateAuctionRequest createAuctionRequest) { return ResponseEntity.status(HttpStatus.CREATED).body(auctionFacade.createAuction(createAuctionRequest)); } + + @GetMapping("/auction") + public Mono readAuctions(Pageable pageable) { + return Mono.just(auctionFacade.readAuctions(pageable)); + } } diff --git a/src/main/java/com/dailyon/auctionservice/document/Auction.java b/src/main/java/com/dailyon/auctionservice/document/Auction.java index 0846b6b..249bff8 100644 --- a/src/main/java/com/dailyon/auctionservice/document/Auction.java +++ b/src/main/java/com/dailyon/auctionservice/document/Auction.java @@ -14,7 +14,7 @@ @NoArgsConstructor @AllArgsConstructor @DynamoDBTable(tableName = "auctions") -public class Auction { +public class Auction implements Comparable { @Id @DynamoDBHashKey @DynamoDBAutoGeneratedKey @@ -46,6 +46,11 @@ public class Auction { @Builder.Default private boolean isEnded = false; + @DynamoDBTypeConverted(converter = DynamoDbConfig.LocalDateTimeConverter.class) + @DynamoDBAttribute(attributeName = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + public static Auction create( Long auctionProductId, String auctionName, @@ -61,4 +66,9 @@ public static Auction create( .startAt(startAt) .build(); } + + @Override + public int compareTo(Auction o) { + return o.getCreatedAt().compareTo(this.getCreatedAt()); + } } diff --git a/src/main/java/com/dailyon/auctionservice/dto/response/ReadAuctionPageResponse.java b/src/main/java/com/dailyon/auctionservice/dto/response/ReadAuctionPageResponse.java new file mode 100644 index 0000000..1a31001 --- /dev/null +++ b/src/main/java/com/dailyon/auctionservice/dto/response/ReadAuctionPageResponse.java @@ -0,0 +1,60 @@ +package com.dailyon.auctionservice.dto.response; + +import com.dailyon.auctionservice.document.Auction; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReadAuctionPageResponse { + private long totalElements; + private int totalPages; + private List responses; + + public static ReadAuctionPageResponse of(Page auctions) { + return ReadAuctionPageResponse.builder() + .totalPages(auctions.getTotalPages()) + .totalElements(auctions.getTotalElements()) + .responses(auctions.stream() + .map(ReadAuctionResponse::of) + .collect(Collectors.toList())) + .build(); + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ReadAuctionResponse { + private String id; + private Long auctionProductId; + private String auctionName; + private Integer startBidPrice; + private Integer maximumWinner; + private LocalDateTime startAt; + private boolean isStarted; + private boolean isEnded; + + public static ReadAuctionResponse of(Auction auction) { + return ReadAuctionResponse.builder() + .id(auction.getId()) + .auctionProductId(auction.getAuctionProductId()) + .auctionName(auction.getAuctionName()) + .startBidPrice(auction.getStartBidPrice()) + .maximumWinner(auction.getMaximumWinner()) + .startAt(auction.getStartAt()) + .isStarted(auction.isStarted()) + .isEnded(auction.isEnded()) + .build(); + } + } +} diff --git a/src/main/java/com/dailyon/auctionservice/facade/AuctionFacade.java b/src/main/java/com/dailyon/auctionservice/facade/AuctionFacade.java index 783f3c9..b2c348d 100644 --- a/src/main/java/com/dailyon/auctionservice/facade/AuctionFacade.java +++ b/src/main/java/com/dailyon/auctionservice/facade/AuctionFacade.java @@ -5,10 +5,13 @@ import com.dailyon.auctionservice.document.Auction; import com.dailyon.auctionservice.dto.request.CreateAuctionRequest; import com.dailyon.auctionservice.dto.response.CreateAuctionResponse; +import com.dailyon.auctionservice.dto.response.ReadAuctionPageResponse; import com.dailyon.auctionservice.service.AuctionService; import feign.FeignException; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; import java.util.List; @@ -31,4 +34,8 @@ public CreateAuctionResponse createAuction(CreateAuctionRequest createAuctionReq } return CreateAuctionResponse.create(auction, productResponse); } + + public ReadAuctionPageResponse readAuctions(Pageable pageable) { + return ReadAuctionPageResponse.of(auctionService.readAuctions(pageable)); + } } diff --git a/src/main/java/com/dailyon/auctionservice/repository/AuctionRepository.java b/src/main/java/com/dailyon/auctionservice/repository/AuctionRepository.java index 458291b..aad16eb 100644 --- a/src/main/java/com/dailyon/auctionservice/repository/AuctionRepository.java +++ b/src/main/java/com/dailyon/auctionservice/repository/AuctionRepository.java @@ -4,6 +4,9 @@ import org.socialsignin.spring.data.dynamodb.repository.EnableScan; import org.springframework.data.repository.CrudRepository; +/* Caused by: com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: Auction[created_at]; no HASH key for GSI auction_sort_idx + https://stackoverflow.com/questions/68067091/sorting-not-supported-for-scan-expressions-and-no-hash-key-for-gsi-for-dynamodbp + */ @EnableScan public interface AuctionRepository extends CrudRepository { diff --git a/src/main/java/com/dailyon/auctionservice/service/AuctionService.java b/src/main/java/com/dailyon/auctionservice/service/AuctionService.java index 66c3e77..e67b378 100644 --- a/src/main/java/com/dailyon/auctionservice/service/AuctionService.java +++ b/src/main/java/com/dailyon/auctionservice/service/AuctionService.java @@ -5,9 +5,16 @@ import com.dailyon.auctionservice.dto.request.CreateAuctionRequest; import com.dailyon.auctionservice.repository.AuctionRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -26,4 +33,26 @@ public Auction create(CreateAuctionRequest auctionRequest, CreateProductResponse ) ); } + + public Page readAuctions(Pageable pageable) { + int currentPage = pageable.getPageNumber(); + int pageSize = pageable.getPageSize(); + + int startIdx = currentPage * pageSize; + int endIdx = startIdx + pageSize; + + List auctions = (List) auctionRepository.findAll(); + if(auctions.isEmpty()) { + return new PageImpl<>(new ArrayList<>(), pageable, 0); + } + + int totalSize = auctions.size(); + + List sorted = auctions.stream() + .sorted(Auction::compareTo) + .collect(Collectors.toList()) + .subList(startIdx, Math.min(endIdx, totalSize)); + + return new PageImpl<>(sorted, pageable, totalSize); + } } diff --git a/src/test/java/com/dailyon/auctionservice/repository/AuctionRepositoryTests.java b/src/test/java/com/dailyon/auctionservice/repository/AuctionRepositoryTests.java index 4731b11..75e7952 100644 --- a/src/test/java/com/dailyon/auctionservice/repository/AuctionRepositoryTests.java +++ b/src/test/java/com/dailyon/auctionservice/repository/AuctionRepositoryTests.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.time.LocalDateTime; +import java.util.List; class AuctionRepositoryTests extends ContainerBaseTestSupport { @Autowired private AmazonDynamoDBAsync dynamoDB; @@ -58,5 +59,25 @@ void createAuctionTest() { assertEquals(5, created.getMaximumWinner()); assertFalse(created.isEnded()); assertNotNull(created.getId()); + assertNotNull(created.getCreatedAt()); + } + + @Test + @DisplayName("경매 전체 목록 조회") + void readAuctionPageTest() { + for(int i=0; i<5; i++) { + auctionRepository.save(Auction.builder() + .auctionProductId((long) i) + .auctionName("TEST_"+i) + .startBidPrice(1000) + .maximumWinner(5) + .startAt(LocalDateTime.now()) + .build() + ); + } + + List auctions = (List) auctionRepository.findAll(); + + assertEquals(5, auctions.size()); } } diff --git a/src/test/java/com/dailyon/auctionservice/service/AuctionServiceTests.java b/src/test/java/com/dailyon/auctionservice/service/AuctionServiceTests.java new file mode 100644 index 0000000..2691d4b --- /dev/null +++ b/src/test/java/com/dailyon/auctionservice/service/AuctionServiceTests.java @@ -0,0 +1,76 @@ +package com.dailyon.auctionservice.service; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.util.TableUtils; +import com.dailyon.auctionservice.ContainerBaseTestSupport; +import com.dailyon.auctionservice.document.Auction; +import com.dailyon.auctionservice.repository.AuctionRepository; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.stream.IntStream; + +public class AuctionServiceTests extends ContainerBaseTestSupport { + @Autowired private AmazonDynamoDBAsync dynamoDB; + @Autowired private DynamoDBMapper dynamoDBMapper; + @Autowired private AuctionRepository auctionRepository; + @Autowired private AuctionService auctionService; + + @BeforeEach + void beforeEach() { + CreateTableRequest createTableRequest = dynamoDBMapper + .generateCreateTableRequest(Auction.class) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)); + + TableUtils.createTableIfNotExists(dynamoDB, createTableRequest); + } + + @AfterEach + void afterEach() { + TableUtils.deleteTableIfExists( + dynamoDB, + dynamoDBMapper.generateDeleteTableRequest(Auction.class) + ); + } + + @Test + @DisplayName("경매 목록 생성 내림차순 기준 정렬 페이지네이션 조회") + void paginationTest() { + for(int i=0; i<10; i++) { + auctionRepository.save(Auction.builder() + .auctionProductId((long) i) + .auctionName("TEST_"+i) + .startBidPrice(1000) + .maximumWinner(5) + .startAt(LocalDateTime.now()) + .build() + ); + } + + Page auctions = auctionService.readAuctions(PageRequest.of(0, 5)); + assertEquals(10, auctions.getTotalElements()); + assertEquals(2, auctions.getTotalPages()); + assertEquals(5, auctions.getContent().size()); + IntStream.range(1, 5).forEach(i -> { + Auction prev = auctions.getContent().get(i-1); + Auction next = auctions.getContent().get(i); + + BDDAssertions + .then(prev.getCreatedAt().isAfter(next.getCreatedAt())) + .isTrue(); + }); + } +}