Skip to content

Latest commit

ย 

History

History
310 lines (242 loc) ยท 6.6 KB

File metadata and controls

310 lines (242 loc) ยท 6.6 KB

๐Ÿค– AI ์ž๋™ ๋‹ต๋ณ€ ์‹œ์Šคํ…œ

๐Ÿ“‹ ๋ชฉ์ฐจ


์‹œ์Šคํ…œ ๊ฐœ์š”

๐ŸŽฏ Q&A ์ž๋™ ๋‹ต๋ณ€ ๋ด‡

๋ชฉ์ : Q&A ๊ฒŒ์‹œ๊ธ€์— AI๊ฐ€ ์ž๋™์œผ๋กœ ๊ณ ํ’ˆ์งˆ ๋‹ต๋ณ€ ์ƒ์„ฑ

์ž‘๋™ ๋ฐฉ์‹:

  1. Q&A ์นดํ…Œ๊ณ ๋ฆฌ ๊ฒŒ์‹œ๊ธ€ ๊ฐ์ง€
  2. OpenRouter API๋กœ ๋‹ต๋ณ€ ์ƒ์„ฑ
  3. ๋งˆํฌ๋‹ค์šด โ†’ HTML ๋ณ€ํ™˜
  4. ๋Œ“๊ธ€๋กœ ์ž๋™ ๋“ฑ๋ก

๐Ÿ“Š ๊ธฐ์ˆ  ์ŠคํŽ™

๊ตฌ์„ฑ ๋‚ด์šฉ
AI ๋ชจ๋ธ GPT-4, Claude 3 (OpenRouter)
Vision ์ง€์› ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ฐ€๋Šฅ
์ตœ๋Œ€ ํ† ํฐ 8,000 ํ† ํฐ (์•ฝ 6,000~16,000์ž)
์‘๋‹ต ์‹œ๊ฐ„ ์ตœ๋Œ€ 3๋ถ„ (ํƒ€์ž„์•„์›ƒ)
์žฌ์‹œ๋„ 3ํšŒ (๋‹ค๋ฅธ ๋ชจ๋ธ๋กœ ํด๋ฐฑ)

๊ธฐ์ˆ  ๊ตฌํ˜„

๐Ÿ”ง AI ๋ชจ๋ธ ํ†ตํ•ฉ

// lib/ai/openrouter-client.ts
export const AI_MODELS = {
  VISION: 'openai/gpt-4-vision-preview',    // ์ด๋ฏธ์ง€ ๋ถ„์„
  PRIMARY: 'anthropic/claude-3-opus',       // ์ฃผ ๋ชจ๋ธ
  SECONDARY: 'openai/gpt-4-turbo',         // ๋ณด์กฐ ๋ชจ๋ธ
  DEFAULT: 'openai/gpt-3.5-turbo'          // ํด๋ฐฑ ๋ชจ๋ธ
}

๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ๋ถ„์„ ์ง€์›

// ๊ฒŒ์‹œ๊ธ€์— ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด Vision ๋ชจ๋ธ ์‚ฌ์šฉ
const imageUrls = extractImageUrls(post.content)
if (imageUrls.length > 0) {
  // GPT-4 Vision์œผ๋กœ ์ด๋ฏธ์ง€ ๋ถ„์„
  const completion = await callAIModel(
    AI_MODELS.VISION, 
    prompt, 
    imageUrls
  )
}

๐Ÿ“ ๋งˆํฌ๋‹ค์šด โ†’ HTML ๋ณ€ํ™˜

function markdownToHTML(markdown: string): string {
  // 1. ์ฝ”๋“œ ๋ธ”๋ก ๋ณดํ˜ธ
  html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
    return `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`
  })
  
  // 2. ํ…Œ์ด๋ธ” ๋ณ€ํ™˜
  // 3. ์ œ๋ชฉ ์ฒ˜๋ฆฌ (H1~H6)
  // 4. ๋ฆฌ์ŠคํŠธ (ul/ol)
  // 5. ๋งํฌ, ๊ตต์€ ๊ธ€์”จ, ๊ธฐ์šธ์ž„
  // 6. ๋ฌธ๋‹จ ์ฒ˜๋ฆฌ
  
  return html
}

ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง

๐ŸŽฏ ๋™์  ๋‹ต๋ณ€ ๊ธธ์ด

const prompt = `
IMPORTANT RULES:
- Answer MUST be in Korean language
- Adjust answer length based on question complexity:
  * Simple questions โ†’ 1-3 paragraphs
  * Technical questions โ†’ 5-8 paragraphs with code
  * Complex problems โ†’ Detailed multi-section answer
- Use markdown formatting
- Include code examples when relevant
- Be concise for simple, detailed for complex
`

๐ŸŒ ํ•œ๊ตญ์–ด ์ตœ์ ํ™”

ํŠน์ง•:

  • ๋ชจ๋“  ๋‹ต๋ณ€ ํ•œ๊ตญ์–ด๋กœ ์ƒ์„ฑ
  • ๊ธฐ์ˆ  ์šฉ์–ด๋Š” ์˜์–ด ๋ณ‘๊ธฐ
  • ํ•œ๊ตญ ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ ํ†ค ์œ ์ง€

๐Ÿ“Š ๋‹ต๋ณ€ ๊ตฌ์กฐํ™”

## ๋ฌธ์ œ ๋ถ„์„
์งˆ๋ฌธ์— ๋Œ€ํ•œ ํ•ต์‹ฌ ์ดํ•ด

## ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
๋‹จ๊ณ„๋ณ„ ์ ‘๊ทผ๋ฒ•

## ์ฝ”๋“œ ์˜ˆ์‹œ
```javascript
// ์‹ค์ œ ๋™์ž‘ํ•˜๋Š” ์ฝ”๋“œ

์ถ”๊ฐ€ ํŒ

  • ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐฉ๋ฒ•
  • ์ฃผ์˜์‚ฌํ•ญ

---

## ๋‹ต๋ณ€ ํ’ˆ์งˆ ์ตœ์ ํ™”

### โœ… ์นดํ…Œ๊ณ ๋ฆฌ ์ž๋™ ๊ฐ์ง€

```typescript
function isQACategory(category: MainCategory | null): boolean {
  const qaCategories = [
    'qa', 'qna', 'question', 
    'help', '์งˆ๋ฌธ๋‹ต๋ณ€', '๋ฌธ์˜'
  ]
  
  return qaCategories.some(qa => 
    category.slug.includes(qa) || 
    category.name.includes(qa)
  )
}

๐Ÿ”„ ๋‹ค์ค‘ ๋ชจ๋ธ ํด๋ฐฑ

// 3๋‹จ๊ณ„ ํด๋ฐฑ ์ „๋žต
while (retryCount <= maxRetries) {
  try {
    if (retryCount === 0) {
      // 1์ฐจ: ์ตœ๊ณ  ์„ฑ๋Šฅ ๋ชจ๋ธ
      completion = await callAIModel(AI_MODELS.PRIMARY)
    } else if (retryCount === 1) {
      // 2์ฐจ: ๋ณด์กฐ ๋ชจ๋ธ
      completion = await callAIModel(AI_MODELS.SECONDARY)
    } else {
      // 3์ฐจ: ๊ธฐ๋ณธ ๋ชจ๋ธ
      completion = await callAIModel(AI_MODELS.DEFAULT)
    }
  } catch (error) {
    retryCount++
    await new Promise(resolve => 
      setTimeout(resolve, retryCount * 1000)
    )
  }
}

๐Ÿ“ ๋‹ต๋ณ€ ๊ธธ์ด ์ œ์–ด

์งˆ๋ฌธ ์œ ํ˜• ๋‹ต๋ณ€ ๊ธธ์ด ์˜ˆ์‹œ
๊ฐ„๋‹จํ•œ ์งˆ๋ฌธ 1-3 ๋ฌธ๋‹จ "์ด๊ฒƒ์ด ๋ฌด์—‡์ธ๊ฐ€์š”?"
๊ธฐ์ˆ ์  ์งˆ๋ฌธ 5-8 ๋ฌธ๋‹จ + ์ฝ”๋“œ "์ด ์—๋Ÿฌ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๋‚˜์š”?"
๋ณต์žกํ•œ ๋ฌธ์ œ 10+ ๋ฌธ๋‹จ + ์„น์…˜ "์‹œ์Šคํ…œ ์„ค๊ณ„๋ฅผ ๋„์™€์ฃผ์„ธ์š”"

์„ฑ๋Šฅ ๋ฐ ์•ˆ์ •์„ฑ

โšก ์‘๋‹ต ์‹œ๊ฐ„ ์ตœ์ ํ™”

const AI_CONFIG = {
  MAX_TOKENS: 8000,           // ์ถฉ๋ถ„ํ•œ ๋‹ต๋ณ€ ๊ธธ์ด
  TIMEOUT_MS: 180000,         // 3๋ถ„ ํƒ€์ž„์•„์›ƒ
  API_WAIT_TIMEOUT_MS: 180000,
  BATCH_DELAY_MS: 2000,       // ๋ฐฐ์น˜ ๊ฐ„ ๋Œ€๊ธฐ
  MAX_BATCH_SIZE: 10          // ๋ฐฐ์น˜ ํฌ๊ธฐ
}

๐Ÿ›ก๏ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

// ์•ˆ์ •์ ์ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
try {
  const response = await generateAIResponse(post)
  if (!response) {
    // ํด๋ฐฑ ์ฒ˜๋ฆฌ
    return null
  }
} catch (error) {
  console.error('[AI Bot] ์˜ค๋ฅ˜:', error)
  // ์—๋Ÿฌ ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง
}

๐Ÿ“ˆ ์„ฑ๋Šฅ ์ง€ํ‘œ

์ง€ํ‘œ ์ˆ˜์น˜
ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ 15-30์ดˆ
์„ฑ๊ณต๋ฅ  95%+
๋‹ต๋ณ€ ํ’ˆ์งˆ ์ ์ˆ˜ 4.2/5.0
์ผ์ผ ์ฒ˜๋ฆฌ๋Ÿ‰ 500+ ์งˆ๋ฌธ

๐Ÿ” ๋ณด์•ˆ ๋ฐ ์ œํ•œ

์ค‘๋ณต ๋ฐฉ์ง€

// ์ด๋ฏธ AI ๋Œ“๊ธ€์ด ์žˆ๋Š”์ง€ ํ™•์ธ
const existingAIComment = await prisma.mainComment.findFirst({
  where: {
    postId,
    authorId: AI_CONFIG.BOT_USER_ID
  }
})

if (existingAIComment) {
  return // ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€
}

๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ

// Redis ์บ์‹œ ๋ฌดํšจํ™”
await redisCache.del(
  generateCacheKey('main:post:comments', { postId })
)

// ๋Œ“๊ธ€ ์ˆ˜ ์—…๋ฐ์ดํŠธ
await prisma.mainPost.update({
  where: { id: postId },
  data: { commentCount: { increment: 1 }}
})

๐Ÿš€ ์‚ฌ์šฉ ์˜ˆ์‹œ

์‹ค์ œ ์ž‘๋™ ํ”Œ๋กœ์šฐ

graph LR
    A[Q&A ๊ฒŒ์‹œ๊ธ€] --> B{์นดํ…Œ๊ณ ๋ฆฌ ํ™•์ธ}
    B -->|Q&A| C[์ด๋ฏธ์ง€ ํ™•์ธ]
    B -->|์ผ๋ฐ˜| X[์ข…๋ฃŒ]
    C -->|์žˆ์Œ| D[Vision ๋ชจ๋ธ]
    C -->|์—†์Œ| E[Text ๋ชจ๋ธ]
    D --> F[๋‹ต๋ณ€ ์ƒ์„ฑ]
    E --> F
    F --> G[๋งˆํฌ๋‹ค์šดโ†’HTML]
    G --> H[๋Œ“๊ธ€ ๋“ฑ๋ก]
Loading

API ์—”๋“œํฌ์ธํŠธ

// app/api/ai/qa-bot/route.ts
export async function POST(req: Request) {
  const { postId } = await req.json()
  
  // AI ๋Œ“๊ธ€ ์ƒ์„ฑ
  await createAIComment(postId)
  
  return NextResponse.json({ success: true })
}

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง

// ๋กœ๊น… ์‹œ์Šคํ…œ
console.error(`[AI Bot] ๊ฒŒ์‹œ๊ธ€ ํ™•์ธ - ${post.title}`)
console.error(`[AI Bot] AI ์‘๋‹ต ์ƒ์„ฑ - ${response.length}์ž`)
console.error(`[AI Bot] ๋Œ“๊ธ€ ์ƒ์„ฑ ์„ฑ๊ณต - ${comment.id}`)

๐Ÿ”ฎ ํ–ฅํ›„ ๊ฐœ์„  ๊ณ„ํš

  • ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต: ์‹ค์‹œ๊ฐ„ ํƒ€์ดํ•‘ ํšจ๊ณผ
  • ๋‹ค๊ตญ์–ด ์ง€์›: ์˜์–ด/์ผ๋ณธ์–ด ๋‹ต๋ณ€
  • ์ปจํ…์ŠคํŠธ ํ•™์Šต: ์ด์ „ ๋‹ต๋ณ€ ์ฐธ์กฐ
  • ํ‰๊ฐ€ ์‹œ์Šคํ…œ: ๋‹ต๋ณ€ ํ’ˆ์งˆ ํ”ผ๋“œ๋ฐฑ