Ajax 전송 / 콜백 / 렌더링까지 통합 처리 정보
Ajax 전송 / 콜백 / 렌더링까지 통합 처리첨부파일
본문
주말을 맞아 주로 사용하는 코드를 좀 정리해서 쉽게 사용할 수 있도록 수정하였습니다.
그누보드나 영카트에서 쉽게 ajax 전송, 결과 처리 페이지 랜더링을 좀 더 쉽게 사용할 수 있습니다.
!-->
그누보드나 영카트에서 쉽게 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
7
댓글 2개

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

감사합니다.