ERP-node/frontend/lib/registry/utils/autoDiscovery.ts

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();
}