/**
* @file avokado-nudge.js
* Remote-config banner replacement and Q&A keyword nudge (toast) for apartment ERP embeds.
*/
(function (global) {
'use strict';
var SDK_VERSION = '1.0.1';
var TOAST_ID = '__avn_toast__';
var STYLE_ID = '__avn_style__';
var BANNER_TARGET = '.bg_area2_left';
var VEHICLE_COUNT_ID = 'avokado-banner-vehicle-count';
var _vehicleCountObs = null;
var DEFAULTS = {
configUrl: null,
configTtl: 300,
bannerTarget: BANNER_TARGET,
onBannerLoad: null,
onDetect: null,
onAppClick: null,
};
var _cfg = {};
var _remoteCfg = null;
var _debounceT = null;
var _dismissed = false;
var _lastKw = '';
var _toastEl = null;
var _inputEl = null;
var _qnaTriggerKeyword = null;
var _qnaImpressionSent = false;
var _moveActionBound = false;
var _inputHandler = null;
var _boundInputEl = null;
var TOAST_CSS = [
'#' + TOAST_ID + '{',
' position:fixed;bottom:24px;right:24px;width:340px;',
' background:#fff;border:1.5px solid #5b9bd5;border-radius:12px;',
' box-shadow:0 6px 24px rgba(0,0,0,.15);padding:18px 20px 16px;',
' z-index:99999;font-family:"Noto Sans KR","Apple SD Gothic Neo",sans-serif;',
' font-size:15px;line-height:1.6;',
' opacity:0;transform:translateY(10px);pointer-events:none;',
' transition:opacity .25s,transform .25s;',
'}',
'#' + TOAST_ID + '.avn-show{opacity:1;transform:translateY(0);pointer-events:auto;}',
'#' + TOAST_ID + ' .avn-hd{display:flex;align-items:center;gap:8px;',
' font-weight:700;color:#0d3d8c;margin-bottom:10px;font-size:16px;}',
'#' + TOAST_ID + ' .avn-ico{font-size:20px;line-height:1;}',
'#' + TOAST_ID + ' .avn-list{list-style:none;margin:0 0 12px;padding:0;}',
'#' +
TOAST_ID +
' .avn-list-item a{display:flex;align-items:center;justify-content:space-between;',
' padding:10px 6px;font-size:15px;color:#1D6ADD;text-decoration:none;',
' border-bottom:1px solid #f0f0f0;border-radius:4px;transition:background .1s;}',
'#' + TOAST_ID + ' .avn-list-item a::after{content:"›";font-size:18px;color:#88bef5;}',
'#' + TOAST_ID + ' .avn-list-item:last-child a{border-bottom:none;}',
'#' + TOAST_ID + ' .avn-list-item a:hover{background:#e8f2ff;}',
'#' + TOAST_ID + ' .avn-ch{width:100%;background:#f5f5f5;color:#444;',
' border:1px solid #ddd;border-radius:10px;padding:14px 0;font-size:15px;',
' cursor:pointer;font-family:inherit;transition:background .15s;}',
'#' + TOAST_ID + ' .avn-ch:hover{background:#ebebeb;}',
'#' + TOAST_ID + ' .avn-kw{display:inline-block;background:#e8f2ff;color:#1558b0;',
' border:1px solid #88bef5;border-radius:10px;font-size:13px;',
' padding:2px 10px;margin-left:2px;font-weight:500;}',
'#' + TOAST_ID + ' .avn-body{color:#333;font-size:15px;margin-bottom:14px;line-height:1.7;}',
'#' + TOAST_ID + ' .avn-btns{display:flex;gap:8px;}',
'#' + TOAST_ID + ' .avn-cta{flex:1;background:#1D6ADD;color:#fff;border:none;',
' border-radius:10px;padding:14px 0;font-size:16px;font-weight:700;',
' cursor:pointer;font-family:inherit;transition:background .15s;}',
'#' + TOAST_ID + ' .avn-cta:hover{background:#155fc4;}',
'#' + TOAST_ID + ' .avn-sub{background:#f5f5f5;color:#555;border:1px solid #ddd;',
' border-radius:10px;padding:14px 14px;font-size:16px;cursor:pointer;',
' font-family:inherit;transition:background .15s;}',
'#' + TOAST_ID + ' .avn-sub:hover{background:#ebebeb;}',
'#' + TOAST_ID + ' .avn-x{position:absolute;top:10px;right:12px;font-size:18px;',
' color:#aaa;cursor:pointer;background:none;border:none;padding:4px;',
' font-family:inherit;line-height:1;}',
'#' + TOAST_ID + ' .avn-x:hover{color:#555;}',
].join('\n');
function _loadRemoteConfig(url, callback) {
var cacheKey = '__avn_cfg__';
var cacheTime = '__avn_cfg_t__';
var ttl = (_cfg.configTtl || 300) * 1000;
try {
var cached = sessionStorage.getItem(cacheKey);
var ts = parseInt(sessionStorage.getItem(cacheTime) || '0', 10);
if (cached && Date.now() - ts < ttl) {
return callback(null, JSON.parse(cached));
}
} catch (e) {}
var xhr = new XMLHttpRequest();
xhr.open('GET', url + '?_=' + Date.now(), true);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
try {
sessionStorage.setItem(cacheKey, JSON.stringify(data));
sessionStorage.setItem(cacheTime, String(Date.now()));
} catch (e) {}
callback(null, data);
} catch (e) {
callback(new Error('Config JSON parse error: ' + e.message), null);
}
} else {
callback(new Error('Config load failed: HTTP ' + xhr.status), null);
}
};
xhr.send();
}
function _vehicleCountHostEmpty(text) {
if (text == null) return true;
var s = String(text)
.replace(/\uFEFF/g, '')
.replace(/\u00A0/g, '')
.replace(/\u2007/g, '')
.replace(/\u202F/g, '')
.replace(/\s/g, '');
return s.length === 0;
}
function _vehicleCountRow(countEl) {
var row = countEl.parentElement;
while (row && row !== document.body) {
if (_hasClass(row, 'avk-tb-info-count')) return row;
row = row.parentElement;
}
return countEl.parentElement;
}
function _syncVehicleCountPlaceholder(countEl) {
if (!countEl) return;
var row = _vehicleCountRow(countEl);
var raw = countEl.textContent || '';
if (_vehicleCountHostEmpty(raw)) {
countEl.textContent = '미사용';
if (row) row.classList.add('avk-vehicle-placeholder');
return;
}
if (raw === '미사용') {
if (row) row.classList.add('avk-vehicle-placeholder');
return;
}
if (row) row.classList.remove('avk-vehicle-placeholder');
}
function _bindVehicleCountPlaceholderFix(bannerRoot) {
if (!bannerRoot) return;
if (_vehicleCountObs) {
try {
_vehicleCountObs.disconnect();
} catch (e) {}
_vehicleCountObs = null;
}
function findEl() {
if (bannerRoot.querySelector) {
var q = bannerRoot.querySelector('#' + VEHICLE_COUNT_ID);
if (q) return q;
}
return document.getElementById(VEHICLE_COUNT_ID);
}
var rafPending = false;
function runSync() {
rafPending = false;
_syncVehicleCountPlaceholder(findEl());
}
function schedule() {
if (rafPending) return;
rafPending = true;
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(runSync);
} else {
setTimeout(runSync, 0);
}
}
runSync();
try {
_vehicleCountObs = new MutationObserver(schedule);
_vehicleCountObs.observe(bannerRoot, {
childList: true,
subtree: true,
characterData: true,
});
} catch (e) {
_warn('방문차량 건수 placeholder 보정 observer 설정 실패');
}
}
function _renderBanner(bannerCfg) {
var target = document.querySelector(_cfg.bannerTarget);
if (!target) {
_warn('배너 대상 요소를 찾을 수 없습니다: ' + _cfg.bannerTarget);
return;
}
var type = (bannerCfg && bannerCfg.type) || 'default';
function fireBannerLoad() {
if (typeof _cfg.onBannerLoad === 'function') {
_cfg.onBannerLoad(type, target);
}
}
if (type === 'hidden') {
target.style.display = 'none';
fireBannerLoad();
return;
}
if (type === 'html') {
if (bannerCfg.url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', bannerCfg.url + '?_=' + Date.now(), true);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
target.innerHTML = xhr.responseText;
_bindVehicleCountPlaceholderFix(target);
fireBannerLoad();
} else {
_warn('배너 HTML 로딩 실패: HTTP ' + xhr.status);
}
};
xhr.send();
return;
}
if (bannerCfg.html) {
target.innerHTML = bannerCfg.html;
_bindVehicleCountPlaceholderFix(target);
fireBannerLoad();
return;
}
}
fireBannerLoad();
}
function _injectStyle() {
if (document.getElementById(STYLE_ID)) return;
var el = document.createElement('style');
el.id = STYLE_ID;
el.textContent = TOAST_CSS;
document.head.appendChild(el);
}
function _buildToast() {
if (document.getElementById(TOAST_ID)) return;
var div = document.createElement('div');
div.id = TOAST_ID;
div.setAttribute('role', 'alert');
div.setAttribute('aria-live', 'polite');
div.innerHTML = [
'',
'
',
' 🥑',
' ',
'
',
'',
].join('');
document.body.appendChild(div);
_toastEl = div;
div.addEventListener('click', function (e) {
if (e.isTrusted === false) return;
var t = e.target || e.srcElement;
while (t && t.nodeType !== 1) t = t.parentNode;
while (t && t !== div) {
if (
t.nodeType === 1 &&
(t.tagName || '').toUpperCase() === 'A' &&
t.href &&
t.parentNode &&
_hasClass(t.parentNode, 'avn-list-item')
) {
if (_qnaTriggerKeyword) {
trackEvent('apt_qna_help_tooltip_item_click', { trigger_keyword: _qnaTriggerKeyword });
}
return;
}
if (t.id === '__avn_x__') {
if (_qnaTriggerKeyword) {
trackEvent('apt_qna_help_tooltip_close_btn_click', {
trigger_keyword: _qnaTriggerKeyword,
});
}
_dismiss();
return;
}
if (_hasClass(t, 'avn-sub')) {
_dismiss();
return;
}
if (_hasClass(t, 'avn-cta')) {
if (typeof _cfg.onAppClick === 'function') _cfg.onAppClick(_lastKw);
window.open((_remoteCfg && _remoteCfg.appUrl) || '#', '_blank');
_dismiss();
return;
}
if (_hasClass(t, 'avn-ch')) {
_openChannelTalk();
return;
}
t = t.parentNode;
}
});
}
function _renderBody(kwObj) {
var word = typeof kwObj === 'string' ? kwObj : kwObj.word;
var contents =
typeof kwObj === 'object' && kwObj.contents && kwObj.contents.length ? kwObj.contents : null;
var toastCfg = (_remoteCfg && _remoteCfg.nudge && _remoteCfg.nudge.toast) || {};
if (contents) {
var items = '';
for (var i = 0; i < contents.length; i++) {
items +=
'' +
'' +
_esc(contents[i].title) +
'';
}
return '';
}
return [
'',
' "' + _esc(word) + '" ',
' ' + _esc(toastCfg.body || '아보카도 앱에서 더 빠르게 처리됩니다.'),
'
',
'',
' ',
' ',
'
',
].join('');
}
function trackEvent(name, params) {
if (typeof global.gtag === 'function') {
global.gtag('event', name, params || {});
}
}
function resolveQnaTriggerKeyword(kwObj) {
if (kwObj == null || kwObj === '') return null;
if (typeof kwObj === 'object' && kwObj.trigger_keyword) {
var fixed = String(kwObj.trigger_keyword).trim();
if (fixed === 'vote' || fixed === 'fire_check') return fixed;
}
var word = typeof kwObj === 'string' ? kwObj : kwObj.word != null ? String(kwObj.word) : '';
word = String(word);
if (word.indexOf('소방') !== -1) return 'fire_check';
if (word.indexOf('투표') !== -1) return 'vote';
return null;
}
function getButtonType(el) {
if (!el) return null;
var text = (el.textContent || '').trim();
if (text.indexOf('전자투표') !== -1) return 'vote';
if (text.indexOf('소방') !== -1) return 'fire_check';
if (text.indexOf('사용 방법') !== -1) return 'guide';
if (text.indexOf('입주민 등록') !== -1) return 'resident_register';
if (text.indexOf('방문차량') !== -1) return 'visitor_parking';
return null;
}
function resolveBannerButtonType(el) {
if (!el || !el.getAttribute) return null;
var raw = el.getAttribute('data-analytics');
if (raw && String(raw).trim()) return String(raw).trim();
var tag = (el.tagName || '').toUpperCase();
if (tag !== 'BUTTON' && tag !== 'A') return null;
return getButtonType(el);
}
function _bindMoveAction() {
if (_moveActionBound) return;
_moveActionBound = true;
document.addEventListener('click', function (e) {
if (e.isTrusted === false) return;
var toastRoot = document.getElementById(TOAST_ID);
if (
toastRoot &&
e.target &&
typeof toastRoot.contains === 'function' &&
toastRoot.contains(e.target)
) {
return;
}
var el = e.target;
var bannerTracked = false;
while (el && el !== document) {
if (!bannerTracked) {
var buttonType = resolveBannerButtonType(el);
if (buttonType) {
trackEvent('apt_banner_btn_click', { button_type: buttonType });
bannerTracked = true;
}
}
if (el.hasAttribute && el.getAttribute('data-params') === 'resident_list') {
e.preventDefault();
var target = document.getElementById('VIN66100');
if (target) {
target.click();
} else {
console.warn('[AvokadoNudge] #VIN66100 요소를 찾을 수 없음');
}
return;
}
if (el.hasAttribute && el.hasAttribute('data-move')) {
e.preventDefault();
var type = el.getAttribute('data-move');
if (typeof window.MoveAvo === 'function') {
window.MoveAvo(type);
} else {
console.warn('[AvokadoNudge] MoveAvo 함수 없음:', type);
}
return;
}
if (el.hasAttribute && el.hasAttribute('data-action')) {
e.preventDefault();
var action = el.getAttribute('data-action');
var params = el.getAttribute('data-params');
if (typeof window[action] === 'function') {
window[action](params);
} else {
try {
new Function(action + '(' + JSON.stringify(params) + ')')();
} catch (e) {
console.warn('[AvokadoNudge] 실행 실패:', action);
}
}
return;
}
el = el.parentNode;
}
});
}
function _showToast(kwObj) {
if (!_toastEl) return;
var word = typeof kwObj === 'string' ? kwObj : kwObj.word;
var contents =
typeof kwObj === 'object' && kwObj.contents && kwObj.contents.length ? kwObj.contents : null;
_lastKw = word;
var tk = resolveQnaTriggerKeyword(kwObj);
if (tk) {
if (!_qnaImpressionSent || tk !== _qnaTriggerKeyword) {
trackEvent('apt_qna_help_tooltip_impression', { trigger_keyword: tk });
_qnaImpressionSent = true;
}
_qnaTriggerKeyword = tk;
} else {
_qnaTriggerKeyword = null;
_qnaImpressionSent = false;
}
var hdEl = document.getElementById('__avn_hdtxt__');
var toastCfg = (_remoteCfg && _remoteCfg.nudge && _remoteCfg.nudge.toast) || {};
if (hdEl) {
hdEl.textContent = contents
? '"' + word + '" 관련 도움말'
: toastCfg.title || '아보카도 관련 문의인가요?';
}
var bodyEl = document.getElementById('__avn_body__');
if (bodyEl) bodyEl.innerHTML = _renderBody(kwObj);
requestAnimationFrame(function () {
_toastEl.classList.add('avn-show');
});
if (typeof _cfg.onDetect === 'function') _cfg.onDetect(word);
_slackNotify(word);
}
function _openChannelTalk() {
var chCfg = _remoteCfg && _remoteCfg.channelTalk;
if (!chCfg) return;
if (typeof global.ChannelIO === 'function') {
global.ChannelIO('openChat');
} else if (chCfg.fallbackUrl) {
window.open(chCfg.fallbackUrl, '_blank');
}
_dismiss();
}
function _hideToast() {
if (_toastEl) _toastEl.classList.remove('avn-show');
_qnaTriggerKeyword = null;
_qnaImpressionSent = false;
}
function _dismiss() {
_hideToast();
setTimeout(function () {
_dismissed = false;
}, 2000);
}
function _detectKeyword(text, keywords) {
for (var i = 0; i < keywords.length; i++) {
var kw = keywords[i];
var word = typeof kw === 'string' ? kw : kw.word;
if (text.indexOf(word) !== -1) return kw;
}
return null;
}
function _onInput(keywords) {
return function (e) {
if (_dismissed) return;
clearTimeout(_debounceT);
var el = e.target || e.srcElement;
_debounceT = setTimeout(
function () {
var found = _detectKeyword(el.value || '', keywords);
if (found) {
_showToast(found);
} else {
_hideToast();
_dismissed = false;
}
},
(_remoteCfg && _remoteCfg.nudge && _remoteCfg.nudge.debounce) || 400,
);
};
}
function _bindInput(nudgeCfg) {
var targetId = (nudgeCfg && nudgeCfg.targetId) || 'txtCnts';
var keywords = (nudgeCfg && nudgeCfg.keywords) || [];
var el = document.getElementById(targetId);
if (!el) {
_warn('Nudge 대상 요소를 찾을 수 없습니다: #' + targetId);
return;
}
if (_inputHandler && _boundInputEl) {
try {
_boundInputEl.removeEventListener('input', _inputHandler);
_boundInputEl.removeEventListener('keyup', _inputHandler);
} catch (err) {}
}
_boundInputEl = el;
_inputEl = el;
_inputHandler = _onInput(keywords);
_inputEl.addEventListener('input', _inputHandler);
_inputEl.addEventListener('keyup', _inputHandler);
}
function _slackNotify(keyword) {
var slackCfg = _remoteCfg && _remoteCfg.slack;
if (!slackCfg || !slackCfg.enabled || !slackCfg.webhookUrl) return;
var payload = JSON.stringify({
text: '[Avokado Nudge] 키워드 감지: *' + keyword + '* — ' + (document.title || location.href),
});
var xhr = new XMLHttpRequest();
xhr.open('POST', slackCfg.webhookUrl, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(payload);
}
function _bootstrap(remoteCfg) {
_remoteCfg = remoteCfg;
if (remoteCfg.enabled === false) {
_log('SDK disabled via remote config.');
return;
}
if (remoteCfg.banner) {
_renderBanner(remoteCfg.banner);
}
var nudgeCfg = remoteCfg.nudge;
if (nudgeCfg && nudgeCfg.enabled !== false) {
_injectStyle();
_buildToast();
_bindInput(nudgeCfg);
}
_bindMoveAction();
}
function _hasClass(el, cls) {
return !!(el && el.className && el.className.indexOf(cls) !== -1);
}
function _merge(a, b) {
var r = {};
for (var k in a) r[k] = a[k];
if (b) for (var k in b) r[k] = b[k];
return r;
}
function _esc(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function _log(msg) {
if (global.console) console.log('[AvokadoNudge] ' + msg);
}
function _warn(msg) {
if (global.console) console.warn('[AvokadoNudge] ' + msg);
}
function _ready(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
/**
* @typedef {object} AvokadoNudgeInitOptions
* @property {string} [configUrl] Remote JSON config URL.
* @property {number} [configTtl] SessionStorage cache TTL in seconds.
* @property {string} [bannerTarget] Banner mount selector (default {@code .bg_area2_left}).
* @property {function(string, HTMLElement)} [onBannerLoad] Banner type and mount element.
* @property {function(string)} [onDetect] Keyword detected in the bound textarea.
* @property {function(string)} [onAppClick] Fallback toast primary CTA clicked.
*/
/**
* @namespace AvokadoNudge
*/
var AvokadoNudge = {
version: SDK_VERSION,
/**
* 원격 JSON 설정을 읽어 배너와 키워드 넛지를 붙입니다. {@code configUrl}이 없으면 내장 기본값으로만 동작합니다.
* DOM 준비 후 실행되므로, 스크립트는 {@code