아코디언 컴포넌트 생성

This commit is contained in:
kjs
2025-09-12 16:47:02 +09:00
parent 49e8e40521
commit 52dd18747a
28 changed files with 3027 additions and 956 deletions
+150 -156
View File
@@ -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);
});