369 lines
8.8 KiB
TypeScript
369 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ComponentDefinition,
|
|
CreateComponentDefinitionOptions,
|
|
ComponentValidation,
|
|
DEFAULT_COMPONENT_SIZE,
|
|
} from "@/types/component";
|
|
|
|
/**
|
|
* 컴포넌트 정의 생성 헬퍼 함수
|
|
* 타입 안전성을 보장하면서 컴포넌트 정의를 쉽게 생성할 수 있도록 도와줍니다
|
|
* 레이아웃 시스템의 createLayoutDefinition과 동일한 패턴
|
|
*/
|
|
export function createComponentDefinition(options: CreateComponentDefinitionOptions): ComponentDefinition {
|
|
const {
|
|
id,
|
|
name,
|
|
nameEng,
|
|
description,
|
|
category,
|
|
webType,
|
|
component,
|
|
renderer,
|
|
defaultConfig = {},
|
|
defaultSize = DEFAULT_COMPONENT_SIZE,
|
|
configPanel,
|
|
icon,
|
|
previewImage,
|
|
tags = [],
|
|
version = "1.0.0",
|
|
author,
|
|
documentation,
|
|
validation,
|
|
dependencies = [],
|
|
} = options;
|
|
|
|
// 기본 검증
|
|
if (!id) {
|
|
throw new Error("컴포넌트 ID는 필수입니다");
|
|
}
|
|
|
|
if (!name) {
|
|
throw new Error("컴포넌트 이름은 필수입니다");
|
|
}
|
|
|
|
if (!description) {
|
|
throw new Error("컴포넌트 설명은 필수입니다");
|
|
}
|
|
|
|
if (!component) {
|
|
throw new Error("React 컴포넌트는 필수입니다");
|
|
}
|
|
|
|
// ID 형식 검증 (kebab-case)
|
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
|
|
throw new Error("컴포넌트 ID는 kebab-case 형식이어야 합니다 (예: button-primary)");
|
|
}
|
|
|
|
// 기본 태그 자동 추가
|
|
const enhancedTags = [
|
|
...tags,
|
|
category, // 카테고리를 태그로 추가
|
|
webType, // 웹타입을 태그로 추가
|
|
...name.split(" "), // 이름을 단어별로 분리하여 태그로 추가
|
|
].filter((tag, index, array) => array.indexOf(tag) === index); // 중복 제거
|
|
|
|
// 컴포넌트 정의 생성
|
|
const definition: ComponentDefinition = {
|
|
id,
|
|
name,
|
|
nameEng,
|
|
description,
|
|
category,
|
|
webType,
|
|
component,
|
|
renderer,
|
|
defaultConfig,
|
|
defaultSize,
|
|
configPanel,
|
|
icon,
|
|
previewImage,
|
|
tags: enhancedTags,
|
|
version,
|
|
author,
|
|
documentation,
|
|
validation,
|
|
dependencies,
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
// 정의 검증
|
|
const validationResult = validateComponentDefinition(definition);
|
|
|
|
if (!validationResult.isValid) {
|
|
throw new Error(`컴포넌트 정의 검증 실패: ${validationResult.errors.join(", ")}`);
|
|
}
|
|
|
|
// 경고사항 출력 (개발 모드에서만) - 로그 제거
|
|
|
|
return definition;
|
|
}
|
|
|
|
/**
|
|
* 컴포넌트 정의 유효성 검사
|
|
*/
|
|
export function validateComponentDefinition(definition: ComponentDefinition): {
|
|
isValid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
} {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// 필수 필드 검사
|
|
if (!definition.id) errors.push("id는 필수입니다");
|
|
if (!definition.name) errors.push("name은 필수입니다");
|
|
if (!definition.description) errors.push("description은 필수입니다");
|
|
if (!definition.category) errors.push("category는 필수입니다");
|
|
if (!definition.webType) errors.push("webType은 필수입니다");
|
|
if (!definition.component) errors.push("component는 필수입니다");
|
|
if (!definition.defaultSize) errors.push("defaultSize는 필수입니다");
|
|
|
|
// ID 형식 검사
|
|
if (definition.id && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(definition.id)) {
|
|
errors.push("id는 kebab-case 형식이어야 합니다 (예: button-primary)");
|
|
}
|
|
|
|
// ID 길이 검사
|
|
if (definition.id && definition.id.length > 50) {
|
|
errors.push("id는 50자를 초과할 수 없습니다");
|
|
}
|
|
|
|
// 이름 길이 검사
|
|
if (definition.name && definition.name.length > 100) {
|
|
errors.push("name은 100자를 초과할 수 없습니다");
|
|
}
|
|
|
|
// 설명 길이 검사
|
|
if (definition.description && definition.description.length > 500) {
|
|
errors.push("description은 500자를 초과할 수 없습니다");
|
|
}
|
|
|
|
// 크기 유효성 검사
|
|
if (definition.defaultSize) {
|
|
if (definition.defaultSize.width <= 0) {
|
|
errors.push("defaultSize.width는 0보다 커야 합니다");
|
|
}
|
|
if (definition.defaultSize.height <= 0) {
|
|
errors.push("defaultSize.height는 0보다 커야 합니다");
|
|
}
|
|
if (definition.defaultSize.width > 2000) {
|
|
warnings.push("defaultSize.width가 매우 큽니다 (2000px 초과)");
|
|
}
|
|
if (definition.defaultSize.height > 1000) {
|
|
warnings.push("defaultSize.height가 매우 큽니다 (1000px 초과)");
|
|
}
|
|
}
|
|
|
|
// 태그 검사
|
|
if (definition.tags) {
|
|
if (definition.tags.length > 20) {
|
|
warnings.push("태그가 너무 많습니다 (20개 초과)");
|
|
}
|
|
definition.tags.forEach((tag) => {
|
|
if (tag.length > 30) {
|
|
warnings.push(`태그가 너무 깁니다: ${tag}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 버전 형식 검사
|
|
if (definition.version && !/^\d+\.\d+\.\d+/.test(definition.version)) {
|
|
warnings.push("버전 형식은 semantic versioning을 권장합니다 (예: 1.0.0)");
|
|
}
|
|
|
|
// 의존성 검사
|
|
if (definition.dependencies && definition.dependencies.length > 10) {
|
|
warnings.push("의존성이 너무 많습니다 (10개 초과)");
|
|
}
|
|
|
|
// 권장사항 검사
|
|
if (!definition.icon) {
|
|
warnings.push("검색 및 식별을 위해 아이콘 설정을 권장합니다");
|
|
}
|
|
|
|
if (!definition.tags || definition.tags.length === 0) {
|
|
warnings.push("검색을 위한 태그 설정을 권장합니다");
|
|
}
|
|
|
|
if (!definition.author) {
|
|
warnings.push("작성자 정보 설정을 권장합니다");
|
|
}
|
|
|
|
if (!definition.documentation) {
|
|
warnings.push("사용법 안내를 위한 문서 URL 설정을 권장합니다");
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 컴포넌트 정의 복제 헬퍼
|
|
*/
|
|
export function cloneComponentDefinition(
|
|
original: ComponentDefinition,
|
|
overrides: Partial<CreateComponentDefinitionOptions>,
|
|
): ComponentDefinition {
|
|
return createComponentDefinition({
|
|
id: original.id,
|
|
name: original.name,
|
|
nameEng: original.nameEng,
|
|
description: original.description,
|
|
category: original.category,
|
|
webType: original.webType,
|
|
component: original.component,
|
|
renderer: original.renderer,
|
|
defaultConfig: original.defaultConfig,
|
|
defaultSize: original.defaultSize,
|
|
configPanel: original.configPanel,
|
|
icon: original.icon,
|
|
previewImage: original.previewImage,
|
|
tags: original.tags,
|
|
version: original.version,
|
|
author: original.author,
|
|
documentation: original.documentation,
|
|
validation: original.validation,
|
|
dependencies: original.dependencies,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 웹타입에 따른 기본 설정 생성
|
|
*/
|
|
export function getDefaultConfigByWebType(webType: string): Record<string, any> {
|
|
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 "date":
|
|
return {
|
|
format: "YYYY-MM-DD",
|
|
};
|
|
|
|
case "datetime":
|
|
return {
|
|
format: "YYYY-MM-DD HH:mm",
|
|
};
|
|
|
|
case "textarea":
|
|
return {
|
|
placeholder: "내용을 입력하세요",
|
|
rows: 3,
|
|
maxLength: 1000,
|
|
};
|
|
|
|
case "select":
|
|
case "dropdown":
|
|
return {
|
|
options: [],
|
|
placeholder: "선택하세요",
|
|
};
|
|
|
|
case "checkbox":
|
|
return {
|
|
checked: false,
|
|
};
|
|
|
|
case "radio":
|
|
return {
|
|
checked: false,
|
|
group: "radio-group",
|
|
};
|
|
|
|
case "button":
|
|
return {
|
|
text: "버튼",
|
|
actionType: "button",
|
|
variant: "primary",
|
|
};
|
|
|
|
case "file":
|
|
return {
|
|
multiple: false,
|
|
accept: "*/*",
|
|
};
|
|
|
|
case "range":
|
|
return {
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
value: 50,
|
|
};
|
|
|
|
case "color":
|
|
return {
|
|
value: "#000000",
|
|
};
|
|
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리에 따른 기본 아이콘 추천
|
|
*/
|
|
export function getDefaultIconByCategory(category: string): string {
|
|
switch (category) {
|
|
case "ui":
|
|
return "Square";
|
|
case "input":
|
|
return "Edit";
|
|
case "display":
|
|
return "Eye";
|
|
case "action":
|
|
return "MousePointer";
|
|
case "layout":
|
|
return "Layout";
|
|
case "chart":
|
|
return "BarChart";
|
|
case "form":
|
|
return "FormInput";
|
|
case "media":
|
|
return "Image";
|
|
case "navigation":
|
|
return "Menu";
|
|
case "feedback":
|
|
return "Bell";
|
|
case "utility":
|
|
return "Settings";
|
|
case "container":
|
|
return "Package";
|
|
case "system":
|
|
return "Cpu";
|
|
case "admin":
|
|
return "Shield";
|
|
default:
|
|
return "Component";
|
|
}
|
|
}
|