Skip to content

Commit e885d65

Browse files
committed
feat: Apache Tika 기반 이미지 파일 유효성 검증 로직 구현
1 parent 5a536d3 commit e885d65

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/users/service/UserService.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
1313
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
1414
import com.back.web7_9_codecrete_be.global.storage.FileStorageService;
15+
import com.back.web7_9_codecrete_be.global.storage.ImageFileValidator;
1516
import lombok.RequiredArgsConstructor;
1617
import lombok.extern.slf4j.Slf4j;
1718
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -34,7 +35,7 @@ public class UserService {
3435
private final UserRestoreTokenRedisRepository userRestoreTokenRedisRepository;
3536
private final EmailService emailService;
3637

37-
private static final long MAX_PROFILE_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
38+
private final ImageFileValidator imageFileValidator;
3839

3940
// 내 정보 조회
4041
@Transactional(readOnly = true)
@@ -74,14 +75,7 @@ public void deleteMyAccount(User user) {
7475
public String updateProfileImage(User user, MultipartFile file) {
7576
validateActiveUser(user);
7677

77-
// 파일 유효성 검사
78-
if (file == null || file.isEmpty()) {
79-
throw new BusinessException(UserErrorCode.INVALID_PROFILE_IMAGE);
80-
}
81-
82-
if (file.getSize() > MAX_PROFILE_IMAGE_SIZE) {
83-
throw new BusinessException(UserErrorCode.PROFILE_IMAGE_SIZE_EXCEEDED);
84-
}
78+
imageFileValidator.validateImageFile(file);
8579

8680
// 기존 이미지 URL 보관
8781
String oldImageUrl = user.getProfileImage();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.back.web7_9_codecrete_be.global.storage;
2+
3+
import com.back.web7_9_codecrete_be.global.error.code.FileErrorCode;
4+
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
5+
import org.apache.tika.Tika;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.web.multipart.MultipartFile;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
12+
@Component
13+
public class ImageFileValidator {
14+
15+
private final Tika tika = new Tika();
16+
private static final long MAX_PROFILE_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
17+
18+
public void validateImageFile(MultipartFile file) {
19+
20+
// 파일 비어있는지 검사
21+
if (file == null || file.isEmpty()) {
22+
throw new BusinessException(FileErrorCode.FILE_EMPTY);
23+
}
24+
25+
// 파일 크기 검사
26+
if (file.getSize() > MAX_PROFILE_IMAGE_SIZE) {
27+
throw new BusinessException(FileErrorCode.PROFILE_IMAGE_SIZE_EXCEEDED);
28+
}
29+
30+
// 실제 MIME 타입 분석
31+
String detectedMimeType;
32+
try (InputStream is = file.getInputStream()) {
33+
detectedMimeType = tika.detect(is);
34+
} catch (IOException e) {
35+
throw new BusinessException(FileErrorCode.FILE_ANALYZE_FAILED);
36+
}
37+
38+
ImageMimeType imageMimeType = ImageMimeType.from(detectedMimeType);
39+
40+
// 확장자 추출
41+
String filename = file.getOriginalFilename();
42+
if (filename == null || !filename.contains(".")) {
43+
throw new BusinessException(FileErrorCode.EXTENSION_MISMATCH);
44+
}
45+
46+
String extension = filename.substring(filename.lastIndexOf('.') + 1);
47+
48+
// 확장자 ↔ MIME 타입 매칭
49+
if (!imageMimeType.matches(extension)) {
50+
throw new BusinessException(FileErrorCode.EXTENSION_MISMATCH);
51+
}
52+
}
53+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.back.web7_9_codecrete_be.global.storage;
2+
3+
import com.back.web7_9_codecrete_be.global.error.code.FileErrorCode;
4+
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
5+
6+
import java.util.Arrays;
7+
import java.util.List;
8+
9+
public enum ImageMimeType {
10+
11+
JPEG("image/jpeg", List.of("jpg", "jpeg")),
12+
PNG("image/png", List.of("png")),
13+
WEBP("image/webp", List.of("webp"));
14+
15+
private final String mimeType;
16+
private final List<String> extensions;
17+
18+
ImageMimeType(String mimeType, List<String> extensions) {
19+
this.mimeType = mimeType;
20+
this.extensions = extensions;
21+
}
22+
23+
public static ImageMimeType from(String detectedMimeType) {
24+
return Arrays.stream(values())
25+
.filter(it -> it.mimeType.equals(detectedMimeType))
26+
.findFirst()
27+
.orElseThrow(() ->
28+
new BusinessException(FileErrorCode.INVALID_IMAGE_TYPE)
29+
);
30+
}
31+
32+
public boolean matches(String extension) {
33+
return extensions.contains(extension.toLowerCase());
34+
}
35+
}
36+

0 commit comments

Comments
 (0)