UNPKG

@restnfeel/agentc-starter-kit

Version:

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

578 lines (526 loc) 14.5 kB
import { PrismaClient, Prisma } from "@prisma/client"; import { createSiteFromTemplate } from "@/templates"; const prisma = new PrismaClient(); // Site 관련 타입 정의 export interface CreateSiteData { name: string; slug: string; domain?: string; subdomain?: string; description?: string; templateId: string; ownerId: string; } export interface UpdateSiteData { name?: string; description?: string; domain?: string; subdomain?: string; status?: "DRAFT" | "ACTIVE" | "INACTIVE" | "ARCHIVED"; isPublished?: boolean; settings?: Record<string, unknown>; templateData?: Record<string, unknown>; } export interface SiteListOptions { ownerId?: string; status?: "DRAFT" | "ACTIVE" | "INACTIVE" | "ARCHIVED"; search?: string; page?: number; pageSize?: number; sortBy?: "name" | "createdAt" | "updatedAt"; sortOrder?: "asc" | "desc"; } export interface Site { id: string; name: string; slug: string; domain?: string | null; subdomain?: string | null; description?: string | null; templateId: string; templateData?: Record<string, unknown> | null; status: "DRAFT" | "ACTIVE" | "INACTIVE" | "ARCHIVED"; isPublished: boolean; publishedAt?: Date | null; ownerId: string; settings?: Record<string, unknown> | null; createdAt: Date; updatedAt: Date; _count?: { siteSections: number; pages: number; assets: number; siteUsers: number; }; } export class SiteApiService { // 사이트 목록 조회 async getSites(options: SiteListOptions = {}): Promise<{ sites: Site[]; totalCount: number; page: number; pageSize: number; totalPages: number; }> { const { ownerId, status, search, page = 1, pageSize = 20, sortBy = "updatedAt", sortOrder = "desc", } = options; // 기본 WHERE 조건 const where: { ownerId?: string; status?: "DRAFT" | "ACTIVE" | "INACTIVE" | "ARCHIVED"; OR?: Array<{ name?: { contains: string; mode: "insensitive" }; description?: { contains: string; mode: "insensitive" }; domain?: { contains: string; mode: "insensitive" }; subdomain?: { contains: string; mode: "insensitive" }; }>; } = {}; if (ownerId) { where.ownerId = ownerId; } if (status) { where.status = status; } if (search) { where.OR = [ { name: { contains: search, mode: "insensitive" } }, { description: { contains: search, mode: "insensitive" } }, { domain: { contains: search, mode: "insensitive" } }, { subdomain: { contains: search, mode: "insensitive" } }, ]; } // 총 개수 조회 const totalCount = await prisma.site.count({ where }); // 페이지네이션 계산 const skip = (page - 1) * pageSize; const totalPages = Math.ceil(totalCount / pageSize); // 사이트 목록 조회 const sites = (await prisma.site.findMany({ where, skip, take: pageSize, orderBy: { [sortBy]: sortOrder }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, })) as Site[]; return { sites, totalCount, page, pageSize, totalPages, }; } // 사이트 상세 조회 async getSite( siteId: string, includeRelations = false ): Promise<Site | null> { const site = await prisma.site.findUnique({ where: { id: siteId }, include: includeRelations ? { siteSections: { orderBy: { order: "asc" }, }, pages: { orderBy: { createdAt: "desc" }, }, siteUsers: { include: { user: { select: { id: true, name: true, email: true, }, }, }, }, _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, } : { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site | null; } // Slug로 사이트 조회 async getSiteBySlug(slug: string): Promise<Site | null> { const site = await prisma.site.findUnique({ where: { slug }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site | null; } // 도메인으로 사이트 조회 async getSiteByDomain(domain: string): Promise<Site | null> { const site = await prisma.site.findFirst({ where: { OR: [{ domain }, { subdomain: domain }], }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site | null; } // 사이트 생성 async createSite(data: CreateSiteData): Promise<Site> { // Slug 중복 확인 const existingSite = await this.getSiteBySlug(data.slug); if (existingSite) { throw new Error(`사이트 슬러그 '${data.slug}'는 이미 사용 중입니다.`); } // 도메인 중복 확인 if (data.domain) { const existingDomain = await this.getSiteByDomain(data.domain); if (existingDomain) { throw new Error(`도메인 '${data.domain}'는 이미 사용 중입니다.`); } } // 템플릿 기반 사이트 데이터 생성 const siteFromTemplate = createSiteFromTemplate(data.templateId, { name: data.name, slug: data.slug, domain: data.domain, }); // 사이트 생성 const site = await prisma.site.create({ data: { name: data.name, slug: data.slug, domain: data.domain, subdomain: data.subdomain, description: data.description, templateId: data.templateId, templateData: siteFromTemplate.templateData as Prisma.InputJsonValue, ownerId: data.ownerId, settings: siteFromTemplate.settings, status: "DRAFT", isPublished: false, }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); // 사이트 섹션 생성 for (const sectionData of siteFromTemplate.sections) { await prisma.siteSection.create({ data: { siteId: site.id, type: sectionData.type, name: sectionData.name, content: sectionData.content, styles: sectionData.styles, order: sectionData.order, isVisible: sectionData.isVisible, isPublished: sectionData.isPublished, }, }); } // 기본 페이지 생성 await prisma.sitePage.create({ data: { siteId: site.id, title: data.name, slug: "home", path: "/", description: data.description || `${data.name} 홈페이지`, metaTitle: data.name, metaDescription: data.description, isPublished: false, isDraft: true, }, }); // 사이트 소유자를 OWNER 역할로 추가 await prisma.siteUser.create({ data: { siteId: site.id, userId: data.ownerId, role: "OWNER", isActive: true, joinedAt: new Date(), }, }); return site as Site; } // 사이트 업데이트 async updateSite(siteId: string, data: UpdateSiteData): Promise<Site> { // 도메인 중복 확인 (변경되는 경우) if (data.domain) { const existingDomain = await prisma.site.findFirst({ where: { AND: [ { id: { not: siteId } }, { OR: [{ domain: data.domain }, { subdomain: data.domain }], }, ], }, }); if (existingDomain) { throw new Error(`도메인 '${data.domain}'는 이미 사용 중입니다.`); } } const site = await prisma.site.update({ where: { id: siteId }, data: { ...data, ...(data.templateData !== undefined ? { templateData: data.templateData as Prisma.InputJsonValue } : {}), updatedAt: new Date(), } as any, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site; } // 사이트 삭제 async deleteSite(siteId: string): Promise<void> { // 관련 데이터들이 cascade delete로 자동 삭제됨 await prisma.site.delete({ where: { id: siteId }, }); } // 사이트 발행 async publishSite(siteId: string): Promise<Site> { const site = await prisma.site.update({ where: { id: siteId }, data: { isPublished: true, publishedAt: new Date(), status: "ACTIVE", }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site; } // 사이트 발행 취소 async unpublishSite(siteId: string): Promise<Site> { const site = await prisma.site.update({ where: { id: siteId }, data: { isPublished: false, status: "DRAFT", }, include: { _count: { select: { siteSections: true, pages: true, assets: true, siteUsers: true, }, }, }, }); return site as Site; } // 사이트 복제 async cloneSite( siteId: string, newData: { name: string; slug: string; ownerId: string; } ): Promise<Site> { const originalSite = await this.getSite(siteId, true); if (!originalSite) { throw new Error("원본 사이트를 찾을 수 없습니다."); } // 새로운 사이트 생성 const createData: CreateSiteData = { name: newData.name, slug: newData.slug, ownerId: newData.ownerId, templateId: originalSite.templateId, description: originalSite.description || undefined, domain: undefined, subdomain: undefined, }; return await this.createSite(createData); } // 사이트 통계 async getSiteStats(ownerId?: string): Promise<{ total: number; published: number; draft: number; active: number; inactive: number; byTemplate: Record<string, number>; }> { const where = ownerId ? { ownerId } : {}; const [total, published, draft, active, inactive, byTemplate] = await Promise.all([ prisma.site.count({ where }), prisma.site.count({ where: { ...where, isPublished: true } }), prisma.site.count({ where: { ...where, status: "DRAFT" } }), prisma.site.count({ where: { ...where, status: "ACTIVE" } }), prisma.site.count({ where: { ...where, status: "INACTIVE" } }), prisma.site.groupBy({ by: ["templateId"], where, _count: { id: true }, }), ]); const templateStats = byTemplate.reduce( ( acc: Record<string, number>, item: { templateId: string; _count: { id: number } } ) => { acc[item.templateId] = item._count.id; return acc; }, {} as Record<string, number> ); return { total, published, draft, active, inactive, byTemplate: templateStats, }; } // 사이트 검색 (자동완성용) async searchSites( query: string, ownerId?: string, limit = 10 ): Promise< { id: string; name: string; slug: string; domain?: string | null; }[] > { const where: { ownerId?: string; OR?: Array<{ name?: { contains: string; mode: "insensitive" }; slug?: { contains: string; mode: "insensitive" }; domain?: { contains: string; mode: "insensitive" }; }>; } = { OR: [ { name: { contains: query, mode: "insensitive" } }, { slug: { contains: query, mode: "insensitive" } }, { domain: { contains: query, mode: "insensitive" } }, ], }; if (ownerId) { where.ownerId = ownerId; } const sites = await prisma.site.findMany({ where, select: { id: true, name: true, slug: true, domain: true, }, take: limit, orderBy: { updatedAt: "desc" }, }); return sites; } } // 전역 Site API 서비스 인스턴스 export const siteApiService = new SiteApiService(); // 유틸리티 함수들 export const createSite = (data: CreateSiteData) => siteApiService.createSite(data); export const getSites = (options?: SiteListOptions) => siteApiService.getSites(options); export const getSite = (id: string, includeRelations?: boolean) => siteApiService.getSite(id, includeRelations); export const getSiteBySlug = (slug: string) => siteApiService.getSiteBySlug(slug); export const getSiteByDomain = (domain: string) => siteApiService.getSiteByDomain(domain); export const updateSite = (id: string, data: UpdateSiteData) => siteApiService.updateSite(id, data); export const deleteSite = (id: string) => siteApiService.deleteSite(id); export const publishSite = (id: string) => siteApiService.publishSite(id); export const unpublishSite = (id: string) => siteApiService.unpublishSite(id); export const cloneSite = ( id: string, newData: Parameters<typeof siteApiService.cloneSite>[1] ) => siteApiService.cloneSite(id, newData); export const getSiteStats = (ownerId?: string) => siteApiService.getSiteStats(ownerId); export const searchSites = (query: string, ownerId?: string, limit?: number) => siteApiService.searchSites(query, ownerId, limit);