1.0.1 โ€ข Published 6 months ago

@restnfeel/agentc-starter-kit v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

๐Ÿš€ AgentC CMS Starter Kit

์™„์ „ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ CMS ๋ชจ๋“ˆ - AI๊ฐ€ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ CMS ์‹œ์Šคํ…œ

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘

1. ์„ค์น˜

npm install @restnfeel/agentc-starter-kit
# ๋˜๋Š”
yarn add @restnfeel/agentc-starter-kit

2. Supabase ์„ค์ • (๊ธฐ๋ณธ BaaS)

  1. Supabase ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

    • Supabase ์ฝ˜์†”์—์„œ ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
    • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
  2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

    cp .env.example .env.local

    .env.local ํŒŒ์ผ์— Supabase ์ •๋ณด ์ž…๋ ฅ:

    DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
    SUPABASE_URL="https://[YOUR-PROJECT-REF].supabase.co"
    SUPABASE_ANON_KEY="your-supabase-anon-key"
    NEXTAUTH_SECRET="your-nextauth-secret"
    
    # RAG ์ฑ—๋ด‡์šฉ LLM API ํ‚ค (OpenAI, Anthropic ๋“ฑ)
    LLM_API_KEY="your-llm-api-key"
  3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

    npx prisma db push
    npx prisma generate
  4. Supabase Storage ์„ค์ • (RAG ์ฑ—๋ด‡์šฉ)

    • Supabase ์ฝ˜์†”์—์„œ Storage > Create Bucket > "documents" ์ƒ์„ฑ
    • ๋ฒ„ํ‚ท ์ •์ฑ…์—์„œ ์—…๋กœ๋“œ ๊ถŒํ•œ ์„ค์ •

3. ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

import {
  HeroSection,
  AboutSection,
  AuthProvider,
  createSupabaseAdapter,
} from "@restnfeel/agentc-starter-kit";
import { createClient } from "@supabase/supabase-js";

// Supabase ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// ์ธ์ฆ ์–ด๋Œ‘ํ„ฐ ์„ค์ •
const authAdapter = createSupabaseAdapter(supabase);

export default function Home() {
  return (
    <AuthProvider {...authAdapter}>
      <HeroSection
        title="AgentC๋กœ ๋น ๋ฅธ ์›น์‚ฌ์ดํŠธ ๊ตฌ์ถ•"
        subtitle="AI์™€ ํ•จ๊ป˜ ํ•˜๋Š” ํ˜„๋Œ€์ ์ธ CMS ์†”๋ฃจ์…˜"
        ctaText="์‹œ์ž‘ํ•˜๊ธฐ"
        ctaLink="/get-started"
      />
      <AboutSection
        title="์™œ AgentC์ธ๊ฐ€?"
        content="๊ฐœ๋ฐœ์ž์™€ ๊ธฐ์—…์„ ์œ„ํ•œ ์™„๋ฒฝํ•œ ์›น์‚ฌ์ดํŠธ ๊ตฌ์ถ• ์†”๋ฃจ์…˜"
      />
    </AuthProvider>
  );
}

๐Ÿ—๏ธ ๋ชจ๋“ˆ ๊ฐœ์š”

๐Ÿ“ฆ ํฌํ•จ๋œ ๊ตฌ์„ฑ ์š”์†Œ

1. ์„น์…˜ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (28๊ฐœ)

import {
  HeroSection,
  AboutSection,
  FeaturesSection,
  GallerySection,
  ContactSection,
  TestimonialSection,
  PricingSection,
  TeamSection,
  FAQSection,
  CTASection,
} from "@restnfeel/agentc-starter-kit/components";

2. CMS ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

import {
  PageEditor,
  ComponentLibrary,
  MediaManager,
  UserManager,
  ContentTypeManager,
} from "@restnfeel/agentc-starter-kit/admin";

3. API ์‹œ์Šคํ…œ

import {
  createCMSApiRoutes,
  mediaUploadHandler,
  contentValidator,
} from "@restnfeel/agentc-starter-kit/api";

4. RAG ๊ธฐ๋ฐ˜ AI ์ฑ—๋ด‡ ์‹œ์Šคํ…œ

import {
  ChatbotProvider,
  FloatingChatButton,
  ChatDock,
  DocumentUploader,
  KnowledgeEditor,
  RAGManager,
} from "@restnfeel/agentc-starter-kit/chatbot";

๐Ÿ“ฑ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ๋ฒ•

Hero ์„น์…˜ ๊ธฐ๋ณธ ์‚ฌ์šฉ

import { HeroSection } from "@restnfeel/agentc-starter-kit";

function HomePage() {
  return (
    <HeroSection
      title="ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค"
      subtitle="์ตœ๊ณ ์˜ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค"
      backgroundImage="/hero-bg.jpg"
      ctaButton={{
        text: "์‹œ์ž‘ํ•˜๊ธฐ",
        link: "/get-started",
        variant: "primary",
      }}
      features={["๋น ๋ฅธ ๊ตฌํ˜„", "์™„์ „ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•", "์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ"]}
    />
  );
}

Features ์„น์…˜ ์‚ฌ์šฉ

import { FeaturesSection } from "@restnfeel/agentc-starter-kit";

function FeaturesPage() {
  const features = [
    {
      icon: "๐Ÿš€",
      title: "๋น ๋ฅธ ๋ฐฐํฌ",
      description: "๋ช‡ ๋ถ„ ์•ˆ์— ์‚ฌ์ดํŠธ๋ฅผ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค",
      link: "/docs/deployment",
    },
    {
      icon: "๐Ÿ”’",
      title: "๋ณด์•ˆ",
      description: "์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์ด ๊ธฐ๋ณธ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค",
      link: "/docs/security",
    },
    {
      icon: "๐Ÿ“ฑ",
      title: "๋ฐ˜์‘ํ˜•",
      description: "๋ชจ๋“  ๋””๋ฐ”์ด์Šค์—์„œ ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค",
      link: "/docs/responsive",
    },
  ];

  return (
    <FeaturesSection
      title="ํ•ต์‹ฌ ๊ธฐ๋Šฅ"
      description="์šฐ๋ฆฌ ์ œํ’ˆ์˜ ์ฃผ์š” ํŠน์ง•๋“ค์„ ํ™•์ธํ•ด๋ณด์„ธ์š”"
      features={features}
      layout="grid" // 'grid' | 'list' | 'cards'
      columns={3}
    />
  );
}

Gallery ์„น์…˜ ์‚ฌ์šฉ

import { GallerySection } from "@restnfeel/agentc-starter-kit";

function PortfolioPage() {
  const images = [
    {
      src: "/portfolio/project1.jpg",
      alt: "ํ”„๋กœ์ ํŠธ 1",
      title: "์›น ๊ฐœ๋ฐœ",
      description: "ํ˜„๋Œ€์ ์ธ ์›น์‚ฌ์ดํŠธ ๊ตฌ์ถ•",
    },
    {
      src: "/portfolio/project2.jpg",
      alt: "ํ”„๋กœ์ ํŠธ 2",
      title: "๋ชจ๋ฐ”์ผ ์•ฑ",
      description: "ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์•ฑ ๊ฐœ๋ฐœ",
    },
  ];

  return (
    <GallerySection
      title="ํฌํŠธํด๋ฆฌ์˜ค"
      images={images}
      layout="masonry" // 'grid' | 'masonry' | 'carousel'
      lightbox={true}
      showThumbnails={true}
    />
  );
}

Contact ํผ ์„น์…˜ ์‚ฌ์šฉ

import { ContactSection } from "@restnfeel/agentc-starter-kit";

function ContactPage() {
  return (
    <ContactSection
      title="๋ฌธ์˜ํ•˜๊ธฐ"
      description="๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์—ฐ๋ฝ์ฃผ์„ธ์š”"
      fields={[
        { type: "text", name: "name", label: "์ด๋ฆ„", required: true },
        { type: "email", name: "email", label: "์ด๋ฉ”์ผ", required: true },
        { type: "tel", name: "phone", label: "์ „ํ™”๋ฒˆํ˜ธ" },
        { type: "textarea", name: "message", label: "๋ฉ”์‹œ์ง€", required: true },
      ]}
      onSubmit={(data) => {
        // ์ปค์Šคํ…€ ์ฒ˜๋ฆฌ ๋กœ์ง
        console.log("Form submitted:", data);
      }}
      contactInfo={{
        address: "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123",
        phone: "02-1234-5678",
        email: "contact@company.com",
        hours: "ํ‰์ผ 9:00-18:00",
      }}
    />
  );
}

RAG ๊ธฐ๋ฐ˜ AI ์ฑ—๋ด‡ ์‚ฌ์šฉ

import {
  ChatbotProvider,
  FloatingChatButton,
  DocumentUploader,
  KnowledgeEditor,
  RAGManager,
} from "@restnfeel/agentc-starter-kit";

// ๊ณ ๊ฐ ์‚ฌ์ดํŠธ์— AI ์ฑ—๋ด‡ ์ถ”๊ฐ€
function CustomerSite() {
  return (
    <ChatbotProvider
      vectorStorePath="/store"
      llmApiKey={process.env.LLM_API_KEY}
      initialQuestions={3}
      supabaseConfig={{
        url: process.env.NEXT_PUBLIC_SUPABASE_URL,
        anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
        bucket: "documents",
      }}
    >
      <main>
        <HeroSection />
        <FeaturesSection />

        {/* ์šฐ์ธก ํ•˜๋‹จ ํ”Œ๋กœํŒ… ์ฑ—๋ด‡ ๋ฒ„ํŠผ ์ž๋™ ํ‘œ์‹œ */}
        <FloatingChatButton />
      </main>
    </ChatbotProvider>
  );
}

// ๊ด€๋ฆฌ์ž ์ง€์‹ ๋ฒ ์ด์Šค ๊ด€๋ฆฌ
function AdminKnowledgeBase() {
  return (
    <div className="admin-knowledge">
      <h2>AI ์ฑ—๋ด‡ ํ•™์Šต ๊ด€๋ฆฌ</h2>

      {/* ๋ฌธ์„œ ์—…๋กœ๋“œ (Supabase Storage) */}
      <DocumentUploader
        onUpload={(files) => console.log("์—…๋กœ๋“œ๋œ ๋ฌธ์„œ:", files)}
        acceptedTypes={["pdf", "doc", "docx", "txt"]}
        storage="supabase"
        bucket="documents"
        maxFileSize="10MB"
      />

      {/* ์ง์ ‘ ์ง€์‹ ์ž…๋ ฅ */}
      <KnowledgeEditor
        onSave={(content) => console.log("์ €์žฅ๋œ ์ง€์‹:", content)}
        placeholder="์‚ฌ์—… ์•„์ดํ…œ์— ๋Œ€ํ•ด ๊ฐœ์กฐ์‹์œผ๋กœ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”..."
        autoSave={true}
      />

      {/* RAG ์‹œ์Šคํ…œ ๊ด€๋ฆฌ */}
      <RAGManager
        vectorStore="/store"
        onReindex={() => console.log("๋ฒกํ„ฐ ์žฌ์ธ๋ฑ์‹ฑ ์‹œ์ž‘")}
        showMetrics={true}
        showPreview={true}
      />
    </div>
  );
}

โš™๏ธ CMS ๊ด€๋ฆฌ์ž ๊ตฌํ˜„

1. ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ

// app/admin/page.tsx
import { AdminDashboard } from "@restnfeel/agentc-starter-kit/admin";

export default function AdminPage() {
  return (
    <AdminDashboard
      user={{
        name: "๊ด€๋ฆฌ์ž",
        role: "admin",
        avatar: "/admin-avatar.jpg",
      }}
      stats={{
        pages: 12,
        components: 28,
        media: 156,
        users: 8,
      }}
      quickActions={[
        { label: "์ƒˆ ํŽ˜์ด์ง€", action: "create-page", icon: "๐Ÿ“„" },
        { label: "์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€", action: "add-component", icon: "๐Ÿงฉ" },
        { label: "๋ฏธ๋””์–ด ์—…๋กœ๋“œ", action: "upload-media", icon: "๐Ÿ–ผ๏ธ" },
      ]}
    />
  );
}

2. ํŽ˜์ด์ง€ ์—๋””ํ„ฐ

// app/admin/pages/[id]/edit/page.tsx
import { PageEditor } from "@restnfeel/agentc-starter-kit/admin";

export default function EditPage({ params }: { params: { id: string } }) {
  return (
    <PageEditor
      pageId={params.id}
      onSave={(pageData) => {
        // ํŽ˜์ด์ง€ ์ €์žฅ ๋กœ์ง
        console.log("Saving page:", pageData);
      }}
      componentLibrary={{
        categories: ["content", "media", "layout", "interactive"],
        search: true,
        dragAndDrop: true,
      }}
      preview={{
        enabled: true,
        responsive: true,
        realtime: true,
      }}
    />
  );
}

3. ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ด€๋ฆฌ

// app/admin/components/page.tsx
import { ComponentLibraryManager } from "@restnfeel/agentc-starter-kit/admin";

export default function ComponentsPage() {
  return (
    <ComponentLibraryManager
      components={[
        {
          type: "hero",
          category: "layout",
          metadata: {
            title: "Hero Section",
            description: "ํŽ˜์ด์ง€ ์ƒ๋‹จ ๋ฉ”์ธ ์„น์…˜",
            tags: ["header", "banner", "main"],
            icon: "๐ŸŽฏ",
          },
        },
        // ... ๋” ๋งŽ์€ ์ปดํฌ๋„ŒํŠธ๋“ค
      ]}
      onComponentSelect={(component) => {
        // ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฒ˜๋ฆฌ
      }}
      filters={{
        category: true,
        search: true,
        tags: true,
      }}
    />
  );
}

๐Ÿ”Œ API ์‚ฌ์šฉ๋ฒ•

1. API ๋ผ์šฐํŠธ ์„ค์ •

// app/api/cms/[...slug]/route.ts
import { createCMSApiRoutes } from "@restnfeel/agentc-starter-kit/api";

const { GET, POST, PUT, DELETE } = createCMSApiRoutes({
  database: {
    type: "postgresql", // ๋˜๋Š” "mysql", "sqlite"
    connection: process.env.DATABASE_URL,
  },
  auth: {
    enabled: true,
    provider: "nextauth", // ๋˜๋Š” "custom"
    roles: ["admin", "editor", "viewer"],
  },
  media: {
    storage: "local", // ๋˜๋Š” "s3", "cloudinary"
    maxSize: "10MB",
    allowedTypes: ["image/*", "video/*", "application/pdf"],
  },
});

export { GET, POST, PUT, DELETE };

2. ๋ฏธ๋””์–ด ์—…๋กœ๋“œ API

// app/api/upload/route.ts
import { mediaUploadHandler } from "@restnfeel/agentc-starter-kit/api";

export const POST = mediaUploadHandler({
  destination: "uploads/",
  generateThumbnails: true,
  imageOptimization: {
    quality: 80,
    formats: ["webp", "jpeg"],
    sizes: [400, 800, 1200],
  },
  onSuccess: (fileInfo) => {
    console.log("File uploaded:", fileInfo);
  },
});

3. ์ฝ˜ํ…์ธ  ๊ฒ€์ฆ API

// app/api/content/validate/route.ts
import { contentValidator } from "@restnfeel/agentc-starter-kit/api";

export const POST = async (request: Request) => {
  const contentData = await request.json();

  const validation = contentValidator(contentData, {
    schema: "page", // ๋˜๋Š” "component", "media"
    strict: true,
    sanitize: true,
  });

  if (!validation.isValid) {
    return new Response(
      JSON.stringify({
        error: "Validation failed",
        details: validation.errors,
      }),
      { status: 400 }
    );
  }

  return new Response(
    JSON.stringify({
      success: true,
      sanitizedData: validation.data,
    })
  );
};

๐ŸŽจ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

1. ํ…Œ๋งˆ ์‹œ์Šคํ…œ

// lib/theme.ts
import { createTheme } from "@restnfeel/agentc-starter-kit/theme";

export const customTheme = createTheme({
  colors: {
    primary: "#3B82F6",
    secondary: "#10B981",
    accent: "#F59E0B",
    background: "#FFFFFF",
    text: "#1F2937",
  },
  fonts: {
    heading: "Pretendard, sans-serif",
    body: "Pretendard, sans-serif",
    mono: "JetBrains Mono, monospace",
  },
  spacing: {
    xs: "0.5rem",
    sm: "1rem",
    md: "1.5rem",
    lg: "2rem",
    xl: "3rem",
  },
  breakpoints: {
    sm: "640px",
    md: "768px",
    lg: "1024px",
    xl: "1280px",
  },
});

// app/layout.tsx์—์„œ ์ ์šฉ
import { ThemeProvider } from "@restnfeel/agentc-starter-kit/theme";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider theme={customTheme}>{children}</ThemeProvider>
      </body>
    </html>
  );
}

2. ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ

// components/CustomHero.tsx
import { BaseSection } from "@restnfeel/agentc-starter-kit/base";

interface CustomHeroProps {
  title: string;
  subtitle?: string;
  backgroundVideo?: string;
  particles?: boolean;
}

export function CustomHero({
  title,
  subtitle,
  backgroundVideo,
  particles = false,
}: CustomHeroProps) {
  return (
    <BaseSection
      className="min-h-screen flex items-center justify-center relative"
      background={backgroundVideo ? "video" : "gradient"}
    >
      {backgroundVideo && (
        <video
          autoPlay
          muted
          loop
          className="absolute inset-0 w-full h-full object-cover"
        >
          <source src={backgroundVideo} type="video/mp4" />
        </video>
      )}

      {particles && <ParticleBackground />}

      <div className="relative z-10 text-center text-white">
        <h1 className="text-6xl font-bold mb-4">{title}</h1>
        {subtitle && <p className="text-xl opacity-90">{subtitle}</p>}
      </div>
    </BaseSection>
  );
}

3. ์Šคํƒ€์ผ ์˜ค๋ฒ„๋ผ์ด๋“œ

/* styles/custom.css */
@import "@restnfeel/agentc-starter-kit/styles.css";

/* ์ปค์Šคํ…€ ์Šคํƒ€์ผ */
.agentc-hero {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.agentc-button-primary {
  background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
  box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
}

.agentc-card {
  backdrop-filter: blur(10px);
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
}

๐Ÿค– RAG ๊ธฐ๋ฐ˜ AI ์ฑ—๋ด‡ ์‹œ์Šคํ…œ

AgentC Starter Kit์—๋Š” LangChain JS ๊ธฐ๋ฐ˜์˜ ๊ฐ•๋ ฅํ•œ RAG (Retrieval-Augmented Generation) ์ฑ—๋ด‡ ์‹œ์Šคํ…œ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  • ์ง€๋Šฅํ˜• ๋ฌธ์„œ ํ•™์Šต: PDF, DOC, TXT ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์—ฌ ์ž๋™ ํ•™์Šต
  • ๊ฐœ์กฐ์‹ ์ง€์‹ ์ž…๋ ฅ: ๊ด€๋ฆฌ์ž๊ฐ€ ์ง์ ‘ ์‚ฌ์—… ์ •๋ณด๋ฅผ ์ž…๋ ฅ ๊ฐ€๋Šฅ
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰: ํ•™์Šต๋œ ๋‚ด์šฉ์—์„œ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ
  • Supabase ํ†ตํ•ฉ: ๋ฌธ์„œ๋Š” Supabase Storage์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ
  • ์ฆ‰์‹œ ์‚ฌ์šฉ: ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€๋กœ ์ฑ—๋ด‡ ์™„์„ฑ

๋น ๋ฅธ ์„ค์ •

// 1. ChatbotProvider๋กœ ์•ฑ ๊ฐ์‹ธ๊ธฐ
<ChatbotProvider
  vectorStorePath="/store"
  llmApiKey={process.env.LLM_API_KEY}
  supabaseConfig={{
    url: process.env.NEXT_PUBLIC_SUPABASE_URL,
    anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    bucket: "documents"
  }}
>
  <YourApp />
</ChatbotProvider>

// 2. ํ”Œ๋กœํŒ… ์ฑ—๋ด‡ ๋ฒ„ํŠผ ์ถ”๊ฐ€
<FloatingChatButton />

๊ด€๋ฆฌ์ž ์ง€์‹ ๋ฒ ์ด์Šค ๊ตฌ์ถ•

// ๋ฌธ์„œ ์—…๋กœ๋“œ
<DocumentUploader
  storage="supabase"
  bucket="documents"
  acceptedTypes={['pdf', 'doc', 'txt']}
  onUpload={(files) => console.log('ํ•™์Šต ์‹œ์ž‘:', files)}
/>

// ์ง์ ‘ ์ง€์‹ ์ž…๋ ฅ
<KnowledgeEditor
  placeholder="ํšŒ์‚ฌ ์†Œ๊ฐœ, ์ œํ’ˆ ์ •๋ณด ๋“ฑ์„ ์ž…๋ ฅํ•˜์„ธ์š”..."
  onSave={(content) => console.log('์ง€์‹ ์ €์žฅ:', content)}
/>

// ํ•™์Šต ์ƒํƒœ ๊ด€๋ฆฌ
<RAGManager
  vectorStore="/store"
  showMetrics={true}
  onReindex={() => console.log('์žฌํ•™์Šต ์‹œ์ž‘')}
/>

๊ณ ๊ฐ ๊ฒฝํ—˜

  1. ํ”Œ๋กœํŒ… ๋ฒ„ํŠผ: ์šฐ์ธก ํ•˜๋‹จ์— ์ฑ„ํŒ… ์•„์ด์ฝ˜ ํ‘œ์‹œ
  2. ํด๋ฆญ ์‹œ ์ฑ„ํŒ…์ฐฝ: ๊น”๋”ํ•œ ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ๋…ธ์ถœ
  3. ์ถ”์ฒœ ์งˆ๋ฌธ: ํ•™์Šต๋œ ๋‚ด์šฉ ๊ธฐ๋ฐ˜ 3๊ฐ€์ง€ ์งˆ๋ฌธ ์ž๋™ ์ œ์‹œ
  4. ์ง€๋Šฅํ˜• ๋‹ต๋ณ€: ์—…๋กœ๋“œ๋œ ๋ฌธ์„œ์™€ ์ž…๋ ฅ๋œ ์ง€์‹์—์„œ๋งŒ ๋‹ต๋ณ€

์ด ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๊ณ ๊ฐ์€ 24์‹œ๊ฐ„ ์–ธ์ œ๋“ ์ง€ ์ •ํ™•ํ•œ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๊ณ , ๊ด€๋ฆฌ์ž๋Š” ๊ฐ„ํŽธํ•˜๊ฒŒ ์ง€์‹ ๋ฒ ์ด์Šค๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ”ง ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

1. ์›Œํฌํ”Œ๋กœ์šฐ ์‹œ์Šคํ…œ

// lib/workflow.ts
import { createWorkflow } from "@restnfeel/agentc-starter-kit/workflow";

export const contentWorkflow = createWorkflow({
  stages: [
    { name: "draft", label: "์ดˆ์•ˆ", color: "gray" },
    { name: "review", label: "๊ฒ€ํ†  ์ค‘", color: "yellow" },
    { name: "approved", label: "์Šน์ธ๋จ", color: "green" },
    { name: "published", label: "๊ฒŒ์‹œ๋จ", color: "blue" },
  ],
  transitions: [
    { from: "draft", to: "review", role: "editor" },
    { from: "review", to: "approved", role: "admin" },
    { from: "approved", to: "published", role: "admin" },
  ],
  notifications: {
    email: true,
    slack: process.env.SLACK_WEBHOOK_URL,
  },
});

2. ๋‹ค๊ตญ์–ด ์ง€์›

// lib/i18n.ts
import { createI18n } from "@restnfeel/agentc-starter-kit/i18n";

export const i18n = createI18n({
  defaultLocale: "ko",
  locales: ["ko", "en", "ja"],
  fallbackLocale: "en",
  loadPath: "/locales/{{lng}}/{{ns}}.json",
  detection: {
    order: ["path", "cookie", "header"],
    caches: ["cookie"],
  },
});

// ์‚ฌ์šฉ๋ฒ•
import { useTranslation } from "@restnfeel/agentc-starter-kit/i18n";

function MyComponent() {
  const { t } = useTranslation("common");

  return (
    <div>
      <h1>{t("welcome")}</h1>
      <p>{t("description")}</p>
    </div>
  );
}

3. SEO ์ตœ์ ํ™”

// lib/seo.ts
import { createSEOManager } from "@restnfeel/agentc-starter-kit/seo";

export const seoManager = createSEOManager({
  defaultMeta: {
    title: "์‚ฌ์ดํŠธ ์ œ๋ชฉ",
    description: "์‚ฌ์ดํŠธ ์„ค๋ช…",
    keywords: ["ํ‚ค์›Œ๋“œ1", "ํ‚ค์›Œ๋“œ2"],
    openGraph: {
      siteName: "์‚ฌ์ดํŠธ๋ช…",
      type: "website",
      image: "/og-image.jpg",
    },
  },
  autoGenerate: {
    sitemap: true,
    robots: true,
    schema: true,
  },
});

// ํŽ˜์ด์ง€๋ณ„ SEO ์„ค์ •
import { SEOHead } from "@restnfeel/agentc-starter-kit/seo";

export default function Page() {
  return (
    <>
      <SEOHead
        title="ํŽ˜์ด์ง€ ์ œ๋ชฉ"
        description="ํŽ˜์ด์ง€ ์„ค๋ช…"
        canonical="https://example.com/page"
        schema={{
          "@type": "Article",
          headline: "ํŽ˜์ด์ง€ ์ œ๋ชฉ",
          author: "์ž‘์„ฑ์ž๋ช…",
        }}
      />
      <main>{/* ํŽ˜์ด์ง€ ๋‚ด์šฉ */}</main>
    </>
  );
}

4. ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

// lib/analytics.ts
import { createAnalytics } from "@restnfeel/agentc-starter-kit/analytics";

export const analytics = createAnalytics({
  providers: {
    googleAnalytics: process.env.GA_TRACKING_ID,
    mixpanel: process.env.MIXPANEL_TOKEN,
    custom: {
      endpoint: "/api/analytics",
      events: ["pageview", "click", "form_submit"],
    },
  },
  privacy: {
    cookieConsent: true,
    anonymizeIP: true,
    respectDNT: true,
  },
});

๐ŸŽฏ Task Master ์›Œํฌํ”Œ๋กœ์šฐ

์ด ๋ชจ๋“ˆ์€ Task Master AI ์‹œ์Šคํ…œ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋„๋ก ์ตœ์ ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. AI๊ฐ€ ์ฒด๊ณ„์ ์œผ๋กœ ๊ณ ๊ฐ ์‚ฌ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Task Master ์ดˆ๊ธฐ ์„ค์ •

# 1. Task Master ์ดˆ๊ธฐํ™”
npm install -g task-master-ai
task-master init --name="Customer Website" --description="๊ณ ๊ฐ์‚ฌ ์›น์‚ฌ์ดํŠธ ๊ตฌํ˜„"

# 2. AI ๋ชจ๋ธ ์„ค์ • (๊ถŒ์žฅ)
task-master models --setup
# ๋˜๋Š” ๋น ๋ฅธ ์„ค์ •
task-master models --set-main="anthropic/claude-3-5-sonnet-20241022" --set-research="anthropic/claude-3-5-sonnet-20241022"

PRD ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

  1. PRD ์ž‘์„ฑ (scripts/prd.txt):
# ๊ณ ๊ฐ์‚ฌ ์›น์‚ฌ์ดํŠธ ํ”„๋กœ์ ํŠธ

## ํ”„๋กœ์ ํŠธ ๊ฐœ์š”
- ํšŒ์‚ฌ๋ช…: [๊ณ ๊ฐ์‚ฌ๋ช…]
- ์‚ฌ์ดํŠธ ์œ ํ˜•: [๋น„์ฆˆ๋‹ˆ์Šค/์ด์ปค๋จธ์Šค/ํฌํŠธํด๋ฆฌ์˜ค/๋ธ”๋กœ๊ทธ]
- ์ฃผ์š” ๊ธฐ๋Šฅ: [ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค]
- ๋ฐฑ์—”๋“œ: Supabase (PostgreSQL, Auth, Storage)

## ๊ธฐ์ˆ  ์Šคํƒ
- Frontend: Next.js 14 + TypeScript + Tailwind CSS
- Backend: Supabase (BaaS)
- Database: PostgreSQL (Supabase)
- Auth: Supabase Auth + NextAuth.js
- File Storage: Supabase Storage
- State Management: Zustand
- UI Components: @restnfeel/agentc-starter-kit

## ํ•„์ˆ˜ ํŽ˜์ด์ง€
- ํ™ˆํŽ˜์ด์ง€ (Hero, About, Features, Contact)
- ์ œํ’ˆ/์„œ๋น„์Šค ์†Œ๊ฐœ
- ๊ณ ๊ฐ ํ›„๊ธฐ
- ์—ฐ๋ฝ์ฒ˜/๋ฌธ์˜ํ•˜๊ธฐ
- ๊ด€๋ฆฌ์ž ์‹œ์Šคํ…œ (CMS)

## Supabase ์„ค์ • ์š”๊ตฌ์‚ฌํ•ญ
- ์‚ฌ์šฉ์ž ์ธ์ฆ (์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ, ์†Œ์…œ ๋กœ๊ทธ์ธ)
- ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ (ํŽ˜์ด์ง€, ๋ฏธ๋””์–ด)
- ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ์ €์žฅ
- ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (์„ ํƒ์‚ฌํ•ญ)
  1. ํƒœ์Šคํฌ ์ž๋™ ์ƒ์„ฑ:
task-master parse-prd scripts/prd.txt --num-tasks=15 --research

ํ‘œ์ค€ ์›Œํฌํ”Œ๋กœ์šฐ

# 1. ๋‹ค์Œ ์ž‘์—… ํ™•์ธ
task-master next

# 2. ๋ณต์žกํ•œ ํƒœ์Šคํฌ ์„ธ๋ถ„ํ™” (์˜ˆ: Supabase ์„ค์ •)
task-master expand --id=1 --research --force
# ์ƒ์„ฑ ์˜ˆ์‹œ:
# 1.1 Supabase ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ ์„ค์ •
# 1.2 ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ตฌ์„ฑ
# 1.3 Prisma ์Šคํ‚ค๋งˆ ์ƒ์„ฑ
# 1.4 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
# 1.5 Supabase Auth ์„ค์ •

# 3. ํƒœ์Šคํฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
task-master set-status --id=1.1 --status=done

# 4. ์ง„ํ–‰๋ฅ  ํ™•์ธ
task-master list --status=pending

# 5. ๊ตฌํ˜„ ๋…ธํŠธ ์ถ”๊ฐ€
task-master update-subtask --id=1.2 --prompt="Supabase URL๊ณผ ํ‚ค ์„ค์ • ์™„๋ฃŒ"

# 6. ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰
task-master next

AI ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Task Master ํ™œ์šฉ๋ฒ•

  1. ํ”„๋กœ์ ํŠธ ์‹œ์ž‘: PRD๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ฒด๊ณ„์ ์ธ ํƒœ์Šคํฌ ์ƒ์„ฑ
  2. ์šฐ์„ ์ˆœ์œ„ ๊ด€๋ฆฌ: ์˜์กด์„ฑ๊ณผ ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ฅธ ์ž‘์—… ์ˆœ์„œ ๊ฒฐ์ •
  3. ๋ณต์žก๋„ ๋ถ„์„: AI๊ฐ€ ๊ฐ ํƒœ์Šคํฌ์˜ ๋‚œ์ด๋„๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ๋ถ„ํ•ด
  4. ์ง„ํ–‰ ์ƒํ™ฉ ์ถ”์ : ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ”„๋กœ์ ํŠธ ์ง„ํ–‰๋ฅ  ๋ชจ๋‹ˆํ„ฐ๋ง
  5. ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ด€๋ฆฌ: ๊ณ ๊ฐ ์š”๊ตฌ์‚ฌํ•ญ ๋ณ€๊ฒฝ ์‹œ ๊ด€๋ จ ํƒœ์Šคํฌ ์ž๋™ ์—…๋ฐ์ดํŠธ

์ž์„ธํ•œ Task Master ๊ฐ€์ด๋“œ๋Š” Task Master ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.


๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์šด์˜

1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

# .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/cms"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-key"

# Supabase (๊ธฐ๋ณธ BaaS)
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_ANON_KEY="your-supabase-anon-key"

# RAG ์ฑ—๋ด‡ AI API
LLM_API_KEY="your-openai-or-anthropic-api-key"

# ๋ฏธ๋””์–ด ์ €์žฅ์†Œ (์„ ํƒ์‚ฌํ•ญ - Supabase Storage ์‚ฌ์šฉ ์‹œ ๋ถˆํ•„์š”)
CLOUDINARY_CLOUD_NAME="your-cloud-name"
CLOUDINARY_API_KEY="your-api-key"
CLOUDINARY_API_SECRET="your-api-secret"

# ์ด๋ฉ”์ผ ์„œ๋น„์Šค
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="your-app-password"

# ๋ถ„์„
GOOGLE_ANALYTICS_ID="GA_TRACKING_ID"

2. ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ

# ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ
npm run build

# Vercel ๋ฐฐํฌ
npx vercel

# Docker ๋ฐฐํฌ
docker build -t my-cms .
docker run -p 3000:3000 my-cms

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”

// next.config.js
module.exports = {
  experimental: {
    appDir: true,
  },
  images: {
    domains: ["res.cloudinary.com"],
    formats: ["image/webp", "image/avif"],
  },
  compress: true,
  poweredByHeader: false,
};

๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ


๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋”ฐ๋ผํ•˜๋ฉด AI๊ฐ€ ์–ด๋–ค ๊ณ ๊ฐ ์š”๊ตฌ์‚ฌํ•ญ๋„ ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!