// 기본 실행 (페이지 로드시 전체 document) class IpAutoComplete { /** * @param {HTMLElement|string} input * @param {HTMLElement|string} panel * @param {string[]} ipArray * @param {object} opts */ constructor(input, panel, ipArray = [], opts = {}) { this.inputEl = typeof input === 'string' ? document.querySelector(input) : input; this.panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel; if (!this.inputEl || !this.panelEl) throw new Error('Invalid input/panel element'); this.opts = Object.assign({ validateIPv4: true, enforceListOnly: false, previewOnFocus: true, // 포커스 시 상위 N개 미리보기 previewCount: 30, // 미리보기 개수 resultLimit: 1000 // 너무 많을 때 렌더 최대치(성능 안전장치) }, opts); this.errorEl = document.getElementById('ipError'); this.ipv4Re = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/; this.setArray(ipArray); // 상태 this.activeIndex = -1; // 이벤트 this.onInput = this._handleInput.bind(this); this.onChange = this._handleChange.bind(this); this.onFocus = this._handleFocus.bind(this); this.onKeydown = this._handleKeydown.bind(this); this.onClickOutside = this._handleClickOutside.bind(this); this.inputEl.addEventListener('input', this.onInput); this.inputEl.addEventListener('change', this.onChange); this.inputEl.addEventListener('focus', this.onFocus); this.inputEl.addEventListener('keydown', this.onKeydown); document.addEventListener('mousedown', this.onClickOutside); } destroy(){ this.inputEl.removeEventListener('input', this.onInput); this.inputEl.removeEventListener('change', this.onChange); this.inputEl.removeEventListener('focus', this.onFocus); this.inputEl.removeEventListener('keydown', this.onKeydown); document.removeEventListener('mousedown', this.onClickOutside); this.hidePanel(); } setArray(arr){ this.ipArray = Array.from(new Set((arr||[]).filter(Boolean))); } _handleInput(e){ const q = (e.target.value||'').trim(); this._filterAndRender(q); this._checkIPv4(q); } _handleChange(){ const v = (this.inputEl.value||'').trim(); this._checkIPv4(v); if (this.opts.enforceListOnly && v && !this.ipArray.includes(v)) { this._setError('❌ 목록에 없는 IP입니다.'); } } _handleFocus(){ if (!this.inputEl.value && this.opts.previewOnFocus) { this._renderPanel(this.ipArray.slice(0, this.opts.resultLimit)); this.showPanel(); } } _handleKeydown(e){ if (this.panelEl.hidden) return; const items = this._items(); if (!items.length) return; if (e.key === 'ArrowDown'){ e.preventDefault(); this._move(1); } else if (e.key === 'ArrowUp'){ e.preventDefault(); this._move(-1); } else if (e.key === 'Enter'){ if (this.activeIndex >= 0){ e.preventDefault(); this._pick(items[this.activeIndex].dataset.value); } } else if (e.key === 'Escape'){ this.hidePanel(); } } _handleClickOutside(e){ if (!this.inputEl.closest('.ac-wrap')?.contains(e.target)) this.hidePanel(); } _filterAndRender(query){ if (!query){ this.hidePanel(); return; } const q = query.toLowerCase(); let results = this.ipArray.filter(ip => ip.toLowerCase().includes(q)); if (this.opts.resultLimit > 0) results = results.slice(0, this.opts.resultLimit); if (results.length){ this._renderPanel(results); this.showPanel(); } else { this.hidePanel(); } } _renderPanel(list){ this.panelEl.innerHTML = ''; this.activeIndex = -1; const frag = document.createDocumentFragment(); list.forEach((ip) => { const item = document.createElement('div'); item.className = 'ac-item'; item.textContent = ip; item.dataset.value = ip; item.addEventListener('mousedown', (ev) => { ev.preventDefault(); this._pick(ip); }); frag.appendChild(item); }); this.panelEl.appendChild(frag); } _items(){ return Array.from(this.panelEl.querySelectorAll('.ac-item')); } _move(delta){ const items = this._items(); if (!items.length) return; this.activeIndex = (this.activeIndex + delta + items.length) % items.length; items.forEach(el => el.classList.remove('active')); const el = items[this.activeIndex]; el.classList.add('active'); el.scrollIntoView({ block: 'nearest' }); } _pick(value){ this.inputEl.value = value; this.hidePanel(); this._checkIPv4(value); } showPanel(){ this.panelEl.hidden = false; } hidePanel(){ this.panelEl.hidden = true; this.activeIndex = -1; } _checkIPv4(val){ if (!this.opts.validateIPv4 || !val){ this._setError(''); return; } this._setError(this.ipv4Re.test(val) ? '' : '❌ IPv4 형식이 아닙니다.'); } _setError(text){ if (this.errorEl) this.errorEl.textContent = text || ''; } } /* ipData(JSON) 읽기: (이미 페이지에 있다면) */ function parseEmbeddedJson(id){ const el=document.getElementById(id); if(!el) return []; let txt=el.textContent||''; txt = txt .replace(/\/\*[\s\S]*?\*\//g,'') .replace(/(^|[^:])\/\/.*$/mg,'$1') .replace(/,\s*([}\]])/g,'$1'); try { const data=JSON.parse(txt); return Array.isArray(data)?data:[]; } catch(e){ console.error('ipData parse error',e); return []; } } /* 초기화 */ document.addEventListener('DOMContentLoaded', () => { // ipData