- 약관 종류 관리에 '전체 동의' 옵션 추가
eapim-admin CI / build (push) Waiting to run

- '전체 동의' 설정 조회/저장 로직 및 화면 처리 반영
This commit is contained in:
Rinjae(gf63)
2026-09-15 13:42:17 +09:00
parent 507b8263e3
commit dfcfb0ac0b
3 changed files with 79 additions and 101 deletions
@@ -55,6 +55,7 @@
data: {cmd: 'TYPE_CONFIG'},
success: function(json) {
termsTypeRows = json.typeRows || [];
$("#agreeAllEnabled").prop('checked', json.agreeAllEnabled === true);
renderTermsTypeConfig();
},
error: function(e) {
@@ -134,7 +135,8 @@
$.ajax({
type: "POST",
url: url,
data: {cmd: 'TYPE_CONFIG_SAVE', enabledTypes: enabledTypes.join(','), displayTypes: displayTypes.join(',')},
data: {cmd: 'TYPE_CONFIG_SAVE', enabledTypes: enabledTypes.join(','), displayTypes: displayTypes.join(','),
agreeAllEnabled: $("#agreeAllEnabled").is(':checked')},
success: function() {
alert("저장되었습니다.");
// 목록 화면의 '(사용안함)' 표기가 바뀌므로 부모 화면을 다시 읽도록 알린다
@@ -188,6 +190,24 @@
</div>
</td>
</tr>
<tr>
<th style="width:180px;">회원가입 전체 동의</th>
<td>
<ul class="terms-toggle-list">
<li>
<span>'전체 동의' 사용</span>
<label class="terms-switch">
<input type="checkbox" id="agreeAllEnabled">
<span class="slider"></span>
</label>
</li>
</ul>
<div class="terms-help">
<b>사용</b> : 회원가입(개인/법인) 약관 동의에 '전체 동의' 체크박스를 노출하고, 항목별로 끝까지 스크롤하지 않아도 동의할 수 있습니다.<br>
<b>미사용</b> : '전체 동의' 체크박스를 노출하지 않고, 각 약관을 펼쳐 끝까지 읽어야 개별 동의할 수 있습니다.
</div>
</td>
</tr>
<tr>
<th>약관 페이지 노출</th>
<td>
@@ -79,24 +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());
resultMap.put("agreeAllEnabled", portalTermsTypeConfigService.isAgreeAllEnabled());
return ResponseEntity.ok(resultMap);
}
/**
* 약관 종류 관리 저장.
*
* @param enabledTypes 사용할 약관 종류 코드(쉼표 구분)
* @param displayTypes 약관 페이지에 배치한 약관 종류 코드(쉼표 구분, 기재 순서 = 노출 순서)
* @param enabledTypes 사용할 약관 종류 코드(쉼표 구분)
* @param displayTypes 약관 페이지에 배치한 약관 종류 코드(쉼표 구분, 기재 순서 = 노출 순서)
* @param agreeAllEnabled 회원가입 약관 동의에서 '전체 동의' 허용 여부
*/
@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);
@RequestParam(value = "displayTypes", required = false) String displayTypes,
@RequestParam(value = "agreeAllEnabled", defaultValue = "false") boolean agreeAllEnabled) {
portalTermsTypeConfigService.saveTypeConfig(enabledTypes, displayTypes, agreeAllEnabled);
return ResponseEntity.ok().build();
}
@@ -1,11 +1,11 @@
package com.eactive.eai.rms.onl.apim.portalterms;
import com.eactive.apim.portal.agreements.entity.AgreementType;
import com.eactive.apim.portal.agreements.service.AgreementTypeConfigService;
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;
@@ -15,7 +15,6 @@ 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;
@@ -25,68 +24,42 @@ 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>값의 의미와 프로퍼티 키는 {@link AgreementTypeConfigService} 참고 — 읽기는 그쪽에 위임하고
* (admin·portal 이 같은 해석을 쓰도록) 이 클래스는 관리 화면 표현과 저장만 담당한다.
*
* <p>PTL_PROPERTY 는 (PROPERTY_GROUP_NAME, PROPERTY_NAME) 에 유니크 제약이 없어 중복 행이 존재할 수 있다
* ({@code PortalPropertyDuplicateChecker} 참고). 따라서 단건 조회({@code findById})는 쓰지 않고
* 그룹 단위로 읽고 쓴다. 조회 시에는 값이 없어도 프로퍼티를 만들지 않으며(동시 요청이 각각 행을 만드는 것을 막는다)
* 기본값을 메모리에서만 사용하고, 실제 행은 관리 화면에서 저장할 때 생성된다.
* ({@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 String AGREE_ALL_ENABLED_DESC =
"회원가입 약관 동의에서 '전체 동의' 허용 여부(true/false). false 면 항목별로 끝까지 읽고 개별 동의";
/** 프로퍼티 미설정 시 기본값 — 현재 운영중인 약관 종류 */
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 AgreementTypeConfigService agreementTypeConfigService;
private final PortalPropertyGroupRepository portalPropertyGroupRepository;
/** 시스템에서 사용하는 약관 종류 */
public Set<AgreementType> getEnabledTypes() {
Set<AgreementType> enabled = EnumSet.noneOf(AgreementType.class);
enabled.addAll(readTypes(readGroupProperties(), ENABLED_TYPES_KEY));
return enabled;
return agreementTypeConfigService.getEnabledTypes();
}
/** 약관 페이지 노출 약관 종류(노출 순서대로) */
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());
/** 회원가입 약관 동의에서 '전체 동의' 허용 여부 */
public boolean isAgreeAllEnabled() {
return agreementTypeConfigService.isAgreeAllEnabled();
}
/** 약관 종류 관리 화면 행 목록 (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());
Set<AgreementType> enabled = agreementTypeConfigService.getEnabledTypes();
List<AgreementType> display = agreementTypeConfigService.getDisplayTypes();
return Arrays.stream(AgreementType.values())
.map(type -> {
@@ -104,10 +77,11 @@ public class PortalTermsTypeConfigService extends BaseService {
/**
* 약관 종류 설정 저장.
*
* @param enabledCodes 사용할 약관 종류 코드 목록(쉼표 구분)
* @param displayCodes 약관 페이지 노출 약관 종류 코드 목록(쉼표 구분, 기재 순서 = 노출 순서)
* @param enabledCodes 사용할 약관 종류 코드 목록(쉼표 구분)
* @param displayCodes 약관 페이지 노출 약관 종류 코드 목록(쉼표 구분, 기재 순서 = 노출 순서)
* @param agreeAllEnabled 회원가입 약관 동의에서 '전체 동의' 허용 여부
*/
public void saveTypeConfig(String enabledCodes, String displayCodes) {
public void saveTypeConfig(String enabledCodes, String displayCodes, boolean agreeAllEnabled) {
List<AgreementType> enabled = parseStrict(enabledCodes);
List<AgreementType> display = parseStrict(displayCodes);
@@ -120,35 +94,13 @@ public class PortalTermsTypeConfigService extends BaseService {
}
Map<String, String> values = new LinkedHashMap<>();
values.put(ENABLED_TYPES_KEY, join(enabled));
values.put(DISPLAY_TYPES_KEY, join(display));
values.put(AgreementTypeConfigService.ENABLED_TYPES_KEY, join(enabled));
values.put(AgreementTypeConfigService.DISPLAY_TYPES_KEY, join(display));
values.put(AgreementTypeConfigService.AGREE_ALL_ENABLED_KEY, String.valueOf(agreeAllEnabled));
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));
logger.info("약관 종류 설정 저장 - 사용: [{}], 노출: [{}], 전체 동의: {}",
join(enabled), join(display), agreeAllEnabled);
}
/**
@@ -158,11 +110,12 @@ public class PortalTermsTypeConfigService extends BaseService {
* (중복 행이 이미 있더라도 갱신 UPDATE 는 같은 키의 모든 행에 적용되므로 값이 어긋나지 않는다.)
*/
private void writeGroupProperties(Map<String, String> values) {
PortalPropertyGroup group = portalPropertyGroupRepository.findByIdWithProperties(PROPERTY_GROUP)
.orElseThrow(() -> new BizException("PTL_PROPERTY 프로퍼티 그룹이 없습니다. : " + PROPERTY_GROUP));
String group = AgreementTypeConfigService.PROPERTY_GROUP;
PortalPropertyGroup propertyGroup = portalPropertyGroupRepository.findByIdWithProperties(group)
.orElseThrow(() -> new BizException("PTL_PROPERTY 프로퍼티 그룹이 없습니다. : " + group));
values.forEach((propertyName, propertyValue) -> {
Optional<PortalProperty> existing = group.getPortalProperties().stream()
Optional<PortalProperty> existing = propertyGroup.getPortalProperties().stream()
.filter(property -> property.getId() != null
&& propertyName.equals(property.getId().getPropertyName()))
.findFirst();
@@ -175,44 +128,46 @@ public class PortalTermsTypeConfigService extends BaseService {
}
} else {
PortalProperty property = new PortalProperty();
property.setId(new PortalPropertyId(PROPERTY_GROUP, propertyName));
property.setId(new PortalPropertyId(group, propertyName));
property.setPropertyValue(propertyValue);
property.setPropertyDesc(describe(propertyName));
property.setPortalPropertyGroup(group);
group.getPortalProperties().add(property);
property.setPortalPropertyGroup(propertyGroup);
propertyGroup.getPortalProperties().add(property);
}
});
portalPropertyGroupRepository.save(group);
portalPropertyGroupRepository.save(propertyGroup);
}
private static String describe(String propertyName) {
return DISPLAY_TYPES_KEY.equals(propertyName) ? DISPLAY_TYPES_DESC : ENABLED_TYPES_DESC;
if (AgreementTypeConfigService.DISPLAY_TYPES_KEY.equals(propertyName)) {
return DISPLAY_TYPES_DESC;
}
if (AgreementTypeConfigService.AGREE_ALL_ENABLED_KEY.equals(propertyName)) {
return AGREE_ALL_ENABLED_DESC;
}
return 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);
if (StringUtils.isNotBlank(codes)) {
for (String code : codes.split(",")) {
String trimmed = code.trim();
if (trimmed.isEmpty()) {
continue;
}
try {
types.add(AgreementType.valueOf(trimmed));
} catch (IllegalArgumentException e) {
throw new BizException("알 수 없는 약관 종류입니다. : " + trimmed);
}
}
}
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(","));
}