+
{CARDS.map((c) => (
= {
- informational: '정보 전달형',
- review: '리뷰/후기형',
- howto: '방법/튜토리얼형',
- listicle: '리스트형 (OO가지)',
- comparison: '비교 분석형',
- story: '에세이/스토리형',
-};
-
-const TONE_LABELS: Record = {
- professional: '전문적이고 신뢰감 있는',
- friendly: '친근하고 대화하듯',
- casual: '편한 말투, 구어체',
- formal: '격식 있는 존댓말',
-};
-
-const LENGTH_RANGES: Record = {
- short: '800~1200자',
- medium: '1500~2500자',
- long: '3000~4500자',
-};
-
-function buildPrompt(req: BlogRequest): string {
- return `당신은 네이버 블로그 SEO 전문 작가입니다.
-
-## 작성 요청
-
-- **주제**: ${req.topic}
-- **핵심 키워드**: ${req.keywords.join(', ')}
-- **글 형식**: ${STYLE_LABELS[req.style] || req.style}
-- **톤앤매너**: ${TONE_LABELS[req.tone] || req.tone}
-- **목표 분량**: ${LENGTH_RANGES[req.length] || req.length}
-- **소제목 수**: ${req.sections}개
-- **이미지 가이드**: ${req.imageGuide ? '포함' : '미포함'}
-
-## 출력 형식 (반드시 아래 JSON 구조로 출력)
-
-\`\`\`json
-{
- "title": "블로그 제목 (네이버 검색에 최적화된 30자 내외)",
- "subtitle": "부제목 또는 한 줄 요약",
- "sections": [
- {
- "heading": "소제목",
- "body": "본문 내용 (마크다운 불릿·볼드 허용)",
- "imageSlot": true
- }
- ],
- "tags": ["태그1", "태그2", "태그3", "태그4", "태그5"],
- "seoTitle": "검색 노출용 제목 (핵심 키워드 포함 40자 이내)",
- "seoDescription": "메타 설명 (120자 이내, 키워드 자연스럽게 포함)",
- "imageGuides": [
- {
- "position": "섹션 제목 또는 위치",
- "description": "어떤 이미지가 적합한지 설명",
- "searchKeyword": "이미지 검색 키워드",
- "altText": "대체 텍스트"
- }
- ]
-}
-\`\`\`
-
-## 작성 규칙
-
-1. 네이버 블로그 검색 상위 노출을 위해 핵심 키워드를 제목·첫 문단·소제목에 자연스럽게 배치
-2. 각 소제목(heading) 아래 본문은 구체적이고 실용적인 정보 포함
-3. 첫 문단은 독자의 공감 또는 궁금증을 유발하는 도입부
-4. 마지막 섹션은 요약 또는 CTA(댓글 유도, 공유 요청 등)
-5. 본문에서 핵심 키워드는 전체 글의 2~3% 밀도로 자연스럽게 반복
-6. imageSlot이 true인 섹션에는 반드시 imageGuides에 해당 위치의 이미지 가이드 포함
-7. 태그는 5~8개, 관련 검색어와 롱테일 키워드 조합
-8. JSON만 출력 — 설명이나 마크다운 코드블록 바깥 텍스트 금지`;
-}
-
-export async function generateBlogPost(req: BlogRequest): Promise {
- const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
-
- if (hasApiKey) {
- try {
- const ai = getClient();
- const response = await ai.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 4096,
- messages: [
- { role: 'user', content: buildPrompt(req) },
- ],
- });
-
- const text = response.content
- .filter((b): b is Anthropic.TextBlock => b.type === 'text')
- .map((b) => b.text)
- .join('');
-
- // JSON 파싱 (코드블록 래핑 제거)
- const jsonStr = text.replace(/^```json?\s*/i, '').replace(/\s*```$/i, '').trim();
- const parsed = JSON.parse(jsonStr);
-
- const sections: BlogSection[] = (parsed.sections || []).map((s: Record) => ({
- heading: String(s.heading || ''),
- body: String(s.body || ''),
- imageSlot: Boolean(s.imageSlot),
- }));
-
- const imageGuides: ImageGuide[] = (parsed.imageGuides || []).map((g: Record) => ({
- position: String(g.position || ''),
- description: String(g.description || ''),
- searchKeyword: String(g.searchKeyword || ''),
- altText: String(g.altText || ''),
- }));
-
- const totalChars = sections.reduce((sum, s) => sum + s.heading.length + s.body.length, 0);
-
- return {
- success: true,
- data: {
- title: String(parsed.title || ''),
- subtitle: String(parsed.subtitle || ''),
- content: sections,
- tags: Array.isArray(parsed.tags) ? parsed.tags.map(String) : [],
- seoTitle: String(parsed.seoTitle || ''),
- seoDescription: String(parsed.seoDescription || ''),
- imageGuides,
- meta: {
- charCount: totalChars,
- sectionCount: sections.length,
- estimatedReadTime: `${Math.max(1, Math.round(totalChars / 500))}분`,
- generatedAt: new Date().toISOString(),
- model: 'claude-sonnet-4-20250514',
- },
- },
- };
- } catch (err) {
- console.error('[BlogGenerator] AI error, using fallback:', err);
- }
- }
-
- // Fallback (API 키 없거나 실패 시)
- return buildFallback(req);
-}
-
-function buildFallback(req: BlogRequest): BlogResult {
- const sections: BlogSection[] = [];
- for (let i = 0; i < req.sections; i++) {
- if (i === 0) {
- sections.push({
- heading: `${req.topic}이란?`,
- body: `${req.keywords[0] || req.topic}에 대해 알아보겠습니다. 이 글에서는 ${req.topic}의 핵심 내용을 정리해 드립니다.`,
- imageSlot: req.imageGuide,
- });
- } else if (i === req.sections - 1) {
- sections.push({
- heading: '마치며',
- body: `지금까지 ${req.topic}에 대해 알아보았습니다. 도움이 되셨다면 댓글과 공감 부탁드립니다!`,
- });
- } else {
- sections.push({
- heading: `포인트 ${i}`,
- body: `${req.topic} 관련 상세 내용이 이 자리에 들어갑니다. AI API 키 설정 후 실제 콘텐츠가 생성됩니다.`,
- imageSlot: i % 2 === 0 && req.imageGuide,
- });
- }
- }
-
- return {
- success: true,
- data: {
- title: `${req.topic} — 완벽 가이드`,
- subtitle: `${req.keywords.join(', ')} 핵심 정리`,
- content: sections,
- tags: req.keywords.slice(0, 5),
- seoTitle: `${req.topic} ${req.keywords[0] || ''} 총정리`,
- seoDescription: `${req.topic}에 대한 핵심 정보를 정리했습니다.`,
- imageGuides: req.imageGuide
- ? [{ position: '도입부', description: '주제 대표 이미지', searchKeyword: req.keywords[0] || req.topic, altText: req.topic }]
- : [],
- meta: {
- charCount: sections.reduce((s, sec) => s + sec.heading.length + sec.body.length, 0),
- sectionCount: sections.length,
- estimatedReadTime: '1분',
- generatedAt: new Date().toISOString(),
- model: 'fallback (no API key)',
- },
- },
- };
-}
diff --git a/lib/blog-tools/types.ts b/lib/blog-tools/types.ts
deleted file mode 100644
index 29fb221..0000000
--- a/lib/blog-tools/types.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export interface BlogRequest {
- topic: string;
- keywords: string[];
- style: BlogStyle;
- tone: BlogTone;
- length: BlogLength;
- imageGuide: boolean;
- sections: number;
-}
-
-export type BlogStyle =
- | 'informational' // 정보 전달
- | 'review' // 리뷰/후기
- | 'howto' // 방법/튜토리얼
- | 'listicle' // 리스트형
- | 'comparison' // 비교 분석
- | 'story'; // 에세이/스토리
-
-export type BlogTone =
- | 'professional' // 전문적
- | 'friendly' // 친근한
- | 'casual' // 캐주얼
- | 'formal'; // 격식체
-
-export type BlogLength =
- | 'short' // 800~1200자
- | 'medium' // 1500~2500자
- | 'long'; // 3000~4500자
-
-export interface BlogResult {
- success: true;
- data: {
- title: string;
- subtitle: string;
- content: BlogSection[];
- tags: string[];
- seoTitle: string;
- seoDescription: string;
- imageGuides: ImageGuide[];
- meta: {
- charCount: number;
- sectionCount: number;
- estimatedReadTime: string;
- generatedAt: string;
- model: string;
- };
- };
-}
-
-export interface BlogSection {
- heading: string;
- body: string;
- imageSlot?: boolean;
-}
-
-export interface ImageGuide {
- position: string;
- description: string;
- searchKeyword: string;
- altText: string;
-}
diff --git a/next.config.ts b/next.config.ts
index 39b1398..04c2699 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -39,7 +39,9 @@ const nextConfig: NextConfig = {
{ source: '/freelance', destination: '/work/freelance', permanent: true },
{ source: '/services/website', destination: '/work/website', permanent: true },
{ source: '/services/website/samples/:slug', destination: '/work/website/samples/:slug', permanent: true },
- { source: '/services/blog', destination: '/work/blog', permanent: true },
+ // 블로그 자동화 폐기(2026-05-29 재정의): 기존 URL은 제품 라인 허브로 안내
+ { source: '/services/blog', destination: '/work', permanent: true },
+ { source: '/work/blog', destination: '/work', permanent: true },
// 사주 마이그 (단순 URL, 카탈로그 spec은 보류)
{ source: '/saju', destination: '/work/saju', permanent: true },
{ source: '/saju/input', destination: '/work/saju/input', permanent: true },