Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ditda.backend.domain.commission.draft.processor;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import ditda.backend.domain.commission.draft.service.DraftWatermarkTransitionService;
import ditda.backend.global.image.WatermarkImageProcessor;
import ditda.backend.global.image.dto.WatermarkedImage;
import ditda.backend.global.image.exception.ImageProcessingException;
import ditda.backend.global.s3.enums.BucketType;
import ditda.backend.global.s3.enums.S3ContentType;
import ditda.backend.global.s3.manager.S3FileManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "watermark.mode", havingValue = "local")
public class LocalWatermarkProcessor implements WatermarkProcessor {

private static final BucketType BUCKET = BucketType.PRIVATE;

private final WatermarkImageProcessor watermarkImageProcessor;
private final S3FileManager s3FileManager;
private final WatermarkKeyResolver watermarkKeyResolver;
private final DraftWatermarkTransitionService draftWatermarkTransitionService;

// 로컬 구현체의 경우에는 리소스 제한을 위해 단일 스레드로 진행
@Async("watermarkExecutor")
@Override
public void process(Long draftFileId, String originalKey) {

long start = System.nanoTime();
try {
String watermarkedKey = createWatermarked(originalKey);
draftWatermarkTransitionService.complete(draftFileId, watermarkedKey);
log.info("워터마크 완료. draftFileId={}, elapsedMs={}", draftFileId, elapsedMs(start));
} catch (ImageProcessingException exception) {
log.error("워터마크 영구 실패(이미지 문제). draftFileId={}", draftFileId, exception);
draftWatermarkTransitionService.failPermanently(draftFileId);
} catch (Exception e) {
log.error("워터마크 실패. draftFileId={}, elapsedMs={}", draftFileId, elapsedMs(start), e);
draftWatermarkTransitionService.fail(draftFileId);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

private String createWatermarked(String originalKey) throws IOException {

byte[] watermarked;
// 원본 s3 다운로드 후 워터마크 진행
try (InputStream original = s3FileManager.download(BUCKET, originalKey)) {
WatermarkedImage image = watermarkImageProcessor.createWatermarkedPreview(original);
watermarked = image.bytes();
}

String watermarkedKey = watermarkKeyResolver.resolve(originalKey);
s3FileManager.upload(BUCKET, watermarkedKey, watermarked, S3ContentType.PNG.getContentType());

return watermarkedKey;
}

private long elapsedMs(long start) {
return (System.nanoTime() - start) / 1_000_000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ditda.backend.domain.commission.draft.processor;

import org.springframework.stereotype.Component;

@Component
public class WatermarkKeyResolver {

private static final String WATERMARK_DIR = "wm";

public String resolve(String originalKey) {

// 워터마크 출력 키 생성
int lastSlash = originalKey.lastIndexOf('/');
if (lastSlash < 0) {
throw new IllegalArgumentException("유효하지 않은 S3 키: " + originalKey);
}
String dir = originalKey.substring(0, lastSlash);
String filename = originalKey.substring(lastSlash + 1);
int lastDot = filename.lastIndexOf('.');
String baseName = lastDot > 0 ? filename.substring(0, lastDot) : filename;

return dir + "/" + WATERMARK_DIR + "/" + baseName + ".png";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ditda.backend.domain.commission.draft.processor;

/**
* 워터마크 처리 주체 추상화.
* 구현체는 처리 완료/실패 시 상태 전이까지 책임진다.
* - local: 서버 내 처리 (LocalWatermarkProcessor)
* - lambda: AWS Lambda 위임 (LambdaWatermarkProcessor)
*/
public interface WatermarkProcessor {

void process(Long draftFileId, String originalKey);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
package ditda.backend.domain.commission.draft.service;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import ditda.backend.domain.commission.draft.entity.CommissionDraftFile;
import ditda.backend.domain.commission.draft.entity.enums.WatermarkStatus;
import ditda.backend.domain.commission.draft.processor.WatermarkProcessor;
import ditda.backend.domain.commission.draft.repository.CommissionDraftFileRepository;
import ditda.backend.global.image.WatermarkImageProcessor;
import ditda.backend.global.image.dto.WatermarkedImage;
import ditda.backend.global.image.exception.ImageProcessingException;
import ditda.backend.global.s3.enums.BucketType;
import ditda.backend.global.s3.enums.S3ContentType;
import ditda.backend.global.s3.manager.S3FileManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -24,13 +16,9 @@
@RequiredArgsConstructor
public class DraftWatermarkService {

private static final BucketType BUCKET = BucketType.PRIVATE;
private static final String WATERMARK_DIR = "wm";

private final CommissionDraftFileRepository commissionDraftFileRepository;
private final WatermarkImageProcessor watermarkImageProcessor;
private final S3FileManager s3FileManager;
private final DraftWatermarkTransitionService draftWatermarkTransitionService;
private final WatermarkProcessor watermarkProcessor;

public void watermarkDraftFiles(Long draftId) {

Expand All @@ -40,59 +28,23 @@ public void watermarkDraftFiles(Long draftId) {
WatermarkStatus.PROCESSING
);

files.forEach(f -> process(f.getId(), f.getFileUrl()));
files.forEach(f -> safeProcess(f.getId(), f.getFileUrl()));
}

// 워터마크 재처리
@Async("watermarkExecutor")
public void reprocessFile(Long draftFileId) {

String originalKey = draftWatermarkTransitionService.getOriginalKey(draftFileId);
process(draftFileId, originalKey);
}

private String createWatermarked(String originalKey) throws IOException {

byte[] watermarked;
// 원본 s3 다운로드 후 워터마크 진행
try (InputStream original = s3FileManager.download(BUCKET, originalKey)) {
WatermarkedImage image = watermarkImageProcessor.createWatermarkedPreview(original);
watermarked = image.bytes();
}

// 워터마크 진행된 파일 s3 업로드 (commission/draft/{uuid}.png -> commission/draft/wm/{uuid}.png)
int lastSlash = originalKey.lastIndexOf('/');
if (lastSlash < 0) {
throw new IllegalArgumentException("유효하지 않은 S3 키: " + originalKey);
}
String dir = originalKey.substring(0, lastSlash);
String filename = originalKey.substring(lastSlash + 1);
int lastDot = filename.lastIndexOf('.');
String baseName = lastDot > 0 ? filename.substring(0, lastDot) : filename;
String watermarkedKey = dir + "/" + WATERMARK_DIR + "/" + baseName + ".png";

s3FileManager.upload(BUCKET, watermarkedKey, watermarked, S3ContentType.PNG.getContentType());

return watermarkedKey;
safeProcess(draftFileId, originalKey);
}

private void process(Long fileId, String originalKey) {

long start = System.nanoTime();
// 예외가 터져도 나머지 파일은 진행
private void safeProcess(Long draftFileId, String originalKey) {
try {
String watermarkedKey = createWatermarked(originalKey);
draftWatermarkTransitionService.complete(fileId, watermarkedKey);
log.info("워터마크 완료. draftFileId={}, elapsedMs={}", fileId, elapsedMs(start));
} catch (ImageProcessingException exception) {
log.error("워터마크 영구 실패(이미지 문제). draftFileId={}", fileId, exception);
draftWatermarkTransitionService.failPermanently(fileId);
watermarkProcessor.process(draftFileId, originalKey);
} catch (Exception e) {
log.error("워터마크 실패. draftFileId={}, elapsedMs={}", fileId, elapsedMs(start), e);
draftWatermarkTransitionService.fail(fileId);
log.error("워터마크 처리 위임 실패. draftFileId={}", draftFileId, e);
draftWatermarkTransitionService.fail(draftFileId);
}
}

private long elapsedMs(long start) {
return (System.nanoTime() - start) / 1_000_000;
}
}
Comment thread
Jong0128 marked this conversation as resolved.
3 changes: 3 additions & 0 deletions src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ spring:
swagger:
server-url: http://localhost:8080
server-description: Local Server

watermark:
mode: local
5 changes: 4 additions & 1 deletion src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ spring:

swagger:
server-url: https://api.ditda.kr
server-description: Production Server
server-description: Production Server

watermark:
mode: local
Loading