푸터 "관련 사이트" 조회 및 렌더링 기능 추가:
- RelatedSite/RelatedSiteService 생성 및 PortalProperty 연계 - GlobalControllerAdvice에서 셀렉트 라벨/목록 주입 - footer.html 템플릿 및 관련 JS 수정
This commit is contained in:
@@ -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<RelatedSite> relatedSites() {
|
||||
return relatedSiteService.getSites();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.eactive.apim.portal.djb.footer;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 푸터 "관련 사이트" 셀렉트에 노출되는 사이트 한 건.
|
||||
*
|
||||
* <p>{@link RelatedSiteService} 가 PortalProperty 문자열을 파싱해 만든다.</p>
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public class RelatedSite {
|
||||
|
||||
/** 셀렉트에 표시되는 이름 (예: 신한은행) */
|
||||
private final String name;
|
||||
|
||||
/** 이동 대상 URL (http/https 절대주소 또는 `/` 로 시작하는 사이트 상대주소) */
|
||||
private final String url;
|
||||
}
|
||||
@@ -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)에서 조회한다.
|
||||
*
|
||||
* <p>group 은 기존 {@code Portal} 을 재사용하여 {@link PortalPropertyService#getOrCreateProperty}
|
||||
* 의 자동 생성(self-seed)이 동작하도록 한다.</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code footer.related-sites.label} - 셀렉트 첫 항목(플레이스홀더) 문구</li>
|
||||
* <li>{@code footer.related-sites} - 사이트 목록. 한 줄에 하나씩 {@code 이름=URL}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>목록 표기 규칙</h3>
|
||||
* <pre>
|
||||
* # 주석 (# 으로 시작하는 줄은 무시)
|
||||
* 신한은행=http://www.shinhan.com
|
||||
* 제주은행 = https://www.jejubank.co.kr ← = 앞뒤 공백 허용
|
||||
* </pre>
|
||||
* <ul>
|
||||
* <li>빈 줄 · {@code #} 로 시작하는 줄은 건너뛴다.</li>
|
||||
* <li>줄 순서가 곧 화면 노출 순서다.</li>
|
||||
* <li>URL 에 {@code =} 가 들어가도 <b>첫 번째</b> {@code =} 만 구분자로 쓰므로 안전하다.</li>
|
||||
* <li>{@code =} 가 없거나 이름/URL 이 비었거나 허용되지 않은 스킴이면 그 줄만 버리고 WARN 로그를
|
||||
* 남긴다. (한 줄이 잘못돼도 나머지 사이트는 정상 노출)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>허용 스킴을 {@code http/https} 와 사이트 상대경로로 제한하는 이유는, 이 값이 관리자 화면에서
|
||||
* 편집되어 그대로 앵커/스크립트 이동 대상이 되기 때문이다({@code javascript:} 등 차단).</p>
|
||||
*/
|
||||
@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<RelatedSite> getSites() {
|
||||
return parse(portalPropertyService.getOrCreateProperty(GROUP, NAME_SITES, DEFAULT_SITES, DESC_SITES));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code 이름=URL} 줄 목록을 파싱한다. 잘못된 줄은 건너뛴다.
|
||||
*/
|
||||
static List<RelatedSite> parse(String raw) {
|
||||
if (raw == null || raw.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<RelatedSite> 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("/");
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,31 @@
|
||||
<p class="footer-copyright">Copyright © 2026 JEJU Bank. All Rights Reserved.</p>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<div class="footer-related-sites">
|
||||
<select class="related-sites-select">
|
||||
<option>DJBank 관련 사이트</option>
|
||||
<option>DJBank 홈페이지</option>
|
||||
<option>DJBank 인터넷뱅킹</option>
|
||||
<option>DJBank 모바일뱅킹</option>
|
||||
<!-- 관련 사이트: PortalProperty(Portal/footer.related-sites[.label])로 제어. 목록이 비면 미노출 -->
|
||||
<div class="footer-related-sites" th:if="${!#lists.isEmpty(relatedSites)}">
|
||||
<select class="related-sites-select" data-related-sites th:aria-label="${relatedSitesLabel}">
|
||||
<option value="" th:text="${relatedSitesLabel}">DJBank 관련 사이트</option>
|
||||
<option th:each="site : ${relatedSites}" th:value="${site.url}" th:text="${site.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="footer-contact" th:text="'고객센터 ' + ${customerCenterContact}">고객센터 1588-3388</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:inline="none">
|
||||
// 관련 사이트 셀렉트: 선택 시 새 창으로 이동 후 라벨(첫 항목)로 되돌린다.
|
||||
(function () {
|
||||
document.addEventListener('change', function (e) {
|
||||
var el = e.target;
|
||||
if (!el || !el.hasAttribute || !el.hasAttribute('data-related-sites')) return;
|
||||
var url = el.value;
|
||||
el.selectedIndex = 0;
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user