|
|
|
|
@ -751,3 +751,355 @@ function closeChildPopupsAndSelf(childPopups) {
|
|
|
|
|
return childPopups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 // 비활성화 여부
|
|
|
|
|
}, 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.dropdownId = 'xit-dropdown-' + Date.now() + Math.random().toString(36).substr(2, 9);
|
|
|
|
|
|
|
|
|
|
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('<div class="' + containerClass + '" style="position: relative; display: inline-block;"></div>');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 드롭다운 엘리먼트 생성
|
|
|
|
|
var $dropdown = $('<div>')
|
|
|
|
|
.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();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 데이터 필터링
|
|
|
|
|
* 중요한 로직 주석: 검색어에 따라 데이터를 필터링한다.
|
|
|
|
|
*/
|
|
|
|
|
filterData: function(searchTerm) {
|
|
|
|
|
var term = searchTerm.trim().toLowerCase();
|
|
|
|
|
var searchField = this.options.searchField;
|
|
|
|
|
|
|
|
|
|
if (term === '') {
|
|
|
|
|
this.filteredData = this.data;
|
|
|
|
|
} else {
|
|
|
|
|
this.filteredData = this.data.filter(function(item) {
|
|
|
|
|
return item[searchField] && item[searchField].toString().toLowerCase().indexOf(term) > -1;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 드롭다운 표시
|
|
|
|
|
* 중요한 로직 주석: 필터링된 데이터를 기반으로 드롭다운을 생성하고 표시한다.
|
|
|
|
|
*/
|
|
|
|
|
showDropdown: function() {
|
|
|
|
|
var html = '';
|
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
|
|
if (this.filteredData.length === 0) {
|
|
|
|
|
html = '<div class="' + this.options.cssClass + '-item no-results" style="padding: 10px 12px; color: #666; font-style: italic; text-align: center; cursor: default;">' + this.options.noResultsText + '</div>';
|
|
|
|
|
} else {
|
|
|
|
|
this.filteredData.forEach(function(item, index) {
|
|
|
|
|
html += '<div class="' + self.options.cssClass + '-item" data-index="' + index + '" data-value="' + item[self.options.valueField] + '" style="padding: 10px 12px; cursor: pointer; border-bottom: 1px solid #f5f5f5; transition: background-color 0.2s;">';
|
|
|
|
|
html += ' <div class="' + self.options.cssClass + '-main" style="font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px;">' + item[self.options.textField] + '</div>';
|
|
|
|
|
|
|
|
|
|
if (self.options.displayFields.length > 0) {
|
|
|
|
|
html += ' <div class="' + self.options.cssClass + '-details" style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px;">';
|
|
|
|
|
self.options.displayFields.forEach(function(field) {
|
|
|
|
|
if (item[field.name] !== undefined) {
|
|
|
|
|
var value = field.formatter ? field.formatter(item[field.name]) : item[field.name];
|
|
|
|
|
html += ' <span class="' + self.options.cssClass + '-value" style="color: #666; background-color: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-size: 11px;">' + field.label + ': ' + value + '</span>';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
html += ' </div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
$(this).css('background-color', '#f8f9fa');
|
|
|
|
|
},
|
|
|
|
|
function() {
|
|
|
|
|
if (!$(this).hasClass('selected')) {
|
|
|
|
|
$(this).css('background-color', '');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 드롭다운 숨기기
|
|
|
|
|
*/
|
|
|
|
|
hideDropdown: function() {
|
|
|
|
|
this.$dropdown.hide();
|
|
|
|
|
this.isOpen = false;
|
|
|
|
|
this.selectedIndex = -1;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 드롭다운 네비게이션
|
|
|
|
|
* 중요한 로직 주석: 화살표 키로 드롭다운 항목들을 네비게이션한다.
|
|
|
|
|
*/
|
|
|
|
|
navigateDropdown: function(direction) {
|
|
|
|
|
var $items = this.$dropdown.find('.' + this.options.cssClass + '-item:not(.no-results)');
|
|
|
|
|
if ($items.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// 이전 선택 항목 하이라이트 제거
|
|
|
|
|
$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() {
|
|
|
|
|
// 이벤트 제거
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|