"use client"; import { LayoutRegistry } from "../LayoutRegistry"; /** * 자동 디스커버리 옵션 */ export interface AutoDiscoveryOptions { /** 스캔할 디렉토리 패턴 */ pattern?: string; /** 개발 모드에서 상세 로그 출력 */ verbose?: boolean; /** 에러 시 계속 진행할지 여부 */ continueOnError?: boolean; /** 최대 대기 시간 (ms) */ timeout?: number; } /** * 레이아웃 모듈 정보 */ export interface LayoutModuleInfo { path: string; id: string; name: string; loaded: boolean; error?: Error; timestamp: number; } /** * 자동 디스커버리 결과 */ export interface DiscoveryResult { success: boolean; totalFound: number; successfullyLoaded: number; failed: number; modules: LayoutModuleInfo[]; errors: Error[]; duration: number; } /** * 레이아웃 자동 디스커버리 클래스 */ export class LayoutAutoDiscovery { private static instance: LayoutAutoDiscovery; private discoveryResults: DiscoveryResult[] = []; private isDiscovering = false; static getInstance(): LayoutAutoDiscovery { if (!this.instance) { this.instance = new LayoutAutoDiscovery(); } return this.instance; } /** * 레이아웃 자동 디스커버리 실행 */ async discover(options: AutoDiscoveryOptions = {}): Promise { const startTime = Date.now(); const { pattern = "./**/*LayoutRenderer.tsx", verbose = process.env.NODE_ENV === "development", continueOnError = true, timeout = 10000, } = options; if (this.isDiscovering) { throw new Error("디스커버리가 이미 진행 중입니다."); } this.isDiscovering = true; try { if (verbose) { console.log("🔍 레이아웃 자동 디스커버리 시작..."); console.log(`📁 스캔 패턴: ${pattern}`); } const modules: LayoutModuleInfo[] = []; const errors: Error[] = []; // 현재는 정적 import를 사용 (Vite/Next.js 환경에서) const layoutModules = await this.discoverLayoutModules(pattern); for (const [path, moduleFactory] of Object.entries(layoutModules)) { const moduleInfo: LayoutModuleInfo = { path, id: this.extractLayoutId(path), name: this.extractLayoutName(path), loaded: false, timestamp: Date.now(), }; try { // 모듈 로드 시도 await this.loadLayoutModule(moduleFactory, moduleInfo); moduleInfo.loaded = true; if (verbose) { console.log(`✅ 로드 성공: ${moduleInfo.name} (${moduleInfo.id})`); } } catch (error) { const err = error as Error; moduleInfo.error = err; errors.push(err); if (verbose) { console.error(`❌ 로드 실패: ${moduleInfo.name}`, err); } if (!continueOnError) { throw err; } } modules.push(moduleInfo); } const result: DiscoveryResult = { success: errors.length === 0, totalFound: modules.length, successfullyLoaded: modules.filter((m) => m.loaded).length, failed: errors.length, modules, errors, duration: Date.now() - startTime, }; this.discoveryResults.push(result); if (verbose) { this.logDiscoveryResult(result); } return result; } finally { this.isDiscovering = false; } } /** * 레이아웃 모듈 발견 */ private async discoverLayoutModules(pattern: string): Promise Promise>> { try { // Vite의 import.meta.glob 사용 if (typeof import.meta !== "undefined" && import.meta.glob) { return import.meta.glob(pattern, { eager: false }); } // Next.js의 경우 또는 fallback return await this.discoverModulesViaWebpack(pattern); } catch (error) { console.warn("자동 디스커버리 실패, 수동 import로 전환:", error); return {}; } } /** * Webpack 기반 모듈 디스커버리 (Next.js용) */ private async discoverModulesViaWebpack(pattern: string): Promise Promise>> { // Next.js 환경에서는 require.context 사용 if (typeof require !== "undefined" && require.context) { const context = require.context("../layouts", true, /.*LayoutRenderer\.tsx$/); const modules: Record Promise> = {}; context.keys().forEach((key: string) => { modules[key] = () => Promise.resolve(context(key)); }); return modules; } return {}; } /** * 레이아웃 모듈 로드 */ private async loadLayoutModule(moduleFactory: () => Promise, moduleInfo: LayoutModuleInfo): Promise { const module = await moduleFactory(); // default export가 있는 경우 if (module.default && typeof module.default.registerSelf === "function") { module.default.registerSelf(); return; } // named export 중에 레이아웃 렌더러가 있는 경우 for (const [exportName, exportValue] of Object.entries(module)) { if (exportValue && typeof (exportValue as any).registerSelf === "function") { (exportValue as any).registerSelf(); return; } } throw new Error(`레이아웃 렌더러를 찾을 수 없습니다: ${moduleInfo.path}`); } /** * 경로에서 레이아웃 ID 추출 */ private extractLayoutId(path: string): string { const match = path.match(/\/([^/]+)LayoutRenderer\.tsx$/); return match ? match[1].toLowerCase() : "unknown"; } /** * 경로에서 레이아웃 이름 추출 */ private extractLayoutName(path: string): string { const id = this.extractLayoutId(path); return id.charAt(0).toUpperCase() + id.slice(1) + " Layout"; } /** * 디스커버리 결과 로그 출력 */ private logDiscoveryResult(result: DiscoveryResult): void { console.group("📊 레이아웃 디스커버리 결과"); console.log(`⏱️ 소요 시간: ${result.duration}ms`); console.log(`📦 발견된 모듈: ${result.totalFound}개`); console.log(`✅ 성공적으로 로드: ${result.successfullyLoaded}개`); console.log(`❌ 실패: ${result.failed}개`); if (result.modules.length > 0) { console.table( result.modules.map((m) => ({ ID: m.id, Name: m.name, Loaded: m.loaded ? "✅" : "❌", Path: m.path, })), ); } if (result.errors.length > 0) { console.group("❌ 오류 상세:"); result.errors.forEach((error) => console.error(error)); console.groupEnd(); } console.groupEnd(); } /** * 이전 디스커버리 결과 조회 */ getDiscoveryHistory(): DiscoveryResult[] { return [...this.discoveryResults]; } /** * 통계 정보 조회 */ getStats(): { totalAttempts: number; successRate: number; avgDuration: number } { const attempts = this.discoveryResults.length; const successful = this.discoveryResults.filter((r) => r.success).length; const avgDuration = attempts > 0 ? this.discoveryResults.reduce((sum, r) => sum + r.duration, 0) / attempts : 0; return { totalAttempts: attempts, successRate: attempts > 0 ? (successful / attempts) * 100 : 0, avgDuration: Math.round(avgDuration), }; } } /** * 편의 함수: 레이아웃 자동 디스커버리 실행 */ export async function discoverLayouts(options?: AutoDiscoveryOptions): Promise { const discovery = LayoutAutoDiscovery.getInstance(); return discovery.discover(options); } /** * 편의 함수: 통계 정보 조회 */ export function getDiscoveryStats() { const discovery = LayoutAutoDiscovery.getInstance(); return discovery.getStats(); }