Add attachment support to agreements and enhance image handling
This commit is contained in:
@@ -35,6 +35,12 @@ public class AgreementsDTO {
|
||||
|
||||
private LocalDateTime updatedDate;
|
||||
|
||||
/* 첨부파일 ID */
|
||||
private String attachedFileId;
|
||||
|
||||
/* 첨부파일명 (확장자 포함) */
|
||||
private String attachedFileName;
|
||||
|
||||
// 기본 생성자 추가
|
||||
public AgreementsDTO() {
|
||||
this.contents = ""; // null 방지를 위한 기본값 설정
|
||||
|
||||
+86
-12
@@ -7,30 +7,48 @@ import com.eactive.apim.portal.apps.agreements.dto.AgreementsDTO;
|
||||
import com.eactive.apim.portal.apps.agreements.mapper.AgreementsMapper;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserPrivacyAgreementService;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.file.entity.FileDetail;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
|
||||
// 플레이스홀더 패턴: {{IMG:fileId:fileSn}} 또는 {{IMG:fileId:fileSn|속성}}
|
||||
private static final Pattern PLACEHOLDER_PATTERN =
|
||||
Pattern.compile("\\{\\{IMG:([a-f0-9\\-]+):(\\d+)(\\|[^}]+)?\\}\\}");
|
||||
|
||||
private final AgreementsService agreementsService;
|
||||
private final AgreementsMapper agreementsMapper;
|
||||
private final PortalUserPrivacyAgreementService userAgreementService;
|
||||
private final FileService fileService;
|
||||
|
||||
@Value("${editor.image.base-url:/file/view}")
|
||||
private String editorImageBaseUrl;
|
||||
|
||||
@Autowired
|
||||
public AgreementsFacadeImpl(AgreementsService agreementsService,
|
||||
AgreementsMapper agreementsMapper,
|
||||
PortalUserPrivacyAgreementService userAgreementService) {
|
||||
PortalUserPrivacyAgreementService userAgreementService,
|
||||
FileService fileService) {
|
||||
this.agreementsService = agreementsService;
|
||||
this.agreementsMapper = agreementsMapper;
|
||||
this.userAgreementService = userAgreementService;
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,15 +89,79 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
}
|
||||
}
|
||||
|
||||
// HTML 언이스케이프 처리
|
||||
// HTML 언이스케이프 및 이미지 플레이스홀더 변환 처리
|
||||
private AgreementsDTO convertToDTO(Agreements agreement) {
|
||||
AgreementsDTO dto = agreementsMapper.map(agreement);
|
||||
if (dto.getContents() != null) {
|
||||
dto.setContents(StringEscapeUtils.unescapeHtml4(dto.getContents()));
|
||||
String contents = StringEscapeUtils.unescapeHtml4(dto.getContents());
|
||||
// 이미지 플레이스홀더를 HTML img 태그로 변환
|
||||
contents = convertPlaceholdersToHtml(contents);
|
||||
dto.setContents(contents);
|
||||
}
|
||||
// 첨부파일 정보 설정
|
||||
if (StringUtils.isNotBlank(agreement.getAttachedFileId())) {
|
||||
dto.setAttachedFileId(agreement.getAttachedFileId());
|
||||
try {
|
||||
FileDetail fileDetail = fileService.getFileDetailByIds(agreement.getAttachedFileId(), 1);
|
||||
String fileName = fileDetail.getOriginalFileName();
|
||||
if (StringUtils.isNotBlank(fileDetail.getFileExtension())) {
|
||||
fileName += "." + fileDetail.getFileExtension();
|
||||
}
|
||||
dto.setAttachedFileName(fileName);
|
||||
} catch (Exception e) {
|
||||
log.warn("첨부파일 정보 조회 실패: fileId={}", agreement.getAttachedFileId(), e);
|
||||
}
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플레이스홀더를 실제 이미지 태그로 변환
|
||||
* {{IMG:fileId:fileSn}} 또는 {{IMG:fileId:fileSn|width=300|height=200|style=...}}
|
||||
* → <img src="/file/view?fileId={fileId}&fileSn={fileSn}" width="300" height="200" style="..." />
|
||||
*/
|
||||
private String convertPlaceholdersToHtml(String contents) {
|
||||
if (contents == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(contents);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String fileId = matcher.group(1);
|
||||
String fileSn = matcher.group(2);
|
||||
String attrs = matcher.group(3);
|
||||
|
||||
StringBuilder img = new StringBuilder();
|
||||
img.append("<img src=\"").append(editorImageBaseUrl)
|
||||
.append("?fileId=").append(fileId)
|
||||
.append("&fileSn=").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(sb, Matcher.quoteReplacement(img.toString()));
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgreementsDTO getAgreement(String agreementsTypeCode) {
|
||||
LocalDateTime date = LocalDateTime.now();
|
||||
@@ -142,15 +224,7 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
return Collections.singletonList(createDefaultAgreement(String.valueOf(agreementTypeCode)));
|
||||
}
|
||||
return agreementsList.stream()
|
||||
.map(agreement -> {
|
||||
AgreementsDTO dto = agreementsMapper.map(agreement);
|
||||
|
||||
// HTML 언이스케이프 처리
|
||||
if (dto.getContents() != null) {
|
||||
dto.setContents(StringEscapeUtils.unescapeHtml4(dto.getContents()));
|
||||
}
|
||||
return dto;
|
||||
})
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
+50
@@ -39,4 +39,54 @@ public class FileDownloadController {
|
||||
IOUtils.write(fileContents, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 인라인 표시용 (브라우저에서 직접 렌더링)
|
||||
* 약관 등에서 이미지를 표시할 때 사용
|
||||
*/
|
||||
@GetMapping(value = "/view")
|
||||
public void viewImage(@RequestParam(value = "fileId") String fileId,
|
||||
@RequestParam(value = "fileSn", defaultValue = "1") int fileSn,
|
||||
HttpServletResponse response) throws IOException {
|
||||
|
||||
FileDetail fileDetail = fileService.getFileDetailByIds(fileId, fileSn);
|
||||
|
||||
// Content-Type 설정 (이미지용)
|
||||
String contentType = getImageContentType(fileDetail.getFileExtension());
|
||||
response.setContentType(contentType);
|
||||
response.setContentLength(fileDetail.getFileSize());
|
||||
|
||||
// inline으로 설정 (브라우저에서 직접 표시)
|
||||
response.setHeader("Content-Disposition", "inline");
|
||||
|
||||
// 캐시 설정 (1일)
|
||||
response.setHeader("Cache-Control", "public, max-age=86400");
|
||||
|
||||
byte[] fileContents = fileService.retrieveFileContent(fileDetail);
|
||||
IOUtils.write(fileContents, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
private String getImageContentType(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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,13 @@ page:
|
||||
corp:
|
||||
name: "법인회원가입"
|
||||
path: "/signup/portalOrg"
|
||||
agreements:
|
||||
name: 약관
|
||||
path: "#"
|
||||
children:
|
||||
terms:
|
||||
name: "이용약관 및 개인정보처리방침"
|
||||
path: "/agreements/terms"
|
||||
about:
|
||||
name: 안내
|
||||
path: "#"
|
||||
@@ -311,3 +318,8 @@ page:
|
||||
api_statistics:
|
||||
name: "이용 통계"
|
||||
path: "/statistics/api"
|
||||
|
||||
# 에디터 이미지 설정 (약관 이미지 표시용)
|
||||
editor:
|
||||
image:
|
||||
base-url: /file/view
|
||||
|
||||
@@ -17506,6 +17506,32 @@ body.commission-print-page .btn-primary:hover {
|
||||
|
||||
.terms-content {
|
||||
padding: 0 32px;
|
||||
}
|
||||
.terms-content .terms-attachment {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.terms-content .terms-attachment .attachment-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #0049b4;
|
||||
border-radius: 6px;
|
||||
color: #0049b4;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.terms-content .terms-attachment .attachment-link:hover {
|
||||
background: #0049b4;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.terms-content .terms-attachment .attachment-link svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.terms-content {
|
||||
min-height: 400px;
|
||||
background-color: #F6F9FB;
|
||||
}
|
||||
@@ -17516,6 +17542,8 @@ body.commission-print-page .btn-primary:hover {
|
||||
}
|
||||
}
|
||||
.terms-content .content-body {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
font-size: 16px;
|
||||
color: #1A1A2E;
|
||||
line-height: 1.8;
|
||||
@@ -17523,6 +17551,8 @@ body.commission-print-page .btn-primary:hover {
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.terms-content .content-body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -190,6 +190,35 @@
|
||||
// Terms Content (Figma: 컨텐츠 영역)
|
||||
.terms-content {
|
||||
padding: 0 $spacing-xl;
|
||||
|
||||
// Attachment Download (본문 영역 내)
|
||||
.terms-attachment {
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.attachment-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: 10px 20px;
|
||||
background: $white;
|
||||
border: 1px solid $primary-blue;
|
||||
border-radius: $border-radius-sm;
|
||||
color: $primary-blue;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
text-decoration: none;
|
||||
transition: $transition-base;
|
||||
|
||||
&:hover {
|
||||
background: $primary-blue;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
min-height: 400px;
|
||||
background-color: #F6F9FB;
|
||||
|
||||
@@ -199,12 +228,16 @@
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding-top: $spacing-lg;
|
||||
padding-bottom: $spacing-lg;
|
||||
font-size: 16px;
|
||||
color: $text-dark;
|
||||
line-height: 1.8;
|
||||
word-wrap: break-word;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
padding-top: $spacing-md;
|
||||
padding-bottom: $spacing-md;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,18 @@
|
||||
|
||||
<!-- Terms Content -->
|
||||
<div class="terms-content">
|
||||
<!-- Attachment Download -->
|
||||
<div class="terms-attachment" th:if="${selectedAgreement.attachedFileId != null}">
|
||||
<a th:href="@{/file/download(fileId=${selectedAgreement.attachedFileId}, fileSn=1)}"
|
||||
class="attachment-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
<span th:text="${selectedAgreement.attachedFileName} ?: '첨부파일 다운로드'">첨부파일 다운로드</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="content-body editor-content" th:utext="${selectedAgreement.contents}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user