UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

487 lines (438 loc) 13.8 kB
/** * 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();