발행일: 2026-10-06(화) 11:44
← 목차로 돌아가기|하루 30분 n8n 시리즈 ② 실무 자동화 레시피 100
RECIPE 049

미답변 채팅 자동 배정

  • ▸GitHub Webhook으로 PR(Pull Request) 생성 이벤트를 감지할 수 있다
  • ▸변경된 코드를 OpenAI로 자동 리뷰해 피드백을 생성할 수 있다
  • ▸리뷰 결과를 GitHub PR 코멘트로 자동 등록할 수 있다

✅사전 준비

  • □GitHub Personal Access Token 발급 (repo, pull_requests 권한)
  • □n8n Webhook URL을 GitHub 저장소 Webhook에 등록 (이벤트: pull_request)
  • □OpenAI API 자격증명 연결 완료
  • □리뷰 기준 정의 (보안 취약점 / 코드 스타일 / 성능 / 테스트 여부)

PR diff 수집 — HTTP Request

URL: https://api.github.com/repos/{{ $json.repository.full_name }}/pulls/{{ $json.number }}/files
Method: GET
Headers:
  Authorization: Bearer YOUR_GITHUB_TOKEN
  Accept: application/vnd.github.v3+json

변경 코드 추출 — Code 노드

const files = $input.all().map(i => i.json);
const diff = files
  .slice(0, 5) // 최대 5개 파일만 (토큰 절약)
  .map(f => `### ${f.filename}\n${f.patch?.slice(0, 1000) ?? '(변경 없음)'}`)
  .join('\n\n');

return [{ json: { diff, fileCount: files.length, prNumber: $('Webhook').first().json.number } }];

코드 리뷰 — OpenAI 프롬프트

Model: gpt-4o

System:
너는 시니어 개발자야. PR 코드를 리뷰하고 아래 항목을 체크해줘:
1. 보안 취약점
2. 성능 문제
3. 코드 가독성
4. 잠재적 버그

User:
다음 코드 변경사항을 리뷰해줘.
구체적인 개선 제안을 포함해서 마크다운 형식으로 작성해.

{{ $json.diff }}

GitHub PR 코멘트 등록 — HTTP Request

URL: https://api.github.com/repos/{{ $('Webhook').first().json.repository.full_name }}/issues/{{ $json.prNumber }}/comments
Method: POST
Headers:
  Authorization: Bearer YOUR_GITHUB_TOKEN
Body (JSON):
  { "body": "## 🤖 AI 코드 리뷰\n\n{{ $json.reviewContent }}" }
⚠️ 주의AI 코드 리뷰는 참고용으로만 활용하고 최종 승인은 반드시 사람이 해야 합니다. 보안에 민감한 코드는 외부 AI API에 전달하지 않도록 주의하세요.
⚡

워크플로우 JSON

아래 JSON을 복사해 n8n에 바로 임포트하세요

📥 n8n 임포트 방법
n8n 화면 우측 상단 메뉴 (⋮) → Import from JSON → 아래 JSON 전체 복사 후 붙여넣기 → Import 클릭
{
  "recipeId": "recipe049",
  "title": "미답변 채팅 자동 배정",
  "chapter": 4,
  "category": "고객 응대 자동화",
  "difficulty": "⭐⭐",
  "estimatedTime": "30분",
  "usedApps": [
    "채널톡 API",
    "Slack"
  ],
  "description": "30분마다 채널톡에서 30분 이상 미답변 채팅을 조회하여, 현재 대기 중인 상담원 중 응대 건수가 가장 적은 담당자를 자동 배정하고 해당 담당자 Slack DM으로 알림을 보냅니다.",
  "nodes": [
    {
      "id": "trigger_schedule",
      "type": "trigger",
      "service": "Schedule",
      "name": "30분마다 실행",
      "config": {
        "interval": "every 30 minutes"
      }
    },
    {
      "id": "get_unanswered",
      "type": "action",
      "service": "HTTP Request",
      "name": "미답변 채팅 조회",
      "config": {
        "method": "GET",
        "url": "https://api.channel.io/open/v5/conversations",
        "headers": {
          "x-access-key": "{{$env.CHANNELTALK_ACCESS_KEY}}",
          "x-access-secret": "{{$env.CHANNELTALK_ACCESS_SECRET}}"
        },
        "queryParams": {
          "state": "open",
          "assigneeType": "unassigned",
          "waitingTimeGte": 30
        }
      }
    },
    {
      "id": "check_exists",
      "type": "condition",
      "service": "IF",
      "name": "미답변 채팅 존재 여부",
      "config": {
        "condition": "{{$json['conversations'] && $json['conversations'].length > 0}}"
      }
    },
    {
      "id": "get_agents",
      "type": "action",
      "service": "HTTP Request",
      "name": "상담원 현황 조회",
      "config": {
        "method": "GET",
        "url": "https://api.channel.io/open/v5/members",
        "headers": {
          "x-access-key": "{{$env.CHANNELTALK_ACCESS_KEY}}",
          "x-access-secret": "{{$env.CHANNELTALK_ACCESS_SECRET}}"
        },
        "queryParams": {
          "available": true
        }
      }
    },
    {
      "id": "find_least_busy",
      "type": "action",
      "service": "Function",
      "name": "가장 여유로운 상담원 선택",
      "config": {
        "code": "const agents = $node['get_agents'].json['members'];\nconst conversations = $node['get_unanswered'].json['conversations'];\n\n// 담당 건수 계산\nconst agentLoad = {};\nagents.forEach(a => agentLoad[a.id] = 0);\nconversations.forEach(c => {\n  if (c.assigneeId) agentLoad[c.assigneeId] = (agentLoad[c.assigneeId] || 0) + 1;\n});\n\n// 가장 적은 담당자 선택\nconst leastBusy = agents.reduce((min, a) => \n  (agentLoad[a.id] || 0) < (agentLoad[min.id] || 0) ? a : min\n);\n\nreturn conversations.map(conv => ({\n  json: {\n    conversationId: conv.id,\n    customerName: conv.customer?.name || '비회원',\n    waitingMinutes: conv.waitingMinutes,\n    assigneeId: leastBusy.id,\n    assigneeName: leastBusy.name,\n    assigneeSlackId: leastBusy.slackId\n  }\n}));"
      }
    },
    {
      "id": "assign_agent",
      "type": "action",
      "service": "HTTP Request",
      "name": "채널톡 담당자 배정",
      "config": {
        "method": "PUT",
        "url": "https://api.channel.io/open/v5/conversations/{{$json['conversationId']}}/assign",
        "headers": {
          "x-access-key": "{{$env.CHANNELTALK_ACCESS_KEY}}",
          "x-access-secret": "{{$env.CHANNELTALK_ACCESS_SECRET}}"
        },
        "body": {
          "assigneeId": "{{$json['assigneeId']}}"
        }
      }
    },
    {
      "id": "notify_agent",
      "type": "action",
      "service": "Slack",
      "name": "담당자 Slack 알림",
      "config": {
        "operation": "sendDM",
        "userId": "{{$json['assigneeSlackId']}}",
        "message": "💬 미답변 채팅이 배정되었습니다!\n\n👤 고객: {{$json['customerName']}}\n⏰ 대기 시간: {{$json['waitingMinutes']}}분\n\n👉 채널톡에서 바로 확인하세요!"
      }
    },
    {
      "id": "no_action",
      "type": "noOp",
      "name": "미답변 채팅 없음 — 스킵",
      "config": {}
    }
  ],
  "edges": [
    {
      "from": "trigger_schedule",
      "to": "get_unanswered"
    },
    {
      "from": "get_unanswered",
      "to": "check_exists"
    },
    {
      "from": "check_exists",
      "to": "get_agents",
      "branch": "true"
    },
    {
      "from": "check_exists",
      "to": "no_action",
      "branch": "false"
    },
    {
      "from": "get_agents",
      "to": "find_least_busy"
    },
    {
      "from": "find_least_busy",
      "to": "assign_agent"
    },
    {
      "from": "assign_agent",
      "to": "notify_agent"
    }
  ],
  "tips": [
    "상담원의 Slack ID는 채널톡 멤버 정보 커스텀 필드에 미리 저장해두면 자동 매핑이 편해요.",
    "대기 시간 기준(30분)은 팀 운영 방식에 따라 자유롭게 조정하세요. 바쁜 시간대는 15분으로 줄여볼 수 있어요.",
    "배정 후 고객에게도 '담당자가 배정되었습니다. 잠시만 기다려주세요.' 자동 메시지를 보내면 이탈률을 낮출 수 있어요."
  ],
  "qrUrl": "www.koreaaitimes.com/books/n8n-vol2/recipe049"
}
← 이전 레시피☰ 목차다음 레시피 →