From 6c78fcfd7b6f015df341674920f275f5acb1caa1 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Tue, 3 Sep 2024 22:00:22 +0900 Subject: [PATCH 01/72] feat: implement photo sort method in findAllByAlbumId --- .../kr/mafoo/photo/repository/PhotoRepository.java | 4 ++++ .../main/java/kr/mafoo/photo/service/PhotoService.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/repository/PhotoRepository.java b/photo-service/src/main/java/kr/mafoo/photo/repository/PhotoRepository.java index 981b69f7..1d65cfb0 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/repository/PhotoRepository.java +++ b/photo-service/src/main/java/kr/mafoo/photo/repository/PhotoRepository.java @@ -10,6 +10,10 @@ public interface PhotoRepository extends R2dbcRepository { Flux findAllByAlbumIdOrderByDisplayIndexDesc(String ownerAlbumId); + Flux findAllByAlbumIdOrderByCreatedAtDesc(String ownerAlbumId); + + Flux findAllByAlbumIdOrderByCreatedAtAsc(String ownerAlbumId); + @Modifying @Query("UPDATE photo SET display_index = display_index - 1 WHERE album_id = :albumId AND display_index > :startIndex") Mono popDisplayIndexGreaterThan(String albumId, int startIndex); diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index e25fd207..5cd638de 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -70,7 +70,7 @@ public Flux uploadPhoto(Flux files, String requestMemberI ).sequential(); } - public Flux findAllByAlbumId(String albumId, String requestMemberId) { + public Flux findAllByAlbumId(String albumId, String requestMemberId, String sort) { return albumRepository .findById(albumId) .switchIfEmpty(Mono.error(new AlbumNotFoundException())) @@ -79,7 +79,13 @@ public Flux findAllByAlbumId(String albumId, String requestMemberId // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { - return photoRepository.findAllByAlbumIdOrderByDisplayIndexDesc(albumId); + String sortMethod = (sort == null) ? "CUSTOM" : sort.toUpperCase(); + + return switch (sortMethod) { + case "ASC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtAsc(albumId); + case "DESC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtDesc(albumId); + default -> photoRepository.findAllByAlbumIdOrderByDisplayIndexDesc(albumId); + }; } }); } From c4c354bed48eb04b1fde07eb49f084afa9916c4d Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Tue, 3 Sep 2024 22:01:37 +0900 Subject: [PATCH 02/72] feat: add photo sort parameter in get photos api --- .../src/main/java/kr/mafoo/photo/api/PhotoApi.java | 6 +++++- .../java/kr/mafoo/photo/controller/PhotoController.java | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java index f6567474..9316edbb 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java @@ -31,7 +31,11 @@ Flux getPhotos( @ULID @Parameter(description = "앨범 ID", example = "test_album_id") @RequestParam - String albumId + String albumId, + + @Parameter(description = "정렬 종류", example = "ASC | DESC") + @RequestParam(required = false) + String sort ); @Operation(summary = "사진 생성", description = "사진을 생성합니다.") diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index 62a3bea5..bb95900f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -22,10 +22,11 @@ public class PhotoController implements PhotoApi { @Override public Flux getPhotos( String memberId, - String albumId + String albumId, + String sort ){ return photoService - .findAllByAlbumId(albumId, memberId) + .findAllByAlbumId(albumId, memberId, sort) .map(PhotoResponse::fromEntity); } From e07d54ef7e2e7986e754a326b911db8bc5d7f5a9 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 7 Sep 2024 20:12:09 +0900 Subject: [PATCH 03/72] refactor: change handleExceptionInternal to send requestBody info --- .../photo/handler/GlobalExceptionHandler.java | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java index 1ba2d332..58d2bdbe 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java +++ b/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -13,6 +14,8 @@ import reactor.core.publisher.Mono; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Optional; @ControllerAdvice @RequiredArgsConstructor @@ -21,36 +24,70 @@ public class GlobalExceptionHandler { private final SlackService slackService; @ExceptionHandler(ResponseStatusException.class) - public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException ex) { - return handleExceptionInternal(exchange, ex, (HttpStatus) ex.getStatusCode()); + public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException exception) { + return handleExceptionInternal(exchange, exception, (HttpStatus) exception.getStatusCode()); } @ExceptionHandler(Exception.class) - public Mono> handleGenericException(ServerWebExchange exchange, Exception ex) { - return handleExceptionInternal(exchange, ex, HttpStatus.INTERNAL_SERVER_ERROR); + public Mono> handleGenericException(ServerWebExchange exchange, Exception exception) { + return handleExceptionInternal(exchange, exception, HttpStatus.INTERNAL_SERVER_ERROR); } - private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception ex, HttpStatus status) { - String method = exchange.getRequest().getMethod().toString(); - String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); + private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception exception, HttpStatus status) { + String method = extractMethod(exchange); + String userAgent = extractUserAgent(exchange); + String fullPath = extractFullPath(exchange); + String originIp = extractOriginIp(exchange); + + return extractRequestBody(exchange).flatMap(requestBody -> { + + logException(method, fullPath, originIp, userAgent, exception); + + if (status == HttpStatus.INTERNAL_SERVER_ERROR) { + return slackService.sendErrorNotification( + method, fullPath, requestBody, originIp, userAgent, exception.getMessage() + ).then(Mono.just(new ResponseEntity<>("Internal Server Error", status))); + } + + return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status)); + }); + } + + private String extractMethod(ServerWebExchange exchange) { + return exchange.getRequest().getMethod().toString(); + } + + private String extractUserAgent(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().getFirst("User-Agent"); + } + + private String extractFullPath(ServerWebExchange exchange) { + var request = exchange.getRequest(); + String fullPath = request.getURI().getRawPath(); + String query = request.getURI().getQuery(); + return (query != null && !query.isEmpty()) ? fullPath + "?" + query : fullPath; + } + + private String extractOriginIp(ServerWebExchange exchange) { String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); - InetSocketAddress address = exchange.getRequest().getRemoteAddress(); - String originIp = proxyIp != null ? proxyIp : (address != null ? address.toString() : "UNKNOWN SOURCE"); - String fullPath = exchange.getRequest().getURI().getPath() + - (exchange.getRequest().getURI().getQuery() != null ? "?" + exchange.getRequest().getURI().getQuery() : ""); + InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); + return Optional.ofNullable(proxyIp) + .orElseGet(() -> remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "UNKNOWN SOURCE"); + } - logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, ex.getMessage(), userAgent); + private Mono extractRequestBody(ServerWebExchange exchange) { + return exchange.getRequest().getBody().map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); - if (status == HttpStatus.INTERNAL_SERVER_ERROR) { - return slackService.sendErrorNotification( - method, - fullPath, - originIp, - userAgent, - ex.getMessage() - ).then(Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR))); - } + return new String(bytes, StandardCharsets.UTF_8); + }).reduce(new StringBuilder(), StringBuilder::append) + .map(StringBuilder::toString); + } - return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status)); + private void logException(String method, String fullPath, String originIp, String userAgent, Exception exception) { + logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, exception.getMessage(), userAgent); } + } \ No newline at end of file From 83249d2ecb22506273f4b336f66d8d37b6de1d28 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 8 Sep 2024 22:14:37 +0900 Subject: [PATCH 04/72] refactor: create sendQrRelatedErrorNotification in SlackService --- .../kr/mafoo/photo/service/SlackService.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/SlackService.java b/photo-service/src/main/java/kr/mafoo/photo/service/SlackService.java index 7f7ced03..de062638 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/SlackService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/SlackService.java @@ -20,15 +20,15 @@ @RequiredArgsConstructor public class SlackService { - @Value(value = "${slack.webhook.token}") - private String token; - @Value(value = "${slack.webhook.channel.error}") private String errorChannel; + @Value(value = "${slack.webhook.channel.qr}") + private String qrErrorChannel; + private final MethodsClient methodsClient; - public Mono sendErrorNotification(String method, String uri, String originIp, String userAgent, String message) { + public Mono sendNotification(String channel, String headerText, String method, String uri, String requestBody, String originIp, String userAgent, String message) { return Mono.fromCallable(() -> { List layoutBlocks = new ArrayList<>(); @@ -36,7 +36,7 @@ public Mono sendErrorNotification(String method, String uri, String origin layoutBlocks.add( Blocks.header( headerBlockBuilder -> - headerBlockBuilder.text(plainText("🚨 예상하지 못한 에러 발생")) + headerBlockBuilder.text(plainText(headerText)) ) ); @@ -55,6 +55,15 @@ public Mono sendErrorNotification(String method, String uri, String origin ) ); + MarkdownTextObject requestBodyMarkdown = + MarkdownTextObject.builder().text("`요청 바디`\n" + requestBody).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(requestBodyMarkdown)) + ) + ); + MarkdownTextObject errorOriginIpMarkdown = MarkdownTextObject.builder().text("`에러 발생 IP`\n" + originIp).build(); @@ -79,8 +88,8 @@ public Mono sendErrorNotification(String method, String uri, String origin ChatPostMessageRequest chatPostMessageRequest = ChatPostMessageRequest .builder() - .text("예상하지 못한 에러 발생 알림") - .channel(errorChannel) + .text("에러 발생 알림") + .channel(channel) // 동적으로 채널 선택 .blocks(layoutBlocks) .build(); @@ -89,4 +98,11 @@ public Mono sendErrorNotification(String method, String uri, String origin }).then(); } + public Mono sendErrorNotification(String method, String uri, String requestBody, String originIp, String userAgent, String message) { + return sendNotification(errorChannel, "🚨 예상하지 못한 에러 발생", method, uri, requestBody, originIp, userAgent, message); + } + + public Mono sendQrRelatedErrorNotification(String method, String uri, String requestBody, String originIp, String userAgent, String message) { + return sendNotification(qrErrorChannel, "📸 지원하지 않는 QR 브랜드 에러 발생", method, uri, requestBody, originIp, userAgent, message); + } } From 40b35096e128a7b1ece815bf9f058b380ef10da1 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Tue, 10 Sep 2024 22:31:43 +0900 Subject: [PATCH 05/72] refactor: combine GlobalExceptionHandler to WebExceptionHandler --- .../photo/config/WebExceptionHandler.java | 85 +++++++++++++++++ .../photo/handler/GlobalExceptionHandler.java | 93 ------------------- 2 files changed, 85 insertions(+), 93 deletions(-) delete mode 100644 photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java index 816fb53e..328b3338 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java @@ -4,14 +4,32 @@ import kr.mafoo.photo.controller.dto.response.ErrorResponse; import kr.mafoo.photo.exception.DomainException; import kr.mafoo.photo.exception.ErrorCode; +import kr.mafoo.photo.service.SlackService; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Optional; @RestControllerAdvice +@RequiredArgsConstructor public class WebExceptionHandler { + + private final Logger logger = LoggerFactory.getLogger(WebExceptionHandler.class); + private final SlackService slackService; + @ExceptionHandler(DomainException.class) public ResponseEntity handleDomainException(DomainException exception) { return ResponseEntity @@ -42,4 +60,71 @@ public ResponseEntity validException(Exception ex) { .badRequest() .body(response); } + + @ExceptionHandler(ResponseStatusException.class) + public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException exception) { + return handleExceptionInternal(exchange, exception, (HttpStatus) exception.getStatusCode()); + } + + @ExceptionHandler(Exception.class) + public Mono> handleGenericException(ServerWebExchange exchange, Exception exception) { + return handleExceptionInternal(exchange, exception, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception exception, HttpStatus status) { + String method = extractMethod(exchange); + String userAgent = extractUserAgent(exchange); + String fullPath = extractFullPath(exchange); + String originIp = extractOriginIp(exchange); + + return extractRequestBody(exchange).flatMap(requestBody -> { + + logException(method, fullPath, originIp, userAgent, exception); + + if (status == HttpStatus.INTERNAL_SERVER_ERROR) { + return slackService.sendErrorNotification( + method, fullPath, requestBody, originIp, userAgent, exception.getMessage() + ).then(Mono.just(new ResponseEntity<>("Internal Server Error", status))); + } + + return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status)); + }); + } + + private String extractMethod(ServerWebExchange exchange) { + return exchange.getRequest().getMethod().toString(); + } + + private String extractUserAgent(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().getFirst("User-Agent"); + } + + private String extractFullPath(ServerWebExchange exchange) { + var request = exchange.getRequest(); + String fullPath = request.getURI().getRawPath(); + String query = request.getURI().getQuery(); + return (query != null && !query.isEmpty()) ? fullPath + "?" + query : fullPath; + } + + private String extractOriginIp(ServerWebExchange exchange) { + String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); + InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); + return Optional.ofNullable(proxyIp) + .orElseGet(() -> remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "UNKNOWN SOURCE"); + } + + private Mono extractRequestBody(ServerWebExchange exchange) { + return exchange.getRequest().getBody().map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + + return new String(bytes, StandardCharsets.UTF_8); + }).reduce(new StringBuilder(), StringBuilder::append) + .map(StringBuilder::toString); + } + + private void logException(String method, String fullPath, String originIp, String userAgent, Exception exception) { + logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, exception.getMessage(), userAgent); + } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java deleted file mode 100644 index 58d2bdbe..00000000 --- a/photo-service/src/main/java/kr/mafoo/photo/handler/GlobalExceptionHandler.java +++ /dev/null @@ -1,93 +0,0 @@ -package kr.mafoo.photo.handler; - -import kr.mafoo.photo.service.SlackService; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; - -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -@ControllerAdvice -@RequiredArgsConstructor -public class GlobalExceptionHandler { - private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - private final SlackService slackService; - - @ExceptionHandler(ResponseStatusException.class) - public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException exception) { - return handleExceptionInternal(exchange, exception, (HttpStatus) exception.getStatusCode()); - } - - @ExceptionHandler(Exception.class) - public Mono> handleGenericException(ServerWebExchange exchange, Exception exception) { - return handleExceptionInternal(exchange, exception, HttpStatus.INTERNAL_SERVER_ERROR); - } - - private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception exception, HttpStatus status) { - String method = extractMethod(exchange); - String userAgent = extractUserAgent(exchange); - String fullPath = extractFullPath(exchange); - String originIp = extractOriginIp(exchange); - - return extractRequestBody(exchange).flatMap(requestBody -> { - - logException(method, fullPath, originIp, userAgent, exception); - - if (status == HttpStatus.INTERNAL_SERVER_ERROR) { - return slackService.sendErrorNotification( - method, fullPath, requestBody, originIp, userAgent, exception.getMessage() - ).then(Mono.just(new ResponseEntity<>("Internal Server Error", status))); - } - - return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status)); - }); - } - - private String extractMethod(ServerWebExchange exchange) { - return exchange.getRequest().getMethod().toString(); - } - - private String extractUserAgent(ServerWebExchange exchange) { - return exchange.getRequest().getHeaders().getFirst("User-Agent"); - } - - private String extractFullPath(ServerWebExchange exchange) { - var request = exchange.getRequest(); - String fullPath = request.getURI().getRawPath(); - String query = request.getURI().getQuery(); - return (query != null && !query.isEmpty()) ? fullPath + "?" + query : fullPath; - } - - private String extractOriginIp(ServerWebExchange exchange) { - String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); - InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); - return Optional.ofNullable(proxyIp) - .orElseGet(() -> remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "UNKNOWN SOURCE"); - } - - private Mono extractRequestBody(ServerWebExchange exchange) { - return exchange.getRequest().getBody().map(dataBuffer -> { - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - dataBuffer.read(bytes); - DataBufferUtils.release(dataBuffer); - - return new String(bytes, StandardCharsets.UTF_8); - }).reduce(new StringBuilder(), StringBuilder::append) - .map(StringBuilder::toString); - } - - private void logException(String method, String fullPath, String originIp, String userAgent, Exception exception) { - logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, exception.getMessage(), userAgent); - } - -} \ No newline at end of file From 894bd6838becedbf9f21a7fb0b1fdaab7e137eb0 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 17 Oct 2024 22:58:48 +0900 Subject: [PATCH 06/72] feat: implement presigned url generating logic --- .../photo/service/ObjectStorageService.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 8381fd95..e944d62e 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -1,7 +1,9 @@ package kr.mafoo.photo.service; +import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import org.springframework.beans.factory.annotation.Value; @@ -13,7 +15,10 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.net.URL; +import java.util.Date; import java.util.UUID; +import java.util.stream.Stream; @Service @RequiredArgsConstructor @@ -44,5 +49,31 @@ public Mono uploadFile(byte[] fileByte) { }); } + public Mono getPreSignedUrls(String[] fileNames, String memberId) { + return Mono.fromCallable(() -> + Stream.of(fileNames) + .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) + .toArray(String[]::new) + ); + } + + private URL generatePresignedUrl(String fileName, String memberId) { + + String newFileName = UUID.randomUUID() + "_" + fileName; + + String filePath = String.format("%s/photo/%s", memberId, newFileName); + + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 30; + expiration.setTime(expTimeMillis); + + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucketName, filePath) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + return amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); + } } \ No newline at end of file From c44122871499cceed32c64b35be2e15c68070f3b Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 17 Oct 2024 23:00:30 +0900 Subject: [PATCH 07/72] feat: implement presigned url generating api --- .../kr/mafoo/photo/api/ObjectStorageApi.java | 26 +++++++++++++++++++ .../controller/ObjectStorageController.java | 26 +++++++++++++++++++ .../ObjectStoragePreSignedUrlRequest.java | 14 ++++++++++ .../dto/response/PreSignedUrlResponse.java | 19 ++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/ObjectStoragePreSignedUrlRequest.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/PreSignedUrlResponse.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java new file mode 100644 index 00000000..506af3f6 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java @@ -0,0 +1,26 @@ +package kr.mafoo.photo.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.mafoo.photo.annotation.RequestMemberId; +import kr.mafoo.photo.controller.dto.request.ObjectStoragePreSignedUrlRequest; +import kr.mafoo.photo.controller.dto.response.PreSignedUrlResponse; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +@Validated +@Tag(name = "Object Storage 관련 API", description = "Object Storage 관련 API") +@RequestMapping("/v1/object-storage") +public interface ObjectStorageApi { + + @Operation(summary = "Pre-signed Url 요청", description = "Pre-signed Url 목록을 발급합니다.") + @GetMapping + Mono getPreSignedUrls( + @RequestMemberId + String memberId, + + @RequestBody + ObjectStoragePreSignedUrlRequest request + ); +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java new file mode 100644 index 00000000..42119503 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java @@ -0,0 +1,26 @@ +package kr.mafoo.photo.controller; + +import kr.mafoo.photo.api.ObjectStorageApi; +import kr.mafoo.photo.controller.dto.request.*; +import kr.mafoo.photo.controller.dto.response.PreSignedUrlResponse; +import kr.mafoo.photo.service.ObjectStorageService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@RestController +public class ObjectStorageController implements ObjectStorageApi { + + private final ObjectStorageService objectStorageService; + + @Override + public Mono getPreSignedUrls( + String memberId, + ObjectStoragePreSignedUrlRequest request + ) { + return objectStorageService + .getPreSignedUrls(request.fileNames(), memberId) + .map(PreSignedUrlResponse::fromStringArray); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/ObjectStoragePreSignedUrlRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/ObjectStoragePreSignedUrlRequest.java new file mode 100644 index 00000000..bb952283 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/ObjectStoragePreSignedUrlRequest.java @@ -0,0 +1,14 @@ +package kr.mafoo.photo.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Pre-signed Url 발급 요청") +public record ObjectStoragePreSignedUrlRequest( + @ArraySchema( + schema = @Schema(description = "파일 이름 목록"), + arraySchema = @Schema(example = "[\"test_file_name_1\", \"test_file_name_2\", \"test_file_name_3\"]") + ) + String[] fileNames +) { +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/PreSignedUrlResponse.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/PreSignedUrlResponse.java new file mode 100644 index 00000000..bbd24b22 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/PreSignedUrlResponse.java @@ -0,0 +1,19 @@ +package kr.mafoo.photo.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.domain.BrandType; +import kr.mafoo.photo.domain.PhotoEntity; + +@Schema(description = "Pre-signed Url 응답") +public record PreSignedUrlResponse( + @Schema(description = "URL 목록", example = "url") + String[] urls +) { + public static PreSignedUrlResponse fromStringArray( + String[] urls + ) { + return new PreSignedUrlResponse( + urls + ); + } +} From b496af755d6c122921281485cbffede80d111da4 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 17 Oct 2024 23:02:47 +0900 Subject: [PATCH 08/72] feat: implement file url upload api --- .../java/kr/mafoo/photo/api/PhotoApi.java | 24 ++++++++++++------- .../photo/controller/PhotoController.java | 21 ++++++++++------ .../request/PhotoFileUrlUploadRequest.java | 14 +++++++++++ ...Request.java => PhotoQrUploadRequest.java} | 4 ++-- .../kr/mafoo/photo/service/PhotoService.java | 20 ++++++++++++++-- 5 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java rename photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/{PhotoCreateRequest.java => PhotoQrUploadRequest.java} (73%) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java index f6567474..6c1465c9 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java @@ -6,10 +6,7 @@ import jakarta.validation.Valid; import kr.mafoo.photo.annotation.RequestMemberId; import kr.mafoo.photo.annotation.ULID; -import kr.mafoo.photo.controller.dto.request.PhotoCreateRequest; -import kr.mafoo.photo.controller.dto.request.PhotoBulkUpdateAlbumIdRequest; -import kr.mafoo.photo.controller.dto.request.PhotoUpdateAlbumIdRequest; -import kr.mafoo.photo.controller.dto.request.PhotoUpdateDisplayIndexRequest; +import kr.mafoo.photo.controller.dto.request.*; import kr.mafoo.photo.controller.dto.response.PhotoResponse; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; @@ -34,15 +31,26 @@ Flux getPhotos( String albumId ); - @Operation(summary = "사진 생성", description = "사진을 생성합니다.") - @PostMapping - Mono createPhoto( + @Operation(summary = "QR 사진 업로드", description = "QR을 사용해 사진을 업로드합니다.") + @PostMapping(value = "/qr") + Mono uploadQrPhoto( @RequestMemberId String memberId, @Valid @RequestBody - PhotoCreateRequest request + PhotoQrUploadRequest request + ); + + @Operation(summary = "파일(url) 사진 n건 업로드", description = "파일(url)을 사용해 사진을 업로드합니다.") + @PostMapping(value = "/file-urls") + Flux uploadFileUrlPhoto( + @RequestMemberId + String memberId, + + @Valid + @RequestBody + PhotoFileUrlUploadRequest request ); @Operation(summary = "사진 파일로 업로드", description = "사진을 직접 업로드합니다.") diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index 62a3bea5..b7f3f01b 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -1,10 +1,7 @@ package kr.mafoo.photo.controller; import kr.mafoo.photo.api.PhotoApi; -import kr.mafoo.photo.controller.dto.request.PhotoCreateRequest; -import kr.mafoo.photo.controller.dto.request.PhotoBulkUpdateAlbumIdRequest; -import kr.mafoo.photo.controller.dto.request.PhotoUpdateAlbumIdRequest; -import kr.mafoo.photo.controller.dto.request.PhotoUpdateDisplayIndexRequest; +import kr.mafoo.photo.controller.dto.request.*; import kr.mafoo.photo.controller.dto.response.PhotoResponse; import kr.mafoo.photo.service.PhotoService; import lombok.RequiredArgsConstructor; @@ -30,12 +27,22 @@ public Flux getPhotos( } @Override - public Mono createPhoto( + public Mono uploadQrPhoto( String memberId, - PhotoCreateRequest request + PhotoQrUploadRequest request ){ return photoService - .createNewPhoto(request.qrUrl(), memberId) + .createNewPhotoByQrUrl(request.qrUrl(), memberId) + .map(PhotoResponse::fromEntity); + } + + @Override + public Flux uploadFileUrlPhoto( + String memberId, + PhotoFileUrlUploadRequest request + ){ + return photoService + .createNewPhotoFileUrl(request.fileUrls(), memberId) .map(PhotoResponse::fromEntity); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java new file mode 100644 index 00000000..17873662 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java @@ -0,0 +1,14 @@ +package kr.mafoo.photo.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "파일(url) 사진 n건 업로드 요청") +public record PhotoFileUrlUploadRequest( + @ArraySchema( + schema = @Schema(description = "파일 URL 목록"), + arraySchema = @Schema(example = "[\"file_url_1\", \"file_url_2\", \"file_url_3\"]") + ) + String[] fileUrls +) { +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoQrUploadRequest.java similarity index 73% rename from photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java rename to photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoQrUploadRequest.java index f4c80e65..62322971 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoQrUploadRequest.java @@ -3,8 +3,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.hibernate.validator.constraints.URL; -@Schema(description = "사진 생성 요청") -public record PhotoCreateRequest( +@Schema(description = "QR 사진 업로드 요청") +public record PhotoQrUploadRequest( @URL @Schema(description = "QR URL", example = "qr_url") String qrUrl diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index e25fd207..b2e55850 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -21,6 +21,8 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import static kr.mafoo.photo.domain.BrandType.EXTERNAL; + @Slf4j @RequiredArgsConstructor @Service @@ -33,17 +35,31 @@ public class PhotoService { private final ObjectStorageService objectStorageService; @Transactional - public Mono createNewPhoto(String qrUrl, String requestMemberId) { + public Mono createNewPhotoByQrUrl(String qrUrl, String requestMemberId) { return qrService .getFileFromQrUrl(qrUrl) .flatMap(fileDto -> objectStorageService.uploadFile(fileDto.fileByte()) .flatMap(photoUrl -> { + this.createNewPhoto(photoUrl, fileDto.type(), requestMemberId); PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, fileDto.type(), requestMemberId); return photoRepository.save(photoEntity); }) ); } + @Transactional + public Flux createNewPhotoFileUrl(String[] fileUrls, String requestMemberId) { + return Flux.fromArray(fileUrls) + .flatMap(fileUrl -> + this.createNewPhoto(fileUrl, EXTERNAL, requestMemberId) + ); + } + + private Mono createNewPhoto(String photoUrl, BrandType type, String requestMemberId) { + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, type, requestMemberId); + return photoRepository.save(photoEntity); + } + @Transactional public Flux uploadPhoto(Flux files, String requestMemberId) { return files @@ -62,7 +78,7 @@ public Flux uploadPhoto(Flux files, String requestMemberI }) .flatMap(bytes -> objectStorageService.uploadFile(bytes) .flatMap(photoUrl -> { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, requestMemberId); + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, EXTERNAL, requestMemberId); return photoRepository.save(photoEntity); })) .subscribeOn(Schedulers.boundedElastic()) From a69bbdfb10650cfc7579d64c6771241643d2d94c Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 18 Oct 2024 17:33:37 +0900 Subject: [PATCH 09/72] refactor: change BrandType.EXTERNAL importing method --- .../src/main/java/kr/mafoo/photo/service/PhotoService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index b2e55850..f42bc65c 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -21,7 +21,6 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -import static kr.mafoo.photo.domain.BrandType.EXTERNAL; @Slf4j @RequiredArgsConstructor @@ -51,7 +50,7 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe public Flux createNewPhotoFileUrl(String[] fileUrls, String requestMemberId) { return Flux.fromArray(fileUrls) .flatMap(fileUrl -> - this.createNewPhoto(fileUrl, EXTERNAL, requestMemberId) + this.createNewPhoto(fileUrl, BrandType.EXTERNAL, requestMemberId) ); } @@ -78,7 +77,7 @@ public Flux uploadPhoto(Flux files, String requestMemberI }) .flatMap(bytes -> objectStorageService.uploadFile(bytes) .flatMap(photoUrl -> { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, EXTERNAL, requestMemberId); + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, requestMemberId); return photoRepository.save(photoEntity); })) .subscribeOn(Schedulers.boundedElastic()) From 477efc76f9b5d4c6c1bca0b8547f766d05f1573b Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 18 Oct 2024 17:49:31 +0900 Subject: [PATCH 10/72] refactor: create presigned-url-expiration in application.yaml --- .../java/kr/mafoo/photo/service/ObjectStorageService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index e944d62e..2d70da51 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -29,6 +29,9 @@ public class ObjectStorageService { @Value("${cloud.aws.s3.bucket}") private String bucketName; + @Value("${cloud.aws.s3.presigned-url-expiration}") + private long urlExpiration; // Expiration time in milliseconds + public Mono uploadFile(byte[] fileByte) { String keyName = "/" + UUID.randomUUID(); @@ -65,7 +68,7 @@ private URL generatePresignedUrl(String fileName, String memberId) { Date expiration = new Date(); long expTimeMillis = expiration.getTime(); - expTimeMillis += 1000 * 60 * 30; + expTimeMillis += urlExpiration; // Use the configured expiration time expiration.setTime(expTimeMillis); GeneratePresignedUrlRequest generatePresignedUrlRequest = From 7c0e70607ecf1c27bd077ea0ae43eae06939fb20 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 18 Oct 2024 17:51:47 +0900 Subject: [PATCH 11/72] refactor: refactor generatePresignedUrl method in ObjectStorageService --- .../photo/service/ObjectStorageService.java | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 2d70da51..3ffb473f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -30,7 +30,7 @@ public class ObjectStorageService { private String bucketName; @Value("${cloud.aws.s3.presigned-url-expiration}") - private long urlExpiration; // Expiration time in milliseconds + private long presignedUrlExpiration; public Mono uploadFile(byte[] fileByte) { String keyName = "/" + UUID.randomUUID(); @@ -52,7 +52,7 @@ public Mono uploadFile(byte[] fileByte) { }); } - public Mono getPreSignedUrls(String[] fileNames, String memberId) { + public Mono getPreSignedUrls(String[] fileNames, String memberId) { return Mono.fromCallable(() -> Stream.of(fileNames) .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) @@ -61,22 +61,12 @@ public Mono uploadFile(byte[] fileByte) { } private URL generatePresignedUrl(String fileName, String memberId) { + String filePath = String.format("%s/photo/%s_%s", memberId, UUID.randomUUID(), fileName); + Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); - String newFileName = UUID.randomUUID() + "_" + fileName; - - String filePath = String.format("%s/photo/%s", memberId, newFileName); - - Date expiration = new Date(); - long expTimeMillis = expiration.getTime(); - expTimeMillis += urlExpiration; // Use the configured expiration time - expiration.setTime(expTimeMillis); - - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(bucketName, filePath) - .withMethod(HttpMethod.PUT) - .withExpiration(expiration); - - return amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); + return amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration)); } } \ No newline at end of file From dfde7aa2dc6c4b803c108ff70ea5be2ebdff3369 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 18 Oct 2024 17:55:33 +0900 Subject: [PATCH 12/72] feat: add presigned-url-expiration value for object storage --- photo-service/src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index 6e8f1c6f..5cbcb3db 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -28,6 +28,7 @@ cloud: s3: endpoint: https://kr.object.ncloudstorage.com bucket: ${NCP_BUCKET} + presigned-url-expiration: 1800000 management: endpoints: From a04ed38e0e27db26c42c01e55dba443fceacaf38 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 16:49:50 +0900 Subject: [PATCH 13/72] fix: fix presigned url generating api to post mapping --- .../src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java | 4 ++-- .../kr/mafoo/photo/controller/ObjectStorageController.java | 4 ++-- .../java/kr/mafoo/photo/service/ObjectStorageService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java index 506af3f6..802e0b3f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/ObjectStorageApi.java @@ -15,8 +15,8 @@ public interface ObjectStorageApi { @Operation(summary = "Pre-signed Url 요청", description = "Pre-signed Url 목록을 발급합니다.") - @GetMapping - Mono getPreSignedUrls( + @PostMapping + Mono createPreSignedUrls( @RequestMemberId String memberId, diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java index 42119503..41f694c7 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/ObjectStorageController.java @@ -15,12 +15,12 @@ public class ObjectStorageController implements ObjectStorageApi { private final ObjectStorageService objectStorageService; @Override - public Mono getPreSignedUrls( + public Mono createPreSignedUrls( String memberId, ObjectStoragePreSignedUrlRequest request ) { return objectStorageService - .getPreSignedUrls(request.fileNames(), memberId) + .createPreSignedUrls(request.fileNames(), memberId) .map(PreSignedUrlResponse::fromStringArray); } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 3ffb473f..86a2d451 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -52,7 +52,7 @@ public Mono uploadFile(byte[] fileByte) { }); } - public Mono getPreSignedUrls(String[] fileNames, String memberId) { + public Mono createPreSignedUrls(String[] fileNames, String memberId) { return Mono.fromCallable(() -> Stream.of(fileNames) .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) From aa6de5f7ecb0a4a67f493bff918af12a0ae92e7b Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 16:51:11 +0900 Subject: [PATCH 14/72] fix: restore original api for uploading qr photo --- .../src/main/java/kr/mafoo/photo/api/PhotoApi.java | 11 +++++++++++ .../kr/mafoo/photo/controller/PhotoController.java | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java index 6c1465c9..db40e44f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java @@ -31,6 +31,17 @@ Flux getPhotos( String albumId ); + @Operation(summary = "(수정 이전) QR 사진 업로드", description = "QR을 사용해 사진을 업로드합니다.") + @PostMapping(value = "") + Mono uploadQrPhotoOriginal( + @RequestMemberId + String memberId, + + @Valid + @RequestBody + PhotoQrUploadRequest request + ); + @Operation(summary = "QR 사진 업로드", description = "QR을 사용해 사진을 업로드합니다.") @PostMapping(value = "/qr") Mono uploadQrPhoto( diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index b7f3f01b..72a57564 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -26,6 +26,16 @@ public Flux getPhotos( .map(PhotoResponse::fromEntity); } + @Override + public Mono uploadQrPhotoOriginal( + String memberId, + PhotoQrUploadRequest request + ){ + return photoService + .createNewPhotoByQrUrl(request.qrUrl(), memberId) + .map(PhotoResponse::fromEntity); + } + @Override public Mono uploadQrPhoto( String memberId, From b45359f30b1ab2351814df3d620fd91a06f1ab02 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 17:04:39 +0900 Subject: [PATCH 15/72] feat: add pre signed url generation maximum checking logic --- .../java/kr/mafoo/photo/exception/ErrorCode.java | 2 ++ .../exception/PreSignedUrlExceedMaximum.java | 7 +++++++ .../mafoo/photo/service/ObjectStorageService.java | 15 ++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlExceedMaximum.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java index 97f350c5..2b3c256f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java @@ -18,6 +18,8 @@ public enum ErrorCode { PHOTO_DISPLAY_INDEX_IS_SAME("PE0004", "옮기려는 대상 사진 인덱스가 같습니다"), PHOTO_DISPLAY_INDEX_NOT_VALID("PE0005", "옮기려는 대상 사진 인덱스가 유효하지 않습니다"), + PRE_SIGNED_URL_EXCEED_MAXIMUM("OE0005", "한 번에 생성할 수 있는 Pre-signed url 최대치를 초과했습니다"), + ; private final String code; private final String message; diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlExceedMaximum.java b/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlExceedMaximum.java new file mode 100644 index 00000000..a3ad2673 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlExceedMaximum.java @@ -0,0 +1,7 @@ +package kr.mafoo.photo.exception; + +public class PreSignedUrlExceedMaximum extends DomainException { + public PreSignedUrlExceedMaximum() { + super(ErrorCode.PRE_SIGNED_URL_EXCEED_MAXIMUM); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 86a2d451..2049e400 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -6,6 +6,7 @@ import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import kr.mafoo.photo.exception.PreSignedUrlExceedMaximum; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -53,11 +54,15 @@ public Mono uploadFile(byte[] fileByte) { } public Mono createPreSignedUrls(String[] fileNames, String memberId) { - return Mono.fromCallable(() -> - Stream.of(fileNames) - .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) - .toArray(String[]::new) - ); + return Mono.fromCallable(() -> { + if (fileNames.length > 30) { + throw new PreSignedUrlExceedMaximum(); + } + + return Stream.of(fileNames) + .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) + .toArray(String[]::new); + }); } private URL generatePresignedUrl(String fileName, String memberId) { From b6e3b848d92406eaf5019b448e2561c1de759a1a Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 17:58:18 +0900 Subject: [PATCH 16/72] feat: implement acl setting method for object storage service --- .../kr/mafoo/photo/service/ObjectStorageService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 2049e400..a5c4cd15 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -74,4 +74,14 @@ private URL generatePresignedUrl(String fileName, String memberId) { .withExpiration(expiration)); } + public Mono setObjectPublicRead(String filePath) { + return Mono.fromRunnable(() -> { + try { + amazonS3Client.setObjectAcl(bucketName, filePath, CannedAccessControlList.PublicRead); + } catch (Exception e) { + throw new RuntimeException("Failed to set ACL to PublicRead for the file: " + filePath, e); + } + }); + } + } \ No newline at end of file From 7360576ed378c595f2a5d0dc5fbcc9757780e595 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 17:59:44 +0900 Subject: [PATCH 17/72] feat: attach acl setting logic in createNewPhotoFileUrl --- .../src/main/java/kr/mafoo/photo/service/PhotoService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index f42bc65c..e73091f4 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -49,8 +49,8 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe @Transactional public Flux createNewPhotoFileUrl(String[] fileUrls, String requestMemberId) { return Flux.fromArray(fileUrls) - .flatMap(fileUrl -> - this.createNewPhoto(fileUrl, BrandType.EXTERNAL, requestMemberId) + .flatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) + .then(createNewPhoto(fileUrl, BrandType.EXTERNAL, requestMemberId)) ); } From 3f5332aee7f99c040d7d68145d339b079183244f Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 18:00:25 +0900 Subject: [PATCH 18/72] fix: fix duplicating lines in createNewPhotoByQrUrl --- .../src/main/java/kr/mafoo/photo/service/PhotoService.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index e73091f4..960dbe9c 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -38,11 +38,7 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe return qrService .getFileFromQrUrl(qrUrl) .flatMap(fileDto -> objectStorageService.uploadFile(fileDto.fileByte()) - .flatMap(photoUrl -> { - this.createNewPhoto(photoUrl, fileDto.type(), requestMemberId); - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, fileDto.type(), requestMemberId); - return photoRepository.save(photoEntity); - }) + .flatMap(photoUrl -> createNewPhoto(photoUrl, fileDto.type(), requestMemberId)) ); } From 68a8f6efef2bec405fae354b845abe8647534ee0 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 18:09:08 +0900 Subject: [PATCH 19/72] fix: fix object storage error code typo --- .../src/main/java/kr/mafoo/photo/exception/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java index 2b3c256f..8105a0d5 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java @@ -18,7 +18,7 @@ public enum ErrorCode { PHOTO_DISPLAY_INDEX_IS_SAME("PE0004", "옮기려는 대상 사진 인덱스가 같습니다"), PHOTO_DISPLAY_INDEX_NOT_VALID("PE0005", "옮기려는 대상 사진 인덱스가 유효하지 않습니다"), - PRE_SIGNED_URL_EXCEED_MAXIMUM("OE0005", "한 번에 생성할 수 있는 Pre-signed url 최대치를 초과했습니다"), + PRE_SIGNED_URL_EXCEED_MAXIMUM("OE0001", "한 번에 생성할 수 있는 Pre-signed url 최대치를 초과했습니다"), ; private final String code; From 07563721bcfbcb911fd9dd9d9b82de51387873ce Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 18:09:52 +0900 Subject: [PATCH 20/72] refactor: add message for uploadFile runtime exception --- .../main/java/kr/mafoo/photo/service/ObjectStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index a5c4cd15..44b53761 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -48,7 +48,7 @@ public Mono uploadFile(byte[] fileByte) { return "https://kr.object.ncloudstorage.com/" + bucketName + "/" + keyName; } catch (Exception e) { - throw new RuntimeException(e); + throw new RuntimeException("Failed to upload image to object storage: ", e); } }); } From dab1d106c1539db0b4f175a53d49a9c5b5e04a8d Mon Sep 17 00:00:00 2001 From: gmkim Date: Sat, 19 Oct 2024 19:01:37 +0900 Subject: [PATCH 21/72] fix: add key extracting logic from filePath --- .../java/kr/mafoo/photo/service/ObjectStorageService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 44b53761..e691932c 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -77,11 +77,12 @@ private URL generatePresignedUrl(String fileName, String memberId) { public Mono setObjectPublicRead(String filePath) { return Mono.fromRunnable(() -> { try { - amazonS3Client.setObjectAcl(bucketName, filePath, CannedAccessControlList.PublicRead); + String key = filePath.split(bucketName + "/")[1]; + amazonS3Client.setObjectAcl(bucketName, key, CannedAccessControlList.PublicRead); } catch (Exception e) { throw new RuntimeException("Failed to set ACL to PublicRead for the file: " + filePath, e); } }); } -} \ No newline at end of file +} From e84f88fdf4ba0057b46b7782a673e08365a29f00 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 19:29:23 +0900 Subject: [PATCH 22/72] fix: fix presigned url format --- .../main/java/kr/mafoo/photo/service/ObjectStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index e691932c..1f5b9273 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -66,7 +66,7 @@ public Mono createPreSignedUrls(String[] fileNames, String memberId) { } private URL generatePresignedUrl(String fileName, String memberId) { - String filePath = String.format("%s/photo/%s_%s", memberId, UUID.randomUUID(), fileName); + String filePath = String.format("%s/%s/photo/%s_%s", bucketName, memberId, UUID.randomUUID(), fileName); Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); return amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) From 26f8710ea26836cfbc4bf9d56332d325dc22225a Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 19 Oct 2024 20:20:30 +0900 Subject: [PATCH 23/72] fix: fix object storage accessible link format --- .../photo/service/ObjectStorageService.java | 26 +++++++++++++------ .../kr/mafoo/photo/service/PhotoService.java | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 1f5b9273..e786aa31 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -7,6 +7,7 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import kr.mafoo.photo.exception.PreSignedUrlExceedMaximum; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -21,12 +22,16 @@ import java.util.UUID; import java.util.stream.Stream; +@Slf4j @Service @RequiredArgsConstructor public class ObjectStorageService { private final AmazonS3Client amazonS3Client; + @Value("${cloud.aws.s3.endpoint}") + private String endpoint; + @Value("${cloud.aws.s3.bucket}") private String bucketName; @@ -34,7 +39,7 @@ public class ObjectStorageService { private long presignedUrlExpiration; public Mono uploadFile(byte[] fileByte) { - String keyName = "/" + UUID.randomUUID(); + String keyName = "qr/" + UUID.randomUUID(); ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(fileByte.length); @@ -46,7 +51,7 @@ public Mono uploadFile(byte[] fileByte) { new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata) .withCannedAcl(CannedAccessControlList.PublicRead)); - return "https://kr.object.ncloudstorage.com/" + bucketName + "/" + keyName; + return generateFileLink(keyName); } catch (Exception e) { throw new RuntimeException("Failed to upload image to object storage: ", e); } @@ -66,7 +71,7 @@ public Mono createPreSignedUrls(String[] fileNames, String memberId) { } private URL generatePresignedUrl(String fileName, String memberId) { - String filePath = String.format("%s/%s/photo/%s_%s", bucketName, memberId, UUID.randomUUID(), fileName); + String filePath = String.format("%s/photo/%s_%s", memberId, UUID.randomUUID(), fileName); Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); return amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) @@ -74,15 +79,20 @@ private URL generatePresignedUrl(String fileName, String memberId) { .withExpiration(expiration)); } - public Mono setObjectPublicRead(String filePath) { - return Mono.fromRunnable(() -> { + public Mono setObjectPublicRead(String filePath) { + String keyName = filePath.split("object.ncloudstorage.com/")[1]; + + return Mono.fromCallable(() -> { try { - String key = filePath.split(bucketName + "/")[1]; - amazonS3Client.setObjectAcl(bucketName, key, CannedAccessControlList.PublicRead); + amazonS3Client.setObjectAcl(bucketName, keyName, CannedAccessControlList.PublicRead); + return generateFileLink(keyName); } catch (Exception e) { - throw new RuntimeException("Failed to set ACL to PublicRead for the file: " + filePath, e); + throw new RuntimeException("Failed to set ACL to PublicRead for the file: " + keyName, e); } }); } + private String generateFileLink(String keyName) { + return endpoint + "/" + bucketName + "/" + keyName; + } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 8bd82ebf..b2a681f9 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -46,7 +46,7 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe public Flux createNewPhotoFileUrl(String[] fileUrls, String requestMemberId) { return Flux.fromArray(fileUrls) .flatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) - .then(createNewPhoto(fileUrl, BrandType.EXTERNAL, requestMemberId)) + .flatMap(fileLink -> createNewPhoto(fileLink, BrandType.EXTERNAL, requestMemberId)) ); } From f07d0025c9904899ea45b59ddb954fcd20b56609 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 25 Oct 2024 22:51:45 +0900 Subject: [PATCH 24/72] refactor: advance extractFullPath as extractURI --- .../kr/mafoo/photo/config/WebExceptionHandler.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java index 328b3338..fa97269f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java @@ -74,7 +74,7 @@ public Mono> handleGenericException(ServerWebExchange exc private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception exception, HttpStatus status) { String method = extractMethod(exchange); String userAgent = extractUserAgent(exchange); - String fullPath = extractFullPath(exchange); + String fullPath = extractURI(exchange); String originIp = extractOriginIp(exchange); return extractRequestBody(exchange).flatMap(requestBody -> { @@ -99,11 +99,19 @@ private String extractUserAgent(ServerWebExchange exchange) { return exchange.getRequest().getHeaders().getFirst("User-Agent"); } - private String extractFullPath(ServerWebExchange exchange) { + private String extractURI(ServerWebExchange exchange) { var request = exchange.getRequest(); + + String scheme = request.getURI().getScheme(); + String host = request.getURI().getHost(); + int port = request.getURI().getPort(); String fullPath = request.getURI().getRawPath(); String query = request.getURI().getQuery(); - return (query != null && !query.isEmpty()) ? fullPath + "?" + query : fullPath; + + String baseUrl = (port != -1) ? host + ":" + port : host; + String uriWithHost = scheme + "://" + baseUrl + fullPath; + + return (query != null && !query.isEmpty()) ? uriWithHost + "?" + query : uriWithHost; } private String extractOriginIp(ServerWebExchange exchange) { From 655b9f90a6c988b5df787c3d3a49018e10b24457 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 00:07:43 +0900 Subject: [PATCH 25/72] feat: add albumId saving & photoCount updating logic for uploadFileUrlPhoto api --- .../photo/controller/PhotoController.java | 2 +- .../request/PhotoFileUrlUploadRequest.java | 7 ++++- .../kr/mafoo/photo/domain/AlbumEntity.java | 8 +++--- .../kr/mafoo/photo/domain/PhotoEntity.java | 3 ++- .../kr/mafoo/photo/service/AlbumService.java | 8 +++--- .../kr/mafoo/photo/service/PhotoService.java | 27 +++++++++++-------- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index dcb79816..fb41db2a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -53,7 +53,7 @@ public Flux uploadFileUrlPhoto( PhotoFileUrlUploadRequest request ){ return photoService - .createNewPhotoFileUrl(request.fileUrls(), memberId) + .createNewPhotoFileUrl(request.fileUrls(), request.albumId(), memberId) .map(PhotoResponse::fromEntity); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java index 17873662..442e8b9a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoFileUrlUploadRequest.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.annotation.ULID; @Schema(description = "파일(url) 사진 n건 업로드 요청") public record PhotoFileUrlUploadRequest( @@ -9,6 +10,10 @@ public record PhotoFileUrlUploadRequest( schema = @Schema(description = "파일 URL 목록"), arraySchema = @Schema(example = "[\"file_url_1\", \"file_url_2\", \"file_url_3\"]") ) - String[] fileUrls + String[] fileUrls, + + @ULID + @Schema(description = "앨범 ID", example = "test_album_id") + String albumId ) { } diff --git a/photo-service/src/main/java/kr/mafoo/photo/domain/AlbumEntity.java b/photo-service/src/main/java/kr/mafoo/photo/domain/AlbumEntity.java index 467e76ef..312addf0 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/domain/AlbumEntity.java +++ b/photo-service/src/main/java/kr/mafoo/photo/domain/AlbumEntity.java @@ -77,13 +77,13 @@ public AlbumEntity updateType(AlbumType newType) { return this; } - public AlbumEntity increasePhotoCount() { - this.photoCount += 1; + public AlbumEntity increasePhotoCount(int count) { + this.photoCount += count; return this; } - public AlbumEntity decreasePhotoCount() { - this.photoCount -= 1; + public AlbumEntity decreasePhotoCount(int count) { + this.photoCount -= count; return this; } diff --git a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java index 5084cb5c..91b79dae 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java +++ b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java @@ -82,12 +82,13 @@ public PhotoEntity updateDisplayIndex(Integer displayIndex) { return this; } - public static PhotoEntity newPhoto(String photoId, String photoUrl, BrandType brandType, String ownerMemberId) { + public static PhotoEntity newPhoto(String photoId, String photoUrl, BrandType brandType, String albumId, String ownerMemberId) { PhotoEntity photo = new PhotoEntity(); photo.photoId = photoId; photo.photoUrl = photoUrl; photo.brand = brandType; photo.ownerMemberId = ownerMemberId; + photo.albumId = albumId; photo.displayIndex = 0; photo.isNew = true; photo.createdAt = LocalDateTime.now(); 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 1b801897..9cefe858 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 @@ -114,7 +114,7 @@ public Mono updateAlbumType(String albumId, AlbumType albumType, St } @Transactional - public Mono increaseAlbumPhotoCount(String albumId, String requestMemberId) { + public Mono increaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { return albumRepository .findById(albumId) .switchIfEmpty(Mono.error(new AlbumNotFoundException())) @@ -123,13 +123,13 @@ public Mono increaseAlbumPhotoCount(String albumId, String requestMemberId // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { - return albumRepository.save(albumEntity.increasePhotoCount()).then(); + return albumRepository.save(albumEntity.increasePhotoCount(count)).then(); } }); } @Transactional - public Mono decreaseAlbumPhotoCount(String albumId, String requestMemberId) { + public Mono decreaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { if (albumId == null) { return Mono.empty(); @@ -143,7 +143,7 @@ public Mono decreaseAlbumPhotoCount(String albumId, String requestMemberId // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { - return albumRepository.save(albumEntity.decreasePhotoCount()).then(); + return albumRepository.save(albumEntity.decreasePhotoCount(count)).then(); } }); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index b2a681f9..2a4580a5 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -38,20 +38,25 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe return qrService .getFileFromQrUrl(qrUrl) .flatMap(fileDto -> objectStorageService.uploadFile(fileDto.fileByte()) - .flatMap(photoUrl -> createNewPhoto(photoUrl, fileDto.type(), requestMemberId)) + .flatMap(photoUrl -> createNewPhoto(photoUrl, fileDto.type(), null, requestMemberId)) ); } @Transactional - public Flux createNewPhotoFileUrl(String[] fileUrls, String requestMemberId) { - return Flux.fromArray(fileUrls) - .flatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) - .flatMap(fileLink -> createNewPhoto(fileLink, BrandType.EXTERNAL, requestMemberId)) + public Flux createNewPhotoFileUrl(String[] fileUrls, String albumId, String requestMemberId) { + + return albumService.findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> albumService.increaseAlbumPhotoCount(albumId, fileUrls.length, requestMemberId)) + .thenMany( + Flux.fromArray(fileUrls) + .flatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) + .flatMap(fileLink -> createNewPhoto(fileLink, BrandType.EXTERNAL, albumId, requestMemberId)) + ) ); } - private Mono createNewPhoto(String photoUrl, BrandType type, String requestMemberId) { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, type, requestMemberId); + private Mono createNewPhoto(String photoUrl, BrandType type, String albumId, String requestMemberId) { + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, type, albumId, requestMemberId); return photoRepository.save(photoEntity); } @@ -73,7 +78,7 @@ public Flux uploadPhoto(Flux files, String requestMemberI }) .flatMap(bytes -> objectStorageService.uploadFile(bytes) .flatMap(photoUrl -> { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, requestMemberId); + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, null, requestMemberId); return photoRepository.save(photoEntity); })) .subscribeOn(Schedulers.boundedElastic()) @@ -111,7 +116,7 @@ public Mono deletePhotoById(String photoId, String requestMemberId) { // 내 사진이 아니면 그냥 없는 사진 처리 return Mono.error(new PhotoNotFoundException()); } else { - return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), requestMemberId) + return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) .then(photoRepository.popDisplayIndexGreaterThan(photoEntity.getAlbumId(), photoEntity.getDisplayIndex())) .then(photoRepository.deleteById(photoId)); } @@ -149,9 +154,9 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri // 내 앨범이 아니면 그냥 없는 앨범 처리 return Mono.error(new AlbumNotFoundException()); } else { - return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), requestMemberId) + return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) .then(photoRepository.popDisplayIndexGreaterThan(photoEntity.getAlbumId(), photoEntity.getDisplayIndex())) - .then(albumService.increaseAlbumPhotoCount(albumId, requestMemberId)) + .then(albumService.increaseAlbumPhotoCount(albumId, 1, requestMemberId)) .then(photoRepository.save( photoEntity .updateAlbumId(albumId) From 6f1847adc8428b8d5c26ca26e4dafaaaf4d573ad Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 00:56:01 +0900 Subject: [PATCH 26/72] feat: create findByPhotoId method --- .../java/kr/mafoo/photo/service/PhotoService.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 2a4580a5..9fe2497a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -106,6 +106,20 @@ public Flux findAllByAlbumId(String albumId, String requestMemberId }); } + public Mono findByPhotoId(String photoId, String requestMemberId) { + return photoRepository + .findById(photoId) + .switchIfEmpty(Mono.error(new PhotoNotFoundException())) + .flatMap(photoEntity -> { + if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { + // 내 사진이 아니면 그냥 없는 사진 처리 + return Mono.error(new PhotoNotFoundException()); + } else { + return Mono.just(photoEntity); + } + }); + } + @Transactional public Mono deletePhotoById(String photoId, String requestMemberId) { return photoRepository From 3c51ab3b2befcb65a49255c9e2849471a6c51f97 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 01:09:18 +0900 Subject: [PATCH 27/72] refactor: remove album repository from photo service --- .../kr/mafoo/photo/service/PhotoService.java | 52 +++++-------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 9fe2497a..1e4e4ad4 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -2,11 +2,9 @@ import kr.mafoo.photo.domain.BrandType; import kr.mafoo.photo.domain.PhotoEntity; -import kr.mafoo.photo.exception.AlbumNotFoundException; import kr.mafoo.photo.exception.PhotoDisplayIndexIsSameException; import kr.mafoo.photo.exception.PhotoDisplayIndexNotValidException; import kr.mafoo.photo.exception.PhotoNotFoundException; -import kr.mafoo.photo.repository.AlbumRepository; import kr.mafoo.photo.repository.PhotoRepository; import kr.mafoo.photo.util.IdGenerator; import lombok.RequiredArgsConstructor; @@ -27,7 +25,6 @@ @Service public class PhotoService { private final PhotoRepository photoRepository; - private final AlbumRepository albumRepository; private final AlbumService albumService; private final QrService qrService; @@ -87,23 +84,14 @@ public Flux uploadPhoto(Flux files, String requestMemberI } public Flux findAllByAlbumId(String albumId, String requestMemberId, String sort) { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMapMany(albumEntity -> { - if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - String sortMethod = (sort == null) ? "CUSTOM" : sort.toUpperCase(); - - return switch (sortMethod) { + return albumService.findByAlbumId(albumId, requestMemberId) + .thenMany( + switch (sort) { case "ASC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtAsc(albumId); case "DESC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtDesc(albumId); default -> photoRepository.findAllByAlbumIdOrderByDisplayIndexDesc(albumId); - }; - } - }); + } + ); } public Mono findByPhotoId(String photoId, String requestMemberId) { @@ -160,24 +148,17 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri // 내 사진이 아니면 그냥 없는 사진 처리 return Mono.error(new PhotoNotFoundException()); } else { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) + return albumService.findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> + albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) .then(photoRepository.popDisplayIndexGreaterThan(photoEntity.getAlbumId(), photoEntity.getDisplayIndex())) .then(albumService.increaseAlbumPhotoCount(albumId, 1, requestMemberId)) .then(photoRepository.save( photoEntity .updateAlbumId(albumId) .updateDisplayIndex(albumEntity.getPhotoCount()) - )); - } - }); + )) + ); } }); } @@ -187,17 +168,10 @@ public Mono updatePhotoDisplayIndex(String photoId, Integer newInde return photoRepository .findById(photoId) .switchIfEmpty(Mono.error(new PhotoNotFoundException())) - .flatMap(photoEntity -> albumRepository - .findById(photoEntity.getAlbumId()) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) + .flatMap(photoEntity -> albumService.findByAlbumId(photoEntity.getAlbumId(), requestMemberId) .flatMap(albumEntity -> { - int targetIndex = albumEntity.getPhotoCount() - newIndex - 1; - if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - return Mono.error(new AlbumNotFoundException()); - } - if (photoEntity.getDisplayIndex().equals(targetIndex)) { return Mono.error(new PhotoDisplayIndexIsSameException()); } @@ -215,8 +189,8 @@ public Mono updatePhotoDisplayIndex(String photoId, Integer newInde .pushDisplayIndexBetween(photoEntity.getAlbumId(), targetIndex, photoEntity.getDisplayIndex() - 1) .then(photoRepository.save(photoEntity.updateDisplayIndex(targetIndex))); } - - })); + }) + ); } } From 45228f4970b52c4fde4f9df789de4f9a4a999ce9 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 01:23:23 +0900 Subject: [PATCH 28/72] refactor: refactor deletePhotoById to use findByPhotoId instead of photoRepository --- .../kr/mafoo/photo/service/PhotoService.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 1e4e4ad4..6d57e02a 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -110,19 +110,12 @@ public Mono findByPhotoId(String photoId, String requestMemberId) { @Transactional public Mono deletePhotoById(String photoId, String requestMemberId) { - return photoRepository - .findById(photoId) - .switchIfEmpty(Mono.error(new PhotoNotFoundException())) - .flatMap(photoEntity -> { - if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 사진이 아니면 그냥 없는 사진 처리 - return Mono.error(new PhotoNotFoundException()); - } else { - return albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) + return findByPhotoId(photoId, requestMemberId) + .flatMap(photoEntity -> + albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) .then(photoRepository.popDisplayIndexGreaterThan(photoEntity.getAlbumId(), photoEntity.getDisplayIndex())) - .then(photoRepository.deleteById(photoId)); - } - }); + .then(photoRepository.deleteById(photoId)) + ); } @Transactional From 2236e5418c7c89af0f5da315e6b7d6df7f2f03cc Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 02:34:40 +0900 Subject: [PATCH 29/72] refactor: refactor updatePhotoDisplayIndex to use findByPhotoId instead of photoRepository --- .../src/main/java/kr/mafoo/photo/service/PhotoService.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 6d57e02a..586ebbc1 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -41,7 +41,6 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe @Transactional public Flux createNewPhotoFileUrl(String[] fileUrls, String albumId, String requestMemberId) { - return albumService.findByAlbumId(albumId, requestMemberId) .flatMap(albumEntity -> albumService.increaseAlbumPhotoCount(albumId, fileUrls.length, requestMemberId)) .thenMany( @@ -158,9 +157,7 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri @Transactional public Mono updatePhotoDisplayIndex(String photoId, Integer newIndex, String requestMemberId) { - return photoRepository - .findById(photoId) - .switchIfEmpty(Mono.error(new PhotoNotFoundException())) + return findByPhotoId(photoId, requestMemberId) .flatMap(photoEntity -> albumService.findByAlbumId(photoEntity.getAlbumId(), requestMemberId) .flatMap(albumEntity -> { int targetIndex = albumEntity.getPhotoCount() - newIndex - 1; From 9ca74cc17c168a8083d0aecec04ed5c571d60e2e Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 04:54:36 +0900 Subject: [PATCH 30/72] fix: fix updatePhotoBulkAlbumId photoCount & displayIndex updating error --- .../kr/mafoo/photo/service/PhotoService.java | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 586ebbc1..00f12615 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -98,7 +98,10 @@ public Mono findByPhotoId(String photoId, String requestMemberId) { .findById(photoId) .switchIfEmpty(Mono.error(new PhotoNotFoundException())) .flatMap(photoEntity -> { - if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { + if (!photoEntity.hasOwnerMemberId()) { + return photoRepository.save(photoEntity.updateOwnerMemberId(requestMemberId)); + } + else if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { // 내 사진이 아니면 그냥 없는 사진 처리 return Mono.error(new PhotoNotFoundException()); } else { @@ -120,27 +123,14 @@ public Mono deletePhotoById(String photoId, String requestMemberId) { @Transactional public Flux updatePhotoBulkAlbumId(String[] photoIds, String albumId, String requestMemberId) { return Flux.fromArray(photoIds) - .flatMap(photoId -> - this.updatePhotoAlbumId(photoId, albumId, requestMemberId) - ); + .concatMap(photoId -> this.updatePhotoAlbumId(photoId, albumId, requestMemberId)); } @Transactional public Mono updatePhotoAlbumId(String photoId, String albumId, String requestMemberId) { - return photoRepository - .findById(photoId) - .switchIfEmpty(Mono.error(new PhotoNotFoundException())) - .flatMap(photoEntity -> { - - if (!photoEntity.hasOwnerMemberId()) { - photoRepository.save(photoEntity.updateOwnerMemberId(requestMemberId)); - } - - if (!photoEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 사진이 아니면 그냥 없는 사진 처리 - return Mono.error(new PhotoNotFoundException()); - } else { - return albumService.findByAlbumId(albumId, requestMemberId) + return findByPhotoId(photoId, requestMemberId) + .flatMap(photoEntity -> + albumService.findByAlbumId(albumId, requestMemberId) .flatMap(albumEntity -> albumService.decreaseAlbumPhotoCount(photoEntity.getAlbumId(), 1, requestMemberId) .then(photoRepository.popDisplayIndexGreaterThan(photoEntity.getAlbumId(), photoEntity.getDisplayIndex())) @@ -150,9 +140,8 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri .updateAlbumId(albumId) .updateDisplayIndex(albumEntity.getPhotoCount()) )) - ); - } - }); + ) + ); } @Transactional From 7a4a50f5e1070f0fb1cc6359b279309734bcd5d3 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 04:55:08 +0900 Subject: [PATCH 31/72] style: remove useless indent --- .../kr/mafoo/photo/service/PhotoService.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 00f12615..11240861 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -147,28 +147,29 @@ public Mono updatePhotoAlbumId(String photoId, String albumId, Stri @Transactional public Mono updatePhotoDisplayIndex(String photoId, Integer newIndex, String requestMemberId) { return findByPhotoId(photoId, requestMemberId) - .flatMap(photoEntity -> albumService.findByAlbumId(photoEntity.getAlbumId(), requestMemberId) - .flatMap(albumEntity -> { - int targetIndex = albumEntity.getPhotoCount() - newIndex - 1; - - if (photoEntity.getDisplayIndex().equals(targetIndex)) { - return Mono.error(new PhotoDisplayIndexIsSameException()); - } - - if (targetIndex < 0 || targetIndex >= albumEntity.getPhotoCount()) { - return Mono.error(new PhotoDisplayIndexNotValidException()); - } - - if (photoEntity.getDisplayIndex() < targetIndex) { - return photoRepository - .popDisplayIndexBetween(photoEntity.getAlbumId(), photoEntity.getDisplayIndex() + 1, targetIndex) - .then(photoRepository.save(photoEntity.updateDisplayIndex(targetIndex))); - } else { - return photoRepository - .pushDisplayIndexBetween(photoEntity.getAlbumId(), targetIndex, photoEntity.getDisplayIndex() - 1) - .then(photoRepository.save(photoEntity.updateDisplayIndex(targetIndex))); - } - }) + .flatMap(photoEntity -> + albumService.findByAlbumId(photoEntity.getAlbumId(), requestMemberId) + .flatMap(albumEntity -> { + int targetIndex = albumEntity.getPhotoCount() - newIndex - 1; + + if (photoEntity.getDisplayIndex().equals(targetIndex)) { + return Mono.error(new PhotoDisplayIndexIsSameException()); + } + + if (targetIndex < 0 || targetIndex >= albumEntity.getPhotoCount()) { + return Mono.error(new PhotoDisplayIndexNotValidException()); + } + + if (photoEntity.getDisplayIndex() < targetIndex) { + return photoRepository + .popDisplayIndexBetween(photoEntity.getAlbumId(), photoEntity.getDisplayIndex() + 1, targetIndex) + .then(photoRepository.save(photoEntity.updateDisplayIndex(targetIndex))); + } else { + return photoRepository + .pushDisplayIndexBetween(photoEntity.getAlbumId(), targetIndex, photoEntity.getDisplayIndex() - 1) + .then(photoRepository.save(photoEntity.updateDisplayIndex(targetIndex))); + } + }) ); } From 05ae694d8a514e5770aa2d59110fbd6c2c3b3577 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 05:12:09 +0900 Subject: [PATCH 32/72] refactor: refactor methods in album service to use findByAlbumId instead of album repository --- .../kr/mafoo/photo/service/AlbumService.java | 80 ++++--------------- 1 file changed, 17 insertions(+), 63 deletions(-) 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 9cefe858..b32abacf 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 @@ -67,85 +67,39 @@ public Mono findByAlbumId(String albumId, String requestMemberId) { @Transactional public Mono deleteAlbumById(String albumId, String requestMemberId) { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if(!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumRepository + return findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> + albumRepository .deleteById(albumId) .then(albumRepository.popDisplayIndexBetween( - requestMemberId, albumEntity.getDisplayIndex(), Integer.MAX_VALUE)); - } - }); + requestMemberId, albumEntity.getDisplayIndex(), Integer.MAX_VALUE)) + ); } @Transactional public Mono updateAlbumName(String albumId, String albumName, String requestMemberId) { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if(!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumRepository.save(albumEntity.updateName(albumName)); - } - }); + return findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> albumRepository.save(albumEntity.updateName(albumName))); } @Transactional public Mono updateAlbumType(String albumId, AlbumType albumType, String requestMemberId) { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if(!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumRepository.save(albumEntity.updateType(albumType)); - } - }); + return findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> albumRepository.save(albumEntity.updateType(albumType))); } @Transactional - public Mono increaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumRepository.save(albumEntity.increasePhotoCount(count)).then(); - } - }); + public Mono increaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { + return findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> albumRepository.save(albumEntity.increasePhotoCount(count))); } @Transactional - public Mono decreaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { - - if (albumId == null) { - return Mono.empty(); - } - - return albumRepository - .findById(albumId) - .switchIfEmpty(Mono.error(new AlbumNotFoundException())) - .flatMap(albumEntity -> { - if (!albumEntity.getOwnerMemberId().equals(requestMemberId)) { - // 내 앨범이 아니면 그냥 없는 앨범 처리 - return Mono.error(new AlbumNotFoundException()); - } else { - return albumRepository.save(albumEntity.decreasePhotoCount(count)).then(); - } - }); + public Mono decreaseAlbumPhotoCount(String albumId, int count, String requestMemberId) { + return Mono.justOrEmpty(albumId) + .switchIfEmpty(Mono.empty()) + .flatMap(id -> findByAlbumId(id, requestMemberId)) + .flatMap(albumEntity -> albumRepository.save(albumEntity.decreasePhotoCount(count))); } } From c1267c65b724ab2c655ebcd8cf86aedfcc6f7c54 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 05:29:38 +0900 Subject: [PATCH 33/72] fix: add sortMethod setting logic for findAllByAlbumId --- .../src/main/java/kr/mafoo/photo/service/PhotoService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 11240861..116c2bc0 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -83,9 +83,11 @@ public Flux uploadPhoto(Flux files, String requestMemberI } public Flux findAllByAlbumId(String albumId, String requestMemberId, String sort) { + String sortMethod = (sort == null) ? "CUSTOM" : sort.toUpperCase(); + return albumService.findByAlbumId(albumId, requestMemberId) .thenMany( - switch (sort) { + switch (sortMethod) { case "ASC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtAsc(albumId); case "DESC" -> photoRepository.findAllByAlbumIdOrderByCreatedAtDesc(albumId); default -> photoRepository.findAllByAlbumIdOrderByDisplayIndexDesc(albumId); From 3ddada228fcf76438b1d7b22675a192f558f679c Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 06:20:00 +0900 Subject: [PATCH 34/72] feat: add display index setting logic for createNewPhotoFileUrls --- .../photo/controller/PhotoController.java | 2 +- .../kr/mafoo/photo/domain/PhotoEntity.java | 4 +-- .../kr/mafoo/photo/service/PhotoService.java | 36 ++++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java index fb41db2a..7859d638 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/PhotoController.java @@ -53,7 +53,7 @@ public Flux uploadFileUrlPhoto( PhotoFileUrlUploadRequest request ){ return photoService - .createNewPhotoFileUrl(request.fileUrls(), request.albumId(), memberId) + .createNewPhotoFileUrls(request.fileUrls(), request.albumId(), memberId) .map(PhotoResponse::fromEntity); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java index 91b79dae..5b446b32 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java +++ b/photo-service/src/main/java/kr/mafoo/photo/domain/PhotoEntity.java @@ -82,14 +82,14 @@ public PhotoEntity updateDisplayIndex(Integer displayIndex) { return this; } - public static PhotoEntity newPhoto(String photoId, String photoUrl, BrandType brandType, String albumId, String ownerMemberId) { + public static PhotoEntity newPhoto(String photoId, String photoUrl, BrandType brandType, String albumId, Integer displayIndex, String ownerMemberId) { PhotoEntity photo = new PhotoEntity(); photo.photoId = photoId; photo.photoUrl = photoUrl; photo.brand = brandType; photo.ownerMemberId = ownerMemberId; photo.albumId = albumId; - photo.displayIndex = 0; + photo.displayIndex = displayIndex; photo.isNew = true; photo.createdAt = LocalDateTime.now(); photo.updatedAt = LocalDateTime.now(); diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java index 116c2bc0..e5f2a676 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/PhotoService.java @@ -19,6 +19,8 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import java.util.concurrent.atomic.AtomicInteger; + @Slf4j @RequiredArgsConstructor @@ -35,24 +37,34 @@ public Mono createNewPhotoByQrUrl(String qrUrl, String requestMembe return qrService .getFileFromQrUrl(qrUrl) .flatMap(fileDto -> objectStorageService.uploadFile(fileDto.fileByte()) - .flatMap(photoUrl -> createNewPhoto(photoUrl, fileDto.type(), null, requestMemberId)) + .flatMap(photoUrl -> createNewPhoto(photoUrl, fileDto.type(), requestMemberId)) ); } @Transactional - public Flux createNewPhotoFileUrl(String[] fileUrls, String albumId, String requestMemberId) { + public Flux createNewPhotoFileUrls(String[] fileUrls, String albumId, String requestMemberId) { return albumService.findByAlbumId(albumId, requestMemberId) - .flatMap(albumEntity -> albumService.increaseAlbumPhotoCount(albumId, fileUrls.length, requestMemberId)) - .thenMany( - Flux.fromArray(fileUrls) - .flatMap(fileUrl -> objectStorageService.setObjectPublicRead(fileUrl) - .flatMap(fileLink -> createNewPhoto(fileLink, BrandType.EXTERNAL, albumId, requestMemberId)) - ) - ); + .flatMapMany(albumEntity -> { + AtomicInteger displayIndex = new AtomicInteger(albumEntity.getPhotoCount()); + + return Flux.fromArray(fileUrls) + .concatMap(fileUrl -> + createNewPhotoFileUrl(fileUrl, BrandType.EXTERNAL, albumId, displayIndex.getAndIncrement(), requestMemberId) + ); + }); + } + + private Mono createNewPhotoFileUrl(String fileUrl, BrandType type, String albumId, Integer displayIndex, String requestMemberId) { + return objectStorageService.setObjectPublicRead(fileUrl) + .flatMap(fileLink -> { + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), fileLink, type, albumId, displayIndex, requestMemberId); + return albumService.increaseAlbumPhotoCount(albumId, 1, requestMemberId) + .then(photoRepository.save(photoEntity)); + }); } - private Mono createNewPhoto(String photoUrl, BrandType type, String albumId, String requestMemberId) { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, type, albumId, requestMemberId); + private Mono createNewPhoto(String photoUrl, BrandType type, String requestMemberId) { + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, type, null, 0, requestMemberId); return photoRepository.save(photoEntity); } @@ -74,7 +86,7 @@ public Flux uploadPhoto(Flux files, String requestMemberI }) .flatMap(bytes -> objectStorageService.uploadFile(bytes) .flatMap(photoUrl -> { - PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, null, requestMemberId); + PhotoEntity photoEntity = PhotoEntity.newPhoto(IdGenerator.generate(), photoUrl, BrandType.EXTERNAL, null, 0, requestMemberId); return photoRepository.save(photoEntity); })) .subscribeOn(Schedulers.boundedElastic()) From 88a73af4aab18399dd51b30040ed1732d5dc65ca Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 06:58:21 +0900 Subject: [PATCH 35/72] feat: implement handlePhotoBrandNotExistsException in WebExceptionHandler --- .../photo/config/WebExceptionHandler.java | 21 +++++++++++++++++++ .../src/main/resources/application.yaml | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java index fa97269f..ad014e49 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java @@ -4,6 +4,7 @@ import kr.mafoo.photo.controller.dto.response.ErrorResponse; import kr.mafoo.photo.exception.DomainException; import kr.mafoo.photo.exception.ErrorCode; +import kr.mafoo.photo.exception.PhotoBrandNotExistsException; import kr.mafoo.photo.service.SlackService; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -61,6 +62,26 @@ public ResponseEntity validException(Exception ex) { .body(response); } + @ExceptionHandler(PhotoBrandNotExistsException.class) + public Mono> handlePhotoBrandNotExistsException(ServerWebExchange exchange, PhotoBrandNotExistsException exception) { + String method = extractMethod(exchange); + String userAgent = extractUserAgent(exchange); + String fullPath = extractURI(exchange); + String originIp = extractOriginIp(exchange); + + return extractRequestBody(exchange).flatMap(requestBody -> { + logException(method, fullPath, originIp, userAgent, exception); + + return slackService.sendQrRelatedErrorNotification( + method, fullPath, requestBody, originIp, userAgent, exception.getMessage() + ).then(Mono.just( + ResponseEntity + .badRequest() + .body(ErrorResponse.fromErrorCode(exception.getErrorCode())) + )); + }); + } + @ExceptionHandler(ResponseStatusException.class) public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException exception) { return handleExceptionInternal(exchange, exception, (HttpStatus) exception.getStatusCode()); diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index 6e8f1c6f..ec381384 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -46,4 +46,5 @@ slack: webhook: token: ${SLACK_TOKEN} channel: - error: ${SLACK_ERROR_CHANNEL} \ No newline at end of file + error: ${SLACK_ERROR_CHANNEL} + qr: ${SLACK_QR_ERROR_CHANNEL} \ No newline at end of file From 20b06b718a46d0d9da0cca9f66496f5cd969e5c6 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 07:10:53 +0900 Subject: [PATCH 36/72] feat: create RequestBodyCachingFilter --- .../config/RequestBodyCachingFilter.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java b/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java new file mode 100644 index 00000000..5321ce67 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java @@ -0,0 +1,46 @@ +package kr.mafoo.photo.config; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebExchangeDecorator; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +public class RequestBodyCachingFilter implements WebFilter { + + // TODO: 추후 정리 필요 + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return DataBufferUtils.join(exchange.getRequest().getBody()) + .flatMap(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + + ServerHttpRequestDecorator decoratedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { + @Override + public Flux getBody() { + return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)); + } + }; + + ServerWebExchangeDecorator decoratedExchange = new ServerWebExchangeDecorator(exchange) { + @Override + public ServerHttpRequest getRequest() { + return decoratedRequest; + } + }; + + return chain.filter(decoratedExchange); + }); + } +} + From 8b4938ce4d08be31f159a1c375c3bda67397f3c4 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 26 Oct 2024 21:50:22 +0900 Subject: [PATCH 37/72] hotfix: remove problematic RequestBodyCachingFilter temporarily --- .../config/RequestBodyCachingFilter.java | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java b/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java index 5321ce67..1f8e31ec 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/RequestBodyCachingFilter.java @@ -1,46 +1,46 @@ -package kr.mafoo.photo.config; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpRequestDecorator; -import org.springframework.stereotype.Component; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.ServerWebExchangeDecorator; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Component -public class RequestBodyCachingFilter implements WebFilter { - - // TODO: 추후 정리 필요 - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return DataBufferUtils.join(exchange.getRequest().getBody()) - .flatMap(dataBuffer -> { - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - dataBuffer.read(bytes); - DataBufferUtils.release(dataBuffer); - - ServerHttpRequestDecorator decoratedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { - @Override - public Flux getBody() { - return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)); - } - }; - - ServerWebExchangeDecorator decoratedExchange = new ServerWebExchangeDecorator(exchange) { - @Override - public ServerHttpRequest getRequest() { - return decoratedRequest; - } - }; - - return chain.filter(decoratedExchange); - }); - } -} - +//package kr.mafoo.photo.config; +// +//import org.springframework.core.io.buffer.DataBuffer; +//import org.springframework.core.io.buffer.DataBufferUtils; +//import org.springframework.http.server.reactive.ServerHttpRequest; +//import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +//import org.springframework.stereotype.Component; +//import org.springframework.web.server.ServerWebExchange; +//import org.springframework.web.server.ServerWebExchangeDecorator; +//import org.springframework.web.server.WebFilter; +//import org.springframework.web.server.WebFilterChain; +//import reactor.core.publisher.Flux; +//import reactor.core.publisher.Mono; +// +//@Component +//public class RequestBodyCachingFilter implements WebFilter { +// +// // TODO: 추후 정리 필요 +// +// @Override +// public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { +// return DataBufferUtils.join(exchange.getRequest().getBody()) +// .flatMap(dataBuffer -> { +// byte[] bytes = new byte[dataBuffer.readableByteCount()]; +// dataBuffer.read(bytes); +// DataBufferUtils.release(dataBuffer); +// +// ServerHttpRequestDecorator decoratedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { +// @Override +// public Flux getBody() { +// return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)); +// } +// }; +// +// ServerWebExchangeDecorator decoratedExchange = new ServerWebExchangeDecorator(exchange) { +// @Override +// public ServerHttpRequest getRequest() { +// return decoratedRequest; +// } +// }; +// +// return chain.filter(decoratedExchange); +// }); +// } +//} +// From 88cdcaa1173a8514380817e749c51600716f2a28 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 01:13:57 +0900 Subject: [PATCH 38/72] hotfix: fix object storage filePath format error * feat: implement extractFileType --- .../kr/mafoo/photo/service/ObjectStorageService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index e786aa31..9072cb6c 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -39,7 +39,7 @@ public class ObjectStorageService { private long presignedUrlExpiration; public Mono uploadFile(byte[] fileByte) { - String keyName = "qr/" + UUID.randomUUID(); + String keyName = "qr/" + UUID.randomUUID() + ".jpeg"; ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(fileByte.length); @@ -71,7 +71,8 @@ public Mono createPreSignedUrls(String[] fileNames, String memberId) { } private URL generatePresignedUrl(String fileName, String memberId) { - String filePath = String.format("%s/photo/%s_%s", memberId, UUID.randomUUID(), fileName); + String fileType = extractFileType(fileName); + String filePath = String.format("%s/photo/%s.%s", memberId, UUID.randomUUID(), fileType); Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); return amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) @@ -92,6 +93,10 @@ public Mono setObjectPublicRead(String filePath) { }); } + private String extractFileType(String fileName) { + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + private String generateFileLink(String keyName) { return endpoint + "/" + bucketName + "/" + keyName; } From 4dd332ea03c9cb3d2505252e13c39b9c5e20f286 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:32:29 +0900 Subject: [PATCH 39/72] feat: add recap related values in application.yaml --- .../src/main/resources/application.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index 38a863cb..ecebca28 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -48,4 +48,21 @@ slack: token: ${SLACK_TOKEN} channel: error: ${SLACK_ERROR_CHANNEL} - qr: ${SLACK_QR_ERROR_CHANNEL} \ No newline at end of file + qr: ${SLACK_QR_ERROR_CHANNEL} + +recap: + tmp: + dir: ${RECAP_TMP_DIR} + file: + download: ${RECAP_DOWNLOAD_FILE} + photo: ${RECAP_PHOTO_FILE} + video: ${RECAP_VIDEO_FILE} + chip: ${RECAP_CHIP_IMAGE_FILE} + frame: ${RECAP_FRAME_FILE} + src: + background: ${RECAP_BACKGROUND_SOURCE} + icon: ${RECAP_ICON_SOURCE} + font: + aggro-m: ${FONT_AGGRO_M} + aggro-b: ${FONT_AGGRO_B} + pretendard: ${FONT_PRETENDARD} From dd8641fc99741c6c0c7b03150c504b7a9629c5fe Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:40:53 +0900 Subject: [PATCH 40/72] chore: add ffmpeg in dependencies list --- photo-service/build.gradle.kts | 1 + .../kr/mafoo/photo/config/FFmpegConfig.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/config/FFmpegConfig.java diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index db7418d0..475e4212 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation("io.opentelemetry:opentelemetry-exporter-zipkin:1.40.0") implementation("io.micrometer:micrometer-registry-prometheus:1.13.2") implementation("com.slack.api:slack-api-client:1.40.3") + implementation("net.bramp.ffmpeg:ffmpeg:0.8.0") } tasks.withType { diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/FFmpegConfig.java b/photo-service/src/main/java/kr/mafoo/photo/config/FFmpegConfig.java new file mode 100644 index 00000000..99c32749 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/config/FFmpegConfig.java @@ -0,0 +1,24 @@ +package kr.mafoo.photo.config; + +import lombok.extern.slf4j.Slf4j; +import net.bramp.ffmpeg.FFmpeg; +import net.bramp.ffmpeg.FFmpegExecutor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Slf4j +@Configuration +public class FFmpegConfig { + + @Value("${ffmpeg.path}") + private String ffmpegPath; + + @Bean + public FFmpegExecutor ffMpegExecutor() throws IOException { + FFmpeg ffmpeg = new FFmpeg(ffmpegPath); + return new FFmpegExecutor(ffmpeg); + } +} From ca46658ff7888353519d9b74f07eebd7f4f28fed Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:43:15 +0900 Subject: [PATCH 41/72] feat: implement recap api interface * feat: create RecapResponse --- .../java/kr/mafoo/photo/api/RecapApi.java | 33 +++++++++++++++++++ .../dto/response/RecapResponse.java | 19 +++++++++++ 2 files changed, 52 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/RecapResponse.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java new file mode 100644 index 00000000..14e7d18f --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java @@ -0,0 +1,33 @@ +package kr.mafoo.photo.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.mafoo.photo.annotation.RequestMemberId; +import kr.mafoo.photo.annotation.ULID; +import kr.mafoo.photo.controller.dto.response.RecapResponse; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +@Validated +@Tag(name = "리캡 관련 API", description = "리캡 생성 API") +@RequestMapping("/v1/recaps") +public interface RecapApi { + @Operation(summary = "리캡 생성", description = "앨범의 리캡을 생성합니다.") + @PostMapping + Mono createRecap( + @RequestMemberId + String memberId, + + @ULID + @Parameter(description = "앨범 ID", example = "test_album_id") + @RequestParam + String albumId, + + @Parameter(description = "정렬 종류", example = "ASC | DESC") + @RequestParam(required = false) + String sort + ); + +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/RecapResponse.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/RecapResponse.java new file mode 100644 index 00000000..97d7917f --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/response/RecapResponse.java @@ -0,0 +1,19 @@ +package kr.mafoo.photo.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "리캡 응답") +public record RecapResponse( + + @Schema(description = "리캡 URL", example = "recap_url") + String recapUrl + +) { + public static RecapResponse fromString( + String recapUrl + ) { + return new RecapResponse( + recapUrl + ); + } +} From 09cad495ac63a97b6c8445fa7afc5181121142ea Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:49:46 +0900 Subject: [PATCH 42/72] feat: implement downloadFilesForRecap in ObjectStorageService --- .../photo/service/ObjectStorageService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 9072cb6c..1a0ccb96 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -8,6 +8,7 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import kr.mafoo.photo.exception.PreSignedUrlExceedMaximum; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -15,11 +16,16 @@ import reactor.core.publisher.Mono; import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Date; +import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; @Slf4j @@ -38,6 +44,9 @@ public class ObjectStorageService { @Value("${cloud.aws.s3.presigned-url-expiration}") private long presignedUrlExpiration; + @Value("${recap.tmp.file.download}") + private String localDownloadPath; + public Mono uploadFile(byte[] fileByte) { String keyName = "qr/" + UUID.randomUUID() + ".jpeg"; @@ -100,4 +109,27 @@ private String extractFileType(String fileName) { private String generateFileLink(String keyName) { return endpoint + "/" + bucketName + "/" + keyName; } + + public Mono> downloadFilesForRecap(List fileUrls, String recapId) { + return Mono.defer(() -> { + try { + List downloadedPaths = IntStream.range(0, fileUrls.size()) + .mapToObj(i -> { + try { + String downloadedPath = String.format(localDownloadPath, recapId, i + 1); + FileUtils.copyURLToFile(new URL(fileUrls.get(i)), new File(downloadedPath)); + + return downloadedPath; + } catch (IOException e) { + throw new RuntimeException("Failed to download image for recap: " + fileUrls.get(i), e); + } + }) + .collect(Collectors.toList()); + + return Mono.just(downloadedPaths); + } catch (Exception e) { + return Mono.error(e); + } + }); + } } From 12b6168984d8c301fb65d1881ac95d3c8a59f3e7 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:51:27 +0900 Subject: [PATCH 43/72] feat: implement uploadFileFromPath in ObjectStorageService --- .../photo/service/ObjectStorageService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 1a0ccb96..2e931680 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -67,6 +67,31 @@ public Mono uploadFile(byte[] fileByte) { }); } + public Mono uploadFileFromPath(String filePath) { + return Mono.fromCallable(() -> { + File file = new File(filePath); + String keyName = "recap/" + file.getName(); + + if (!file.exists() || !file.isFile()) { + throw new IllegalArgumentException("Invalid file path: " + filePath); + } + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(file.length()); + objectMetadata.setContentType("application/octet-stream"); + + try (InputStream inputStream = FileUtils.openInputStream(file)) { + amazonS3Client.putObject( + new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + return generateFileLink(keyName); + } catch (IOException e) { + throw new RuntimeException("Failed to upload file to object storage: " + filePath, e); + } + }); + } + public Mono createPreSignedUrls(String[] fileNames, String memberId) { return Mono.fromCallable(() -> { if (fileNames.length > 30) { From 01b84d3f4310a7922ede44bdd84bab4826de770d Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:54:43 +0900 Subject: [PATCH 44/72] feat: implement generateAlbumChipForRecap in Graphics2dService --- .../photo/service/Graphics2dService.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java b/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java new file mode 100644 index 00000000..64f59f27 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java @@ -0,0 +1,172 @@ +package kr.mafoo.photo.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class Graphics2dService { + + @Value("${recap.src.font.pretendard}") + private String fontPretendardPath; + + @Value("${recap.src.icon}") + private String iconPath; + + @Value("${recap.tmp.file.chip}") + private String chipPath; + + public Mono generateAlbumChipForRecap(String recapId, String albumName, String albumType) { + + return Mono.fromCallable(() -> createAlbumChipImage(recapId, albumName, albumType)) + .flatMap(buffer -> ServerResponse.ok() + .contentType(MediaType.IMAGE_PNG) + .bodyValue(buffer)) + .onErrorResume(IOException.class, e -> ServerResponse.status(500) + .bodyValue("Error generating album chip image: " + e.getMessage())).then(); + } + + private final Map iconCache = new HashMap<>(); + + private FontMetrics cachedMetrics; + + private String createAlbumChipImage(String recapId, String albumName, String albumType) throws IOException { + int paddingLeftRight = 32; + int paddingTopBottom = 22; + int iconTextSpacing = 8; + int borderThickness = 2; + Font font = new Font(fontPretendardPath, Font.BOLD, 36); + + FontMetrics metrics = getCachedFontMetrics(font); + BufferedImage icon = getCachedIcon(albumType); + + int chipHeight = 90; + int chipWidth = calculateChipWidth( + icon.getWidth(), + metrics.stringWidth(albumName), + paddingLeftRight, + iconTextSpacing + ); + + // TODO: 이해가 쉬운 형태로 정리 필요 + int[] coordinates = calculateCoordinates( + chipHeight, + metrics.getHeight(), + paddingTopBottom, + paddingLeftRight, + icon.getHeight(), + icon.getWidth(), + iconTextSpacing, + metrics.getAscent() + ); + + BufferedImage image = new BufferedImage(chipWidth, chipHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = setupGraphics(image, font); + + drawRoundedRectangle( + g2d, + new Color(255, 255, 255, 180), + chipWidth, + coordinates[0], + metrics.getHeight(), + paddingTopBottom, + borderThickness, + chipHeight + ); + + drawIcon( + g2d, + icon, + paddingLeftRight, + coordinates[1] + ); + + drawText( + g2d, + albumName, + new Color(33, 37, 41), + coordinates[2], + coordinates[3] + ); + + g2d.dispose(); + + return saveImage(generateAlbumChipImagePath(recapId), image); + } + + private int calculateChipWidth(int iconWidth, int textWidth, int paddingLeftRight, int iconTextSpacing) { + return iconWidth + iconTextSpacing + textWidth + paddingLeftRight * 2; + } + + // TODO: 이해가 쉬운 형태로 정리 필요 + private int[] calculateCoordinates(int imageHeight, int textHeight, int paddingTopBottom, int paddingLeftRight, int iconHeight, int iconWidth, int iconTextSpacing, int ascent) { + int y = (imageHeight - textHeight - paddingTopBottom * 2) / 2; + int iconY = y + (textHeight + paddingTopBottom * 2 - iconHeight) / 2; + int textX = paddingLeftRight + iconWidth + iconTextSpacing; + int textBaselineY = y + paddingTopBottom + ascent; + return new int[]{y, iconY, textX, textBaselineY}; + } + + private String generateAlbumChipImagePath(String recapId) { + return String.format(chipPath, recapId); + } + + private FontMetrics getCachedFontMetrics(Font font) { + if (cachedMetrics == null) { + BufferedImage tempImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + Graphics2D tempGraphics = tempImage.createGraphics(); + tempGraphics.setFont(font); + cachedMetrics = tempGraphics.getFontMetrics(font); + tempGraphics.dispose(); + } + return cachedMetrics; + } + + private BufferedImage getCachedIcon(String iconType) throws IOException { + if (!iconCache.containsKey(iconType)) { + BufferedImage icon = ImageIO.read(new File(String.format(iconPath, iconType))); + iconCache.put(iconType, icon); + } + return iconCache.get(iconType); + } + + private Graphics2D setupGraphics(BufferedImage image, Font font) { + Graphics2D g2d = image.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setFont(font); + return g2d; + } + + private void drawRoundedRectangle(Graphics2D g2d, Color color, int rectangleWidth, int y, int textHeight, int paddingTopBottom, int borderThickness, int imageHeight) { + g2d.setStroke(new BasicStroke(borderThickness)); + g2d.setColor(color); + g2d.drawRoundRect(borderThickness / 2, y + borderThickness / 2, rectangleWidth - borderThickness, textHeight + paddingTopBottom * 2 - borderThickness, imageHeight, imageHeight); + g2d.fillRoundRect(borderThickness / 2, y + borderThickness / 2, rectangleWidth - borderThickness, textHeight + paddingTopBottom * 2 - borderThickness, imageHeight, imageHeight); + } + + private void drawIcon(Graphics2D g2d, BufferedImage icon, int x, int y) { + g2d.drawImage(icon, x, y, null); + } + + private void drawText(Graphics2D g2d, String text, Color color, int x, int y) { + g2d.setColor(color); + g2d.drawString(text, x, y); + } + + private String saveImage(String imagePath, BufferedImage image) throws IOException { + ImageIO.write(image, "png", new File(imagePath)); + return imagePath; + } + +} + From 41b1283f9a5e5309d7c2b460d2d074e23d7a74eb Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:55:21 +0900 Subject: [PATCH 45/72] feat: implement deleteSimilarNameFileForPath in LocalFileService --- .../mafoo/photo/service/LocalFileService.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/LocalFileService.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/LocalFileService.java b/photo-service/src/main/java/kr/mafoo/photo/service/LocalFileService.java new file mode 100644 index 00000000..8b99c1f8 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/LocalFileService.java @@ -0,0 +1,40 @@ +package kr.mafoo.photo.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.io.File; + +@RequiredArgsConstructor +@Service +public class LocalFileService { + + public Mono deleteSimilarNameFileForPath(String path, String name) { + return Mono.fromCallable(() -> new File(path).listFiles()) + .flatMapMany(files -> { + if (files == null) { + throw new RuntimeException("Failed to retrieve file list from the specified path: " + path); + } + return Flux.fromArray(files); + }) + .filter(file -> file.getName().contains(name) && file.isFile()) + .flatMap(file -> Mono.fromCallable(() -> { + boolean deleted = file.delete(); + if (!deleted) { + throw new RuntimeException("Failed to delete file: " + file.getAbsolutePath()); + } + return deleted; + }) + .subscribeOn(Schedulers.boundedElastic()) + .onErrorResume(e -> Mono.empty()) + .then() + ) + .then() + .onErrorResume(e -> { + throw new RuntimeException("Error occurred while processing files in the directory: " + path, e); + }); + } +} From 011ad9a3162267cd42253674976ec2a610f94c89 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:57:07 +0900 Subject: [PATCH 46/72] feat: implement createRecap in RecapService --- .../kr/mafoo/photo/service/RecapService.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java new file mode 100644 index 00000000..44db0d31 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -0,0 +1,176 @@ +package kr.mafoo.photo.service; + +import kr.mafoo.photo.domain.PhotoEntity; +import kr.mafoo.photo.util.IdGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import net.bramp.ffmpeg.builder.FFmpegBuilder; +import net.bramp.ffmpeg.FFmpegExecutor; + +@Slf4j +@RequiredArgsConstructor +@Service +public class RecapService { + + @Value("${recap.src.background}") + private String backgroundPath; + + @Value("${recap.src.font.aggro-m}") + private String fontAggroMPath; + + @Value("${recap.src.font.aggro-b}") + private String fontAggroBPath; + + @Value("${recap.tmp.dir}") + private String dirPath; + + @Value("${recap.tmp.file.chip}") + private String chipPath; + + @Value("${recap.tmp.file.frame}") + private String framePath; + + @Value("${recap.tmp.file.photo}") + private String photoPath; + + @Value("${recap.tmp.file.video}") + private String videoPath; + + private final AlbumService albumService; + private final PhotoService photoService; + + private final ObjectStorageService objectStorageService; + private final Graphics2dService graphics2dService; + private final FFmpegExecutor ffmpegExecutor; + private final LocalFileService localFileService; + + public Mono createRecap(String albumId, String requestMemberId, String sort) { + + String recapId = IdGenerator.generate(); + + return albumService.findByAlbumId(albumId, requestMemberId) + .flatMap(albumEntity -> { + String albumName = albumEntity.getName(); + String albumType = String.valueOf(albumEntity.getType()); + + // temp + String memberName = "시금치파슷하"; + + return graphics2dService.generateAlbumChipForRecap(recapId, albumName, albumType) + .then(generateRecapFrame(recapId, memberName, albumType)) + .then(photoService.findAllByAlbumId(albumId, requestMemberId, sort) + .collectList() + .flatMap(photoEntities -> { + List photoUrls = photoEntities.stream() + .map(PhotoEntity::getPhotoUrl) + .toList(); + + return objectStorageService.downloadFilesForRecap(photoUrls, recapId); + }) + ) + .flatMap(downloadedPath -> generateRecapPhotos(downloadedPath, recapId)) + .then(Mono.defer(() -> generateRecapVideo(recapId))) + .flatMap(objectStorageService::uploadFileFromPath) + ; + }) + .flatMap(recapUploadedPath -> + localFileService.deleteSimilarNameFileForPath(dirPath, recapId) + .thenReturn(recapUploadedPath) + ); + } + + private Mono generateRecapFrame(String recapId, String memberName, String albumType) { + return Mono.fromCallable(() -> { + try { + String recapBackgroundPath = String.format(backgroundPath, albumType); + String recapChipPath = String.format(chipPath, recapId); + String recapFramePath = String.format(framePath, recapId); + String recapCreatedDate = DateTimeFormatter.ofPattern("yyyy.MM.dd").format(LocalDate.now()); + + FFmpegBuilder builder = new FFmpegBuilder() + .addExtraArgs( + "-filter_complex", + String.format( + "[1]scale=w=-1:h=176[chip]; " + + "[0][chip]overlay=188:H-h-120[bg_w_chip]; " + + "[bg_w_chip]drawtext=fontfile=%s:text='@%s님의 RECAP':fontcolor=white@0.7:fontsize=72:x=(w-tw)/2:y=208[bg_w_title]; " + + "[bg_w_title]drawtext=fontfile=%s:text='%s':fontcolor=white:fontsize=72:x=w-tw-188:y=h-th-180;", + fontAggroBPath, memberName, + fontAggroMPath, recapCreatedDate + ) + ) + .addInput(recapBackgroundPath) + .addInput(recapChipPath) + .addOutput(recapFramePath) + .done(); + + ffmpegExecutor.createJob(builder).run(); + + return recapFramePath; + } catch (Exception e) { + log.error("Failed to generate recap frame", e); + throw new RuntimeException("Failed to generate recap_frame", e); + } + }); + } + + private Mono generateRecapPhotos(List downloadedPath, String recapId) { + + return Mono.fromRunnable(() -> { + String recapFramePath = String.format(framePath, recapId); + + FFmpegBuilder builder = new FFmpegBuilder() + .addInput(recapFramePath); + + for (String path : downloadedPath) { + builder.addInput(path); + } + + StringBuilder filterComplex = new StringBuilder(); + + for (int inputIndex = 1; inputIndex <= downloadedPath.size(); inputIndex++) { + filterComplex.append(String.format("[%d]scale='min(1200,iw)':'min(1776,ih)':force_original_aspect_ratio=decrease[photo_scaled_%d]; ", inputIndex, inputIndex)); + filterComplex.append(String.format("[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d];", inputIndex, inputIndex)); + } + + builder.addExtraArgs("-filter_complex", filterComplex.toString()); + + for (int outputIndex = 1; outputIndex <= downloadedPath.size(); outputIndex++) { + builder.addOutput(String.format(photoPath, recapId, outputIndex)) + .addExtraArgs("-map", String.format("[final%d]", outputIndex)); + } + + ffmpegExecutor.createJob(builder).run(); + }).then(); + } + + private Mono generateRecapVideo(String recapId) { + return Mono.fromCallable(() -> { + try { + String recapVideoPath = String.format(videoPath, recapId); + + FFmpegBuilder builder = new FFmpegBuilder() + .addExtraArgs("-r", "2") + .addInput(photoPath.replace("%s", recapId)) + .addOutput(recapVideoPath) + .done(); + + ffmpegExecutor.createJob(builder).run(); + + return recapVideoPath; + } catch (Exception e) { + log.error("Failed to generate recap video", e); + throw new RuntimeException("Failed to generate recap video", e); + } + }); + } + +} \ No newline at end of file From 1243acf3b8654b3a1e544f9daae8d26a8ed66523 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:58:01 +0900 Subject: [PATCH 47/72] feat: create RecapController --- .../photo/controller/RecapController.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java new file mode 100644 index 00000000..4a893c65 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java @@ -0,0 +1,26 @@ +package kr.mafoo.photo.controller; + +import kr.mafoo.photo.api.RecapApi; +import kr.mafoo.photo.controller.dto.response.RecapResponse; +import kr.mafoo.photo.service.RecapService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@RestController +public class RecapController implements RecapApi { + + private final RecapService recapService; + + @Override + public Mono createRecap( + String memberId, + String albumId, + String sort + ) { + return recapService.createRecap(albumId, memberId, sort) + .map(RecapResponse::fromString); + } + +} From 0a41edd52d36716e9677d08ac449a78835f66e27 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 21:00:46 +0900 Subject: [PATCH 48/72] hotfix: add file type checking logic for presigned-url generating method --- .../kr/mafoo/photo/exception/ErrorCode.java | 1 + .../exception/PreSignedUrlBannedFileType.java | 7 +++ .../photo/service/ObjectStorageService.java | 54 ++++++++++++------- 3 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlBannedFileType.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java index 8105a0d5..1a641113 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java @@ -19,6 +19,7 @@ public enum ErrorCode { PHOTO_DISPLAY_INDEX_NOT_VALID("PE0005", "옮기려는 대상 사진 인덱스가 유효하지 않습니다"), PRE_SIGNED_URL_EXCEED_MAXIMUM("OE0001", "한 번에 생성할 수 있는 Pre-signed url 최대치를 초과했습니다"), + PRE_SIGNED_URL_BANNED_FILE_TYPE("OE0002", "Pre-signed url 발급이 허용되지 않는 파일 형식입니다") ; private final String code; diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlBannedFileType.java b/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlBannedFileType.java new file mode 100644 index 00000000..f4f6127e --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/PreSignedUrlBannedFileType.java @@ -0,0 +1,7 @@ +package kr.mafoo.photo.exception; + +public class PreSignedUrlBannedFileType extends DomainException { + public PreSignedUrlBannedFileType() { + super(ErrorCode.PRE_SIGNED_URL_BANNED_FILE_TYPE); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 9072cb6c..c16232ff 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -6,12 +6,14 @@ import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import kr.mafoo.photo.exception.PreSignedUrlBannedFileType; import kr.mafoo.photo.exception.PreSignedUrlExceedMaximum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.ByteArrayInputStream; @@ -19,8 +21,8 @@ import java.net.URL; import java.util.Date; +import java.util.List; import java.util.UUID; -import java.util.stream.Stream; @Slf4j @Service @@ -59,25 +61,35 @@ public Mono uploadFile(byte[] fileByte) { } public Mono createPreSignedUrls(String[] fileNames, String memberId) { - return Mono.fromCallable(() -> { - if (fileNames.length > 30) { - throw new PreSignedUrlExceedMaximum(); - } - - return Stream.of(fileNames) - .map(fileName -> generatePresignedUrl(fileName, memberId).toString()) - .toArray(String[]::new); - }); + if (fileNames.length > 30) { + return Mono.error(new PreSignedUrlExceedMaximum()); + } + + return Flux.fromArray(fileNames) + .flatMap(fileName -> generatePresignedUrlForImage(fileName, memberId) + .map(URL::toString)) + .collectList() + .map(list -> list.toArray(new String[0])); } - private URL generatePresignedUrl(String fileName, String memberId) { - String fileType = extractFileType(fileName); - String filePath = String.format("%s/photo/%s.%s", memberId, UUID.randomUUID(), fileType); - Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); - - return amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) - .withMethod(HttpMethod.PUT) - .withExpiration(expiration)); + private Mono generatePresignedUrlForImage(String fileName, String memberId) { + List allowedFileTypes = List.of("jpg", "jpeg", "png"); + + return extractFileType(fileName) + .flatMap(fileType -> { + if (allowedFileTypes.contains(fileType)) { + String filePath = String.format("%s/photo/%s.%s", memberId, UUID.randomUUID(), fileType); + Date expiration = new Date(System.currentTimeMillis() + presignedUrlExpiration); + + return Mono.just( + amazonS3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucketName, filePath) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration)) + ); + } else { + return Mono.error(new PreSignedUrlBannedFileType()); + } + }); } public Mono setObjectPublicRead(String filePath) { @@ -93,8 +105,10 @@ public Mono setObjectPublicRead(String filePath) { }); } - private String extractFileType(String fileName) { - return fileName.substring(fileName.lastIndexOf(".") + 1); + private Mono extractFileType(String fileName) { + return Mono.just( + fileName.substring(fileName.lastIndexOf(".") + 1) + ); } private String generateFileLink(String keyName) { From 8987e43ce66e0820b927b475d867f5c665002c96 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:24:02 +0900 Subject: [PATCH 49/72] chore: add ffmpeg path to application.yaml --- photo-service/src/main/resources/application.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index ecebca28..80473eb1 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -50,6 +50,9 @@ slack: error: ${SLACK_ERROR_CHANNEL} qr: ${SLACK_QR_ERROR_CHANNEL} +ffmpeg: + path: ${FFMPEG_PATH} + recap: tmp: dir: ${RECAP_TMP_DIR} From c2a8df840629829b10eefe6261097cf2a2ed5a2f Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:25:11 +0900 Subject: [PATCH 50/72] refactor: change create recap api's parameter to request body --- .../src/main/java/kr/mafoo/photo/api/RecapApi.java | 10 +++++----- .../controller/dto/request/RecapCreateRequest.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/RecapCreateRequest.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java index 14e7d18f..b8c35c85 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java @@ -3,8 +3,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import kr.mafoo.photo.annotation.RequestMemberId; -import kr.mafoo.photo.annotation.ULID; +import kr.mafoo.photo.controller.dto.request.RecapCreateRequest; import kr.mafoo.photo.controller.dto.response.RecapResponse; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -20,10 +21,9 @@ Mono createRecap( @RequestMemberId String memberId, - @ULID - @Parameter(description = "앨범 ID", example = "test_album_id") - @RequestParam - String albumId, + @Valid + @RequestBody + RecapCreateRequest request, @Parameter(description = "정렬 종류", example = "ASC | DESC") @RequestParam(required = false) diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/RecapCreateRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/RecapCreateRequest.java new file mode 100644 index 00000000..bfd179e4 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/RecapCreateRequest.java @@ -0,0 +1,12 @@ +package kr.mafoo.photo.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.annotation.ULID; + +@Schema(description = "리캡 생성 요청") +public record RecapCreateRequest( + @ULID + @Schema(description = "앨범 ID", example = "test_album_id") + String albumId +) { +} From 3bdab9a771b0b2746b7f1591123397d999d094bf Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:26:15 +0900 Subject: [PATCH 51/72] feat: create getMemberInfo in MemberService --- .../kr/mafoo/photo/service/MemberService.java | 27 +++++++++++++++++++ .../kr/mafoo/photo/service/dto/MemberDto.java | 8 ++++++ 2 files changed, 35 insertions(+) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/dto/MemberDto.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java b/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java new file mode 100644 index 00000000..260710ab --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java @@ -0,0 +1,27 @@ +package kr.mafoo.photo.service; + +import kr.mafoo.photo.service.dto.MemberDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.ClientResponse; + +@RequiredArgsConstructor +@Service +public class MemberService { + + private final WebClient client; + + public Mono getMemberInfo(String authorizationToken) { + return client + .get() + .uri("https://gateway.mafoo.kr/user/v1/me") + .header("Authorization", "Bearer " + authorizationToken) + .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + ClientResponse::createException) + .bodyToMono(MemberDto.class); + } +} + diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/dto/MemberDto.java b/photo-service/src/main/java/kr/mafoo/photo/service/dto/MemberDto.java new file mode 100644 index 00000000..116f5ddb --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/dto/MemberDto.java @@ -0,0 +1,8 @@ +package kr.mafoo.photo.service.dto; + +public record MemberDto( + String memberId, + String name, + String profileImageUrl +) { +} From 43b13b81006cf1ae4bebe4bd00684b34c289d590 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:26:41 +0900 Subject: [PATCH 52/72] feat: add userInfo getting logic for create recap api --- .../main/java/kr/mafoo/photo/api/RecapApi.java | 5 ++++- .../mafoo/photo/controller/RecapController.java | 8 +++++--- .../java/kr/mafoo/photo/service/RecapService.java | 15 ++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java index b8c35c85..a4184489 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java @@ -27,7 +27,10 @@ Mono createRecap( @Parameter(description = "정렬 종류", example = "ASC | DESC") @RequestParam(required = false) - String sort + String sort, + + @RequestHeader("Authorization") + String authorizationToken ); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java index 4a893c65..aec37185 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java @@ -1,6 +1,7 @@ package kr.mafoo.photo.controller; import kr.mafoo.photo.api.RecapApi; +import kr.mafoo.photo.controller.dto.request.RecapCreateRequest; import kr.mafoo.photo.controller.dto.response.RecapResponse; import kr.mafoo.photo.service.RecapService; import lombok.RequiredArgsConstructor; @@ -16,10 +17,11 @@ public class RecapController implements RecapApi { @Override public Mono createRecap( String memberId, - String albumId, - String sort + RecapCreateRequest request, + String sort, + String authorizationToken ) { - return recapService.createRecap(albumId, memberId, sort) + return recapService.createRecap(request.albumId(), memberId, sort, authorizationToken) .map(RecapResponse::fromString); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 44db0d31..486a29a1 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -46,26 +46,24 @@ public class RecapService { private final AlbumService albumService; private final PhotoService photoService; + private final MemberService memberService; private final ObjectStorageService objectStorageService; private final Graphics2dService graphics2dService; private final FFmpegExecutor ffmpegExecutor; private final LocalFileService localFileService; - public Mono createRecap(String albumId, String requestMemberId, String sort) { + public Mono createRecap(String albumId, String requestMemberId, String sort, String token) { String recapId = IdGenerator.generate(); return albumService.findByAlbumId(albumId, requestMemberId) .flatMap(albumEntity -> { - String albumName = albumEntity.getName(); String albumType = String.valueOf(albumEntity.getType()); - // temp - String memberName = "시금치파슷하"; - - return graphics2dService.generateAlbumChipForRecap(recapId, albumName, albumType) - .then(generateRecapFrame(recapId, memberName, albumType)) + return graphics2dService.generateAlbumChipForRecap(recapId, albumEntity.getName(), albumType) + .then(memberService.getMemberInfo(token)) + .flatMap(memberInfo -> generateRecapFrame(recapId, memberInfo.name(), albumType)) .then(photoService.findAllByAlbumId(albumId, requestMemberId, sort) .collectList() .flatMap(photoEntities -> { @@ -78,8 +76,7 @@ public Mono createRecap(String albumId, String requestMemberId, String s ) .flatMap(downloadedPath -> generateRecapPhotos(downloadedPath, recapId)) .then(Mono.defer(() -> generateRecapVideo(recapId))) - .flatMap(objectStorageService::uploadFileFromPath) - ; + .flatMap(objectStorageService::uploadFileFromPath); }) .flatMap(recapUploadedPath -> localFileService.deleteSimilarNameFileForPath(dirPath, recapId) From 9830b5725f8567d7ba9e659a1d5d4d9b3b1ac7d0 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:33:24 +0900 Subject: [PATCH 53/72] feat: set max-size for recap --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 4 ++++ photo-service/src/main/resources/application.yaml | 1 + 2 files changed, 5 insertions(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 486a29a1..ced121dd 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -44,6 +44,9 @@ public class RecapService { @Value("${recap.tmp.file.video}") private String videoPath; + @Value("${recap.max-size}") + private int recapImageMaxSize; + private final AlbumService albumService; private final PhotoService photoService; private final MemberService memberService; @@ -68,6 +71,7 @@ public Mono createRecap(String albumId, String requestMemberId, String s .collectList() .flatMap(photoEntities -> { List photoUrls = photoEntities.stream() + .limit(recapImageMaxSize) .map(PhotoEntity::getPhotoUrl) .toList(); diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index 80473eb1..83eb86ae 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -54,6 +54,7 @@ ffmpeg: path: ${FFMPEG_PATH} recap: + max-size: 15 tmp: dir: ${RECAP_TMP_DIR} file: From 89aa935687dea53f1143a9a721344ac929ab5e32 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 2 Nov 2024 08:18:38 +0900 Subject: [PATCH 54/72] chore: create Dockerfile for custom-base-image --- photo-service/Dockerfile | 10 ++++++++++ photo-service/build.gradle.kts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 photo-service/Dockerfile diff --git a/photo-service/Dockerfile b/photo-service/Dockerfile new file mode 100644 index 00000000..69725225 --- /dev/null +++ b/photo-service/Dockerfile @@ -0,0 +1,10 @@ +FROM amazoncorretto:17-alpine3.17-jdk + +RUN apt-get update && \ + apt-get install -y ffmpeg && \ + apt-get clean + +RUN mkdir -p /usr/bin/recap/tmp && \ + wget -O /tmp/src.tar.gz $SRC_URL && \ + tar -xvzf /tmp/src.tar.gz -C /usr/bin/recap && \ + rm /tmp/src.tar.gz diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 475e4212..7e728236 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -53,7 +53,7 @@ jib { val imageTag: String? = System.getenv("IMAGE_TAG") val serverPort: String = System.getenv("SERVER_PORT") ?: "8080" from { - image = "amazoncorretto:17-alpine3.17-jdk" + image = "custom-base-image" } to { image = imageName From d6b900a5b8c25e4d74f4450cc9f27602f47138bd Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 2 Nov 2024 08:26:50 +0900 Subject: [PATCH 55/72] chore: add environment value for jib --- photo-service/Dockerfile | 3 ++- photo-service/build.gradle.kts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/photo-service/Dockerfile b/photo-service/Dockerfile index 69725225..76d4e5e5 100644 --- a/photo-service/Dockerfile +++ b/photo-service/Dockerfile @@ -1,10 +1,11 @@ FROM amazoncorretto:17-alpine3.17-jdk +ARG RECAP_SRC_URL RUN apt-get update && \ apt-get install -y ffmpeg && \ apt-get clean RUN mkdir -p /usr/bin/recap/tmp && \ - wget -O /tmp/src.tar.gz $SRC_URL && \ + wget -O /tmp/src.tar.gz $RECAP_SRC_URL && \ tar -xvzf /tmp/src.tar.gz -C /usr/bin/recap && \ rm /tmp/src.tar.gz diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 7e728236..30722bc8 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -52,6 +52,7 @@ jib { val imageName: String? = System.getenv("IMAGE_NAME") val imageTag: String? = System.getenv("IMAGE_TAG") val serverPort: String = System.getenv("SERVER_PORT") ?: "8080" + val recapSrcUrl: String? = System.getenv("RECAP_SRC_URL") from { image = "custom-base-image" } @@ -60,6 +61,9 @@ jib { tags = setOf(imageTag, "latest") } container { + environment = mapOf( + "RECAP_SRC_URL" to recapSrcUrl + ) jvmFlags = listOf( "-Dspring.profiles.active=$activeProfile", From 19397583fa48f26580f04aa45bc7a91c65289023 Mon Sep 17 00:00:00 2001 From: gmkim Date: Sat, 2 Nov 2024 08:29:46 +0900 Subject: [PATCH 56/72] chore: add RECAP_SRC_URL in jib-build.yaml --- .github/workflows/jib-build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/jib-build.yaml b/.github/workflows/jib-build.yaml index 70e27041..4ce92673 100644 --- a/.github/workflows/jib-build.yaml +++ b/.github/workflows/jib-build.yaml @@ -58,6 +58,7 @@ jobs: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} SERVER_PORT: ${{ inputs.server-port }} + RECAP_SRC_URL: ${{ inputs.recap-src-url }} run: | cd ${{ inputs.module-path }} && \ chmod +x gradlew && ./gradlew jib From 76504832c91e2435959836ed0bd8685bb1f5fb97 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 3 Nov 2024 13:25:39 +0900 Subject: [PATCH 57/72] feat: create recap properties --- .../photo/service/Graphics2dService.java | 19 +++--- .../photo/service/ObjectStorageService.java | 7 +-- .../kr/mafoo/photo/service/RecapService.java | 55 ++++++----------- .../service/properties/RecapProperties.java | 61 +++++++++++++++++++ .../src/main/resources/application.yaml | 20 ++---- 5 files changed, 92 insertions(+), 70 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/service/properties/RecapProperties.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java b/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java index 64f59f27..bf651f40 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/Graphics2dService.java @@ -1,6 +1,7 @@ package kr.mafoo.photo.service; -import org.springframework.beans.factory.annotation.Value; +import kr.mafoo.photo.service.properties.RecapProperties; +import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.server.ServerResponse; @@ -15,16 +16,10 @@ import java.util.Map; @Service +@RequiredArgsConstructor public class Graphics2dService { - @Value("${recap.src.font.pretendard}") - private String fontPretendardPath; - - @Value("${recap.src.icon}") - private String iconPath; - - @Value("${recap.tmp.file.chip}") - private String chipPath; + private final RecapProperties recapProperties; public Mono generateAlbumChipForRecap(String recapId, String albumName, String albumType) { @@ -45,7 +40,7 @@ private String createAlbumChipImage(String recapId, String albumName, String alb int paddingTopBottom = 22; int iconTextSpacing = 8; int borderThickness = 2; - Font font = new Font(fontPretendardPath, Font.BOLD, 36); + Font font = new Font(recapProperties.getPretendardFontPath(), Font.BOLD, 36); FontMetrics metrics = getCachedFontMetrics(font); BufferedImage icon = getCachedIcon(albumType); @@ -118,7 +113,7 @@ private int[] calculateCoordinates(int imageHeight, int textHeight, int paddingT } private String generateAlbumChipImagePath(String recapId) { - return String.format(chipPath, recapId); + return recapProperties.getChipFilePath(recapId); } private FontMetrics getCachedFontMetrics(Font font) { @@ -134,7 +129,7 @@ private FontMetrics getCachedFontMetrics(Font font) { private BufferedImage getCachedIcon(String iconType) throws IOException { if (!iconCache.containsKey(iconType)) { - BufferedImage icon = ImageIO.read(new File(String.format(iconPath, iconType))); + BufferedImage icon = ImageIO.read(new File(recapProperties.getIconPath(iconType))); iconCache.put(iconType, icon); } return iconCache.get(iconType); diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java index 904d0ea4..8a77dc3d 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/ObjectStorageService.java @@ -8,6 +8,7 @@ import com.amazonaws.services.s3.model.PutObjectRequest; import kr.mafoo.photo.exception.PreSignedUrlBannedFileType; import kr.mafoo.photo.exception.PreSignedUrlExceedMaximum; +import kr.mafoo.photo.service.properties.RecapProperties; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; @@ -28,7 +29,6 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; @Slf4j @Service @@ -46,8 +46,7 @@ public class ObjectStorageService { @Value("${cloud.aws.s3.presigned-url-expiration}") private long presignedUrlExpiration; - @Value("${recap.tmp.file.download}") - private String localDownloadPath; + private final RecapProperties recapProperties; public Mono uploadFile(byte[] fileByte) { String keyName = "qr/" + UUID.randomUUID() + ".jpeg"; @@ -155,7 +154,7 @@ public Mono> downloadFilesForRecap(List fileUrls, String re List downloadedPaths = IntStream.range(0, fileUrls.size()) .mapToObj(i -> { try { - String downloadedPath = String.format(localDownloadPath, recapId, i + 1); + String downloadedPath = recapProperties.getDownloadFilePath(recapId, i+1); FileUtils.copyURLToFile(new URL(fileUrls.get(i)), new File(downloadedPath)); return downloadedPath; diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index ced121dd..8ea586fd 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -1,6 +1,7 @@ package kr.mafoo.photo.service; import kr.mafoo.photo.domain.PhotoEntity; +import kr.mafoo.photo.service.properties.RecapProperties; import kr.mafoo.photo.util.IdGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,33 +21,12 @@ @Service public class RecapService { - @Value("${recap.src.background}") - private String backgroundPath; - - @Value("${recap.src.font.aggro-m}") - private String fontAggroMPath; - - @Value("${recap.src.font.aggro-b}") - private String fontAggroBPath; - - @Value("${recap.tmp.dir}") - private String dirPath; - - @Value("${recap.tmp.file.chip}") - private String chipPath; - - @Value("${recap.tmp.file.frame}") - private String framePath; - - @Value("${recap.tmp.file.photo}") - private String photoPath; - - @Value("${recap.tmp.file.video}") - private String videoPath; - @Value("${recap.max-size}") private int recapImageMaxSize; + @Value("${recap.path.tmp}") + private String tmpPath; + private final AlbumService albumService; private final PhotoService photoService; private final MemberService memberService; @@ -56,6 +36,8 @@ public class RecapService { private final FFmpegExecutor ffmpegExecutor; private final LocalFileService localFileService; + private final RecapProperties recapProperties; + public Mono createRecap(String albumId, String requestMemberId, String sort, String token) { String recapId = IdGenerator.generate(); @@ -83,7 +65,7 @@ public Mono createRecap(String albumId, String requestMemberId, String s .flatMap(objectStorageService::uploadFileFromPath); }) .flatMap(recapUploadedPath -> - localFileService.deleteSimilarNameFileForPath(dirPath, recapId) + localFileService.deleteSimilarNameFileForPath(tmpPath, recapId) .thenReturn(recapUploadedPath) ); } @@ -91,9 +73,7 @@ public Mono createRecap(String albumId, String requestMemberId, String s private Mono generateRecapFrame(String recapId, String memberName, String albumType) { return Mono.fromCallable(() -> { try { - String recapBackgroundPath = String.format(backgroundPath, albumType); - String recapChipPath = String.format(chipPath, recapId); - String recapFramePath = String.format(framePath, recapId); + String recapFramePath = recapProperties.getFrameFilePath(recapId); String recapCreatedDate = DateTimeFormatter.ofPattern("yyyy.MM.dd").format(LocalDate.now()); FFmpegBuilder builder = new FFmpegBuilder() @@ -104,12 +84,12 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin "[0][chip]overlay=188:H-h-120[bg_w_chip]; " + "[bg_w_chip]drawtext=fontfile=%s:text='@%s님의 RECAP':fontcolor=white@0.7:fontsize=72:x=(w-tw)/2:y=208[bg_w_title]; " + "[bg_w_title]drawtext=fontfile=%s:text='%s':fontcolor=white:fontsize=72:x=w-tw-188:y=h-th-180;", - fontAggroBPath, memberName, - fontAggroMPath, recapCreatedDate + recapProperties.getAggroBFontPath(), memberName, + recapProperties.getAggroMFontPath(), recapCreatedDate ) ) - .addInput(recapBackgroundPath) - .addInput(recapChipPath) + .addInput(recapProperties.getBackgroundPath(albumType)) + .addInput(recapProperties.getChipFilePath(recapId)) .addOutput(recapFramePath) .done(); @@ -126,10 +106,9 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin private Mono generateRecapPhotos(List downloadedPath, String recapId) { return Mono.fromRunnable(() -> { - String recapFramePath = String.format(framePath, recapId); FFmpegBuilder builder = new FFmpegBuilder() - .addInput(recapFramePath); + .addInput(recapProperties.getFrameFilePath(recapId)); for (String path : downloadedPath) { builder.addInput(path); @@ -145,8 +124,8 @@ private Mono generateRecapPhotos(List downloadedPath, String recap builder.addExtraArgs("-filter_complex", filterComplex.toString()); for (int outputIndex = 1; outputIndex <= downloadedPath.size(); outputIndex++) { - builder.addOutput(String.format(photoPath, recapId, outputIndex)) - .addExtraArgs("-map", String.format("[final%d]", outputIndex)); + builder.addOutput(recapProperties.getPhotoFilePath(recapId, outputIndex)) + .addExtraArgs("-map", String.format("[final%d]", outputIndex)); } ffmpegExecutor.createJob(builder).run(); @@ -156,11 +135,11 @@ private Mono generateRecapPhotos(List downloadedPath, String recap private Mono generateRecapVideo(String recapId) { return Mono.fromCallable(() -> { try { - String recapVideoPath = String.format(videoPath, recapId); + String recapVideoPath = recapProperties.getVideoFilePath(recapId); FFmpegBuilder builder = new FFmpegBuilder() .addExtraArgs("-r", "2") - .addInput(photoPath.replace("%s", recapId)) + .addInput(recapProperties.getPhotoFilePath(recapId)) .addOutput(recapVideoPath) .done(); diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/properties/RecapProperties.java b/photo-service/src/main/java/kr/mafoo/photo/service/properties/RecapProperties.java new file mode 100644 index 00000000..1dbc1d66 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/service/properties/RecapProperties.java @@ -0,0 +1,61 @@ +package kr.mafoo.photo.service.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "recap") +public class RecapProperties { + + @Value("${recap.path.tmp}") + private String tmpPath; + + @Value("${recap.path.src}") + private String srcPath; + + public String getDownloadFilePath(String identifier, int index) { + return String.format("%s%s_download_%02d.png", tmpPath, identifier, index); + } + + public String getPhotoFilePath(String identifier) { + return String.format("%s%s_photo_%%02d.png", tmpPath, identifier); + } + + public String getPhotoFilePath(String identifier, int index) { + return String.format("%s%s_photo_%02d.png", tmpPath, identifier, index); + } + + public String getVideoFilePath(String identifier) { + return String.format("%s%s_video.mp4", tmpPath, identifier); + } + + public String getChipFilePath(String identifier) { + return String.format("%s%s_chip.png", tmpPath, identifier); + } + + public String getFrameFilePath(String identifier) { + return String.format("%s%s_frame.png", tmpPath, identifier); + } + + public String getBackgroundPath(String identifier) { + return String.format("%sbackground/%s.png", srcPath, identifier); + } + + public String getIconPath(String identifier) { + return String.format("%sicon/%s.png", srcPath, identifier); + } + + public String getAggroMFontPath() { + return String.format("%sfont/SB_AggroOTF_M.otf", srcPath); + } + + public String getAggroBFontPath() { + return String.format("%sfont/SB_AggroOTF_B.otf", srcPath); + } + + public String getPretendardFontPath() { + return String.format("%sfont/Pretendard-SemiBold.otf", srcPath); + } +} + diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index 83eb86ae..b6656bfc 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -51,22 +51,10 @@ slack: qr: ${SLACK_QR_ERROR_CHANNEL} ffmpeg: - path: ${FFMPEG_PATH} + path: /usr/bin/ffmpeg recap: max-size: 15 - tmp: - dir: ${RECAP_TMP_DIR} - file: - download: ${RECAP_DOWNLOAD_FILE} - photo: ${RECAP_PHOTO_FILE} - video: ${RECAP_VIDEO_FILE} - chip: ${RECAP_CHIP_IMAGE_FILE} - frame: ${RECAP_FRAME_FILE} - src: - background: ${RECAP_BACKGROUND_SOURCE} - icon: ${RECAP_ICON_SOURCE} - font: - aggro-m: ${FONT_AGGRO_M} - aggro-b: ${FONT_AGGRO_B} - pretendard: ${FONT_PRETENDARD} + path: + tmp: /usr/bin/recap/tmp/ + src: /usr/bin/recap/src/ From 2597eb9341fe4bee19fcb8135536a45c182baa4e Mon Sep 17 00:00:00 2001 From: gmkim Date: Sun, 3 Nov 2024 13:39:31 +0900 Subject: [PATCH 58/72] chore: change recap_src_url as secrets value --- .github/workflows/jib-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jib-build.yaml b/.github/workflows/jib-build.yaml index 4ce92673..e8eddb01 100644 --- a/.github/workflows/jib-build.yaml +++ b/.github/workflows/jib-build.yaml @@ -58,7 +58,7 @@ jobs: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} SERVER_PORT: ${{ inputs.server-port }} - RECAP_SRC_URL: ${{ inputs.recap-src-url }} + RECAP_SRC_URL: ${{ secrets.RECAP_SRC_URL }} run: | cd ${{ inputs.module-path }} && \ chmod +x gradlew && ./gradlew jib From e0c524c25d4963a43fd7e1de7142914d04a90571 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 05:00:40 +0900 Subject: [PATCH 59/72] chore: remove custom-base-image related useless elements --- .github/workflows/jib-build.yaml | 1 - photo-service/Dockerfile | 11 ----------- photo-service/build.gradle.kts | 4 ---- 3 files changed, 16 deletions(-) delete mode 100644 photo-service/Dockerfile diff --git a/.github/workflows/jib-build.yaml b/.github/workflows/jib-build.yaml index e8eddb01..70e27041 100644 --- a/.github/workflows/jib-build.yaml +++ b/.github/workflows/jib-build.yaml @@ -58,7 +58,6 @@ jobs: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} SERVER_PORT: ${{ inputs.server-port }} - RECAP_SRC_URL: ${{ secrets.RECAP_SRC_URL }} run: | cd ${{ inputs.module-path }} && \ chmod +x gradlew && ./gradlew jib diff --git a/photo-service/Dockerfile b/photo-service/Dockerfile deleted file mode 100644 index 76d4e5e5..00000000 --- a/photo-service/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM amazoncorretto:17-alpine3.17-jdk - -ARG RECAP_SRC_URL -RUN apt-get update && \ - apt-get install -y ffmpeg && \ - apt-get clean - -RUN mkdir -p /usr/bin/recap/tmp && \ - wget -O /tmp/src.tar.gz $RECAP_SRC_URL && \ - tar -xvzf /tmp/src.tar.gz -C /usr/bin/recap && \ - rm /tmp/src.tar.gz diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 30722bc8..7e728236 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -52,7 +52,6 @@ jib { val imageName: String? = System.getenv("IMAGE_NAME") val imageTag: String? = System.getenv("IMAGE_TAG") val serverPort: String = System.getenv("SERVER_PORT") ?: "8080" - val recapSrcUrl: String? = System.getenv("RECAP_SRC_URL") from { image = "custom-base-image" } @@ -61,9 +60,6 @@ jib { tags = setOf(imageTag, "latest") } container { - environment = mapOf( - "RECAP_SRC_URL" to recapSrcUrl - ) jvmFlags = listOf( "-Dspring.profiles.active=$activeProfile", From 26334b8a6d59a3b8bb943dace797a46488ac258d Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 09:15:56 +0900 Subject: [PATCH 60/72] fix: specify docker image repo name --- photo-service/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 7e728236..6c526e78 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -53,7 +53,7 @@ jib { val imageTag: String? = System.getenv("IMAGE_TAG") val serverPort: String = System.getenv("SERVER_PORT") ?: "8080" from { - image = "custom-base-image" + image = "spinachpasta/custom-base-image:latest" } to { image = imageName From 8d17980b8a6ca83f1a6adf2177563c369efc6182 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 09:25:35 +0900 Subject: [PATCH 61/72] fix: change docker base image name --- photo-service/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 6c526e78..cda793ee 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -53,7 +53,7 @@ jib { val imageTag: String? = System.getenv("IMAGE_TAG") val serverPort: String = System.getenv("SERVER_PORT") ?: "8080" from { - image = "spinachpasta/custom-base-image:latest" + image = "spinachpasta/photo-service-base:latest" } to { image = imageName From 0c29ec6fc0b9e5e4b69757ca763540e0a8280361 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 13:49:06 +0900 Subject: [PATCH 62/72] refactor: change authorization header getting logic for recap api --- photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java | 4 ++-- .../main/java/kr/mafoo/photo/controller/RecapController.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java index a4184489..70326e53 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java @@ -7,6 +7,7 @@ import kr.mafoo.photo.annotation.RequestMemberId; import kr.mafoo.photo.controller.dto.request.RecapCreateRequest; import kr.mafoo.photo.controller.dto.response.RecapResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; @@ -29,8 +30,7 @@ Mono createRecap( @RequestParam(required = false) String sort, - @RequestHeader("Authorization") - String authorizationToken + ServerHttpRequest serverHttpRequest ); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java index aec37185..19f18667 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/RecapController.java @@ -5,6 +5,7 @@ import kr.mafoo.photo.controller.dto.response.RecapResponse; import kr.mafoo.photo.service.RecapService; import lombok.RequiredArgsConstructor; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @@ -19,8 +20,10 @@ public Mono createRecap( String memberId, RecapCreateRequest request, String sort, - String authorizationToken + ServerHttpRequest serverHttpRequest ) { + String authorizationToken = serverHttpRequest.getHeaders().getFirst("Authorization"); + return recapService.createRecap(request.albumId(), memberId, sort, authorizationToken) .map(RecapResponse::fromString); } From 0e6fc222674e26ce370be8928a016035538f0b41 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 14:56:11 +0900 Subject: [PATCH 63/72] feat: add tmp file deleting logic for recap api error case --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 8ea586fd..1e22eb3f 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -67,7 +67,8 @@ public Mono createRecap(String albumId, String requestMemberId, String s .flatMap(recapUploadedPath -> localFileService.deleteSimilarNameFileForPath(tmpPath, recapId) .thenReturn(recapUploadedPath) - ); + ) + .doOnError(e -> localFileService.deleteSimilarNameFileForPath(tmpPath, recapId)); } private Mono generateRecapFrame(String recapId, String memberName, String albumType) { From 1a4a7c96b7fe02dbcbfc512f28841e4bf426f029 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 15:12:11 +0900 Subject: [PATCH 64/72] docs: add comment for RecapApi --- photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java | 1 + 1 file changed, 1 insertion(+) diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java index 70326e53..0a9a4464 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/RecapApi.java @@ -30,6 +30,7 @@ Mono createRecap( @RequestParam(required = false) String sort, + // Authorization Header를 받아올 목적 ServerHttpRequest serverHttpRequest ); From e54538c4693a800ce4bce8c2a8817610df57c54c Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 15:33:06 +0900 Subject: [PATCH 65/72] fix: fix getMemberInfo to use proper endpoint * feat: create MafooUserApiFailed exception * fix: set proper custom exception for getMemberInfo --- .../main/java/kr/mafoo/photo/exception/ErrorCode.java | 4 +++- .../kr/mafoo/photo/exception/MafooUserApiFailed.java | 7 +++++++ .../java/kr/mafoo/photo/service/MemberService.java | 10 +++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/exception/MafooUserApiFailed.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java index 1a641113..4dc16e89 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java @@ -19,7 +19,9 @@ public enum ErrorCode { PHOTO_DISPLAY_INDEX_NOT_VALID("PE0005", "옮기려는 대상 사진 인덱스가 유효하지 않습니다"), PRE_SIGNED_URL_EXCEED_MAXIMUM("OE0001", "한 번에 생성할 수 있는 Pre-signed url 최대치를 초과했습니다"), - PRE_SIGNED_URL_BANNED_FILE_TYPE("OE0002", "Pre-signed url 발급이 허용되지 않는 파일 형식입니다") + PRE_SIGNED_URL_BANNED_FILE_TYPE("OE0002", "Pre-signed url 발급이 허용되지 않는 파일 형식입니다"), + + MAFOO_USER_API_FAILED("MUE0001", "마푸의 user-service API 호출이 실패했습니다") ; private final String code; diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/MafooUserApiFailed.java b/photo-service/src/main/java/kr/mafoo/photo/exception/MafooUserApiFailed.java new file mode 100644 index 00000000..62027f08 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/MafooUserApiFailed.java @@ -0,0 +1,7 @@ +package kr.mafoo.photo.exception; + +public class MafooUserApiFailed extends DomainException { + public MafooUserApiFailed() { + super(ErrorCode.MAFOO_USER_API_FAILED); + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java b/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java index 260710ab..4fc6bfc1 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/MemberService.java @@ -1,7 +1,9 @@ package kr.mafoo.photo.service; +import kr.mafoo.photo.exception.MafooUserApiFailed; import kr.mafoo.photo.service.dto.MemberDto; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import org.springframework.web.reactive.function.client.WebClient; @@ -11,16 +13,18 @@ @Service public class MemberService { + @Value("${app.gateway.endpoint}") + private String endpoint; + private final WebClient client; public Mono getMemberInfo(String authorizationToken) { return client .get() - .uri("https://gateway.mafoo.kr/user/v1/me") + .uri(endpoint + "/user/v1/me") .header("Authorization", "Bearer " + authorizationToken) .retrieve() - .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), - ClientResponse::createException) + .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new MafooUserApiFailed())) .bodyToMono(MemberDto.class); } } From 461626bf66f8b19a20f50dd082aca3525ce24c62 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 16:09:39 +0900 Subject: [PATCH 66/72] style: remove underbar from RuntimException message --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 1e22eb3f..8e0e9b9d 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -99,7 +99,7 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin return recapFramePath; } catch (Exception e) { log.error("Failed to generate recap frame", e); - throw new RuntimeException("Failed to generate recap_frame", e); + throw new RuntimeException("Failed to generate recap frame", e); } }); } From 485a2195226ae37b6d724505acfb82c94443203c Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 16:22:00 +0900 Subject: [PATCH 67/72] feat: add debug option for every ffmpeg commend --- .../kr/mafoo/photo/service/RecapService.java | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 8e0e9b9d..5fafeffa 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -78,6 +78,7 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin String recapCreatedDate = DateTimeFormatter.ofPattern("yyyy.MM.dd").format(LocalDate.now()); FFmpegBuilder builder = new FFmpegBuilder() + .addExtraArgs("-loglevel", "debug") .addExtraArgs( "-filter_complex", String.format( @@ -107,29 +108,38 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin private Mono generateRecapPhotos(List downloadedPath, String recapId) { return Mono.fromRunnable(() -> { - - FFmpegBuilder builder = new FFmpegBuilder() + try { + FFmpegBuilder builder = new FFmpegBuilder() + .addExtraArgs("-loglevel", "debug") .addInput(recapProperties.getFrameFilePath(recapId)); - for (String path : downloadedPath) { - builder.addInput(path); - } + for (String path : downloadedPath) { + builder.addInput(path); + } - StringBuilder filterComplex = new StringBuilder(); + StringBuilder filterComplex = new StringBuilder(); - for (int inputIndex = 1; inputIndex <= downloadedPath.size(); inputIndex++) { - filterComplex.append(String.format("[%d]scale='min(1200,iw)':'min(1776,ih)':force_original_aspect_ratio=decrease[photo_scaled_%d]; ", inputIndex, inputIndex)); - filterComplex.append(String.format("[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d];", inputIndex, inputIndex)); - } + for (int inputIndex = 1; inputIndex <= downloadedPath.size(); inputIndex++) { + filterComplex.append(String.format( + "[%d]scale='min(1200,iw)':'min(1776,ih)':force_original_aspect_ratio=decrease[photo_scaled_%d]; ", + inputIndex, inputIndex)); + filterComplex.append(String.format( + "[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d];", + inputIndex, inputIndex)); + } - builder.addExtraArgs("-filter_complex", filterComplex.toString()); + builder.addExtraArgs("-filter_complex", filterComplex.toString()); - for (int outputIndex = 1; outputIndex <= downloadedPath.size(); outputIndex++) { - builder.addOutput(recapProperties.getPhotoFilePath(recapId, outputIndex)) - .addExtraArgs("-map", String.format("[final%d]", outputIndex)); - } + for (int outputIndex = 1; outputIndex <= downloadedPath.size(); outputIndex++) { + builder.addOutput(recapProperties.getPhotoFilePath(recapId, outputIndex)) + .addExtraArgs("-map", String.format("[final%d]", outputIndex)); + } - ffmpegExecutor.createJob(builder).run(); + ffmpegExecutor.createJob(builder).run(); + } catch (Exception e) { + log.error("Failed to generate recap photos", e); + throw new RuntimeException("Failed to generate recap photos", e); + } }).then(); } @@ -139,8 +149,9 @@ private Mono generateRecapVideo(String recapId) { String recapVideoPath = recapProperties.getVideoFilePath(recapId); FFmpegBuilder builder = new FFmpegBuilder() + .addExtraArgs("-loglevel", "debug") .addExtraArgs("-r", "2") - .addInput(recapProperties.getPhotoFilePath(recapId)) + .addInput(recapProperties.getPhotoFilePath(recapId)) .addOutput(recapVideoPath) .done(); From c66aa81948ee6de0b3b095e75d6a060db69e5450 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 16:47:59 +0900 Subject: [PATCH 68/72] fix: remove wrong semicolon in ffmpeg commend --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 5fafeffa..d6bcdeaa 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -85,7 +85,7 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin "[1]scale=w=-1:h=176[chip]; " + "[0][chip]overlay=188:H-h-120[bg_w_chip]; " + "[bg_w_chip]drawtext=fontfile=%s:text='@%s님의 RECAP':fontcolor=white@0.7:fontsize=72:x=(w-tw)/2:y=208[bg_w_title]; " + - "[bg_w_title]drawtext=fontfile=%s:text='%s':fontcolor=white:fontsize=72:x=w-tw-188:y=h-th-180;", + "[bg_w_title]drawtext=fontfile=%s:text='%s':fontcolor=white:fontsize=72:x=w-tw-188:y=h-th-180", recapProperties.getAggroBFontPath(), memberName, recapProperties.getAggroMFontPath(), recapCreatedDate ) From 59bd3285c039550abbd67f0c452025ba66570549 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 16:54:25 +0900 Subject: [PATCH 69/72] fix: fix code to remove wrong semicolon in ffmpeg commend --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index d6bcdeaa..2add3177 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -123,9 +123,14 @@ private Mono generateRecapPhotos(List downloadedPath, String recap filterComplex.append(String.format( "[%d]scale='min(1200,iw)':'min(1776,ih)':force_original_aspect_ratio=decrease[photo_scaled_%d]; ", inputIndex, inputIndex)); + filterComplex.append(String.format( - "[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d];", + "[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d]", inputIndex, inputIndex)); + + if (inputIndex < downloadedPath.size()) { + filterComplex.append(";"); + } } builder.addExtraArgs("-filter_complex", filterComplex.toString()); From ec5d2338fd272dc59455286d7fb05e89bf788755 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 17:06:01 +0900 Subject: [PATCH 70/72] fix: fix to specify input index for ffmpeg commend --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 2add3177..75f17207 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -125,7 +125,7 @@ private Mono generateRecapPhotos(List downloadedPath, String recap inputIndex, inputIndex)); filterComplex.append(String.format( - "[recap_bg][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d]", + "[0][photo_scaled_%d]overlay=(W-w)/2:(H-h)/2+80[final%d]", inputIndex, inputIndex)); if (inputIndex < downloadedPath.size()) { From f9f12fa24c206cd3079d6aabd340ab1e1bc23b1e Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Mon, 4 Nov 2024 17:51:51 +0900 Subject: [PATCH 71/72] refactor: change ffmpeg debug related lines as comment --- .../src/main/java/kr/mafoo/photo/service/RecapService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java index 75f17207..dd2bb1ec 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java +++ b/photo-service/src/main/java/kr/mafoo/photo/service/RecapService.java @@ -78,7 +78,7 @@ private Mono generateRecapFrame(String recapId, String memberName, Strin String recapCreatedDate = DateTimeFormatter.ofPattern("yyyy.MM.dd").format(LocalDate.now()); FFmpegBuilder builder = new FFmpegBuilder() - .addExtraArgs("-loglevel", "debug") +// .addExtraArgs("-loglevel", "debug") .addExtraArgs( "-filter_complex", String.format( @@ -110,7 +110,7 @@ private Mono generateRecapPhotos(List downloadedPath, String recap return Mono.fromRunnable(() -> { try { FFmpegBuilder builder = new FFmpegBuilder() - .addExtraArgs("-loglevel", "debug") +// .addExtraArgs("-loglevel", "debug") .addInput(recapProperties.getFrameFilePath(recapId)); for (String path : downloadedPath) { @@ -154,7 +154,7 @@ private Mono generateRecapVideo(String recapId) { String recapVideoPath = recapProperties.getVideoFilePath(recapId); FFmpegBuilder builder = new FFmpegBuilder() - .addExtraArgs("-loglevel", "debug") +// .addExtraArgs("-loglevel", "debug") .addExtraArgs("-r", "2") .addInput(recapProperties.getPhotoFilePath(recapId)) .addOutput(recapVideoPath) From 2bba7a51c34c2d67b462c48a3e0c2090b0785865 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 7 Nov 2024 23:17:41 +0900 Subject: [PATCH 72/72] refactor: fix recap max size as 5 --- photo-service/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo-service/src/main/resources/application.yaml b/photo-service/src/main/resources/application.yaml index b6656bfc..73b5812e 100644 --- a/photo-service/src/main/resources/application.yaml +++ b/photo-service/src/main/resources/application.yaml @@ -54,7 +54,7 @@ ffmpeg: path: /usr/bin/ffmpeg recap: - max-size: 15 + max-size: 5 path: tmp: /usr/bin/recap/tmp/ src: /usr/bin/recap/src/