Ajax 전송 / 콜백 / 렌더링까지 통합 처리 > 영카트5 팁자료실

영카트5 팁자료실

Ajax 전송 / 콜백 / 렌더링까지 통합 처리 정보

Ajax 전송 / 콜백 / 렌더링까지 통합 처리

첨부파일

WwizFrontCore.js (19.5K) 6회 다운로드 2025-10-26 16:26:07

본문

주말을 맞아 주로 사용하는 코드를 좀 정리해서 쉽게 사용할 수 있도록 수정하였습니다.
그누보드나 영카트에서 쉽게 ajax 전송, 결과 처리 페이지 랜더링을 좀 더 쉽게 사용할 수 있습니다.


const WwizFrontCore = (() => {
    const API_BASE_URL = window.API_FULL_BASE_URL || '/api/v1';
    let cachedCsrfToken = null;
    const callbacks = {};
    const renderers = {};
    // 설정 가능한 옵션
    const config = {
        timeout: 30000, // 30초 기본 타임아웃
        errorHandler: null, // 커스텀 에러 핸들러
        progressElement: null, // 커스텀 진행률 엘리먼트
        showProgress: null, // 커스텀 진행률 표시 함수
    };
    // 디바운스 유틸리티
    const debounce = (func, wait) => {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    };
    // 쓰로틀 유틸리티
    const throttle = (func, limit) => {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    };
    // -----------------------------------
    // 초기화 (Progress 스타일 및 버튼 감지)
    // -----------------------------------
    function init() {
        addProgressStyles();
        addProgressElement();
        document.addEventListener('click', handleButtonClick);
    }
    // Progress 로딩 스타일 추가
    function addProgressStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #progress { display:none; position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:#000; opacity:.1; }
            #progress:after { content:""; position:fixed; top:calc(50% - 30px); left:calc(50% - 30px); border:6px solid #60718b; border-top-color:#fff; border-bottom-color:#fff; border-radius:50%; width:60px; height:60px; animation:spin 1s linear infinite; }
            @keyframes spin { 0%{transform:rotate(0);}100%{transform:rotate(360deg);} }
        `;
        document.head.appendChild(style);
    }
    // Progress 요소 추가
    function addProgressElement() {
        if (!document.getElementById('progress') && !config.progressElement) {
            const el = document.createElement('div');
            el.id = 'progress';
            document.body.appendChild(el);
        }
    }
    // Progress 표시/숨김
    function toggleProgress(show = true) {
        // 커스텀 진행률 표시 함수가 있으면 우선 사용
        if (config.showProgress && typeof config.showProgress === 'function') {
            config.showProgress(show);
            return;
        }
        // 커스텀 진행률 엘리먼트가 있으면 사용
        if (config.progressElement) {
            config.progressElement.style.display = show ? 'block' : 'none';
            return;
        }
        // 기본 진행률 표시
        let el = document.getElementById('progress');
        if (!el) addProgressElement();
        el = document.getElementById('progress');
        el.style.display = show ? 'block' : 'none';
    }
    // -----------------------------------
    // CSRF 토큰 관리
    // - const res = await fetch(`${API_BASE_URL}/token/getUserCsrfToken`);
    // -- 위 API 가 없다면 생략 또는 API 경로 변경
    // -- TOKEN API 형식 { token: '1dafarertasasf' }
    // -----------------------------------
    async function getCsrfToken() {
        if (cachedCsrfToken) return cachedCsrfToken;
        try {
            const res = await fetch(`${API_BASE_URL}/token/getUserCsrfToken`);
            if (!res.ok) throw new Error(`CSRF fetch failed: ${res.status}`);
            const data = await res.json();
            if (!data || !data.token) throw new Error('Invalid CSRF response (no token)');
            cachedCsrfToken = data.token;
            return cachedCsrfToken;
        } catch (err) {
            console.error('[WwizFrontCore] CSRF Token Fetch Error:', err);
            throw err; // sendAjaxRequest에서 처리됨
        }
    }
    function resetCsrfToken() {
        cachedCsrfToken = null;
    }
    // -----------------------------------
    // 에디터 내용 동기화 유틸리티 (그누보드/영카트/TinyMCE/CKEditor 지원)
    // -----------------------------------
    function syncAllEditors() {
        document.querySelectorAll('.editor-form, textarea[id^="wr_content"], textarea.smarteditor2').forEach(ed => {
            const editorId = ed.id;
            try {
                // SmartEditor2 (그누보드 기본)
                if (window.oEditors?.getById?.[editorId]) {
                    oEditors.getById[editorId].exec("UPDATE_CONTENTS_FIELD", []);
                    return;
                }
                // CKEditor
                if (window.CKEDITOR?.instances?.[editorId]) {
                    CKEDITOR.instances[editorId].updateElement();
                    return;
                }
                // TinyMCE
                if (window.tinymce?.get(editorId)) {
                    tinymce.get(editorId).save();
                    return;
                }
            } catch (e) {
                console.warn(`[Editor Sync Error] ${editorId}:`, e);
            }
        });
    }
    // -----------------------------------
    // 공통 Ajax 요청 처리
    // -----------------------------------
    async function sendAjaxRequest(method, url, data, isFormData = false, isLoading = false, retryCount = 0) {
        // AbortController for timeout
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), config.timeout);
        try {
            const csrfToken = await getCsrfToken();
            const options = {
                method,
                headers: {
                    'X-CSRF-Token': csrfToken,
                    'X-Requested-With': 'XMLHttpRequest',
                },
                signal: controller.signal,
            };
            if (method.toUpperCase() !== 'GET') {
                if (isFormData && data instanceof FormData) {
                    options.body = data;
                } else if (!isFormData) {
                    options.headers['Content-Type'] = 'application/json';
                    options.body = JSON.stringify(data);
                } else {
                    throw new Error('Invalid data type for isFormData=true');
                }
            } else if (data) {
                url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
            }
            if (isLoading) toggleProgress(true);
            const response = await fetch(url, options);
            let json;
            try {
                json = await response.json();
            } catch {
                json = { result: false, message: 'Invalid JSON response' };
            }
            if (isLoading) toggleProgress(false);
            if (!response.ok) {
                if (response.status === 419 && retryCount < 3) {
                    resetCsrfToken();
                    return sendAjaxRequest(method, url, data, isFormData, isLoading, retryCount + 1);
                }
                throw new Error(json.error || 'Request failed');
            }
            return json;
        } catch (e) {
            if (e.name === 'AbortError') {
                handleError(new Error('요청 시간이 초과되었습니다.'));
            } else {
                handleError(e);
            }
            toggleProgress(false);
            throw e;
        } finally {
            clearTimeout(timeoutId);
        }
    }
    // -----------------------------------
    // 폼 기반 Ajax 요청
    // -----------------------------------
    async function handleAjaxFormSubmit(button) {
        const form = button.closest('form');
        const url = button.dataset.target;
        const beforeSubmit = button.dataset.beforeSubmit;
        const callback = button.dataset.callback;
        const containerId = button.dataset.container;
        const loading = button.dataset.loading === 'true';
        // 기본 검증
        if (!form) {
            alert('폼이 지정되지 않았습니다.');
            return;
        }
        if (!url) {
            alert('URL이 지정되지 않았습니다.');
            return;
        }
        // 전송 전 커스텀 훅
        if (beforeSubmit && typeof window[beforeSubmit] === 'function') {
            const proceed = await window[beforeSubmit]();
            if (!proceed) return;
        }
        // 에디터 내용 동기화
        syncAllEditors();
        // 폼 유효성 검사 (사용자 커스텀)
        if (typeof validateForm === 'function') {
            if (!validateForm(form)) return;
        } else if (window.Common?.validateForm) {
            if (!Common.validateForm(form)) return;
        }
        button.disabled = true;
        try {
            const formData = new FormData(form);
            const result = await sendAjaxRequest('POST', url, formData, true, loading);
            if (result.result && callback) {
                await executeCallback(callback, result, containerId);
            } else {
                alert(result.message || '처리 중 오류가 발생했습니다.');
            }
        } catch (e) {
            handleError(e);
        } finally {
            button.disabled = false;
        }
    }
    // 디바운스된 폼 제출 핸들러 (연속 클릭 방지)
    const debouncedHandleAjaxFormSubmit = debounce(handleAjaxFormSubmit, 300);
    // -----------------------------------
    // 커스텀 Ajax 호출 (폼 외부)
    // -----------------------------------
    async function sendCustomAjaxRequest(method, url, data, loading = false, callback = null, containerId = null) {
        try {
            const result = await sendAjaxRequest(method, url, data, false, loading);
            if (callback) {
                await executeCallback(callback, result, containerId);
            }
            return result;
        } catch (e) {
            handleError(e);
            throw e;
        }
    }
    // -----------------------------------
    // 공통 에러 핸들러
    // -----------------------------------
    function handleError(error) {
        const msg = error?.message || '요청 처리 중 오류가 발생했습니다.';
        console.error('[WwizFrontCore Error]', msg);
        
        // 커스텀 에러 핸들러가 있으면 사용
        if (config.errorHandler && typeof config.errorHandler === 'function') {
            config.errorHandler(error);
        } else {
            // 기본 동작
            alert(msg);
        }
    }
    // -----------------------------------
    // 콜백 & 렌더러 시스템
    // -----------------------------------
    function registerCallback(name, fn) {
        if (typeof fn === 'function') callbacks[name] = fn;
    }
    async function executeCallback(name, data, containerId) {
        if (callbacks[name]) return await callbacks[name](data, containerId);
        if (typeof window[name] === 'function') return await window[name](data, containerId);
        console.warn(`콜백 [${name}]를 찾을 수 없습니다.`);
    }
    function registerRenderer(name, fn) {
        if (typeof fn === 'function') renderers[name] = fn;
    }
    function render(name, container, data, ...args) {
        if (renderers[name]) return renderers[name](container, data, ...args);
        console.warn(`렌더러 [${name}]를 찾을 수 없습니다.`);
    }
    // -----------------------------------
    // 이벤트 위임 (폼 전송 버튼 감지)
    // -----------------------------------
    function handleButtonClick(e) {
        const btn = e.target.closest('.btn-form-submit-ajax');
        if (btn) {
            // 디바운스된 핸들러 사용 (연속 클릭 방지)
            debouncedHandleAjaxFormSubmit(btn);
        }
    }
    // -----------------------------------
    // 메모리 정리 함수
    // -----------------------------------
    function destroy() {
        document.removeEventListener('click', handleButtonClick);
        // 진행률 엘리먼트 제거
        const progressEl = document.getElementById('progress');
        if (progressEl) progressEl.remove();
        // 캐시된 토큰 정리
        cachedCsrfToken = null;
    }
    // -----------------------------------
    // 설정 변경 함수
    // -----------------------------------
    function configure(options) {
        Object.assign(config, options);
    }
    // -----------------------------------
    // 외부 노출 API
    // -----------------------------------
    return {
        init,
        sendAjaxRequest,
        sendCustomAjaxRequest,
        handleAjaxFormSubmit,
        registerCallback,
        executeCallback,
        registerRenderer,
        render,
        destroy,
        configure,
        debounce,
        throttle,
        syncAllEditors,
    };
})();
// -----------------------------------
// 자동 초기화
// -----------------------------------
document.addEventListener('DOMContentLoaded', () => WwizFrontCore.init());
추천
7

댓글 2개

와.. 얼핏 보았는데 디바운스 설계까지 잘 되어있고 온리 바닐라 스크립트로만 짜여진 멋진 코드 같아 보입니다. 역시 숨은 고수님들이 많으시군요 ㅎㅎ 좋은 코드 공유 감사합니다.

전체 438
영카트5 팁자료실 내용 검색

회원로그인

(주)에스아이알소프트 / 대표:홍석명 / (06211) 서울특별시 강남구 역삼동 707-34 한신인터밸리24 서관 1402호 / E-Mail: admin@sir.kr
사업자등록번호: 217-81-36347 / 통신판매업신고번호:2014-서울강남-02098호 / 개인정보보호책임자:김민섭(minsup@sir.kr)
© SIRSOFT