/** * 그누보드7 템플릿 엔진 진입점 * * 모든 코어 엔진 모듈을 통합하고 외부에서 사용할 수 있는 공개 API를 제공합니다. * * @packageDocumentation */ import React, { startTransition } from 'react'; import ReactDOM from 'react-dom/client'; import { ComponentRegistry } from './template-engine/ComponentRegistry'; import { DataBindingEngine } from './template-engine/DataBindingEngine'; import { TranslationEngine, TranslationContext } from './template-engine/TranslationEngine'; import { ActionDispatcher, setActionDispatcherInstance } from './template-engine/ActionDispatcher'; import DynamicRenderer from './template-engine/DynamicRenderer'; import { LayoutLoader } from './template-engine/LayoutLoader'; import { TransitionProvider } from './template-engine/TransitionContext'; import { TranslationProvider } from './template-engine/TranslationContext'; import { transitionManager } from './template-engine/TransitionManager'; import { ResponsiveProvider } from './template-engine/ResponsiveContext'; import { responsiveManager } from './template-engine/ResponsiveManager'; import { SlotProvider } from './template-engine/SlotContext'; import { DataSourceManager } from './template-engine/DataSourceManager'; import { ModalDataSourceWrapper } from './template-engine/ModalDataSourceWrapper'; import { ParentContextProvider } from './template-engine/ParentContextProvider'; import { TemplateNotFoundError } from './template-engine/TemplateEngineError'; import { mergeLocalInitSlot } from './template-engine/localInitSlot'; import { ErrorDisplay } from './template-engine/ErrorDisplay'; import { TemplateApp, initTemplateApp } from './TemplateApp'; import { createLogger } from './utils/Logger'; import { webSocketManager } from './websocket/WebSocketManager'; import { initializeG7CoreGlobals, initDevToolsAPI } from './template-engine/G7CoreGlobals'; import { checkLayoutEditorMode } from './template-engine/layout-editor/hooks/useEditorMode'; import { loadScriptWithRetry } from './template-engine/networkResilience'; // LayoutEditorChrome 은 정적 import 하지 않는다 — 편집기는 별도 lazy 번들 // (layout-editor.min.js)로 분리되어 `/admin/layout-editor/*` 진입 시에만 로드된다. // @since engine-v1.51.0 const logger = createLogger('TemplateEngine'); /** * 템플릿 메타데이터 인터페이스 */ interface TemplateMetadata { identifier: string; locales: string[]; name: Record; description: Record; version: string; type: string; } /** * 템플릿 엔진 상태 인터페이스 */ interface TemplateEngineState { templateId: string | null; locale: string; isInitialized: boolean; reactRoot: ReactDOM.Root | null; containerId: string | null; currentLayoutJson: any | null; currentDataContext: Record; translationContext: TranslationContext; registry: ComponentRegistry | null; bindingEngine: DataBindingEngine | null; translationEngine: TranslationEngine | null; actionDispatcher: ActionDispatcher | null; templateMetadata: TemplateMetadata | null; } /** * 템플릿 엔진 초기화 옵션 */ interface InitOptions { templateId: string; templateType?: string; locale?: string; debug?: boolean; /** 확장 기능 캐시 버전 (모듈/플러그인 활성화 시 갱신됨) */ cacheVersion?: number; } /** * 렌더링 옵션 */ interface RenderOptions { containerId: string; layoutJson: any; dataContext?: Record; translationContext?: TranslationContext; } /** * 데이터 업데이트 옵션 */ interface UpdateOptions { /** * true인 경우 startTransition 없이 즉시 동기적으로 렌더링합니다. * 드래그 앤 드롭 후 순서 변경 등 즉각적인 UI 반영이 필요한 경우 사용합니다. * 기본값: false (startTransition 사용) */ sync?: boolean; } /** * 템플릿 엔진 공개 API */ interface TemplateEngineAPI { initTemplateEngine: (options: InitOptions) => Promise; renderTemplate: (options: RenderOptions) => Promise; updateTemplateData: (data: Record) => void; destroyTemplate: () => void; getState: () => Readonly; } /** * 전역 상태 */ const state: TemplateEngineState = { templateId: null, locale: 'ko', isInitialized: false, reactRoot: null, containerId: null, currentLayoutJson: null, currentDataContext: {}, translationContext: { templateId: '', locale: 'ko', }, registry: null, bindingEngine: null, translationEngine: null, actionDispatcher: null, templateMetadata: null, }; /** * 디버그 모드 플래그 (ModalDataSourceWrapper 등에서 참조) */ let debugMode = false; /** * React가 준비되기 전에 도착한 데이터 업데이트를 저장하는 큐 * renderTemplate 완료 후 순차적으로 적용됨 */ const pendingDataUpdates: Array<{ data: Record; options?: UpdateOptions }> = []; /** * 템플릿 메타데이터 로드 * * 템플릿 컴포넌트 번들에 포함된 메타데이터를 가져옵니다. * API 호출 없이 전역 변수에서 직접 접근하여 성능을 개선합니다. * * @param templateId - 템플릿 ID * @returns 템플릿 메타데이터 */ async function loadTemplateMetadata(templateId: string): Promise { try { logger.log('템플릿 메타데이터 로드 중...', templateId); // 템플릿 컴포넌트 번들의 전역 변수명 생성 // 예: 'sirsoft-admin_basic' → 'SirsoftAdminBasic' const globalVarName = templateId .split('-') .map(part => part .split('_') .map(subPart => subPart.charAt(0).toUpperCase() + subPart.slice(1)) .join('') ) .join(''); // 전역 변수에서 메타데이터 가져오기 const templateBundle = (window as any)[globalVarName]; if (templateBundle?.templateMetadata) { logger.log('템플릿 메타데이터 로드 완료 (전역 변수)', templateBundle.templateMetadata); return templateBundle.templateMetadata; } // 폴백: 템플릿 번들에 메타데이터가 없는 경우 경고 logger.warn( `템플릿 번들에 메타데이터가 없습니다. 전역 변수: ${globalVarName}` ); // 기본값 반환 return { identifier: templateId, locales: ['ko', 'en'], name: { ko: templateId, en: templateId }, description: { ko: '', en: '' }, version: '1.0.0', type: 'admin', }; } catch (error) { logger.error('템플릿 메타데이터 로드 실패', error); // 기본값 반환 return { identifier: templateId, locales: ['ko', 'en'], name: { ko: templateId, en: templateId }, description: { ko: '', en: '' }, version: '1.0.0', type: 'admin', }; } } /** * 전역 변수 생성 * * 템플릿 엔진 기획서에 명시된 전역 변수를 생성합니다: * - $locale: 현재 언어 코드 (예: 'ko', 'en') * - $locales: 시스템 활성 언어 목록 (활성 언어팩 기반, 예: ['ko', 'en', 'ja']) * - $templateLocales: 현재 템플릿이 자체 번역을 제공하는 언어 목록 (template.json `locales`) * - $user: 현재 로그인한 사용자 정보 (추후 구현) * - $auth: 인증 상태 (추후 구현) * * @returns 전역 변수 객체 */ function createGlobalVariables(): Record { // 시스템 활성 언어팩(_global.appConfig.supportedLocales) 을 우선 사용한다. // 언어팩 설치/제거 시 즉시 사용자 선택 UI 에 반영되어야 하기 때문이다. // 미초기화/누락 시에만 템플릿 정적 메타데이터로 폴백한다. const templateApp = (typeof window !== 'undefined' ? (window as any).__templateApp : undefined); const systemLocales = templateApp?.globalState?.appConfig?.supportedLocales; const templateLocales = state.templateMetadata?.locales; return { $locale: state.locale, $locales: (Array.isArray(systemLocales) && systemLocales.length > 0) ? systemLocales : (templateLocales || ['ko', 'en']), $templateLocales: templateLocales || ['ko', 'en'], $templateId: state.templateId, // 추후 추가 예정: // $user: getCurrentUser(), // $auth: getAuthState(), }; } /** * 템플릿 엔진 초기화 * * 모든 엔진 인스턴스를 생성하고 템플릿 컴포넌트를 로드합니다. * * @param options - 초기화 옵션 * @throws {Error} 이미 초기화된 경우 또는 초기화 실패 시 */ async function initTemplateEngine(options: InitOptions): Promise { try { logger.log('템플릿 엔진 초기화 시작', options); // 이미 초기화된 경우 에러 if (state.isInitialized) { throw new Error('템플릿 엔진이 이미 초기화되었습니다. destroyTemplate()을 먼저 호출하세요.'); } // 옵션 검증 if (!options.templateId) { throw new TemplateNotFoundError(); } // 디버그 모드 설정 debugMode = options.debug ?? false; // G7Config에 debug 모드 설정 (DevTools에서 참조) if (typeof window !== 'undefined') { if (!(window as any).G7Config) { (window as any).G7Config = {}; } (window as any).G7Config.debug = debugMode; } // DevTools API 초기화 (debug 옵션 설정 후) initDevToolsAPI(); // 상태 업데이트 state.templateId = options.templateId; state.locale = options.locale || 'ko'; // 엔진 인스턴스 생성 logger.log('엔진 인스턴스 생성 중...'); state.registry = ComponentRegistry.getInstance(); state.bindingEngine = new DataBindingEngine(); state.translationEngine = TranslationEngine.getInstance(); // TranslationEngine에 캐시 버전 설정 if (options.cacheVersion !== undefined && options.cacheVersion > 0) { state.translationEngine.setCacheVersion(options.cacheVersion); } // TranslationContext 초기화 state.translationContext = { templateId: options.templateId, locale: state.locale, }; // ActionDispatcher에 TranslationEngine과 TranslationContext 전달 state.actionDispatcher = new ActionDispatcher( {}, state.translationEngine, state.translationContext ); // ActionDispatcher 싱글톤 인스턴스로 설정 // renderItemChildren 등에서 getActionDispatcher()를 통해 동일한 인스턴스를 사용할 수 있도록 함 setActionDispatcherInstance(state.actionDispatcher); // 다국어 파일 병렬 로드 logger.log('다국어 파일 로드 중...', options.templateId, state.locale); const fallbackLocale = 'en'; const translationPromises = [ state.translationEngine .loadTranslations(options.templateId, state.locale) .then(() => { logger.log(`다국어 파일 로드 완료: ${state.locale}`); }) .catch((error) => { logger.warn( `다국어 파일 로드 실패 (${state.locale}):`, error instanceof Error ? error.message : error ); }), ]; // 폴백 로케일 로드 (기본 로케일과 다른 경우에만) if (state.locale !== fallbackLocale) { translationPromises.push( state.translationEngine .loadTranslations(options.templateId, fallbackLocale) .then(() => { logger.log(`폴백 다국어 파일 로드 완료: ${fallbackLocale}`); }) .catch((error) => { logger.warn( `폴백 다국어 파일 로드 실패 (${fallbackLocale}):`, error instanceof Error ? error.message : error ); }) ); } // 모든 다국어 파일 로드 대기 (병렬 실행) await Promise.allSettled(translationPromises); // 템플릿 메타데이터 로드 logger.log('템플릿 메타데이터 로드 중...', options.templateId); state.templateMetadata = await loadTemplateMetadata(options.templateId); // ComponentRegistry 로딩은 TemplateApp에서 병렬로 처리됨 // (성능 최적화를 위해 routes.json과 함께 병렬 로드) // 초기화 완료 state.isInitialized = true; logger.log('템플릿 엔진 초기화 완료 (ComponentRegistry는 별도 로드)'); } catch (error) { logger.error('템플릿 엔진 초기화 실패', error); // 초기화 실패 시 상태 초기화 state.isInitialized = false; state.registry = null; state.bindingEngine = null; state.translationEngine = null; state.actionDispatcher = null; throw error; } } /** * 편집기 lazy 번들(layout-editor.min.js) 로드 후 LayoutEditorChrome 컴포넌트 반환. * * @since engine-v1.51.0 * * 편집기는 메인 코어 번들에서 분리되어 `/admin/layout-editor/*` 진입 시에만 로드된다. * 이미 로드돼 있으면 즉시 반환(멱등), 아니면 `