diff --git a/KakaoTalk_20240724_132729019.jpg b/KakaoTalk_20240724_132729019.jpg new file mode 100644 index 0000000..adfb996 Binary files /dev/null and b/KakaoTalk_20240724_132729019.jpg differ diff --git a/WIN_20240605_10_27_05_Pro.jpg b/WIN_20240605_10_27_05_Pro.jpg new file mode 100644 index 0000000..398b958 Binary files /dev/null and b/WIN_20240605_10_27_05_Pro.jpg differ diff --git a/WIN_20240624_21_14_17_Pro.jpg b/WIN_20240624_21_14_17_Pro.jpg new file mode 100644 index 0000000..01d4256 Binary files /dev/null and b/WIN_20240624_21_14_17_Pro.jpg differ diff --git a/src/main/java/ongjong/namanmoo/controller/ChallengeController.java b/src/main/java/ongjong/namanmoo/controller/ChallengeController.java index 2d89b46..e44b108 100644 --- a/src/main/java/ongjong/namanmoo/controller/ChallengeController.java +++ b/src/main/java/ongjong/namanmoo/controller/ChallengeController.java @@ -303,7 +303,7 @@ public ApiResponse> saveFaceTimeAnswer( fileType = FileType.VIDEO; // S3에 파일 업로드 및 URL 저장 - String uploadedUrl = awsS3Service.uploadFile(answerFile); + String uploadedUrl = awsS3Service.uploadOriginalFile(answerFile); // Answer 업데이트 answerService.modifyAnswer(challengeId, uploadedUrl); diff --git a/src/main/java/ongjong/namanmoo/service/AwsS3Service.java b/src/main/java/ongjong/namanmoo/service/AwsS3Service.java index 42bedd7..8e99218 100644 --- a/src/main/java/ongjong/namanmoo/service/AwsS3Service.java +++ b/src/main/java/ongjong/namanmoo/service/AwsS3Service.java @@ -5,6 +5,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -89,6 +92,29 @@ public String uploadFile(MultipartFile multipartFile) throws IOException { return uploadFileUrl; } + /** + * MultipartFile을 S3에 업로드하고 업로드된 파일의 URL을 반환하는 메소드. + * + * @param multipartFile 업로드할 MultipartFile + * @return 업로드된 파일의 URL + * @throws IOException 파일 변환 또는 업로드 중 발생하는 예외 + */ + public String uploadOriginalFile(MultipartFile multipartFile) throws IOException { + log.info("Converting MultipartFile to File..."); + File uploadFile = convertFile(multipartFile) + .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File convert fail")); + + String fileType = determineFileType(multipartFile); + String fileName = generateFileName(uploadFile, fileType); + + log.info("Uploading file to S3: {}", fileName); + String uploadFileUrl = uploadFileToS3(uploadFile, fileName); + log.info("File uploaded to S3: {}", uploadFileUrl); + + removeNewFile(uploadFile); + return uploadFileUrl; + } + /** * 이미지를 최적화하는 메소드. * @@ -167,9 +193,8 @@ private String determineFileType(MultipartFile multipartFile) { * @throws IOException 파일 변환 중 발생하는 예외 */ private Optional convertFile(MultipartFile file) throws IOException { -// String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); - String fileName = file.getOriginalFilename(); - assert fileName != null; + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); +// String fileName = file.getOriginalFilename(); File convertFile = new File(fileName); if (convertFile.createNewFile()) { @@ -190,7 +215,13 @@ private Optional convertFile(MultipartFile file) throws IOException { * @return String 고유한 파일 이름 */ private String generateFileName(File uploadFile, String fileType) { - return fileType + "/" + UUID.randomUUID() + "_" + uploadFile.getName(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS") + .withZone(ZoneId.systemDefault()); + String formattedDate = formatter.format(Instant.now()); + +// return fileType + "/" + UUID.randomUUID() + "_" + formattedDate + "_" + uploadFile.getName(); + return fileType + "/" + formattedDate + "_" + uploadFile.getName(); + } /** @@ -261,6 +292,32 @@ public void delete(String fileName) { amazonS3Client.deleteObject(bucket, fileName); } + // 업로드 실패 시 재시도 로직 추가 + public String uploadFileWithRetry(MultipartFile multipartFile, int retries) throws IOException, InterruptedException { + File uploadFile = convertFile(multipartFile) + .orElseThrow(() -> new IllegalArgumentException("Incorrect conversion from MultipartFile to File")); + + String fileType = determineFileType(multipartFile); + String fileName = generateFileName(uploadFile, fileType); + + for (int i = 0; i < retries; i++) { + try { + amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + String uploadedUrl = getS3FileURL(fileName); + log.info("Successfully uploaded file to S3 on retry " + i + ": {}", uploadedUrl); + + removeNewFile(uploadFile); // Remember to clean up local file after upload + return uploadedUrl; + } catch (Exception ex) { + log.warn("업로드에 실패하였습니다. 재시도하는중... (시도: {}/{})", i + 1, retries); + Thread.sleep(3000); // waiting for 3 seconds before the next retry + } + } + throw new RuntimeException("Failed to upload file after " + retries + " retries."); + } + // 오디오 파일 고정 경로 생성 public String uploadAudioFile(MultipartFile multipartFile, String s3Path) throws IOException { log.info("Converting MultipartFile to File..."); diff --git a/src/main/java/ongjong/namanmoo/service/SharedFileService.java b/src/main/java/ongjong/namanmoo/service/SharedFileService.java index 96cc1ee..4f454b1 100644 --- a/src/main/java/ongjong/namanmoo/service/SharedFileService.java +++ b/src/main/java/ongjong/namanmoo/service/SharedFileService.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +43,7 @@ import java.util.stream.Collectors; @Slf4j +@EnableAsync @Service public class SharedFileService { @@ -61,6 +63,7 @@ public class SharedFileService { private final String region; private final ReentrantLock lock = new ReentrantLock(); + // 병합 작업 상태를 저장하는 Map private final Map mergeTasks = new ConcurrentHashMap<>(); public SharedFileService( @@ -83,7 +86,6 @@ public SharedFileService( this.region = region; } - // 이미지 업로드 메서드 @Transactional public Map uploadImageFile(Challenge challenge, MultipartFile photo, FileType fileType) throws Exception { Map response = new HashMap<>(); @@ -95,143 +97,207 @@ public Map uploadImageFile(Challenge challenge, MultipartFile ph throw new IllegalArgumentException("Invalid file type: " + fileType); } - // S3에 파일 업로드 및 URL 저장 - String uploadedUrl = awsS3Service.uploadFile(photo); + try { + // S3에 파일 업로드 및 URL 저장 + String uploadedUrl = awsS3Service.uploadOriginalFile(photo); +// String uploadedUrl = awsS3Service.uploadFileWithRetry(photo, 3); - Optional optionalLucky = luckyRepository.findByFamilyFamilyIdAndRunningTrue(family.getFamilyId()); - Lucky lucky = optionalLucky.orElseThrow(() -> new RuntimeException("Running Lucky not found for family")); + Optional optionalLucky = luckyRepository.findByFamilyFamilyIdAndRunningTrue(family.getFamilyId()); + Lucky lucky = optionalLucky.orElseThrow(() -> new RuntimeException("Running Lucky not found for family")); - try { // SharedFile 엔티티 저장 SharedFile sharedFile = new SharedFile(); sharedFile.setFileName(uploadedUrl); sharedFile.setFileType(FileType.IMAGE); sharedFile.setChallengeNum(challenge.getChallengeNum()); sharedFile.setCreateDate(System.currentTimeMillis()); - sharedFile.setLucky(lucky); // Lucky 엔티티 설정 + sharedFile.setLucky(lucky); sharedFileRepository.save(sharedFile); - // 저장이 성공했는지 확인하는 로그 - System.out.println("SharedFile 저장 성공: " + sharedFile.getSharedFileId()); + log.info("SharedFile saved successfully: {}", sharedFile.getSharedFileId()); + + response.put("url", uploadedUrl); + response.put("message", "Photo uploaded successfully"); + } catch (Exception e) { - e.printStackTrace(); // Or use appropriate logging + log.error("Error occurred while uploading image file", e); + throw e; } - response.put("url", uploadedUrl); - response.put("message", "Photo uploaded successfully"); - return response; } - public void checkAndMergeImages(int challengeNum, Lucky lucky) throws IOException { - final int MAX_WAIT_TIME = 15000; // 최대 대기 시간 15초 - final int SLEEP_INTERVAL = 3000; // 3초 간격으로 재시도 - long startTime = System.currentTimeMillis(); - - Map> groupedFiles = new HashMap<>(); - Pattern pattern = Pattern.compile("screenshot_(\\d+)"); - - while (true) { - List sharedFiles; - - lock.lock(); + @Async + public CompletableFuture> uploadImageFileAsync(Challenge challenge, MultipartFile photo, FileType fileType) { + return CompletableFuture.supplyAsync(() -> { try { - sharedFiles = sharedFileRepository.findByChallengeNumAndLucky(challengeNum, lucky); - } finally { - lock.unlock(); - } - - for (SharedFile sharedFile : sharedFiles) { - String fileName = sharedFile.getFileName(); - Matcher matcher = pattern.matcher(fileName); - - if (matcher.find()) { - String group = matcher.group(1); - groupedFiles.computeIfAbsent(group, k -> new ArrayList<>()).add(sharedFile); - } - } - - // 모든 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 - boolean allGroupsHaveEnoughImages = groupedFiles.values().stream().allMatch(list -> list.size() >= 4); - - if (allGroupsHaveEnoughImages) { - break; // 모든 그룹에 4개 이상의 이미지가 있는 경우 병합을 시작 - } - - if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { - throw new IOException("이미지 업로드 대기 시간이 초과되었습니다."); + return uploadImageFile(challenge, photo, fileType); + } catch (Exception e) { + throw new RuntimeException(e); } + }); + } + @Async + public CompletableFuture scheduleMergeImagesAsync(int challengeNum, Lucky lucky) { + return CompletableFuture.runAsync(() -> { try { - Thread.sleep(SLEEP_INTERVAL); - } catch (InterruptedException e) { - throw new IOException("병합 대기 중 인터럽트 발생", e); + scheduleMergeImages(challengeNum, lucky); + } catch (Exception e) { + log.error("Error occurred while scheduling image merge", e); + throw new RuntimeException(e); } - } - - // 그룹 키를 정렬하여 순서대로 처리 - List sortedKeys = new ArrayList<>(groupedFiles.keySet()); - Collections.sort(sortedKeys); - - // 각 그룹의 이미지를 BufferedImage 리스트로 변환 - for (String key : sortedKeys) { - List sharedFilesInGroup = groupedFiles.get(key); + }); + } - // 중복되지 않도록 최대 4개의 고유한 이미지 URL을 선택 - Set uniqueImageUrls = sharedFilesInGroup.stream() - .map(SharedFile::getFileName) - .collect(Collectors.toSet()); +// // TODO: 방법 1: (동기) 이미지 업로드와 병합 분리 +// // 병합을 수행하는 메소드 +// public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOException { +// final int MAX_WAIT_TIME = 15000; // 최대 대기 시간 15초 +// final int SLEEP_INTERVAL = 3000; // 3초 간격으로 재시도 +// long startTime = System.currentTimeMillis(); +// +// Map> groupedFiles = new HashMap<>(); +// Pattern pattern = Pattern.compile("screenshot_(\\d+)"); +// +// while (true) { +// List sharedFiles; +// +// lock.lock(); +// try { +// sharedFiles = sharedFileRepository.findByChallengeNumAndLucky(challengeNum, lucky); +// } finally { +// lock.unlock(); +// } +// +// for (SharedFile sharedFile : sharedFiles) { +// String fileName = sharedFile.getFileName(); +// Matcher matcher = pattern.matcher(fileName); +// +// if (matcher.find()) { +// String group = matcher.group(1); +// groupedFiles.computeIfAbsent(group, k -> new ArrayList<>()).add(sharedFile); +// } +// } +// +//// // 모든 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 +//// boolean allGroupsHaveEnoughImages = groupedFiles.values().stream().allMatch(list -> list.size() >= 4); +//// +//// if (allGroupsHaveEnoughImages) { +//// break; // 모든 그룹에 4개 이상의 이미지가 있는 경우 병합을 시작 +//// } +// // 어떠한 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 +// boolean anyGroupHasEnoughImages = groupedFiles.values().stream().anyMatch(list -> list.size() >= 4); +// +// if (anyGroupHasEnoughImages || System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { +// break; // 어떠한 그룹에 4개 이상의 이미지가 있거나, 최대 대기 시간이 지나면 병합을 시작합니다. +// } +// +// if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { +// throw new IOException("이미지 업로드 대기 시간이 초과되었습니다."); +// } +// try { +// Thread.sleep(SLEEP_INTERVAL); +// } catch (InterruptedException e) { +// throw new IOException("병합 대기 중 인터럽트 발생", e); +// } +// } +// +// // 그룹 키를 정렬하여 순서대로 처리 +// List sortedKeys = new ArrayList<>(groupedFiles.keySet()); +// Collections.sort(sortedKeys); +// +// // 각 그룹의 이미지를 BufferedImage 리스트로 변환하고 병합 +// for (String key : sortedKeys) { +// List sharedFilesInGroup = groupedFiles.get(key); +// +// Set uniqueImageUrls = sharedFilesInGroup.stream() +// .map(SharedFile::getFileName) +// .collect(Collectors.toSet()); +// +//// // 만약 고유한 이미지 수가 4개 미만이면 예외를 던짐 +//// if (uniqueImageUrls.size() < 4) { +//// throw new IOException("충분한 수의 고유 이미지를 찾을 수 없습니다."); +//// } +// // 만약 고유한 이미지 수가 4개 미만이면 이 그룹 무시하고 다음 그룹으로 넘어감 +// if (uniqueImageUrls.size() < 4) { +// continue; +// } +// +// List selectedImages = uniqueImageUrls.stream() +// .limit(4) +// .map(url -> { +// try { +// return ImageIO.read(new URL(url)); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// }) +// .collect(Collectors.toList()); +// +// // 빈 공간을 투명하게 채워 4개가 되도록 처리 +// while (selectedImages.size() < 4) { +// BufferedImage emptyImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); +// selectedImages.add(emptyImage); +// } +// +// // UUID 생성 및 파일 이름 설정 +// String uuid = UUID.randomUUID().toString(); +//// String baseName = "merged-images/" + uuid + "_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; +// String baseName = "merged-images/" + "life4cut_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; +// BufferedImage mergedImage = ImageMerger.mergeImages(selectedImages); +// +// // 병합된 이미지를 S3에 업로드 +// String mergedImageUrl = uploadMergedImageToS3(mergedImage, bucket, baseName); +// +// // 병합된 이미지 URL을 데이터베이스에 저장 +// SharedFile mergedFile = new SharedFile(); +// mergedFile.setChallengeNum(challengeNum); +// mergedFile.setCreateDate(System.currentTimeMillis()); +// mergedFile.setFileName(mergedImageUrl); +// mergedFile.setFileType(FileType.IMAGE); +// mergedFile.setLucky(lucky); +// +// sharedFileRepository.save(mergedFile); // 데이터베이스에 새 SharedFile 저장 +// +// // 디버깅 로그 추가 +// System.out.println("Merged image saved: " + mergedImageUrl); +// } +// } - if (uniqueImageUrls.size() < 4) { - throw new IOException("충분한 수의 고유 이미지를 찾을 수 없습니다."); - } + // TODO: 방법 2: (비동기) 병합을 서버 측에서 스케줄링 + // 병합 작업을 비동기적으로 예약하는 메서드 + @Async + public CompletableFuture scheduleMergeImages(int challengeNum, Lucky lucky) { + String key = challengeNum + "_" + lucky.getLuckyId(); - List selectedImages = uniqueImageUrls.stream() - .limit(4) - .map(url -> { - try { - return ImageIO.read(new URL(url)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()); + // 병합 작업이 이미 진행 중인 경우 종료 + if (mergeTasks.putIfAbsent(key, true) != null) { + return CompletableFuture.completedFuture(null); + } - // 빈 공간을 투명하게 채워 4개가 되도록 처리 - while (selectedImages.size() < 4) { - BufferedImage emptyImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); // 너비와 높이를 100으로 설정하여 빈 이미지를 추가 - selectedImages.add(emptyImage); + CompletableFuture.runAsync(() -> { + try { + // 이미지 업로드가 완료될 때까지 대기 + waitForImageUploadCompletion(challengeNum, lucky); + // 모든 이미지가 업로드된 후 병합 수행 + mergeImagesIfNeeded(challengeNum, lucky); + } catch (IOException e) { + log.error("이미지 병합 작업 실패", e); + } finally { + // 병합 작업이 완료되면 플래그를 제거 + mergeTasks.remove(key); } + }); - // UUID 생성 및 파일 이름 설정 - String uuid = UUID.randomUUID().toString(); - String baseName = "merged-images/" + uuid + "_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; - BufferedImage mergedImage = ImageMerger.mergeImages(selectedImages); // 이미지를 병합하여 새로운 BufferedImage를 생성 - - // 병합된 이미지를 S3에 업로드 - String mergedImageUrl = uploadMergedImageToS3(mergedImage, bucket, baseName); - - // 병합된 이미지 URL을 데이터베이스에 저장 - SharedFile mergedFile = new SharedFile(); - mergedFile.setChallengeNum(challengeNum); - mergedFile.setCreateDate(System.currentTimeMillis()); - mergedFile.setFileName(mergedImageUrl); - mergedFile.setFileType(FileType.IMAGE); - mergedFile.setLucky(lucky); - - sharedFileRepository.save(mergedFile); // 데이터베이스에 새 SharedFile 저장 - } + return CompletableFuture.completedFuture(null); } - // TODO: 방법 1: (동기) 이미지 업로드와 병합 분리 - // 병합을 수행하는 메소드 - public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOException { - final int MAX_WAIT_TIME = 15000; // 최대 대기 시간 15초 + // 이미지 업로드가 완료될 때까지 대기하는 메소드 + private void waitForImageUploadCompletion(int challengeNum, Lucky lucky) throws IOException { + final int MAX_WAIT_TIME = 20000; // 최대 대기 시간 20초 final int SLEEP_INTERVAL = 3000; // 3초 간격으로 재시도 long startTime = System.currentTimeMillis(); - Map> groupedFiles = new HashMap<>(); - Pattern pattern = Pattern.compile("screenshot_(\\d+)"); - while (true) { List sharedFiles; @@ -242,44 +308,72 @@ public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOExceptio lock.unlock(); } - for (SharedFile sharedFile : sharedFiles) { - String fileName = sharedFile.getFileName(); - Matcher matcher = pattern.matcher(fileName); - - if (matcher.find()) { - String group = matcher.group(1); - groupedFiles.computeIfAbsent(group, k -> new ArrayList<>()).add(sharedFile); - } + // 모든 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 + boolean allGroupsHaveEnoughImages = sharedFiles.stream() + .map(SharedFile::getFileName) + .filter(fileName -> fileName.startsWith("screenshot_")) + .collect(Collectors.groupingBy(fileName -> { + Matcher matcher = Pattern.compile("screenshot_(\\d+)").matcher(fileName); + return matcher.find() ? matcher.group(1) : ""; + })) + .values().stream() + .allMatch(list -> list.size() >= 4); + if (allGroupsHaveEnoughImages) { + break; // 모든 그룹에 4개 이상의 이미지가 있는 경우 대기 종료 } - -// // 모든 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 -// boolean allGroupsHaveEnoughImages = groupedFiles.values().stream().allMatch(list -> list.size() >= 4); -// -// if (allGroupsHaveEnoughImages) { -// break; // 모든 그룹에 4개 이상의 이미지가 있는 경우 병합을 시작 +// // 어떤 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 +// boolean anyGroupHasEnoughImages = sharedFiles.stream() +// .map(SharedFile::getFileName) +// .filter(fileName -> fileName.startsWith("screenshot_")) +// .collect(Collectors.groupingBy(fileName -> { +// Matcher matcher = Pattern.compile("screenshot_(\\d+)").matcher(fileName); +// return matcher.find() ? matcher.group(1) : ""; +// })) +// .values().stream() +// .anyMatch(list -> list.size() >= 4); +// if (anyGroupHasEnoughImages) { +// break; // 어떠한 그룹에 4개 이상의 이미지가 있거나, 최대 대기 시간이 지나면 대기를 종료합니다. // } - // 어떠한 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 - boolean anyGroupHasEnoughImages = groupedFiles.values().stream().anyMatch(list -> list.size() >= 4); - - if (anyGroupHasEnoughImages || System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { - break; // 어떠한 그룹에 4개 이상의 이미지가 있거나, 최대 대기 시간이 지나면 병합을 시작합니다. - } if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { throw new IOException("이미지 업로드 대기 시간이 초과되었습니다."); } + try { Thread.sleep(SLEEP_INTERVAL); } catch (InterruptedException e) { throw new IOException("병합 대기 중 인터럽트 발생", e); } } + } + + // 병합이 필요한 경우 이미지를 병합하는 메서드 + public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOException { + Map> groupedFiles = new HashMap<>(); + Pattern pattern = Pattern.compile("screenshot_(\\d+)"); + + List sharedFiles; + + lock.lock(); + try { + sharedFiles = sharedFileRepository.findByChallengeNumAndLucky(challengeNum, lucky); + } finally { + lock.unlock(); + } + + for (SharedFile sharedFile : sharedFiles) { + String fileName = sharedFile.getFileName(); + Matcher matcher = pattern.matcher(fileName); + + if (matcher.find()) { + String group = matcher.group(1); + groupedFiles.computeIfAbsent(group, k -> new ArrayList<>()).add(sharedFile); + } + } - // 그룹 키를 정렬하여 순서대로 처리 List sortedKeys = new ArrayList<>(groupedFiles.keySet()); Collections.sort(sortedKeys); - // 각 그룹의 이미지를 BufferedImage 리스트로 변환하고 병합 for (String key : sortedKeys) { List sharedFilesInGroup = groupedFiles.get(key); @@ -287,7 +381,6 @@ public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOExceptio .map(SharedFile::getFileName) .collect(Collectors.toSet()); -// // 만약 고유한 이미지 수가 4개 미만이면 예외를 던짐 // if (uniqueImageUrls.size() < 4) { // throw new IOException("충분한 수의 고유 이미지를 찾을 수 없습니다."); // } @@ -307,22 +400,18 @@ public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOExceptio }) .collect(Collectors.toList()); - // 빈 공간을 투명하게 채워 4개가 되도록 처리 while (selectedImages.size() < 4) { BufferedImage emptyImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); selectedImages.add(emptyImage); } - // UUID 생성 및 파일 이름 설정 String uuid = UUID.randomUUID().toString(); // String baseName = "merged-images/" + uuid + "_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; String baseName = "merged-images/" + "life4cut_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; BufferedImage mergedImage = ImageMerger.mergeImages(selectedImages); - // 병합된 이미지를 S3에 업로드 String mergedImageUrl = uploadMergedImageToS3(mergedImage, bucket, baseName); - // 병합된 이미지 URL을 데이터베이스에 저장 SharedFile mergedFile = new SharedFile(); mergedFile.setChallengeNum(challengeNum); mergedFile.setCreateDate(System.currentTimeMillis()); @@ -330,173 +419,10 @@ public void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOExceptio mergedFile.setFileType(FileType.IMAGE); mergedFile.setLucky(lucky); - sharedFileRepository.save(mergedFile); // 데이터베이스에 새 SharedFile 저장 - - // 디버깅 로그 추가 - System.out.println("Merged image saved: " + mergedImageUrl); + sharedFileRepository.save(mergedFile); } } -// // TODO: 방법 2: (비동기) 병합을 서버 측에서 스케줄링 -// // 이미지 업로드 후 병합을 예약하는 메소드 -// public void scheduleMergeImages(int challengeNum, Lucky lucky) { -// // 이미지 업로드 후 병합 예약 -// CompletableFuture.supplyAsync(() -> { -// try { -// // 이미지 업로드가 완료될 때까지 대기 -// waitForImageUploadCompletion(challengeNum, lucky); -// -// // 모든 이미지가 업로드된 후 병합 수행 -// mergeImagesIfNeeded(challengeNum, lucky); -// } catch (IOException e) { -// // 예외 처리 로직 추가 -// e.printStackTrace(); -// } -// return null; -// }); -// } - - // 병합 작업을 비동기적으로 예약하는 메서드 - @Async - public CompletableFuture scheduleMergeImages(int challengeNum, Lucky lucky) { - String key = challengeNum + "_" + lucky.getLuckyId(); - - // 병합 작업이 이미 진행 중인 경우 종료 - if (mergeTasks.putIfAbsent(key, true) != null) { - return CompletableFuture.completedFuture(null); - } - - CompletableFuture.runAsync(() -> { - try { - // 이미지 업로드가 완료될 때까지 대기 - waitForImageUploadCompletion(challengeNum, lucky); - // 모든 이미지가 업로드된 후 병합 수행 - mergeImagesIfNeeded(challengeNum, lucky); - } catch (IOException e) { - e.printStackTrace(); // Or use appropriate logging - } finally { - // 병합 작업이 완료되면 플래그를 제거 - mergeTasks.remove(key); - } - }); - - return CompletableFuture.completedFuture(null); - } - - // 이미지 업로드가 완료될 때까지 대기하는 메소드 - private void waitForImageUploadCompletion(int challengeNum, Lucky lucky) throws IOException { - final int MAX_WAIT_TIME = 15000; // 최대 대기 시간 15초 - final int SLEEP_INTERVAL = 3000; // 3초 간격으로 재시도 - long startTime = System.currentTimeMillis(); - - while (true) { - List sharedFiles; - - lock.lock(); - try { - sharedFiles = sharedFileRepository.findByChallengeNumAndLucky(challengeNum, lucky); - } finally { - lock.unlock(); - } - - // 모든 그룹에 대해 4개 이상의 이미지를 가진 그룹이 있는지 확인 - boolean allGroupsHaveEnoughImages = sharedFiles.stream() - .map(SharedFile::getFileName) - .filter(fileName -> fileName.startsWith("screenshot_")) - .collect(Collectors.groupingBy(fileName -> { - Matcher matcher = Pattern.compile("screenshot_(\\d+)").matcher(fileName); - return matcher.find() ? matcher.group(1) : ""; - })) - .values().stream() - .allMatch(list -> list.size() >= 4); - - if (allGroupsHaveEnoughImages) { - break; // 모든 그룹에 4개 이상의 이미지가 있는 경우 대기 종료 - } - - if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) { - throw new IOException("이미지 업로드 대기 시간이 초과되었습니다."); - } - - try { - Thread.sleep(SLEEP_INTERVAL); - } catch (InterruptedException e) { - throw new IOException("병합 대기 중 인터럽트 발생", e); - } - } - } - -// // 병합이 필요한 경우 이미지를 병합하는 메서드 -// private void mergeImagesIfNeeded(int challengeNum, Lucky lucky) throws IOException { -// Map> groupedFiles = new HashMap<>(); -// Pattern pattern = Pattern.compile("screenshot_(\\d+)"); -// -// List sharedFiles; -// -// lock.lock(); -// try { -// sharedFiles = sharedFileRepository.findByChallengeNumAndLucky(challengeNum, lucky); -// } finally { -// lock.unlock(); -// } -// -// for (SharedFile sharedFile : sharedFiles) { -// String fileName = sharedFile.getFileName(); -// Matcher matcher = pattern.matcher(fileName); -// -// if (matcher.find()) { -// String group = matcher.group(1); -// groupedFiles.computeIfAbsent(group, k -> new ArrayList<>()).add(sharedFile); -// } -// } -// -// List sortedKeys = new ArrayList<>(groupedFiles.keySet()); -// Collections.sort(sortedKeys); -// -// for (String key : sortedKeys) { -// List sharedFilesInGroup = groupedFiles.get(key); -// -// Set uniqueImageUrls = sharedFilesInGroup.stream() -// .map(SharedFile::getFileName) -// .collect(Collectors.toSet()); -// -// if (uniqueImageUrls.size() < 4) { -// throw new IOException("충분한 수의 고유 이미지를 찾을 수 없습니다."); -// } -// -// List selectedImages = uniqueImageUrls.stream() -// .limit(4) -// .map(url -> { -// try { -// return ImageIO.read(new URL(url)); -// } catch (Exception e) { -// throw new RuntimeException(e); -// } -// }) -// .collect(Collectors.toList()); -// -// while (selectedImages.size() < 4) { -// BufferedImage emptyImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); -// selectedImages.add(emptyImage); -// } -// -// String uuid = UUID.randomUUID().toString(); -// String baseName = "merged-images/" + uuid + "_" + challengeNum + "_" + lucky.getLuckyId() + "_cut_" + key + ".png"; -// BufferedImage mergedImage = ImageMerger.mergeImages(selectedImages); -// -// String mergedImageUrl = uploadMergedImageToS3(mergedImage, bucket, baseName); -// -// SharedFile mergedFile = new SharedFile(); -// mergedFile.setChallengeNum(challengeNum); -// mergedFile.setCreateDate(System.currentTimeMillis()); -// mergedFile.setFileName(mergedImageUrl); -// mergedFile.setFileType(FileType.IMAGE); -// mergedFile.setLucky(lucky); -// -// sharedFileRepository.save(mergedFile); -// } -// } - // 병합된 이미지를 S3에 업로드하는 메서드 public String uploadMergedImageToS3(BufferedImage mergedImage, String bucketName, String fileObjKeyName) throws IOException { @@ -654,4 +580,4 @@ public void uploadMergeVoice(Lucky lucky, String voiceUrl, FileType fileType){ public SharedFile getMergeVoice(Lucky lucky, FileType fileType) { return sharedFileRepository.findByLuckyAndFileType(lucky, fileType); } -} +} \ No newline at end of file