feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
259
deploy/tkfb-package/web-ui/js/common/security.js
Normal file
259
deploy/tkfb-package/web-ui/js/common/security.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Security Utilities - 보안 관련 유틸리티 함수
|
||||
*
|
||||
* XSS 방지, 입력값 검증, 안전한 DOM 조작을 위한 함수 모음
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-02-04
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
const SecurityUtils = {
|
||||
/**
|
||||
* HTML 특수문자 이스케이프 (XSS 방지)
|
||||
* innerHTML에 사용자 입력을 삽입할 때 반드시 사용
|
||||
*
|
||||
* @param {string} str - 이스케이프할 문자열
|
||||
* @returns {string} 이스케이프된 문자열
|
||||
*
|
||||
* @example
|
||||
* element.innerHTML = `<span>${SecurityUtils.escapeHtml(userInput)}</span>`;
|
||||
*/
|
||||
escapeHtml: function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
if (typeof str !== 'string') str = String(str);
|
||||
|
||||
const htmlEntities = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
return str.replace(/[&<>"'`=\/]/g, function(char) {
|
||||
return htmlEntities[char];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* URL 파라미터 이스케이프
|
||||
* URL에 사용자 입력을 포함할 때 사용
|
||||
*
|
||||
* @param {string} str - 이스케이프할 문자열
|
||||
* @returns {string} URL 인코딩된 문자열
|
||||
*/
|
||||
escapeUrl: function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return encodeURIComponent(String(str));
|
||||
},
|
||||
|
||||
/**
|
||||
* JavaScript 문자열 이스케이프
|
||||
* 동적 JavaScript 생성 시 사용 (권장하지 않음)
|
||||
*
|
||||
* @param {string} str - 이스케이프할 문자열
|
||||
* @returns {string} 이스케이프된 문자열
|
||||
*/
|
||||
escapeJs: function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t');
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전한 텍스트 설정
|
||||
* innerHTML 대신 textContent 사용 권장
|
||||
*
|
||||
* @param {Element} element - DOM 요소
|
||||
* @param {string} text - 설정할 텍스트
|
||||
*/
|
||||
setTextSafe: function(element, text) {
|
||||
if (element && element.nodeType === 1) {
|
||||
element.textContent = text;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전한 HTML 삽입
|
||||
* 사용자 입력이 포함된 HTML을 삽입할 때 사용
|
||||
*
|
||||
* @param {Element} element - DOM 요소
|
||||
* @param {string} template - HTML 템플릿 ({{변수}} 형식)
|
||||
* @param {Object} data - 삽입할 데이터 (자동 이스케이프됨)
|
||||
*
|
||||
* @example
|
||||
* SecurityUtils.setHtmlSafe(div, '<span>{{name}}</span>', { name: userInput });
|
||||
*/
|
||||
setHtmlSafe: function(element, template, data) {
|
||||
if (!element || element.nodeType !== 1) return;
|
||||
|
||||
const self = this;
|
||||
const safeHtml = template.replace(/\{\{(\w+)\}\}/g, function(match, key) {
|
||||
return data.hasOwnProperty(key) ? self.escapeHtml(data[key]) : '';
|
||||
});
|
||||
|
||||
element.innerHTML = safeHtml;
|
||||
},
|
||||
|
||||
/**
|
||||
* 입력값 검증 - 숫자
|
||||
*
|
||||
* @param {any} value - 검증할 값
|
||||
* @param {Object} options - 옵션 { min, max, allowFloat }
|
||||
* @returns {number|null} 유효한 숫자 또는 null
|
||||
*/
|
||||
validateNumber: function(value, options) {
|
||||
options = options || {};
|
||||
const num = options.allowFloat ? parseFloat(value) : parseInt(value, 10);
|
||||
|
||||
if (isNaN(num)) return null;
|
||||
if (options.min !== undefined && num < options.min) return null;
|
||||
if (options.max !== undefined && num > options.max) return null;
|
||||
|
||||
return num;
|
||||
},
|
||||
|
||||
/**
|
||||
* 입력값 검증 - 이메일
|
||||
*
|
||||
* @param {string} email - 검증할 이메일
|
||||
* @returns {boolean} 유효 여부
|
||||
*/
|
||||
validateEmail: function(email) {
|
||||
if (!email || typeof email !== 'string') return false;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
},
|
||||
|
||||
/**
|
||||
* 입력값 검증 - 길이
|
||||
*
|
||||
* @param {string} str - 검증할 문자열
|
||||
* @param {Object} options - 옵션 { min, max }
|
||||
* @returns {boolean} 유효 여부
|
||||
*/
|
||||
validateLength: function(str, options) {
|
||||
options = options || {};
|
||||
if (!str || typeof str !== 'string') return false;
|
||||
|
||||
const len = str.length;
|
||||
if (options.min !== undefined && len < options.min) return false;
|
||||
if (options.max !== undefined && len > options.max) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전한 JSON 파싱
|
||||
*
|
||||
* @param {string} jsonString - 파싱할 JSON 문자열
|
||||
* @param {any} defaultValue - 파싱 실패 시 기본값
|
||||
* @returns {any} 파싱된 객체 또는 기본값
|
||||
*/
|
||||
parseJsonSafe: function(jsonString, defaultValue) {
|
||||
defaultValue = defaultValue === undefined ? null : defaultValue;
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
console.warn('[SecurityUtils] JSON 파싱 실패:', e.message);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* localStorage에서 안전하게 데이터 가져오기
|
||||
*
|
||||
* @param {string} key - 키
|
||||
* @param {any} defaultValue - 기본값
|
||||
* @returns {any} 저장된 값 또는 기본값
|
||||
*/
|
||||
getStorageSafe: function(key, defaultValue) {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null) return defaultValue;
|
||||
return this.parseJsonSafe(item, defaultValue);
|
||||
} catch (e) {
|
||||
console.warn('[SecurityUtils] localStorage 접근 실패:', e.message);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* URL 파라미터 안전하게 가져오기
|
||||
*
|
||||
* @param {string} name - 파라미터 이름
|
||||
* @param {string} defaultValue - 기본값
|
||||
* @returns {string} 파라미터 값 (이스케이프됨)
|
||||
*/
|
||||
getUrlParamSafe: function(name, defaultValue) {
|
||||
defaultValue = defaultValue === undefined ? '' : defaultValue;
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const value = urlParams.get(name);
|
||||
return value !== null ? value : defaultValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ID 파라미터 안전하게 가져오기 (숫자 검증)
|
||||
*
|
||||
* @param {string} name - 파라미터 이름
|
||||
* @returns {number|null} 유효한 ID 또는 null
|
||||
*/
|
||||
getIdParamSafe: function(name) {
|
||||
const value = this.getUrlParamSafe(name);
|
||||
return this.validateNumber(value, { min: 1 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Content Security Policy 위반 리포터
|
||||
*
|
||||
* @param {string} reportUri - 리포트 전송 URL
|
||||
*/
|
||||
enableCspReporting: function(reportUri) {
|
||||
document.addEventListener('securitypolicyviolation', function(e) {
|
||||
console.error('[CSP Violation]', {
|
||||
blockedUri: e.blockedURI,
|
||||
violatedDirective: e.violatedDirective,
|
||||
originalPolicy: e.originalPolicy
|
||||
});
|
||||
|
||||
if (reportUri) {
|
||||
fetch(reportUri, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
blocked_uri: e.blockedURI,
|
||||
violated_directive: e.violatedDirective,
|
||||
document_uri: e.documentURI,
|
||||
timestamp: new Date().toISOString()
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).catch(function() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 전역 노출
|
||||
global.SecurityUtils = SecurityUtils;
|
||||
|
||||
// 편의를 위한 단축 함수
|
||||
global.escapeHtml = SecurityUtils.escapeHtml.bind(SecurityUtils);
|
||||
global.escapeUrl = SecurityUtils.escapeUrl.bind(SecurityUtils);
|
||||
|
||||
console.log('[Module] common/security.js 로드 완료');
|
||||
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
Reference in New Issue
Block a user