레이아웃 추가기능
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
/**
|
||||
* 레이아웃 스캐폴딩 CLI 도구
|
||||
*
|
||||
* 사용법:
|
||||
* npm run create-layout <layoutName> [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 <layoutName> [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<LayoutRendererProps> = (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 (
|
||||
<div className="error-layout flex items-center justify-center p-4 border-2 border-red-300 bg-red-50 rounded">
|
||||
<div className="text-center text-red-600">
|
||||
<div className="font-medium">${config.name} 설정이 없습니다.</div>
|
||||
<div className="text-sm mt-1">layoutConfig.${toCamelCase(config.id)}가 필요합니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={\`${config.id}-layout \${isDesignMode ? "design-mode" : ""} \${className}\`}
|
||||
style={${config.name}Style}
|
||||
onClick={onClick}
|
||||
draggable={isDesignMode}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragEnd={props.onDragEnd}
|
||||
{...props}
|
||||
>
|
||||
{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 && (
|
||||
<div
|
||||
className="empty-${config.id}-container"
|
||||
style={{
|
||||
flex: 1,
|
||||
border: "2px dashed #cbd5e1",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "rgba(148, 163, 184, 0.05)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "14px",
|
||||
color: "#64748b",
|
||||
minHeight: "100px",
|
||||
padding: "20px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
${config.name}에 존을 추가하세요
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`;
|
||||
|
||||
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<LayoutRendererProps> = (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user