- 약관 종류 관리 서비스 추가 (사용/노출 설정 로직 구현)

- 약관 종류 관리 화면 및 저장 처리 로직 추가
- 약관 조회/등록 시 사용 여부 검증 로직 반영
This commit is contained in:
Rinjae(gf63)
2026-09-15 10:32:41 +09:00
parent b8daa16c6a
commit dd3ac675ae
6 changed files with 535 additions and 16 deletions
@@ -14,6 +14,12 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style type="text/css">
/* '약관 종류 관리' 버튼 - 신규 버튼과 구분되도록 색상을 달리한다 */
.cssbtn.btn_terms_type { margin-right:14px; border-color:rgba(65,125,200,0.6); color:rgb(65,125,200);
background-color:rgba(65,125,200,0.06); box-shadow:0px 2px 3px rgba(65,125,200,0.15); }
.cssbtn.btn_terms_type:hover { color:rgb(65,125,200); background-color:rgba(65,125,200,0.2); }
</style>
<script language="javascript" >
var url = '<c:url value="/onl/apim/portalterms/portalTermsMan.json" />';
var url_view = '<c:url value="/onl/apim/portalterms/portalTermsMan.view" />';
@@ -44,6 +50,21 @@
});
}
/** 약관 종류 관리 팝업 (사용 여부 / 약관 페이지 노출 순서) */
function openTermsTypeManPopup() {
var popupUrl = url_view + '?cmd=TYPE_CONFIG_POPUP';
var args = {
title: '약관 종류 관리',
callback: function(saved) {
if (!saved) return;
// '(사용안함)' 표기가 바뀌므로 검색 콤보와 목록을 다시 읽는다
init();
$("#grid").trigger("reloadGrid");
}
};
showModal(popupUrl, args, 980, 640);
}
$(document).ready(function() {
init();
$('#grid').jqGrid({
@@ -141,6 +162,10 @@
}
});
$("#btn_terms_type").click(function(){
openTermsTypeManPopup();
});
buttonControl();
});
</script>
@@ -154,6 +179,7 @@
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn btn_terms_type" id="btn_terms_type" level="W"><i class="material-icons">tune</i> 약관 종류 관리</button>
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<%-- <button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>--%>
</div>
@@ -0,0 +1,210 @@
<%-- portalTermsTypeManPopup.jsp : 약관 종류 관리(사용 여부 / 약관 페이지 노출 순서) --%>
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style type="text/css">
/* 약관 종류 관리 영역 */
.terms-toggle-list { list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; }
.terms-toggle-list li { display:flex; align-items:center; justify-content:space-between;
width:280px; margin:0 20px 8px 0; padding:6px 10px; border:1px solid #e0e0e0; border-radius:3px; }
.terms-switch { position:relative; display:inline-block; width:40px; height:20px; flex:none; margin:0; }
.terms-switch input { opacity:0; width:0; height:0; }
.terms-switch .slider { position:absolute; top:0; left:0; right:0; bottom:0; cursor:pointer;
background-color:#c4c4c4; border-radius:20px; transition:background-color .2s; }
.terms-switch .slider:before { content:""; position:absolute; width:16px; height:16px; left:2px; top:2px;
background-color:#fff; border-radius:50%; transition:transform .2s; }
.terms-switch input:checked + .slider { background-color:#4BD763; }
.terms-switch input:checked + .slider:before { transform:translateX(20px); }
.terms-dnd-wrap { display:flex; gap:20px; }
.terms-dnd-col { flex:1; min-width:220px; }
.terms-dnd-head { font-weight:bold; padding:6px 0; }
.terms-dnd-sub { font-weight:normal; color:#888; font-size:12px; }
.terms-dnd-list { list-style:none; margin:0; padding:6px; min-height:140px;
border:1px solid #e0e0e0; border-radius:3px; background-color:#fafafa; }
.terms-dnd-list li { display:flex; align-items:center; margin-bottom:6px; padding:7px 10px; cursor:move;
background-color:#fff; border:1px solid #d5d5d5; border-radius:3px; }
.terms-dnd-list li .material-icons { font-size:16px; color:#999; margin-right:6px; }
.terms-dnd-list li.terms-dnd-placeholder { height:32px; background-color:#eef4ff; border:1px dashed #9db8e8; }
.terms-help { color:#666; font-size:12px; margin-top:8px; line-height:1.6; }
.terms-help code { font-family:Consolas,Menlo,monospace; color:#444; background-color:#f2f2f2;
padding:1px 4px; border-radius:3px; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/portalterms/portalTermsMan.json" />';
// 서버에서 내려준 약관 종류 행 (code / name / enabled / displayed / displayOrder)
var termsTypeRows = [];
function loadTermsTypeConfig() {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'TYPE_CONFIG'},
success: function(json) {
termsTypeRows = json.typeRows || [];
renderTermsTypeConfig();
},
error: function(e) {
alert(e.responseText);
}
});
}
function termsTypeItem(row) {
var $li = $('<li></li>').attr('data-code', row.code);
$li.append($('<i class="material-icons">drag_indicator</i>'));
$li.append($('<span></span>').text(row.name));
return $li;
}
function renderTermsTypeConfig() {
var $toggleList = $("#termsEnabledList").empty();
var $displayed = $("#termsDisplayedList").empty();
var $undisplayed = $("#termsUndisplayedList").empty();
// 사용 여부 토글 (약관 종류 선언 순서)
termsTypeRows.forEach(function(row) {
var $li = $('<li></li>').attr('data-code', row.code);
$li.append($('<span></span>').text(row.name));
var $label = $('<label class="terms-switch"></label>');
var $input = $('<input type="checkbox">').attr('data-code', row.code).prop('checked', row.enabled);
$label.append($input).append($('<span class="slider"></span>'));
$li.append($label);
$toggleList.append($li);
});
// 노출 배치 (배치 목록은 노출 순서대로, 사용 OFF 인 종류는 배치 대상에서 제외)
termsTypeRows.filter(function(row) { return row.enabled && row.displayed; })
.sort(function(a, b) { return a.displayOrder - b.displayOrder; })
.forEach(function(row) { $displayed.append(termsTypeItem(row)); });
termsTypeRows.filter(function(row) { return row.enabled && !row.displayed; })
.forEach(function(row) { $undisplayed.append(termsTypeItem(row)); });
$("#termsDisplayedList, #termsUndisplayedList").sortable({
connectWith: '.terms-dnd-list',
placeholder: 'terms-dnd-placeholder',
forcePlaceholderSize: true,
cursor: 'move',
tolerance: 'pointer'
}).disableSelection();
}
// 사용 토글 변경 시 배치 영역도 즉시 맞춘다 (OFF 면 제거, ON 이면 미배치에 추가)
function bindTermsTypeToggle() {
$("#termsEnabledList").on('change', 'input[type=checkbox]', function() {
var code = $(this).data('code');
var row = termsTypeRows.filter(function(r) { return r.code === code; })[0];
if (!row) return;
if ($(this).is(':checked')) {
if ($(".terms-dnd-list li[data-code='" + code + "']").length === 0) {
$("#termsUndisplayedList").append(termsTypeItem(row));
}
} else {
$(".terms-dnd-list li[data-code='" + code + "']").remove();
}
});
}
function saveTermsTypeConfig() {
var enabledTypes = [];
$("#termsEnabledList input[type=checkbox]:checked").each(function() {
enabledTypes.push($(this).data('code'));
});
var displayTypes = [];
$("#termsDisplayedList li").each(function() {
displayTypes.push($(this).data('code'));
});
$.ajax({
type: "POST",
url: url,
data: {cmd: 'TYPE_CONFIG_SAVE', enabledTypes: enabledTypes.join(','), displayTypes: displayTypes.join(',')},
success: function() {
alert("저장되었습니다.");
// 목록 화면의 '(사용안함)' 표기가 바뀌므로 부모 화면을 다시 읽도록 알린다
if (window.dialogArguments && window.dialogArguments.callback) {
window.dialogArguments.callback(true);
}
window.close();
},
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
loadTermsTypeConfig();
bindTermsTypeToggle();
$("#btn_save").click(function() {
saveTermsTypeConfig();
});
$("#btn_close").click(function() {
window.close();
});
buttonControl();
});
</script>
</head>
<body>
<div class="popup_box">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_save" level="W">
<i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %>
</button>
<button type="button" class="cssbtn" id="btn_close" level="R">
<i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
</button>
</div>
<div class="title">약관 종류 관리</div>
<table class="table_row" cellspacing="0">
<tr>
<th style="width:180px;">시스템 사용 약관 종류</th>
<td>
<ul id="termsEnabledList" class="terms-toggle-list"></ul>
<div class="terms-help">
사용하지 않는 약관 종류는 신규 등록 시 선택할 수 없으며, 목록/검색에 '(사용안함)'으로 표시됩니다.<br>
약관 종류 목록은 <code>com.eactive.apim.portal.agreements.entity.AgreementType</code>(elink-portal-common)에 정의되어 있으며,
종류 자체를 추가/삭제하려면 해당 enum을 수정한 뒤 재배포해야 합니다.
</div>
</td>
</tr>
<tr>
<th>약관 페이지 노출</th>
<td>
<div class="terms-dnd-wrap">
<div class="terms-dnd-col">
<div class="terms-dnd-head">배치 <span class="terms-dnd-sub">(위에서부터 노출 순서)</span></div>
<ul id="termsDisplayedList" class="terms-dnd-list"></ul>
</div>
<div class="terms-dnd-col">
<div class="terms-dnd-head">미배치</div>
<ul id="termsUndisplayedList" class="terms-dnd-list"></ul>
</div>
</div>
<div class="terms-help">드래그하여 배치/미배치로 옮기고, 배치 영역 안에서의 순서가 포털 약관 페이지의 노출 순서가 됩니다.</div>
</td>
</tr>
</table>
</div>
</body>
</html>
@@ -28,10 +28,13 @@ import java.util.stream.Collectors;
@Controller
public class PortalTermsManController extends BaseAnnotationController {
private final PortalTermsManService portalTermsManService;
private final PortalTermsTypeConfigService portalTermsTypeConfigService;
@Autowired
public PortalTermsManController(PortalTermsManService portalTermsManService){
public PortalTermsManController(PortalTermsManService portalTermsManService,
PortalTermsTypeConfigService portalTermsTypeConfigService){
this.portalTermsManService = portalTermsManService;
this.portalTermsTypeConfigService = portalTermsTypeConfigService;
}
@GetMapping(value = "/onl/apim/portalterms/portalTermsMan.view")
@@ -44,6 +47,12 @@ public class PortalTermsManController extends BaseAnnotationController {
return "/onl/apim/portalterms/portalTermsManDetail";
}
/** 약관 종류 관리 팝업 (사용 여부 / 약관 페이지 노출 순서) */
@GetMapping(value = "/onl/apim/portalterms/portalTermsMan.view", params = "cmd=TYPE_CONFIG_POPUP")
public String typeConfigPopupView() {
return "/onl/apim/portalterms/portalTermsTypeManPopup";
}
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<PortalTermsUI>> selectList(@SortDefault.SortDefaults({
@@ -56,11 +65,12 @@ public class PortalTermsManController extends BaseAnnotationController {
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=LIST_INIT_COMBO")
public ResponseEntity<Map<String, Object>> initCombo() {
Set<AgreementType> enabledTypes = portalTermsTypeConfigService.getEnabledTypes();
List<ComboVo> termsTypeList = Arrays.stream(AgreementType.values())
.map(type -> {
ComboVo vo = new ComboVo();
vo.setCode(type.name());
vo.setCodename(PortalTermsManService.displayLabel(type));
vo.setCodename(PortalTermsManService.displayLabel(type, enabledTypes));
return vo;
}).collect(Collectors.toList());
@@ -69,6 +79,27 @@ public class PortalTermsManController extends BaseAnnotationController {
return ResponseEntity.ok(resultMap);
}
/** 약관 종류 관리(사용 여부 / 약관 페이지 노출 순서) 조회 */
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=TYPE_CONFIG")
public ResponseEntity<Map<String, Object>> selectTypeConfig() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("typeRows", portalTermsTypeConfigService.loadTypeConfig());
return ResponseEntity.ok(resultMap);
}
/**
* 약관 종류 관리 저장.
*
* @param enabledTypes 사용할 약관 종류 코드(쉼표 구분)
* @param displayTypes 약관 페이지에 배치한 약관 종류 코드(쉼표 구분, 기재 순서 = 노출 순서)
*/
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=TYPE_CONFIG_SAVE")
public ResponseEntity<Void> saveTypeConfig(@RequestParam(value = "enabledTypes", required = false) String enabledTypes,
@RequestParam(value = "displayTypes", required = false) String displayTypes) {
portalTermsTypeConfigService.saveTypeConfig(enabledTypes, displayTypes);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalterms/portalTermsMan.json", params = "cmd=DETAIL")
public ResponseEntity<PortalTermsUI> selectDetail(String agreementsType, int revision) {
PortalTermsUI portalTermsUI = portalTermsManService.selectDetail(agreementsType, revision);
@@ -11,6 +11,7 @@ import com.eactive.apim.portal.user.entity.UserInfo;
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 com.eactive.eai.rms.onl.common.exception.BizException;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -21,7 +22,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
@@ -34,6 +34,7 @@ public class PortalTermsManService extends BaseService {
private final UserInfoService userInfoService;
private final EditorImageService editorImageService;
private final FileService fileService;
private final PortalTermsTypeConfigService portalTermsTypeConfigService;
@Autowired
public PortalTermsManService(PortalTermsService portalTermsService,
@@ -41,30 +42,28 @@ public class PortalTermsManService extends BaseService {
AgreementsService agreementsService,
UserInfoService userInfoService,
EditorImageService editorImageService,
FileService fileService){
FileService fileService,
PortalTermsTypeConfigService portalTermsTypeConfigService){
this.portalTermsService = portalTermsService;
this.portalTermsUIMapper = portalTermsUIMapper;
this.agreementsService = agreementsService;
this.userInfoService = userInfoService;
this.editorImageService = editorImageService;
this.fileService = fileService;
this.portalTermsTypeConfigService = portalTermsTypeConfigService;
}
/** 관리 화면에서 '(사용안함)'으로 표시할(=신규 등록에 더 이상 쓰지 않는) 약관 유형 */
private static final Set<AgreementType> DEPRECATED_AGREEMENT_TYPES = EnumSet.of(
AgreementType.PRIVACY_POLICY,
AgreementType.PRIVACY_COLLECT_IND,
AgreementType.PRIVACY_COLLECT_ORG);
/**
* 관리 화면(콤보/목록)에 표시할 약관 유형 라벨.
* 개인정보처리방침 및 개인용/법인용 개인정보수집동의서는 신규 통합 유형 {@link AgreementType#PRIVACY_COLLECT} 등으로
* 대체될 예정이므로 '(사용안함)'을 덧붙여 안내한다.
* 약관 목록 화면 하단의 '약관 종류 관리'에서 사용 OFF 한 유형은 '(사용안함)'을 덧붙여 안내한다
* (기존 데이터 조회/수정은 계속 가능해야 하므로 목록·콤보에서 감추지는 않는다).
* enum 자체의 description은 포털 사용자 화면(AgreementsController#agreementTitle)에서도 쓰이므로 변경하지 않는다.
*
* @param enabledTypes 시스템에서 사용하는 약관 유형 ({@link PortalTermsTypeConfigService#getEnabledTypes()})
*/
public static String displayLabel(AgreementType type) {
public static String displayLabel(AgreementType type, Set<AgreementType> enabledTypes) {
String label = type.getDescription();
if (DEPRECATED_AGREEMENT_TYPES.contains(type)) {
if (!enabledTypes.contains(type)) {
label += "(사용안함)";
}
return label;
@@ -82,12 +81,13 @@ public class PortalTermsManService extends BaseService {
}
public Page<PortalTermsUI> selectList(Pageable pageable, String searchAgreementsType) {
Set<AgreementType> enabledTypes = portalTermsTypeConfigService.getEnabledTypes();
return portalTermsService.findAll(pageable, searchAgreementsType)
.map(agreements -> {
PortalTermsUI portalTermsUI = portalTermsUIMapper.toVo(agreements);
portalTermsUI.setCreatedByName(findName(agreements.getCreatedBy()));
portalTermsUI.setLastModifiedByName(findName(agreements.getLastModifiedBy()));
portalTermsUI.setName(displayLabel(agreements.getAgreementsType()));
portalTermsUI.setName(displayLabel(agreements.getAgreementsType(), enabledTypes));
return portalTermsUI;
});
}
@@ -119,7 +119,13 @@ public class PortalTermsManService extends BaseService {
}
public void insert(PortalTermsUI portalTermsUI, MultipartFile attachedFile) throws IOException {
portalTermsUI.setName(AgreementType.fromCode(portalTermsUI.getAgreementsType()).getDescription());
AgreementType type = AgreementType.fromCode(portalTermsUI.getAgreementsType());
// 사용하지 않는 약관 종류로는 신규 등록할 수 없다(기존 데이터 수정은 허용).
if (!portalTermsTypeConfigService.getEnabledTypes().contains(type)) {
throw new BizException("사용하지 않는 약관 종류입니다. 약관 목록 화면의 '약관 종류 관리'에서 먼저 사용으로 변경해 주세요. : "
+ type.getDescription());
}
portalTermsUI.setName(type.getDescription());
portalTermsUI.setContents(StringEscapeUtils.unescapeHtml(portalTermsUI.getContents()));
Agreements agreements = portalTermsUIMapper.toEntity(portalTermsUI);
@@ -0,0 +1,219 @@
package com.eactive.eai.rms.onl.apim.portalterms;
import com.eactive.apim.portal.agreements.entity.AgreementType;
import com.eactive.apim.portal.portalproperty.entity.PortalProperty;
import com.eactive.apim.portal.portalproperty.entity.PortalPropertyGroup;
import com.eactive.apim.portal.portalproperty.entity.PortalPropertyId;
import com.eactive.apim.portal.portalproperty.repository.PortalPropertyGroupRepository;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.onl.common.exception.BizException;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 약관 종류 운영 설정. PTL_PROPERTY(그룹 {@code Portal})로 관리한다.
*
* <table>
* <caption>PTL_PROPERTY (group = Portal)</caption>
* <tr><th>{@code portal.terms.enabled-types}</th>
* <td>시스템에서 사용할 약관 종류. 쉼표 구분, 순서 무의미</td></tr>
* <tr><th>{@code portal.terms.display-types}</th>
* <td>약관 페이지에 노출할 약관 종류. 쉼표 구분, <b>기재 순서가 노출 순서</b></td></tr>
* </table>
*
* <p>노출(display) 목록은 항상 사용(enabled) 목록의 부분집합이어야 한다.
* 알 수 없는 코드는 읽을 때 무시한다(enum 항목이 제거돼도 화면이 깨지지 않도록).
*
* <p>PTL_PROPERTY 는 (PROPERTY_GROUP_NAME, PROPERTY_NAME) 에 유니크 제약이 없어 중복 행이 존재할 수 있다
* ({@code PortalPropertyDuplicateChecker} 참고). 따라서 단건 조회({@code findById})는 쓰지 않고
* 그룹 단위로 읽고 쓴다. 조회 시에는 값이 없어도 프로퍼티를 만들지 않으며(동시 요청이 각각 행을 만드는 것을 막는다)
* 기본값을 메모리에서만 사용하고, 실제 행은 관리 화면에서 저장할 때 생성된다.
*/
@Service
@RequiredArgsConstructor
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalTermsTypeConfigService extends BaseService {
static final String PROPERTY_GROUP = "Portal";
static final String ENABLED_TYPES_KEY = "portal.terms.enabled-types";
static final String DISPLAY_TYPES_KEY = "portal.terms.display-types";
private static final String ENABLED_TYPES_DESC = "시스템에서 사용할 약관 종류(쉼표 구분, AgreementType 코드)";
private static final String DISPLAY_TYPES_DESC = "포털 약관 페이지에 노출할 약관 종류(쉼표 구분, 기재 순서가 노출 순서)";
/** 프로퍼티 미설정 시 기본값 — 현재 운영중인 약관 종류 */
private static final List<AgreementType> DEFAULT_TYPES = Arrays.asList(
AgreementType.TERMS_OF_USE,
AgreementType.PRIVACY_COLLECT,
AgreementType.NOTIFICATION_CONSENT);
private final PortalPropertyService portalPropertyService;
private final PortalPropertyGroupRepository portalPropertyGroupRepository;
/** 시스템에서 사용하는 약관 종류 */
public Set<AgreementType> getEnabledTypes() {
Set<AgreementType> enabled = EnumSet.noneOf(AgreementType.class);
enabled.addAll(readTypes(readGroupProperties(), ENABLED_TYPES_KEY));
return enabled;
}
/** 약관 페이지 노출 약관 종류(노출 순서대로) */
public List<AgreementType> getDisplayTypes() {
Map<String, String> properties = readGroupProperties();
List<AgreementType> enabled = readTypes(properties, ENABLED_TYPES_KEY);
return readTypes(properties, DISPLAY_TYPES_KEY).stream()
.filter(enabled::contains)
.collect(Collectors.toList());
}
/** 약관 종류 관리 화면 행 목록 (AgreementType 선언 순서) */
public List<PortalTermsTypeUI> loadTypeConfig() {
Map<String, String> properties = readGroupProperties();
List<AgreementType> enabled = readTypes(properties, ENABLED_TYPES_KEY);
List<AgreementType> display = readTypes(properties, DISPLAY_TYPES_KEY).stream()
.filter(enabled::contains)
.collect(Collectors.toList());
return Arrays.stream(AgreementType.values())
.map(type -> {
PortalTermsTypeUI ui = new PortalTermsTypeUI();
ui.setCode(type.name());
ui.setName(type.getDescription());
ui.setEnabled(enabled.contains(type));
ui.setDisplayed(display.contains(type));
ui.setDisplayOrder(display.indexOf(type) + 1);
return ui;
})
.collect(Collectors.toList());
}
/**
* 약관 종류 설정 저장.
*
* @param enabledCodes 사용할 약관 종류 코드 목록(쉼표 구분)
* @param displayCodes 약관 페이지 노출 약관 종류 코드 목록(쉼표 구분, 기재 순서 = 노출 순서)
*/
public void saveTypeConfig(String enabledCodes, String displayCodes) {
List<AgreementType> enabled = parseStrict(enabledCodes);
List<AgreementType> display = parseStrict(displayCodes);
List<AgreementType> notEnabled = display.stream()
.filter(type -> !enabled.contains(type))
.collect(Collectors.toList());
if (!notEnabled.isEmpty()) {
throw new BizException("사용하지 않는 약관 종류는 약관 페이지에 배치할 수 없습니다. : "
+ notEnabled.stream().map(AgreementType::getDescription).collect(Collectors.joining(", ")));
}
Map<String, String> values = new LinkedHashMap<>();
values.put(ENABLED_TYPES_KEY, join(enabled));
values.put(DISPLAY_TYPES_KEY, join(display));
writeGroupProperties(values);
logger.info("약관 종류 설정 저장 - 사용: [{}], 노출: [{}]", join(enabled), join(display));
}
/** {@code Portal} 그룹 프로퍼티 전체를 읽는다(중복 행이 있어도 예외 없이 마지막 값을 쓴다). */
private Map<String, String> readGroupProperties() {
return portalPropertyService.getPortalPropertiesAsMap(PROPERTY_GROUP);
}
/** 프로퍼티 값을 약관 종류 목록으로 변환. 미설정이면 기본값, 알 수 없는 코드는 무시한다. */
private List<AgreementType> readTypes(Map<String, String> properties, String propertyName) {
String value = properties.get(propertyName);
if (value == null) {
value = join(DEFAULT_TYPES);
}
List<AgreementType> types = new ArrayList<>();
for (String code : splitCodes(value)) {
try {
types.add(AgreementType.valueOf(code));
} catch (IllegalArgumentException e) {
logger.warn("PTL_PROPERTY {}/{} 에 알 수 없는 약관 종류 코드가 있어 무시합니다. : {}",
PROPERTY_GROUP, propertyName, code);
}
}
return new ArrayList<>(new LinkedHashSet<>(types));
}
/**
* {@code Portal} 그룹에 프로퍼티를 기록한다.
*
* <p>단건 조회는 중복 행에서 터지므로 그룹의 프로퍼티 컬렉션에서 찾아 갱신하고, 없을 때만 새로 만든다.
* (중복 행이 이미 있더라도 갱신 UPDATE 는 같은 키의 모든 행에 적용되므로 값이 어긋나지 않는다.)
*/
private void writeGroupProperties(Map<String, String> values) {
PortalPropertyGroup group = portalPropertyGroupRepository.findByIdWithProperties(PROPERTY_GROUP)
.orElseThrow(() -> new BizException("PTL_PROPERTY 프로퍼티 그룹이 없습니다. : " + PROPERTY_GROUP));
values.forEach((propertyName, propertyValue) -> {
Optional<PortalProperty> existing = group.getPortalProperties().stream()
.filter(property -> property.getId() != null
&& propertyName.equals(property.getId().getPropertyName()))
.findFirst();
if (existing.isPresent()) {
PortalProperty property = existing.get();
property.setPropertyValue(propertyValue);
if (StringUtils.isBlank(property.getPropertyDesc())) {
property.setPropertyDesc(describe(propertyName));
}
} else {
PortalProperty property = new PortalProperty();
property.setId(new PortalPropertyId(PROPERTY_GROUP, propertyName));
property.setPropertyValue(propertyValue);
property.setPropertyDesc(describe(propertyName));
property.setPortalPropertyGroup(group);
group.getPortalProperties().add(property);
}
});
portalPropertyGroupRepository.save(group);
}
private static String describe(String propertyName) {
return DISPLAY_TYPES_KEY.equals(propertyName) ? DISPLAY_TYPES_DESC : ENABLED_TYPES_DESC;
}
/** 화면에서 넘어온 코드 목록 파싱. 알 수 없는 코드는 오류로 처리한다. */
private List<AgreementType> parseStrict(String codes) {
List<AgreementType> types = new ArrayList<>();
for (String code : splitCodes(codes)) {
try {
types.add(AgreementType.valueOf(code));
} catch (IllegalArgumentException e) {
throw new BizException("알 수 없는 약관 종류입니다. : " + code);
}
}
return new ArrayList<>(new LinkedHashSet<>(types));
}
private static List<String> splitCodes(String value) {
if (StringUtils.isBlank(value)) {
return new ArrayList<>();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}
private static String join(List<AgreementType> types) {
return types.stream().map(AgreementType::name).collect(Collectors.joining(","));
}
}
@@ -0,0 +1,27 @@
package com.eactive.eai.rms.onl.apim.portalterms;
import lombok.Data;
/**
* 약관 종류 관리 화면(약관 목록 하단)의 한 행.
*
* @see PortalTermsTypeConfigService
*/
@Data
public class PortalTermsTypeUI {
/** 약관 종류 코드 (AgreementType 이름) */
private String code;
/** 약관명 (AgreementType description) */
private String name;
/** 시스템에서 사용하는 약관 종류 여부 (토글) */
private boolean enabled;
/** 약관 페이지 노출(배치) 여부 */
private boolean displayed;
/** 약관 페이지 노출 순서. 1부터 시작하며 미배치는 0 */
private int displayOrder;
}