#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); /** * 레이아웃 스캐폴딩 CLI 도구 * * 사용법: * npm run create-layout [options] * * 예시: * npm run create-layout accordion * npm run create-layout sidebar --category=navigation --zones=3 */ // 명령행 인자 파싱 const args = process.argv.slice(2); const layoutName = args[0]; if (!layoutName) { console.error("❌ 레이아웃 이름이 필요합니다."); console.log("사용법: npm run create-layout [options]"); console.log("예시: npm run create-layout accordion --category=navigation"); process.exit(1); } // 옵션 파싱 const options = {}; args.slice(1).forEach((arg) => { if (arg.startsWith("--")) { const [key, value] = arg.slice(2).split("="); options[key] = value || true; } }); /** * 하이픈을 카멜케이스로 변환 */ function toCamelCase(str) { return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase()); } /** * 하이픈을 파스칼케이스로 변환 */ function toPascalCase(str) { const camelCase = toCamelCase(str); return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); } /** * 안전한 식별자명 생성 (하이픈 제거) */ function toSafeId(str) { return str.toLowerCase().replace(/[^a-z0-9]/g, ""); } // 안전한 이름들 생성 const safeLayoutName = toCamelCase(layoutName); const pascalLayoutName = toPascalCase(layoutName); const safeId = layoutName.toLowerCase(); // kebab-case는 id로 유지 // 기본 옵션 const config = { name: safeLayoutName, id: safeId, className: pascalLayoutName, category: options.category || "basic", zones: parseInt(options.zones) || 2, description: options.description || `${safeLayoutName} 레이아웃입니다.`, author: options.author || "Developer", ...options, }; // 검증 if (!/^[a-z][a-z0-9-]*$/.test(config.id)) { console.error("❌ 레이아웃 이름은 소문자로 시작하고 소문자, 숫자, 하이픈만 포함해야 합니다."); process.exit(1); } const layoutDir = path.join(__dirname, "../lib/registry/layouts", config.id); // 디렉토리 존재 확인 if (fs.existsSync(layoutDir)) { console.error(`❌ 레이아웃 디렉토리가 이미 존재합니다: ${layoutDir}`); process.exit(1); } console.log("🚀 새 레이아웃 생성 중..."); console.log(`📁 이름: ${config.name}`); console.log(`🔖 ID: ${config.id}`); console.log(`📂 카테고리: ${config.category}`); console.log(`🎯 존 개수: ${config.zones}`); // 디렉토리 생성 fs.mkdirSync(layoutDir, { recursive: true }); // 템플릿 파일들 생성 createIndexFile(); createLayoutComponent(); createLayoutRenderer(); createConfigFile(); createTypesFile(); createReadme(); // package.json 스크립트 업데이트 updatePackageScripts(); console.log(""); console.log("✅ 레이아웃 생성 완료!"); console.log(""); console.log("📝 다음 단계:"); console.log(`1. ${layoutDir}/${config.className}Layout.tsx 에서 비즈니스 로직 구현`); console.log("2. 파일을 저장하면 자동으로 화면편집기에서 사용 가능"); console.log("3. 필요에 따라 config.ts에서 기본 설정 조정"); console.log(""); console.log("🔧 개발 팁:"); console.log("- 브라우저 개발자 도구에서 window.__LAYOUT_REGISTRY__.list() 로 등록 확인"); console.log("- Hot Reload 지원으로 파일 수정 시 자동 업데이트"); /** * index.ts 파일 생성 */ function createIndexFile() { const content = `"use client"; import { createLayoutDefinition } from "../../utils/createLayoutDefinition"; import { ${config.className}Layout } from "./${config.className}Layout"; import { ${config.className}LayoutRenderer } from "./${config.className}LayoutRenderer"; import { LayoutRendererProps } from "../BaseLayoutRenderer"; import React from "react"; /** * ${config.name} 래퍼 컴포넌트 (DynamicLayoutRenderer용) */ const ${config.className}LayoutWrapper: React.FC = (props) => { const renderer = new ${config.className}LayoutRenderer(props); return renderer.render(); }; /** * ${config.name} 레이아웃 정의 */ export const ${config.className}LayoutDefinition = createLayoutDefinition({ id: "${config.id}", name: "${config.name}", nameEng: "${config.className} Layout", description: "${config.description}", category: "${config.category}", icon: "${config.icon || config.id}", component: ${config.className}LayoutWrapper, defaultConfig: { ${toCamelCase(config.id)}: { // TODO: 레이아웃별 설정 정의 }, }, defaultZones: [${generateDefaultZones()} ], tags: ["${config.id}", "${config.category}", "layout"], version: "1.0.0", author: "${config.author}", documentation: "${config.description}", }); // 자동 등록을 위한 export export { ${config.className}LayoutRenderer } from "./${config.className}LayoutRenderer"; `; fs.writeFileSync(path.join(layoutDir, "index.ts"), content); console.log("✅ index.ts 생성됨"); } /** * 레이아웃 컴포넌트 파일 생성 */ function createLayoutComponent() { const content = `"use client"; import React from "react"; import { LayoutRendererProps } from "../BaseLayoutRenderer"; /** * ${config.name} 컴포넌트 */ export interface ${config.className}LayoutProps extends LayoutRendererProps { renderer: any; // ${config.className}LayoutRenderer 타입 } export const ${config.className}Layout: React.FC<${config.className}LayoutProps> = ({ layout, isDesignMode = false, isSelected = false, onClick, className = "", renderer, ...props }) => { if (!layout.layoutConfig.${toCamelCase(config.id)}) { return (
${config.name} 설정이 없습니다.
layoutConfig.${toCamelCase(config.id)}가 필요합니다.
); } const ${config.name}Config = layout.layoutConfig.${toCamelCase(config.id)}; const containerStyle = renderer.getLayoutContainerStyle(); // ${config.name} 컨테이너 스타일 const ${config.name}Style: React.CSSProperties = { ...containerStyle, // TODO: 레이아웃 전용 스타일 정의 height: "100%", width: "100%", }; // 디자인 모드 스타일 if (isDesignMode) { ${config.name}Style.border = isSelected ? "2px solid #3b82f6" : "1px solid #e2e8f0"; ${config.name}Style.borderRadius = "8px"; } return (
{layout.zones.map((zone: any) => { const zoneChildren = renderer.getZoneChildren(zone.id); // TODO: 존별 스타일 정의 const zoneStyle: React.CSSProperties = { // 레이아웃별 존 스타일 구현 }; return renderer.renderZone(zone, zoneChildren, { style: zoneStyle, className: "${config.id}-zone", }); })} {/* 디자인 모드에서 빈 영역 표시 */} {isDesignMode && layout.zones.length === 0 && (
${config.name}에 존을 추가하세요
)}
); }; `; fs.writeFileSync(path.join(layoutDir, `${config.className}Layout.tsx`), content); console.log(`✅ ${config.className}Layout.tsx 생성됨`); } /** * 레이아웃 렌더러 파일 생성 */ function createLayoutRenderer() { const content = `"use client"; import { AutoRegisteringLayoutRenderer } from "../AutoRegisteringLayoutRenderer"; import { LayoutRendererProps } from "../BaseLayoutRenderer"; import { ${config.className}LayoutDefinition } from "./index"; import { ${config.className}Layout } from "./${config.className}Layout"; /** * ${config.name} 렌더러 (새 구조) */ export class ${config.className}LayoutRenderer extends AutoRegisteringLayoutRenderer { /** * 레이아웃 정의 (자동 등록용) */ static readonly layoutDefinition = ${config.className}LayoutDefinition; /** * 클래스 로드 시 자동 등록 실행 */ static { this.registerSelf(); } /** * 렌더링 실행 */ render(): React.ReactElement { return <${config.className}Layout {...this.props} renderer={this} />; } } /** * React 함수 컴포넌트로 래핑 (외부 사용용) */ export const ${config.className}LayoutComponent: React.FC = (props) => { const renderer = new ${config.className}LayoutRenderer(props); return renderer.render(); }; // 개발 모드에서 Hot Reload 지원 if (process.env.NODE_ENV === 'development') { // HMR API 등록 if ((module as any).hot) { (module as any).hot.accept(); (module as any).hot.dispose(() => { ${config.className}LayoutRenderer.unregisterSelf(); }); } } `; fs.writeFileSync(path.join(layoutDir, `${config.className}LayoutRenderer.tsx`), content); console.log(`✅ ${config.className}LayoutRenderer.tsx 생성됨`); } /** * 설정 파일 생성 */ function createConfigFile() { const content = `/** * ${config.name} 기본 설정 */ export const ${config.className}LayoutConfig = { defaultConfig: { ${config.id}: { // TODO: 레이아웃 전용 설정 정의 // 예시: // spacing: 16, // orientation: "vertical", // allowResize: true, }, }, defaultZones: [${generateDefaultZones()} ], // 설정 스키마 (검증용) configSchema: { type: "object", properties: { ${config.id}: { type: "object", properties: { // TODO: 설정 스키마 정의 }, additionalProperties: false, }, }, required: ["${config.id}"], }, }; `; fs.writeFileSync(path.join(layoutDir, "config.ts"), content); console.log("✅ config.ts 생성됨"); } /** * 타입 정의 파일 생성 */ function createTypesFile() { const content = `import { LayoutRendererProps } from "../BaseLayoutRenderer"; /** * ${config.name} 설정 타입 */ export interface ${config.className}Config { // TODO: 레이아웃 전용 설정 타입 정의 // 예시: // spacing?: number; // orientation?: "vertical" | "horizontal"; // allowResize?: boolean; } /** * ${config.name} Props 타입 */ export interface ${config.className}LayoutProps extends LayoutRendererProps { renderer: any; // ${config.className}LayoutRenderer 타입 } /** * ${config.name} 존 타입 */ export interface ${config.className}Zone { id: string; name: string; // TODO: 존별 전용 속성 정의 } `; fs.writeFileSync(path.join(layoutDir, "types.ts"), content); console.log("✅ types.ts 생성됨"); } /** * README 파일 생성 */ function createReadme() { const content = `# ${config.name} ${config.description} ## 사용법 이 레이아웃은 자동으로 등록되어 화면편집기에서 사용할 수 있습니다. ## 구성 - \`${config.className}Layout.tsx\`: 메인 레이아웃 컴포넌트 - \`${config.className}LayoutRenderer.tsx\`: 렌더러 (자동 등록) - \`config.ts\`: 기본 설정 - \`types.ts\`: 타입 정의 - \`index.ts\`: 진입점 ## 개발 1. \`${config.className}Layout.tsx\`에서 레이아웃 로직 구현 2. \`config.ts\`에서 기본 설정 조정 3. \`types.ts\`에서 타입 정의 추가 ## 설정 \`\`\`typescript { ${config.id}: { // TODO: 설정 옵션 문서화 } } \`\`\` ## 존 구성 ${generateZoneDocumentation()} --- 생성일: ${new Date().toLocaleDateString()} 버전: 1.0.0 작성자: ${config.author} `; fs.writeFileSync(path.join(layoutDir, "README.md"), content); console.log("✅ README.md 생성됨"); } /** * 기본 존 생성 */ function generateDefaultZones() { const zones = []; for (let i = 1; i <= config.zones; i++) { zones.push(` { id: "zone${i}", name: "존 ${i}", position: {}, size: { width: "100%", height: "100%" }, }`); } return zones.join(","); } /** * 존 문서화 */ function generateZoneDocumentation() { const docs = []; for (let i = 1; i <= config.zones; i++) { docs.push(`- **존 ${i}** (\`zone${i}\`): 기본 영역`); } return docs.join("\n"); } /** * package.json 스크립트 업데이트 */ function updatePackageScripts() { const packagePath = path.join(__dirname, "../package.json"); if (fs.existsSync(packagePath)) { try { const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); if (!packageJson.scripts) { packageJson.scripts = {}; } if (!packageJson.scripts["create-layout"]) { packageJson.scripts["create-layout"] = "node scripts/create-layout.js"; fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); console.log("✅ package.json 스크립트 추가됨"); } } catch (error) { console.warn("⚠️ package.json 업데이트 실패:", error.message); } } }