2025-09-11 18:38:28 +09:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-12 16:47:02 +09:00
|
|
|
* 화면 관리 시스템 컴포넌트 자동 생성 CLI 스크립트
|
|
|
|
|
* 실제 컴포넌트 구조에 맞게 설계
|
2025-09-11 18:38:28 +09:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
const path = require("path");
|
|
|
|
|
|
|
|
|
|
// CLI 인자 파싱
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
const componentName = args[0];
|
2025-09-12 16:47:02 +09:00
|
|
|
const displayName = args[1];
|
|
|
|
|
const description = args[2];
|
|
|
|
|
const category = args[3];
|
|
|
|
|
const webType = args[4] || "text";
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
function validateInputs() {
|
|
|
|
|
if (!componentName) {
|
|
|
|
|
console.error("❌ 컴포넌트 이름을 제공해주세요.");
|
|
|
|
|
showUsage();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2025-09-11 18:38:28 +09:00
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
if (!displayName) {
|
|
|
|
|
console.error("❌ 표시 이름을 제공해주세요.");
|
|
|
|
|
showUsage();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!description) {
|
|
|
|
|
console.error("❌ 설명을 제공해주세요.");
|
|
|
|
|
showUsage();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!category) {
|
|
|
|
|
console.error("❌ 카테고리를 제공해주세요.");
|
|
|
|
|
showUsage();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 이름 형식 검증 (kebab-case)
|
|
|
|
|
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(componentName)) {
|
|
|
|
|
console.error("❌ 컴포넌트 이름은 kebab-case 형식이어야 합니다. (예: text-input, date-picker)");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 카테고리 검증
|
|
|
|
|
const validCategories = ['input', 'display', 'action', 'layout', 'form', 'chart', 'media', 'navigation', 'feedback', 'utility', 'container', 'system', 'admin'];
|
|
|
|
|
if (!validCategories.includes(category)) {
|
|
|
|
|
console.error(`❌ 유효하지 않은 카테고리입니다. 사용 가능한 카테고리: ${validCategories.join(', ')}`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 웹타입 검증
|
|
|
|
|
const validWebTypes = ['text', 'number', 'email', 'password', 'textarea', 'select', 'button', 'checkbox', 'radio', 'date', 'file'];
|
|
|
|
|
if (webType && !validWebTypes.includes(webType)) {
|
|
|
|
|
console.error(`❌ 유효하지 않은 웹타입입니다. 사용 가능한 웹타입: ${validWebTypes.join(', ')}`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showUsage() {
|
|
|
|
|
console.log("\n📖 사용법:");
|
|
|
|
|
console.log("node scripts/create-component.js <컴포넌트이름> <표시이름> <설명> <카테고리> [웹타입]");
|
|
|
|
|
console.log("\n📋 예시:");
|
|
|
|
|
console.log("node scripts/create-component.js text-input '텍스트 입력' '기본 텍스트 입력 컴포넌트' input text");
|
|
|
|
|
console.log("node scripts/create-component.js action-button '액션 버튼' '사용자 액션 버튼' action button");
|
|
|
|
|
console.log("\n📂 카테고리: input, display, action, layout, form, chart, media, navigation, feedback, utility");
|
|
|
|
|
console.log("🎯 웹타입: text, number, email, password, textarea, select, button, checkbox, radio, date, file");
|
|
|
|
|
console.log("\n📚 자세한 사용법: docs/CLI_컴포넌트_생성_가이드.md");
|
2025-09-11 18:38:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
validateInputs();
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
// 옵션 파싱
|
|
|
|
|
const options = {};
|
2025-09-12 16:47:02 +09:00
|
|
|
args.slice(5).forEach((arg) => {
|
2025-09-11 18:38:28 +09:00
|
|
|
if (arg.startsWith("--")) {
|
|
|
|
|
const [key, value] = arg.substring(2).split("=");
|
|
|
|
|
options[key] = value || true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 기본값 설정
|
|
|
|
|
const config = {
|
|
|
|
|
name: componentName,
|
2025-09-12 16:47:02 +09:00
|
|
|
displayName: displayName || componentName,
|
|
|
|
|
description: description || `${displayName || componentName} 컴포넌트`,
|
|
|
|
|
category: category || "display",
|
|
|
|
|
webType: webType,
|
|
|
|
|
author: options.author || "개발팀",
|
2025-09-11 18:38:28 +09:00
|
|
|
size: options.size || "200x36",
|
|
|
|
|
tags: options.tags ? options.tags.split(",") : [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 이름 변환 함수들
|
|
|
|
|
function toCamelCase(str) {
|
|
|
|
|
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toPascalCase(str) {
|
|
|
|
|
return str.charAt(0).toUpperCase() + toCamelCase(str).slice(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toKebabCase(str) {
|
|
|
|
|
return str
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9]/g, "-")
|
|
|
|
|
.replace(/-+/g, "-")
|
|
|
|
|
.replace(/^-|-$/g, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 안전한 이름 생성 (하이픈 제거)
|
|
|
|
|
function toSafeName(str) {
|
|
|
|
|
return toCamelCase(str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파싱된 이름들
|
|
|
|
|
const names = {
|
|
|
|
|
kebab: toKebabCase(componentName),
|
|
|
|
|
camel: toSafeName(componentName),
|
|
|
|
|
pascal: toPascalCase(componentName),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1. index.ts 파일 생성
|
2025-09-12 16:47:02 +09:00
|
|
|
function createIndexFile(componentDir, names, config, width, height) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
|
|
|
import { ComponentCategory } from "@/types/component";
|
|
|
|
|
import type { WebType } from "@/types/screen";
|
|
|
|
|
import { ${names.pascal}Wrapper } from "./${names.pascal}Component";
|
|
|
|
|
import { ${names.pascal}ConfigPanel } from "./${names.pascal}ConfigPanel";
|
|
|
|
|
import { ${names.pascal}Config } from "./types";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 컴포넌트 정의
|
|
|
|
|
* ${config.description}
|
|
|
|
|
*/
|
|
|
|
|
export const ${names.pascal}Definition = createComponentDefinition({
|
|
|
|
|
id: "${names.kebab}",
|
2025-09-12 16:47:02 +09:00
|
|
|
name: "${config.displayName}",
|
2025-09-11 18:38:28 +09:00
|
|
|
nameEng: "${names.pascal} Component",
|
2025-09-12 16:47:02 +09:00
|
|
|
description: "${config.description}",
|
2025-09-11 18:38:28 +09:00
|
|
|
category: ComponentCategory.${config.category.toUpperCase()},
|
|
|
|
|
webType: "${config.webType}",
|
|
|
|
|
component: ${names.pascal}Wrapper,
|
|
|
|
|
defaultConfig: {
|
|
|
|
|
${getDefaultConfigByWebType(config.webType)}
|
|
|
|
|
},
|
|
|
|
|
defaultSize: { width: ${width}, height: ${height} },
|
|
|
|
|
configPanel: ${names.pascal}ConfigPanel,
|
|
|
|
|
icon: "${getIconByCategory(config.category)}",
|
|
|
|
|
tags: [${config.tags.map((tag) => `"${tag}"`).join(", ")}],
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
author: "${config.author}",
|
|
|
|
|
documentation: "https://docs.example.com/components/${names.kebab}",
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
// 컴포넌트는 ${names.pascal}Renderer에서 자동 등록됩니다
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
// 타입 내보내기
|
|
|
|
|
export type { ${names.pascal}Config } from "./types";
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, "index.ts"), content);
|
|
|
|
|
console.log("✅ index.ts 생성 완료");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Component 파일 생성
|
2025-09-12 16:47:02 +09:00
|
|
|
function createComponentFile(componentDir, names, config) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { ComponentRendererProps } from "@/types/component";
|
|
|
|
|
import { ${names.pascal}Config } from "./types";
|
|
|
|
|
|
|
|
|
|
export interface ${names.pascal}ComponentProps extends ComponentRendererProps {
|
|
|
|
|
config?: ${names.pascal}Config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 컴포넌트
|
|
|
|
|
* ${config.description}
|
|
|
|
|
*/
|
|
|
|
|
export const ${names.pascal}Component: React.FC<${names.pascal}ComponentProps> = ({
|
|
|
|
|
component,
|
|
|
|
|
isDesignMode = false,
|
|
|
|
|
isSelected = false,
|
2025-09-12 16:47:02 +09:00
|
|
|
isInteractive = false,
|
2025-09-11 18:38:28 +09:00
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
|
|
|
|
config,
|
|
|
|
|
className,
|
|
|
|
|
style,
|
2025-09-12 16:47:02 +09:00
|
|
|
formData,
|
|
|
|
|
onFormDataChange,
|
|
|
|
|
screenId,
|
2025-09-11 18:38:28 +09:00
|
|
|
...props
|
|
|
|
|
}) => {
|
|
|
|
|
// 컴포넌트 설정
|
|
|
|
|
const componentConfig = {
|
|
|
|
|
...config,
|
|
|
|
|
...component.config,
|
|
|
|
|
} as ${names.pascal}Config;
|
|
|
|
|
|
|
|
|
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
|
|
|
|
const componentStyle: React.CSSProperties = {
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
...component.style,
|
|
|
|
|
...style,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 디자인 모드 스타일
|
|
|
|
|
if (isDesignMode) {
|
|
|
|
|
componentStyle.border = "1px dashed #cbd5e1";
|
|
|
|
|
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 이벤트 핸들러
|
|
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onClick?.();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// DOM에 전달하면 안 되는 React-specific props 필터링
|
|
|
|
|
const {
|
|
|
|
|
selectedScreen,
|
|
|
|
|
onZoneComponentDrop,
|
|
|
|
|
onZoneClick,
|
|
|
|
|
componentConfig: _componentConfig,
|
|
|
|
|
component: _component,
|
|
|
|
|
isSelected: _isSelected,
|
|
|
|
|
onClick: _onClick,
|
|
|
|
|
onDragStart: _onDragStart,
|
|
|
|
|
onDragEnd: _onDragEnd,
|
|
|
|
|
size: _size,
|
|
|
|
|
position: _position,
|
|
|
|
|
style: _style,
|
|
|
|
|
...domProps
|
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
${getComponentJSXByWebType(config.webType)}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 래퍼 컴포넌트
|
|
|
|
|
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
|
|
|
|
*/
|
|
|
|
|
export const ${names.pascal}Wrapper: React.FC<${names.pascal}ComponentProps> = (props) => {
|
2025-09-12 16:47:02 +09:00
|
|
|
return <${names.pascal}Component {...props} />;
|
2025-09-11 18:38:28 +09:00
|
|
|
};
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, `${names.pascal}Component.tsx`), content);
|
|
|
|
|
console.log(`✅ ${names.pascal}Component.tsx 생성 완료`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Renderer 파일 생성
|
2025-09-12 16:47:02 +09:00
|
|
|
function createRendererFile(componentDir, names, config) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
|
|
|
import { ${names.pascal}Definition } from "./index";
|
|
|
|
|
import { ${names.pascal}Component } from "./${names.pascal}Component";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 렌더러
|
|
|
|
|
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
|
|
|
|
*/
|
|
|
|
|
export class ${names.pascal}Renderer extends AutoRegisteringComponentRenderer {
|
|
|
|
|
static componentDefinition = ${names.pascal}Definition;
|
|
|
|
|
|
|
|
|
|
render(): React.ReactElement {
|
|
|
|
|
return <${names.pascal}Component {...this.props} renderer={this} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컴포넌트별 특화 메서드들
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ${config.webType} 타입 특화 속성 처리
|
|
|
|
|
protected get${names.pascal}Props() {
|
|
|
|
|
const baseProps = this.getWebTypeProps();
|
|
|
|
|
|
|
|
|
|
// ${config.webType} 타입에 특화된 추가 속성들
|
|
|
|
|
return {
|
|
|
|
|
...baseProps,
|
|
|
|
|
// 여기에 ${config.webType} 타입 특화 속성들 추가
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 값 변경 처리
|
|
|
|
|
protected handleValueChange = (value: any) => {
|
|
|
|
|
this.updateComponent({ value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 포커스 처리
|
|
|
|
|
protected handleFocus = () => {
|
|
|
|
|
// 포커스 로직
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 블러 처리
|
|
|
|
|
protected handleBlur = () => {
|
|
|
|
|
// 블러 로직
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 자동 등록 실행
|
|
|
|
|
${names.pascal}Renderer.registerSelf();
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, `${names.pascal}Renderer.tsx`), content);
|
|
|
|
|
console.log(`✅ ${names.pascal}Renderer.tsx 생성 완료`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Config Panel 파일 생성
|
2025-09-12 16:47:02 +09:00
|
|
|
function createConfigPanelFile(componentDir, names, config) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { ${names.pascal}Config } from "./types";
|
|
|
|
|
|
|
|
|
|
export interface ${names.pascal}ConfigPanelProps {
|
|
|
|
|
config: ${names.pascal}Config;
|
|
|
|
|
onChange: (config: Partial<${names.pascal}Config>) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 설정 패널
|
|
|
|
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
|
|
|
|
*/
|
|
|
|
|
export const ${names.pascal}ConfigPanel: React.FC<${names.pascal}ConfigPanelProps> = ({
|
|
|
|
|
config,
|
|
|
|
|
onChange,
|
|
|
|
|
}) => {
|
|
|
|
|
const handleChange = (key: keyof ${names.pascal}Config, value: any) => {
|
|
|
|
|
onChange({ [key]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="text-sm font-medium">
|
2025-09-12 16:47:02 +09:00
|
|
|
${config.description} 설정
|
2025-09-11 18:38:28 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${getConfigPanelJSXByWebType(config.webType)}
|
|
|
|
|
|
|
|
|
|
{/* 공통 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="disabled">비활성화</Label>
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="disabled"
|
|
|
|
|
checked={config.disabled || false}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("disabled", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="required">필수 입력</Label>
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="required"
|
|
|
|
|
checked={config.required || false}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("required", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="readonly">읽기 전용</Label>
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="readonly"
|
|
|
|
|
checked={config.readonly || false}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, `${names.pascal}ConfigPanel.tsx`), content);
|
|
|
|
|
console.log(`✅ ${names.pascal}ConfigPanel.tsx 생성 완료`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Types 파일 생성
|
2025-09-12 16:47:02 +09:00
|
|
|
function createTypesFile(componentDir, names, config) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `"use client";
|
|
|
|
|
|
|
|
|
|
import { ComponentConfig } from "@/types/component";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 컴포넌트 설정 타입
|
|
|
|
|
*/
|
|
|
|
|
export interface ${names.pascal}Config extends ComponentConfig {
|
|
|
|
|
${getConfigTypesByWebType(config.webType)}
|
|
|
|
|
|
|
|
|
|
// 공통 설정
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
required?: boolean;
|
|
|
|
|
readonly?: boolean;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
helperText?: string;
|
|
|
|
|
|
|
|
|
|
// 스타일 관련
|
|
|
|
|
variant?: "default" | "outlined" | "filled";
|
|
|
|
|
size?: "sm" | "md" | "lg";
|
|
|
|
|
|
|
|
|
|
// 이벤트 관련
|
|
|
|
|
onChange?: (value: any) => void;
|
|
|
|
|
onFocus?: () => void;
|
|
|
|
|
onBlur?: () => void;
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ${names.pascal} 컴포넌트 Props 타입
|
|
|
|
|
*/
|
|
|
|
|
export interface ${names.pascal}Props {
|
|
|
|
|
id?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
value?: any;
|
|
|
|
|
config?: ${names.pascal}Config;
|
|
|
|
|
className?: string;
|
|
|
|
|
style?: React.CSSProperties;
|
|
|
|
|
|
|
|
|
|
// 이벤트 핸들러
|
|
|
|
|
onChange?: (value: any) => void;
|
|
|
|
|
onFocus?: () => void;
|
|
|
|
|
onBlur?: () => void;
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, "types.ts"), content);
|
|
|
|
|
console.log("✅ types.ts 생성 완료");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
// README 파일 생성 (config.ts는 제거)
|
|
|
|
|
function createReadmeFile(componentDir, names, config, width, height) {
|
2025-09-11 18:38:28 +09:00
|
|
|
const content = `# ${names.pascal} 컴포넌트
|
|
|
|
|
|
|
|
|
|
${config.description}
|
|
|
|
|
|
|
|
|
|
## 개요
|
|
|
|
|
|
|
|
|
|
- **ID**: \`${names.kebab}\`
|
|
|
|
|
- **카테고리**: ${config.category}
|
|
|
|
|
- **웹타입**: ${config.webType}
|
|
|
|
|
- **작성자**: ${config.author}
|
|
|
|
|
- **버전**: 1.0.0
|
|
|
|
|
|
|
|
|
|
## 특징
|
|
|
|
|
|
|
|
|
|
- ✅ 자동 등록 시스템
|
|
|
|
|
- ✅ 타입 안전성
|
|
|
|
|
- ✅ Hot Reload 지원
|
|
|
|
|
- ✅ 설정 패널 제공
|
|
|
|
|
- ✅ 반응형 디자인
|
|
|
|
|
|
|
|
|
|
## 사용법
|
|
|
|
|
|
|
|
|
|
### 기본 사용법
|
|
|
|
|
|
|
|
|
|
\`\`\`tsx
|
|
|
|
|
import { ${names.pascal}Component } from "@/lib/registry/components/${names.kebab}";
|
|
|
|
|
|
|
|
|
|
<${names.pascal}Component
|
|
|
|
|
component={{
|
|
|
|
|
id: "my-${names.kebab}",
|
|
|
|
|
type: "widget",
|
|
|
|
|
webType: "${config.webType}",
|
|
|
|
|
position: { x: 100, y: 100, z: 1 },
|
|
|
|
|
size: { width: ${width}, height: ${height} },
|
|
|
|
|
config: {
|
|
|
|
|
// 설정값들
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
/>
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
### 설정 옵션
|
|
|
|
|
|
|
|
|
|
| 속성 | 타입 | 기본값 | 설명 |
|
|
|
|
|
|------|------|--------|------|
|
|
|
|
|
${getConfigDocumentationByWebType(config.webType)}
|
|
|
|
|
| disabled | boolean | false | 비활성화 여부 |
|
|
|
|
|
| required | boolean | false | 필수 입력 여부 |
|
|
|
|
|
| readonly | boolean | false | 읽기 전용 여부 |
|
|
|
|
|
|
|
|
|
|
## 이벤트
|
|
|
|
|
|
|
|
|
|
- \`onChange\`: 값 변경 시
|
|
|
|
|
- \`onFocus\`: 포커스 시
|
|
|
|
|
- \`onBlur\`: 포커스 해제 시
|
|
|
|
|
- \`onClick\`: 클릭 시
|
|
|
|
|
|
|
|
|
|
## 스타일링
|
|
|
|
|
|
|
|
|
|
컴포넌트는 다음과 같은 스타일 옵션을 제공합니다:
|
|
|
|
|
|
|
|
|
|
- \`variant\`: "default" | "outlined" | "filled"
|
|
|
|
|
- \`size\`: "sm" | "md" | "lg"
|
|
|
|
|
|
|
|
|
|
## 예시
|
|
|
|
|
|
|
|
|
|
\`\`\`tsx
|
|
|
|
|
// 기본 예시
|
|
|
|
|
<${names.pascal}Component
|
|
|
|
|
component={{
|
|
|
|
|
id: "sample-${names.kebab}",
|
|
|
|
|
config: {
|
|
|
|
|
placeholder: "입력하세요",
|
|
|
|
|
required: true,
|
|
|
|
|
variant: "outlined"
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
## 개발자 정보
|
|
|
|
|
|
|
|
|
|
- **생성일**: ${new Date().toISOString().split("T")[0]}
|
2025-09-12 16:47:02 +09:00
|
|
|
- **CLI 명령어**: \`node scripts/create-component.js ${componentName} "${config.displayName}" "${config.description}" ${config.category} ${config.webType}\`
|
2025-09-11 18:38:28 +09:00
|
|
|
- **경로**: \`lib/registry/components/${names.kebab}/\`
|
|
|
|
|
|
|
|
|
|
## 관련 문서
|
|
|
|
|
|
|
|
|
|
- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md)
|
|
|
|
|
- [개발자 문서](https://docs.example.com/components/${names.kebab})
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(componentDir, "README.md"), content);
|
|
|
|
|
console.log("✅ README.md 생성 완료");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
|
|
|
|
// index.ts 파일에 import 자동 추가 함수
|
|
|
|
|
function addToRegistryIndex(names) {
|
|
|
|
|
const indexFilePath = path.join(__dirname, "../lib/registry/components/index.ts");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 기존 파일 읽기
|
|
|
|
|
const existingContent = fs.readFileSync(indexFilePath, "utf8");
|
|
|
|
|
|
|
|
|
|
// 새로운 import 구문
|
|
|
|
|
const newImport = `import "./${names.kebab}/${names.pascal}Renderer";`;
|
|
|
|
|
|
|
|
|
|
// 이미 존재하는지 확인
|
|
|
|
|
if (existingContent.includes(newImport)) {
|
|
|
|
|
console.log("⚠️ import가 이미 존재합니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기존 import들 찾기 (마지막 import 이후에 추가)
|
|
|
|
|
const lines = existingContent.split('\n');
|
|
|
|
|
const lastImportIndex = lines.findLastIndex(line => line.trim().startsWith('import ') && line.includes('Renderer'));
|
|
|
|
|
|
|
|
|
|
if (lastImportIndex !== -1) {
|
|
|
|
|
// 마지막 import 다음에 새로운 import 추가
|
|
|
|
|
lines.splice(lastImportIndex + 1, 0, newImport);
|
|
|
|
|
} else {
|
|
|
|
|
// import가 없으면 기존 import 구역 끝에 추가
|
|
|
|
|
const importSectionEnd = lines.findIndex(line => line.trim() === '' && lines.indexOf(line) > 10);
|
|
|
|
|
if (importSectionEnd !== -1) {
|
|
|
|
|
lines.splice(importSectionEnd, 0, newImport);
|
|
|
|
|
} else {
|
|
|
|
|
// 적절한 위치를 찾지 못했으면 끝에 추가
|
|
|
|
|
lines.push(newImport);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일에 다시 쓰기
|
|
|
|
|
const newContent = lines.join('\n');
|
|
|
|
|
fs.writeFileSync(indexFilePath, newContent);
|
|
|
|
|
console.log("✅ index.ts에 import 자동 추가 완료");
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("⚠️ index.ts 업데이트 중 오류:", error.message);
|
|
|
|
|
console.log(`📝 수동으로 다음을 추가해주세요:`);
|
|
|
|
|
console.log(` ${newImport}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
// 헬퍼 함수들
|
|
|
|
|
function getDefaultConfigByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
return `placeholder: "텍스트를 입력하세요",
|
|
|
|
|
maxLength: 255,`;
|
|
|
|
|
case "number":
|
|
|
|
|
return `min: 0,
|
|
|
|
|
max: 999999,
|
|
|
|
|
step: 1,`;
|
|
|
|
|
case "email":
|
|
|
|
|
return `placeholder: "이메일을 입력하세요",`;
|
|
|
|
|
case "password":
|
|
|
|
|
return `placeholder: "비밀번호를 입력하세요",
|
|
|
|
|
minLength: 8,`;
|
|
|
|
|
case "textarea":
|
|
|
|
|
return `placeholder: "내용을 입력하세요",
|
|
|
|
|
rows: 3,
|
|
|
|
|
maxLength: 1000,`;
|
|
|
|
|
case "select":
|
|
|
|
|
return `options: [],
|
|
|
|
|
placeholder: "선택하세요",`;
|
|
|
|
|
case "button":
|
|
|
|
|
return `text: "버튼",
|
|
|
|
|
actionType: "button",
|
|
|
|
|
variant: "primary",`;
|
|
|
|
|
default:
|
|
|
|
|
return `placeholder: "입력하세요",`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getIconByCategory(category) {
|
|
|
|
|
const icons = {
|
|
|
|
|
ui: "Square",
|
|
|
|
|
input: "Edit",
|
|
|
|
|
display: "Eye",
|
|
|
|
|
action: "MousePointer",
|
|
|
|
|
layout: "Layout",
|
|
|
|
|
chart: "BarChart",
|
|
|
|
|
form: "FormInput",
|
|
|
|
|
media: "Image",
|
|
|
|
|
navigation: "Menu",
|
|
|
|
|
feedback: "Bell",
|
|
|
|
|
utility: "Settings",
|
|
|
|
|
container: "Package",
|
|
|
|
|
system: "Cpu",
|
|
|
|
|
admin: "Shield",
|
|
|
|
|
};
|
|
|
|
|
return icons[category] || "Component";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getComponentJSXByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "password":
|
|
|
|
|
case "number":
|
|
|
|
|
return ` return (
|
|
|
|
|
<div style={componentStyle} className={className} {...domProps}>
|
|
|
|
|
{/* 라벨 렌더링 */}
|
|
|
|
|
{component.label && (
|
|
|
|
|
<label
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "-25px",
|
|
|
|
|
left: "0px",
|
|
|
|
|
fontSize: component.style?.labelFontSize || "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
color: component.style?.labelColor || "#212121",
|
2025-09-11 18:38:28 +09:00
|
|
|
fontWeight: "500",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{component.label}
|
|
|
|
|
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
type="${webType}"
|
|
|
|
|
value={component.value || ""}
|
|
|
|
|
placeholder={componentConfig.placeholder || ""}
|
|
|
|
|
disabled={componentConfig.disabled || false}
|
|
|
|
|
required={componentConfig.required || false}
|
|
|
|
|
readOnly={componentConfig.readonly || false}
|
|
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
border: "1px solid #d1d5db",
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
padding: "8px 12px",
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
outline: "none",
|
|
|
|
|
}}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
if (domProps.onChange) {
|
|
|
|
|
domProps.onChange(e.target.value);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);`;
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return ` return (
|
|
|
|
|
<div style={componentStyle} className={className} {...domProps}>
|
|
|
|
|
{/* 라벨 렌더링 */}
|
|
|
|
|
{component.label && (
|
|
|
|
|
<label
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "-25px",
|
|
|
|
|
left: "0px",
|
|
|
|
|
fontSize: component.style?.labelFontSize || "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
color: component.style?.labelColor || "#212121",
|
2025-09-11 18:38:28 +09:00
|
|
|
fontWeight: "500",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{component.label}
|
|
|
|
|
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
|
value={component.value || ""}
|
|
|
|
|
placeholder={componentConfig.placeholder || ""}
|
|
|
|
|
disabled={componentConfig.disabled || false}
|
|
|
|
|
required={componentConfig.required || false}
|
|
|
|
|
readOnly={componentConfig.readonly || false}
|
|
|
|
|
rows={componentConfig.rows || 3}
|
|
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
border: "1px solid #d1d5db",
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
padding: "8px 12px",
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
outline: "none",
|
|
|
|
|
resize: "none",
|
|
|
|
|
}}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
if (domProps.onChange) {
|
|
|
|
|
domProps.onChange(e.target.value);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);`;
|
|
|
|
|
|
|
|
|
|
case "button":
|
|
|
|
|
return ` return (
|
|
|
|
|
<div style={componentStyle} className={className} {...domProps}>
|
|
|
|
|
<button
|
|
|
|
|
type={componentConfig.actionType || "button"}
|
|
|
|
|
disabled={componentConfig.disabled || false}
|
|
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
border: "1px solid #3b82f6",
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
backgroundColor: "#3b82f6",
|
|
|
|
|
color: "white",
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
fontWeight: "500",
|
|
|
|
|
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
|
|
|
|
outline: "none",
|
|
|
|
|
}}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
>
|
|
|
|
|
{componentConfig.text || component.label || "버튼"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);`;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return ` return (
|
|
|
|
|
<div style={componentStyle} className={className} {...domProps}>
|
|
|
|
|
{/* 라벨 렌더링 */}
|
|
|
|
|
{component.label && (
|
|
|
|
|
<label
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "-25px",
|
|
|
|
|
left: "0px",
|
|
|
|
|
fontSize: component.style?.labelFontSize || "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
color: component.style?.labelColor || "#212121",
|
2025-09-11 18:38:28 +09:00
|
|
|
fontWeight: "500",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{component.label}
|
|
|
|
|
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
border: "1px solid #d1d5db",
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
padding: "8px 12px",
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
backgroundColor: "#f9fafb",
|
|
|
|
|
}}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
>
|
|
|
|
|
{component.value || componentConfig.placeholder || "${webType} 컴포넌트"}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getConfigPanelJSXByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "password":
|
|
|
|
|
return ` {/* 텍스트 관련 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="placeholder">플레이스홀더</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="placeholder"
|
|
|
|
|
value={config.placeholder || ""}
|
|
|
|
|
onChange={(e) => handleChange("placeholder", e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="maxLength">최대 길이</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="maxLength"
|
|
|
|
|
type="number"
|
|
|
|
|
value={config.maxLength || ""}
|
|
|
|
|
onChange={(e) => handleChange("maxLength", parseInt(e.target.value) || undefined)}
|
|
|
|
|
/>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return ` {/* 숫자 관련 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="min">최소값</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="min"
|
|
|
|
|
type="number"
|
|
|
|
|
value={config.min || ""}
|
|
|
|
|
onChange={(e) => handleChange("min", parseFloat(e.target.value) || undefined)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="max">최대값</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="max"
|
|
|
|
|
type="number"
|
|
|
|
|
value={config.max || ""}
|
|
|
|
|
onChange={(e) => handleChange("max", parseFloat(e.target.value) || undefined)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="step">단계</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="step"
|
|
|
|
|
type="number"
|
|
|
|
|
value={config.step || 1}
|
|
|
|
|
onChange={(e) => handleChange("step", parseFloat(e.target.value) || 1)}
|
|
|
|
|
/>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return ` {/* 텍스트영역 관련 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="placeholder">플레이스홀더</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="placeholder"
|
|
|
|
|
value={config.placeholder || ""}
|
|
|
|
|
onChange={(e) => handleChange("placeholder", e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="rows">행 수</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="rows"
|
|
|
|
|
type="number"
|
|
|
|
|
value={config.rows || 3}
|
|
|
|
|
onChange={(e) => handleChange("rows", parseInt(e.target.value) || 3)}
|
|
|
|
|
/>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
case "button":
|
|
|
|
|
return ` {/* 버튼 관련 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="text">버튼 텍스트</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="text"
|
|
|
|
|
value={config.text || ""}
|
|
|
|
|
onChange={(e) => handleChange("text", e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="actionType">액션 타입</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={config.actionType || "button"}
|
|
|
|
|
onValueChange={(value) => handleChange("actionType", value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="button">Button</SelectItem>
|
|
|
|
|
<SelectItem value="submit">Submit</SelectItem>
|
|
|
|
|
<SelectItem value="reset">Reset</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return ` {/* ${webType} 관련 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="placeholder">플레이스홀더</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="placeholder"
|
|
|
|
|
value={config.placeholder || ""}
|
|
|
|
|
onChange={(e) => handleChange("placeholder", e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getConfigTypesByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "password":
|
|
|
|
|
return ` // 텍스트 관련 설정
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
minLength?: number;`;
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return ` // 숫자 관련 설정
|
|
|
|
|
min?: number;
|
|
|
|
|
max?: number;
|
|
|
|
|
step?: number;`;
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return ` // 텍스트영역 관련 설정
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
rows?: number;
|
|
|
|
|
cols?: number;
|
|
|
|
|
maxLength?: number;`;
|
|
|
|
|
|
|
|
|
|
case "button":
|
|
|
|
|
return ` // 버튼 관련 설정
|
|
|
|
|
text?: string;
|
|
|
|
|
actionType?: "button" | "submit" | "reset";
|
|
|
|
|
variant?: "primary" | "secondary" | "danger";`;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return ` // ${webType} 관련 설정
|
|
|
|
|
placeholder?: string;`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getConfigSchemaByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "password":
|
|
|
|
|
return ` placeholder: { type: "string", default: "" },
|
|
|
|
|
maxLength: { type: "number", min: 1 },
|
|
|
|
|
minLength: { type: "number", min: 0 },`;
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return ` min: { type: "number" },
|
|
|
|
|
max: { type: "number" },
|
|
|
|
|
step: { type: "number", default: 1, min: 0.01 },`;
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return ` placeholder: { type: "string", default: "" },
|
|
|
|
|
rows: { type: "number", default: 3, min: 1, max: 20 },
|
|
|
|
|
cols: { type: "number", min: 1 },
|
|
|
|
|
maxLength: { type: "number", min: 1 },`;
|
|
|
|
|
|
|
|
|
|
case "button":
|
|
|
|
|
return ` text: { type: "string", default: "버튼" },
|
|
|
|
|
actionType: {
|
|
|
|
|
type: "enum",
|
|
|
|
|
values: ["button", "submit", "reset"],
|
|
|
|
|
default: "button"
|
|
|
|
|
},
|
|
|
|
|
variant: {
|
|
|
|
|
type: "enum",
|
|
|
|
|
values: ["primary", "secondary", "danger"],
|
|
|
|
|
default: "primary"
|
|
|
|
|
},`;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return ` placeholder: { type: "string", default: "" },`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getConfigDocumentationByWebType(webType) {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "password":
|
|
|
|
|
return `| placeholder | string | "" | 플레이스홀더 텍스트 |
|
|
|
|
|
| maxLength | number | 255 | 최대 입력 길이 |
|
|
|
|
|
| minLength | number | 0 | 최소 입력 길이 |`;
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return `| min | number | - | 최소값 |
|
|
|
|
|
| max | number | - | 최대값 |
|
|
|
|
|
| step | number | 1 | 증감 단위 |`;
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return `| placeholder | string | "" | 플레이스홀더 텍스트 |
|
|
|
|
|
| rows | number | 3 | 표시할 행 수 |
|
|
|
|
|
| maxLength | number | 1000 | 최대 입력 길이 |`;
|
|
|
|
|
|
|
|
|
|
case "button":
|
|
|
|
|
return `| text | string | "버튼" | 버튼 텍스트 |
|
|
|
|
|
| actionType | string | "button" | 버튼 타입 |
|
|
|
|
|
| variant | string | "primary" | 버튼 스타일 |`;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return `| placeholder | string | "" | 플레이스홀더 텍스트 |`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 메인 실행 함수
|
|
|
|
|
async function main() {
|
|
|
|
|
// 크기 파싱
|
|
|
|
|
const [width, height] = config.size.split("x").map(Number);
|
|
|
|
|
if (!width || !height) {
|
|
|
|
|
console.error("❌ 크기 형식이 올바르지 않습니다. 예: 200x36");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 디렉토리 경로
|
|
|
|
|
const componentDir = path.join(__dirname, "../lib/registry/components", names.kebab);
|
|
|
|
|
|
|
|
|
|
console.log("🚀 컴포넌트 생성 시작...");
|
|
|
|
|
console.log(`📁 이름: ${names.camel}`);
|
|
|
|
|
console.log(`🔖 ID: ${names.kebab}`);
|
2025-09-12 16:47:02 +09:00
|
|
|
console.log(`📂 카테고리: ${config.category}`);
|
2025-09-11 18:38:28 +09:00
|
|
|
console.log(`🎯 웹타입: ${config.webType}`);
|
2025-09-12 16:47:02 +09:00
|
|
|
console.log(`🌐 표시이름: ${config.displayName}`);
|
|
|
|
|
console.log(`📝 설명: ${config.description}`);
|
2025-09-11 18:38:28 +09:00
|
|
|
console.log(`📏 크기: ${width}x${height}`);
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 디렉토리 생성
|
|
|
|
|
if (fs.existsSync(componentDir)) {
|
|
|
|
|
console.error(`❌ 컴포넌트 "${names.kebab}"가 이미 존재합니다.`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(componentDir, { recursive: true });
|
|
|
|
|
console.log(`📁 디렉토리 생성: ${componentDir}`);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-12 16:47:02 +09:00
|
|
|
// 파일들 생성 (파라미터 전달하여 호출)
|
|
|
|
|
createIndexFile(componentDir, names, config, width, height);
|
|
|
|
|
createComponentFile(componentDir, names, config);
|
|
|
|
|
createRendererFile(componentDir, names, config);
|
|
|
|
|
createConfigPanelFile(componentDir, names, config);
|
|
|
|
|
createTypesFile(componentDir, names, config);
|
|
|
|
|
createReadmeFile(componentDir, names, config, width, height);
|
|
|
|
|
|
|
|
|
|
// index.ts 파일에 자동으로 import 추가
|
|
|
|
|
addToRegistryIndex(names);
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
|
|
|
console.log("\n🎉 컴포넌트 생성 완료!");
|
|
|
|
|
console.log(`📁 경로: ${componentDir}`);
|
|
|
|
|
console.log(`🔗 다음 단계:`);
|
2025-09-12 16:47:02 +09:00
|
|
|
console.log(` 1. ✅ lib/registry/components/index.ts에 import 자동 추가됨`);
|
2025-09-11 18:38:28 +09:00
|
|
|
console.log(` 2. 브라우저에서 자동 등록 확인`);
|
|
|
|
|
console.log(` 3. 컴포넌트 패널에서 테스트`);
|
|
|
|
|
console.log(`\n🛠️ 개발자 도구 사용법:`);
|
|
|
|
|
console.log(` __COMPONENT_REGISTRY__.get("${names.kebab}")`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 컴포넌트 생성 중 오류 발생:", error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 메인 함수 실행
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
console.error("❌ 실행 중 오류 발생:", error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|