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 클릭
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"
}