You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
672 lines
28 KiB
JavaScript
672 lines
28 KiB
JavaScript
/* =========================================================================
|
|
* XIT Common JavaScript Library
|
|
* 중요도 순서: 1.환경설정 → 2.세션/보안 → 3.Ajax통신 → 4.공통유틸 → 5.UI컴포넌트
|
|
* ========================================================================= */
|
|
|
|
/* =============================================================================
|
|
* 1. ENVIRONMENT CONFIGURATION (최우선 - 시스템 기본 설정)
|
|
* ============================================================================= */
|
|
|
|
/* 컨택스트 패스 설정 로드 */
|
|
// 기본값 설정 (설정 로드 실패 시 사용)
|
|
var contextPath = '';
|
|
|
|
// 현재 스크립트 경로에서 컨택스트 패스 추출 (임시 방법)
|
|
(function() {
|
|
var scripts = document.getElementsByTagName('script');
|
|
var currentScript = scripts[scripts.length - 1];
|
|
var src = currentScript.src;
|
|
|
|
if (src) {
|
|
// 스크립트 경로에서 컨택스트 패스 추출 (예: http://localhost:8080/context/resources/xit/xit-common.js)
|
|
var match = src.match(/^https?:\/\/[^\/]+([^\/]*?)\/resources\//);
|
|
if (match && match[1]) {
|
|
contextPath = match[1];
|
|
}
|
|
}
|
|
})();
|
|
|
|
// 서버에서 정확한 컨택스트 패스 로드 (위의 추출된 값으로 URL 구성)
|
|
(function() {
|
|
var script = document.createElement('script');
|
|
script.src = (contextPath || '') + '/common/config/context-path.do';
|
|
script.async = false; // 동기적으로 로드하여 설정값이 먼저 로드되도록 함
|
|
script.onerror = function() {
|
|
// 컨택스트 패스 로드 실패 시 기본값 유지
|
|
console.warn('컨택스트 패스 설정 로드에 실패했습니다. 추출된 값("' + contextPath + '")을 사용합니다.');
|
|
};
|
|
document.head.appendChild(script);
|
|
})();
|
|
|
|
/* 로그인 URL 설정 로드 */
|
|
// 기본값 설정 (설정 로드 실패 시 사용)
|
|
var loginUrl = '/login/login.do';
|
|
|
|
// 서버에서 설정값 로드 (컨택스트 패스 적용)
|
|
(function() {
|
|
var script = document.createElement('script');
|
|
script.src = (contextPath || '') + '/common/config/login-url.do';
|
|
script.async = false; // 동기적으로 로드하여 설정값이 먼저 로드되도록 함
|
|
document.head.appendChild(script);
|
|
})();
|
|
|
|
/* =============================================================================
|
|
* 2. SESSION & SECURITY MANAGEMENT (세션 및 보안 관리)
|
|
* ============================================================================= */
|
|
|
|
/**
|
|
* 세션 만료 처리 함수 - 모든 팝업을 닫고 로그인 페이지로 이동
|
|
* 중요로직: 팝업 환경에서 세션 만료 시 현재 팝업이 아닌 최상위 창에서 로그인 페이지로 이동
|
|
*/
|
|
function handleSessionExpired() {
|
|
try {
|
|
console.log('[세션 만료] 처리 시작');
|
|
|
|
// 중요로직: 현재 창이 팝업인지 확인
|
|
var isPopup = !!(window.opener && !window.opener.closed);
|
|
console.log('[세션 만료] 현재 창 팝업 여부:', isPopup);
|
|
|
|
if (isPopup) {
|
|
// 팝업인 경우: 최상위 부모창 찾기
|
|
var topWindow = window;
|
|
var windowChain = [];
|
|
var currentWindow = window;
|
|
|
|
// 최상위 창까지 탐색
|
|
while (currentWindow && currentWindow.opener && !currentWindow.opener.closed) {
|
|
windowChain.push(currentWindow);
|
|
currentWindow = currentWindow.opener;
|
|
topWindow = currentWindow;
|
|
|
|
// 무한루프 방지 (최대 10단계)
|
|
if (windowChain.length > 10) {
|
|
console.warn('[세션 만료] 너무 많은 팝업 체인, 탐색 중단');
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log('[세션 만료] 팝업 체인 길이:', windowChain.length);
|
|
console.log('[세션 만료] 최상위 창 찾음:', !!topWindow);
|
|
|
|
// 메시지 표시 (현재 팝업에서)
|
|
alert("세션이 종료되었습니다. 다시 로그인해주세요.");
|
|
|
|
// 최상위 창을 로그인 페이지로 이동
|
|
if (topWindow && !topWindow.closed) {
|
|
try {
|
|
topWindow.location.href = (contextPath || '') + loginUrl;
|
|
console.log('[세션 만료] 최상위 창 리다이렉트 성공');
|
|
} catch (redirectError) {
|
|
console.warn('[세션 만료] 최상위 창 리다이렉트 실패:', redirectError);
|
|
// 리다이렉트 실패 시 현재 창에서 이동
|
|
location.href = (contextPath || '') + loginUrl;
|
|
}
|
|
} else {
|
|
// 최상위 창이 닫혀있는 경우 현재 창에서 이동
|
|
location.href = (contextPath || '') + loginUrl;
|
|
console.log('[세션 만료] 현재 창에서 리다이렉트');
|
|
}
|
|
|
|
// 모든 팝업창 닫기 (약간의 지연 후)
|
|
setTimeout(function() {
|
|
try {
|
|
// 하위 팝업부터 상위 팝업 순서로 닫기
|
|
for (var i = 0; i < windowChain.length; i++) {
|
|
try {
|
|
var popupWindow = windowChain[i];
|
|
if (popupWindow && !popupWindow.closed) {
|
|
popupWindow.close();
|
|
console.log('[세션 만료] 팝업창 닫기 성공:', i + 1);
|
|
}
|
|
} catch (closeError) {
|
|
console.warn('[세션 만료] 팝업창 닫기 실패:', closeError);
|
|
}
|
|
}
|
|
console.log('[세션 만료] 모든 팝업창 닫기 완료');
|
|
} catch (e) {
|
|
console.warn('[세션 만료] 팝업창 일괄 닫기 실패:', e);
|
|
}
|
|
}, 300); // 리다이렉트 후 약간의 지연
|
|
|
|
} else {
|
|
// 일반 페이지인 경우 기존 로직 사용
|
|
console.log('[세션 만료] 일반 페이지에서 처리');
|
|
redirectToLoginWithCloseAllPopups("세션이 종료되었습니다. 다시 로그인해주세요.");
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('[세션 만료] 전체 처리 중 오류:', e);
|
|
// 모든 처리가 실패한 경우 기본 처리
|
|
try {
|
|
alert("세션이 종료되었습니다. 다시 로그인해주세요.");
|
|
location.href = (contextPath || '') + loginUrl;
|
|
} catch (finalError) {
|
|
console.error('[세션 만료] 최종 처리도 실패:', finalError);
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 팝업을 닫고 메인페이지를 로그인 페이지로 이동시키는 함수
|
|
* 중요로직: 다중 팝업 구조에서 안전하게 모든 팝업을 닫고 최상위 창만 로그인 페이지로 이동
|
|
* @param {string} message - 사용자에게 표시할 메시지 (선택사항)
|
|
* @param {boolean} showAlert - alert 창 표시 여부 (기본값: true)
|
|
*/
|
|
function redirectToLoginWithCloseAllPopups(message, showAlert) {
|
|
try {
|
|
// 기본값 설정
|
|
if (showAlert === undefined) showAlert = true;
|
|
if (!message) message = "로그인 페이지로 이동합니다.";
|
|
|
|
// 메시지 표시
|
|
if (showAlert) {
|
|
alert(message);
|
|
}
|
|
|
|
// 중요로직: 최상위 부모창 찾기 (다중 팝업 처리)
|
|
var topWindow = window;
|
|
var currentWindow = window;
|
|
|
|
// 현재 창부터 최상위까지의 모든 창 정보 수집
|
|
var windowChain = [];
|
|
while (currentWindow) {
|
|
windowChain.push({
|
|
window: currentWindow,
|
|
isPopup: !!(currentWindow.opener && !currentWindow.opener.closed)
|
|
});
|
|
|
|
if (currentWindow.opener && !currentWindow.opener.closed) {
|
|
currentWindow = currentWindow.opener;
|
|
topWindow = currentWindow;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log('[로그인 리다이렉트] 창 체인 정보:', windowChain.length + '개의 창 감지');
|
|
|
|
// 최상위 부모창을 로그인 페이지로 이동
|
|
if (topWindow && !topWindow.closed) {
|
|
try {
|
|
topWindow.location.href = (contextPath || '') + loginUrl;
|
|
console.log('[로그인 리다이렉트] 최상위 창 리다이렉트 성공');
|
|
} catch (redirectError) {
|
|
console.warn('[로그인 리다이렉트] 최상위 창 리다이렉트 실패:', redirectError);
|
|
// 리다이렉트 실패 시 현재 창에서 이동
|
|
location.href = (contextPath || '') + loginUrl;
|
|
}
|
|
} else {
|
|
// 최상위 창이 닫혀있는 경우 현재 창에서 이동
|
|
location.href = (contextPath || '') + loginUrl;
|
|
console.log('[로그인 리다이렉트] 현재 창에서 리다이렉트');
|
|
}
|
|
|
|
// 중요로직: 메인창 외에 모든 팝업창 종료
|
|
setTimeout(function() {
|
|
try {
|
|
var popupsToClose = windowChain.filter(function(item) {
|
|
return item.isPopup;
|
|
});
|
|
|
|
console.log('[로그인 리다이렉트] 닫을 팝업 수:', popupsToClose.length);
|
|
|
|
// 수집된 모든 팝업창을 닫기 (하위창부터 상위창 순서)
|
|
for (var i = 0; i < popupsToClose.length; i++) {
|
|
try {
|
|
var popupWindow = popupsToClose[i].window;
|
|
if (popupWindow && !popupWindow.closed) {
|
|
popupWindow.close();
|
|
console.log('[로그인 리다이렉트] 팝업창 닫기 성공:', i + 1);
|
|
}
|
|
} catch (closeError) {
|
|
console.warn('[로그인 리다이렉트] 팝업창 닫기 실패:', closeError);
|
|
}
|
|
}
|
|
|
|
console.log('[로그인 리다이렉트] 모든 팝업창 닫기 완료');
|
|
} catch (e) {
|
|
console.warn('[로그인 리다이렉트] 팝업창 일괄 닫기 실패:', e);
|
|
// 기본 방식으로 현재 창 닫기 시도
|
|
if (window.opener && !window.opener.closed) {
|
|
try {
|
|
window.close();
|
|
} catch (fallbackError) {
|
|
console.warn('[로그인 리다이렉트] 기본 방식 팝업창 닫기도 실패:', fallbackError);
|
|
}
|
|
}
|
|
}
|
|
}, 200); // 리다이렉트 후 약간의 지연을 두어 안정성 확보
|
|
|
|
} catch (e) {
|
|
console.error("[로그인 리다이렉트] 전체 처리 중 오류:", e);
|
|
// 모든 처리가 실패한 경우 현재 창에서 로그인 페이지로 이동
|
|
try {
|
|
location.href = (contextPath || '') + loginUrl;
|
|
} catch (finalError) {
|
|
console.error("[로그인 리다이렉트] 최종 리다이렉트도 실패:", finalError);
|
|
// 최후의 수단으로 새로고침
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그아웃 함수 - 모든 팝업을 닫고 로그인 페이지로 이동
|
|
*/
|
|
function logout() {
|
|
redirectToLoginWithCloseAllPopups("로그아웃 되었습니다.");
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 3. AJAX COMMUNICATION (Ajax 통신 관리)
|
|
* ============================================================================= */
|
|
|
|
/* jQuery Ajax 설정 */
|
|
$.ajaxSetup({
|
|
type:"POST",
|
|
dataType : "JSON",
|
|
cache: false
|
|
});
|
|
|
|
/* Ajax Error 설정 */
|
|
$(document).ajaxError( function( event, jqxhr, settings, exception ){
|
|
if( jqxhr.responseJSON ){
|
|
// 세션 만료인 경우 처리
|
|
if(jqxhr.responseJSON.errorCode === "SESSION_EXPIRED") {
|
|
console.log('[Ajax Error] 세션 만료 감지, URL:', settings.url);
|
|
// 중요로직: 세션 만료 시 안전한 처리를 위해 약간의 지연 후 처리
|
|
setTimeout(function() {
|
|
handleSessionExpired();
|
|
}, 100);
|
|
} else if(jqxhr.responseJSON.errorCode === "MessageException") {
|
|
alert(jqxhr.responseJSON.message);
|
|
} else if(jqxhr.responseJSON.errorCode === "MESSAGE") {
|
|
alert(jqxhr.responseJSON.message);
|
|
} else {
|
|
console.error(jqxhr);
|
|
alert("에러가 발생했습니다.\n\nERROR CODE : "+jqxhr.responseJSON.errorCode+"\nMESSAGE : "+jqxhr.responseJSON.message);
|
|
}
|
|
} else if(jqxhr.status === 401) {
|
|
// HTTP 401 Unauthorized 응답도 세션 만료로 처리
|
|
console.log('[Ajax Error] HTTP 401 감지, 세션 만료로 처리, URL:', settings.url);
|
|
setTimeout(function() {
|
|
handleSessionExpired();
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
/* Ajax Progress Block UI 설정 */
|
|
// Ajax 활성 요청 카운터 (동시 Ajax 요청 처리를 위함)
|
|
var activeAjaxCount = 0;
|
|
|
|
// Progress Block UI 초기화
|
|
$(document).ready(function() {
|
|
// Progress Block UI 생성
|
|
createProgressBlockUI();
|
|
console.log('[Ajax Block UI] 초기화 완료');
|
|
|
|
// activeAjaxCount 리셋 (페이지 로드 시)
|
|
activeAjaxCount = 0;
|
|
console.log('[Ajax Block UI] Ajax 카운터 초기화:', activeAjaxCount);
|
|
});
|
|
|
|
// Ajax 시작 시 Progress Block UI 표시
|
|
$(document).ajaxSend(function(event, jqXHR, ajaxOptions) {
|
|
console.log('[Ajax Block UI] Ajax 시작:', ajaxOptions.url);
|
|
|
|
// Block UI 제외 대상 URL 체크
|
|
if (ajaxOptions.url && ajaxOptions.url.indexOf('/system/user/duplicateCheck.ajax') !== -1) {
|
|
console.log('[Ajax Block UI] 제외 대상 URL, Block UI 표시 안함');
|
|
return;
|
|
}
|
|
|
|
activeAjaxCount++;
|
|
console.log('[Ajax Block UI] 활성 Ajax 카운트 증가:', activeAjaxCount);
|
|
|
|
// 첫 번째 Ajax 요청일 때만 Progress Block UI 표시
|
|
if (activeAjaxCount === 1) {
|
|
// Block UI 요소 존재 확인
|
|
if ($('#ajax-progress-overlay').length === 0) {
|
|
console.warn('[Ajax Block UI] Block UI 요소가 존재하지 않음, 생성 시도');
|
|
createProgressBlockUI();
|
|
}
|
|
|
|
var $overlay = $('#ajax-progress-overlay');
|
|
if ($overlay.length > 0) {
|
|
$overlay.css('display', 'flex').hide().fadeIn(200);
|
|
console.log('[Ajax Block UI] Block UI 표시 완료');
|
|
} else {
|
|
console.error('[Ajax Block UI] Block UI 요소를 찾을 수 없음');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Ajax 완료 시 Progress Block UI 제거 (성공/실패 모든 경우)
|
|
$(document).ajaxComplete(function(event, jqXHR, ajaxOptions) {
|
|
console.log('[Ajax Block UI] Ajax 완료:', ajaxOptions.url);
|
|
|
|
// Block UI 제외 대상 URL 체크 (ajaxSend와 동일한 조건으로 수정)
|
|
if (ajaxOptions.url && ajaxOptions.url.indexOf('/system/user/duplicateCheck.ajax') !== -1) {
|
|
console.log('[Ajax Block UI] 제외 대상 URL, 카운트 감소 안함');
|
|
return;
|
|
}
|
|
|
|
activeAjaxCount--;
|
|
console.log('[Ajax Block UI] 활성 Ajax 카운트 감소:', activeAjaxCount);
|
|
|
|
// 모든 Ajax 요청이 완료되었을 때만 Progress Block UI 숨김
|
|
if (activeAjaxCount <= 0) {
|
|
activeAjaxCount = 0; // 음수 방지
|
|
var $overlay = $('#ajax-progress-overlay');
|
|
if ($overlay.length > 0) {
|
|
$overlay.fadeOut(200);
|
|
console.log('[Ajax Block UI] Block UI 숨김 완료');
|
|
} else {
|
|
console.warn('[Ajax Block UI] Block UI 요소를 찾을 수 없어 숨길 수 없음');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Progress Block UI 생성 함수
|
|
function createProgressBlockUI() {
|
|
if ($('#ajax-progress-overlay').length === 0) {
|
|
var progressHtml =
|
|
'<div id="ajax-progress-overlay" class="ajax-progress-overlay" style="' +
|
|
'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' +
|
|
'background-color: rgba(0, 0, 0, 0.3); z-index: 9999; ' +
|
|
'display: none; justify-content: center; align-items: center;">' +
|
|
'<div class="ajax-progress-content" style="' +
|
|
'background-color: #ffffff; border-radius: 8px; padding: 30px; ' +
|
|
'box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; min-width: 200px;">' +
|
|
'<div class="ajax-progress-spinner" style="' +
|
|
'width: 40px; height: 40px; border: 4px solid #f3f3f3; ' +
|
|
'border-top: 4px solid #007bff; border-radius: 50%; ' +
|
|
'animation: spin 1s linear infinite; margin: 0 auto 15px;"></div>' +
|
|
'<p class="ajax-progress-text" style="' +
|
|
'color: #495057; font-size: 14px; font-weight: 500; margin: 0; ' +
|
|
'text-align: center;">처리 중입니다...</p>' +
|
|
'</div>' +
|
|
'</div>';
|
|
$('body').append(progressHtml);
|
|
|
|
// CSS 애니메이션이 없는 경우를 대비한 인라인 스타일 추가
|
|
var style = document.createElement('style');
|
|
style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
|
|
document.head.appendChild(style);
|
|
|
|
console.log('[Ajax Block UI] Block UI 요소 생성 완료');
|
|
}
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 4. COMMON UTILITIES (공통 유틸리티 함수들)
|
|
* ============================================================================= */
|
|
|
|
/* 4.1 VALIDATION UTILITIES (검증 관련 함수들) */
|
|
|
|
/**
|
|
* HTML 엔티티를 원래 문자로 변환하는 함수
|
|
* JavaScript XSS 공격 패턴도 감지하여 처리
|
|
* 문자열 및 JSON 객체 모두 처리 가능
|
|
* @param {string|object|array} input - escape 처리된 문자열 또는 JSON 객체
|
|
* @return {string|object|array} unescape 처리된 문자열 또는 JSON 객체
|
|
*/
|
|
function unescapeHtml(input) {
|
|
// 입력값이 없는 경우 그대로 반환
|
|
if (input === null || input === undefined) return input;
|
|
|
|
// 입력값이 문자열인 경우 기존 로직으로 처리
|
|
if (typeof input === 'string') {
|
|
// HTML 엔티티를 원래 문자로 변환
|
|
var unescaped = input.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(///g, '/')
|
|
.replace(/(/g, '(')
|
|
.replace(/)/g, ')');
|
|
|
|
// 문자열 연결을 통한 우회 방지를 위한 정규화
|
|
// javascript: 같은 패턴을 javascript:로 변환
|
|
unescaped = unescaped.replace(/j\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*a\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*v\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*a\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*s\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*c\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*r\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*i\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*p\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*t\s*(?:&[^;]+;|\\[0-9A-Fa-f]+)\s*:/gi, 'javascript:');
|
|
|
|
// JavaScript XSS 공격 패턴 감지 및 처리
|
|
var xssPatterns = [
|
|
// 위험한 프로토콜
|
|
{ pattern: /javascript\s*:/i, replacement: 'blocked:' },
|
|
{ pattern: /vbscript\s*:/i, replacement: 'blocked:' },
|
|
{ pattern: /data\s*:[^,]*?base64/i, replacement: 'blocked:' },
|
|
{ pattern: /livescript\s*:/i, replacement: 'blocked:' },
|
|
{ pattern: /mocha\s*:/i, replacement: 'blocked:' },
|
|
|
|
// 스크립트 태그
|
|
{ pattern: /<script\b[^>]*>(.*?)<\/script>/gi, replacement: '' },
|
|
|
|
// 이벤트 핸들러 속성
|
|
{ pattern: /\s+on\w+\s*=\s*["']?[^"']*["']?/gi, replacement: '' },
|
|
|
|
// 위험한 함수 호출
|
|
{ pattern: /eval\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /setTimeout\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /setInterval\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /Function\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /constructor\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /import\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /execScript\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /document\s*\.\s*write\s*\(/gi, replacement: 'blocked.write(' },
|
|
{ pattern: /document\s*\.\s*writeln\s*\(/gi, replacement: 'blocked.writeln(' },
|
|
|
|
// 위험한 CSS 표현식
|
|
{ pattern: /expression\s*\(/gi, replacement: 'blocked(' },
|
|
{ pattern: /url\s*\(\s*["']?\s*javascript:/gi, replacement: 'url(blocked:' },
|
|
|
|
// 인라인 스타일 속성에서의 XSS
|
|
{ pattern: /style\s*=\s*["']?[^"']*(?:expression|javascript|behavior|url\s*\(\s*['"]?\s*javascript:)[^"']*/gi, replacement: 'style="blocked"' },
|
|
|
|
// 위험한 HTML 태그
|
|
{ pattern: /<iframe\b[^>]*>(.*?)<\/iframe>/gi, replacement: '' },
|
|
{ pattern: /<object\b[^>]*>(.*?)<\/object>/gi, replacement: '' },
|
|
{ pattern: /<embed\b[^>]*>(.*?)<\/embed>/gi, replacement: '' },
|
|
{ pattern: /<base\b[^>]*>/gi, replacement: '' },
|
|
{ pattern: /<form\b[^>]*>(.*?)<\/form>/gi, replacement: '' },
|
|
{ pattern: /<svg\b[^>]*>(.*?)<\/svg>/gi, replacement: '' },
|
|
{ pattern: /<math\b[^>]*>(.*?)<\/math>/gi, replacement: '' },
|
|
{ pattern: /<link\b[^>]*>/gi, replacement: '' },
|
|
{ pattern: /<meta\b[^>]*>/gi, replacement: '' },
|
|
|
|
// 주석 내 코드 실행 방지
|
|
{ pattern: /<!--[^>]*-->/g, replacement: '' },
|
|
|
|
// 기타 위험한 패턴
|
|
{ pattern: /document\s*\.\s*cookie/gi, replacement: 'blocked.cookie' },
|
|
{ pattern: /document\s*\.\s*domain/gi, replacement: 'blocked.domain' },
|
|
{ pattern: /document\s*\.\s*location/gi, replacement: 'blocked.location' },
|
|
{ pattern: /window\s*\.\s*location/gi, replacement: 'blocked.location' },
|
|
{ pattern: /location\s*\.\s*href/gi, replacement: 'blocked.href' },
|
|
{ pattern: /location\s*\.\s*replace/gi, replacement: 'blocked.replace' },
|
|
{ pattern: /location\s*\.\s*assign/gi, replacement: 'blocked.assign' },
|
|
{ pattern: /window\s*\.\s*open\s*\(/gi, replacement: 'blocked.open(' },
|
|
{ pattern: /window\s*\.\s*execScript\s*\(/gi, replacement: 'blocked.execScript(' }
|
|
];
|
|
|
|
// 모든 XSS 패턴에 대해 검사 및 처리
|
|
for (var i = 0; i < xssPatterns.length; i++) {
|
|
unescaped = unescaped.replace(xssPatterns[i].pattern, xssPatterns[i].replacement);
|
|
}
|
|
|
|
// 추가적인 보안 검사 - 여러 번 패턴 적용 (중첩된 공격 방지)
|
|
if (/javascript|eval|expression|on\w+=|<script|<iframe|<object|<embed|<svg|<form/i.test(unescaped)) {
|
|
// 위험한 패턴이 여전히 존재하면 한 번 더 패턴 적용
|
|
for (var i = 0; i < xssPatterns.length; i++) {
|
|
unescaped = unescaped.replace(xssPatterns[i].pattern, xssPatterns[i].replacement);
|
|
}
|
|
}
|
|
|
|
return unescaped;
|
|
}
|
|
// 입력값이 배열인 경우 각 요소를 재귀적으로 처리
|
|
else if (Array.isArray(input)) {
|
|
// 배열의 각 요소에 대해 재귀적으로 unescapeHtml 함수 적용
|
|
return input.map(function(item) {
|
|
return unescapeHtml(item);
|
|
});
|
|
}
|
|
// 입력값이 객체인 경우 각 속성을 재귀적으로 처리
|
|
else if (typeof input === 'object') {
|
|
// 객체의 복사본 생성
|
|
var result = {};
|
|
|
|
// 객체의 각 속성에 대해 재귀적으로 unescapeHtml 함수 적용
|
|
for (var key in input) {
|
|
if (input.hasOwnProperty(key)) {
|
|
result[key] = unescapeHtml(input[key]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
// 기타 타입(숫자, 불리언 등)은 그대로 반환
|
|
else {
|
|
return input;
|
|
}
|
|
}
|
|
|
|
/* 4.2 COLOR UTILITIES (색상 관련 함수들) */
|
|
|
|
/**
|
|
* selectbox option에 data-color가 있을 때 자동으로 색상을 적용하는 함수
|
|
* @param {string|jQuery} selector - selectbox 선택자 또는 jQuery 객체
|
|
*/
|
|
function applySelectboxColors(selector) {
|
|
var $select = typeof selector === 'string' ? $(selector) : selector;
|
|
|
|
$select.find('option[data-color]').each(function() {
|
|
var $option = $(this);
|
|
var color = $option.data('color');
|
|
|
|
if (color && color.trim() !== '') {
|
|
// 색상을 투명하게 만들기
|
|
var transparentColor = makeColorTransparent(color, 0.15);
|
|
|
|
$option.css({
|
|
'background-color': transparentColor,
|
|
'color': 'black',
|
|
'padding': '8px',
|
|
'margin': '2px 0',
|
|
'border-radius': '4px'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 색상을 투명하게 만드는 함수
|
|
* @param {string} color - 원본 색상
|
|
* @param {number} opacity - 투명도 (0~1)
|
|
* @returns {string} 투명한 색상
|
|
*/
|
|
function makeColorTransparent(color, opacity) {
|
|
// rgba 형식인 경우
|
|
if (color.startsWith('rgba')) {
|
|
return color.replace(/[\d.]+\)$/, opacity + ')');
|
|
}
|
|
// rgb 형식인 경우
|
|
else if (color.startsWith('rgb')) {
|
|
return color.replace('rgb', 'rgba').replace(')', ', ' + opacity + ')');
|
|
}
|
|
// hex 형식인 경우
|
|
else if (color.startsWith('#')) {
|
|
var r = parseInt(color.substr(1,2), 16);
|
|
var g = parseInt(color.substr(3,2), 16);
|
|
var b = parseInt(color.substr(5,2), 16);
|
|
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + opacity + ')';
|
|
}
|
|
// 기타 형식은 그대로 반환
|
|
return color;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 5. UI COMPONENTS (UI 컴포넌트들)
|
|
* ============================================================================= */
|
|
|
|
/* 5.1 MODAL UTILITIES (모달 관련 함수들) */
|
|
|
|
$(document).ready(function () {
|
|
$('.pop-x-btn, .modalclose').click(function() {
|
|
var tmp = $(this).parents().parents().parents()
|
|
if (tmp.attr('class') == 'modalz act') {
|
|
tmp.removeClass('act');
|
|
} else {
|
|
//tmp.removeClass('act');
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 모달 닫기 이벤트를 설정하는 함수
|
|
* 모달 닫기 버튼 클릭 및 모달 외부 클릭 시 모달을 닫는 이벤트를 설정합니다.
|
|
* @param {string} modalId - 모달 요소의 ID (기본값: 'modal')
|
|
* @param {string} closeBtnSelector - 닫기 버튼의 선택자 (기본값: '.pop-x-btn, .modalclose')
|
|
*/
|
|
function initModalClose(modalId) {
|
|
// 기본값 설정
|
|
modalId = modalId || 'modal';
|
|
|
|
// '#' 접두사가 없는 경우 추가
|
|
if (!modalId.startsWith('#')) {
|
|
modalId = '#' + modalId;
|
|
}
|
|
|
|
// 모달 외부 클릭 시 닫기 (dim 영역 클릭 시)
|
|
$(modalId + ' .dim').on('click', function() {
|
|
$(modalId).removeClass('act');
|
|
});
|
|
|
|
// ESC 키 입력 시 모달 닫기
|
|
$(document).on('keydown', function(e) {
|
|
if (e.keyCode === 27 && $(modalId).hasClass('act')) { // ESC key
|
|
$(modalId).removeClass('act');
|
|
}
|
|
});
|
|
}
|
|
|
|
/* 5.2 SELECTBOX INITIALIZATION (selectbox 자동 색상 적용) */
|
|
|
|
/**
|
|
* 페이지 로드 시 모든 selectbox에 자동 색상 적용
|
|
*/
|
|
$(document).ready(function() {
|
|
// 기존 selectbox에 색상 적용
|
|
applySelectboxColors('select');
|
|
|
|
// 동적으로 추가되는 selectbox를 위한 MutationObserver 설정
|
|
var observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.type === 'childList') {
|
|
mutation.addedNodes.forEach(function(node) {
|
|
if (node.nodeType === 1) { // Element node
|
|
// 새로 추가된 selectbox 찾기
|
|
var $newSelects = $(node).find('select[data-auto-color="true"]');
|
|
if ($newSelects.length > 0) {
|
|
applySelectboxColors($newSelects);
|
|
}
|
|
|
|
// 새로 추가된 option이 있는 selectbox 찾기
|
|
var $selectsWithNewOptions = $(node).find('select option[data-color]').parent();
|
|
if ($selectsWithNewOptions.length > 0) {
|
|
applySelectboxColors($selectsWithNewOptions);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// body 요소의 변경사항 감지
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}); |