From c9f231af5dacb099e3234142ff853d67587f1d0b Mon Sep 17 00:00:00 2001 From: Yeonri Date: Wed, 17 Jun 2026 01:42:32 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B2=BD=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자 대학 로고와 배경 이미지 업로드 API를 추가 - 대학 영문명을 서버에서 업로드 디렉토리명으로 변환 - S3 업로드 서비스에 동적 하위 디렉토리 경로 생성을 추가 --- .../s3/controller/S3Controller.java | 35 +++++++++++++++++++ .../s3/domain/UploadDirectoryName.java | 29 +++++++++++++++ .../solidconnection/s3/domain/UploadPath.java | 2 ++ .../solidconnection/s3/service/S3Service.java | 17 ++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java diff --git a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java index 8e98c863b..3b5832718 100644 --- a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java @@ -1,10 +1,13 @@ package com.example.solidconnection.s3.controller; import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.s3.domain.UploadDirectoryName; import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.dto.UrlPrefixResponse; import com.example.solidconnection.s3.service.S3Service; +import com.example.solidconnection.security.annotation.RequireRoleAccess; +import com.example.solidconnection.siteuser.domain.Role; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -77,6 +80,38 @@ public ResponseEntity> uploadChatFile( return ResponseEntity.ok(chatImageUrls); } + @RequireRoleAccess(roles = Role.ADMIN) + @PostMapping("/admin/university/logo") + public ResponseEntity uploadAdminUniversityLogo( + @AuthorizedUser long adminId, + @RequestParam("file") MultipartFile imageFile, + @RequestParam("englishName") String englishName + ) { + String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); + UploadedFileUrlResponse logoImageUrl = s3Service.uploadFile( + imageFile, + UploadPath.ADMIN_UNIVERSITY_LOGO, + directoryName + ); + return ResponseEntity.ok(logoImageUrl); + } + + @RequireRoleAccess(roles = Role.ADMIN) + @PostMapping("/admin/university/background") + public ResponseEntity uploadAdminUniversityBackground( + @AuthorizedUser long adminId, + @RequestParam("file") MultipartFile imageFile, + @RequestParam("englishName") String englishName + ) { + String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); + UploadedFileUrlResponse backgroundImageUrl = s3Service.uploadFile( + imageFile, + UploadPath.ADMIN_UNIVERSITY_BACKGROUND, + directoryName + ); + return ResponseEntity.ok(backgroundImageUrl); + } + @GetMapping("/s3-url-prefix") public ResponseEntity getS3UrlPrefix() { return ResponseEntity.ok(new UrlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); diff --git a/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java b/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java new file mode 100644 index 000000000..9ea890e4a --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.s3.domain; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; + +public final class UploadDirectoryName { + + private UploadDirectoryName() { + } + + public static String fromUniversityEnglishName(String englishName) { + if (englishName == null || englishName.isBlank()) { + throw new CustomException(ErrorCode.INVALID_INPUT); + } + + String directoryName = englishName.trim() + .toLowerCase() + .replaceAll("\\s*&\\s*", "_and_") + .replaceAll("\\s+", "_") + .replaceAll("_+", "_") + .replaceAll("[^a-z0-9_-]", ""); + + if (directoryName.isBlank()) { + throw new CustomException(ErrorCode.INVALID_INPUT); + } + + return directoryName; + } +} diff --git a/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java index d94ed1fb8..7d9065605 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java @@ -14,6 +14,8 @@ public enum UploadPath { NEWS("news"), CHAT("chat/files"), MENTOR_PROOF("mentor-proof"), + ADMIN_UNIVERSITY_LOGO("admin/logo"), + ADMIN_UNIVERSITY_BACKGROUND("admin/background") ; private final String type; diff --git a/src/main/java/com/example/solidconnection/s3/service/S3Service.java b/src/main/java/com/example/solidconnection/s3/service/S3Service.java index 6dc3004e6..b5ad88d85 100644 --- a/src/main/java/com/example/solidconnection/s3/service/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/service/S3Service.java @@ -50,12 +50,20 @@ public class S3Service { * - 5mb 미만의 파일은 바로 업로드한다. * */ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, UploadPath uploadPath) { + return uploadFile(multipartFile, uploadPath, null); + } + + public UploadedFileUrlResponse uploadFile( + MultipartFile multipartFile, + UploadPath uploadPath, + String subDirectory + ) { validateFile(multipartFile, uploadPath); UUID randomUUID = UUID.randomUUID(); String extension = getFileExtension(Objects.requireNonNull(multipartFile.getOriginalFilename())); String baseFileName = randomUUID + "." + extension; - String fileName = uploadPath.getType() + "/" + baseFileName; + String fileName = createFileName(uploadPath, subDirectory, baseFileName); final boolean shouldResize = uploadPath.isResizable( multipartFile.getSize(), extension, MAX_FILE_SIZE_MB); @@ -73,6 +81,13 @@ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, UploadPat return new UploadedFileUrlResponse(returnPath); } + private String createFileName(UploadPath uploadPath, String subDirectory, String baseFileName) { + if (subDirectory == null || subDirectory.isBlank()) { + return uploadPath.getType() + "/" + baseFileName; + } + return uploadPath.getType() + "/" + subDirectory + "/" + baseFileName; + } + private byte[] extractBytes(MultipartFile file) { try { return file.getBytes(); From 136df16b053ad04d19e0bfe26e2c7172f09d8fac Mon Sep 17 00:00:00 2001 From: Yeonri Date: Wed, 17 Jun 2026 01:42:52 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대학 영문명 디렉토리명 변환 테스트를 추가 - 동적 하위 디렉토리 기반 S3 업로드 경로 테스트를 추가 --- .../s3/domain/UploadDirectoryNameTest.java | 51 +++++++++++++++++ .../s3/service/S3ServiceDynamicPathTest.java | 57 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java create mode 100644 src/test/java/com/example/solidconnection/s3/service/S3ServiceDynamicPathTest.java diff --git a/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java b/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java new file mode 100644 index 000000000..361b4ab3b --- /dev/null +++ b/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.s3.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.example.solidconnection.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("업로드 디렉토리명 테스트") +class UploadDirectoryNameTest { + + @Nested + class 대학_영문명_변환_테스트 { + + @Test + void 대학_영문명의_공백을_언더스코어로_변환한다() { + // given + String englishName = "University of Tokyo"; + + // when + String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); + + // then + assertThat(directoryName).isEqualTo("university_of_tokyo"); + } + + @Test + void 특수문자를_제거하고_앰퍼샌드는_and로_변환한다() { + // given + String englishName = "Texas A&M University, Austin"; + + // when + String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); + + // then + assertThat(directoryName).isEqualTo("texas_a_and_m_university_austin"); + } + + @Test + void 공백_문자열이면_예외가_발생한다() { + // given + String blankName = " "; + + // when & then + assertThatThrownBy(() -> UploadDirectoryName.fromUniversityEnglishName(blankName)) + .isInstanceOf(CustomException.class); + } + } +} diff --git a/src/test/java/com/example/solidconnection/s3/service/S3ServiceDynamicPathTest.java b/src/test/java/com/example/solidconnection/s3/service/S3ServiceDynamicPathTest.java new file mode 100644 index 000000000..679d9ef77 --- /dev/null +++ b/src/test/java/com/example/solidconnection/s3/service/S3ServiceDynamicPathTest.java @@ -0,0 +1,57 @@ +package com.example.solidconnection.s3.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.s3.domain.UploadPath; +import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import software.amazon.awssdk.services.s3.S3Client; + +@DisplayName("S3 서비스 동적 업로드 경로 테스트") +@ExtendWith(MockitoExtension.class) +class S3ServiceDynamicPathTest { + + @InjectMocks + private S3Service s3Service; + + @Mock + private S3Client s3Client; + + @Mock + private SiteUserRepository siteUserRepository; + + @Mock + private FileUploadService fileUploadService; + + @Nested + class 동적_하위_디렉토리_업로드_테스트 { + + @Test + void 업로드_경로와_파일명_사이에_동적_하위_디렉토리를_포함한다() { + // given + MockMultipartFile file = new MockMultipartFile("file", "logo.png", "image/png", new byte[100]); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile( + file, + UploadPath.ADMIN_UNIVERSITY_LOGO, + "university_of_tokyo" + ); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("admin/logo/university_of_tokyo/"), + () -> assertThat(response.fileUrl()).endsWith(".png") + ); + } + } +} From d03b63f418e5398b7fea1fd70347ce866ebcbcd7 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Wed, 17 Jun 2026 15:45:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=8C=80=ED=95=99=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=A9=EB=8F=8C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대학 영문명 디렉토리명에 원본 영문명 해시를 추가 - 파견 대학 영문명 중복 검증과 유니크 제약을 추가 - 정규화 충돌과 영문명 중복 검증 테스트를 추가 --- .../service/AdminHostUniversityService.java | 18 +++++++ .../s3/domain/UploadDirectoryName.java | 18 ++++++- .../university/domain/HostUniversity.java | 2 +- .../repository/HostUniversityRepository.java | 2 + ...traint_to_host_university_english_name.sql | 2 + .../AdminHostUniversityServiceTest.java | 49 +++++++++++++++++++ .../s3/domain/UploadDirectoryNameTest.java | 22 ++++++++- 7 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/db/migration/V52__add_unique_constraint_to_host_university_english_name.sql diff --git a/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java index 416ba8fb8..7f96f3c4e 100644 --- a/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java +++ b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java @@ -67,6 +67,7 @@ public AdminHostUniversityDetailResponse getHostUniversity(Long id) { ) public AdminHostUniversityDetailResponse createHostUniversity(AdminHostUniversityCreateRequest request) { validateKoreanNameNotExists(request.koreanName()); + validateEnglishNameNotExists(request.englishName()); Country country = findCountryByCode(request.countryCode()); Region region = findRegionByCode(request.regionCode()); @@ -97,6 +98,13 @@ private void validateKoreanNameNotExists(String koreanName) { }); } + private void validateEnglishNameNotExists(String englishName) { + hostUniversityRepository.findByEnglishName(englishName) + .ifPresent(existingUniversity -> { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + }); + } + @Transactional @DefaultCacheOut( key = {"univApplyInfoTextSearch", "university:recommend:general"}, @@ -108,6 +116,7 @@ public AdminHostUniversityDetailResponse updateHostUniversity(Long id, AdminHost .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); validateKoreanNameNotDuplicated(request.koreanName(), id); + validateEnglishNameNotDuplicated(request.englishName(), id); Country country = findCountryByCode(request.countryCode()); Region region = findRegionByCode(request.regionCode()); @@ -140,6 +149,15 @@ private void validateKoreanNameNotDuplicated(String koreanName, Long excludeId) }); } + private void validateEnglishNameNotDuplicated(String englishName, Long excludeId) { + hostUniversityRepository.findByEnglishName(englishName) + .ifPresent(existingUniversity -> { + if (!existingUniversity.getId().equals(excludeId)) { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + } + }); + } + private Country findCountryByCode(String countryCode) { return countryRepository.findByCode(countryCode) .orElseThrow(() -> new CustomException(COUNTRY_NOT_FOUND)); diff --git a/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java b/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java index 9ea890e4a..4b53396dc 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadDirectoryName.java @@ -2,9 +2,15 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; public final class UploadDirectoryName { + private static final int HASH_PREFIX_LENGTH = 12; + private UploadDirectoryName() { } @@ -24,6 +30,16 @@ public static String fromUniversityEnglishName(String englishName) { throw new CustomException(ErrorCode.INVALID_INPUT); } - return directoryName; + return directoryName + "_" + hash(englishName.trim()); + } + + private static String hash(String value) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + byte[] digest = messageDigest.digest(value.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(digest).substring(0, HASH_PREFIX_LENGTH); + } catch (NoSuchAlgorithmException e) { + throw new CustomException(ErrorCode.NOT_DEFINED_ERROR); + } } } diff --git a/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java index 6f816a23c..de80a281f 100644 --- a/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java +++ b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java @@ -28,7 +28,7 @@ public class HostUniversity extends BaseEntity { @Column(name = "korean_name", nullable = false, unique = true, length = 100) private String koreanName; - @Column(name = "english_name", nullable = false, length = 100) + @Column(name = "english_name", nullable = false, unique = true, length = 100) private String englishName; @Column(name = "format_name", nullable = false, length = 100) diff --git a/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java index 3fa80629a..4264ed04d 100644 --- a/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java @@ -16,4 +16,6 @@ default HostUniversity getHostUniversityById(Long id) { } Optional findByKoreanName(String koreanName); + + Optional findByEnglishName(String englishName); } diff --git a/src/main/resources/db/migration/V52__add_unique_constraint_to_host_university_english_name.sql b/src/main/resources/db/migration/V52__add_unique_constraint_to_host_university_english_name.sql new file mode 100644 index 000000000..3dc3a5d66 --- /dev/null +++ b/src/main/resources/db/migration/V52__add_unique_constraint_to_host_university_english_name.sql @@ -0,0 +1,2 @@ +ALTER TABLE host_university + ADD CONSTRAINT uk_host_university_english_name UNIQUE (english_name); diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java index 5e2bbbf1d..a97303465 100644 --- a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java @@ -256,6 +256,31 @@ class 생성 { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); } + + @Test + void 이미_존재하는_영문명으로_생성하면_예외_응답을_반환한다() { + // given + HostUniversity existing = universityFixture.괌_대학(); + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + "신규 대학", + existing.getEnglishName(), + "표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.createHostUniversity(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } } @Nested @@ -341,6 +366,30 @@ class 수정 { .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); } + @Test + void 다른_대학의_영문명으로_수정하면_예외_응답을_반환한다() { + // given + HostUniversity university1 = universityFixture.괌_대학(); + HostUniversity university2 = universityFixture.메이지_대학(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + university1.getKoreanName(), + university2.getEnglishName(), + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + university1.getCountry().getCode(), + university1.getRegion().getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.updateHostUniversity(university1.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } + @Test void 같은_대학의_한글명으로_수정하면_성공한다() { // given diff --git a/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java b/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java index 361b4ab3b..041b7c42f 100644 --- a/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java +++ b/src/test/java/com/example/solidconnection/s3/domain/UploadDirectoryNameTest.java @@ -23,7 +23,9 @@ class 대학_영문명_변환_테스트 { String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); // then - assertThat(directoryName).isEqualTo("university_of_tokyo"); + assertThat(directoryName) + .startsWith("university_of_tokyo_") + .matches("university_of_tokyo_[0-9a-f]{12}"); } @Test @@ -35,7 +37,23 @@ class 대학_영문명_변환_테스트 { String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); // then - assertThat(directoryName).isEqualTo("texas_a_and_m_university_austin"); + assertThat(directoryName) + .startsWith("texas_a_and_m_university_austin_") + .matches("texas_a_and_m_university_austin_[0-9a-f]{12}"); + } + + @Test + void 같은_slug로_변환되는_서로_다른_영문명은_다른_디렉토리명을_반환한다() { + // given + String englishName = "Texas A&M University"; + String normalizedCollisionName = "Texas A and M University"; + + // when + String directoryName = UploadDirectoryName.fromUniversityEnglishName(englishName); + String collisionDirectoryName = UploadDirectoryName.fromUniversityEnglishName(normalizedCollisionName); + + // then + assertThat(directoryName).isNotEqualTo(collisionDirectoryName); } @Test From b3f0ce61549cf9940774817211b2f739ea196f37 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Wed, 17 Jun 2026 20:33:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 업로드 경로별 이미지 전용 검증 정책을 추가 - 관리자 대학 이미지 업로드에서 문서 확장자를 차단 - 증빙 파일 업로드의 문서 확장자 허용 동작을 테스트로 보장 --- .../solidconnection/s3/domain/UploadPath.java | 34 ++++++++++------ .../s3/service/S3ServiceTest.java | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java index 7d9065605..b7afabbcc 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java @@ -3,25 +3,28 @@ import com.example.solidconnection.common.constant.FileConstants; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; +import java.util.List; import lombok.Getter; @Getter public enum UploadPath { - PROFILE("profile"), - GPA("gpa"), - LANGUAGE_TEST("language"), - COMMUNITY("community"), - NEWS("news"), - CHAT("chat/files"), - MENTOR_PROOF("mentor-proof"), - ADMIN_UNIVERSITY_LOGO("admin/logo"), - ADMIN_UNIVERSITY_BACKGROUND("admin/background") + PROFILE("profile", true), + GPA("gpa", false), + LANGUAGE_TEST("language", false), + COMMUNITY("community", true), + NEWS("news", true), + CHAT("chat/files", false), + MENTOR_PROOF("mentor-proof", false), + ADMIN_UNIVERSITY_LOGO("admin/logo", true), + ADMIN_UNIVERSITY_BACKGROUND("admin/background", true) ; private final String type; + private final boolean imageOnly; - UploadPath(String type) { + UploadPath(String type, boolean imageOnly) { this.type = type; + this.imageOnly = imageOnly; } public boolean isResizable(long fileSize, String extension, long maxSizeBytes) { @@ -37,7 +40,7 @@ public boolean isResizable(long fileSize, String extension, long maxSizeBytes) { } public void validateExtension(String extension) { - if (extension == null || !FileConstants.ALL_ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) { + if (extension == null || !getAllowedExtensions().contains(extension.toLowerCase())) { throw new CustomException(ErrorCode.NOT_ALLOWED_FILE_EXTENSIONS, "허용된 형식: " + getAllowedExtensionsMessage()); } @@ -48,6 +51,13 @@ public boolean isImage(String extension) { } public String getAllowedExtensionsMessage() { - return String.join(", ", FileConstants.ALL_ALLOWED_EXTENSIONS); + return String.join(", ", getAllowedExtensions()); + } + + private List getAllowedExtensions() { + if (imageOnly) { + return FileConstants.IMAGE_EXTENSIONS; + } + return FileConstants.ALL_ALLOWED_EXTENSIONS; } } diff --git a/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java index c4485858a..543aa924f 100644 --- a/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java +++ b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java @@ -110,5 +110,45 @@ class 파일_검증 { .doesNotThrowAnyException() ); } + + @Test + void 이미지_전용_업로드_경로는_문서_확장자를_허용하지_않는다() { + // given + MockMultipartFile pdfFile = createMockFile("logo.pdf", 100); + + // when & then + assertThatThrownBy(() -> s3Service.uploadFile( + pdfFile, + UploadPath.ADMIN_UNIVERSITY_LOGO, + "university_of_tokyo" + )) + .isInstanceOf(CustomException.class) + .hasMessageContaining("허용된 형식"); + } + + @Test + void 멘토_증빙_업로드는_이미지_외의_허용된_문서_확장자도_검증을_통과한다() { + // given + MockMultipartFile pdfFile = createMockFile("proof.pdf", 100); + + // when & then + assertThatCode(() -> s3Service.uploadFile(pdfFile, UploadPath.MENTOR_PROOF)) + .doesNotThrowAnyException(); + } + + @Test + void 성적_증빙_업로드는_이미지_외의_허용된_문서_확장자도_검증을_통과한다() { + // given + MockMultipartFile gpaPdfFile = createMockFile("gpa.pdf", 100); + MockMultipartFile languageTestPdfFile = createMockFile("language-test.pdf", 100); + + // when & then + assertAll( + () -> assertThatCode(() -> s3Service.uploadFile(gpaPdfFile, UploadPath.GPA)) + .doesNotThrowAnyException(), + () -> assertThatCode(() -> s3Service.uploadFile(languageTestPdfFile, UploadPath.LANGUAGE_TEST)) + .doesNotThrowAnyException() + ); + } } }