$.fn.datepicker.dates['kr'] = { days: ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"], daysShort: ["일", "월", "화", "수", "목", "금", "토"], daysMin: ["일", "월", "화", "수", "목", "금", "토"], months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"], monthsShort: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"], today: "오늘", clear: "Clear", format: "yyyy-mm-dd", titleFormat: "yyyy MM", weekStart: 0 }; $(document).ready(function () { $("body").on("focus", ".datepicker", function () { $(this).inputmask('9999-99-99'); $(this).datepicker({ format: "yyyy-mm-dd", language: "kr" }); }); $("body").on("focus", ".yearpicker", function () { $(this).inputmask('9999'); $(this).datepicker({ format: "yyyy", viewMode: "years", // 시작 뷰를 '연도'로 설정 minViewMode: "years", // 최소 뷰 단위를 '연도'로 설정 format: "yyyy", language: "kr", autoclose: true }); }); $("body").on("focus", ".timeMask", function () { $(this).inputmask("(09|19|20|21|22|23):(09|19|29|39|49|59)"); }); $("body").on("focus", ".numericMask", function () { $(this).inputmask("numeric", { autoGroup: true, // default: false, 정수 부분 그룹화 사용 여부 groupSeparator: ",", // default: "", 그룹 구분자 정의 digits: 0, // default: "*", 소수 크기 정의 allowMinus: false, // default: true, 음수 사용 여부 repeat: 12, // autoUnmask: true }); }); // 중요로직: 소수점을 허용하는 숫자 마스크 (지적 등에 사용) $("body").on("focus", ".decimalMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 groupSeparator: ",", // 그룹 구분자 digits: 2, // 소수점 2자리까지 허용 allowMinus: false, // 음수 사용 안함 repeat: 15, // 최대 15자리 autoUnmask: true }); }); // 중요로직: - decimal(4,4) 스펙에 맞춤 $("body").on("focus", ".decimalMask4", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, groupSeparator: ",", // 그룹 구분자 없음 repeat: 1, // 정수부 최대 1자리 (0) digits: 4, // 소수점 최대 4자리 }); }); // 중요로직: PTOUT 지적 전용 마스크 - decimal(22,2) 스펙에 맞춤 $("body").on("focus", ".ptoutMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 groupSeparator: ",", // 그룹 구분자 없음 digits: 2, // 소수점 2자리까지 허용 allowMinus: false, // 음수 사용 안함 repeat: 20, // 정수부 최대 20자리 autoUnmask: true }); }); // 중요로직: OALP 공시지가 전용 마스크 - decimal(13,0) 스펙에 맞춤 $("body").on("focus", ".oalpMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 groupSeparator: ",", // 그룹 구분자 없음 digits: 0, // 소수점 사용 안함 allowMinus: false, // 음수 사용 안함 repeat: 13, // 정수부 최대 13자리 autoUnmask: true }); }); // 중요로직: Strct_Idx_Cd 전용 마스크 - 숫자 3자리 $("body").on("focus", ".strctIdxCdMask", function () { $(this).inputmask({ mask: "999", // 3자리 숫자 패턴 (선행 0 포함) placeholder: "000", // 입력 안내용 플레이스홀더 definitions: { "9": { validator: "[0-9]", // 숫자만 허용 cardinality: 1 } }, autoUnmask: true, // 마스크 제거 후 실제 값 반환 rightAlign: false // 왼쪽 정렬 }); }); // 중요로직: Strct_Idx 전용 마스크 - decimal(10,2) 스펙에 맞춤 $("body").on("focus", ".strctIdxMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, rightAlign: false, groupSeparator: ",", // 그룹 구분자 없음 repeat: 8, // 정수부 최대 2자리 digits: 2 // 소수점 사용 안함 }); }); // 중요로직: RDVLRT_CN_YR_CNT 잔가율 내용 연도 수 전용 마스크 - decimal(2,0) 스펙에 맞춤 $("body").on("focus", ".rdvlrtCnYrCntMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, rightAlign: false, groupSeparator: ",", // 그룹 구분자 없음 repeat: 2, // 정수부 최대 2자리 digits: 0 // 소수점 사용 안함 }); }); // 중요로직: LAST_YR_RDVLRT 최종 연도 잔가율 전용 마스크 - decimal(4,2) 스펙에 맞춤 $("body").on("focus", ".lastYrRdvlrtMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, rightAlign: false, groupSeparator: ",", // 그룹 구분자 없음 repeat: 2, // 정수부 최대 2자리 digits: 2 // 소수점 최대 2자리 }); }); // 중요로직: DPRT 감가상각률 전용 마스크 - decimal(4,4) 스펙에 맞춤 $("body").on("focus", ".dprtMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, rightAlign: false, groupSeparator: ",", // 그룹 구분자 없음 repeat: 1, // 정수부 최대 1자리 (0) digits: 4, // 소수점 최대 4자리 max: 0.9999, }); }); // 중요로직: Usg_Idx_Cd 전용 마스크 - 숫자 5자리 $("body").on("focus", ".usgIdxCdMask", function () { $(this).inputmask({ mask: "99999", // 3자리 숫자 패턴 (선행 0 포함) placeholder: "00000", // 입력 안내용 플레이스홀더 definitions: { "9": { validator: "[0-9]", // 숫자만 허용 cardinality: 1 } }, autoUnmask: true, // 마스크 제거 후 실제 값 반환 rightAlign: false // 왼쪽 정렬 }); }); // 중요로직: Usg_Idx 전용 마스크 - decimal(10,2) 스펙에 맞춤 $("body").on("focus", ".usgIdxMask", function () { $(this).inputmask("numeric", { autoGroup: true, // 그룹화 사용 안함 allowMinus: false, // 음수 사용 안함 autoUnmask: true, rightAlign: false, groupSeparator: ",", // 그룹 구분자 없음 repeat: 8, // 정수부 최대 2자리 digits: 2 // 소수점 사용 안함 }); }); }); /** * Checks if value is empty. Deep-checks arrays and objects Note: isEmpty([]) == * true, isEmpty({}) == true, isEmpty([{0:false},"",0]) == true, isEmpty({0:1}) == * false * * @param value * @returns {boolean} */ function isEmpty(value) { if (value === null || value === undefined) { return true; } if (typeof value === 'string' && value.trim() === '') { return true; } if (Array.isArray(value) && value.length === 0) { return true; } if (typeof value === 'object' && Object.keys(value).length === 0) { return true; } return false; /* 기존소스 주석. 0 true 가 이닌 false 처리 var isEmptyObject = function(a) { if(typeof a.length === 'undefined') { // it's an Object, not an Array var hasNonempty = Object.keys(a).some(function nonEmpty(element) { return !isEmpty(a[element]); }); return hasNonempty ? false : isEmptyObject(Object.keys(a)); } return !a.some(function nonEmpty(element) { // check if array is really // not empty as JS thinks return !isEmpty(element); // at least one element should be // non-empty }); }; return(value == false || typeof value === 'undefined' || value == null || (typeof value === 'object' && isEmptyObject(value))); */ } /** * null 이나 빈값을 기본값으로 변경 * * @param str * 입력값 * @param defaultVal * 기본값(옵션) * @returns {String} 체크 결과값 */ function nvl(str, defaultVal) { var defaultValue = ""; if(typeof defaultVal != 'undefined') { defaultValue = defaultVal; } if(typeof str == "undefined" || str == null || str == '' || str == "undefined") { return defaultValue; } return str; } /** * 길이체크 * * @param str * @returns {Number} */ function checkLength(str) { var stringLength = str.length; var stringByteLength = 0; for(var i = 0; i < stringLength; i++) { if(escape(str.charAt(i)).length >= 4) { stringByteLength += 3; } else if(escape(str.charAt(i)) == "%A7") { stringByteLength += 3; } else { if(escape(str.charAt(i)) != "%0D") { stringByteLength++; } } } return stringByteLength; } /** * Left 빈자리 만큼 str 을 붙인다. * @param src : Right에 붙을 원본 데이터 * @param len : str붙힐 데이터 길이 * @param str : 대상 데이터 * @returns : str과 src가 붙은 데이터 * @example : lpad("123123", 10, " "); */ function lpad(src, len, str) { var retStr = ""; var padCnt = Number(len) - String(src).length; for(var i=0;i 1 ? decodeURIComponent(pair[1] || '') : ''; } } return params; }; /** * URL에 파라미터 추가 * * @param {string|object} param - 추가할 파라미터 이름 또는 파라미터 객체 * @param {string} value - 파라미터 값 (param이 문자열일 경우) * @param {string} url - 대상 URL (기본값: 현재 페이지 URL) * @returns {string} 파라미터가 추가된 URL */ var addUrlParam = function(param, value, url) { // URL이 없으면 현재 페이지 URL 사용 var targetUrl = url || window.location.href; var params = getUrlParams(targetUrl.split('?')[1] ? '?' + targetUrl.split('?')[1] : ''); var baseUrl = targetUrl.split('?')[0]; // param이 객체인 경우 if (typeof param === 'object') { for (var key in param) { if (param.hasOwnProperty(key)) { params[key] = param[key]; } } } else { // param이 문자열인 경우 params[param] = value; } // 객체를 쿼리 문자열로 변환 var queryString = Object.keys(params) .filter(function(key) { return params[key] !== null && params[key] !== undefined; }) .map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }) .join('&'); // 쿼리 문자열이 있으면 '?'를 붙여서 반환 return baseUrl + (queryString ? '?' + queryString : ''); }; /** * URL에서 파라미터 제거 * * @param {string|array} paramName - 제거할 파라미터 이름 또는 이름 배열 * @param {string} url - 대상 URL (기본값: 현재 페이지 URL) * @returns {string} 파라미터가 제거된 URL */ var removeUrlParam = function(paramName, url) { // URL이 없으면 현재 페이지 URL 사용 var targetUrl = url || window.location.href; var params = getUrlParams(targetUrl.split('?')[1] ? '?' + targetUrl.split('?')[1] : ''); var baseUrl = targetUrl.split('?')[0]; // paramName이 배열인 경우 if (Array.isArray(paramName)) { for (var i = 0; i < paramName.length; i++) { delete params[paramName[i]]; } } else { // paramName이 문자열인 경우 delete params[paramName]; } // 객체를 쿼리 문자열로 변환 var queryString = Object.keys(params) .filter(function(key) { return params[key] !== null && params[key] !== undefined; }) .map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }) .join('&'); // 쿼리 문자열이 있으면 '?'를 붙여서 반환 return baseUrl + (queryString ? '?' + queryString : ''); }; /** * URL에서 파라미터 값 가져오기 * * @param {string} paramName - 가져올 파라미터 이름 * @param {string} defaultValue - 파라미터가 없을 경우 기본값 * @param {string} url - 대상 URL (기본값: 현재 페이지 URL) * @returns {string} 파라미터 값 또는 기본값 */ var getUrlParam = function(paramName, defaultValue, url) { // URL이 없으면 현재 페이지 URL 사용 var targetUrl = url || window.location.href; var params = getUrlParams(targetUrl.split('?')[1] ? '?' + targetUrl.split('?')[1] : ''); // 파라미터 값이 있으면 반환, 없으면 기본값 반환 return params[paramName] !== undefined ? params[paramName] : (defaultValue || null); }; /** * URL에서 파라미터 값 변경 * * @param {string|object} param - 변경할 파라미터 이름 또는 파라미터 객체 * @param {string} value - 변경할 파라미터 값 (param이 문자열일 경우) * @param {string} url - 대상 URL (기본값: 현재 페이지 URL) * @returns {string} 파라미터가 변경된 URL */ var updateUrlParam = function(param, value, url) { // addUrlParam 함수를 사용하여 파라미터 추가 또는 변경 return addUrlParam(param, value, url); }; /** * 현재 URL의 파라미터를 유지하면서 새 URL 생성 * * @param {object} additionalParams - 추가할 파라미터 객체 (선택사항) * @param {array} excludeParams - 제외할 파라미터 이름 배열 (선택사항) * @param {string} newUrl - 새 URL 기본 주소 * @returns {string} 파라미터가 포함된 새 URL */ var buildUrlWithCurrentParams = function(additionalParams, excludeParams, newUrl) { // 현재 URL의 파라미터 가져오기 var currentParams = getUrlParams(); // 제외할 파라미터 처리 if (excludeParams && Array.isArray(excludeParams)) { for (var i = 0; i < excludeParams.length; i++) { delete currentParams[excludeParams[i]]; } } // 새 URL 생성 var resultUrl = newUrl; // 현재 파라미터 추가 for (var key in currentParams) { if (currentParams.hasOwnProperty(key)) { resultUrl = addUrlParam(key, currentParams[key], resultUrl); } } // 추가 파라미터 처리 if (additionalParams && typeof additionalParams === 'object') { resultUrl = addUrlParam(additionalParams, null, resultUrl); } return resultUrl; }; /** * 파라미터를 쿼리 문자열로 변환 * * @param {object} params - 파라미터 객체 * @returns {string} 쿼리 문자열 ('?' 포함) */ var paramsToQueryString = function(params) { if (!params || typeof params !== 'object') { return ''; } var queryString = Object.keys(params) .filter(function(key) { return params[key] !== null && params[key] !== undefined; }) .map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }) .join('&'); return queryString ? '?' + queryString : ''; }; /** * 파라미터 객체와 ID를 URL에 추가 * * @param {object} paramCond - 파라미터 객체 (예: ALL_PARAM_COND) * @param {string} idName - ID 파라미터 이름 * @param {string} idValue - ID 파라미터 값 * @param {string} baseUrl - 기본 URL * @returns {string} 모든 파라미터가 추가된 URL */ var buildUrlWithParamCondAndId = function(paramCond, idName, idValue, baseUrl) { // 파라미터 객체가 정의되어 있지 않으면 기본 URL에 ID만 추가 if (typeof paramCond === 'undefined' || !paramCond) { return addUrlParam(idName, idValue, baseUrl); } // 1. 기본 URL에 파라미터 객체의 모든 파라미터 추가 var urlWithParams = addUrlParam(paramCond, null, baseUrl); // 2. ID 파라미터 추가 return addUrlParam(idName, idValue, urlWithParams); }; /** * 파라미터 객체와 여러 개의 key-value 쌍을 URL에 추가 * * @param {object} paramCond - 파라미터 객체 (예: ALL_PARAM_COND) * @param {object} additionalParams - 추가할 파라미터들의 key-value 객체 (예: {key1: value1, key2: value2}) * @param {string} baseUrl - 기본 URL * @returns {string} 모든 파라미터가 추가된 URL */ var buildUrlWithParamCondAndMultipleKeys = function(paramCond, additionalParams, baseUrl) { // 파라미터 객체가 정의되어 있지 않으면 기본 URL에 추가 파라미터만 추가 if (typeof paramCond === 'undefined' || !paramCond) { return addUrlParam(additionalParams, null, baseUrl); } // 1. 기본 URL에 파라미터 객체의 모든 파라미터 추가 var urlWithParams = addUrlParam(paramCond, null, baseUrl); // 2. 추가 파라미터들 추가 if (additionalParams && typeof additionalParams === 'object') { urlWithParams = addUrlParam(additionalParams, null, urlWithParams); } return urlWithParams; }; /** * URL에 컨택스트 패스를 적용하는 유틸리티 함수 * 상대 URL을 받아서 컨택스트 패스가 적용된 절대 URL을 반환 * * @param {string} url - 상대 URL (예: '/user/list.do', '/bbs/user/post/comment/register.ajax') * @returns {string} 컨택스트 패스가 적용된 URL */ var buildContextUrl = function(url) { // contextPath 변수가 정의되지 않은 경우 기본값 사용 if (typeof contextPath === 'undefined') { console.warn('contextPath 변수가 정의되지 않았습니다. 기본값("")을 사용합니다.'); return url; } // URL이 비어있거나 null인 경우 처리 if (!url || url.trim() === '') { return url; } // 이미 절대 URL인 경우 (http:// 또는 https://로 시작) 그대로 반환 if (url.startsWith('http://') || url.startsWith('https://')) { return url; } // 상대 URL인 경우 컨택스트 패스 적용 // URL이 '/'로 시작하지 않는 경우 '/' 추가 var normalizedUrl = url.startsWith('/') ? url : '/' + url; // 컨택스트 패스가 빈 문자열이거나 '/'인 경우 처리 if (!contextPath || contextPath === '/') { return normalizedUrl; } // 컨택스트 패스와 URL 결합 (중복된 '/' 제거) var result = contextPath; if (!contextPath.endsWith('/') && !normalizedUrl.startsWith('/')) { result += '/'; } else if (contextPath.endsWith('/') && normalizedUrl.startsWith('/')) { normalizedUrl = normalizedUrl.substring(1); } result += normalizedUrl; return result; }; /** * 공통코드를 조회하여 select box에 option을 추가하는 함수 * 중요로직: 코드 그룹 ID를 기준으로 해당 코드들을 조회하여 select box를 동적으로 생성 * 확장된 파라미터를 통해 정렬 컬럼, 정렬 방향, 추가 검색 조건 등을 설정할 수 있음 * * @param {string} cdGroupId - 코드 그룹 ID (예: 'RGN_SE_CD', 'DSCL_MTHD_CD' 등) * @param {string} selectId - select 엘리먼트의 ID * @param {string} firstOptionText - 첫번째 옵션 텍스트 (예: '전체', '선택하세요' 등) * @param {string} selectedValue - 선택될 값 (옵션) * @param {object} options - 추가 옵션 객체 (옵션) * @param {string} options.sortColumn - 정렬 컬럼 (예: 'SORT_ORDR', 'CD_NM', 'CD_ID' 등) * @param {boolean} options.sortAscending - 정렬 방향 (true: 오름차순, false: 내림차순) * @param {string} options.searchCdId - 코드 ID 검색 조건 * @param {string} options.searchCdNm - 코드명 검색 조건 * @param {string} options.searchUseYn - 사용 여부 (기본값: 'Y') * @param {string} options.searchAttribute1 - 속성1 검색 조건 * @param {string} options.searchAttribute2 - 속성2 검색 조건 * @param {string} options.searchAttribute3 - 속성3 검색 조건 * @param {string} options.searchAttribute4 - 속성4 검색 조건 * @param {string} options.searchAttribute5 - 속성5 검색 조건 * @param {string} options.searchRegDttmStart - 등록일시 시작일 * @param {string} options.searchRegDttmEnd - 등록일시 종료일 * @param {string} options.searchRgtr - 등록자 검색 조건 * @param {string} options.searchMdfcnDttmStart - 수정일시 시작일 * @param {string} options.searchMdfcnDttmEnd - 수정일시 종료일 * @param {string} options.searchMdfr - 수정자 검색 조건 */ function commonCodeSelectAjax(cdGroupId, selectId, firstOptionText, selectedValue, options) { if (!cdGroupId || !selectId) { console.error('commonCodeSelectAjax: cdGroupId와 selectId는 필수 파라미터입니다.'); return; } // select 엘리먼트 찾기 var $select = $('#' + selectId); if ($select.length === 0) { console.error('commonCodeSelectAjax: ID "' + selectId + '"인 select 엘리먼트를 찾을 수 없습니다.'); return; } // 기존 option 초기화 (첫번째 옵션 제외하고 모두 삭제) $select.find('option:gt(0)').remove(); // 첫번째 옵션이 제공된 경우 설정 if (firstOptionText) { $select.find('option:first').text(firstOptionText).val(''); } // options 객체가 없으면 빈 객체로 초기화 options = options || {}; // AJAX 요청 데이터 구성 - 중요로직: 백엔드 CmmnCodeSearchVO와 정확히 매핑 var requestData = { searchCdGroupId: cdGroupId, // 필수 파라미터 searchUseYn: options.searchUseYn || 'Y' // 기본값: 사용 중인 코드만 조회 }; // 추가 검색 조건들을 requestData에 추가 (값이 있는 경우만) if (options.searchCdId) requestData.searchCdId = options.searchCdId; if (options.searchCdNm) requestData.searchCdNm = options.searchCdNm; if (options.searchAttribute1) requestData.searchAttribute1 = options.searchAttribute1; if (options.searchAttribute2) requestData.searchAttribute2 = options.searchAttribute2; if (options.searchAttribute3) requestData.searchAttribute3 = options.searchAttribute3; if (options.searchAttribute4) requestData.searchAttribute4 = options.searchAttribute4; if (options.searchAttribute5) requestData.searchAttribute5 = options.searchAttribute5; if (options.searchRegDttmStart) requestData.searchRegDttmStart = options.searchRegDttmStart; if (options.searchRegDttmEnd) requestData.searchRegDttmEnd = options.searchRegDttmEnd; if (options.searchRgtr) requestData.searchRgtr = options.searchRgtr; if (options.searchMdfcnDttmStart) requestData.searchMdfcnDttmStart = options.searchMdfcnDttmStart; if (options.searchMdfcnDttmEnd) requestData.searchMdfcnDttmEnd = options.searchMdfcnDttmEnd; if (options.searchMdfr) requestData.searchMdfr = options.searchMdfr; // 정렬 관련 파라미터 추가 if (options.sortColumn) { requestData.sortColumn = options.sortColumn; // sortAscending이 명시적으로 false인 경우에만 false로 설정 (기본값: true) requestData.sortAscending = options.sortAscending !== false; } // AJAX로 코드 상세 목록 조회 $.ajax({ url: '/common/code/detail/list.ajax', type: 'GET', data: requestData, success: function(response) { if (response && response.success) { // 응답 데이터 구조 확인 (그리드용 응답의 경우 response.data.data에 실제 배열이 있을 수 있음) var codeList = null; if (response.data && Array.isArray(response.data)) { codeList = response.data; } else if (response.data && response.data.data && Array.isArray(response.data.data)) { codeList = response.data.data; } else { console.warn('commonCodeSelectAjax: 응답 데이터 구조를 찾을 수 없습니다. cdGroupId: ' + cdGroupId, response); return; } // 정렬 처리 - 중요로직: 백엔드에서 정렬되지 않은 경우 클라이언트에서 기본 정렬 적용 if (!options.sortColumn) { // 정렬 컬럼이 지정되지 않은 경우 기본적으로 정렬순서(sortOrdr) 기준으로 정렬 codeList.sort(function(a, b) { return (a.sortOrdr || 0) - (b.sortOrdr || 0); }); } // option 추가 $.each(codeList, function(index, item) { var option = $(''); option.val(item.cdId).text(item.cdNm); $select.append(option); }); // 선택될 값이 있으면 설정 if (selectedValue) { $select.val(selectedValue); } } else { console.warn('commonCodeSelectAjax: 코드 조회 결과가 없거나 실패했습니다. cdGroupId: ' + cdGroupId); } }, error: function(xhr, status, error) { console.error('commonCodeSelectAjax: 코드 조회 중 오류 발생', { cdGroupId: cdGroupId, status: status, error: error }); } }); } /** * 자식 팝업창들을 모두 닫고 현재 창을 닫는 공통 함수 * * @param {Array} childPopups - 자식 팝업창 참조 배열 * @returns {Array} 초기화된 빈 배열 */ function closeChildPopupsAndSelf(childPopups) { // 자식 팝업창들이 배열이 아닌 경우 빈 배열로 초기화 if (!Array.isArray(childPopups)) { childPopups = []; } // 열려있는 자식 팝업창들을 먼저 닫기 for (var i = 0; i < childPopups.length; i++) { try { if (childPopups[i] && !childPopups[i].closed) { childPopups[i].close(); } } catch (e) { // 팝업창 접근 오류 무시 (이미 닫혔거나 접근 권한 없음) console.warn('팝업창 닫기 중 오류 발생:', e.message); } } // 자식 팝업 배열 초기화 childPopups.length = 0; // 자신의 창 닫기 window.close(); // 초기화된 배열 반환 return childPopups; } /** * 전역 드롭다운 인스턴스 관리 * 중요한 로직 주석: 활성화된 드롭다운들을 추적하여 다중 오픈 제어 */ if (typeof window.XitDropdownInstances === 'undefined') { window.XitDropdownInstances = []; } /** * XIT 공통 드롭다운 컴포넌트 * 중요한 로직 주석: 재사용 가능한 드롭다운 컴포넌트로 여러 페이지에서 사용할 수 있다. */ function XitDropdown(options) { // 기본 옵션 설정 this.options = $.extend({ inputSelector: null, // input 필드 셀렉터 (필수) hiddenSelector: null, // hidden 필드 셀렉터 (필수) dataUrl: null, // 데이터 조회 URL (필수) width: 'auto', // 드롭다운 넓이 maxHeight: '300px', // 드롭다운 최대 높이 displayFields: [], // 표시할 필드 배열 valueField: 'id', // 값 필드명 textField: 'name', // 텍스트 필드명 searchField: 'name', // 검색 대상 필드명 placeholder: '검색어를 입력하세요', // placeholder 텍스트 noResultsText: '검색 결과가 없습니다', // 검색 결과 없음 텍스트 cssClass: 'xit-dropdown', // CSS 클래스명 disabled: false, // 비활성화 여부 allowMultiple: false // 다중 드롭다운 허용 여부 (기본값: false) }, options); // 필수 옵션 검증 if (!this.options.inputSelector || !this.options.hiddenSelector || !this.options.dataUrl) { throw new Error('XitDropdown: inputSelector, hiddenSelector, dataUrl은 필수입니다.'); } this.$input = $(this.options.inputSelector); this.$hidden = $(this.options.hiddenSelector); this.data = []; this.filteredData = []; this.selectedIndex = -1; this.isOpen = false; this.keyboardNavActive = false; // 키보드 네비게이션 상태 추적 this.dropdownId = 'xit-dropdown-' + Date.now() + Math.random().toString(36).substr(2, 9); // 전역 드롭다운 인스턴스 배열에 현재 인스턴스 등록 window.XitDropdownInstances.push(this); this.init(); } /** * XitDropdown 프로토타입 메소드들 */ XitDropdown.prototype = { /** * 드롭다운 초기화 * 중요한 로직 주석: DOM 구조를 생성하고 데이터를 로드한 후 이벤트를 바인딩한다. */ init: function() { this.createDropdownElement(); this.loadData(); this.bindEvents(); // 비활성화 상태 설정 if (this.options.disabled) { this.$input.prop('disabled', true); } }, /** * 드롭다운 DOM 엘리먼트 생성 * 중요한 로직 주석: input 필드 다음에 드롭다운 컨테이너를 추가한다. */ createDropdownElement: function() { var containerClass = this.options.cssClass + '-container'; var dropdownClass = this.options.cssClass; // 이미 컨테이너가 있다면 제거 this.$input.closest('.' + containerClass).find('.' + dropdownClass).remove(); // 컨테이너가 없다면 생성 if (!this.$input.parent().hasClass(containerClass)) { this.$input.wrap('
'); } // 드롭다운 엘리먼트 생성 var $dropdown = $('
') .attr('id', this.dropdownId) .addClass(dropdownClass) .css({ 'position': 'absolute', 'top': '100%', 'left': '0', 'right': '0', 'background': 'white', 'border': '1px solid #ccc', 'border-top': 'none', 'border-radius': '0 0 4px 4px', 'box-shadow': '0 2px 8px rgba(0, 0, 0, 0.1)', 'z-index': '1000', 'display': 'none', 'max-height': this.options.maxHeight, 'overflow-y': 'auto', 'width': this.options.width }); this.$input.parent().append($dropdown); this.$dropdown = $dropdown; }, /** * 데이터 로드 * 중요한 로직 주석: AJAX로 서버에서 데이터를 가져온다. */ loadData: function() { var self = this; $.ajax({ url: this.options.dataUrl, type: 'POST', success: function(response) { if (response && response.success) { self.data = response.data || []; self.filteredData = self.data; } else { console.error('XitDropdown: 데이터 로드 실패', response); } }, error: function() { console.error('XitDropdown: 데이터 로드 중 오류 발생'); } }); }, /** * 이벤트 바인딩 * 중요한 로직 주석: input 필드와 드롭다운의 각종 이벤트를 처리한다. */ bindEvents: function() { var self = this; // input 클릭 시 드롭다운 열기 this.$input.on('click.xitdropdown', function(e) { e.stopPropagation(); if (!self.options.disabled) { self.showDropdown(); } }); // input 포커스 시 드롭다운 열기 this.$input.on('focus.xitdropdown', function() { if (!self.options.disabled) { self.showDropdown(); } }); // input 키보드 입력 시 실시간 필터링 this.$input.on('input.xitdropdown', function() { if (!self.options.disabled) { self.filterData($(this).val()); self.showDropdown(); self.selectedIndex = -1; } }); // 키보드 네비게이션 this.$input.on('keydown.xitdropdown', function(e) { if (!self.isOpen || self.options.disabled) return; switch (e.keyCode) { case 38: // 위 화살표 e.preventDefault(); self.navigateDropdown(-1); break; case 40: // 아래 화살표 e.preventDefault(); self.navigateDropdown(1); break; case 13: // 엔터 e.preventDefault(); self.selectCurrentItem(); break; case 27: // ESC self.hideDropdown(); break; } }); // 문서 클릭 시 드롭다운 닫기 $(document).on('click.xitdropdown-' + this.dropdownId, function(e) { if (!$(e.target).closest('.' + self.options.cssClass + '-container').length) { self.hideDropdown(); } }); }, /** * 데이터 필터링 * 중요한 로직 주석: 검색어에 따라 데이터를 필터링한다. searchField가 배열인 경우 여러 필드에서 검색한다. */ filterData: function(searchTerm) { var term = searchTerm.trim().toLowerCase(); var searchFields = this.options.searchField; var self = this; if (term === '') { this.filteredData = this.data; } else { this.filteredData = this.data.filter(function(item) { // searchField가 배열인지 확인 if (Array.isArray(searchFields)) { // 배열인 경우: 여러 필드 중 하나라도 검색어가 포함되면 true (OR 조건) return searchFields.some(function(field) { return item[field] && item[field].toString().toLowerCase().indexOf(term) > -1; }); } else { // 단일 필드인 경우: 기존 로직 유지 (하위 호환성) return item[searchFields] && item[searchFields].toString().toLowerCase().indexOf(term) > -1; } }); } }, /** * 드롭다운 표시 * 중요한 로직 주석: 필터링된 데이터를 기반으로 드롭다운을 생성하고 표시한다. */ showDropdown: function() { var html = ''; var self = this; // 다중 드롭다운 허용하지 않는 경우 다른 드롭다운들 모두 닫기 if (!this.options.allowMultiple) { window.XitDropdownInstances.forEach(function(instance) { if (instance !== self && instance.isOpen) { instance.hideDropdown(); } }); } // 중요한 로직 주석: textField에 이미 값이 있다면 해당 값으로 먼저 필터링 처리 var currentInputValue = this.$input.val(); if (currentInputValue && currentInputValue.trim() !== '') { this.filterData(currentInputValue); } else { // textField에 값이 없으면 전체 데이터 표시 this.filteredData = this.data; } if (this.filteredData.length === 0) { html = '
' + this.options.noResultsText + '
'; } else { this.filteredData.forEach(function(item, index) { html += '
'; html += '
' + item[self.options.textField] + '
'; if (self.options.displayFields.length > 0) { html += '
'; self.options.displayFields.forEach(function(field) { if (item[field.name] !== undefined) { var value = field.formatter ? field.formatter(item[field.name]) : item[field.name]; html += ' ' + field.label + ': ' + value + ''; } }); html += '
'; } html += '
'; }); } this.$dropdown.html(html); this.$dropdown.show(); this.isOpen = true; // 클릭 이벤트 바인딩 this.$dropdown.find('.' + this.options.cssClass + '-item:not(.no-results)').on('click', function() { var index = parseInt($(this).attr('data-index')); self.selectItem(index); }); // hover 효과 추가 - 키보드 네비게이션과 상호 배타적으로 동작 this.$dropdown.find('.' + this.options.cssClass + '-item:not(.no-results)').hover( function() { // 키보드 네비게이션이 활성화된 상태가 아닐 때만 hover 효과 적용 if (!self.keyboardNavActive) { $(this).css('background-color', '#e3f2fd'); } }, function() { // 키보드 네비게이션이 활성화된 상태가 아니고 선택되지 않은 항목일 때만 스타일 초기화 if (!self.keyboardNavActive && !$(this).hasClass('selected')) { $(this).css('background-color', ''); } } ); // 마우스가 드롭다운 영역에 들어올 때 키보드 네비게이션 상태 초기화 this.$dropdown.on('mouseenter', function() { if (self.keyboardNavActive) { self.keyboardNavActive = false; self.selectedIndex = -1; self.$dropdown.find('.' + self.options.cssClass + '-item').removeClass('selected').css('background-color', ''); } }); // 중요한 로직 주석: hidden 필드에 값이 있으면 해당 항목을 찾아 자동 하이라이트 및 위치 이동 this.highlightPreselectedItem(); }, /** * 미리 선택된 항목 하이라이트 * 중요한 로직 주석: hidden 필드에 값이 있으면 해당 항목을 찾아 자동으로 하이라이트하고 스크롤 위치 조정 */ highlightPreselectedItem: function() { var self = this; var hiddenValue = this.$hidden.val(); // hidden 필드에 값이 없으면 처리하지 않음 if (!hiddenValue || hiddenValue.trim() === '') { return; } var $items = this.$dropdown.find('.' + this.options.cssClass + '-item:not(.no-results)'); var targetIndex = -1; // valueField와 일치하는 항목 찾기 this.filteredData.forEach(function(item, index) { if (item[self.options.valueField] === hiddenValue) { targetIndex = index; } }); // 일치하는 항목이 있으면 하이라이트 및 스크롤 조정 if (targetIndex >= 0 && targetIndex < $items.length) { this.selectedIndex = targetIndex; var $targetItem = $items.eq(targetIndex); // 기존 하이라이트 제거 $items.removeClass('selected').css('background-color', ''); // 대상 항목 하이라이트 $targetItem.addClass('selected').css('background-color', '#e3f2fd'); // 스크롤 위치 조정 var dropdownHeight = this.$dropdown.height(); var itemHeight = $targetItem.outerHeight(); var scrollTop = this.$dropdown.scrollTop(); var itemTop = $targetItem.position().top + scrollTop; if (itemTop < scrollTop) { this.$dropdown.scrollTop(itemTop); } else if (itemTop + itemHeight > scrollTop + dropdownHeight) { this.$dropdown.scrollTop(itemTop + itemHeight - dropdownHeight); } } }, /** * 드롭다운 숨기기 * 중요한 로직 주석: 드롭다운을 닫을 때 키값 존재 여부에 따라 textField 값을 조정한다. */ hideDropdown: function() { // 중요한 로직 주석: 드롭다운이 닫힐 때 키값이 없으면 텍스트 제거, 키값이 있으면 해당 텍스트로 설정 this.validateAndSetFieldValues(); this.$dropdown.hide(); this.isOpen = false; this.selectedIndex = -1; this.keyboardNavActive = false; // 키보드 네비게이션 상태 초기화 }, /** * 필드 값 검증 및 설정 * 중요한 로직 주석: hidden 필드의 키값에 따라 textField 값을 적절히 조정한다. */ validateAndSetFieldValues: function() { var hiddenValue = this.$hidden.val(); if (!hiddenValue || hiddenValue.trim() === '') { // 키값이 없으면 textField 값 제거 this.$input.val(''); } else { // 키값이 있으면 해당 키값에 맞는 텍스트 값을 찾아서 설정 var textValue = this.getTextValueByKey(hiddenValue); if (textValue) { this.$input.val(textValue); } } }, /** * 키값으로 텍스트 값 찾기 * 중요한 로직 주석: 전체 데이터에서 키값에 해당하는 텍스트 값을 찾는다. */ getTextValueByKey: function(keyValue) { var self = this; var textValue = ''; // 전체 데이터에서 키값과 일치하는 항목 찾기 this.data.forEach(function(item) { if (item[self.options.valueField] === keyValue) { textValue = item[self.options.textField]; return false; // 찾았으면 반복 종료 } }); return textValue; }, /** * 드롭다운 네비게이션 * 중요한 로직 주석: 화살표 키로 드롭다운 항목들을 네비게이션한다. */ navigateDropdown: function(direction) { var $items = this.$dropdown.find('.' + this.options.cssClass + '-item:not(.no-results)'); if ($items.length === 0) return; // 키보드 네비게이션 활성화 상태로 설정 this.keyboardNavActive = true; // 이전 선택 항목 하이라이트 제거 $items.removeClass('selected').css('background-color', ''); // 새로운 인덱스 계산 this.selectedIndex += direction; if (this.selectedIndex < 0) this.selectedIndex = $items.length - 1; if (this.selectedIndex >= $items.length) this.selectedIndex = 0; // 새로운 항목 하이라이트 var $selectedItem = $items.eq(this.selectedIndex); $selectedItem.addClass('selected').css('background-color', '#e3f2fd'); // 스크롤 조정 var dropdownHeight = this.$dropdown.height(); var itemHeight = $selectedItem.outerHeight(); var scrollTop = this.$dropdown.scrollTop(); var itemTop = $selectedItem.position().top + scrollTop; if (itemTop < scrollTop) { this.$dropdown.scrollTop(itemTop); } else if (itemTop + itemHeight > scrollTop + dropdownHeight) { this.$dropdown.scrollTop(itemTop + itemHeight - dropdownHeight); } }, /** * 현재 선택된 항목 선택 */ selectCurrentItem: function() { if (this.selectedIndex >= 0 && this.selectedIndex < this.filteredData.length) { this.selectItem(this.selectedIndex); } }, /** * 항목 선택 * 중요한 로직 주석: 선택된 항목의 값을 input과 hidden 필드에 설정한다. */ selectItem: function(index) { if (index >= 0 && index < this.filteredData.length) { var selectedItem = this.filteredData[index]; this.$input.val(selectedItem[this.options.textField]); this.$hidden.val(selectedItem[this.options.valueField]); this.hideDropdown(); // 선택 이벤트 발생 if (this.options.onSelect && typeof this.options.onSelect === 'function') { this.options.onSelect(selectedItem); } } }, /** * 드롭다운 비활성화/활성화 */ setDisabled: function(disabled) { this.options.disabled = disabled; this.$input.prop('disabled', disabled); if (disabled) { this.hideDropdown(); } }, /** * 드롭다운 파괴 */ destroy: function() { // 전역 인스턴스 배열에서 현재 인스턴스 제거 var index = window.XitDropdownInstances.indexOf(this); if (index > -1) { window.XitDropdownInstances.splice(index, 1); } // 이벤트 제거 this.$input.off('.xitdropdown'); $(document).off('.xitdropdown-' + this.dropdownId); // DOM 제거 this.$dropdown.remove(); // 컨테이너가 빈 경우 원래 상태로 복원 var $container = this.$input.parent(); if ($container.hasClass(this.options.cssClass + '-container') && $container.children().length === 1) { this.$input.unwrap(); } } }; /** * 팝업 창 열기 공통함수 * 중요로직: 팝업창을 화면 중앙에 위치시켜 열기, 위치 조정 가능 * @param {string} url - 팝업에서 열 URL * @param {number} width - 팝업 너비 (기본값: 600) * @param {number} height - 팝업 높이 (기본값: 600) * @param {string} windowName - 윈도우 이름 (기본값: 'popup') * @param {number} leftOffset - 중앙 위치에서 좌우 조정값 (기본값: 0, 음수면 왼쪽, 양수면 오른쪽) * @param {number} topOffset - 중앙 위치에서 상하 조정값 (기본값: 0, 음수면 위쪽, 양수면 아래쪽) * @returns {Window} 열린 팝업 창 객체 */ function openPopup(url, width, height, windowName, leftOffset, topOffset) { // 기본값 설정 width = width || 600; height = height || 600; windowName = windowName || 'popup'; leftOffset = leftOffset || 0; topOffset = topOffset || 0; // 화면 중앙 위치 계산 var left = Math.max(0, (window.screen.availLeft + (window.screen.availWidth - width) / 2)); var top = Math.max(0, (window.screen.availTop + (window.screen.availHeight - height) / 2)); // 중요로직: 최종 계산된 중앙 위치에 조정값 적용 left = Math.max(0, left + leftOffset); top = Math.max(0, top + topOffset); // 팝업 옵션 설정 var popupOptions = 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes'; // 팝업 창 열기 return window.open(url, windowName, popupOptions); }