검증게시판 첨부파일 첨부 및 삭제기능
This commit is contained in:
+20
-3
@@ -12,6 +12,8 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -64,14 +66,29 @@ public class VerificationIssueImageController {
|
||||
return service.loadFile(issueId, imageId)
|
||||
.map(resource -> {
|
||||
String type = service.contentType(issueId, imageId).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
return ResponseEntity.ok()
|
||||
var builder = ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(type))
|
||||
.header(HttpHeaders.CACHE_CONTROL, "private, max-age=3600")
|
||||
.body(resource);
|
||||
.header(HttpHeaders.CACHE_CONTROL, "private, max-age=3600");
|
||||
if (!type.startsWith("image/")) {
|
||||
service.fileName(issueId, imageId)
|
||||
.ifPresent(name -> builder.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
attachmentContentDisposition(name)));
|
||||
}
|
||||
return builder.body(resource);
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
private static String attachmentContentDisposition(String fileName) {
|
||||
String safe = (fileName == null || fileName.isBlank()) ? "download" : fileName;
|
||||
String ascii = safe.replaceAll("[^\\x20-\\x7E]", "_");
|
||||
if (ascii.isBlank()) ascii = "download";
|
||||
ascii = ascii.replace("\"", "\\\"");
|
||||
String encoded = URLEncoder.encode(safe, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
return "attachment; filename=\"" + ascii + "\"; filename*=UTF-8''" + encoded;
|
||||
}
|
||||
|
||||
@DeleteMapping("/{imageId}")
|
||||
public ResponseEntity<Void> delete(
|
||||
@PathVariable Long issueId,
|
||||
|
||||
@@ -93,7 +93,7 @@ public class VerificationIssueImageService {
|
||||
String contentType = resolveContentType(file, data);
|
||||
validateFile(data.length, contentType);
|
||||
|
||||
String original = sanitizeFileName(file.getOriginalFilename());
|
||||
String original = resolveOriginalFileName(file, contentType);
|
||||
String storedName = UUID.randomUUID() + "_" + original;
|
||||
Path target = issueDir.resolve(storedName);
|
||||
Files.write(target, data);
|
||||
@@ -140,6 +140,12 @@ public class VerificationIssueImageService {
|
||||
.map(GcVerificationIssueImage::getContentType);
|
||||
}
|
||||
|
||||
public Optional<String> fileName(Long issueId, Long imageId) {
|
||||
return imageRepository.findById(imageId)
|
||||
.filter(img -> issueId.equals(img.getIssueId()))
|
||||
.map(GcVerificationIssueImage::getFileName);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean delete(Long issueId, Long imageId) throws IOException {
|
||||
Optional<GcVerificationIssueImage> opt = imageRepository.findById(imageId)
|
||||
@@ -234,9 +240,7 @@ public class VerificationIssueImageService {
|
||||
if (header != null && header.length >= 8
|
||||
&& header[0] == (byte) 0xD0 && header[1] == (byte) 0xCF
|
||||
&& header[2] == 0x11 && header[3] == (byte) 0xE0) {
|
||||
if (fileName != null && fileName.toLowerCase().endsWith(".hwp")) {
|
||||
return "application/x-hwp";
|
||||
}
|
||||
return "application/x-hwp";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -274,10 +278,47 @@ public class VerificationIssueImageService {
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String name) {
|
||||
if (name == null || name.isBlank()) return "attachment.bin";
|
||||
String base = Paths.get(name).getFileName().toString();
|
||||
return base.replaceAll("[^a-zA-Z0-9._\\-가-힣]", "_");
|
||||
/** 업로드 multipart의 원본 파일명·확장자를 그대로 보존 (경로·위험 문자만 제거) */
|
||||
private String resolveOriginalFileName(MultipartFile file, String contentType) {
|
||||
String name = extractBaseFileName(file.getOriginalFilename());
|
||||
if (name == null || name.isBlank()) {
|
||||
name = defaultNameForContentType(contentType);
|
||||
}
|
||||
return truncateFileName(name, 255);
|
||||
}
|
||||
|
||||
private static String extractBaseFileName(String name) {
|
||||
if (name == null || name.isBlank()) return null;
|
||||
String normalized = name.replace('\\', '/').trim();
|
||||
int slash = normalized.lastIndexOf('/');
|
||||
String base = slash >= 0 ? normalized.substring(slash + 1) : normalized;
|
||||
if (base.isBlank() || ".".equals(base) || "..".equals(base)) return null;
|
||||
base = base.replaceAll("[\\x00-\\x1F<>:\"|?*]", "");
|
||||
return base.isBlank() ? null : base;
|
||||
}
|
||||
|
||||
private static String defaultNameForContentType(String contentType) {
|
||||
if (contentType == null) return "attachment";
|
||||
return switch (contentType) {
|
||||
case "application/x-hwp", "application/haansoft-hwp",
|
||||
"application/vnd.hancom.hwp", "application/hwp+zip" -> "attachment.hwp";
|
||||
case "application/vnd.hancom.hwpx", "application/x-hwpx" -> "attachment.hwpx";
|
||||
case "application/zip", "application/x-zip-compressed" -> "attachment.zip";
|
||||
default -> "attachment";
|
||||
};
|
||||
}
|
||||
|
||||
private static String truncateFileName(String name, int maxLen) {
|
||||
if (name.length() <= maxLen) return name;
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot > 0 && dot < name.length() - 1) {
|
||||
String ext = name.substring(dot);
|
||||
int baseMax = maxLen - ext.length();
|
||||
if (baseMax > 0) {
|
||||
return name.substring(0, baseMax) + ext;
|
||||
}
|
||||
}
|
||||
return name.substring(0, maxLen);
|
||||
}
|
||||
|
||||
private VerificationIssueImageDto toDto(GcVerificationIssueImage e) {
|
||||
|
||||
Reference in New Issue
Block a user