/************************************************************************** * ¹®ÀÚ³ª¶ó °ø¿ë Progress / Loading Layer * * ÆÄÀϸí : progress_layer.js * ¹öÀü : Ver 0.25 * ¼öÁ¤ÀÏ : 2026.07.27 * * »ç¿ë ±â´É * ------------------------------------------------------------------------ * ProgressLayer.loading(options) * - ÁøÇà·üÀ» ¾Ë ¼ö ¾ø´Â ÀÛ¾÷¿¡ »ç¿ë * - count¸¦ Àü´ÞÇϸé ó¸® °Ç¼ö¸¦ Ç¥½Ã * - ¿Ï·á ½Ã ProgressLayer.complete()¸¦ È£ÃâÇϸé 100% ÁøÇà¹Ù·Î Ç¥½Ã * * ProgressLayer.progress(options) * - ÁøÇà·ü ¶Ç´Â ó¸® °Ç¼ö¸¦ Ç¥½ÃÇÏ´Â ÀÛ¾÷¿¡ »ç¿ë * - progressTypeÀº 'percent' ¶Ç´Â 'count' * - estimatedSeconds¸¦ Á÷Á¢ Àü´ÞÇϸé ÇØ´ç ½Ã°£À» »ç¿ë * - estimatedSeconds¸¦ »ý·«Çϰí count, sampleCount, sampleSeconds¸¦ * Àü´ÞÇÏ¸é ½ÇÁ¦ ÃøÁ¤°ªÀ» ±âÁØÀ¸·Î ¿¹»ó ¼Ò¿ä½Ã°£À» ÀÚµ¿ °è»ê * - count°¡ 1,000°Ç ÀÌÇÏÀÌ¸é ¿¹»ó ¼Ò¿ä½Ã°£À» 'Áï½Ã'·Î Ç¥½Ã * - ¼Ò·® ÀÛ¾÷Àº 25%¿¡¼­ ´ë±âÇϰí, ÀÏ¹Ý ÀÛ¾÷Àº ¿¹»ó½Ã°£ µ¿¾È 100%±îÁö ÁøÇà * - ÀÏ¹Ý ÀÛ¾÷Àº 95~98%¿¡¼­ ÀÎÀ§ÀûÀ¸·Î ¸ØÃßÁö ¾ÊÀ½ * * ProgressLayer.update(options) * - ÁøÇà ÁßÀÎ Á¦¸ñ, ¼³¸í, °Ç¼ö, ÁøÇà·ü µîÀ» °»½Å * * ProgressLayer.setPercent(percent) * - ÁøÇà·üÀ» ÆÛ¼¾Æ® ±âÁØÀ¸·Î Á÷Á¢ °»½Å * * ProgressLayer.complete(options) * - ÀÛ¾÷ ¿Ï·á »óÅ·Πº¯°æ * - ÁøÇà¹Ù¸¦ 100%·Î Ç¥½Ã * - ½ÇÁ¦ ¼Ò¿ä½Ã°£ Ç¥½Ã * - È®ÀÎ ¹öư Ȱ¼ºÈ­ * * ProgressLayer.fail(options) * - ÀÛ¾÷ ½ÇÆÐ »óÅ·Πº¯°æ * - ¿À·ù ¸Þ½ÃÁö¸¦ »¡°£»öÀ¸·Î Ç¥½Ã * - È®ÀÎ ¹öư Ȱ¼ºÈ­ * * ProgressLayer.close() * - ÁøÇà⠴ݱâ * * ±âº» »ç¿ë ¿¹Á¦ * ------------------------------------------------------------------------ * * 1. ÁøÇà·üÀ» ¾Ë ¼ö ¾ø´Â ÀÛ¾÷ * * ProgressLayer.loading({ * title: 'µ¥ÀÌÅÍ Á¶È¸', * message: 'µ¥ÀÌÅ͸¦ Á¶È¸Çϰí ÀÖ½À´Ï´Ù.' * }); * * 2. °Ç¼ö¸¦ Ç¥½ÃÇÏ´Â Loading * * ProgressLayer.loading({ * title: '¿¢¼¿ ÆÄÀÏ ´Ù¿î·Îµå', * message: '¿¢¼¿ ÆÄÀÏÀ» »ý¼ºÇϰí ÀÖ½À´Ï´Ù.', * countLabel: '´Ù¿î·Îµå °Ç¼ö', * count: row * }); * * 3. ¿¹»ó½Ã°£ÀÌ ÀÖ´Â Progress * * ProgressLayer.progress({ * title: '¿¢¼¿ ÆÄÀÏ ´Ù¿î·Îµå', * message: '¿¢¼¿ ÆÄÀÏÀ» »ý¼ºÇϰí ÀÖ½À´Ï´Ù.', * countLabel: '´Ù¿î·Îµå °Ç¼ö', * count: row, * sampleCount: 100000, * sampleSeconds: 5, * progressType: 'count' * }); * * - À§ ¿¹Á¦´Â 100,000°Ç 󸮿¡ 5Ãʰ¡ °É¸° ½ÇÁ¦ ÃøÁ¤°ªÀ» ±âÁØÀ¸·Î * ÇöÀç countÀÇ ¿¹»ó ¼Ò¿ä½Ã°£À» ³»ºÎ¿¡¼­ ÀÚµ¿ °è»êÇÑ´Ù. * - Á¤È®ÇÑ ¿¹»ó½Ã°£À» ÀÌ¹Ì ¾Ë°í ÀÖ´Ù¸é estimatedSeconds¸¦ Á÷Á¢ Àü´ÞÇÑ´Ù. * * 4. ÁøÇà°ª °»½Å * * ProgressLayer.update(5000); * * ¶Ç´Â * * ProgressLayer.setPercent(50); * * 5. ÀÛ¾÷ ¿Ï·á * * ProgressLayer.complete({ * message: 'ÀÛ¾÷ÀÌ ¿Ï·áµÇ¾ú½À´Ï´Ù.', * statusText: '' * }); * * 6. ÀÛ¾÷ ½ÇÆÐ * * ProgressLayer.fail({ * message: 'ÀÛ¾÷ ó¸® Áß ¿À·ù°¡ ¹ß»ýÇß½À´Ï´Ù.' * }); * **************************************************************************/ ((window, document) => { 'use strict'; const state = { mode: '', timer: null, done: false, startTime: 0, expectMs: 0, option: null, confirmCallback: null, progressType: 'percent', totalCount: 0, countSuffix: '°Ç', percent: 0, allowEscape: false }; const ids = { layer: 'common_work_layer', box: 'common_work_box', title: 'common_work_title', countRow: 'common_work_count_row', countLabel: 'common_work_count_label', count: 'common_work_count', message: 'common_work_message', progressArea: 'common_work_progress_area', progressBar: 'common_work_progress_bar', progressTime: 'common_work_progress_time', progressValue: 'common_work_progress_value', loadingArea: 'common_work_loading_area', loadingText: 'common_work_loading_text', status: 'common_work_status', buttonArea: 'common_work_button_area', confirmButton: 'common_work_confirm_button' }; const getElement = id => document.getElementById(id); const setText = (id, value = '') => { const element = getElement(id); if (element) element.textContent = String(value ?? ''); }; const show = id => { const element = getElement(id); if (element) element.hidden = false; }; const hide = id => { const element = getElement(id); if (element) element.hidden = true; }; const formatNumber = value => { const number = Number.parseInt(value, 10); return Number.isFinite(number) && number > 0 ? number.toLocaleString('ko-KR') : '0'; }; const formatSeconds = seconds => { const value = Math.max(1, Math.ceil(Number.parseFloat(seconds) || 1)); if (value < 60) return `¾à ${value}ÃÊ`; const minute = Math.floor(value / 60); const remainSeconds = value % 60; return remainSeconds ? `¾à ${minute}ºÐ ${remainSeconds}ÃÊ` : `¾à ${minute}ºÐ`; }; /** * ½ÇÁ¦ ÃøÁ¤µÈ ±âÁذªÀ» ÀÌ¿ëÇÏ¿© ÇöÀç ÀÛ¾÷ÀÇ ¿¹»ó ¼Ò¿ä½Ã°£À» °è»êÇÑ´Ù. * * ¿¹: 100,000°Ç 󸮿¡ 5Ãʰ¡ °É·È´Ù¸é * calcEstimatedSeconds(ÇöÀç°Ç¼ö, 100000, 5) * * @param {number} currentCount ÇöÀç ó¸®ÇÒ °Ç¼ö * @param {number} sampleCount ±âÁØ Å×½ºÆ® °Ç¼ö * @param {number} sampleSeconds ±âÁØ Å×½ºÆ® ¼Ò¿ä½Ã°£(ÃÊ) * @returns {number} ¿Ã¸² ó¸®µÈ ¿¹»ó ¼Ò¿ä½Ã°£(ÃÊ). °è»ê ºÒ°¡ ½Ã 0 */ const calcEstimatedSeconds = ( currentCount, sampleCount, sampleSeconds ) => { const current = Number.parseFloat(currentCount); const sample = Number.parseFloat(sampleCount); const seconds = Number.parseFloat(sampleSeconds); if ( !Number.isFinite(current) || !Number.isFinite(sample) || !Number.isFinite(seconds) || current <= 0 || sample <= 0 || seconds <= 0 ) { return 0; } // ¼Ò·® ÀÛ¾÷Àº ¿¹»ó ¼Ò¿ä½Ã°£À» Áï½Ã·Î Ç¥½ÃÇÑ´Ù. if (current <= 1000) { return 0; } return Math.max( 1, Math.ceil(current * seconds / sample) ); }; const screenDisabled = disabled => { if (typeof window.ScreenDisabled === 'function') { window.ScreenDisabled(disabled ? 1 : 0); } }; const clearTimer = () => { if (state.timer !== null) { window.clearInterval(state.timer); state.timer = null; } }; const createLayer = () => { if (getElement(ids.layer)) return; const layer = document.createElement('div'); layer.id = ids.layer; layer.className = 'progress-layer'; layer.hidden = true; layer.setAttribute('aria-hidden', 'true'); layer.innerHTML = ` `; document.body.appendChild(layer); document.addEventListener('keydown', event => { if ( event.key === 'Escape' && state.allowEscape && !getElement(ids.layer)?.hidden ) { close(); } }); }; const setBoxState = type => { const box = getElement(ids.box); if (!box) return; box.classList.remove('is-running', 'is-success', 'is-error'); box.classList.add(`is-${type}`); }; const resetLayer = () => { clearTimer(); Object.assign(state, { mode: '', done: false, startTime: 0, expectMs: 0, option: null, confirmCallback: null, progressType: 'percent', totalCount: 0, countSuffix: '°Ç', percent: 0, allowEscape: false }); setBoxState('running'); hide(ids.countRow); hide(ids.progressArea); hide(ids.loadingArea); hide(ids.status); show(ids.buttonArea); show(ids.progressValue); show(ids.progressTime); const progressValue = getElement(ids.progressValue); if (progressValue) { progressValue.classList.remove('is-complete'); } setText(ids.title); setText(ids.message); setText(ids.countLabel); setText(ids.count); setText(ids.progressTime); setText(ids.progressValue, '0%'); setText(ids.loadingText); setText(ids.status); setText(ids.confirmButton, 'È®ÀÎ'); const bar = getElement(ids.progressBar); if (bar) { bar.style.width = '0%'; bar.classList.remove('has-value'); } const track = getElement(ids.progressArea)?.querySelector('[role="progressbar"]'); if (track) track.setAttribute('aria-valuenow', '0'); const button = getElement(ids.confirmButton); if (button) { button.disabled = true; button.onclick = null; } }; const openLayer = option => { createLayer(); resetLayer(); state.allowEscape = option?.allowEscape === true; const layer = getElement(ids.layer); layer.hidden = false; layer.setAttribute('aria-hidden', 'false'); screenDisabled(true); }; const updateProgressText = percent => { if (state.progressType === 'count' && state.totalCount > 0) { let currentCount = Math.floor(state.totalCount * percent / 100); if (percent >= 100) currentCount = state.totalCount; currentCount = Math.min(Math.max(currentCount, 0), state.totalCount); setText( ids.progressValue, `${formatNumber(currentCount)}${state.countSuffix}` ); return; } setText(ids.progressValue, `${percent}%`); }; const setPercent = percent => { const value = Math.min( 100, Math.max(0, Number.parseInt(percent, 10) || 0) ); state.percent = value; updateProgressText(value); const bar = getElement(ids.progressBar); if (bar) { bar.style.width = `${value}%`; bar.classList.toggle('has-value', value > 0); } const track = getElement(ids.progressArea)?.querySelector('[role="progressbar"]'); if (track) track.setAttribute('aria-valuenow', String(value)); }; const startAutoProgress = () => { state.timer = window.setInterval(() => { if (state.done || state.mode !== 'progress') return; const elapsed = Date.now() - state.startTime; // ¿¹»ó½Ã°£ µ¿¾È 0%¿¡¼­ 99%±îÁö ÀÚ¿¬½º·´°Ô ÁøÇàÇÑ´Ù. // 100%´Â ½ÇÁ¦ complete()°¡ È£ÃâµÆÀ» ¶§¸¸ Ç¥½ÃÇÑ´Ù. const percent = state.expectMs > 0 ? Math.min(99, Math.floor((elapsed / state.expectMs) * 99)) : 99; setPercent(percent); if (percent >= 99) { clearTimer(); } }, 100); }; const progress = (option = {}) => { openLayer(option); const totalCount = Math.max( 0, Number.parseInt(option.count, 10) || 0 ); /* * estimatedSeconds°¡ Àü´ÞµÇ¸é ÇØ´ç °ªÀ» ¿ì¼± »ç¿ëÇÑ´Ù. * »ý·«µÈ °æ¿ì count, sampleCount, sampleSeconds°¡ ¸ðµÎ ÀÖÀ¸¸é * calcEstimatedSeconds()·Î ½ÇÁ¦ ÃøÁ¤ ºñÀ²¿¡ µû¶ó ÀÚµ¿ °è»êÇÑ´Ù. * * 1,000°Ç ÀÌÇÏ´Â ¿¹»ó½Ã°£À» 0ÃÊ·Î °è»êÇÏ¿© 'Áï½Ã'·Î Ç¥½ÃÇÑ´Ù. * °è»ê ±âÁØ ÀÚü°¡ ¾ø°Å³ª ¿Ã¹Ù¸£Áö ¾ÊÀº °æ¿ì¿¡¸¸ ±âº» 2Ãʸ¦ »ç¿ëÇÑ´Ù. */ let estimatedSeconds; const hasSampleData = totalCount > 0 && Number.parseFloat(option.sampleCount) > 0 && Number.parseFloat(option.sampleSeconds) > 0; if (option.estimatedSeconds !== undefined) { estimatedSeconds = Math.max( 0, Number.parseFloat(option.estimatedSeconds) || 0 ); } else if (hasSampleData) { estimatedSeconds = calcEstimatedSeconds( totalCount, option.sampleCount, option.sampleSeconds ); } else { /* * ±âÁذªÀÌ ¾ø´Â °æ¿ì ±âº» ¿¹»ó½Ã°£ 2Ãʸ¦ »ç¿ëÇÑ´Ù. */ estimatedSeconds = 2; } Object.assign(state, { mode: 'progress', option, startTime: Date.now(), expectMs: estimatedSeconds * 1000, progressType: option.progressType === 'count' ? 'count' : 'percent', totalCount, countSuffix: option.countSuffix !== undefined ? String(option.countSuffix) : '°Ç' }); setText(ids.title, option.title || 'ÀÛ¾÷ ÁøÇà Áß'); setText(ids.message, option.message || 'µ¥ÀÌÅ͸¦ ó¸®Çϰí ÀÖ½À´Ï´Ù.'); show(ids.progressArea); show(ids.progressValue); show(ids.progressTime); if (option.count !== undefined) { show(ids.countRow); setText(ids.countLabel, option.countLabel || 'ó¸® °Ç¼ö'); setText(ids.count, `${formatNumber(totalCount)}${state.countSuffix}`); } setText( ids.progressTime, estimatedSeconds <= 0 ? `${option.timeLabel || '¿¹»ó ¼Ò¿ä½Ã°£'} : Áï½Ã` : `${option.timeLabel || '¿¹»ó ¼Ò¿ä½Ã°£'} : ${formatSeconds(estimatedSeconds)}` ); if (estimatedSeconds <= 0) { /* * 1,000°Ç ÀÌÇÏÀÇ ¼Ò·® ÀÛ¾÷Àº ¿¹»ó½Ã°£À» 'Áï½Ã'·Î Ç¥½ÃÇÑ´Ù. * ÁøÇà¹Ù´Â ¾à 0.7ÃÊ µ¿¾È 0%¿¡¼­ 99%±îÁö ºü¸£°Ô Áõ°¡ÇÑ µÚ * ½ÇÁ¦ ¿Ï·á ½ÅÈ£°¡ ¿Ã ¶§±îÁö 99%¿¡¼­ ±â´Ù¸°´Ù. */ setPercent(option.percent ?? 0); if (option.autoProgress !== false) { const quickStartTime = Date.now(); const quickDuration = 700; state.timer = window.setInterval(() => { if (state.done || state.mode !== 'progress') return; const elapsed = Date.now() - quickStartTime; const percent = Math.min( 99, Math.floor((elapsed / quickDuration) * 99) ); setPercent(percent); if (percent >= 99) { clearTimer(); } }, 30); } } else { /* * ´ë·® ÀÛ¾÷Àº °¢ ÀÛ¾÷¿¡¼­ Àü´ÞÇÑ sampleCount/sampleSeconds·Î * °è»êÇÑ ¿¹»ó½Ã°£ µ¿¾È 0%¿¡¼­ 99%±îÁö ÀÚ¿¬½º·´°Ô ÁøÇàÇÑ´Ù. */ setPercent(option.percent ?? 0); if (option.autoProgress !== false) { startAutoProgress(); } } /* * È£ÃâÇÑ °÷¿¡¼­ µ¿ÀÏÇÑ ¿¹»ó½Ã°£À» * º¸Á¶ ¿Ï·á ŸÀÌ¸Ó µî¿¡ »ç¿ëÇÒ ¼ö ÀÖµµ·Ï ¹ÝȯÇÑ´Ù. */ return estimatedSeconds; }; const loading = (option = {}) => { openLayer(option); Object.assign(state, { mode: 'loading', option, startTime: Date.now() }); setText(ids.title, option.title || 'ó¸® Áß'); setText(ids.message, option.message || 'µ¥ÀÌÅ͸¦ ó¸®Çϰí ÀÖ½À´Ï´Ù.'); setText(ids.loadingText, option.statusText || 'Àá½Ã¸¸ ±â´Ù·ÁÁÖ¼¼¿ä.'); show(ids.loadingArea); if (option.count !== undefined) { const suffix = option.countSuffix !== undefined ? String(option.countSuffix) : '°Ç'; show(ids.countRow); setText(ids.countLabel, option.countLabel || 'ó¸® °Ç¼ö'); setText(ids.count, `${formatNumber(option.count)}${suffix}`); } }; const update = (option = {}) => { if (!getElement(ids.layer) || getElement(ids.layer).hidden) return; if (option.title !== undefined) setText(ids.title, option.title); if (option.message !== undefined) setText(ids.message, option.message); if (option.countSuffix !== undefined) { state.countSuffix = String(option.countSuffix); } if (option.count !== undefined) { state.totalCount = Math.max( 0, Number.parseInt(option.count, 10) || 0 ); show(ids.countRow); setText( ids.count, `${formatNumber(state.totalCount)}${state.countSuffix}` ); if (state.mode === 'progress') { updateProgressText(state.percent); } } if (option.countLabel !== undefined) { setText(ids.countLabel, option.countLabel); } if ( option.progressType !== undefined && state.mode === 'progress' ) { state.progressType = option.progressType === 'count' ? 'count' : 'percent'; updateProgressText(state.percent); } if ( option.percent !== undefined && state.mode === 'progress' ) { setPercent(option.percent); } if (option.statusText !== undefined) { if (state.mode === 'loading') { setText(ids.loadingText, option.statusText); } else { setText(ids.progressTime, option.statusText); } } }; const setStatus = (statusText, defaultText) => { if (statusText === '') { hide(ids.status); setText(ids.status); return; } show(ids.status); setText(ids.status, statusText ?? defaultText); }; const showConfirmButton = (option = {}) => { const button = getElement(ids.confirmButton); state.confirmCallback = typeof option.onConfirm === 'function' ? option.onConfirm : null; setText(ids.confirmButton, option.buttonText || 'È®ÀÎ'); button.disabled = false; button.onclick = () => { const callback = state.confirmCallback; close(); if (typeof callback === 'function') { callback(); } }; window.setTimeout(() => button.focus(), 0); }; const complete = (option = {}) => { if ( !getElement(ids.layer) || getElement(ids.layer).hidden || state.done ) { return; } state.done = true; clearTimer(); setBoxState('success'); /* * progress/loading ±¸ºÐ ¾øÀÌ ¿Ï·á »óÅ´ µ¿ÀÏÇÏ°Ô Ç¥½ÃÇÑ´Ù. * ÁøÇà¹Ù¸¦ 100%·Î ä¿ì°í ÁøÇà¹Ù Áß¾Ó¿¡ 100%¸¦ °íÁ¤ Ç¥½ÃÇÑ µÚ * ½ÇÁ¦ ¼Ò¿ä½Ã°£À» Ç¥½ÃÇÑ´Ù. */ hide(ids.loadingArea); show(ids.progressArea); show(ids.progressTime); setPercent(100); const progressValue = getElement(ids.progressValue); if (progressValue) { progressValue.textContent = '100%'; progressValue.hidden = false; progressValue.classList.add('is-complete'); } const elapsedSeconds = Math.max( 1, Math.ceil((Date.now() - state.startTime) / 1000) ); setText( ids.progressTime, `${option.timeLabel || '¼Ò¿ä½Ã°£'} : ${formatSeconds(elapsedSeconds)}` ); setText(ids.title, option.title || 'ÀÛ¾÷ ¿Ï·á'); setText(ids.message, option.message || 'ÀÛ¾÷ÀÌ ¿Ï·áµÇ¾ú½À´Ï´Ù.'); setStatus( option.statusText, 'È®ÀÎ ¹öưÀ» ´­·¯ÁÖ¼¼¿ä.' ); showConfirmButton(option); }; const fail = (option = {}) => { if (!getElement(ids.layer) || getElement(ids.layer).hidden) return; state.done = true; clearTimer(); setBoxState('error'); hide(ids.loadingArea); hide(ids.progressArea); setText(ids.title, option.title || 'ó¸® ¿À·ù'); setText(ids.message, option.message || 'ó¸® Áß ¿À·ù°¡ ¹ß»ýÇÏ¿´½À´Ï´Ù.'); setStatus( option.statusText, 'Àá½Ã ÈÄ ´Ù½Ã ½ÃµµÇØÁÖ¼¼¿ä.' ); showConfirmButton(option); }; function close() { clearTimer(); state.done = true; const layer = getElement(ids.layer); if (layer) { layer.hidden = true; layer.setAttribute('aria-hidden', 'true'); } screenDisabled(false); } window.ProgressLayer = { progress, loading, update, setPercent, complete, fail, close, formatSeconds, formatNumber }; })(window, document);