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

월간 영업 목표 달성률 자동 계산 & 시각화

  • ▸데이터 처리 중 발생하는 에러를 유형별로 분류해 처리할 수 있다
  • ▸에러 발생 시 자동으로 재시도(retry) 로직을 구현할 수 있다
  • ▸처리 실패 데이터를 별도 저장해 나중에 수동 검토할 수 있다

✅사전 준비

  • □에러 핸들링을 적용할 워크플로우 선정
  • □Google Sheets에 에러 로그 저장용 시트 생성
  • □Slack 자격증명 연결 완료 (심각 에러 알림용)
  • □n8n Error Trigger 워크플로우 별도 생성

에러 유형 분류 — Code 노드

const error = $input.first().json.error ?? {};
const message = error.message ?? '';

let type = 'UNKNOWN';
if (message.includes('rate limit') || message.includes('429')) type = 'RATE_LIMIT';
else if (message.includes('timeout') || message.includes('ETIMEDOUT')) type = 'TIMEOUT';
else if (message.includes('401') || message.includes('403')) type = 'AUTH_ERROR';
else if (message.includes('404')) type = 'NOT_FOUND';
else if (message.includes('500') || message.includes('502')) type = 'SERVER_ERROR';

const retryable = ['RATE_LIMIT', 'TIMEOUT', 'SERVER_ERROR'].includes(type);

return [{ json: { errorType: type, retryable, message, originalData: $input.first().json } }];

재시도 대기 시간 — Wait 노드 설정

RATE_LIMIT 에러 → Wait 60초 후 재시도
TIMEOUT 에러   → Wait 30초 후 재시도
SERVER_ERROR   → Wait 120초 후 재시도

Wait 노드:
  Amount: {{ $json.errorType === 'RATE_LIMIT' ? 60 : $json.errorType === 'SERVER_ERROR' ? 120 : 30 }}
  Unit:   Seconds

에러 로그 Sheets 기록

에러유형:    {{ $json.errorType }}
메시지:      {{ $json.message }}
재시도가능:  {{ $json.retryable ? 'Y' : 'N' }}
발생시각:    {{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}
원본데이터:  {{ JSON.stringify($json.originalData).slice(0, 500) }}
⚠️ 주의재시도 횟수를 무제한으로 설정하면 무한 루프가 발생할 수 있습니다. Sheets에 재시도 횟수를 기록하고 3회 초과 시 에러로 마킹 후 중단하는 로직을 반드시 추가하세요.
⚡

워크플로우 JSON

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

📥 n8n 임포트 방법
n8n 화면 우측 상단 메뉴 (⋮) → Import from JSON → 아래 JSON 전체 복사 후 붙여넣기 → Import 클릭
{
  "recipeId": "recipe040",
  "title": "월간 영업 목표 달성률 자동 계산 & 시각화",
  "chapter": 3,
  "category": "영업 & 리드 관리 자동화",
  "difficulty": "⭐⭐⭐",
  "estimatedTime": "45분",
  "description": "매일 오후 6시 HubSpot에서 이번 달 완료 딜 합계를 가져와 구글 시트에서 목표 금액과 비교하고, 달성률과 이모지를 포함한 현황 리포트를 Slack에 발송합니다.",
  "apps": [
    "HubSpot",
    "Google Sheets",
    "Slack"
  ],
  "nodes": [
    {
      "id": "trigger_schedule",
      "type": "trigger",
      "name": "Schedule Trigger - 매일 오후 6시",
      "service": "scheduleTrigger",
      "config": {
        "rule": "0 18 * * *"
      }
    },
    {
      "id": "sheets_goal",
      "type": "action",
      "name": "Google Sheets - 이번 달 목표 금액 읽기",
      "service": "googleSheets",
      "config": {
        "operation": "readRow",
        "documentId": "={{ $env.SALES_GOAL_SHEET_ID }}",
        "sheetName": "월별목표",
        "filter": {
          "month": "={{ $now.format(\"YYYY-MM\") }}"
        }
      }
    },
    {
      "id": "hubspot_actual",
      "type": "action",
      "name": "HubSpot - 이번 달 실적 합계 조회",
      "service": "hubspot",
      "config": {
        "operation": "getDeals",
        "filters": [
          {
            "propertyName": "dealstage",
            "operator": "EQ",
            "value": "closedwon"
          },
          {
            "propertyName": "closedate",
            "operator": "THIS_MONTH"
          }
        ],
        "properties": [
          "amount"
        ]
      }
    },
    {
      "id": "function_calc",
      "type": "function",
      "name": "Function - 달성률 계산",
      "code": "const goal = $node['sheets_goal'].json['목표금액'];\nconst actual = $input.all().reduce((sum, d) => sum + Number(d.json.amount || 0), 0);\nconst rate = ((actual / goal) * 100).toFixed(1);\nconst today = $now.format('M월 D일');\nconst remaining = $now.daysInMonth - $now.day;\nconst dailyNeeded = ((goal - actual) / remaining).toFixed(0);\nlet emoji = '🟡';\nif (rate >= 100) emoji = '🟢';\nelse if (rate < 50) emoji = '🔴';\nreturn [{ json: { goal, actual, rate, today, remaining, dailyNeeded, emoji } }];"
    },
    {
      "id": "slack_report",
      "type": "action",
      "name": "Slack - 달성률 리포트 발송",
      "service": "slack",
      "config": {
        "channel": "={{ $env.SLACK_SALES_CHANNEL }}",
        "message": "📈 오늘의 영업 달성률 ({{ $json[\"today\"] }} 기준)\n이번 달 목표: ₩{{ $json[\"goal\"].toLocaleString() }}\n현재 실적: ₩{{ $json[\"actual\"].toLocaleString() }}\n달성률: {{ $json[\"rate\"] }}% {{ $json[\"emoji\"] }}\n남은 영업일: {{ $json[\"remaining\"] }}일\n일평균 필요 실적: ₩{{ Number($json[\"dailyNeeded\"]).toLocaleString() }}"
      }
    }
  ],
  "connections": [
    {
      "from": "trigger_schedule",
      "to": "sheets_goal"
    },
    {
      "from": "sheets_goal",
      "to": "hubspot_actual"
    },
    {
      "from": "hubspot_actual",
      "to": "function_calc"
    },
    {
      "from": "function_calc",
      "to": "slack_report"
    }
  ],
  "envVars": [
    {
      "key": "SALES_GOAL_SHEET_ID",
      "description": "월별 영업 목표 구글 시트 ID"
    },
    {
      "key": "SLACK_SALES_CHANNEL",
      "description": "영업팀 Slack 채널 ID"
    }
  ],
  "qrCodeUrl": "https://www.koreaaitimes.com/books/n8n-vol2/recipe040"
}
← 이전 레시피☰ 목차다음 레시피 →