From 4dd332ea03c9cb3d2505252e13c39b9c5e20f286 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 09:32:29 +0900 Subject: [PATCH 01/19] 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 02/19] 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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 8987e43ce66e0820b927b475d867f5c665002c96 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sun, 27 Oct 2024 22:24:02 +0900 Subject: [PATCH 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 17/19] 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 18/19] 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 19/19] 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