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:
Rinjae
2026-01-07 16:59:32 +09:00
14 changed files with 1524 additions and 268 deletions
@@ -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 태그가 필요하므로 &lt; &gt; 변환을 하지 않음
*/
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";
}
}
}
@@ -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;
}