@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
487 lines (438 loc) • 13.8 kB
text/typescript
/**
* Template Manager
* 템플릿 데이터베이스 연동 및 관리 시스템
*/
import { PrismaClient } from "@prisma/client";
import {
TemplateManager,
BaseTemplate,
ServiceTier,
} from "../../types/template-system";
import { templateFactory } from "./template-factory";
const prisma = new PrismaClient();
export class AgentCTemplateManager implements TemplateManager {
/**
* 템플릿 조회
*/
async getTemplate(id: string): Promise<BaseTemplate | null> {
try {
const template = await prisma.siteTemplate.findUnique({
where: { id },
});
if (!template) {
return null;
}
return this.convertPrismaToBaseTemplate(template);
} catch (error) {
console.error("템플릿 조회 중 오류:", error);
return null;
}
}
/**
* 템플릿 저장
*/
async saveTemplate(template: BaseTemplate): Promise<void> {
try {
const templateData = this.convertBaseTemplateToPrisma(template);
await prisma.siteTemplate.upsert({
where: { id: template.id },
update: templateData,
create: {
id: template.id,
...templateData,
},
});
} catch (error) {
console.error("템플릿 저장 중 오류:", error);
throw new Error("템플릿 저장에 실패했습니다.");
}
}
/**
* 템플릿 삭제
*/
async deleteTemplate(id: string): Promise<void> {
try {
// 템플릿을 사용하는 사이트가 있는지 확인
const sitesUsingTemplate = await prisma.site.count({
where: { templateId: id },
});
if (sitesUsingTemplate > 0) {
throw new Error(
"이 템플릿을 사용하는 사이트가 있어 삭제할 수 없습니다."
);
}
await prisma.siteTemplate.delete({
where: { id },
});
} catch (error) {
console.error("템플릿 삭제 중 오류:", error);
throw error;
}
}
/**
* 템플릿 목록 조회
*/
async listTemplates(tier?: ServiceTier): Promise<BaseTemplate[]> {
try {
const templates = await prisma.template.findMany({
where: tier
? {
features: {
contains: tier,
},
}
: undefined,
orderBy: {
updatedAt: "desc",
},
});
return templates.map(
(template: {
id: string;
name: string;
slug: string;
description: string;
category: string;
sections: string | null;
styles: string | null;
features: string | null;
tags: string | null;
previewImage: string | null;
previewUrl: string | null;
updatedAt: Date;
}) => this.convertPrismaToBaseTemplate(template)
);
} catch (error) {
console.error("Error listing templates:", error);
throw new Error("Failed to list templates");
}
}
/**
* 사이트에 템플릿 적용
*/
async applyTemplate(siteId: string, templateId: string): Promise<void> {
try {
// 템플릿 존재 확인
const template = await this.getTemplate(templateId);
if (!template) {
throw new Error("템플릿을 찾을 수 없습니다.");
}
// 사이트 존재 확인
const site = await prisma.site.findUnique({
where: { id: siteId },
});
if (!site) {
throw new Error("사이트를 찾을 수 없습니다.");
}
// 템플릿 적용
await prisma.site.update({
where: { id: siteId },
data: {
templateId: templateId,
// 템플릿의 기본 설정을 사이트 설정에 병합
settings: JSON.stringify({
...JSON.parse(site.settings || "{}"),
template: {
tier: template.tier,
globalSettings: template.globalSettings,
customizationOptions: template.customizationOptions,
},
}),
},
});
} catch (error) {
console.error("템플릿 적용 중 오류:", error);
throw error;
}
}
/**
* 콘텐츠 마이그레이션
*/
async migrateContent(
fromTemplateId: string,
toTemplateId: string
): Promise<void> {
try {
const fromTemplate = await this.getTemplate(fromTemplateId);
const toTemplate = await this.getTemplate(toTemplateId);
if (!fromTemplate || !toTemplate) {
throw new Error("템플릿을 찾을 수 없습니다.");
}
// 두 템플릿 간의 공통 컴포넌트 찾기
const commonComponents = fromTemplate.components.filter((fromComp) =>
toTemplate.components.some((toComp) => toComp.type === fromComp.type)
);
// 해당 템플릿을 사용하는 모든 사이트 조회
const sites = await prisma.site.findMany({
where: { templateId: fromTemplateId },
});
// 각 사이트의 콘텐츠 마이그레이션
for (const site of sites) {
const siteSettings = JSON.parse(site.settings || "{}");
const migratedSettings = this.migrateContentSettings(
siteSettings,
commonComponents,
toTemplate
);
await prisma.site.update({
where: { id: site.id },
data: {
templateId: toTemplateId,
settings: JSON.stringify(migratedSettings),
},
});
}
} catch (error) {
console.error("콘텐츠 마이그레이션 중 오류:", error);
throw error;
}
}
/**
* 서비스 티어별 기본 템플릿 생성
*/
async createDefaultTemplates(): Promise<void> {
try {
const tiers = [
ServiceTier.STARTER,
ServiceTier.STANDARD,
ServiceTier.PLUS,
];
for (const tier of tiers) {
// Business 템플릿 생성
const businessTemplate = templateFactory.create(tier, {
name: `${tier} Business Template`,
slug: `${tier.toLowerCase()}-business`,
description: `${tier} 티어용 비즈니스 템플릿`,
metadata: {
category: "Business",
tags: ["business", "professional", tier.toLowerCase()],
version: "1.0.0",
lastModified: new Date(),
author: "AgentC Team",
previewImages: [],
minimumTier: tier,
features: this.getTierFeatures(tier),
compatibility: {
browsers: ["Chrome 90+", "Firefox 88+", "Safari 14+"],
devices: ["Desktop", "Tablet", "Mobile"],
frameworks: ["Next.js 15+", "React 18+"],
},
},
});
await this.saveTemplate(businessTemplate);
// Portfolio 템플릿 생성
const portfolioTemplate = templateFactory.create(tier, {
name: `${tier} Portfolio Template`,
slug: `${tier.toLowerCase()}-portfolio`,
description: `${tier} 티어용 포트폴리오 템플릿`,
metadata: {
category: "Portfolio",
tags: ["portfolio", "creative", tier.toLowerCase()],
version: "1.0.0",
lastModified: new Date(),
author: "AgentC Team",
previewImages: [],
minimumTier: tier,
features: this.getTierFeatures(tier),
compatibility: {
browsers: ["Chrome 90+", "Firefox 88+", "Safari 14+"],
devices: ["Desktop", "Tablet", "Mobile"],
frameworks: ["Next.js 15+", "React 18+"],
},
},
});
await this.saveTemplate(portfolioTemplate);
}
} catch (error) {
console.error("기본 템플릿 생성 중 오류:", error);
throw error;
}
}
/**
* Prisma 모델을 BaseTemplate으로 변환
*/
private convertPrismaToBaseTemplate(prismaTemplate: {
id: string;
name: string;
slug: string;
description: string;
category: string;
sections: string | null;
styles: string | null;
features: string | null;
tags: string | null;
previewImage: string | null;
previewUrl: string | null;
updatedAt: Date;
}): BaseTemplate {
const tier = this.extractTierFromFeatures(prismaTemplate.features || "");
return {
id: prismaTemplate.id,
name: prismaTemplate.name,
slug: prismaTemplate.slug,
tier: tier,
description: prismaTemplate.description,
components: JSON.parse(prismaTemplate.sections || "[]"),
globalSettings: JSON.parse(prismaTemplate.styles || "{}"),
customizationOptions: this.createCustomizationOptionsFromTier(tier),
metadata: {
version: "1.0.0",
lastModified: prismaTemplate.updatedAt,
author: "AgentC",
category: prismaTemplate.category,
tags: prismaTemplate.tags?.split(",") || [],
previewImages: prismaTemplate.previewImage
? [prismaTemplate.previewImage]
: [],
demoUrl: prismaTemplate.previewUrl || undefined,
minimumTier: tier,
features: prismaTemplate.features?.split(",") || [],
compatibility: {
browsers: ["Chrome 90+", "Firefox 88+", "Safari 14+"],
devices: ["Desktop", "Tablet", "Mobile"],
frameworks: ["React", "Next.js"],
},
},
};
}
/**
* BaseTemplate을 Prisma 모델 형식으로 변환
*/
private convertBaseTemplateToPrisma(template: BaseTemplate) {
return {
name: template.name,
slug: template.slug,
description: template.description,
category: template.metadata.category,
pricing: "FREE", // 기본값
sections: JSON.stringify(template.components),
styles: JSON.stringify(template.globalSettings),
previewImage: template.metadata.previewImages?.[0] || null,
previewUrl: template.metadata.demoUrl || null,
features: template.metadata.features.join(","),
tags: template.metadata.tags.join(","),
isActive: true,
isPublic: true,
};
}
/**
* features 문자열에서 ServiceTier 추출
*/
private extractTierFromFeatures(features: string): ServiceTier {
if (features?.includes("plus")) return ServiceTier.PLUS;
if (features?.includes("standard")) return ServiceTier.STANDARD;
return ServiceTier.STARTER;
}
/**
* 티어에 따른 커스터마이제이션 옵션 생성
*/
private createCustomizationOptionsFromTier(tier: ServiceTier) {
return templateFactory.create(tier, {}).customizationOptions;
}
/**
* 기존 콘텐츠를 새 템플릿으로 마이그레이션할 때 설정을 변환
*/
private migrateContentSettings(
oldSettings: Record<string, unknown>,
commonComponents: Array<{ type: string; [key: string]: unknown }>,
newTemplate: BaseTemplate
) {
const migratedSettings = { ...oldSettings };
// 공통 컴포넌트의 콘텐츠만 유지
const migratedContent: Record<string, unknown> = {};
commonComponents.forEach((component) => {
if (oldSettings.content?.[component.type]) {
migratedContent[component.type] = oldSettings.content[component.type];
}
});
migratedSettings.content = migratedContent;
return migratedSettings;
}
/**
* 티어별 기능 목록 반환
*/
private getTierFeatures(tier: ServiceTier): string[] {
const baseFeatures = ["반응형 디자인", "모바일 최적화", "기본 SEO"];
if (tier === ServiceTier.STANDARD) {
return [
...baseFeatures,
"고급 커스터마이제이션",
"컴포넌트 토글",
"추가 레이아웃",
];
}
if (tier === ServiceTier.PLUS) {
return [
...baseFeatures,
"완전한 커스터마이제이션",
"커스텀 CSS/JS",
"서드파티 통합",
"고급 SEO",
];
}
return baseFeatures;
}
/**
* 템플릿 통계 조회
*/
async getTemplateStats(templateId: string): Promise<{
sitesCount: number;
lastUsed: Date | null;
popularComponents: string[];
}> {
try {
const sitesCount = await prisma.site.count({
where: { templateId },
});
const lastUsedSite = await prisma.site.findFirst({
where: { templateId },
orderBy: { updatedAt: "desc" },
select: { updatedAt: true },
});
// 인기 컴포넌트 분석은 실제 사용 데이터에 기반하여 구현
const popularComponents: string[] = [];
return {
sitesCount,
lastUsed: lastUsedSite?.updatedAt || null,
popularComponents,
};
} catch (error) {
console.error("템플릿 통계 조회 중 오류:", error);
throw error;
}
}
/**
* 템플릿 복제
*/
async cloneTemplate(
templateId: string,
newName: string
): Promise<BaseTemplate> {
try {
const originalTemplate = await this.getTemplate(templateId);
if (!originalTemplate) {
throw new Error("복제할 템플릿을 찾을 수 없습니다.");
}
const clonedTemplate = templateFactory.clone(originalTemplate);
clonedTemplate.name = newName;
clonedTemplate.slug = this.generateSlug(newName);
await this.saveTemplate(clonedTemplate);
return clonedTemplate;
} catch (error) {
console.error("템플릿 복제 중 오류:", error);
throw error;
}
}
/**
* 슬러그 생성
*/
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9가-힣]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
}
// 싱글톤 인스턴스 export
export const templateManager = new AgentCTemplateManager();