diff --git a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java index 933992b..97d1b63 100644 --- a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java +++ b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java @@ -12,6 +12,8 @@ import com.eactive.apim.portal.config.PortalProperties; import com.eactive.apim.portal.apps.auth.AuthNoticeProperties; import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.common.security.ClientGuardService; +import com.eactive.apim.portal.djb.footer.RelatedSite; +import com.eactive.apim.portal.djb.footer.RelatedSiteService; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; @ControllerAdvice @@ -35,6 +37,9 @@ public class GlobalControllerAdvice { @Autowired private AuthNoticeProperties authNoticeProperties; + @Autowired + private RelatedSiteService relatedSiteService; + @Autowired private Environment environment; @@ -111,4 +116,21 @@ public class GlobalControllerAdvice { return portalPropertyService.getOrCreateProperty( "Portal", "customer.center.contact", "1588-3388", "고객센터 연락처"); } + + /** + * 푸터 관련 사이트 셀렉트 라벨. PortalProperty(Portal/footer.related-sites.label)에서 조회. + */ + @ModelAttribute("relatedSitesLabel") + public String relatedSitesLabel() { + return relatedSiteService.getLabel(); + } + + /** + * 푸터 관련 사이트 목록. PortalProperty(Portal/footer.related-sites)의 '이름=URL' 줄 목록을 파싱한 결과. + * 비어 있으면 푸터에서 셀렉트 자체를 렌더하지 않는다. + */ + @ModelAttribute("relatedSites") + public List relatedSites() { + return relatedSiteService.getSites(); + } } diff --git a/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java b/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java new file mode 100644 index 0000000..e29f705 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java @@ -0,0 +1,22 @@ +package com.eactive.apim.portal.djb.footer; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * 푸터 "관련 사이트" 셀렉트에 노출되는 사이트 한 건. + * + *

{@link RelatedSiteService} 가 PortalProperty 문자열을 파싱해 만든다.

+ */ +@Getter +@ToString +@AllArgsConstructor +public class RelatedSite { + + /** 셀렉트에 표시되는 이름 (예: 신한은행) */ + private final String name; + + /** 이동 대상 URL (http/https 절대주소 또는 `/` 로 시작하는 사이트 상대주소) */ + private final String url; +} diff --git a/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java b/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java new file mode 100644 index 0000000..b08495f --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java @@ -0,0 +1,131 @@ +package com.eactive.apim.portal.djb.footer; + +import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 푸터 "관련 사이트" 셀렉트의 라벨과 목록을 DB(PortalProperty)에서 조회한다. + * + *

group 은 기존 {@code Portal} 을 재사용하여 {@link PortalPropertyService#getOrCreateProperty} + * 의 자동 생성(self-seed)이 동작하도록 한다.

+ * + * + * + *

목록 표기 규칙

+ *
+ * # 주석 (# 으로 시작하는 줄은 무시)
+ * 신한은행=http://www.shinhan.com
+ * 제주은행 = https://www.jejubank.co.kr   ← = 앞뒤 공백 허용
+ * 
+ * + * + *

허용 스킴을 {@code http/https} 와 사이트 상대경로로 제한하는 이유는, 이 값이 관리자 화면에서 + * 편집되어 그대로 앵커/스크립트 이동 대상이 되기 때문이다({@code javascript:} 등 차단).

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RelatedSiteService { + + private static final String GROUP = "Portal"; + + static final String NAME_LABEL = "footer.related-sites.label"; + static final String NAME_SITES = "footer.related-sites"; + + static final String DESC_LABEL = "푸터 관련 사이트 셀렉트 라벨(첫 항목 문구)"; + static final String DESC_SITES = "푸터 관련 사이트 목록. 한 줄에 하나씩 '이름=URL' (빈 줄·# 주석 무시)"; + + static final String DEFAULT_LABEL = "DJBank 관련 사이트"; + + /** 기본값: 신한금융그룹 Family site (shinhangroup.com 하단 목록 기준) */ + static final String DEFAULT_SITES = String.join("\n", + "신한은행=http://www.shinhan.com", + "신한카드=http://www.shinhancard.com", + "신한투자증권=http://www.shinhansec.com", + "신한라이프=http://www.shinhanlife.co.kr", + "신한캐피탈=http://www.shcap.co.kr", + "신한자산운용=https://www.shinhanfund.com", + "제주은행=https://www.jejubank.co.kr", + "신한저축은행=http://www.shinhansavings.co.kr", + "신한자산신탁=http://www.shinhantrust.kr", + "신한DS=http://www.shinhansys.co.kr", + "신한펀드파트너스=https://www.shinhanfundpartners.com", + "신한리츠운용=http://shinhanrem.com", + "신한벤처투자=http://www.shinhanvc.com", + "신한EZ손해보험=http://www.shinhanez.co.kr", + "신한장학재단=http://www.shsf.or.kr", + "신한금융희망재단=http://www.shinhanfoundation.or.kr"); + + private final PortalPropertyService portalPropertyService; + + /** 셀렉트 첫 항목에 노출할 라벨 */ + @Transactional + public String getLabel() { + String label = portalPropertyService.getOrCreateProperty(GROUP, NAME_LABEL, DEFAULT_LABEL, DESC_LABEL); + return (label == null || label.trim().isEmpty()) ? DEFAULT_LABEL : label.trim(); + } + + /** 셀렉트에 노출할 사이트 목록. 파싱 결과가 없으면 빈 리스트(푸터에서 셀렉트 미노출) */ + @Transactional + public List getSites() { + return parse(portalPropertyService.getOrCreateProperty(GROUP, NAME_SITES, DEFAULT_SITES, DESC_SITES)); + } + + /** + * {@code 이름=URL} 줄 목록을 파싱한다. 잘못된 줄은 건너뛴다. + */ + static List parse(String raw) { + if (raw == null || raw.trim().isEmpty()) { + return Collections.emptyList(); + } + + List sites = new ArrayList<>(); + for (String rawLine : raw.split("\\r?\\n")) { + String line = rawLine.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + int sep = line.indexOf('='); + if (sep <= 0) { + log.warn("관련 사이트 설정 형식 오류(= 구분자 없음) - 해당 줄 무시: {}", line); + continue; + } + + String name = line.substring(0, sep).trim(); + String url = line.substring(sep + 1).trim(); + if (name.isEmpty() || url.isEmpty()) { + log.warn("관련 사이트 설정 형식 오류(이름 또는 URL 없음) - 해당 줄 무시: {}", line); + continue; + } + if (!isAllowedUrl(url)) { + log.warn("관련 사이트 설정 URL 스킴 불허(http/https 또는 / 로 시작해야 함) - 해당 줄 무시: {}", line); + continue; + } + + sites.add(new RelatedSite(name, url)); + } + return sites; + } + + private static boolean isAllowedUrl(String url) { + String lower = url.toLowerCase(); + return lower.startsWith("http://") || lower.startsWith("https://") || url.startsWith("/"); + } +} diff --git a/src/main/resources/templates/views/fragment/djbank/footer.html b/src/main/resources/templates/views/fragment/djbank/footer.html index 62cdd33..6eefe23 100644 --- a/src/main/resources/templates/views/fragment/djbank/footer.html +++ b/src/main/resources/templates/views/fragment/djbank/footer.html @@ -24,18 +24,31 @@ + +