Merge remote-tracking branch 'origin/jenkins_with_weblogic' of C:/KJB_DEV/eapim-bundle/bundles/260107/eapim-admin_incremental_2025-12-15.bundle into jenkins_with_weblogic
This commit is contained in:
@@ -53,6 +53,12 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
"^data:image/.*",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// XSS 문자 변환을 건너뛸 파라미터 목록 (리치 텍스트 에디터 콘텐츠 등)
|
||||
// XSS 패턴 필터링은 적용하지만 <, > 등으로 변환하지 않음
|
||||
private static final String[] SKIP_CHAR_CONVERT_PARAMS = {"contents", "content", "description"};
|
||||
|
||||
// 현재 처리 중인 파라미터명 (cleanXSS에서 문자 변환 여부 결정에 사용)
|
||||
private ThreadLocal<String> currentParameter = new ThreadLocal<>();
|
||||
|
||||
public RequestWrapper(HttpServletRequest servletRequest) {
|
||||
super(servletRequest);
|
||||
@@ -67,8 +73,13 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
int count = values.length;
|
||||
String[] encodedValues = new String[count];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
encodedValues[i] = cleanXSS(values[i]);
|
||||
currentParameter.set(parameter);
|
||||
try {
|
||||
for (int i = 0; i < count; i++) {
|
||||
encodedValues[i] = cleanXSS(values[i]);
|
||||
}
|
||||
} finally {
|
||||
currentParameter.remove();
|
||||
}
|
||||
|
||||
return encodedValues;
|
||||
@@ -81,13 +92,19 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
for (Entry<String, String[]> entry : map.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
String paramName = entry.getKey();
|
||||
|
||||
if (values != null && values.length > 0) {
|
||||
String[] cloneValues = new String[values.length];
|
||||
for (int idx = 0; idx < values.length; idx++) {
|
||||
cloneValues[idx] = cleanXSS(values[idx]);
|
||||
currentParameter.set(paramName);
|
||||
try {
|
||||
for (int idx = 0; idx < values.length; idx++) {
|
||||
cloneValues[idx] = cleanXSS(values[idx]);
|
||||
}
|
||||
} finally {
|
||||
currentParameter.remove();
|
||||
}
|
||||
clonedMap.put(entry.getKey(), cloneValues);
|
||||
clonedMap.put(paramName, cloneValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +119,12 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
return value;
|
||||
}
|
||||
|
||||
return cleanXSS(value);
|
||||
currentParameter.set(parameter);
|
||||
try {
|
||||
return cleanXSS(value);
|
||||
} finally {
|
||||
currentParameter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,9 +156,11 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
|
||||
value = filterSrcAttribute(value);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
value = convertChars(value, sb);
|
||||
// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용
|
||||
if (!shouldSkipCharConvert()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
value = convertChars(value, sb);
|
||||
}
|
||||
|
||||
// eval, javascript 등 스크립트 관련 문자열 제거
|
||||
value = value.replaceAll("(?i)eval\\((.*?)\\)", "");
|
||||
@@ -144,6 +168,23 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 파라미터가 문자 변환을 건너뛸지 여부 확인
|
||||
* 리치 텍스트 에디터 콘텐츠 등은 HTML 태그가 필요하므로 < > 변환을 하지 않음
|
||||
*/
|
||||
private boolean shouldSkipCharConvert() {
|
||||
String param = currentParameter.get();
|
||||
if (param == null) {
|
||||
return false;
|
||||
}
|
||||
for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
|
||||
if (StringUtils.equalsIgnoreCase(param, skipParam)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* src 속성을 화이트리스트 방식으로 필터링
|
||||
* 허용: data:image/ (Base64 이미지)
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
@@ -32,6 +33,14 @@ public class AuthorizeInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
|
||||
// InterceptorSkipController 구현 컨트롤러는 권한 체크 스킵
|
||||
if (handler instanceof HandlerMethod) {
|
||||
Class<?> clazz = ((HandlerMethod) handler).getBeanType();
|
||||
if (ClassUtils.isAssignable(clazz, InterceptorSkipController.class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 요청 URI, 사용자 ID, 서비스 타입, cmd 추출
|
||||
String uri = request.getRequestURI();
|
||||
String userId = SessionManager.getUserId(request);
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalterms;
|
||||
|
||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 에디터 이미지 컨트롤러
|
||||
* - summernote 에디터에서 사용하는 이미지 업로드/서빙 API
|
||||
* - InterceptorSkipController: 인터셉터의 상세 검증 우회 (필터에서 기본 로그인 체크는 수행됨)
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/onl/apim/editor/image")
|
||||
public class EditorImageController extends BaseAnnotationController implements InterceptorSkipController {
|
||||
|
||||
private final EditorImageService editorImageService;
|
||||
|
||||
@Autowired
|
||||
public EditorImageController(EditorImageService editorImageService) {
|
||||
this.editorImageService = editorImageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 업로드 API
|
||||
* - summernote에서 이미지 선택/붙여넣기 시 호출
|
||||
* - 즉시 DB에 저장하고 플레이스홀더 정보 반환
|
||||
*
|
||||
* @param file MultipartFile 이미지 파일
|
||||
* @return { success: true, fileId: "uuid", fileSn: 1, placeholder: "{{IMG:uuid:1}}" }
|
||||
*/
|
||||
@PostMapping("/upload.json")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> uploadImage(@RequestParam("file") MultipartFile file) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 파일 검증
|
||||
if (file.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("error", "파일이 비어있습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// 이미지 타입 검증
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("image/")) {
|
||||
result.put("success", false);
|
||||
result.put("error", "이미지 파일만 업로드 가능합니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// 파일 크기 검증 (10MB)
|
||||
long maxSize = 10 * 1024 * 1024;
|
||||
if (file.getSize() > maxSize) {
|
||||
result.put("success", false);
|
||||
result.put("error", "파일 크기는 10MB를 초과할 수 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// 이미지 저장
|
||||
EditorImageDTO saved = editorImageService.saveImage(file);
|
||||
|
||||
// 성공 응답
|
||||
result.put("success", true);
|
||||
result.put("fileId", saved.getFileId());
|
||||
result.put("fileSn", saved.getFileSn());
|
||||
result.put("placeholder", "{{IMG:" + saved.getFileId() + ":" + saved.getFileSn() + "}}");
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("이미지 업로드 중 오류 발생", e);
|
||||
result.put("success", false);
|
||||
result.put("error", "이미지 업로드 중 오류가 발생했습니다: " + e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 서빙 API
|
||||
* - 브라우저에서 이미지 표시용 (관리자포탈)
|
||||
* - 캐시 설정으로 성능 최적화
|
||||
*
|
||||
* @param fileId 파일 ID (PTL_FILE.FILE_ID)
|
||||
* @param fileSn 파일 순번 (PTL_FILE_DETAIL.FILE_SN)
|
||||
* @return 이미지 바이너리 데이터
|
||||
*/
|
||||
@GetMapping("/view.do")
|
||||
public ResponseEntity<byte[]> viewImage(
|
||||
@RequestParam String fileId,
|
||||
@RequestParam(defaultValue = "1") int fileSn) {
|
||||
try {
|
||||
EditorImageDTO image = editorImageService.getImage(fileId, fileSn);
|
||||
|
||||
if (image == null || image.getImageData() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType(image.getContentType()));
|
||||
headers.setContentLength(image.getFileSize());
|
||||
headers.setCacheControl("public, max-age=86400"); // 1일 캐시
|
||||
headers.set("Content-Disposition", "inline");
|
||||
|
||||
return new ResponseEntity<>(image.getImageData(), headers, HttpStatus.OK);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("이미지 조회 중 오류 발생: fileId=" + fileId + ", fileSn=" + fileSn, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 삭제 API
|
||||
* - 에디터에서 이미지 삭제 시 호출
|
||||
* - DB에서 파일 삭제
|
||||
*
|
||||
* @param fileId 파일 ID (PTL_FILE.FILE_ID)
|
||||
* @return { success: true }
|
||||
*/
|
||||
@PostMapping("/delete.json")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> deleteImage(@RequestParam String fileId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
if (fileId == null || fileId.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("error", "파일 ID가 필요합니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
editorImageService.deleteImage(fileId);
|
||||
|
||||
result.put("success", true);
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("이미지 삭제 중 오류 발생: fileId=" + fileId, e);
|
||||
result.put("success", false);
|
||||
result.put("error", "이미지 삭제 중 오류가 발생했습니다: " + e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalterms;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 에디터 이미지 DTO
|
||||
* - summernote 에디터에서 업로드된 이미지 정보를 담는 DTO
|
||||
*/
|
||||
@Data
|
||||
public class EditorImageDTO {
|
||||
|
||||
/**
|
||||
* 파일 ID (PTL_FILE.FILE_ID)
|
||||
*/
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* 파일 순번 (PTL_FILE_DETAIL.FILE_SN)
|
||||
*/
|
||||
private int fileSn;
|
||||
|
||||
/**
|
||||
* 원본 파일명
|
||||
*/
|
||||
private String originalFileName;
|
||||
|
||||
/**
|
||||
* 파일 확장자
|
||||
*/
|
||||
private String fileExtension;
|
||||
|
||||
/**
|
||||
* 파일 크기 (bytes)
|
||||
*/
|
||||
private int fileSize;
|
||||
|
||||
/**
|
||||
* Content-Type (MIME 타입)
|
||||
*/
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* 이미지 바이너리 데이터 (조회 시에만 사용)
|
||||
*/
|
||||
private byte[] imageData;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalterms;
|
||||
|
||||
import com.eactive.apim.portal.file.entity.FileDetail;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 에디터 이미지 서비스
|
||||
* - summernote 에디터에서 사용하는 이미지 업로드/조회/삭제 서비스
|
||||
* - FileService를 래핑하여 이미지 전용 기능 제공
|
||||
*/
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@Slf4j
|
||||
public class EditorImageService {
|
||||
|
||||
/**
|
||||
* 플레이스홀더 패턴: {{IMG:fileId:fileSn}} 또는 {{IMG:fileId:fileSn|속성}}
|
||||
* - fileId: UUID 형식 (예: 550e8400-e29b-41d4-a716-446655440000)
|
||||
* - fileSn: 정수 (예: 1)
|
||||
* - 속성: 선택적 (예: |width=300|height=200|style=max-width:100%)
|
||||
*/
|
||||
private static final Pattern PLACEHOLDER_PATTERN =
|
||||
Pattern.compile("\\{\\{IMG:([a-f0-9\\-]+):(\\d+)(\\|[^}]+)?\\}\\}");
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@Autowired
|
||||
public EditorImageService(FileService fileService) {
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 저장
|
||||
*
|
||||
* @param file MultipartFile 이미지 파일
|
||||
* @return EditorImageDTO (fileId, fileSn 포함)
|
||||
* @throws IOException 파일 처리 중 오류 발생 시
|
||||
*/
|
||||
public EditorImageDTO saveImage(MultipartFile file) throws IOException {
|
||||
String fileName = file.getOriginalFilename();
|
||||
String contentType = file.getContentType();
|
||||
byte[] data = file.getBytes();
|
||||
|
||||
// FileService를 통해 저장
|
||||
FileInfo fileInfo = fileService.createSingleFileFromBytes(fileName, contentType, data);
|
||||
FileDetail detail = fileInfo.getFileDetails().get(0);
|
||||
|
||||
// DTO로 변환하여 반환
|
||||
EditorImageDTO dto = new EditorImageDTO();
|
||||
dto.setFileId(fileInfo.getFileId());
|
||||
dto.setFileSn(detail.getFileSn());
|
||||
dto.setOriginalFileName(detail.getOriginalFileName());
|
||||
dto.setFileExtension(detail.getFileExtension());
|
||||
dto.setFileSize(detail.getFileSize());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 조회
|
||||
*
|
||||
* @param fileId 파일 ID
|
||||
* @param fileSn 파일 순번
|
||||
* @return EditorImageDTO (imageData 포함)
|
||||
* @throws IOException 파일 조회 중 오류 발생 시
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public EditorImageDTO getImage(String fileId, int fileSn) throws IOException {
|
||||
FileDetail detail = fileService.getFileDetailByIds(fileId, fileSn);
|
||||
byte[] data = fileService.retrieveFileContent(detail);
|
||||
|
||||
EditorImageDTO dto = new EditorImageDTO();
|
||||
dto.setFileId(fileId);
|
||||
dto.setFileSn(fileSn);
|
||||
dto.setOriginalFileName(detail.getOriginalFileName());
|
||||
dto.setFileExtension(detail.getFileExtension());
|
||||
dto.setFileSize(detail.getFileSize());
|
||||
dto.setContentType(getContentType(detail.getFileExtension()));
|
||||
dto.setImageData(data);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 본문에서 사용 중인 파일 ID 추출
|
||||
*
|
||||
* @param contents 본문 내용 (플레이스홀더 포함)
|
||||
* @return 사용 중인 파일 ID Set
|
||||
*/
|
||||
public Set<String> extractFileIds(String contents) {
|
||||
Set<String> fileIds = new HashSet<>();
|
||||
if (contents == null || contents.isEmpty()) {
|
||||
return fileIds;
|
||||
}
|
||||
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(contents);
|
||||
while (matcher.find()) {
|
||||
fileIds.add(matcher.group(1)); // fileId
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 삭제
|
||||
*
|
||||
* @param fileId 삭제할 파일 ID
|
||||
*/
|
||||
public void deleteImage(String fileId) {
|
||||
try {
|
||||
fileService.deleteFile(fileId);
|
||||
log.info("Deleted image: {}", fileId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to delete image: {}", fileId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미사용 이미지 정리
|
||||
* - 원래 저장된 이미지 중 현재 본문에서 사용되지 않는 이미지 삭제
|
||||
*
|
||||
* @param usedFileIds 본문에서 사용 중인 파일 ID 목록
|
||||
* @param originalFileIds 원래 저장된 모든 파일 ID 목록
|
||||
*/
|
||||
public void cleanupUnusedImages(Set<String> usedFileIds, Set<String> originalFileIds) {
|
||||
if (originalFileIds == null || originalFileIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String fileId : originalFileIds) {
|
||||
if (!usedFileIds.contains(fileId)) {
|
||||
try {
|
||||
fileService.deleteFile(fileId);
|
||||
log.info("Deleted unused image: {}", fileId);
|
||||
} catch (Exception e) {
|
||||
// 삭제 실패 시 로그만 남기고 계속 진행
|
||||
log.warn("Failed to delete unused image: {}", fileId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플레이스홀더를 실제 이미지 태그로 변환
|
||||
*
|
||||
* @param contents 본문 내용 (플레이스홀더 포함)
|
||||
* @param baseUrl 이미지 서빙 베이스 URL (예: /onl/apim/editor/image/view/)
|
||||
* @return 이미지 태그가 포함된 HTML
|
||||
*/
|
||||
public String convertPlaceholdersToHtml(String contents, String baseUrl) {
|
||||
if (contents == null || contents.isEmpty()) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(contents);
|
||||
|
||||
while (matcher.find()) {
|
||||
String fileId = matcher.group(1);
|
||||
String fileSn = matcher.group(2);
|
||||
String attrs = matcher.group(3); // |width=300|height=200 형태
|
||||
|
||||
StringBuilder img = new StringBuilder();
|
||||
img.append("<img src=\"").append(baseUrl).append(fileId).append("/").append(fileSn).append("\"");
|
||||
|
||||
// 속성 파싱 및 추가
|
||||
if (attrs != null && !attrs.isEmpty()) {
|
||||
String[] parts = attrs.substring(1).split("\\|"); // 앞의 | 제거 후 분리
|
||||
for (String part : parts) {
|
||||
String[] kv = part.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
img.append(" ").append(kv[0]).append("=\"").append(kv[1]).append("\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 스타일 (style 속성이 없으면 추가)
|
||||
if (attrs == null || !attrs.contains("style=")) {
|
||||
img.append(" style=\"max-width:100%;height:auto;\"");
|
||||
}
|
||||
|
||||
img.append(" />");
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(img.toString()));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 확장자를 Content-Type으로 변환
|
||||
*
|
||||
* @param extension 파일 확장자
|
||||
* @return MIME 타입
|
||||
*/
|
||||
private String getContentType(String extension) {
|
||||
if (extension == null) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
switch (extension.toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "bmp":
|
||||
return "image/bmp";
|
||||
case "webp":
|
||||
return "image/webp";
|
||||
case "svg":
|
||||
return "image/svg+xml";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -13,12 +13,16 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@@ -72,14 +76,22 @@ public class PortalTermsManController extends BaseAnnotationController {
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=INSERT")
|
||||
public ResponseEntity<String> insert(PortalTermsUI portalTermsUI) throws IOException {
|
||||
portalTermsManService.insert(portalTermsUI);
|
||||
public ResponseEntity<String> insert(PortalTermsUI portalTermsUI,
|
||||
@RequestParam(value = "attachedFile", required = false) MultipartFile attachedFile) throws IOException {
|
||||
portalTermsManService.insert(portalTermsUI, attachedFile);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=UPDATE")
|
||||
public ResponseEntity<String> update(PortalTermsUI portalTermsUI) throws IOException {
|
||||
portalTermsManService.update(portalTermsUI);
|
||||
public ResponseEntity<String> update(PortalTermsUI portalTermsUI, String usedFileIds,
|
||||
@RequestParam(value = "attachedFile", required = false) MultipartFile attachedFile,
|
||||
@RequestParam(value = "deleteAttachment", defaultValue = "false") boolean deleteAttachment) throws IOException {
|
||||
// 사용 중인 파일 ID 목록 파싱
|
||||
Set<String> usedFileIdSet = new HashSet<>();
|
||||
if (usedFileIds != null && !usedFileIds.isEmpty()) {
|
||||
usedFileIdSet.addAll(Arrays.asList(usedFileIds.split(",")));
|
||||
}
|
||||
portalTermsManService.update(portalTermsUI, usedFileIdSet, attachedFile, deleteAttachment);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,24 @@ import com.eactive.apim.portal.agreements.entity.AgreementType;
|
||||
import com.eactive.apim.portal.agreements.entity.Agreements;
|
||||
import com.eactive.apim.portal.agreements.entity.AgreementsId;
|
||||
import com.eactive.apim.portal.agreements.service.AgreementsService;
|
||||
import com.eactive.apim.portal.file.entity.FileDetail;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.portalterms.PortalTermsService;
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@@ -21,16 +30,22 @@ public class PortalTermsManService extends BaseService {
|
||||
private final PortalTermsUIMapper portalTermsUIMapper;
|
||||
private final AgreementsService agreementsService;
|
||||
private final UserInfoService userInfoService;
|
||||
private final EditorImageService editorImageService;
|
||||
private final FileService fileService;
|
||||
|
||||
@Autowired
|
||||
public PortalTermsManService(PortalTermsService portalTermsService,
|
||||
PortalTermsUIMapper portalTermsUIMapper,
|
||||
AgreementsService agreementsService,
|
||||
UserInfoService userInfoService){
|
||||
UserInfoService userInfoService,
|
||||
EditorImageService editorImageService,
|
||||
FileService fileService){
|
||||
this.portalTermsService = portalTermsService;
|
||||
this.portalTermsUIMapper = portalTermsUIMapper;
|
||||
this.agreementsService = agreementsService;
|
||||
this.userInfoService = userInfoService;
|
||||
this.editorImageService = editorImageService;
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
private String findName(String userId) {
|
||||
@@ -55,25 +70,90 @@ public class PortalTermsManService extends BaseService {
|
||||
portalTermsUI.setCreatedByName(findName(agreements.getCreatedBy()));
|
||||
portalTermsUI.setLastModifiedByName(findName(agreements.getLastModifiedBy()));
|
||||
portalTermsUI.setName(agreements.getAgreementsType().getDescription());
|
||||
|
||||
// 첨부파일 정보 조회
|
||||
if (StringUtils.isNotBlank(agreements.getAttachedFileId())) {
|
||||
try {
|
||||
FileInfo fileInfo = fileService.findById(agreements.getAttachedFileId());
|
||||
if (fileInfo != null && !fileInfo.getFileDetails().isEmpty()) {
|
||||
FileDetail fileDetail = fileInfo.getFileDetails().get(0);
|
||||
portalTermsUI.setAttachedFileName(fileDetail.getOriginalFileName() + "." + fileDetail.getFileExtension());
|
||||
portalTermsUI.setAttachedFileSize((long) fileDetail.getFileSize());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 파일 조회 실패 시 무시
|
||||
logger.warn("첨부파일 조회 실패: {}", agreements.getAttachedFileId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return portalTermsUI;
|
||||
}
|
||||
|
||||
public void insert(PortalTermsUI portalTermsUI) {
|
||||
public void insert(PortalTermsUI portalTermsUI, MultipartFile attachedFile) throws IOException {
|
||||
portalTermsUI.setName(AgreementType.fromCode(portalTermsUI.getAgreementsType()).getDescription());
|
||||
portalTermsUI.setContents(StringEscapeUtils.unescapeHtml(portalTermsUI.getContents()));
|
||||
Agreements agreements = portalTermsUIMapper.toEntity(portalTermsUI);
|
||||
|
||||
// 첨부파일 저장
|
||||
if (attachedFile != null && !attachedFile.isEmpty()) {
|
||||
FileInfo fileInfo = fileService.createOrUpdateSingleFile(null, attachedFile, attachedFile.getOriginalFilename());
|
||||
agreements.setAttachedFileId(fileInfo.getFileId());
|
||||
}
|
||||
|
||||
agreementsService.save(agreements);
|
||||
}
|
||||
|
||||
public void update(PortalTermsUI portalTermsUI) {
|
||||
public void update(PortalTermsUI portalTermsUI, Set<String> usedFileIds,
|
||||
MultipartFile attachedFile, boolean deleteAttachment) throws IOException {
|
||||
Agreements agreements = agreementsService.findById(portalTermsUI.getAgreementsType(), portalTermsUI.getRevision());
|
||||
|
||||
// 원래 내용에서 사용 중이던 파일 ID 추출
|
||||
Set<String> originalFileIds = editorImageService.extractFileIds(agreements.getContents());
|
||||
|
||||
// 내용 업데이트
|
||||
agreements.setName(AgreementType.fromCode(portalTermsUI.getAgreementsType()).getDescription());
|
||||
agreements.setContents(StringEscapeUtils.unescapeHtml(portalTermsUI.getContents()));
|
||||
agreements.setPublishedOn(portalTermsUI.getPublishedOn());
|
||||
|
||||
// 첨부파일 처리
|
||||
String oldAttachedFileId = agreements.getAttachedFileId();
|
||||
|
||||
if (deleteAttachment && StringUtils.isNotBlank(oldAttachedFileId)) {
|
||||
// 첨부파일 삭제 요청
|
||||
fileService.deleteFile(oldAttachedFileId);
|
||||
agreements.setAttachedFileId(null);
|
||||
} else if (attachedFile != null && !attachedFile.isEmpty()) {
|
||||
// 새 첨부파일 업로드
|
||||
FileInfo fileInfo = fileService.createOrUpdateSingleFile(
|
||||
oldAttachedFileId, attachedFile, attachedFile.getOriginalFilename());
|
||||
agreements.setAttachedFileId(fileInfo.getFileId());
|
||||
}
|
||||
|
||||
agreementsService.save(agreements);
|
||||
|
||||
// 미사용 이미지 정리 (저장 후 비동기적으로 처리)
|
||||
editorImageService.cleanupUnusedImages(usedFileIds, originalFileIds);
|
||||
}
|
||||
|
||||
public void delete(String agreementsType, Integer revision) {
|
||||
// 삭제 전 사용 중인 파일 ID 추출
|
||||
Agreements agreements = agreementsService.findById(agreementsType, revision);
|
||||
Set<String> fileIdsToDelete = editorImageService.extractFileIds(agreements.getContents());
|
||||
String attachedFileId = agreements.getAttachedFileId();
|
||||
|
||||
// 약관 삭제
|
||||
agreementsService.delete(agreementsType, revision);
|
||||
|
||||
// 관련 이미지 모두 삭제
|
||||
editorImageService.cleanupUnusedImages(new HashSet<>(), fileIdsToDelete);
|
||||
|
||||
// 첨부파일 삭제
|
||||
if (StringUtils.isNotBlank(attachedFileId)) {
|
||||
try {
|
||||
fileService.deleteFile(attachedFileId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("첨부파일 삭제 실패: {}", attachedFileId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,13 @@ public class PortalTermsUI {
|
||||
|
||||
private String searchAgreementsType;
|
||||
|
||||
/* 첨부파일 ID */
|
||||
private String attachedFileId;
|
||||
|
||||
/* 첨부파일명 (조회용) */
|
||||
private String attachedFileName;
|
||||
|
||||
/* 첨부파일 크기 (조회용) */
|
||||
private Long attachedFileSize;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user