@restnfeel/agentc-starter-kit v1.0.0
๐ AgentC CMS Starter Kit
์์ ํ ์ํฐํ๋ผ์ด์ฆ๊ธ CMS ๋ชจ๋ - AI๊ฐ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ CMS ์์คํ
๐ ๋ชฉ์ฐจ
- ๋น ๋ฅธ ์์
- ๋ชจ๋ ๊ฐ์
- ์ค์น ๋ฐ ์ค์
- ์ปดํฌ๋ํธ ์ฌ์ฉ๋ฒ
- CMS ๊ด๋ฆฌ์ ๊ตฌํ
- API ์ฌ์ฉ๋ฒ
- ์ปค์คํฐ๋ง์ด์ง
- ๊ณ ๊ธ ๊ธฐ๋ฅ
- Task Master ์ํฌํ๋ก์ฐ
๐ ๋น ๋ฅธ ์์
1. ์ค์น
npm install @restnfeel/agentc-starter-kit
# ๋๋
yarn add @restnfeel/agentc-starter-kit2. Supabase ์ค์ (๊ธฐ๋ณธ BaaS)
Supabase ํ๋ก์ ํธ ์์ฑ
- Supabase ์ฝ์์์ ์ ํ๋ก์ ํธ ์์ฑ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋น๋ฐ๋ฒํธ ์ค์
ํ๊ฒฝ ๋ณ์ ์ค์
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"๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
npx prisma db push npx prisma generateSupabase 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('์ฌํ์ต ์์')}
/>๊ณ ๊ฐ ๊ฒฝํ
- ํ๋กํ ๋ฒํผ: ์ฐ์ธก ํ๋จ์ ์ฑํ ์์ด์ฝ ํ์
- ํด๋ฆญ ์ ์ฑํ ์ฐฝ: ๊น๋ํ ์ฑํ ์ธํฐํ์ด์ค ๋ ธ์ถ
- ์ถ์ฒ ์ง๋ฌธ: ํ์ต๋ ๋ด์ฉ ๊ธฐ๋ฐ 3๊ฐ์ง ์ง๋ฌธ ์๋ ์ ์
- ์ง๋ฅํ ๋ต๋ณ: ์ ๋ก๋๋ ๋ฌธ์์ ์ ๋ ฅ๋ ์ง์์์๋ง ๋ต๋ณ
์ด ์์คํ ์ ํตํด ๊ณ ๊ฐ์ 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 ๊ธฐ๋ฐ ํ๋ก์ ํธ ์์ฑ
- 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 ์ค์ ์๊ตฌ์ฌํญ
- ์ฌ์ฉ์ ์ธ์ฆ (์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ, ์์
๋ก๊ทธ์ธ)
- ์ฝํ
์ธ ๊ด๋ฆฌ (ํ์ด์ง, ๋ฏธ๋์ด)
- ํ์ผ ์
๋ก๋ ๋ฐ ์ ์ฅ
- ์ค์๊ฐ ์
๋ฐ์ดํธ (์ ํ์ฌํญ)- ํ์คํฌ ์๋ ์์ฑ:
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 nextAI ๊ฐ๋ฐ์๋ฅผ ์ํ Task Master ํ์ฉ๋ฒ
- ํ๋ก์ ํธ ์์: PRD๋ฅผ ๋ถ์ํ์ฌ ์ฒด๊ณ์ ์ธ ํ์คํฌ ์์ฑ
- ์ฐ์ ์์ ๊ด๋ฆฌ: ์์กด์ฑ๊ณผ ์ฐ์ ์์์ ๋ฐ๋ฅธ ์์ ์์ ๊ฒฐ์
- ๋ณต์ก๋ ๋ถ์: AI๊ฐ ๊ฐ ํ์คํฌ์ ๋์ด๋๋ฅผ ๋ถ์ํ์ฌ ์ ์ ํ ๋ถํด
- ์งํ ์ํฉ ์ถ์ : ์ค์๊ฐ์ผ๋ก ํ๋ก์ ํธ ์งํ๋ฅ ๋ชจ๋ํฐ๋ง
- ๋ณ๊ฒฝ์ฌํญ ๊ด๋ฆฌ: ๊ณ ๊ฐ ์๊ตฌ์ฌํญ ๋ณ๊ฒฝ ์ ๊ด๋ จ ํ์คํฌ ์๋ ์ ๋ฐ์ดํธ
์์ธํ 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-cms3. ์ฑ๋ฅ ์ต์ ํ
// next.config.js
module.exports = {
experimental: {
appDir: true,
},
images: {
domains: ["res.cloudinary.com"],
formats: ["image/webp", "image/avif"],
},
compress: true,
poweredByHeader: false,
};๐ ์ถ๊ฐ ์๋ฃ
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ฐ๋ผํ๋ฉด AI๊ฐ ์ด๋ค ๊ณ ๊ฐ ์๊ตฌ์ฌํญ๋ ๋น ๋ฅด๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค!