diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/initData/BaseInitData.java b/src/main/java/com/back/web7_9_codecrete_be/global/initData/BaseInitData.java index 7e73c464..9f1d27d6 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/initData/BaseInitData.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/initData/BaseInitData.java @@ -1,12 +1,5 @@ package com.back.web7_9_codecrete_be.global.initData; -import java.time.LocalDate; -import java.time.LocalDateTime; - -import org.springframework.context.annotation.Profile; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - import com.back.web7_9_codecrete_be.domain.concerts.entity.Concert; import com.back.web7_9_codecrete_be.domain.concerts.entity.ConcertPlace; import com.back.web7_9_codecrete_be.domain.concerts.repository.ConcertPlaceRepository; @@ -14,9 +7,14 @@ import com.back.web7_9_codecrete_be.domain.users.entity.SocialType; import com.back.web7_9_codecrete_be.domain.users.entity.User; import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository; - import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; @Profile("dev") @Component @@ -51,6 +49,8 @@ private void createTestUser() { .socialId(null) .build(); + testUser.initSetting(); + userRepository.save(testUser); } @@ -69,6 +69,8 @@ private void createAdminUser() { .socialId(null) .build(); + adminUser.initSetting(); + // dev 전용 어드민 권한 부여 adminUser.promoteToAdmin(); diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueue.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueue.java new file mode 100644 index 00000000..cd3ab4cd --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueue.java @@ -0,0 +1,28 @@ +package com.back.web7_9_codecrete_be.global.storage; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FileDeleteQueue { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String fileUrl; + + private LocalDateTime deleteAt; +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueueRepository.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueueRepository.java new file mode 100644 index 00000000..bc2bb9b2 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteQueueRepository.java @@ -0,0 +1,10 @@ +package com.back.web7_9_codecrete_be.global.storage; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.List; + +public interface FileDeleteQueueRepository extends JpaRepository { + List findAllByDeleteAtBefore(LocalDateTime now); +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteScheduler.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteScheduler.java new file mode 100644 index 00000000..135fe6e3 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/FileDeleteScheduler.java @@ -0,0 +1,52 @@ +package com.back.web7_9_codecrete_be.global.storage; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.s3.S3Client; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class FileDeleteScheduler { + + private final FileDeleteQueueRepository repository; + private final S3Client s3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @Scheduled(fixedDelay = 600000) // 10분마다 실행 + public void deleteFiles() { + + List targets = + repository.findAllByDeleteAtBefore(LocalDateTime.now()); + + for (FileDeleteQueue file : targets) { + try { + String key = extractKey(file.getFileUrl()); + + s3Client.deleteObject(builder -> builder + .bucket(bucket) + .key(key) + ); + + } catch (Exception e) { + // 실패해도 계속 진행 + } + } + + repository.deleteAll(targets); + } + + private String extractKey(String fileUrl) { + String prefix = "https://" + bucket + ".s3." + region + ".amazonaws.com/"; + return fileUrl.substring(prefix.length()); + } +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageFileValidator.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageFileValidator.java index 45085184..7a75dfae 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageFileValidator.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageFileValidator.java @@ -43,7 +43,7 @@ public void validateImageFile(MultipartFile file) { throw new BusinessException(FileErrorCode.EXTENSION_MISMATCH); } - String extension = filename.substring(filename.lastIndexOf('.') + 1); + String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); // 확장자 ↔ MIME 타입 매칭 if (!imageMimeType.matches(extension)) { diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageMimeType.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageMimeType.java index 0f0f2c9f..ac57678e 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageMimeType.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/ImageMimeType.java @@ -22,7 +22,7 @@ public enum ImageMimeType { public static ImageMimeType from(String detectedMimeType) { return Arrays.stream(values()) - .filter(it -> it.mimeType.equals(detectedMimeType)) + .filter(it -> detectedMimeType.startsWith(it.mimeType)) .findFirst() .orElseThrow(() -> new BusinessException(FileErrorCode.INVALID_IMAGE_TYPE) diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/storage/S3FileStorageService.java b/src/main/java/com/back/web7_9_codecrete_be/global/storage/S3FileStorageService.java index e5c6fdec..31fb4975 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/storage/S3FileStorageService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/storage/S3FileStorageService.java @@ -9,6 +9,7 @@ import software.amazon.awssdk.services.s3.model.PutObjectRequest; import java.io.IOException; +import java.time.LocalDateTime; import java.util.UUID; @Service @@ -16,6 +17,8 @@ public class S3FileStorageService implements FileStorageService { private final S3Client s3Client; + private final ImageFileValidator imageFileValidator; + private final FileDeleteQueueRepository fileDeleteQueueRepository; @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -27,7 +30,16 @@ public class S3FileStorageService implements FileStorageService { @Override public String upload(MultipartFile file, String basePath) { - String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); + imageFileValidator.validateImageFile(file); + + String originalFilename = file.getOriginalFilename(); + + // 확장자 추출 + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase(); + + // 파일명 제거 (UUID만 사용) + String fileName = UUID.randomUUID() + extension; + String key = basePath + "/" + fileName; try { @@ -56,16 +68,11 @@ public void delete(String fileUrl) { return; } - String prefix = "https://" + bucket + ".s3." + region + ".amazonaws.com/"; - if (!fileUrl.startsWith(prefix)) { - throw new IllegalArgumentException("잘못된 S3 파일 URL입니다."); - } - - String key = fileUrl.substring(prefix.length()); - - s3Client.deleteObject(builder -> builder - .bucket(bucket) - .key(key) + fileDeleteQueueRepository.save( + FileDeleteQueue.builder() + .fileUrl(fileUrl) + .deleteAt(LocalDateTime.now().plusMinutes(30)) + .build() ); } }