From 285ccdfe5863bdcb75a3f5b4248776a2724f3566 Mon Sep 17 00:00:00 2001 From: Clover Song Date: Fri, 29 Nov 2024 21:42:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8D=B8=EC=9B=90=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yaml | 6 ++ .../mafoo/photo/PhotoServiceApplication.java | 2 + .../kr/mafoo/photo/config/WebFluxConfig.java | 13 ++++ .../photo/controller/AlbumController.java | 3 + .../photo/controller/SumoneController.java | 75 ++++++++++++++++--- .../dto/response/SumoneAlbumResponse.java | 17 +++++ .../dto/response/SumonePhotoResponse.java | 17 +++++ .../dto/response/SumoneSummaryResponse.java | 2 +- .../photo/repository/AlbumRepository.java | 2 + .../kr/mafoo/photo/service/AlbumQuery.java | 19 +++++ .../kr/mafoo/photo/service/AlbumService.java | 6 +- .../V10__addAlbumTypeIndexInAlbumTable.sql | 1 + 12 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneAlbumResponse.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumonePhotoResponse.java create mode 100644 photo-service/src/main/resources/db/migration/V10__addAlbumTypeIndexInAlbumTable.sql diff --git a/api-gateway/src/main/resources/application.yaml b/api-gateway/src/main/resources/application.yaml index 367aeaa..bbf3744 100644 --- a/api-gateway/src/main/resources/application.yaml +++ b/api-gateway/src/main/resources/application.yaml @@ -14,6 +14,12 @@ spring: allowedHeaders: '*' allowedMethods: '*' routes: + - id: sumone-event + uri: http://photo-service + predicates: + - Path=/sumone/** + filters: + - RewritePath=/sumone/(?/?.*), /$\{segment} - id: admin-service uri: http://admin-service predicates: diff --git a/photo-service/src/main/java/kr/mafoo/photo/PhotoServiceApplication.java b/photo-service/src/main/java/kr/mafoo/photo/PhotoServiceApplication.java index 5a2b68d..156c518 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/PhotoServiceApplication.java +++ b/photo-service/src/main/java/kr/mafoo/photo/PhotoServiceApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class PhotoServiceApplication { diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java index 1be8590..fd0f2df 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebFluxConfig.java @@ -1,5 +1,9 @@ package kr.mafoo.photo.config; +import java.util.List; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.config.EnableWebFlux; @@ -10,6 +14,7 @@ @EnableWebFlux @Configuration +@EnableCaching public class WebFluxConfig implements WebFluxConfigurer { @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { @@ -29,4 +34,12 @@ public WebClient externalServiceWebClient() { }) .build(); } + + @Bean + public CacheManager cacheManager() { + ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); + cacheManager.setAllowNullValues(false); + cacheManager.setCacheNames(List.of("albumCount")); + return cacheManager; + } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java index d2c9545..6fe41d1 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java @@ -50,6 +50,9 @@ public Mono createAlbum( String memberId, AlbumCreateRequest request ){ + if (request.name().equals("SUMONE")) { + throw new RuntimeException(); // should not be happened + } return albumService .addAlbum(request.name(), request.type(), memberId) .map(AlbumResponse::fromEntity); diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/SumoneController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/SumoneController.java index ebdb6ed..46abe69 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/SumoneController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/SumoneController.java @@ -2,14 +2,22 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import kr.mafoo.photo.annotation.RequestMemberId; +import java.util.concurrent.atomic.AtomicInteger; import kr.mafoo.photo.controller.dto.request.ObjectStoragePreSignedUrlRequest; -import kr.mafoo.photo.controller.dto.response.AlbumResponse; -import kr.mafoo.photo.controller.dto.response.PhotoResponse; import kr.mafoo.photo.controller.dto.response.PreSignedUrlResponse; +import kr.mafoo.photo.controller.dto.response.SumoneAlbumResponse; import kr.mafoo.photo.controller.dto.response.SumoneBulkUrlRequest; +import kr.mafoo.photo.controller.dto.response.SumonePhotoResponse; import kr.mafoo.photo.controller.dto.response.SumoneSummaryResponse; import kr.mafoo.photo.domain.enums.AlbumType; +import kr.mafoo.photo.domain.enums.BrandType; +import kr.mafoo.photo.exception.AlbumNotFoundException; +import kr.mafoo.photo.exception.PhotoNotFoundException; +import kr.mafoo.photo.service.AlbumCommand; +import kr.mafoo.photo.service.AlbumQuery; +import kr.mafoo.photo.service.ObjectStorageService; +import kr.mafoo.photo.service.PhotoCommand; +import kr.mafoo.photo.service.PhotoQuery; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -27,33 +35,69 @@ @RestController @RequestMapping("/v1/sumone") public class SumoneController { + private final static String sumoneAlbumCommonName = "2024 연말 결산 이벤트"; + private final static String sumoneAlbumCommonMemberId = "01JDVYCPARAM63X560YSNDXQ4S"; + + private final PhotoCommand photoCommand; + private final AlbumQuery albumQuery; + private final PhotoQuery photoQuery; + private final AlbumCommand albumCommand; + private final ObjectStorageService objectStorageService; + @Operation(summary = "통계 api") @GetMapping("/summary") - public SumoneSummaryResponse getSummary() { - return new SumoneSummaryResponse(1024); + public Mono getSummary() { + return albumQuery + .countAlbumByAlbumType(AlbumType.SUMONE) + .map(SumoneSummaryResponse::new); } @Operation(summary = "앨범 생성 api") @PostMapping("/albums") - public Mono createAlbum() { - return Mono.just(new AlbumResponse("test_album_id", "야뿌들", AlbumType.SUMONE, "6")); + public Mono createAlbum() { + return albumCommand + .addAlbum(sumoneAlbumCommonName, "SUMONE", sumoneAlbumCommonMemberId) + .map(SumoneAlbumResponse::fromEntity); } @Operation(summary = "앨범에 이미지 url 추가") @PostMapping("/albums/{albumId}/photos") - public Flux addPhotos( + public Flux addPhotos( @PathVariable String albumId, @RequestBody SumoneBulkUrlRequest request ) { - return Flux.empty(); + return albumQuery.findById(albumId).flatMapMany(album -> { + if(album.getType() != AlbumType.SUMONE) { + return Mono.error(new AlbumNotFoundException()); + } + AtomicInteger displayIndex = new AtomicInteger(album.getPhotoCount()); + return Flux + .fromArray(request.fileUrls()) + .concatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) + .flatMap(fileLink -> photoCommand.addPhoto(fileLink, BrandType.EXTERNAL, albumId, displayIndex.getAndIncrement(), album.getOwnerMemberId())) + ) + .collectList() + .flatMapMany(addedPhotos -> + albumCommand.increaseAlbumPhotoCount(album, addedPhotos.size()) + .thenMany(Flux.fromIterable(addedPhotos)) + ); + }).map(SumonePhotoResponse::fromEntity); } @Operation(summary = "앨범에 이미지 조회") @GetMapping("/albums/{albumId}/photos") - public Flux getPhotos( + public Flux getPhotos( @PathVariable String albumId ) { - return Flux.empty(); + return albumQuery.findById(albumId).flatMapMany(album -> { + if(album.getType() != AlbumType.SUMONE) { + return Mono.error(new AlbumNotFoundException()); + } + //TODO: add timeout + return photoQuery + .findAllByAlbumIdOrderByCreatedAtDesc(albumId) + .onErrorResume(PhotoNotFoundException.class, ex -> Flux.empty()); + }).map(SumonePhotoResponse::fromEntity); } @Operation(summary = "Pre-signed Url 요청", description = "Pre-signed Url 목록을 발급합니다.") @@ -63,6 +107,13 @@ Mono createPreSignedUrls( @RequestBody ObjectStoragePreSignedUrlRequest request ) { - return Mono.empty(); + return albumQuery.findById(albumId).flatMap(album -> { + if (album.getType() != AlbumType.SUMONE) { + return Mono.error(new AlbumNotFoundException()); + } + return objectStorageService + .createPreSignedUrls(request.fileNames(), album.getOwnerMemberId()) + .map(PreSignedUrlResponse::fromStringArray); + }); } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneAlbumResponse.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneAlbumResponse.java new file mode 100644 index 0000000..3c4f744 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneAlbumResponse.java @@ -0,0 +1,17 @@ +package kr.mafoo.photo.controller.dto.response; + + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.domain.AlbumEntity; + +@Schema(description = "앨범 응답") +public record SumoneAlbumResponse( + @Schema(description = "앨범 ID", example = "test_album_id") + String albumId +) { + public static SumoneAlbumResponse fromEntity(AlbumEntity entity) { + return new SumoneAlbumResponse( + entity.getAlbumId() + ); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumonePhotoResponse.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumonePhotoResponse.java new file mode 100644 index 0000000..23538f8 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumonePhotoResponse.java @@ -0,0 +1,17 @@ +package kr.mafoo.photo.controller.dto.response; + + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.domain.PhotoEntity; + +@Schema(description = "앨범 응답") +public record SumonePhotoResponse( + @Schema(description = "사진 URL", example = "photo_url") + String photoUrl +) { + public static SumonePhotoResponse fromEntity(PhotoEntity entity) { + return new SumonePhotoResponse( + entity.getPhotoUrl() + ); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneSummaryResponse.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneSummaryResponse.java index af559f3..482f64a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneSummaryResponse.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/SumoneSummaryResponse.java @@ -5,7 +5,7 @@ @Schema(description = "리캡 응답") public record SumoneSummaryResponse( @Schema(description = "사용자 수", example = "1565") - int userCount + Long userCount ){ } diff --git a/photo-service/src/main/java/kr/mafoo/photo/repository/AlbumRepository.java b/photo-service/src/main/java/kr/mafoo/photo/repository/AlbumRepository.java index 9d3b7ad..6bbbe4a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/repository/AlbumRepository.java +++ b/photo-service/src/main/java/kr/mafoo/photo/repository/AlbumRepository.java @@ -32,4 +32,6 @@ public interface AlbumRepository extends R2dbcRepository { Flux findAllByTypeOrderByAlbumIdDesc(AlbumType type, Pageable pageable); Flux findAllByOwnerMemberIdOrderByAlbumIdDesc(String ownerMemberId, Pageable pageable); + + Mono countAlbumEntityByType(AlbumType albumType); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumQuery.java b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumQuery.java index 2f272a3..da0bc6d 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumQuery.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumQuery.java @@ -1,9 +1,15 @@ package kr.mafoo.photo.service; +import java.time.Duration; import kr.mafoo.photo.domain.AlbumEntity; +import kr.mafoo.photo.domain.enums.AlbumType; import kr.mafoo.photo.exception.AlbumNotFoundException; import kr.mafoo.photo.repository.AlbumRepository; import lombok.RequiredArgsConstructor; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -23,4 +29,17 @@ public Flux findByMemberId(String memberId) { return albumRepository.findAllByOwnerMemberIdOrderByDisplayIndex(memberId) .switchIfEmpty(Mono.error(new AlbumNotFoundException())); } + + @Cacheable(value = "albumCount", key = "#albumType") + public Mono countAlbumByAlbumType(AlbumType albumType) { + return albumRepository + .countAlbumEntityByType(albumType) + .cache(); + } + + @CacheEvict(value = "albumCount", allEntries = true) + @Scheduled(fixedDelay = 1000 * 10) + public void clearAlbumCountCache() { + LoggerFactory.getLogger(AlbumQuery.class).debug("clear album count cache"); + } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java index ea62ba1..8e3ea2a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/AlbumService.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.Optional; import kr.mafoo.photo.domain.AlbumEntity; +import kr.mafoo.photo.domain.enums.AlbumType; import kr.mafoo.photo.exception.AlbumNotFoundException; import kr.mafoo.photo.exception.AlbumOwnerChangeDeniedException; import kr.mafoo.photo.exception.SharedMemberNotFoundException; @@ -105,4 +106,7 @@ public Mono removeAlbum(String albumId, String requestMemberId) { .flatMap(albumCommand::removeAlbum); } -} \ No newline at end of file + public Mono countAlbumByAlbumType(AlbumType albumType) { + return albumQuery.countAlbumByAlbumType(albumType); + } +} diff --git a/photo-service/src/main/resources/db/migration/V10__addAlbumTypeIndexInAlbumTable.sql b/photo-service/src/main/resources/db/migration/V10__addAlbumTypeIndexInAlbumTable.sql new file mode 100644 index 0000000..ec03408 --- /dev/null +++ b/photo-service/src/main/resources/db/migration/V10__addAlbumTypeIndexInAlbumTable.sql @@ -0,0 +1 @@ +ALTER TABLE album ADD INDEX idx_album_type (type);