Loading...
Loading...
02-reusable-code-java/05-util/FileUtil.java
/**
* 파일 업로드/삭제/검증 유틸리티
* - 날짜별 디렉토리 자동 생성 (yyyy/MM)
* - UUID 기반 고유 파일명 생성
* - 경로 순회(Path Traversal) 공격 방지
* - MIME 타입 기반 파일 유효성 검사
* - 파일 크기 포맷 변환
*
* @source kcsi-smpa-internal
* @extracted 2026-03-08
* @version 1.0.0
*
* @dependencies spring-boot-starter-web
*
* @config application.yml 설정 항목:
* app.upload.path: ./uploads (업로드 경로)
* app.upload.allowed-types: image/jpeg,image/png,image/gif,application/pdf (허용 MIME 타입)
* app.upload.max-files: 5 (최대 업로드 파일 수)
*/
package com.example.app.util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 파일 유틸리티
*/
@Component
public class FileUtil {
@Value("${app.upload.path:./uploads}")
private String uploadPath;
@Value("${app.upload.allowed-types:image/jpeg,image/png,image/gif,image/webp,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document}")
private String allowedTypes;
@Value("${app.upload.max-files:5}")
private int maxFiles;
/**
* 파일 업로드
* @param file 업로드할 파일
* @return 상대 경로 (예: "2026/03/uuid.png")
*/
public String uploadFile(MultipartFile file) throws IOException {
validateFile(file);
// 날짜별 디렉토리 생성 (절대 경로로 통일)
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
Path uploadDir = Paths.get(uploadPath, dateDir).toAbsolutePath();
Files.createDirectories(uploadDir);
// 고유 파일명 생성
String originalFilename = file.getOriginalFilename();
String extension = getExtension(originalFilename);
String newFilename = UUID.randomUUID().toString() + extension;
// 파일 저장 (절대 경로 사용 — transferTo는 상대 경로를 Tomcat 임시 디렉토리 기준으로 해석)
Path filePath = uploadDir.resolve(newFilename).toAbsolutePath();
file.transferTo(filePath.toFile());
// 상대 경로 반환
return dateDir + "/" + newFilename;
}
/**
* 파일 삭제
* @param relativePath 상대 경로
* @return 삭제 성공 여부
*/
public boolean deleteFile(String relativePath) {
try {
Path filePath = resolveAndValidate(relativePath);
return Files.deleteIfExists(filePath);
} catch (IOException | IllegalArgumentException e) {
return false;
}
}
/**
* 파일 존재 여부 확인
*/
public boolean fileExists(String relativePath) {
try {
Path filePath = resolveAndValidate(relativePath);
return Files.exists(filePath);
} catch (IllegalArgumentException e) {
return false;
}
}
/**
* 파일 경로 조회
*/
public File getFile(String relativePath) {
Path filePath = resolveAndValidate(relativePath);
return filePath.toFile();
}
/**
* 경로 순회(Path Traversal) 방지: 정규화 후 업로드 디렉토리 내부인지 검증
* - "../" 등을 이용한 디렉토리 탈출 차단
*/
private Path resolveAndValidate(String relativePath) {
Path base = Paths.get(uploadPath).toAbsolutePath().normalize();
Path resolved = base.resolve(relativePath).normalize();
if (!resolved.startsWith(base)) {
throw new IllegalArgumentException("잘못된 파일 경로입니다.");
}
return resolved;
}
/**
* 파일 유효성 검사 (빈 파일, MIME 타입)
*/
public void validateFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("파일이 비어있습니다.");
}
String contentType = file.getContentType();
List<String> allowed = Arrays.asList(allowedTypes.split(","));
if (!allowed.contains(contentType)) {
throw new IllegalArgumentException("허용되지 않는 파일 형식입니다: " + contentType);
}
}
/**
* 파일 확장자 추출
*/
public String getExtension(String filename) {
if (filename == null || filename.lastIndexOf('.') == -1) {
return "";
}
return filename.substring(filename.lastIndexOf('.'));
}
/**
* 파일 크기 포맷 (바이트 → 읽기 쉬운 형태)
*/
public String formatFileSize(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1f KB", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
}
}
/**
* 허용된 최대 파일 수 반환
*/
public int getMaxFiles() {
return maxFiles;
}
}