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

영업 성과 개인별 순위 리포트

  • ▸HTTP Request 또는 Webhook으로 JSON 데이터를 수신할 수 있다
  • ▸Code 노드로 JSON을 평탄화(flatten)해 표 형태로 변환할 수 있다
  • ▸변환된 데이터를 Google Sheets 또는 Excel 파일로 저장할 수 있다

✅사전 준비

  • □변환할 JSON 데이터 구조 파악 (중첩 여부 확인)
  • □Google Sheets 자격증명 연결 완료
  • □출력 컬럼 구조 확정

중첩 JSON 평탄화 — Code 노드

function flatten(obj, prefix = '') {
  return Object.entries(obj).reduce((acc, [key, val]) => {
    const newKey = prefix ? `${prefix}_${key}` : key;
    if (val && typeof val === 'object' && !Array.isArray(val)) {
      Object.assign(acc, flatten(val, newKey));
    } else if (Array.isArray(val)) {
      acc[newKey] = val.join(', ');
    } else {
      acc[newKey] = val;
    }
    return acc;
  }, {});
}

const items = $input.all();
return items.map(({ json }) => ({ json: flatten(json) }));

Spreadsheet File 노드 — Excel 저장 설정

Operation:    Write
File Format:  XLSX
File Name:    export_{{ $now.toFormat('yyyyMMdd_HHmm') }}.xlsx
Sheet Name:   데이터
Header Row:   true
💡 TIPJSON 배열이 루트에 있는 경우 n8n이 자동으로 각 항목을 개별 아이템으로 펼쳐줍니다. 배열이 중첩된 경우엔 Code 노드에서 먼저 items.flatMap() 으로 풀어낸 뒤 평탄화하세요.
⚡

워크플로우 JSON

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

📥 n8n 임포트 방법
n8n 화면 우측 상단 메뉴 (⋮) → Import from JSON → 아래 JSON 전체 복사 후 붙여넣기 → Import 클릭
{
  "recipeId": "recipe037",
  "title": "영업 성과 개인별 순위 리포트",
  "chapter": 3,
  "category": "영업 & 리드 관리 자동화",
  "difficulty": "⭐⭐",
  "estimatedTime": "35분",
  "description": "매주 금요일 오후 5시 HubSpot에서 이번 주 완료된 딜을 담당자별로 집계하고 실적 순서로 정렬하여 Slack 팀 채널에 순위표를 발송합니다.",
  "apps": [
    "HubSpot",
    "Slack"
  ],
  "nodes": [
    {
      "id": "trigger_schedule",
      "type": "trigger",
      "name": "Schedule Trigger - 매주 금요일 오후 5시",
      "service": "scheduleTrigger",
      "config": {
        "rule": "0 17 * * 5"
      }
    },
    {
      "id": "hubspot_deals",
      "type": "action",
      "name": "HubSpot - 이번 주 완료 딜 조회",
      "service": "hubspot",
      "config": {
        "operation": "getDeals",
        "filters": [
          {
            "propertyName": "dealstage",
            "operator": "EQ",
            "value": "closedwon"
          },
          {
            "propertyName": "closedate",
            "operator": "THIS_WEEK"
          }
        ],
        "properties": [
          "dealname",
          "amount",
          "hubspot_owner_id",
          "closedate"
        ]
      }
    },
    {
      "id": "function_rank",
      "type": "function",
      "name": "Function - 담당자별 집계 및 정렬",
      "code": "const deals = $input.all();\nconst ownerMap = {};\ndeals.forEach(d => {\n  const owner = d.json.hubspot_owner_id || '미배정';\n  if (!ownerMap[owner]) ownerMap[owner] = { count: 0, total: 0 };\n  ownerMap[owner].count++;\n  ownerMap[owner].total += Number(d.json.amount || 0);\n});\nconst sorted = Object.entries(ownerMap)\n  .map(([owner, v]) => ({ owner, ...v }))\n  .sort((a, b) => b.total - a.total);\nreturn sorted.map((item, i) => ({ json: { rank: i + 1, ...item } }));"
    },
    {
      "id": "slack_ranking",
      "type": "action",
      "name": "Slack - 순위 리포트 발송",
      "service": "slack",
      "config": {
        "channel": "={{ $env.SLACK_SALES_CHANNEL }}",
        "message": "🏆 이번 주 영업 실적 순위\n{{ $items().map((item, i) => `${i+1}위 ${item.json.owner} — ${item.json.count}건 / ₩${item.json.total.toLocaleString()}`).join('\\n') }}"
      }
    }
  ],
  "connections": [
    {
      "from": "trigger_schedule",
      "to": "hubspot_deals"
    },
    {
      "from": "hubspot_deals",
      "to": "function_rank"
    },
    {
      "from": "function_rank",
      "to": "slack_ranking"
    }
  ],
  "envVars": [
    {
      "key": "SLACK_SALES_CHANNEL",
      "description": "영업팀 Slack 채널 ID"
    }
  ],
  "qrCodeUrl": "https://www.koreaaitimes.com/books/n8n-vol2/recipe037"
}
← 이전 레시피☰ 목차다음 레시피 →