아코디언 컴포넌트 생성
This commit is contained in:
@@ -1,43 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 컴포넌트 자동 생성 CLI 스크립트
|
||||
* 레이아웃 시스템의 create-layout.js와 동일한 패턴으로 설계
|
||||
* 화면 관리 시스템 컴포넌트 자동 생성 CLI 스크립트
|
||||
* 실제 컴포넌트 구조에 맞게 설계
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const readline = require("readline");
|
||||
|
||||
// 대화형 입력을 위한 readline 인터페이스
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// 사용자 입력을 기다리는 함수
|
||||
function askQuestion(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
if (!componentName) {
|
||||
console.error("❌ 컴포넌트 이름을 제공해주세요.");
|
||||
console.log("사용법: node scripts/create-component.js <컴포넌트이름> [옵션]");
|
||||
console.log("예시: node scripts/create-component.js button-primary --category=ui --webType=button");
|
||||
process.exit(1);
|
||||
// 입력값 검증
|
||||
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(1).forEach((arg) => {
|
||||
args.slice(5).forEach((arg) => {
|
||||
if (arg.startsWith("--")) {
|
||||
const [key, value] = arg.substring(2).split("=");
|
||||
options[key] = value || true;
|
||||
@@ -47,10 +88,11 @@ args.slice(1).forEach((arg) => {
|
||||
// 기본값 설정
|
||||
const config = {
|
||||
name: componentName,
|
||||
category: options.category || "ui",
|
||||
webType: options.webType || "text",
|
||||
description: options.description || `${componentName} 컴포넌트입니다`,
|
||||
author: options.author || "Developer",
|
||||
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(",") : [],
|
||||
};
|
||||
@@ -85,7 +127,7 @@ const names = {
|
||||
};
|
||||
|
||||
// 1. index.ts 파일 생성
|
||||
function createIndexFile() {
|
||||
function createIndexFile(componentDir, names, config, width, height) {
|
||||
const content = `"use client";
|
||||
|
||||
import React from "react";
|
||||
@@ -102,9 +144,9 @@ import { ${names.pascal}Config } from "./types";
|
||||
*/
|
||||
export const ${names.pascal}Definition = createComponentDefinition({
|
||||
id: "${names.kebab}",
|
||||
name: "${config.koreanName}",
|
||||
name: "${config.displayName}",
|
||||
nameEng: "${names.pascal} Component",
|
||||
description: "${config.koreanDescription}",
|
||||
description: "${config.description}",
|
||||
category: ComponentCategory.${config.category.toUpperCase()},
|
||||
webType: "${config.webType}",
|
||||
component: ${names.pascal}Wrapper,
|
||||
@@ -120,12 +162,10 @@ export const ${names.pascal}Definition = createComponentDefinition({
|
||||
documentation: "https://docs.example.com/components/${names.kebab}",
|
||||
});
|
||||
|
||||
// 컴포넌트는 ${names.pascal}Renderer에서 자동 등록됩니다
|
||||
|
||||
// 타입 내보내기
|
||||
export type { ${names.pascal}Config } from "./types";
|
||||
|
||||
// 컴포넌트 내보내기
|
||||
export { ${names.pascal}Component } from "./${names.pascal}Component";
|
||||
export { ${names.pascal}Renderer } from "./${names.pascal}Renderer";
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(componentDir, "index.ts"), content);
|
||||
@@ -133,7 +173,7 @@ export { ${names.pascal}Renderer } from "./${names.pascal}Renderer";
|
||||
}
|
||||
|
||||
// 2. Component 파일 생성
|
||||
function createComponentFile() {
|
||||
function createComponentFile(componentDir, names, config) {
|
||||
const content = `"use client";
|
||||
|
||||
import React from "react";
|
||||
@@ -152,12 +192,16 @@ 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
|
||||
}) => {
|
||||
// 컴포넌트 설정
|
||||
@@ -211,7 +255,7 @@ export const ${names.pascal}Component: React.FC<${names.pascal}ComponentProps> =
|
||||
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
||||
*/
|
||||
export const ${names.pascal}Wrapper: React.FC<${names.pascal}ComponentProps> = (props) => {
|
||||
return <${names.pascal}Component {...domProps} />;
|
||||
return <${names.pascal}Component {...props} />;
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -220,7 +264,7 @@ export const ${names.pascal}Wrapper: React.FC<${names.pascal}ComponentProps> = (
|
||||
}
|
||||
|
||||
// 3. Renderer 파일 생성
|
||||
function createRendererFile() {
|
||||
function createRendererFile(componentDir, names, config) {
|
||||
const content = `"use client";
|
||||
|
||||
import React from "react";
|
||||
@@ -272,11 +316,6 @@ export class ${names.pascal}Renderer extends AutoRegisteringComponentRenderer {
|
||||
|
||||
// 자동 등록 실행
|
||||
${names.pascal}Renderer.registerSelf();
|
||||
|
||||
// Hot Reload 지원 (개발 모드)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
${names.pascal}Renderer.enableHotReload();
|
||||
}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(componentDir, `${names.pascal}Renderer.tsx`), content);
|
||||
@@ -284,7 +323,7 @@ if (process.env.NODE_ENV === "development") {
|
||||
}
|
||||
|
||||
// 4. Config Panel 파일 생성
|
||||
function createConfigPanelFile() {
|
||||
function createConfigPanelFile(componentDir, names, config) {
|
||||
const content = `"use client";
|
||||
|
||||
import React from "react";
|
||||
@@ -314,7 +353,7 @@ export const ${names.pascal}ConfigPanel: React.FC<${names.pascal}ConfigPanelProp
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">
|
||||
${config.description.replace(" 컴포넌트입니다", "")} 설정
|
||||
${config.description} 설정
|
||||
</div>
|
||||
|
||||
${getConfigPanelJSXByWebType(config.webType)}
|
||||
@@ -356,7 +395,7 @@ export const ${names.pascal}ConfigPanel: React.FC<${names.pascal}ConfigPanelProp
|
||||
}
|
||||
|
||||
// 5. Types 파일 생성
|
||||
function createTypesFile() {
|
||||
function createTypesFile(componentDir, names, config) {
|
||||
const content = `"use client";
|
||||
|
||||
import { ComponentConfig } from "@/types/component";
|
||||
@@ -408,56 +447,8 @@ export interface ${names.pascal}Props {
|
||||
console.log("✅ types.ts 생성 완료");
|
||||
}
|
||||
|
||||
// 6. Config 파일 생성
|
||||
function createConfigFile() {
|
||||
const content = `"use client";
|
||||
|
||||
import { ${names.pascal}Config } from "./types";
|
||||
|
||||
/**
|
||||
* ${names.pascal} 컴포넌트 기본 설정
|
||||
*/
|
||||
export const ${names.pascal}DefaultConfig: ${names.pascal}Config = {
|
||||
${getDefaultConfigByWebType(config.webType)}
|
||||
|
||||
// 공통 기본값
|
||||
disabled: false,
|
||||
required: false,
|
||||
readonly: false,
|
||||
variant: "default",
|
||||
size: "md",
|
||||
};
|
||||
|
||||
/**
|
||||
* ${names.pascal} 컴포넌트 설정 스키마
|
||||
* 유효성 검사 및 타입 체크에 사용
|
||||
*/
|
||||
export const ${names.pascal}ConfigSchema = {
|
||||
${getConfigSchemaByWebType(config.webType)}
|
||||
|
||||
// 공통 스키마
|
||||
disabled: { type: "boolean", default: false },
|
||||
required: { type: "boolean", default: false },
|
||||
readonly: { type: "boolean", default: false },
|
||||
variant: {
|
||||
type: "enum",
|
||||
values: ["default", "outlined", "filled"],
|
||||
default: "default"
|
||||
},
|
||||
size: {
|
||||
type: "enum",
|
||||
values: ["sm", "md", "lg"],
|
||||
default: "md"
|
||||
},
|
||||
};
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(componentDir, "config.ts"), content);
|
||||
console.log("✅ config.ts 생성 완료");
|
||||
}
|
||||
|
||||
// 7. README 파일 생성
|
||||
function createReadmeFile() {
|
||||
// README 파일 생성 (config.ts는 제거)
|
||||
function createReadmeFile(componentDir, names, config, width, height) {
|
||||
const content = `# ${names.pascal} 컴포넌트
|
||||
|
||||
${config.description}
|
||||
@@ -542,7 +533,7 @@ ${getConfigDocumentationByWebType(config.webType)}
|
||||
## 개발자 정보
|
||||
|
||||
- **생성일**: ${new Date().toISOString().split("T")[0]}
|
||||
- **CLI 명령어**: \`node scripts/create-component.js ${componentName} --category=${config.category} --webType=${config.webType}\`
|
||||
- **CLI 명령어**: \`node scripts/create-component.js ${componentName} "${config.displayName}" "${config.description}" ${config.category} ${config.webType}\`
|
||||
- **경로**: \`lib/registry/components/${names.kebab}/\`
|
||||
|
||||
## 관련 문서
|
||||
@@ -555,6 +546,54 @@ ${getConfigDocumentationByWebType(config.webType)}
|
||||
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) {
|
||||
@@ -1005,7 +1044,6 @@ async function main() {
|
||||
const [width, height] = config.size.split("x").map(Number);
|
||||
if (!width || !height) {
|
||||
console.error("❌ 크기 형식이 올바르지 않습니다. 예: 200x36");
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1015,57 +1053,15 @@ async function main() {
|
||||
console.log("🚀 컴포넌트 생성 시작...");
|
||||
console.log(`📁 이름: ${names.camel}`);
|
||||
console.log(`🔖 ID: ${names.kebab}`);
|
||||
|
||||
// 사용자로부터 한글 이름과 설명 입력받기
|
||||
console.log("\n📝 컴포넌트 정보를 입력해주세요:");
|
||||
const koreanName = await askQuestion(`한글 이름 (예: 기본 버튼): `);
|
||||
const koreanDescription = await askQuestion(`설명 (예: 일반적인 액션을 위한 기본 버튼 컴포넌트): `);
|
||||
|
||||
// 카테고리와 웹타입 확인
|
||||
let category = config.category;
|
||||
let webType = config.webType;
|
||||
|
||||
if (!options.category) {
|
||||
console.log("\n📂 카테고리를 선택해주세요:");
|
||||
console.log("1. input - 입력 컴포넌트");
|
||||
console.log("2. display - 표시 컴포넌트");
|
||||
console.log("3. layout - 레이아웃 컴포넌트");
|
||||
console.log("4. action - 액션 컴포넌트");
|
||||
console.log("5. admin - 관리자 컴포넌트");
|
||||
|
||||
const categoryChoice = await askQuestion("카테고리 번호 (1-5): ");
|
||||
const categoryMap = {
|
||||
1: "input",
|
||||
2: "display",
|
||||
3: "layout",
|
||||
4: "action",
|
||||
5: "admin",
|
||||
};
|
||||
category = categoryMap[categoryChoice] || "input";
|
||||
}
|
||||
|
||||
if (!options.webType) {
|
||||
console.log("\n🎯 웹타입을 입력해주세요:");
|
||||
console.log("예시: text, number, email, password, date, select, checkbox, radio, boolean, file, button");
|
||||
webType = (await askQuestion(`웹타입 (기본: text): `)) || "text";
|
||||
}
|
||||
|
||||
// config 업데이트
|
||||
config.category = category;
|
||||
config.webType = webType;
|
||||
config.koreanName = koreanName;
|
||||
config.koreanDescription = koreanDescription;
|
||||
|
||||
console.log(`\n📂 카테고리: ${config.category}`);
|
||||
console.log(`📂 카테고리: ${config.category}`);
|
||||
console.log(`🎯 웹타입: ${config.webType}`);
|
||||
console.log(`🌐 한글이름: ${config.koreanName}`);
|
||||
console.log(`📝 설명: ${config.koreanDescription}`);
|
||||
console.log(`🌐 표시이름: ${config.displayName}`);
|
||||
console.log(`📝 설명: ${config.description}`);
|
||||
console.log(`📏 크기: ${width}x${height}`);
|
||||
|
||||
// 컴포넌트 디렉토리 생성
|
||||
if (fs.existsSync(componentDir)) {
|
||||
console.error(`❌ 컴포넌트 "${names.kebab}"가 이미 존재합니다.`);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1073,35 +1069,33 @@ async function main() {
|
||||
console.log(`📁 디렉토리 생성: ${componentDir}`);
|
||||
|
||||
try {
|
||||
// 파일들 생성
|
||||
createIndexFile();
|
||||
createComponentFile();
|
||||
createRendererFile();
|
||||
createConfigPanelFile();
|
||||
createTypesFile();
|
||||
createConfigFile();
|
||||
createReadmeFile();
|
||||
// 파일들 생성 (파라미터 전달하여 호출)
|
||||
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(` 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);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
}
|
||||
|
||||
// 메인 함수 실행
|
||||
main().catch((error) => {
|
||||
console.error("❌ 실행 중 오류 발생:", error);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user