8
n8n 한국어amn8n.com

실시간 항공편 티켓 추적기 – 문자와 이메일을 통해 즉시 할인 알림 발송

중급

이것은Miscellaneous분야의자동화 워크플로우로, 14개의 노드를 포함합니다.주로 If, Code, Cron, Gmail, Function 등의 노드를 사용하며. 실시간 항공편 티켓 추적기 (Aviation Stack API) – Gmail과 Telegram을 통해 알림 전송

사전 요구사항
  • Google 계정 및 Gmail API 인증 정보
  • Telegram Bot Token
  • 대상 API의 인증 정보가 필요할 수 있음
  • Google Sheets API 인증 정보

카테고리

워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "id": "4vQkJTdJPHnD0XUa",
  "meta": {
    "instanceId": "dd69efaf8212c74ad206700d104739d3329588a6f3f8381a46a481f34c9cc281",
    "templateCredsSetupCompleted": true
  },
  "name": "Live Flight Fare Tracker – Instant Price Drop Alerts via SMS & Email",
  "tags": [],
  "nodes": [
    {
      "id": "aa5f054a-6239-46c1-8ab3-492796e1f1c1",
      "name": "스케줄 트리거",
      "type": "n8n-nodes-base.cron",
      "position": [
        -1440,
        380
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "mode": "everyX",
              "unit": "minutes",
              "value": 15
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "8d308fdf-5c40-46ab-b695-a7c95710ada2",
      "name": "비행 데이터 가져오기",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1220,
        380
      ],
      "parameters": {
        "url": "https://api.aviationstack.com/v1/flights",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "access_key",
              "value": "0987c6845c09876yt"
            },
            {
              "name": "dep_iata",
              "value": "JFK"
            },
            {
              "name": "arr_iata",
              "value": "LAX"
            },
            {
              "name": "limit",
              "value": "10"
            }
          ]
        }
      },
      "credentials": {
        "httpQueryAuth": {
          "id": "xA2e6hA40RZ8bzrI",
          "name": "Query Auth account - test"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "f4675a74-c27d-4b8b-bb8e-624687125b4a",
      "name": "비행 데이터 처리",
      "type": "n8n-nodes-base.function",
      "position": [
        -1000,
        380
      ],
      "parameters": {
        "functionCode": "// Process flight data and check for fare changes\nconst currentFlights = items[0].json.data || [];\nconst processedFlights = [];\n\nfor (const flight of currentFlights) {\n  if (flight.flight_status === 'scheduled' && flight.departure) {\n    // Extract fare information (you may need to adapt based on API response)\n    const flightInfo = {\n      flight_number: flight.flight.iata,\n      airline: flight.airline.name,\n      departure: flight.departure.airport,\n      arrival: flight.arrival.airport,\n      departure_time: flight.departure.scheduled,\n      arrival_time: flight.arrival.scheduled,\n      // Mock fare data - replace with actual fare from your chosen API\n      current_fare: Math.floor(Math.random() * 500) + 200,\n      route: `${flight.departure.iata}-${flight.arrival.iata}`,\n      timestamp: new Date().toISOString()\n    };\n    \n    processedFlights.push(flightInfo);\n  }\n}\n\nreturn processedFlights.map(flight => ({ json: flight }));"
      },
      "executeOnce": true,
      "typeVersion": 1
    },
    {
      "id": "87b6bde3-fe0a-47a2-9410-ddb8d78cbf77",
      "name": "알림 필요 여부 확인",
      "type": "n8n-nodes-base.if",
      "position": [
        -340,
        380
      ],
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.fare_change }}",
              "operation": "notEqual"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "11309f53-cdc0-4cf9-b060-d55ca3e9ca74",
      "name": "알림 메시지 구성",
      "type": "n8n-nodes-base.function",
      "position": [
        -80,
        340
      ],
      "parameters": {
        "functionCode": "// Format alert message\nconst flight = items[0].json;\nconst alertType = flight.alert_type;\nconst emoji = alertType === 'PRICE_DROP' ? '📉' : '📈';\nconst alertColor = alertType === 'PRICE_DROP' ? 'good' : 'warning';\n\nconst message = {\n  email: {\n    subject: `${emoji} Flight Fare Alert: ${flight.flight_number}`,\n    html: `\n      <h2>${emoji} Fare ${alertType.replace('_', ' ')} Alert</h2>\n      <p><strong>Flight:</strong> ${flight.flight_number} (${flight.airline})</p>\n      <p><strong>Route:</strong> ${flight.departure} → ${flight.arrival}</p>\n      <p><strong>Departure:</strong> ${new Date(flight.departure_time).toLocaleString()}</p>\n      <p><strong>Previous Fare:</strong> $${flight.previous_fare}</p>\n      <p><strong>Current Fare:</strong> $${flight.current_fare}</p>\n      <p><strong>Change:</strong> $${flight.fare_change} (${flight.percentage_change}%)</p>\n      <p style=\"color: ${alertType === 'PRICE_DROP' ? 'green' : 'red'};\"><strong>Recommendation:</strong> ${alertType === 'PRICE_DROP' ? 'Consider booking now!' : 'Price increased - monitor for drops'}</p>\n    `\n  },\n  slack: {\n    text: `${emoji} Flight Fare Alert`,\n    attachments: [\n      {\n        color: alertColor,\n        fields: [\n          {\n            title: \"Flight\",\n            value: `${flight.flight_number} (${flight.airline})`,\n            short: true\n          },\n          {\n            title: \"Route\",\n            value: `${flight.departure} → ${flight.arrival}`,\n            short: true\n          },\n          {\n            title: \"Previous Fare\",\n            value: `$${flight.previous_fare}`,\n            short: true\n          },\n          {\n            title: \"Current Fare\",\n            value: `$${flight.current_fare}`,\n            short: true\n          },\n          {\n            title: \"Change\",\n            value: `$${flight.fare_change} (${flight.percentage_change}%)`,\n            short: false\n          }\n        ]\n      }\n    ]\n  },\n  sms: `${emoji} FARE ALERT: ${flight.flight_number} ${flight.departure}-${flight.arrival} fare changed from $${flight.previous_fare} to $${flight.current_fare} (${flight.percentage_change}%)`\n};\n\nreturn [{ json: { ...flight, formatted_messages: message } }];"
      },
      "typeVersion": 1
    },
    {
      "id": "026afcab-023a-4ef4-9573-28ba32edc01c",
      "name": "알림 활동 기록",
      "type": "n8n-nodes-base.function",
      "position": [
        460,
        400
      ],
      "parameters": {
        "functionCode": "// Log alert activity\nconst alert = items[0].json;\n\nconst logEntry = {\n  timestamp: new Date().toISOString(),\n  flight_number: alert.flight_number,\n  route: alert.route,\n  alert_type: alert.alert_type,\n  fare_change: alert.fare_change,\n  percentage_change: alert.percentage_change,\n  notification_sent: true\n};\n\nconsole.log('Fare Alert Sent:', logEntry);\n\nreturn [{ json: logEntry }];"
      },
      "typeVersion": 1
    },
    {
      "id": "374b1e8e-15b3-4109-9c46-01406c0e4ca5",
      "name": "코드",
      "type": "n8n-nodes-base.code",
      "position": [
        -580,
        380
      ],
      "parameters": {
        "jsCode": "// Get current flight data from previous node (Process Flight Data)\nconst currentFlightsItems = $('Process Flight Data').all();\n\n// Get stored fare data from Google Sheets node\nconst sheetsData = $('Previous flight data').all();\n\nconsole.log('Current flights items:', currentFlightsItems.length);\nconsole.log('Sheets data items:', sheetsData.length);\n\n// Build lookup object from Google Sheets data (remove duplicates)\nconst storedFares = {};\nconst uniqueSheetRows = new Map();\n\n// Remove duplicates from sheets data first\nfor (const row of sheetsData) {\n  const rowData = row.json;\n  if (rowData.flight_number && rowData.route) {\n    const routeKey = `${rowData.flight_number}_${rowData.route}`;\n    \n    // Keep only the first occurrence of each flight\n    if (!uniqueSheetRows.has(routeKey)) {\n      uniqueSheetRows.set(routeKey, rowData);\n      storedFares[routeKey] = parseFloat(rowData.current_fare || 0);\n    }\n  }\n}\n\nconsole.log('Stored fares lookup:', Object.keys(storedFares));\n\nconst alertFlights = [];\nconst fareUpdates = [];\n\n// Process each current flight item\nfor (const flightItem of currentFlightsItems) {\n  const flightData = flightItem.json;\n  \n  if (!flightData.flight_number || !flightData.route) {\n    console.log('Skipping invalid flight data:', flightData);\n    continue;\n  }\n  \n  const routeKey = `${flightData.flight_number}_${flightData.route}`;\n  const currentFare = parseFloat(flightData.current_fare || 0);\n  const previousFare = storedFares[routeKey];\n  \n  console.log(`Processing ${routeKey}: Current=${currentFare}, Previous=${previousFare}`);\n  \n  if (previousFare && previousFare !== currentFare && currentFare > 0) {\n    const fareChange = currentFare - previousFare;\n    const percentageChange = (fareChange / previousFare) * 100;\n    \n    console.log(`${routeKey}: Change=${fareChange}, Percentage=${percentageChange.toFixed(2)}%`);\n    \n    // Alert if fare decreased by 10% or more, or increased by 15% or more\n    if (percentageChange <= -10 || percentageChange >= 15) {\n      alertFlights.push({\n        ...flightData,\n        previous_fare: previousFare,\n        fare_change: fareChange,\n        percentage_change: Math.round(percentageChange * 100) / 100,\n        alert_type: percentageChange < 0 ? 'PRICE_DROP' : 'PRICE_INCREASE',\n        alert_message: percentageChange < 0 \n          ? `🔥 PRICE DROP: ${Math.abs(percentageChange).toFixed(1)}% decrease!` \n          : `⚠️ PRICE INCREASE: ${percentageChange.toFixed(1)}% increase!`\n      });\n      \n      console.log(`ALERT TRIGGERED for ${routeKey}: ${percentageChange < 0 ? 'DROP' : 'INCREASE'} of ${Math.abs(percentageChange).toFixed(2)}%`);\n    }\n  } else if (!previousFare) {\n    console.log(`New flight detected: ${routeKey}`);\n  } else if (currentFare <= 0) {\n    console.log(`Invalid current fare for ${routeKey}: ${currentFare}`);\n  }\n  \n  // Prepare fare updates for Google Sheets (to update stored fares)\n  if (currentFare > 0) {\n    fareUpdates.push({\n      row_number: uniqueSheetRows.get(routeKey)?.row_number || null,\n      flight_number: flightData.flight_number,\n      airline: flightData.airline || 'Unknown',\n      departure: flightData.departure || 'Unknown',\n      arrival: flightData.arrival || 'Unknown',\n      departure_time: flightData.departure_time || '',\n      arrival_time: flightData.arrival_time || '',\n      current_fare: currentFare,\n      route: flightData.route,\n      timestamp: flightData.timestamp || new Date().toISOString(),\n      last_updated: new Date().toISOString()\n    });\n  }\n}\n\nconsole.log(`Total alerts generated: ${alertFlights.length}`);\nif (alertFlights.length > 0) {\n  console.log(`Alerts for flights:`, alertFlights.map(f => f.flight_number));\n}\n\n// Store fare updates in node context for the Google Sheets update node\n$node[\"Code\"].context = { fareUpdates };\n\n// If no alerts, return empty array but still process\nif (alertFlights.length === 0) {\n  console.log('No fare alerts triggered');\n  return [];\n}\n\n// Return alert flights for notification processing\nreturn alertFlights.map(flight => ({ json: flight }));\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "43084297-fd73-45ae-83b8-3e31db490777",
      "name": "Telegram",
      "type": "n8n-nodes-base.telegram",
      "onError": "continueRegularOutput",
      "position": [
        180,
        480
      ],
      "webhookId": "30b417c4-a6ea-42ec-8218-c40248df36b8",
      "parameters": {
        "text": "={{ $json.formatted_messages.sms }}",
        "chatId": "123SSHSJNASB",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "id": "3ubbGgZx2YzylQZu",
          "name": "Telegram account - test"
        }
      },
      "executeOnce": true,
      "retryOnFail": false,
      "typeVersion": 1.2
    },
    {
      "id": "b2048186-6d7a-4b76-9849-bc694592ce39",
      "name": "워크플로우 개요",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1440,
        60
      ],
      "parameters": {
        "width": 700,
        "height": 200,
        "content": "## ✈️ Flight Fare Tracker & Alert System\n\nThis workflow is designed to monitor flight prices and send alerts when significant fare changes occur (drops or increases). It automates the process of fetching flight data, comparing current fares against historical records, and notifying users via email or Telegram if an alert condition is met. 📉📈"
      },
      "typeVersion": 1
    },
    {
      "id": "06f7bb02-8151-43f9-b5c2-9a75555d0199",
      "name": "데이터 수집 및 처리",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1400,
        560
      ],
      "parameters": {
        "color": 4,
        "width": 600,
        "height": 300,
        "content": "### 📊 Data Ingestion & Processing\n\n1.  **Schedule Trigger**: Starts the workflow at regular intervals.\n2.  **Fetch Flight Data**: Retrieves flight information from the AviationStack API, filtering by JFK to LAX route.\n3.  **Process Flight Data**: A Function node that extracts and formats relevant flight details (flight number, airline, departure/arrival times, and a *mock* current fare). This prepares data for comparison.\n4.  **Google Sheets**: Reads existing flight fare data from a Google Sheet, which acts as a historical record."
      },
      "typeVersion": 1
    },
    {
      "id": "5b431dd1-1832-4a03-9df0-880dc17d1924",
      "name": "운임 비교 및 알림 로직",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -700,
        20
      ],
      "parameters": {
        "color": 3,
        "width": 600,
        "height": 280,
        "content": "### 🔍 Fare Comparison & Alert Logic\n\n1.  **Code**: This critical node compares the `current_fare` from the \"Process Flight Data\" node with the `previous_fare` fetched from \"Google Sheets\". It calculates fare changes (absolute and percentage) and determines if an alert is needed based on predefined thresholds (e.g., >=10% drop or >=15% increase).\n2.  **Check if Alert Needed**: An If node that checks if the `fare_change` calculated in the Code node is non-zero. If there's a change, it proceeds to format and send alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "a29a641a-f204-463f-8764-91bfaf753f2d",
      "name": "알림 및 기록",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "color": 5,
        "width": 600,
        "height": 280,
        "content": "### 🔔 Notification & Logging\n\n1.  **Format Alert Message**: Prepares the alert message in different formats (email HTML, SMS) using a Function node, based on the `alert_type` (price drop/increase).\n2.  **Gmail**: Sends an email notification with the formatted alert.\n3.  **Telegram**: Sends a Telegram message with the formatted alert.\n4.  **Log Alert Activity**: A final Function node that logs details about the sent alert, useful for auditing or debugging."
      },
      "typeVersion": 1
    },
    {
      "id": "cf96cc07-5679-4ba4-86f7-5a2cfec7dae3",
      "name": "이전 비행 데이터",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -780,
        380
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q/edit?usp=drivesdk",
          "cachedResultName": "fare details change logs"
        },
        "authentication": "serviceAccount"
      },
      "credentials": {
        "googleApi": {
          "id": "ScSS2KxGQULuPtdy",
          "name": "Google Sheets- test"
        }
      },
      "executeOnce": false,
      "typeVersion": 4.6
    },
    {
      "id": "ba34df18-2c7c-46ee-9fc4-acfacb7b8fb1",
      "name": "메시지 보내기",
      "type": "n8n-nodes-base.gmail",
      "position": [
        180,
        300
      ],
      "webhookId": "2402da10-b757-4220-8399-d9dee79a3a51",
      "parameters": {
        "sendTo": "abc@gmail.com",
        "message": "={{ $json.formatted_messages.email.html }}",
        "options": {},
        "subject": "={{ $json.formatted_messages.email.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "PcTqvGU9uCunfltE",
          "name": "Gmail account - test"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "93eb556a-c580-4639-9d7d-0b0baaa6cda4",
  "connections": {
    "374b1e8e-15b3-4109-9c46-01406c0e4ca5": {
      "main": [
        [
          {
            "node": "87b6bde3-fe0a-47a2-9410-ddb8d78cbf77",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "43084297-fd73-45ae-83b8-3e31db490777": {
      "main": [
        [
          {
            "node": "026afcab-023a-4ef4-9573-28ba32edc01c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ba34df18-2c7c-46ee-9fc4-acfacb7b8fb1": {
      "main": [
        [
          {
            "node": "026afcab-023a-4ef4-9573-28ba32edc01c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "aa5f054a-6239-46c1-8ab3-492796e1f1c1": {
      "main": [
        [
          {
            "node": "8d308fdf-5c40-46ab-b695-a7c95710ada2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8d308fdf-5c40-46ab-b695-a7c95710ada2": {
      "main": [
        [
          {
            "node": "f4675a74-c27d-4b8b-bb8e-624687125b4a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f4675a74-c27d-4b8b-bb8e-624687125b4a": {
      "main": [
        [
          {
            "node": "cf96cc07-5679-4ba4-86f7-5a2cfec7dae3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "11309f53-cdc0-4cf9-b060-d55ca3e9ca74": {
      "main": [
        [
          {
            "node": "ba34df18-2c7c-46ee-9fc4-acfacb7b8fb1",
            "type": "main",
            "index": 0
          },
          {
            "node": "43084297-fd73-45ae-83b8-3e31db490777",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cf96cc07-5679-4ba4-86f7-5a2cfec7dae3": {
      "main": [
        [
          {
            "node": "374b1e8e-15b3-4109-9c46-01406c0e4ca5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "87b6bde3-fe0a-47a2-9410-ddb8d78cbf77": {
      "main": [
        [
          {
            "node": "11309f53-cdc0-4cf9-b060-d55ca3e9ca74",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.

이 워크플로우는 어떤 시나리오에 적합한가요?

중급 - 기타

유료인가요?

이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.

워크플로우 정보
난이도
중급
노드 수14
카테고리1
노드 유형9
난이도 설명

일정 경험을 가진 사용자를 위한 6-15개 노드의 중간 복잡도 워크플로우

저자
Oneclick AI Squad

Oneclick AI Squad

@oneclick-ai

The AI Squad Initiative is a pioneering effort to build, automate and scale AI-powered workflows using n8n.io. Our mission is to help individuals and businesses integrate AI agents seamlessly into their daily operations from automating tasks and enhancing productivity to creating innovative, intelligent solutions. We design modular, reusable AI workflow templates that empower creators, developers and teams to supercharge their automation with minimal effort and maximum impact.

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34