diff --git a/WebContent/jsp/onl/apim/portalterms/portalTermsMan.jsp b/WebContent/jsp/onl/apim/portalterms/portalTermsMan.jsp index 09faeab..066b07f 100644 --- a/WebContent/jsp/onl/apim/portalterms/portalTermsMan.jsp +++ b/WebContent/jsp/onl/apim/portalterms/portalTermsMan.jsp @@ -14,6 +14,12 @@ + @@ -154,6 +179,7 @@
+ <%-- --%>
diff --git a/WebContent/jsp/onl/apim/portalterms/portalTermsTypeManPopup.jsp b/WebContent/jsp/onl/apim/portalterms/portalTermsTypeManPopup.jsp new file mode 100644 index 0000000..b80f855 --- /dev/null +++ b/WebContent/jsp/onl/apim/portalterms/portalTermsTypeManPopup.jsp @@ -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"); +%> + + + + + + + + + + + + + diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManController.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManController.java index 418bde8..bba5fcd 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManController.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManController.java @@ -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> 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> initCombo() { + Set enabledTypes = portalTermsTypeConfigService.getEnabledTypes(); List 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> selectTypeConfig() { + Map 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 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 selectDetail(String agreementsType, int revision) { PortalTermsUI portalTermsUI = portalTermsManService.selectDetail(agreementsType, revision); diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManService.java index c9d6696..ae26fd6 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManService.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsManService.java @@ -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 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 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 selectList(Pageable pageable, String searchAgreementsType) { + Set 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); diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeConfigService.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeConfigService.java new file mode 100644 index 0000000..336635f --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeConfigService.java @@ -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})로 관리한다. + * + * + * + * + * + * + * + *
PTL_PROPERTY (group = Portal)
{@code portal.terms.enabled-types}시스템에서 사용할 약관 종류. 쉼표 구분, 순서 무의미
{@code portal.terms.display-types}약관 페이지에 노출할 약관 종류. 쉼표 구분, 기재 순서가 노출 순서
+ * + *

노출(display) 목록은 항상 사용(enabled) 목록의 부분집합이어야 한다. + * 알 수 없는 코드는 읽을 때 무시한다(enum 항목이 제거돼도 화면이 깨지지 않도록). + * + *

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 DEFAULT_TYPES = Arrays.asList( + AgreementType.TERMS_OF_USE, + AgreementType.PRIVACY_COLLECT, + AgreementType.NOTIFICATION_CONSENT); + + private final PortalPropertyService portalPropertyService; + private final PortalPropertyGroupRepository portalPropertyGroupRepository; + + /** 시스템에서 사용하는 약관 종류 */ + public Set getEnabledTypes() { + Set enabled = EnumSet.noneOf(AgreementType.class); + enabled.addAll(readTypes(readGroupProperties(), ENABLED_TYPES_KEY)); + return enabled; + } + + /** 약관 페이지 노출 약관 종류(노출 순서대로) */ + public List getDisplayTypes() { + Map properties = readGroupProperties(); + List enabled = readTypes(properties, ENABLED_TYPES_KEY); + return readTypes(properties, DISPLAY_TYPES_KEY).stream() + .filter(enabled::contains) + .collect(Collectors.toList()); + } + + /** 약관 종류 관리 화면 행 목록 (AgreementType 선언 순서) */ + public List loadTypeConfig() { + Map properties = readGroupProperties(); + List enabled = readTypes(properties, ENABLED_TYPES_KEY); + List 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 enabled = parseStrict(enabledCodes); + List display = parseStrict(displayCodes); + + List 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 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 readGroupProperties() { + return portalPropertyService.getPortalPropertiesAsMap(PROPERTY_GROUP); + } + + /** 프로퍼티 값을 약관 종류 목록으로 변환. 미설정이면 기본값, 알 수 없는 코드는 무시한다. */ + private List readTypes(Map properties, String propertyName) { + String value = properties.get(propertyName); + if (value == null) { + value = join(DEFAULT_TYPES); + } + + List 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} 그룹에 프로퍼티를 기록한다. + * + *

단건 조회는 중복 행에서 터지므로 그룹의 프로퍼티 컬렉션에서 찾아 갱신하고, 없을 때만 새로 만든다. + * (중복 행이 이미 있더라도 갱신 UPDATE 는 같은 키의 모든 행에 적용되므로 값이 어긋나지 않는다.) + */ + private void writeGroupProperties(Map values) { + PortalPropertyGroup group = portalPropertyGroupRepository.findByIdWithProperties(PROPERTY_GROUP) + .orElseThrow(() -> new BizException("PTL_PROPERTY 프로퍼티 그룹이 없습니다. : " + PROPERTY_GROUP)); + + values.forEach((propertyName, propertyValue) -> { + Optional 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 parseStrict(String codes) { + List 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 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 types) { + return types.stream().map(AgreementType::name).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeUI.java new file mode 100644 index 0000000..8acf96f --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalterms/PortalTermsTypeUI.java @@ -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; +}