8
n8n 한국어amn8n.com

Telegram과 Sheets 실시간 업데이트를 지원하는 지능형 POS 시스템

고급

이것은CRM, AI Summarization분야의자동화 워크플로우로, 16개의 노드를 포함합니다.주로 Code, Wait, Webhook, Telegram, GoogleSheets 등의 노드를 사용하며. Web POS 인터페이스, AI 보고서, Telegram 알림, Sheets를 사용하여 판매 주문을 만들기

사전 요구사항
  • HTTP Webhook 엔드포인트(n8n이 자동으로 생성)
  • Telegram Bot Token
  • Google Sheets API 인증 정보
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "id": "XRTJrZHlkDjGCLDq",
  "meta": {
    "instanceId": "0d045f8fe3802ff2be0bb9a9ea445ee6c9ed61973377effe00767e483681e2f4"
  },
  "name": "Smart POS System with Live Updates to Telegram & Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
      "name": "텍스트 메시지 전송",
      "type": "n8n-nodes-base.telegram",
      "position": [
        480,
        420
      ],
      "webhookId": "2ddba555-b1a7-4dca-b5d3-6ccb7866fad7",
      "parameters": {
        "text": "={{ $json.output }}",
        "chatId": "YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "AZVFe6SQjkKyufRE",
          "name": "Laporan Keuangan"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "75d42049-e27a-4208-82ef-4a121812563e",
      "name": "시트에 행 추가 또는 업데이트",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        380,
        40
      ],
      "parameters": {
        "columns": {
          "value": {
            "SALES ID": "={{ $json[\"SALES ID\"] }}",
            "SALES QTY": "={{ $json[\"SALES QTY\"] }}",
            "SALES DATE": "={{ $json[\"SALES DATE\"] }}",
            "SALES DISCOUNT": "={{ $json[\"SALES DISCOUNT\"] }}",
            "SALES PRICE (USD)": "={{ $json[\"SALES PRICE (USD)\"] }}",
            "SALES PRODUCT NAME": "={{ $json[\"SALES PRODUCT NAME\"] }}",
            "SALES CUSTOMER NAME": "={{ $json[\"SALES CUSTOMER NAME\"] }}"
          },
          "schema": [
            {
              "id": "SALES ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "SALES ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES DATE",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES DATE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES CUSTOMER NAME",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES CUSTOMER NAME",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES PRODUCT NAME",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES PRODUCT NAME",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES CATEGORY NAME",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "SALES CATEGORY NAME",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES PRICE (USD)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES PRICE (USD)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES QTY",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES QTY",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES DISCOUNT",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SALES DISCOUNT",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SALES TOTAL",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "SALES TOTAL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "SALES ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1157363351
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEETS_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "wPmZzacn7hIP4akd",
          "name": "Google Sheets account"
        }
      },
      "executeOnce": false,
      "typeVersion": 4.6
    },
    {
      "id": "e8b420ed-e5cb-4835-9f9b-979bb9e44e05",
      "name": "webhook 시작",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1180,
        220
      ],
      "webhookId": "47332c29-1dd6-4aa2-b59e-80b4b265e3f4",
      "parameters": {
        "path": "smartpostsystem",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 1
    },
    {
      "id": "84791b6d-a9b2-42b5-8c26-616c66adadd6",
      "name": "제품 데이터 가져오기",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -960,
        220
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEETS_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "wPmZzacn7hIP4akd",
          "name": "Google Sheets account"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.6
    },
    {
      "id": "dff306ac-b6ad-4814-9ed2-2ffed96b420a",
      "name": "Webhook에 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -460,
        220
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=UTF-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "=<!DOCTYPE html>\n<html>\n\n<head>\n    <title>Food Ordering App</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css\">\n    <style>\n        body {\n            background-color: #f8f9fa;\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n        }\n\n        .main-container {\n            display: flex;\n            height: calc(100vh - 56px);\n            max-width: 1400px;\n            margin: 0 auto;\n            background: white;\n            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);\n        }\n\n        .left-panel {\n            flex: 2;\n            padding: 20px;\n            border-right: 1px solid #e9ecef;\n            overflow-y: auto;\n        }\n\n        .right-panel {\n            flex: 1;\n            padding: 20px;\n            background: #f8f9fa;\n            overflow-y: auto;\n        }\n\n        .header-section {\n            margin-bottom: 20px;\n        }\n\n        .category-header {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            margin-bottom: 15px;\n        }\n\n        .category-title {\n            font-size: 24px;\n            font-weight: 600;\n            color: #333;\n            margin: 0;\n        }\n\n        .search-filter {\n            display: flex;\n            gap: 10px;\n            margin-bottom: 20px;\n        }\n\n        .search-box {\n            flex: 1;\n            position: relative;\n        }\n\n        .search-box input {\n            width: 100%;\n            padding: 10px 40px 10px 15px;\n            border: 1px solid #ddd;\n            border-radius: 8px;\n            font-size: 14px;\n        }\n\n        .search-box i {\n            position: absolute;\n            right: 15px;\n            top: 50%;\n            transform: translateY(-50%);\n            color: #666;\n        }\n\n        .filter-btn {\n            padding: 10px 15px;\n            border: 1px solid #ddd;\n            border-radius: 8px;\n            background: white;\n            color: #666;\n        }\n\n        .category-tabs {\n            display: flex;\n            gap: 10px;\n            margin-bottom: 20px;\n            overflow-x: auto;\n            padding-bottom: 5px;\n        }\n\n        .category-tab {\n            padding: 8px 16px;\n            border-radius: 20px;\n            border: none;\n            background: #e9ecef;\n            color: #666;\n            white-space: nowrap;\n            cursor: pointer;\n            transition: all 0.3s;\n        }\n\n        .category-tab.active {\n            background: #6c757d;\n            color: white;\n        }\n\n        .products-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n            gap: 20px;\n        }\n\n        .product-card {\n            background: white;\n            border-radius: 12px;\n            padding: 15px;\n            text-align: center;\n            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n            transition: transform 0.2s;\n        }\n\n        .product-card:hover {\n            transform: translateY(-2px);\n        }\n\n        .product-image {\n            width: 100%;\n            height: 120px;\n            object-fit: cover;\n            border-radius: 8px;\n            margin-bottom: 10px;\n        }\n\n        .product-name {\n            font-weight: 600;\n            margin-bottom: 5px;\n            color: #333;\n        }\n\n        .product-price {\n            color: #666;\n            font-weight: 500;\n            margin-bottom: 10px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 8px;\n        }\n\n        .discount-badge {\n            background: #dc3545;\n            color: white;\n            padding: 2px 6px;\n            border-radius: 4px;\n            font-size: 12px;\n            font-weight: 600;\n        }\n\n        .add-btn {\n            width: 40px;\n            height: 40px;\n            border-radius: 50%;\n            border: none;\n            background: #333;\n            color: white;\n            font-size: 18px;\n            cursor: pointer;\n            transition: background 0.3s;\n        }\n\n        .add-btn:hover {\n            background: #555;\n        }\n\n        .navbar {\n            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n        }\n\n        .order-header {\n            font-size: 20px;\n            font-weight: 600;\n            margin-bottom: 20px;\n            color: #333;\n        }\n\n        .user-profile {\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            margin-bottom: 20px;\n            padding: 10px;\n            background: white;\n            border-radius: 8px;\n        }\n\n        .user-avatar {\n            width: 40px;\n            height: 40px;\n            border-radius: 50%;\n            background: #6c757d;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: white;\n            font-weight: 600;\n        }\n\n        .customer-form {\n            background: white;\n            border-radius: 8px;\n            padding: 15px;\n            margin-bottom: 20px;\n        }\n\n        .form-title {\n            font-size: 16px;\n            font-weight: 600;\n            margin-bottom: 15px;\n            color: #333;\n        }\n\n        .form-group {\n            margin-bottom: 15px;\n        }\n\n        .form-group label {\n            display: block;\n            margin-bottom: 5px;\n            font-weight: 500;\n            color: #333;\n            font-size: 14px;\n        }\n\n        .form-control {\n            width: 100%;\n            padding: 10px 12px;\n            border: 1px solid #ddd;\n            border-radius: 6px;\n            font-size: 14px;\n            transition: border-color 0.3s;\n        }\n\n        .form-control:focus {\n            outline: none;\n            border-color: #6f42c1;\n            box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);\n        }\n\n        .order-items {\n            margin-bottom: 20px;\n        }\n\n        .order-item {\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            padding: 10px;\n            background: white;\n            border-radius: 8px;\n            margin-bottom: 10px;\n        }\n\n        .item-thumbnail {\n            width: 40px;\n            height: 40px;\n            border-radius: 50%;\n            object-fit: cover;\n        }\n\n        .item-details {\n            flex: 1;\n        }\n\n        .item-name {\n            font-weight: 500;\n            color: #333;\n            margin-bottom: 2px;\n        }\n\n        .item-price {\n            color: #666;\n            font-size: 14px;\n        }\n\n        .item-controls {\n            display: flex;\n            align-items: center;\n            gap: 8px;\n        }\n\n        .qty-btn {\n            width: 25px;\n            height: 25px;\n            border-radius: 50%;\n            border: 1px solid #ddd;\n            background: white;\n            color: #666;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .qty-display {\n            min-width: 20px;\n            text-align: center;\n            font-weight: 500;\n        }\n\n        .order-summary {\n            background: white;\n            border-radius: 8px;\n            padding: 15px;\n            margin-bottom: 20px;\n        }\n\n        .summary-row {\n            display: flex;\n            justify-content: space-between;\n            margin-bottom: 8px;\n        }\n\n        .summary-row.total {\n            font-weight: 600;\n            font-size: 18px;\n            border-top: 1px solid #e9ecef;\n            padding-top: 10px;\n            margin-top: 10px;\n        }\n\n        .continue-btn {\n            width: 100%;\n            padding: 15px;\n            background: #6f42c1;\n            color: white;\n            border: none;\n            border-radius: 8px;\n            font-weight: 600;\n            font-size: 16px;\n            cursor: pointer;\n            transition: background 0.3s;\n        }\n\n        .continue-btn:hover {\n            background: #5a32a3;\n        }\n\n        @media (max-width: 768px) {\n            .main-container {\n                flex-direction: column;\n                height: auto;\n            }\n\n            .left-panel,\n            .right-panel {\n                flex: none;\n            }\n\n            .products-grid {\n                grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n            }\n        }\n    </style>\n</head>\n\n<body>\n    <!-- Navigation Menu -->\n    <nav class=\"navbar navbar-expand-lg navbar-dark bg-dark\">\n        <div class=\"container-fluid\">\n            <a class=\"navbar-brand\" href=\"#\">POS SYSTEM</a>\n        </div>\n    </nav>\n\n    <div class=\"main-container\">\n        <!-- Left Panel - Items Selection -->\n        <div class=\"left-panel\">\n            <div class=\"header-section\">\n                <div class=\"category-header\">\n                    <h1 class=\"category-title\">POS SYSTEM</h1>\n                </div>\n\n                <div class=\"search-filter\">\n                    <div class=\"search-box\">\n                        <input type=\"text\" id=\"searchInput\" placeholder=\"Search products...\" onkeyup=\"filterProducts()\">\n                        <i class=\"fas fa-search\"></i>\n                    </div>\n                </div>\n\n                <div class=\"category-tabs\" id=\"categoryTabs\">\n                    <!-- Categories will be dynamically generated from JSON data -->\n                </div>\n            </div>\n\n            <div class=\"products-grid\" id=\"productsGrid\">\n                <!-- Products will be dynamically generated from JSON data -->\n            </div>\n        </div>\n\n        <!-- Right Panel - Current Order -->\n        <div class=\"right-panel\">\n            <h2 class=\"order-header\">Current Order</h2>\n\n            <div class=\"customer-form\">\n                <h4 class=\"form-title\">Customer Information</h4>\n                <div class=\"form-group\">\n                    <label for=\"customerName\">Name</label>\n                    <input type=\"text\" id=\"customerName\" class=\"form-control\" placeholder=\"Enter customer name\"\n                        required>\n                </div>\n            </div>\n\n            <div class=\"order-items\" id=\"orderItems\">\n                <!-- Order items will be dynamically added here -->\n            </div>\n\n            <div class=\"order-summary\">\n                <div class=\"summary-row\">\n                    <span>Subtotal</span>\n                    <span id=\"subtotal\">$0.00</span>\n                </div>\n                <div class=\"summary-row\">\n                    <span>Discount</span>\n                    <span id=\"discount\">$0.00</span>\n                </div>\n                <div class=\"summary-row total\">\n                    <span>Total</span>\n                    <span id=\"total\">$0.00</span>\n                </div>\n            </div>\n\n            <form action=\"{{ $resumeWebhookUrl }}\" method=\"post\" id=\"orderForm\">\n                <!-- Hidden inputs for order data -->\n                <input type=\"hidden\" name=\"customerName\" id=\"hiddenCustomerName\">\n                <input type=\"hidden\" name=\"orderItems\" id=\"hiddenOrderItems\">\n                <input type=\"hidden\" name=\"orderTotals\" id=\"hiddenOrderTotals\">\n\n                <button type=\"submit\" class=\"continue-btn\" onclick=\"prepareFormData(event)\">Continue</button>\n            </form>\n        </div>\n    </div>\n\n    <script>\n        // Products data from n8n workflow - separate arrays\n        const productIds = `{{ $json.productId }}`;\n        const productNames = `{{ $json.productName }}`;\n        const productImages = `{{ $json.productImage }}`;\n        const categoryNames = `{{ $json.productCategoryName }}`;\n        const prices = `{{ $json.productPriceUsd }}`;\n        const discounts = `{{ $json.productDiscount }}`;\n        \n        let orderItems = [];\n        let orderTotal = 0;\n        let currentCategory = 'All';\n        let allProducts = [];\n\n        // Parse products data from n8n\n        function initializeProducts() {\n            try {\n                // Parse the string arrays into actual arrays\n                const parsedProductIds = JSON.parse(productIds);\n                const parsedProductNames = JSON.parse(productNames);\n                const parsedProductImages = JSON.parse(productImages);\n                const parsedCategoryNames = JSON.parse(categoryNames);\n                const parsedPrices = JSON.parse(prices);\n                const parsedDiscounts = JSON.parse(discounts);\n\n                // Combine arrays into product objects\n                allProducts = parsedProductIds.map((id, index) => ({\n                    'PRODUCT ID': id,\n                    'PRODUCT NAME': parsedProductNames[index],\n                    'PRODUCT IMAGE': parsedProductImages[index],\n                    'CATEGORY NAME': parsedCategoryNames[index],\n                    'PRICE (USD)': parsedPrices[index],\n                    'DISCOUNT': parsedDiscounts[index]\n                }));\n\n                generateCategoryTabs();\n                generateProductsGrid();\n            } catch (error) {\n                console.error('Error parsing products data:', error);\n                // Fallback to empty array if parsing fails\n                allProducts = [];\n            }\n        }\n\n        function addToOrder(name, price, image, originalPrice, discountRate) {\n            const existingItem = orderItems.find(item => item.name === name);\n\n            if (existingItem) {\n                existingItem.quantity += 1;\n                existingItem.total = existingItem.quantity * existingItem.price;\n            } else {\n                orderItems.push({\n                    name: name,\n                    price: price,\n                    originalPrice: originalPrice,\n                    discountRate: discountRate,\n                    image: image,\n                    quantity: 1,\n                    total: price\n                });\n            }\n\n            updateOrderDisplay();\n            calculateTotals();\n        }\n\n        function updateQuantity(name, change) {\n            const item = orderItems.find(item => item.name === name);\n\n            if (item) {\n                item.quantity += change;\n\n                if (item.quantity <= 0) {\n                    orderItems = orderItems.filter(item => item.name !== name);\n                } else {\n                    item.total = item.quantity * item.price;\n                }\n\n                updateOrderDisplay();\n                calculateTotals();\n            }\n        }\n\n        function updateOrderDisplay() {\n            const orderItemsContainer = document.getElementById('orderItems');\n            orderItemsContainer.innerHTML = '';\n\n            orderItems.forEach(item => {\n                const itemElement = document.createElement('div');\n                itemElement.className = 'order-item';\n\n                // Calculate item discount\n                const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n                const discountText = item.discountRate > 0 ? `<br><small style=\"color: #dc3545;\">-$${itemDiscount.toFixed(2)} discount</small>` : '';\n\n                itemElement.innerHTML = `\n          <img src=\"${item.image}\" class=\"item-thumbnail\" alt=\"${item.name}\">\n          <div class=\"item-details\">\n            <div class=\"item-name\">${item.name}</div>\n            <div class=\"item-price\">$${item.price.toFixed(2)}${discountText}</div>\n          </div>\n          <div class=\"item-controls\">\n            <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', -1)\">-</button>\n            <span class=\"qty-display\">${item.quantity}</span>\n            <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', 1)\">+</button>\n          </div>\n        `;\n                orderItemsContainer.appendChild(itemElement);\n            });\n        }\n\n        function calculateTotals() {\n            const subtotal = orderItems.reduce((sum, item) => sum + item.total, 0);\n\n            // Calculate total discount based on individual item discounts\n            const totalDiscount = orderItems.reduce((sum, item) => {\n                const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n                return sum + itemDiscount;\n            }, 0);\n\n            const total = subtotal;\n\n            document.getElementById('subtotal').textContent = `$${subtotal.toFixed(2)}`;\n            document.getElementById('discount').textContent = `$${totalDiscount.toFixed(2)}`;\n            document.getElementById('total').textContent = `$${total.toFixed(2)}`;\n        }\n\n        function prepareFormData(event) {\n            event.preventDefault();\n\n            // Validate customer information\n            const customerName = document.getElementById('customerName').value.trim();\n\n            if (!customerName) {\n                alert('Please enter customer name.');\n                document.getElementById('customerName').focus();\n                return;\n            }\n\n            if (orderItems.length === 0) {\n                alert('Please add items to your order first.');\n                return;\n            }\n\n            // Prepare order data\n            const orderData = {\n                customer: {\n                    name: customerName\n                },\n                items: orderItems,\n                totals: {\n                    subtotal: orderItems.reduce((sum, item) => sum + item.total, 0),\n                    total: orderItems.reduce((sum, item) => sum + item.total, 0)\n                }\n            };\n\n            // Set hidden form values\n            document.getElementById('hiddenCustomerName').value = customerName;\n            document.getElementById('hiddenOrderItems').value = JSON.stringify(orderItems);\n            document.getElementById('hiddenOrderTotals').value = JSON.stringify(orderData.totals);\n\n            // Show loading state\n            const submitBtn = document.querySelector('.continue-btn');\n            const originalText = submitBtn.textContent;\n            submitBtn.textContent = 'Processing...';\n            submitBtn.disabled = true;\n\n            // Submit the form\n            document.getElementById('orderForm').submit();\n        }\n\n        // Function to get unique categories from products data\n        function getCategories() {\n            const categories = [...new Set(allProducts.map(product => product['CATEGORY NAME']))];\n            return ['All', ...categories];\n        }\n\n        // Function to generate category tabs\n        function generateCategoryTabs() {\n            const categories = getCategories();\n            const categoryTabsContainer = document.getElementById('categoryTabs');\n            categoryTabsContainer.innerHTML = '';\n\n            categories.forEach(category => {\n                const tab = document.createElement('button');\n                tab.className = 'category-tab';\n                if (category === 'All') {\n                    tab.classList.add('active');\n                }\n                tab.textContent = category;\n                tab.onclick = () => filterByCategory(category);\n                categoryTabsContainer.appendChild(tab);\n            });\n        }\n\n        // Function to filter products by category\n        function filterByCategory(category) {\n            currentCategory = category;\n\n            // Update active tab\n            document.querySelectorAll('.category-tab').forEach(tab => {\n                tab.classList.remove('active');\n                if (tab.textContent === category) {\n                    tab.classList.add('active');\n                }\n            });\n\n            // Filter products\n            if (category === 'All') {\n                // Re-initialize all products\n                initializeProducts();\n            } else {\n                // Filter by category\n                const parsedProductIds = JSON.parse(productIds);\n                const parsedProductNames = JSON.parse(productNames);\n                const parsedProductImages = JSON.parse(productImages);\n                const parsedCategoryNames = JSON.parse(categoryNames);\n                const parsedPrices = JSON.parse(prices);\n                const parsedDiscounts = JSON.parse(discounts);\n\n                // Combine arrays into product objects and filter by category\n                allProducts = parsedProductIds.map((id, index) => ({\n                    'PRODUCT ID': id,\n                    'PRODUCT NAME': parsedProductNames[index],\n                    'PRODUCT IMAGE': parsedProductImages[index],\n                    'CATEGORY NAME': parsedCategoryNames[index],\n                    'PRICE (USD)': parsedPrices[index],\n                    'DISCOUNT': parsedDiscounts[index]\n                })).filter(product => product['CATEGORY NAME'] === category);\n\n                generateProductsGrid();\n            }\n        }\n\n        // Function to generate product cards\n        function generateProductsGrid() {\n            const productsGrid = document.getElementById('productsGrid');\n            productsGrid.innerHTML = '';\n\n            allProducts.forEach(product => {\n                const productCard = document.createElement('div');\n                productCard.className = 'product-card';\n\n                // Calculate final price with discount\n                const originalPrice = product['PRICE (USD)'];\n                const discount = product['DISCOUNT'];\n                const finalPrice = originalPrice * (1 - discount);\n\n                // Use image from JSON data\n                const imageUrl = product['PRODUCT IMAGE'];\n\n                productCard.innerHTML = `\n          <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n          <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n          <div class=\"product-price\">\n            $${finalPrice.toFixed(2)}\n            ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n          </div>\n          <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n        `;\n\n                productsGrid.appendChild(productCard);\n            });\n        }\n\n        // Function to filter products by search\n        function filterProducts() {\n            const searchTerm = document.getElementById('searchInput').value.toLowerCase();\n            const productsGrid = document.getElementById('productsGrid');\n            productsGrid.innerHTML = '';\n\n            const filteredProducts = allProducts.filter(product =>\n                product['PRODUCT NAME'].toLowerCase().includes(searchTerm) ||\n                product['CATEGORY NAME'].toLowerCase().includes(searchTerm)\n            );\n\n            filteredProducts.forEach(product => {\n                const productCard = document.createElement('div');\n                productCard.className = 'product-card';\n\n                // Calculate final price with discount\n                const originalPrice = product['PRICE (USD)'];\n                const discount = product['DISCOUNT'];\n                const finalPrice = originalPrice * (1 - discount);\n\n                // Use image from JSON data\n                const imageUrl = product['PRODUCT IMAGE'];\n\n                productCard.innerHTML = `\n          <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n          <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n          <div class=\"product-price\">\n            $${finalPrice.toFixed(2)}\n            ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n          </div>\n          <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n        `;\n\n                productsGrid.appendChild(productCard);\n            });\n        }\n\n        // Initialize the display\n        initializeProducts();\n        updateOrderDisplay();\n        calculateTotals();\n    </script>\n</body>\n\n</html>"
      },
      "typeVersion": 1.4
    },
    {
      "id": "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8",
      "name": "클릭 대기",
      "type": "n8n-nodes-base.wait",
      "position": [
        -240,
        220
      ],
      "webhookId": "2b00ae48-f8f7-49f5-8237-d50f83ff5aa2",
      "parameters": {
        "resume": "webhook",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1
    },
    {
      "id": "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1",
      "name": "클릭에 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -40,
        220
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "=<!DOCTYPE html>\n<html lang=\"id\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>Processing Order</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <style>\n    body {\n      font-family: Arial, sans-serif;\n      text-align: center;\n      padding-top: 100px;\n      background-color: #f0f9f4;\n    }\n    .icon {\n      font-size: 64px;\n      color: #4CAF50;\n    }\n    .message {\n      font-size: 24px;\n      color: #333;\n      margin-top: 20px;\n    }\n    .redirect {\n      font-size: 16px;\n      color: #666;\n      margin-top: 10px;\n    }\n  </style>\n  <script>\n    setTimeout(function() {\n      window.location.href = \"{{ $resumeWebhookUrl }}\";\n    }, 3000); // Wait 3 seconds\n  </script>\n</head>\n<body>\n  <div class=\"icon\">✅</div>\n  <div class=\"message\">Order is successfully processed</div>\n  <div class=\"redirect\">You will be redirected shortly...</div>\n</body>\n</html>\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2f766abf-2d22-4721-9942-247d5534b482",
      "name": "시트용 데이터 포맷팅",
      "type": "n8n-nodes-base.code",
      "position": [
        180,
        40
      ],
      "parameters": {
        "jsCode": "// Get data from body\nconst data = $input.first().json.body;\n\n// Parse orderItems and orderTotals\nconst items = JSON.parse(data.orderItems);\n\n// Function to generate unique Sales ID\nfunction generateSalesId() {\n  const timestamp = Date.now();\n  const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');\n  return `S-${timestamp}-${random}`;\n}\n\n// Get today's date\nconst today = new Date().toISOString().split('T')[0];\n\n// Generate output\nconst salesId = generateSalesId();\nconst output = items.map(item => {\n  const discount = Number((item.originalPrice - item.price).toFixed(2));\n  return {\n    'SALES ID': salesId,\n    'SALES DATE': today, \n    'SALES CUSTOMER NAME': data.customerName,\n    'SALES PRODUCT NAME': item.name,\n    'SALES CATEGORY NAME': '', // Category data not available yet\n    'SALES PRICE (USD)': item.price,\n    'SALES QTY': item.quantity,\n    'SALES DISCOUNT': discount,\n    'SALES TOTAL': item.total\n  };\n});\n\nreturn output.map(item => ({ json: item }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
      "name": "AI 에이전트",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        180,
        420
      ],
      "parameters": {
        "text": "=customer name : {{ $json.body.customerName }}\norder items : {{ $json.body.orderItems }}\norder total : {{ $json.body.orderTotals }}\n\nSales report format : \nNew sales! (opening or greetings to the owner )\ncustomer name : \norder details :\nHave a good day (closing)",
        "options": {
          "systemMessage": "=You are a virtual assistant whose primary task is to create sales reports for business owners.\nWrite in a simple and friendly format. Use emojis to make it more interactive.\nSome item prices are separated by commas.\nFormat all numbers such as prices, subtotal, and total to 2 decimal places only (e.g., 12.97, not 12.969999999999999).\nAvoid using long floating-point numbers.\n\nAvoid using special characters that may break Markdown formatting, such as:\n*, _, [, ], (, ), ~, >, #, +, -, =, {, }, ., !, $.\nUse plain text without special symbols unless necessary.\nDo not use Markdown or HTML formatting."
        },
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "f4b29ac5-8dee-48bc-83a3-136795a447fa",
      "name": "OpenRouter 채팅 모델",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        180,
        600
      ],
      "parameters": {
        "model": "google/gemini-2.0-flash-exp:free",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "id": "5gucapot70b4Qz8b",
          "name": "OpenRouter ASNM"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "72a622bc-b3b7-4840-9b08-686c65b482f9",
      "name": "스티커 메모",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1760,
        -440
      ],
      "parameters": {
        "width": 500,
        "height": 1320,
        "content": "# Smart POS System with Live Updates to Telegram & Sheets\n\nThis Smart POS (Point of Sale) System template provides a lightweight yet powerful sales management solution. It features a modern web based interface for placing orders, with **real-time integration** to **Google Sheets** and **instant Telegram notifications**, enhanced by **AI-generated reports**.  \nIdeal for small businesses, mobile vendors, or anyone who needs a quick and smart POS system.\n\n## ✨ Key Features\n- 🖥️ Modern web interface with product catalog and search\n- 🛒 Cart system with quantity, price, and discount handling\n- 🆔 Unique Sales ID generation for every transaction\n- 📊 Google Sheets integration to store product and sales data\n- 🤖 AI-generated sales summary via OpenRouter\n- 🚀 Instant Telegram notifications for new orders\n\n---\n\n## 🔧 Requirements\n- A Google Sheet to store products and sales data  \n  👉 [Use this Google Sheets template to get started](https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_ID/edit?usp=sharing)\n- Telegram Bot Token and User ID  \n  (Create a bot via [@BotFather](https://t.me/BotFather))\n- OpenRouter API Key  \n  (Sign up at [openrouter.ai](https://openrouter.ai) and use the LLM model)\n\n---\n\n## ⚙️ Setup Instructions\n1. **Set Up Your Google Sheets**\n   - Use the template and fill in product details in the `products` tab\n\n2. **Configure Telegram Bot**\n   - Create a bot via BotFather\n   - Obtain your Bot Token and Chat ID (message the bot once to get ID)\n\n3. **Set Up AI Agent**\n   - In the AI agent node, replace the placeholder with your actual OpenRouter API Key\n\n---\n\n## 🚀 Deploy the Workflow\n1. **Activate** the workflow in n8n\n2. **Open** the webhook URL to access the POS interface\n3. **Enter** product orders and customer details\n4. **Submit** the order\n5. **Receive** an instant Telegram notification with AI-generated sales summary\n6. **Data** is automatically saved to Google Sheets for tracking and analysis\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
      "name": "webhook용 데이터 포맷팅",
      "type": "n8n-nodes-base.code",
      "position": [
        -740,
        220
      ],
      "parameters": {
        "jsCode": "// Get all input data\nconst input = $input.all();\n\n// Extract product data columns\nconst productId = input.map(item => item.json[\"PRODUCT ID\"]);\nconst productName = input.map(item => item.json[\"PRODUCT NAME\"]);\nconst productImage = input.map(item => item.json[\"PRODUCT IMAGE\"]);\nconst productCategoryName = input.map(item => item.json[\"PRODUCT CATEGORY NAME\"]);\nconst productPriceUsd = input.map(item => item.json[\"PRODUCT PRICE (USD)\"]);\nconst productDiscount = input.map(item => item.json[\"PRODUCT DISCOUNT\"]);\n\n// Return in JSON string format\nreturn [{\n  json: {\n    // Product data\n    productId: JSON.stringify(productId),\n    productName: JSON.stringify(productName),\n    productImage: JSON.stringify(productImage),\n    productCategoryName: JSON.stringify(productCategoryName),\n    productPriceUsd: JSON.stringify(productPriceUsd),\n    productDiscount: JSON.stringify(productDiscount),\n\n    // Webhook URL (if needed for redirect or resubmit)\n    webhookUrl: '{{ $json.webhookUrl }}',\n    resumeWebhookUrl: '{{ $resumeWebhookUrl }}'\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7b344857-b997-4882-94c9-770c19df9394",
      "name": "스티커 메모1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -540,
        40
      ],
      "parameters": {
        "color": 2,
        "width": 640,
        "height": 340,
        "content": "- Creates POS interface in HTML format\n- Receives order data from HTML form submitted by user using POST method"
      },
      "typeVersion": 1
    },
    {
      "id": "570f22fa-08aa-4b1f-8e84-de57f893f90f",
      "name": "스티커 메모2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1220,
        40
      ],
      "parameters": {
        "width": 640,
        "height": 340,
        "content": "- Provides webhook endpoint accessible from browser\n- Reads and retrieves product data from Google Sheets\n- Data is formatted and prepared for use in POS frontend or subsequent response."
      },
      "typeVersion": 1
    },
    {
      "id": "a873b6fd-a22c-46c6-a6aa-0a88188df4e0",
      "name": "스티커 메모3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        140,
        -120
      ],
      "parameters": {
        "color": 4,
        "width": 500,
        "height": 340,
        "content": "- Creates sales data in format suitable for Google Sheets.\n- Saves formatted sales results to sales sheet in Google Sheets file."
      },
      "typeVersion": 1
    },
    {
      "id": "28e4314f-7883-4517-95f6-3653674085a2",
      "name": "스티커 메모4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        140,
        300
      ],
      "parameters": {
        "color": 5,
        "width": 500,
        "height": 420,
        "content": "- Receives order data, converts it to business owner-friendly sales report (using LLM), then sends the report to Telegram."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5be9223c-a7a3-4295-a853-a7776c722d17",
  "connections": {
    "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e": {
      "main": [
        [
          {
            "node": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8": {
      "main": [
        [
          {
            "node": "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1": {
      "main": [
        [
          {
            "node": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
            "type": "main",
            "index": 0
          },
          {
            "node": "2f766abf-2d22-4721-9942-247d5534b482",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "84791b6d-a9b2-42b5-8c26-616c66adadd6": {
      "main": [
        [
          {
            "node": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e8b420ed-e5cb-4835-9f9b-979bb9e44e05": {
      "main": [
        [
          {
            "node": "84791b6d-a9b2-42b5-8c26-616c66adadd6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "dff306ac-b6ad-4814-9ed2-2ffed96b420a": {
      "main": [
        [
          {
            "node": "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2f766abf-2d22-4721-9942-247d5534b482": {
      "main": [
        [
          {
            "node": "75d42049-e27a-4208-82ef-4a121812563e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f4b29ac5-8dee-48bc-83a3-136795a447fa": {
      "ai_languageModel": [
        [
          {
            "node": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "ae33dc7d-b8c1-483d-a685-370c5b08e61f": {
      "main": [
        [
          {
            "node": "dff306ac-b6ad-4814-9ed2-2ffed96b420a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

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

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

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

고급 - 고객관계관리, AI 요약

유료인가요?

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

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

고급 사용자를 위한 16+개 노드의 복잡한 워크플로우

저자
Budi SJ

Budi SJ

@budisj

I’m a Product Designer who also works as an Automation Developer. With a background in product design and systems thinking, I build user-centered workflows. My focus is on helping teams and businesses work more productively through impactful automation systems.

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34