diff --git a/com.twin.app.shoptime/src/utils/dispatchHelper.js b/com.twin.app.shoptime/src/utils/dispatchHelper.js new file mode 100644 index 00000000..25a731f1 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/dispatchHelper.js @@ -0,0 +1,444 @@ +// src/utils/dispatchHelper.js +// Redux Thunk Sequential Dispatch Helper Functions +// +// ==================== 실제 사용 예시 ==================== +// +// 이 Helper 함수들은 프로젝트의 실제 dispatch 패턴을 분석하여 설계되었습니다. +// 다음과 같은 기존 패턴들을 간소화할 수 있습니다: +// +// 1. setTimeout을 이용한 순차 dispatch (homeActions.js) +// Before: +// const onSuccess = (response) => { +// dispatch({ type: types.GET_HOME_TERMS, payload: response.data }); +// dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap }); +// setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0); +// }; +// After: +// dispatch(createSequentialDispatch([ +// { type: types.GET_HOME_TERMS, payload: response.data }, +// { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, +// getTermsAgreeYn() +// ])); +// +// 2. API 성공 후 재조회 (cartActions.js) +// Before: +// const onSuccess = (response) => { +// dispatch({ type: types.ADD_TO_CART, payload: response.data.data }); +// dispatch(getMyInfoCartSearch({ mbrNo })); +// }; +// After: +// createApiThunkWithChain( +// (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF), +// [ +// (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }), +// (response) => getMyInfoCartSearch({ mbrNo: props.mbrNo }) +// ] +// ); +// +// 3. Loading 상태 관리 (mainActions.js) +// Before: +// dispatch(changeAppStatus({ showLoadingPanel: { show: true } })); +// TAxios(..., onSuccess, onFail); +// const onSuccess = () => { ...; dispatch(changeAppStatus({ show: false })); }; +// const onFail = () => { dispatch(changeAppStatus({ show: false })); }; +// After: +// withLoadingState( +// (dispatch, getState) => TAxiosPromise(dispatch, getState, 'get', URLS.API, props, {}) +// .then((response) => dispatch({ type: types.ACTION, payload: response.data.data })) +// ); +// +// 상세한 문서: .docs/plan/Promise/Redux-Sequential.md +// ======================================================== + +import { changeAppStatus } from '../actions/commonActions'; + +/** + * 여러 dispatch를 순차적으로 실행하는 Helper 함수 + * + * @param {Array} dispatchActions - 실행할 dispatch들의 배열 + * - Function: thunk action creator (props) => (dispatch, getState) => {} + * - Object: plain action { type: 'ACTION_TYPE', payload: data } + * @param {Object} options - 옵션 객체 + * @param {number} options.delay - 각 dispatch 간 지연 시간 (ms, 기본값: 0) + * @param {boolean} options.stopOnError - 에러 발생 시 중단 여부 (기본값: false) + * @returns {Function} Redux thunk 함수 (dispatch, getState) => Promise + * + * @example + * // 사용 예시 1: 여러 plain action 순차 실행 + * dispatch(createSequentialDispatch([ + * { type: types.SET_LOADING, payload: true }, + * { type: types.UPDATE_DATA, payload: data }, + * { type: types.SET_LOADING, payload: false } + * ])); + * + * @example + * // 사용 예시 2: thunk와 plain action 혼합 + * dispatch(createSequentialDispatch([ + * { type: types.GET_HOME_TERMS, payload: response.data }, + * { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, + * getTermsAgreeYn() // thunk action + * ])); + * + * @example + * // 사용 예시 3: delay 옵션 사용 + * dispatch(createSequentialDispatch([ + * fetchUserData(), + * fetchCartData(), + * fetchOrderData() + * ], { delay: 100, stopOnError: true })); + */ +export const createSequentialDispatch = (dispatchActions, options) => + (dispatch, getState) => { + const config = options || {}; + const delay = config.delay || 0; + const stopOnError = config.stopOnError !== undefined ? config.stopOnError : false; + + return dispatchActions.reduce( + (promise, action, index) => { + return promise + .then(() => { + // delay가 설정되어 있고 첫 번째가 아닌 경우 지연 + if (delay > 0 && index > 0) { + return new Promise((resolve) => setTimeout(resolve, delay)); + } + return Promise.resolve(); + }) + .then(() => { + // action 실행 + const result = dispatch(action); + + // Promise인 경우 대기 + if (result && typeof result.then === 'function') { + return result; + } + return Promise.resolve(result); + }) + .catch((error) => { + console.error('createSequentialDispatch error at index', index, error); + + // stopOnError가 true면 에러를 다시 throw + if (stopOnError) { + throw error; + } + + // stopOnError가 false면 계속 진행 + return Promise.resolve(); + }); + }, + Promise.resolve() + ); + }; + +/** + * API 호출 후 콜백에서 여러 dispatch를 자동으로 체이닝하는 Helper + * TAxios의 onSuccess/onFail 콜백 패턴과 호환 + * + * @param {Function} apiCallFactory - API 호출을 생성하는 함수 + * (dispatch, getState, onSuccess, onFail) => void + * @param {Array} successDispatchActions - 성공 시 실행할 dispatch들 + * @param {Function|Object|null} errorDispatch - 에러 시 실행할 dispatch (선택) + * @returns {Function} Redux thunk 함수 + * + * @example + * // 사용 예시 1: API 호출 후 여러 dispatch 자동 실행 + * export const addToCart = (props) => + * createApiThunkWithChain( + * (dispatch, getState, onSuccess, onFail) => { + * TAxios(dispatch, getState, 'post', URLS.ADD_TO_CART, {}, props, onSuccess, onFail); + * }, + * [ + * (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }), + * (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo }) + * ] + * ); + * + * @example + * // 사용 예시 2: 에러 처리 포함 + * export const registerDevice = (params) => + * createApiThunkWithChain( + * (dispatch, getState, onSuccess, onFail) => { + * TAxios(dispatch, getState, 'post', URLS.REGISTER_DEVICE, {}, params, onSuccess, onFail); + * }, + * [ + * (response) => ({ type: types.REGISTER_DEVICE, payload: response.data.data }), + * getAuthenticationCode(), + * fetchCurrentUserHomeTerms() + * ], + * (error) => ({ type: types.API_ERROR, payload: error }) + * ); + */ +export const createApiThunkWithChain = ( + apiCallFactory, + successDispatchActions, + errorDispatch +) => (dispatch, getState) => { + const actions = successDispatchActions || []; + + const enhancedOnSuccess = (response) => { + // 성공 시 순차적으로 dispatch 실행 + actions.forEach((action, index) => { + setTimeout(() => { + if (typeof action === 'function') { + // action이 함수인 경우 (동적 action creator) + // response를 인자로 전달하여 실행 + const dispatchAction = action(response); + dispatch(dispatchAction); + } else { + // action이 객체인 경우 (plain action) + dispatch(action); + } + }, 0); + }); + }; + + const enhancedOnFail = (error) => { + console.error('createApiThunkWithChain error:', error); + + if (errorDispatch) { + if (typeof errorDispatch === 'function') { + // errorDispatch가 함수인 경우 + const dispatchAction = errorDispatch(error); + dispatch(dispatchAction); + } else { + // errorDispatch가 객체인 경우 + dispatch(errorDispatch); + } + } + }; + + // API 호출 실행 + return apiCallFactory(dispatch, getState, enhancedOnSuccess, enhancedOnFail); +}; + +/** + * API 호출 thunk의 Loading 상태를 자동으로 관리하는 Helper + * changeAppStatus로 showLoadingPanel을 자동 on/off + * + * @param {Function} thunk - API 호출 thunk 함수 + * @param {Object} options - 옵션 객체 + * @param {string} options.loadingType - 로딩 타입 (기본값: 'wait') + * @param {Array} options.successDispatch - 성공 시 추가 dispatch (선택) + * @param {Array} options.errorDispatch - 에러 시 추가 dispatch (선택) + * @returns {Function} Redux thunk 함수 + * + * @example + * // 사용 예시 1: 기본 로딩 관리 + * export const getProductDetail = (props) => + * withLoadingState( + * (dispatch, getState) => { + * return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}) + * .then((response) => { + * dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); + * }); + * } + * ); + * + * @example + * // 사용 예시 2: 성공/에러 시 추가 dispatch + * export const fetchUserData = (userId) => + * withLoadingState( + * fetchUser(userId), + * { + * successDispatch: [ + * fetchCart(userId), + * fetchOrders(userId) + * ], + * errorDispatch: [ + * (error) => ({ type: types.SHOW_ERROR_MESSAGE, payload: error.message }) + * ] + * } + * ); + */ +export const withLoadingState = (thunk, options) => (dispatch, getState) => { + const config = options || {}; + const loadingType = config.loadingType || 'wait'; + const successDispatch = config.successDispatch || []; + const errorDispatch = config.errorDispatch || []; + + // 로딩 시작 + dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: loadingType } })); + + // thunk 실행 + const result = dispatch(thunk); + + // Promise인 경우 처리 + if (result && typeof result.then === 'function') { + return result + .then((res) => { + // 로딩 종료 + dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); + + // 성공 시 추가 dispatch 실행 + successDispatch.forEach((action) => { + if (typeof action === 'function') { + dispatch(action(res)); + } else { + dispatch(action); + } + }); + + return res; + }) + .catch((error) => { + // 로딩 종료 + dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); + + // 에러 시 추가 dispatch 실행 + errorDispatch.forEach((action) => { + if (typeof action === 'function') { + dispatch(action(error)); + } else { + dispatch(action); + } + }); + + throw error; + }); + } + + // 동기 실행인 경우 (Promise가 아님) + dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); + return result; +}; + +/** + * 조건에 따라 다른 dispatch를 실행하는 Helper + * getState() 결과를 기반으로 분기 처리 + * + * @param {Function} condition - 조건을 반환하는 함수 (state) => boolean + * @param {Function|Array|Object} trueDispatch - 조건 true 시 실행할 dispatch + * @param {Function|Array|Object|null} falseDispatch - 조건 false 시 실행할 dispatch (선택) + * @returns {Function} Redux thunk 함수 + * + * @example + * // 사용 예시 1: 단일 action 조건부 실행 + * dispatch(createConditionalDispatch( + * (state) => state.common.appStatus.isAlarmEnabled === 'Y', + * addReservation(reservationData), + * deleteReservation(showId) + * )); + * + * @example + * // 사용 예시 2: 여러 action 배열로 실행 + * dispatch(createConditionalDispatch( + * (state) => state.common.appStatus.loginUserData.userNumber, + * [ + * fetchUserProfile(), + * fetchUserCart(), + * fetchUserOrders() + * ], + * [ + * { type: types.SHOW_LOGIN_REQUIRED_POPUP } + * ] + * )); + * + * @example + * // 사용 예시 3: false 조건 없이 + * dispatch(createConditionalDispatch( + * (state) => state.cart.items.length > 0, + * proceedToCheckout() + * )); + */ +export const createConditionalDispatch = ( + condition, + trueDispatch, + falseDispatch +) => (dispatch, getState) => { + const state = getState(); + const shouldExecuteTrue = condition(state); + + const actionsToDispatch = shouldExecuteTrue ? trueDispatch : falseDispatch; + + // dispatch할 것이 없으면 종료 + if (!actionsToDispatch) { + return Promise.resolve(); + } + + // 배열이 아니면 배열로 변환 + const actions = Array.isArray(actionsToDispatch) + ? actionsToDispatch + : [actionsToDispatch]; + + // 각 action 실행 + actions.forEach((action) => { + if (typeof action === 'function') { + // thunk action + dispatch(action); + } else { + // plain action + dispatch(action); + } + }); + + return Promise.resolve(); +}; + +/** + * 여러 API 호출을 병렬로 실행하고 모든 결과를 기다리는 Helper + * Promise.all을 사용하여 동시 실행 + * + * @param {Array} thunks - 실행할 thunk 함수들의 배열 + * @param {Object} options - 옵션 + * @param {boolean} options.withLoading - 로딩 상태 관리 여부 (기본값: false) + * @returns {Function} Redux thunk 함수 + * + * @example + * // 사용 예시: 여러 API를 동시에 호출 + * dispatch(createParallelDispatch([ + * fetchUserProfile(), + * fetchUserCart(), + * fetchUserOrders() + * ], { withLoading: true })); + */ +export const createParallelDispatch = (thunks, options) => (dispatch, getState) => { + const config = options || {}; + const withLoading = config.withLoading || false; + + if (withLoading) { + dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })); + } + + const promises = thunks.map((thunk) => { + const result = dispatch(thunk); + // Promise가 아닌 경우 Promise로 변환 + return Promise.resolve(result); + }); + + return Promise.all(promises) + .then((results) => { + if (withLoading) { + dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); + } + return results; + }) + .catch((error) => { + if (withLoading) { + dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); + } + throw error; + }); +}; + +// ==================== 편의 함수들 ==================== + +/** + * setTimeout을 Promise로 래핑한 delay 함수 + * @param {number} ms - 지연 시간 (밀리초) + * @returns {Promise} + */ +export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * dispatch를 Promise로 변환하는 Helper + * @param {Function} thunk - thunk 함수 + * @returns {Function} Promise를 반환하는 thunk + */ +export const promisifyDispatch = (thunk) => (dispatch, getState) => { + const result = dispatch(thunk); + + if (result && typeof result.then === 'function') { + return result; + } + + return Promise.resolve(result); +};