Files
vexplor/frontend/scripts/create-component.js
2025-10-02 14:34:15 +09:00

1102 lines
31 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* 화면 관리 시스템 컴포넌트 자동 생성 CLI 스크립트
* 실제 컴포넌트 구조에 맞게 설계
*/
const fs = require("fs");
const path = require("path");
// CLI 인자 파싱
const args = process.argv.slice(2);
const componentName = args[0];
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);
}
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");
}
validateInputs();
// 옵션 파싱
const options = {};
args.slice(5).forEach((arg) => {
if (arg.startsWith("--")) {
const [key, value] = arg.substring(2).split("=");
options[key] = value || true;
}
});
// 기본값 설정
const config = {
name: componentName,
displayName: displayName || componentName,
description: description || `${displayName || componentName} 컴포넌트`,
category: category || "display",
webType: webType,
author: options.author || "개발팀",
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 파일 생성
function createIndexFile(componentDir, names, config, width, height) {
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}",
name: "${config.displayName}",
nameEng: "${names.pascal} Component",
description: "${config.description}",
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}",
});
// 컴포넌트는 ${names.pascal}Renderer에서 자동 등록됩니다
// 타입 내보내기
export type { ${names.pascal}Config } from "./types";
`;
fs.writeFileSync(path.join(componentDir, "index.ts"), content);
console.log("✅ index.ts 생성 완료");
}
// 2. Component 파일 생성
function createComponentFile(componentDir, names, config) {
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,
isInteractive = false,
onClick,
onDragStart,
onDragEnd,
config,
className,
style,
formData,
onFormDataChange,
screenId,
...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) => {
return <${names.pascal}Component {...props} />;
};
`;
fs.writeFileSync(path.join(componentDir, `${names.pascal}Component.tsx`), content);
console.log(`${names.pascal}Component.tsx 생성 완료`);
}
// 3. Renderer 파일 생성
function createRendererFile(componentDir, names, config) {
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 파일 생성
function createConfigPanelFile(componentDir, names, config) {
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">
${config.description} 설정
</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 파일 생성
function createTypesFile(componentDir, names, config) {
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 생성 완료");
}
// README 파일 생성 (config.ts는 제거)
function createReadmeFile(componentDir, names, config, width, height) {
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]}
- **CLI 명령어**: \`node scripts/create-component.js ${componentName} "${config.displayName}" "${config.description}" ${config.category} ${config.webType}\`
- **경로**: \`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 생성 완료");
}
// 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}`);
}
}
// 헬퍼 함수들
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",
color: component.style?.labelColor || "#212121",
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",
color: component.style?.labelColor || "#212121",
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",
color: component.style?.labelColor || "#212121",
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}`);
console.log(`📂 카테고리: ${config.category}`);
console.log(`🎯 웹타입: ${config.webType}`);
console.log(`🌐 표시이름: ${config.displayName}`);
console.log(`📝 설명: ${config.description}`);
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 {
// 파일들 생성 (파라미터 전달하여 호출)
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);
console.log("\n🎉 컴포넌트 생성 완료!");
console.log(`📁 경로: ${componentDir}`);
console.log(`🔗 다음 단계:`);
console.log(` 1. ✅ lib/registry/components/index.ts에 import 자동 추가됨`);
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);
});