UNPKG

@ehfuse/forma

Version:

Advanced React state management library with individual field subscriptions - supports both forms and general state management with useFormaState

479 lines 21.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GlobalFormaContext = void 0; exports.GlobalFormaProvider = GlobalFormaProvider; const jsx_runtime_1 = require("react/jsx-runtime"); /** * GlobalFormaContext.tsx * * Forma - 글로벌 Forma 상태 관리 컨텍스트 | Global Forma state management context * 여러 컴포넌트 간 폼 상태 공유를 위한 React Context | React Context for sharing form state across multiple components * * @license MIT License * @copyright 2025 KIM YOUNG JIN (ehfuse@gmail.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const react_1 = require("react"); const FieldStore_1 = require("../core/FieldStore"); /** * 글로벌 폼 상태 관리를 위한 React Context | React Context for global form state management */ exports.GlobalFormaContext = (0, react_1.createContext)({ // FieldStore 관련 getOrCreateStore: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, registerStore: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, unregisterStore: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, clearStores: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, incrementRef: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, decrementRef: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, validateAndStoreAutoCleanupSetting: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, // 핸들러 관리 registerHandlers: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, getHandlers: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, // Actions 관리 registerActions: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, getActions: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, unregisterActions: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, // 모달 스택 관리 appendOpenModal: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, removeOpenModal: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, closeLastModal: () => { throw new Error("GlobalFormaContext must be used within GlobalFormaProvider"); }, }); /** * 글로벌 Forma 상태 관리 Provider | Global Forma state management provider * * 애플리케이션 최상단에 배치하여 전역 Forma 상태 관리를 활성화합니다. | Place at the top of your application to enable global Forma state management. * 각 stateId/formId별로 독립적인 FieldStore 인스턴스를 관리합니다. | Manages independent FieldStore instances for each stateId/formId. * * @param props Provider props * @returns 글로벌 폼 컨텍스트 Provider * * @example * ```typescript * // App.tsx * import { GlobalFormaProvider } from '@/forma'; * * function App() { * return ( * <GlobalFormaProvider> * <Router> * <Routes> * <Route path="/" element={<HomePage />} /> * <Route path="/customer" element={<CustomerPage />} /> * </Routes> * </Router> * </GlobalFormaProvider> * ); * } * ``` */ function GlobalFormaProvider({ children }) { // formId별 FieldStore 인스턴스들을 관리하는 Map | Map managing FieldStore instances by formId const storesRef = (0, react_1.useRef)(new Map()); // formId별 참조 카운트를 관리하는 Map | Map managing reference count by formId const refCountsRef = (0, react_1.useRef)(new Map()); // formId별 autoCleanup 컴포넌트 참조 카운트를 관리하는 Map | Map managing autoCleanup component reference count by formId const autoCleanupRefCountsRef = (0, react_1.useRef)(new Map()); // formId별 autoCleanup 설정을 추적하는 Map | Map tracking autoCleanup settings by formId const autoCleanupSettingsRef = (0, react_1.useRef)(new Map()); // formId별 핸들러를 저장하는 Map | Map storing handlers by formId const handlersRef = (0, react_1.useRef)(new Map()); // formId별 actions를 저장하는 Map | Map storing actions by formId const actionsRef = (0, react_1.useRef)(new Map()); // formId별 cleanup 타이머를 저장하는 Map | Map storing cleanup timers by formId const cleanupTimersRef = (0, react_1.useRef)(new Map()); // formId별 영구 보존 플래그 (autoCleanup: false인 경우 true) | Persist forever flag by formId (true when autoCleanup: false) const persistForeverRef = (0, react_1.useRef)(new Map()); // cleanup 지연 시간 (밀리초) - 리렌더링 대기 시간 | Cleanup delay time (milliseconds) - wait for re-rendering const CLEANUP_DELAY_MS = 100; // ========== 모달 스택 관리 상태 ========== const [openModalIds, setOpenModalIds] = (0, react_1.useState)([]); const openModalIdsRef = (0, react_1.useRef)([]); const modalTrackingRef = (0, react_1.useRef)([]); /** * formId에 해당하는 FieldStore를 가져오거나 새로 생성합니다. | Get or create FieldStore for the given formId. * * @param formId 폼 식별자 | Form identifier * @returns FieldStore 인스턴스 | FieldStore instance */ const getOrCreateStore = (formId) => { const stores = storesRef.current; if (!stores.has(formId)) { // 새로운 스토어를 빈 객체로 생성 | Create new store with empty object // console.log(`🏭 [GlobalFormaContext] 새 store 생성: ${formId}`); const newStore = new FieldStore_1.FieldStore({}); stores.set(formId, newStore); // console.log( // `🏭 [GlobalFormaContext] store 등록 완료. 총 stores:`, // stores.size // ); return newStore; } // console.log( // `♻️ [GlobalFormaContext] 기존 store 재사용: ${formId}, 현재 값:`, // stores.get(formId)?.getValues() // ); return stores.get(formId); }; /** * autoCleanup 설정의 일관성을 검증하고 설정을 저장합니다. | Validate and store autoCleanup setting consistency. * * @param formId 폼 식별자 | Form identifier * @param autoCleanup 현재 autoCleanup 설정 | Current autoCleanup setting */ const validateAndStoreAutoCleanupSetting = (formId, autoCleanup) => { const autoCleanupSettings = autoCleanupSettingsRef.current; const existingSetting = autoCleanupSettings.get(formId); // if (existingSetting !== undefined && existingSetting !== autoCleanup) { // console.warn( // `⚠️ Conflicting autoCleanup settings for stateId "${formId}": ` + // `existing=${existingSetting}, new=${autoCleanup}. ` + // `All components using the same stateId should have consistent autoCleanup settings.` // ); // } autoCleanupSettings.set(formId, autoCleanup); // autoCleanup: false인 경우 영구 보존 플래그 설정 | Set persist forever flag when autoCleanup is false if (!autoCleanup) { persistForeverRef.current.set(formId, true); } }; /** * 기존 FieldStore를 글로벌 폼에 등록합니다. | Register existing FieldStore to global form. * * @param formId 폼 식별자 | Form identifier * @param store 등록할 FieldStore 인스턴스 | FieldStore instance to register */ const registerStore = (formId, store) => { const stores = storesRef.current; stores.set(formId, store); }; /** * 글로벌 스토어에서 특정 formId의 FieldStore를 제거합니다. | Remove specific FieldStore from global store. * 참조 카운트를 무시하고 강제로 제거합니다. | Force remove ignoring reference count. * * @param formId 제거할 폼 식별자 | Form identifier to remove * @returns 제거 성공 여부 | Whether removal was successful */ const unregisterStore = (formId) => { const stores = storesRef.current; const refCounts = refCountsRef.current; const autoCleanupRefCounts = autoCleanupRefCountsRef.current; const autoCleanupSettings = autoCleanupSettingsRef.current; const store = stores.get(formId); // 스토어가 존재하면 리소스 정리 후 제거 | Clean up resources before removal if store exists if (store) { store.destroy(); stores.delete(formId); refCounts.delete(formId); // 참조 카운트도 함께 제거 | Remove reference count as well autoCleanupRefCounts.delete(formId); // autoCleanup 참조 카운트도 제거 | Remove autoCleanup reference count as well autoCleanupSettings.delete(formId); // autoCleanup 설정도 제거 | Remove autoCleanup settings as well persistForeverRef.current.delete(formId); // persistForever 설정도 제거 | Remove persistForever setting as well // 관련 핸들러와 actions도 함께 제거 | Remove related handlers and actions as well handlersRef.current.delete(formId); actionsRef.current.delete(formId); return true; } return false; }; /** * 모든 글로벌 스토어를 제거합니다. | Clear all global stores. * 메모리 정리나 애플리케이션 리셋 시 사용합니다. | Use for memory cleanup or application reset. */ const clearStores = () => { const stores = storesRef.current; const refCounts = refCountsRef.current; const autoCleanupRefCounts = autoCleanupRefCountsRef.current; const autoCleanupSettings = autoCleanupSettingsRef.current; // 모든 스토어의 리소스 정리 후 제거 | Clean up all store resources before clearing stores.forEach((store) => { store.destroy(); }); stores.clear(); refCounts.clear(); autoCleanupRefCounts.clear(); autoCleanupSettings.clear(); persistForeverRef.current.clear(); // 모든 핸들러와 actions도 함께 정리 | Clear all handlers and actions as well handlersRef.current.clear(); actionsRef.current.clear(); }; /** * 스토어 사용 참조를 증가시킵니다 | Increment store usage reference * * @param formId 폼 식별자 | Form identifier * @param autoCleanup autoCleanup 설정 | autoCleanup setting */ const incrementRef = (formId, autoCleanup) => { const refCounts = refCountsRef.current; const autoCleanupRefCounts = autoCleanupRefCountsRef.current; const cleanupTimers = cleanupTimersRef.current; // 예약된 cleanup 타이머가 있으면 취소 (재참조됨) // Cancel scheduled cleanup timer if exists (re-referenced) const existingTimer = cleanupTimers.get(formId); if (existingTimer) { clearTimeout(existingTimer); cleanupTimers.delete(formId); } // 전체 참조 카운트 증가 (모든 컴포넌트) const currentCount = refCounts.get(formId) || 0; const newCount = currentCount + 1; refCounts.set(formId, newCount); // autoCleanup 참조 카운트 증가 (autoCleanup: true인 컴포넌트만) if (autoCleanup) { const currentAutoCleanupCount = autoCleanupRefCounts.get(formId) || 0; const newAutoCleanupCount = currentAutoCleanupCount + 1; autoCleanupRefCounts.set(formId, newAutoCleanupCount); } }; /** * 스토어 사용 참조를 감소시키고, autoCleanup 참조가 0이 되면 자동 정리합니다 | Decrement store usage reference and auto cleanup when autoCleanup refs reach 0 * * @param formId 폼 식별자 | Form identifier * @param autoCleanup autoCleanup 설정 | autoCleanup setting */ const decrementRef = (formId, autoCleanup) => { const refCounts = refCountsRef.current; const autoCleanupRefCounts = autoCleanupRefCountsRef.current; const stores = storesRef.current; const cleanupTimers = cleanupTimersRef.current; // 전체 참조 카운트가 없는 경우 (이미 수동으로 제거됨) 무시 | Ignore if no reference count (already manually removed) if (!refCounts.has(formId)) { return; } const currentCount = refCounts.get(formId) || 0; const currentAutoCleanupCount = autoCleanupRefCounts.get(formId) || 0; // 전체 참조 카운트 감소 const newCount = Math.max(0, currentCount - 1); refCounts.set(formId, newCount); if (autoCleanup) { // autoCleanup 참조 카운트 감소 const newAutoCleanupCount = Math.max(0, currentAutoCleanupCount - 1); autoCleanupRefCounts.set(formId, newAutoCleanupCount); // autoCleanup 참조가 0이 되면 지연된 스토어 정리 예약 // Schedule delayed store cleanup when autoCleanup refs reach 0 // 단, persistForever가 true인 store는 절대 삭제하지 않음 // However, never delete stores with persistForever flag if (newAutoCleanupCount === 0 && !persistForeverRef.current.get(formId)) { // 기존 타이머가 있으면 취소 (중복 방지) // Cancel existing timer if any (prevent duplicates) const existingTimer = cleanupTimers.get(formId); if (existingTimer) { clearTimeout(existingTimer); } // 지연된 cleanup 예약 - 리렌더링 대기 // Schedule delayed cleanup - wait for re-rendering const timer = setTimeout(() => { // 타이머 실행 시점에 여전히 참조가 0인지 확인 // Check if refs are still 0 at timer execution time const finalAutoCleanupCount = autoCleanupRefCounts.get(formId) || 0; if (finalAutoCleanupCount === 0) { const store = stores.get(formId); if (store) { store.destroy(); stores.delete(formId); refCounts.delete(formId); autoCleanupRefCounts.delete(formId); autoCleanupSettingsRef.current.delete(formId); handlersRef.current.delete(formId); actionsRef.current.delete(formId); cleanupTimers.delete(formId); } } else { cleanupTimers.delete(formId); } }, CLEANUP_DELAY_MS); cleanupTimers.set(formId, timer); } } // 전체 참조가 0이 되면 카운트 정리 (스토어는 이미 정리되었거나 영구 참조만 남음) if (newCount === 0) { refCounts.delete(formId); if (autoCleanupRefCounts.get(formId) === 0) { autoCleanupRefCounts.delete(formId); } } }; /** * 글로벌 폼 핸들러 등록 | Register global form handlers * * @param formId 폼 식별자 | Form identifier * @param handlers 핸들러들 | Handlers */ const registerHandlers = (formId, handlers) => { const existingHandlers = handlersRef.current.get(formId); // 이미 핸들러가 등록되어 있으면 새로운 핸들러로 업데이트 if (existingHandlers) { handlersRef.current.set(formId, { ...existingHandlers, ...handlers, }); } else { handlersRef.current.set(formId, handlers); } }; /** * 글로벌 폼 핸들러 조회 | Get global form handlers * * @param formId 폼 식별자 | Form identifier * @returns 핸들러들 또는 undefined | Handlers or undefined */ const getHandlers = (formId) => { return handlersRef.current.get(formId); }; // ========== Actions 관련 함수 ========== /** * 글로벌 actions 등록 | Register global actions * * @param formId 폼/상태 식별자 | Form/state identifier * @param actions 등록할 actions | Actions to register */ const registerActions = (formId, actions) => { actionsRef.current.set(formId, actions); }; /** * 글로벌 actions 조회 | Get global actions * * @param formId 폼/상태 식별자 | Form/state identifier * @returns actions 또는 undefined | Actions or undefined */ const getActions = (formId) => { return actionsRef.current.get(formId); }; /** * 글로벌 actions 제거 | Remove global actions * * @param formId 폼/상태 식별자 | Form/state identifier */ const unregisterActions = (formId) => { actionsRef.current.delete(formId); }; // ========== 모달 및 네비게이션 관련 함수 ========== /** * 모달 등록 */ const appendOpenModal = (0, react_1.useCallback)((modalId) => { if (modalTrackingRef.current.includes(modalId)) return; modalTrackingRef.current.push(modalId); setOpenModalIds((prevIds) => { const newOpenIds = [...prevIds, modalId]; return newOpenIds; }); window.history.pushState({ modalOpen: modalId }, "", window.location.href); }, []); /** * 모달 등록 해제 */ const removeOpenModal = (0, react_1.useCallback)((modalId) => { setOpenModalIds((prevIds) => { if (prevIds.includes(modalId)) { const newOpenIds = prevIds.filter((id) => id !== modalId); modalTrackingRef.current = modalTrackingRef.current.filter((id) => id !== modalId); return newOpenIds; } return prevIds; }); }, []); /** * 마지막으로 열린 모달 닫기 */ const closeLastModal = (0, react_1.useCallback)(() => { if (openModalIds.length === 0) return false; const lastModalId = openModalIds[openModalIds.length - 1]; window.dispatchEvent(new CustomEvent(`modal:close:${lastModalId}`)); return true; }, [openModalIds]); // 모달 ID 추적을 위해 ref 업데이트 (0, react_1.useEffect)(() => { openModalIdsRef.current = openModalIds; }, [openModalIds]); // popstate 이벤트 핸들러 (뒤로가기 시 모달 닫기) (0, react_1.useEffect)(() => { const handlePopState = (e) => { // 열린 모달이 있으면 모달을 닫기 if (openModalIdsRef.current.length > 0) { e.preventDefault(); closeLastModal(); return; } }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; }, [closeLastModal]); const contextValue = { // FieldStore 관련 getOrCreateStore, registerStore, unregisterStore, clearStores, incrementRef, decrementRef, validateAndStoreAutoCleanupSetting, // 핸들러 관리 registerHandlers, getHandlers, // Actions 관리 registerActions, getActions, unregisterActions, // 모달 스택 관리 appendOpenModal, removeOpenModal, closeLastModal, }; return ((0, jsx_runtime_1.jsx)(exports.GlobalFormaContext.Provider, { value: contextValue, children: children })); } //# sourceMappingURL=GlobalFormaContext.js.map