diff --git a/src/main/java/leets/leenk/domain/feed/application/usecase/FeedUsecase.java b/src/main/java/leets/leenk/domain/feed/application/usecase/FeedUsecase.java index 764db225..4068ab28 100644 --- a/src/main/java/leets/leenk/domain/feed/application/usecase/FeedUsecase.java +++ b/src/main/java/leets/leenk/domain/feed/application/usecase/FeedUsecase.java @@ -16,11 +16,10 @@ import leets.leenk.domain.feed.domain.entity.Reaction; import leets.leenk.domain.feed.domain.service.*; import leets.leenk.domain.feed.domain.service.dto.FeedNavigationResult; +import leets.leenk.domain.media.application.dto.request.FeedMediaRequest; import leets.leenk.domain.media.application.mapper.MediaMapper; import leets.leenk.domain.media.domain.entity.Media; -import leets.leenk.domain.media.domain.service.MediaDeleteService; -import leets.leenk.domain.media.domain.service.MediaGetService; -import leets.leenk.domain.media.domain.service.MediaSaveService; +import leets.leenk.domain.media.domain.service.*; import leets.leenk.domain.notification.application.usecase.FeedNotificationUsecase; import leets.leenk.domain.user.domain.entity.User; import leets.leenk.domain.user.domain.entity.UserBlock; @@ -58,6 +57,8 @@ public class FeedUsecase { private final MediaGetService mediaGetService; private final MediaSaveService mediaSaveService; private final MediaDeleteService mediaDeleteService; + private final MediaUpdateService mediaUpdateService; + private final MediaS3Service mediaS3Service; private final LinkedUserGetService linkedUserGetService; private final LinkedUserSaveService linkedUserSaveService; @@ -181,6 +182,11 @@ public void uploadFeed(long userId, FeedUploadRequest request) { .toList(); mediaSaveService.saveAll(medias); + medias.forEach(media -> { + String newMediaUrl = mediaS3Service.moveToOriginals(media.getMediaUrl()); + mediaUpdateService.updateMediaUrl(media, newMediaUrl); + }); + List linkedUsers = getLinkedUsers(author, request.userIds(), feed); linkedUserSaveService.saveAll(linkedUsers); @@ -250,6 +256,11 @@ public void updateFeed(long userId, long feedId, FeedUpdateRequest request) { .map(mediaRequest -> mediaMapper.toMedia(feed, mediaRequest)) .toList(); mediaSaveService.saveAll(newMedias); + + newMedias.forEach(media -> { + String originalsUrl = mediaS3Service.moveToOriginals(media.getMediaUrl()); + mediaUpdateService.updateMediaUrl(media, originalsUrl); + }); } if (request.userIds() != null) { diff --git a/src/main/java/leets/leenk/domain/leenk/application/usecase/LeenkUsecase.java b/src/main/java/leets/leenk/domain/leenk/application/usecase/LeenkUsecase.java index 89b1489b..4dc7667c 100644 --- a/src/main/java/leets/leenk/domain/leenk/application/usecase/LeenkUsecase.java +++ b/src/main/java/leets/leenk/domain/leenk/application/usecase/LeenkUsecase.java @@ -39,9 +39,7 @@ import leets.leenk.domain.leenk.domain.service.LocationSaveService; import leets.leenk.domain.media.application.mapper.MediaMapper; import leets.leenk.domain.media.domain.entity.Media; -import leets.leenk.domain.media.domain.service.MediaDeleteService; -import leets.leenk.domain.media.domain.service.MediaGetService; -import leets.leenk.domain.media.domain.service.MediaSaveService; +import leets.leenk.domain.media.domain.service.*; import leets.leenk.domain.notification.application.usecase.LeenkNotificationUsecase; import leets.leenk.domain.user.domain.entity.User; import leets.leenk.domain.user.domain.service.NotionDatabaseService; @@ -75,6 +73,8 @@ public class LeenkUsecase { private final MediaSaveService mediaSaveService; private final MediaGetService mediaGetService; private final MediaDeleteService mediaDeleteService; + private final MediaUpdateService mediaUpdateService; + private final MediaS3Service mediaS3Service; private final UserGetService userGetService; private final SlackWebhookService slackWebhookService; @@ -105,6 +105,9 @@ public LeenkCreateResponse uploadLeenk(Long userId, LeenkUploadRequest request) .ifPresent(url -> { Media media = mediaMapper.toMedia(leenk, url); mediaSaveService.save(media); + + String originalsUrl = mediaS3Service.moveToOriginals(media.getMediaUrl()); + mediaUpdateService.updateMediaUrl(media, originalsUrl); }); leenkNotificationUsecase.saveNewLeenkNotification(leenk); @@ -137,10 +140,14 @@ public void updateLeenk(Long userId, Long leenkId, LeenkUpdateRequest request) { } } else { if (media != null) { - media.updateMediaUrl(newUrl); + String originalsUrl = mediaS3Service.moveToOriginals(newUrl); + media.updateMediaUrl(originalsUrl); } else { Media newMedia = mediaMapper.toMedia(leenk, newUrl); mediaSaveService.save(newMedia); + + String originalsUrl = mediaS3Service.moveToOriginals(newMedia.getMediaUrl()); + newMedia.updateMediaUrl(originalsUrl); } } } diff --git a/src/main/java/leets/leenk/domain/media/application/exception/ErrorCode.java b/src/main/java/leets/leenk/domain/media/application/exception/ErrorCode.java index 44c8e0a5..23a4840f 100644 --- a/src/main/java/leets/leenk/domain/media/application/exception/ErrorCode.java +++ b/src/main/java/leets/leenk/domain/media/application/exception/ErrorCode.java @@ -10,7 +10,8 @@ public enum ErrorCode implements ErrorCodeInterface { MEDIA_NOT_FOUND(2500, HttpStatus.NOT_FOUND, "존재하지 않는 미디어입니다."), - MEDIA_UPDATE_FAILED(2501, HttpStatus.INTERNAL_SERVER_ERROR, "미디어 업데이트에 실패했습니다."); + MEDIA_UPDATE_FAILED(2501, HttpStatus.INTERNAL_SERVER_ERROR, "미디어 업데이트에 실패했습니다."), + S3_COPY_FAILED(2502, HttpStatus.INTERNAL_SERVER_ERROR, "파일 복사에 실패했습니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/leets/leenk/domain/media/application/exception/S3CopyException.java b/src/main/java/leets/leenk/domain/media/application/exception/S3CopyException.java new file mode 100644 index 00000000..c9bb0e3f --- /dev/null +++ b/src/main/java/leets/leenk/domain/media/application/exception/S3CopyException.java @@ -0,0 +1,9 @@ +package leets.leenk.domain.media.application.exception; + +import leets.leenk.global.common.exception.BaseException; + +public class S3CopyException extends BaseException { + public S3CopyException() { + super(ErrorCode.S3_COPY_FAILED); + } +} diff --git a/src/main/java/leets/leenk/domain/media/domain/entity/Media.java b/src/main/java/leets/leenk/domain/media/domain/entity/Media.java index 85e7b4a4..dc5c2979 100644 --- a/src/main/java/leets/leenk/domain/media/domain/entity/Media.java +++ b/src/main/java/leets/leenk/domain/media/domain/entity/Media.java @@ -72,5 +72,4 @@ public void updateMediaUrl(String mediaUrl) { public void updateThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } - } diff --git a/src/main/java/leets/leenk/domain/media/domain/service/MediaS3Service.java b/src/main/java/leets/leenk/domain/media/domain/service/MediaS3Service.java new file mode 100644 index 00000000..eefa8939 --- /dev/null +++ b/src/main/java/leets/leenk/domain/media/domain/service/MediaS3Service.java @@ -0,0 +1,37 @@ +package leets.leenk.domain.media.domain.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MediaS3Service { + + private final S3PresignedUrlService s3PresignedUrlService; + + public String moveToOriginals(String tempUrl) { + if (tempUrl == null || tempUrl.isBlank()) { + return tempUrl; + } + + if (tempUrl.contains("/originals/")) { + return tempUrl; + } + + String tempKey = extractKeyFromUrl(tempUrl); + String originalsKey = tempKey.replace("temp/", "originals/"); + + s3PresignedUrlService.copyObject(tempKey, originalsKey); + s3PresignedUrlService.deleteObject(tempKey); + + return s3PresignedUrlService.getUrl(originalsKey); + } + + private String extractKeyFromUrl(String url) { + URI uri = URI.create(url); + return uri.getPath().substring(1); + } +} diff --git a/src/main/java/leets/leenk/domain/media/domain/service/MediaUpdateService.java b/src/main/java/leets/leenk/domain/media/domain/service/MediaUpdateService.java index b8ede38f..0699c811 100644 --- a/src/main/java/leets/leenk/domain/media/domain/service/MediaUpdateService.java +++ b/src/main/java/leets/leenk/domain/media/domain/service/MediaUpdateService.java @@ -11,4 +11,8 @@ public class MediaUpdateService { public void update(Media media, String thumbnailUrl){ media.updateThumbnailUrl(thumbnailUrl); } + + public void updateMediaUrl(Media media, String mediaUrl){ + media.updateMediaUrl(mediaUrl); + } } diff --git a/src/main/java/leets/leenk/domain/media/domain/service/S3PresignedUrlService.java b/src/main/java/leets/leenk/domain/media/domain/service/S3PresignedUrlService.java index 2b57385d..c52e69eb 100644 --- a/src/main/java/leets/leenk/domain/media/domain/service/S3PresignedUrlService.java +++ b/src/main/java/leets/leenk/domain/media/domain/service/S3PresignedUrlService.java @@ -1,10 +1,15 @@ package leets.leenk.domain.media.domain.service; import leets.leenk.domain.media.application.dto.response.MediaUrlResponse; +import leets.leenk.domain.media.application.exception.S3CopyException; import leets.leenk.domain.media.domain.entity.enums.DomainType; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; @@ -21,9 +26,14 @@ public class S3PresignedUrlService { private final S3Presigner s3Presigner; + private final S3Client s3Client; + @Value("${cloud.aws.s3.bucket}") private String bucket; + @Value("${cloud.aws.region.static}") + private String region; + public List generateUrlList(DomainType domainType, List fileNames) { return IntStream.range(0, fileNames.size()) @@ -61,8 +71,37 @@ private String generateKey(DomainType domainType, String fileName, int index) { String typePath = domainType.name().toLowerCase(); String prefix = (index == 0) ? "thumbnail_" : ""; - return String.format("originals/%s/%s%s", typePath, prefix, filename); + return String.format("temp/%s/%s%s", typePath, prefix, filename); } + public void copyObject(String sourceKey, String destinationKey) { + try { + CopyObjectRequest copyRequest = CopyObjectRequest.builder() + .sourceBucket(bucket) + .sourceKey(sourceKey) + .destinationBucket(bucket) + .destinationKey(destinationKey) + .build(); + + s3Client.copyObject(copyRequest); + + } catch (SdkException e) { + throw new S3CopyException(); + } + } + + public void deleteObject(String key) { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build(); + + s3Client.deleteObject(deleteRequest); + } + + public String getUrl(String key) { + return String.format("https://%s.s3.%s.amazonaws.com/%s", + bucket, region, key); + } } diff --git a/src/main/java/leets/leenk/domain/user/application/usecase/UserUsecase.java b/src/main/java/leets/leenk/domain/user/application/usecase/UserUsecase.java index 024e48a7..81a8a19b 100644 --- a/src/main/java/leets/leenk/domain/user/application/usecase/UserUsecase.java +++ b/src/main/java/leets/leenk/domain/user/application/usecase/UserUsecase.java @@ -1,5 +1,6 @@ package leets.leenk.domain.user.application.usecase; +import leets.leenk.domain.media.domain.service.MediaS3Service; import leets.leenk.domain.user.application.dto.request.*; import leets.leenk.domain.user.application.dto.response.UserInfoResponse; import leets.leenk.domain.user.application.exception.SelfBlockNotAllowedException; @@ -37,6 +38,8 @@ public class UserUsecase { private final UserBlockMapper userBlockMapper; private final UserBlockService userBlockService; + private final MediaS3Service mediaS3Service; + @Transactional public void initialAgreement(long userId, AgreementRequest request) { User user = userGetService.findById(userId); @@ -69,7 +72,8 @@ public void updateKakaoTalkId(long userId, KakaoTalkIdRequest request) { public void updateProfileImage(long userId, ProfileImageRequest request) { User user = userGetService.findById(userId); - userUpdateService.updateProfileImage(user, request.profileImage()); + String originalsUrl = mediaS3Service.moveToOriginals(request.profileImage()); + userUpdateService.updateProfileImage(user, originalsUrl); } @Transactional diff --git a/src/main/java/leets/leenk/domain/user/domain/service/user/UserUpdateService.java b/src/main/java/leets/leenk/domain/user/domain/service/user/UserUpdateService.java index 1f2f3afc..01c48e4d 100644 --- a/src/main/java/leets/leenk/domain/user/domain/service/user/UserUpdateService.java +++ b/src/main/java/leets/leenk/domain/user/domain/service/user/UserUpdateService.java @@ -1,15 +1,20 @@ package leets.leenk.domain.user.domain.service.user; +import leets.leenk.domain.media.domain.service.MediaS3Service; import leets.leenk.domain.user.application.dto.request.AgreementRequest; import leets.leenk.domain.user.application.dto.request.RegisterRequest; import leets.leenk.domain.user.domain.entity.User; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.LocalDate; @Service +@RequiredArgsConstructor public class UserUpdateService { + private final MediaS3Service mediaS3Service; + public void updateAgreement(User user, AgreementRequest request) { user.updateAgreement(request.termsService(), request.privacyPolicy()); } @@ -22,7 +27,8 @@ public void completeProfile(User user, RegisterRequest request) { } if (request.profileImage() != null) { - user.updateProfileImage(request.profileImage()); + String originalsUrl = mediaS3Service.moveToOriginals(request.profileImage()); + user.updateProfileImage(originalsUrl); } if(request.birthday() != null) { diff --git a/src/main/java/leets/leenk/global/config/AwsS3Config.java b/src/main/java/leets/leenk/global/config/AwsS3Config.java index 13d09ff8..c5b239ad 100644 --- a/src/main/java/leets/leenk/global/config/AwsS3Config.java +++ b/src/main/java/leets/leenk/global/config/AwsS3Config.java @@ -7,6 +7,7 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @Configuration @@ -31,4 +32,14 @@ public S3Presigner s3Presigner() { .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build(); } + + @Bean + public S3Client s3Client() { + AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, accessSecret); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .build(); + } }