289 lines
7.7 KiB
TypeScript
289 lines
7.7 KiB
TypeScript
"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<DiscoveryResult> {
|
|
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<Record<string, () => Promise<any>>> {
|
|
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<Record<string, () => Promise<any>>> {
|
|
// Next.js 환경에서는 require.context 사용
|
|
if (typeof require !== "undefined" && require.context) {
|
|
const context = require.context("../layouts", true, /.*LayoutRenderer\.tsx$/);
|
|
const modules: Record<string, () => Promise<any>> = {};
|
|
|
|
context.keys().forEach((key: string) => {
|
|
modules[key] = () => Promise.resolve(context(key));
|
|
});
|
|
|
|
return modules;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* 레이아웃 모듈 로드
|
|
*/
|
|
private async loadLayoutModule(moduleFactory: () => Promise<any>, moduleInfo: LayoutModuleInfo): Promise<void> {
|
|
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<DiscoveryResult> {
|
|
const discovery = LayoutAutoDiscovery.getInstance();
|
|
return discovery.discover(options);
|
|
}
|
|
|
|
/**
|
|
* 편의 함수: 통계 정보 조회
|
|
*/
|
|
export function getDiscoveryStats() {
|
|
const discovery = LayoutAutoDiscovery.getInstance();
|
|
return discovery.getStats();
|
|
}
|