8
n8n 한국어amn8n.com

8. 이메일을 Notion 지식庫로 전환(IMAP, Postgres 중복 제거 및 Telegram 알림 포함)

고급

이것은Internal Wiki, Multimodal AI분야의자동화 워크플로우로, 28개의 노드를 포함합니다.주로 If, Code, Notion, Postgres, Telegram 등의 노드를 사용하며. 이메일을 Notion 지식庫로 변환, IMAP, Postgres 중복 제거 및 Telegram 알림 기능 포함

사전 요구사항
  • Notion API Key
  • PostgreSQL 데이터베이스 연결 정보
  • Telegram Bot Token
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "meta": {
    "instanceId": "",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "f69ede00-31fd-4c5f-b4ac-1dc3ed08d9c0",
      "name": "이메일 트리거 (IMAP)",
      "type": "n8n-nodes-base.emailReadImap",
      "position": [
        -928,
        656
      ],
      "parameters": {
        "options": {
          "forceReconnect": 60,
          "customEmailConfig": "[\"UNSEEN\"]"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "2ebe795e-796c-4996-a6d2-29c463bbfe06",
      "name": "코드",
      "type": "n8n-nodes-base.code",
      "position": [
        -704,
        656
      ],
      "parameters": {
        "jsCode": "/**\n * Function Node — Normalize & Dedupe (multi-item, no require)\n * Memproses SEMUA items dari IMAP/HTML node dan mengeluarkan jumlah item yang sama.\n * Output per item:\n * { title, snippet, bodyText, slug, messageId, sentAt, fromAddress, sourceUrl, debugFields }\n */\n\n/* ---------- Helpers (dipakai di tiap item) ---------- */\nfunction toISO(d) {\n  const dt = d ? new Date(d) : new Date();\n  return isNaN(dt) ? new Date().toISOString() : dt.toISOString();\n}\nfunction slugify(s) {\n  return String(s || '')\n    .trim().toLowerCase()\n    .replace(/\\s+/g, '-')        // spasi -> -\n    .replace(/[^a-z0-9-]/g, '')  // buang non URL-safe\n    .replace(/-+/g, '-')         // rapikan multiple -\n    .replace(/^-+|-+$/g, '');    // trim - di awal/akhir\n}\nfunction limit(str, max) {\n  const s = String(str || '').replace(/\\s+/g, ' ').trim();\n  return s.length <= max ? s : s.slice(0, max - 1).trim() + '…';\n}\nfunction firstUrlFrom(text) {\n  const s = String(text || '');\n  const m = s.match(/https?:\\/\\/[^\\s<>\"'`]+/i);\n  return m ? m[0] : '';\n}\nfunction extractFromAddress(from) {\n  if (!from) return '';\n  if (typeof from === 'string') {\n    const m = from.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : from.trim();\n  }\n  if (from.value && Array.isArray(from.value) && from.value[0]?.address) {\n    return from.value[0].address;\n  }\n  if (from.text) {\n    const m = String(from.text).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : String(from.text).trim();\n  }\n  if (from.address) return String(from.address);\n  return '';\n}\nfunction stripHtmlToText(html, fallbackText = '') {\n  let h = String(html || '');\n  if (!h) return String(fallbackText || '');\n  h = h.replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n       .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n  let t = h.replace(/<\\/?[^>]+>/g, ' ');\n  return t.replace(/\\s+/g, ' ').trim();\n}\nfunction simpleHash(str) {\n  let hash = 0;\n  const s = String(str || '');\n  for (let i = 0; i < s.length; i++) {\n    hash = (hash << 5) - hash + s.charCodeAt(i);\n    hash |= 0; // 32-bit\n  }\n  return Math.abs(hash).toString(16);\n}\n\n/* ---------- PROSES SEMUA ITEM ---------- */\nconst titleMax = 140;\nconst snippetMax = 260;\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  // 1) SUBJECT (multi-jalur)\n  const subject =\n    j.subject ??\n    j.headers?.subject ??\n    j.message?.subject ??\n    '(no subject)';\n\n  // 2) BODY (multi-jalur) — dukung hasil HTML node juga (mis. bodyText)\n  const htmlCandidates = [\n    j.bodyTextHtml,   // beberapa node\n    j.textHtml,\n    j.html,\n    j.body?.html,\n    j.message?.html,\n  ];\n  const textCandidates = [\n    j.bodyText,       // kalau HTML node menaruh hasil disini\n    j.textPlain,\n    j.text,\n    j.body?.text,\n    j.message?.text,\n  ];\n\n  const htmlSrc = htmlCandidates.find(v => v && String(v).trim().length > 0);\n  let text = textCandidates.find(v => v && String(v).trim().length > 0) || '';\n\n  if (htmlSrc) {\n    text = stripHtmlToText(htmlSrc, text);\n  } else {\n    text = String(text).replace(/\\s+/g, ' ').trim();\n  }\n\n  // 3) DATE (multi-jalur)\n  const rawDate =\n    j.date ??\n    j.internalDate ??\n    j.headers?.date ??\n    j.message?.date ??\n    new Date().toISOString();\n  const sentAt = toISO(rawDate);\n\n  // 4) FROM (multi-jalur)\n  const fromCandidate =\n    j.from ??\n    j.headers?.from ??\n    j.message?.from ??\n    '';\n  const fromAddress = extractFromAddress(fromCandidate);\n\n  // 5) Title/Slug/Snippet\n  const title = limit(String(subject || '(no subject)').trim(), titleMax);\n  const slug = slugify(title);\n  const snippet = limit(text, snippetMax);\n\n  // 6) URL pertama (opsional)\n  const sourceUrl = firstUrlFrom(text) || '';\n\n  // 7) messageId (fallback hash)\n  let messageId =\n    (j.messageId ?? j.headers?.['message-id'] ?? j.message?.messageId ?? '').toString().trim();\n  if (!messageId) {\n    messageId = simpleHash(`${title}__${sentAt}`);\n  }\n\n  // 8) Debug asal field (opsional, boleh dihapus nanti)\n  const debugFields = {\n    subjectPickedFrom: subject === j.subject ? 'subject'\n      : subject === j.headers?.subject ? 'headers.subject'\n      : subject === j.message?.subject ? 'message.subject'\n      : '(default)',\n    bodyPickedFrom: htmlSrc ? 'HTML→text'\n      : (textCandidates.find(v => v && String(v).trim().length > 0) ? 'plain text' : '(none)'),\n    datePickedFrom: rawDate === j.date ? 'date'\n      : rawDate === j.internalDate ? 'internalDate'\n      : rawDate === j.headers?.date ? 'headers.date'\n      : rawDate === j.message?.date ? 'message.date'\n      : '(default now)',\n    fromPickedFrom: typeof fromCandidate === 'string' ? '(string)'\n      : fromCandidate?.value ? 'from.value[0]'\n      : fromCandidate?.text ? 'from.text'\n      : '(unknown)',\n  };\n\n  return {\n    json: {\n      title,\n      snippet,\n      bodyText: text,\n      slug,\n      messageId,\n      sentAt,\n      fromAddress,\n      sourceUrl,\n      debugFields, // hapus kalau sudah yakin\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7b18f198-1e29-4cf7-b5c4-ca483e4f4486",
      "name": "SQL 쿼리 실행",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -480,
        656
      ],
      "parameters": {
        "query": "SELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = '{{ $json.messageId }}'\n) AS exists;\n",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "39cbc53e-23e4-426f-b879-e1a2fc81c4b8",
      "name": "조건문",
      "type": "n8n-nodes-base.if",
      "position": [
        -256,
        656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "086a03b2-23c7-4a90-904e-420bbd480a0a",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.exists }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ee6394e8-31f1-4962-9c56-c7fa924e162c",
      "name": "데이터베이스 페이지 생성",
      "type": "n8n-nodes-base.notion",
      "position": [
        -32,
        656
      ],
      "parameters": {
        "title": "={{ $('Code').item.json.title }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": "Email Knowledge Base"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "date|date",
              "date": "={{ $('Code').item.json.sentAt }}"
            },
            {
              "key": "summary|rich_text",
              "textContent": "={{ $('Code').item.json.snippet }}"
            },
            {
              "key": "sourceUrl|url",
              "urlValue": "={{ $('Code').item.json.sourceUrl }}",
              "ignoreIfEmpty": true
            },
            {
              "key": "From|rich_text",
              "textContent": "={{ $('Code').item.json.fromAddress }}"
            },
            {
              "key": "Slug|rich_text",
              "textContent": "={{ $('Code').item.json.slug }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7d3d8ece-4dc4-484c-a6ba-11a706933de5",
      "name": "텍스트 메시지 전송",
      "type": "n8n-nodes-base.telegram",
      "position": [
        192,
        656
      ],
      "webhookId": "e529cba8-37ec-4427-8404-b1d20e4924c7",
      "parameters": {
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "a2c4f333-cb90-44a5-9469-bc89d33e7824",
      "name": "스티커 노트",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        -160
      ],
      "parameters": {
        "width": 640,
        "height": 672,
        "content": "0) Create Postgres Table (run once)\nCREATE TABLE IF NOT EXISTS email_kb_index (\n  message_id     TEXT PRIMARY KEY,\n  slug           TEXT,\n  created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),\n  notion_page_id TEXT\n);\n\n1) IMAP Email (Trigger)\n\nOptions → customEmailConfig: [\"UNSEEN\"]\n\n(Gmail) Host: imap.gmail.com, Port: 993, SSL/TLS.\n\nKey output fields: subject, textHtml/textPlain/html, date, messageId, from.\n\n2) Function (Normalize)\n\nConvert HTML → plain text (fallback to textPlain).\n\nOutput fields:\n\ntitle, snippet(≤260), bodyText, slug (lowercase, spaces→-, remove non [a-z0-9-])\n\nmessageId (use header, fallback hash title+sentAt)\n\nsentAt (ISO), fromAddress, sourceUrl (first URL in body)\n\n3) Postgres (Execute Query) — Check Duplicate\n\nSafe query that always returns output:\n\nSELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = $1\n) AS exists;\n\n\nQuery Parameters: $1 = ={{ $json.messageId }}\n\n 5. IF — Only Continue if New\n\nCondition: Left ={{ $json.exists }} equals false\n\nTrue → continue\n\nFalse → stop/log (duplicate)"
      },
      "typeVersion": 1
    },
    {
      "id": "d3cd1192-79e8-4d02-b157-5611f53f9141",
      "name": "스티커 노트1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -352,
        -96
      ],
      "parameters": {
        "color": 3,
        "width": 672,
        "height": 528,
        "content": "6. Notion — Create Page (Parent: Database)\nDatabase ID: paste the 32-character ID → ensure the database is shared with the integration.\nProperty mapping (adjust to your Notion column names):\nTitle (Title) ← ={{ $json.title }}\nSummary (Rich text) ← ={{ $json.snippet }}\nTags (Multi-select) ← (optional, array of strings)\nSource (URL) ← ={{ $json.sourceUrl }}\nFrom (Text/Email) ← ={{ $json.fromAddress }}\nDate (Date) ← ={{ $json.sentAt }}\nSlug (Text) ← ={{ $json.slug }}\nNotes (Rich text/Long text, optional) ← ={{ $json.bodyText }}\n\n7. (Recommended) Postgres — Mark as Processed\nAfter Notion succeeds:\nINSERT INTO email_kb_index (message_id, slug, created_at, notion_page_id)\nVALUES ($1, $2, $3, $4)\nON CONFLICT (message_id) DO NOTHING;\n\nParams:\n$1 = ={{ $json.messageId }}\n$2 = ={{ $json.slug }}\n$3 = ={{ $json.sentAt }}\n$4 = ={{ $node[\"Create a database page\"].json.id }}\nReplace with your actual Notion node name.\n\n3. Telegram — Notification\nChat ID: ={{ $env.TELEGRAM_CHAT_ID }}\nText (Markdown):\n\n✅ Saved to Notion\n*{{ $json.title }}*\nFrom: {{ $json.fromAddress }}\nDate: {{ $json.sentAt }}\nLink: {{ $json.sourceUrl || '-' }}\n\nparseMode: Markdown"
      },
      "typeVersion": 1
    },
    {
      "id": "eb42b744-cf6a-49d5-9c5f-5109d823e2af",
      "name": "이메일 트리거 (IMAP)1",
      "type": "n8n-nodes-base.emailReadImap",
      "disabled": true,
      "position": [
        -544,
        -96
      ],
      "parameters": {
        "options": {
          "forceReconnect": 60,
          "customEmailConfig": "[\"UNSEEN\"]"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "1dd93fa5-e5c8-4dd2-963e-1130ff968a73",
      "name": "코드1",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        -544,
        48
      ],
      "parameters": {
        "jsCode": "/**\n * Function Node — Normalize & Dedupe (multi-item, no require)\n * Memproses SEMUA items dari IMAP/HTML node dan mengeluarkan jumlah item yang sama.\n * Output per item:\n * { title, snippet, bodyText, slug, messageId, sentAt, fromAddress, sourceUrl, debugFields }\n */\n\n/* ---------- Helpers (dipakai di tiap item) ---------- */\nfunction toISO(d) {\n  const dt = d ? new Date(d) : new Date();\n  return isNaN(dt) ? new Date().toISOString() : dt.toISOString();\n}\nfunction slugify(s) {\n  return String(s || '')\n    .trim().toLowerCase()\n    .replace(/\\s+/g, '-')        // spasi -> -\n    .replace(/[^a-z0-9-]/g, '')  // buang non URL-safe\n    .replace(/-+/g, '-')         // rapikan multiple -\n    .replace(/^-+|-+$/g, '');    // trim - di awal/akhir\n}\nfunction limit(str, max) {\n  const s = String(str || '').replace(/\\s+/g, ' ').trim();\n  return s.length <= max ? s : s.slice(0, max - 1).trim() + '…';\n}\nfunction firstUrlFrom(text) {\n  const s = String(text || '');\n  const m = s.match(/https?:\\/\\/[^\\s<>\"'`]+/i);\n  return m ? m[0] : '';\n}\nfunction extractFromAddress(from) {\n  if (!from) return '';\n  if (typeof from === 'string') {\n    const m = from.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : from.trim();\n  }\n  if (from.value && Array.isArray(from.value) && from.value[0]?.address) {\n    return from.value[0].address;\n  }\n  if (from.text) {\n    const m = String(from.text).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : String(from.text).trim();\n  }\n  if (from.address) return String(from.address);\n  return '';\n}\nfunction stripHtmlToText(html, fallbackText = '') {\n  let h = String(html || '');\n  if (!h) return String(fallbackText || '');\n  h = h.replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n       .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n  let t = h.replace(/<\\/?[^>]+>/g, ' ');\n  return t.replace(/\\s+/g, ' ').trim();\n}\nfunction simpleHash(str) {\n  let hash = 0;\n  const s = String(str || '');\n  for (let i = 0; i < s.length; i++) {\n    hash = (hash << 5) - hash + s.charCodeAt(i);\n    hash |= 0; // 32-bit\n  }\n  return Math.abs(hash).toString(16);\n}\n\n/* ---------- PROSES SEMUA ITEM ---------- */\nconst titleMax = 140;\nconst snippetMax = 260;\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  // 1) SUBJECT (multi-jalur)\n  const subject =\n    j.subject ??\n    j.headers?.subject ??\n    j.message?.subject ??\n    '(no subject)';\n\n  // 2) BODY (multi-jalur) — dukung hasil HTML node juga (mis. bodyText)\n  const htmlCandidates = [\n    j.bodyTextHtml,   // beberapa node\n    j.textHtml,\n    j.html,\n    j.body?.html,\n    j.message?.html,\n  ];\n  const textCandidates = [\n    j.bodyText,       // kalau HTML node menaruh hasil disini\n    j.textPlain,\n    j.text,\n    j.body?.text,\n    j.message?.text,\n  ];\n\n  const htmlSrc = htmlCandidates.find(v => v && String(v).trim().length > 0);\n  let text = textCandidates.find(v => v && String(v).trim().length > 0) || '';\n\n  if (htmlSrc) {\n    text = stripHtmlToText(htmlSrc, text);\n  } else {\n    text = String(text).replace(/\\s+/g, ' ').trim();\n  }\n\n  // 3) DATE (multi-jalur)\n  const rawDate =\n    j.date ??\n    j.internalDate ??\n    j.headers?.date ??\n    j.message?.date ??\n    new Date().toISOString();\n  const sentAt = toISO(rawDate);\n\n  // 4) FROM (multi-jalur)\n  const fromCandidate =\n    j.from ??\n    j.headers?.from ??\n    j.message?.from ??\n    '';\n  const fromAddress = extractFromAddress(fromCandidate);\n\n  // 5) Title/Slug/Snippet\n  const title = limit(String(subject || '(no subject)').trim(), titleMax);\n  const slug = slugify(title);\n  const snippet = limit(text, snippetMax);\n\n  // 6) URL pertama (opsional)\n  const sourceUrl = firstUrlFrom(text) || '';\n\n  // 7) messageId (fallback hash)\n  let messageId =\n    (j.messageId ?? j.headers?.['message-id'] ?? j.message?.messageId ?? '').toString().trim();\n  if (!messageId) {\n    messageId = simpleHash(`${title}__${sentAt}`);\n  }\n\n  // 8) Debug asal field (opsional, boleh dihapus nanti)\n  const debugFields = {\n    subjectPickedFrom: subject === j.subject ? 'subject'\n      : subject === j.headers?.subject ? 'headers.subject'\n      : subject === j.message?.subject ? 'message.subject'\n      : '(default)',\n    bodyPickedFrom: htmlSrc ? 'HTML→text'\n      : (textCandidates.find(v => v && String(v).trim().length > 0) ? 'plain text' : '(none)'),\n    datePickedFrom: rawDate === j.date ? 'date'\n      : rawDate === j.internalDate ? 'internalDate'\n      : rawDate === j.headers?.date ? 'headers.date'\n      : rawDate === j.message?.date ? 'message.date'\n      : '(default now)',\n    fromPickedFrom: typeof fromCandidate === 'string' ? '(string)'\n      : fromCandidate?.value ? 'from.value[0]'\n      : fromCandidate?.text ? 'from.text'\n      : '(unknown)',\n  };\n\n  return {\n    json: {\n      title,\n      snippet,\n      bodyText: text,\n      slug,\n      messageId,\n      sentAt,\n      fromAddress,\n      sourceUrl,\n      debugFields, // hapus kalau sudah yakin\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "de0849c9-a638-42ce-b436-9f50000f5b0c",
      "name": "SQL 쿼리 실행1",
      "type": "n8n-nodes-base.postgres",
      "disabled": true,
      "position": [
        -544,
        208
      ],
      "parameters": {
        "query": "SELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = '{{ $json.messageId }}'\n) AS exists;\n",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "c092af6c-aa61-432f-9212-7723fdecef1c",
      "name": "조건문1",
      "type": "n8n-nodes-base.if",
      "disabled": true,
      "position": [
        -544,
        368
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "086a03b2-23c7-4a90-904e-420bbd480a0a",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.exists }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3de38f0e-371a-4e52-b6e4-2fa4aafa0306",
      "name": "데이터베이스 페이지 생성1",
      "type": "n8n-nodes-base.notion",
      "disabled": true,
      "position": [
        144,
        -16
      ],
      "parameters": {
        "title": "={{ $('Code').item.json.title }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": "Email Knowledge Base"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "date|date",
              "date": "={{ $('Code').item.json.sentAt }}"
            },
            {
              "key": "summary|rich_text",
              "textContent": "={{ $('Code').item.json.snippet }}"
            },
            {
              "key": "sourceUrl|url",
              "urlValue": "={{ $('Code').item.json.sourceUrl }}",
              "ignoreIfEmpty": true
            },
            {
              "key": "From|rich_text",
              "textContent": "={{ $('Code').item.json.fromAddress }}"
            },
            {
              "key": "Slug|rich_text",
              "textContent": "={{ $('Code').item.json.slug }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "055677f0-bea4-4a65-b603-b918068e3fb2",
      "name": "텍스트 메시지 전송1",
      "type": "n8n-nodes-base.telegram",
      "disabled": true,
      "position": [
        128,
        224
      ],
      "webhookId": "e529cba8-37ec-4427-8404-b1d20e4924c7",
      "parameters": {
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "8071d1b9-8da3-4720-b1ee-0537f2c05262",
      "name": "이메일 트리거 (IMAP)2",
      "type": "n8n-nodes-base.emailReadImap",
      "position": [
        -6096,
        2512
      ],
      "parameters": {
        "options": {
          "forceReconnect": 60,
          "customEmailConfig": "[\"UNSEEN\"]"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "5339fdb6-b7aa-4da0-82f5-1f6e07d96246",
      "name": "코드2",
      "type": "n8n-nodes-base.code",
      "position": [
        -5872,
        2512
      ],
      "parameters": {
        "jsCode": "/**\n * Function Node — Normalize & Dedupe (multi-item, no require)\n * Memproses SEMUA items dari IMAP/HTML node dan mengeluarkan jumlah item yang sama.\n * Output per item:\n * { title, snippet, bodyText, slug, messageId, sentAt, fromAddress, sourceUrl, debugFields }\n */\n\n/* ---------- Helpers (dipakai di tiap item) ---------- */\nfunction toISO(d) {\n  const dt = d ? new Date(d) : new Date();\n  return isNaN(dt) ? new Date().toISOString() : dt.toISOString();\n}\nfunction slugify(s) {\n  return String(s || '')\n    .trim().toLowerCase()\n    .replace(/\\s+/g, '-')        // spasi -> -\n    .replace(/[^a-z0-9-]/g, '')  // buang non URL-safe\n    .replace(/-+/g, '-')         // rapikan multiple -\n    .replace(/^-+|-+$/g, '');    // trim - di awal/akhir\n}\nfunction limit(str, max) {\n  const s = String(str || '').replace(/\\s+/g, ' ').trim();\n  return s.length <= max ? s : s.slice(0, max - 1).trim() + '…';\n}\nfunction firstUrlFrom(text) {\n  const s = String(text || '');\n  const m = s.match(/https?:\\/\\/[^\\s<>\"'`]+/i);\n  return m ? m[0] : '';\n}\nfunction extractFromAddress(from) {\n  if (!from) return '';\n  if (typeof from === 'string') {\n    const m = from.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : from.trim();\n  }\n  if (from.value && Array.isArray(from.value) && from.value[0]?.address) {\n    return from.value[0].address;\n  }\n  if (from.text) {\n    const m = String(from.text).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : String(from.text).trim();\n  }\n  if (from.address) return String(from.address);\n  return '';\n}\nfunction stripHtmlToText(html, fallbackText = '') {\n  let h = String(html || '');\n  if (!h) return String(fallbackText || '');\n  h = h.replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n       .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n  let t = h.replace(/<\\/?[^>]+>/g, ' ');\n  return t.replace(/\\s+/g, ' ').trim();\n}\nfunction simpleHash(str) {\n  let hash = 0;\n  const s = String(str || '');\n  for (let i = 0; i < s.length; i++) {\n    hash = (hash << 5) - hash + s.charCodeAt(i);\n    hash |= 0; // 32-bit\n  }\n  return Math.abs(hash).toString(16);\n}\n\n/* ---------- PROSES SEMUA ITEM ---------- */\nconst titleMax = 140;\nconst snippetMax = 260;\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  // 1) SUBJECT (multi-jalur)\n  const subject =\n    j.subject ??\n    j.headers?.subject ??\n    j.message?.subject ??\n    '(no subject)';\n\n  // 2) BODY (multi-jalur) — dukung hasil HTML node juga (mis. bodyText)\n  const htmlCandidates = [\n    j.bodyTextHtml,   // beberapa node\n    j.textHtml,\n    j.html,\n    j.body?.html,\n    j.message?.html,\n  ];\n  const textCandidates = [\n    j.bodyText,       // kalau HTML node menaruh hasil disini\n    j.textPlain,\n    j.text,\n    j.body?.text,\n    j.message?.text,\n  ];\n\n  const htmlSrc = htmlCandidates.find(v => v && String(v).trim().length > 0);\n  let text = textCandidates.find(v => v && String(v).trim().length > 0) || '';\n\n  if (htmlSrc) {\n    text = stripHtmlToText(htmlSrc, text);\n  } else {\n    text = String(text).replace(/\\s+/g, ' ').trim();\n  }\n\n  // 3) DATE (multi-jalur)\n  const rawDate =\n    j.date ??\n    j.internalDate ??\n    j.headers?.date ??\n    j.message?.date ??\n    new Date().toISOString();\n  const sentAt = toISO(rawDate);\n\n  // 4) FROM (multi-jalur)\n  const fromCandidate =\n    j.from ??\n    j.headers?.from ??\n    j.message?.from ??\n    '';\n  const fromAddress = extractFromAddress(fromCandidate);\n\n  // 5) Title/Slug/Snippet\n  const title = limit(String(subject || '(no subject)').trim(), titleMax);\n  const slug = slugify(title);\n  const snippet = limit(text, snippetMax);\n\n  // 6) URL pertama (opsional)\n  const sourceUrl = firstUrlFrom(text) || '';\n\n  // 7) messageId (fallback hash)\n  let messageId =\n    (j.messageId ?? j.headers?.['message-id'] ?? j.message?.messageId ?? '').toString().trim();\n  if (!messageId) {\n    messageId = simpleHash(`${title}__${sentAt}`);\n  }\n\n  // 8) Debug asal field (opsional, boleh dihapus nanti)\n  const debugFields = {\n    subjectPickedFrom: subject === j.subject ? 'subject'\n      : subject === j.headers?.subject ? 'headers.subject'\n      : subject === j.message?.subject ? 'message.subject'\n      : '(default)',\n    bodyPickedFrom: htmlSrc ? 'HTML→text'\n      : (textCandidates.find(v => v && String(v).trim().length > 0) ? 'plain text' : '(none)'),\n    datePickedFrom: rawDate === j.date ? 'date'\n      : rawDate === j.internalDate ? 'internalDate'\n      : rawDate === j.headers?.date ? 'headers.date'\n      : rawDate === j.message?.date ? 'message.date'\n      : '(default now)',\n    fromPickedFrom: typeof fromCandidate === 'string' ? '(string)'\n      : fromCandidate?.value ? 'from.value[0]'\n      : fromCandidate?.text ? 'from.text'\n      : '(unknown)',\n  };\n\n  return {\n    json: {\n      title,\n      snippet,\n      bodyText: text,\n      slug,\n      messageId,\n      sentAt,\n      fromAddress,\n      sourceUrl,\n      debugFields, // hapus kalau sudah yakin\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6aaf8200-35c7-419a-9480-a54f49b43493",
      "name": "SQL 쿼리 실행2",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -5648,
        2512
      ],
      "parameters": {
        "query": "SELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = '{{ $json.messageId }}'\n) AS exists;\n",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "481d4a86-c36a-4ced-abfa-b9aa1313f2a7",
      "name": "조건문2",
      "type": "n8n-nodes-base.if",
      "position": [
        -5424,
        2512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "086a03b2-23c7-4a90-904e-420bbd480a0a",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.exists }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0db94227-30d5-4cd5-8e00-1c538ebee9d7",
      "name": "데이터베이스 페이지 생성2",
      "type": "n8n-nodes-base.notion",
      "position": [
        -5200,
        2512
      ],
      "parameters": {
        "title": "={{ $('Code2').item.json.title }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": "Email Knowledge Base"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "date|date",
              "date": "={{ $('Code2').item.json.sentAt }}"
            },
            {
              "key": "summary|rich_text",
              "textContent": "={{ $('Code2').item.json.snippet }}"
            },
            {
              "key": "sourceUrl|url",
              "urlValue": "={{ $('Code2').item.json.sourceUrl }}",
              "ignoreIfEmpty": true
            },
            {
              "key": "From|rich_text",
              "textContent": "={{ $('Code2').item.json.fromAddress }}"
            },
            {
              "key": "Slug|rich_text",
              "textContent": "={{ $('Code2').item.json.slug }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "069bafd2-b669-4890-9dd5-417310aecfa5",
      "name": "텍스트 메시지 전송2",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -4976,
        2512
      ],
      "webhookId": "118a1948-3e67-4a62-85b4-c876ff8d69f2",
      "parameters": {
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "0f04de27-a5cf-4a83-ba16-e9530ccbb091",
      "name": "스티커 노트2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6160,
        1712
      ],
      "parameters": {
        "width": 640,
        "height": 688,
        "content": "0) Create Postgres Table (run once)\nCREATE TABLE IF NOT EXISTS email_kb_index (\n  message_id     TEXT PRIMARY KEY,\n  slug           TEXT,\n  created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),\n  notion_page_id TEXT\n);\n\n1) IMAP Email (Trigger)\n\nOptions → customEmailConfig: [\"UNSEEN\"]\n\n(Gmail) Host: imap.gmail.com, Port: 993, SSL/TLS.\n\nKey output fields: subject, textHtml/textPlain/html, date, messageId, from.\n\n2) Function (Normalize)\n\nConvert HTML → plain text (fallback to textPlain).\n\nOutput fields:\n\ntitle, snippet(≤260), bodyText, slug (lowercase, spaces→-, remove non [a-z0-9-])\n\nmessageId (use header, fallback hash title+sentAt)\n\nsentAt (ISO), fromAddress, sourceUrl (first URL in body)\n\n3) Postgres (Execute Query) — Check Duplicate\n\nSafe query that always returns output:\n\nSELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = $1\n) AS exists;\n\n\nQuery Parameters: $1 = ={{ $json.messageId }}\n\n 5. IF — Only Continue if New\n\nCondition: Left ={{ $json.exists }} equals false\n\nTrue → continue\n\nFalse → stop/log (duplicate)"
      },
      "typeVersion": 1
    },
    {
      "id": "40bf69bc-7e99-44a4-ba3d-cb58274b0be1",
      "name": "스티커 노트3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5520,
        1760
      ],
      "parameters": {
        "color": 3,
        "width": 672,
        "height": 528,
        "content": "6. Notion — Create Page (Parent: Database)\nDatabase ID: paste the 32-character ID → ensure the database is shared with the integration.\nProperty mapping (adjust to your Notion column names):\nTitle (Title) ← ={{ $json.title }}\nSummary (Rich text) ← ={{ $json.snippet }}\nTags (Multi-select) ← (optional, array of strings)\nSource (URL) ← ={{ $json.sourceUrl }}\nFrom (Text/Email) ← ={{ $json.fromAddress }}\nDate (Date) ← ={{ $json.sentAt }}\nSlug (Text) ← ={{ $json.slug }}\nNotes (Rich text/Long text, optional) ← ={{ $json.bodyText }}\n\n(Recommended) Postgres — Mark as Processed\nAfter Notion succeeds:\nINSERT INTO email_kb_index (message_id, slug, created_at, notion_page_id)\nVALUES ($1, $2, $3, $4)\nON CONFLICT (message_id) DO NOTHING;\n\nParams:\n$1 = ={{ $json.messageId }}\n$2 = ={{ $json.slug }}\n$3 = ={{ $json.sentAt }}\n$4 = ={{ $node[\"Create a database page\"].json.id }}\nReplace with your actual Notion node name.\n\nTelegram — Notification\nChat ID: ={{ $env.TELEGRAM_CHAT_ID }}\nText (Markdown):\n\n✅ Saved to Notion\n*{{ $json.title }}*\nFrom: {{ $json.fromAddress }}\nDate: {{ $json.sentAt }}\nLink: {{ $json.sourceUrl || '-' }}\n\nparseMode: Markdown"
      },
      "typeVersion": 1
    },
    {
      "id": "ba8081b0-40af-45cd-b748-1fbc2fb69f66",
      "name": "이메일 트리거 (IMAP)3",
      "type": "n8n-nodes-base.emailReadImap",
      "disabled": true,
      "position": [
        -5712,
        1760
      ],
      "parameters": {
        "options": {
          "forceReconnect": 60,
          "customEmailConfig": "[\"UNSEEN\"]"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "6a16fe30-03a2-4ad9-a918-07f10cfe7086",
      "name": "코드3",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        -5712,
        1904
      ],
      "parameters": {
        "jsCode": "/**\n * Function Node — Normalize & Dedupe (multi-item, no require)\n * Memproses SEMUA items dari IMAP/HTML node dan mengeluarkan jumlah item yang sama.\n * Output per item:\n * { title, snippet, bodyText, slug, messageId, sentAt, fromAddress, sourceUrl, debugFields }\n */\n\n/* ---------- Helpers (dipakai di tiap item) ---------- */\nfunction toISO(d) {\n  const dt = d ? new Date(d) : new Date();\n  return isNaN(dt) ? new Date().toISOString() : dt.toISOString();\n}\nfunction slugify(s) {\n  return String(s || '')\n    .trim().toLowerCase()\n    .replace(/\\s+/g, '-')        // spasi -> -\n    .replace(/[^a-z0-9-]/g, '')  // buang non URL-safe\n    .replace(/-+/g, '-')         // rapikan multiple -\n    .replace(/^-+|-+$/g, '');    // trim - di awal/akhir\n}\nfunction limit(str, max) {\n  const s = String(str || '').replace(/\\s+/g, ' ').trim();\n  return s.length <= max ? s : s.slice(0, max - 1).trim() + '…';\n}\nfunction firstUrlFrom(text) {\n  const s = String(text || '');\n  const m = s.match(/https?:\\/\\/[^\\s<>\"'`]+/i);\n  return m ? m[0] : '';\n}\nfunction extractFromAddress(from) {\n  if (!from) return '';\n  if (typeof from === 'string') {\n    const m = from.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : from.trim();\n  }\n  if (from.value && Array.isArray(from.value) && from.value[0]?.address) {\n    return from.value[0].address;\n  }\n  if (from.text) {\n    const m = String(from.text).match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i);\n    return m ? m[0] : String(from.text).trim();\n  }\n  if (from.address) return String(from.address);\n  return '';\n}\nfunction stripHtmlToText(html, fallbackText = '') {\n  let h = String(html || '');\n  if (!h) return String(fallbackText || '');\n  h = h.replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n       .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n  let t = h.replace(/<\\/?[^>]+>/g, ' ');\n  return t.replace(/\\s+/g, ' ').trim();\n}\nfunction simpleHash(str) {\n  let hash = 0;\n  const s = String(str || '');\n  for (let i = 0; i < s.length; i++) {\n    hash = (hash << 5) - hash + s.charCodeAt(i);\n    hash |= 0; // 32-bit\n  }\n  return Math.abs(hash).toString(16);\n}\n\n/* ---------- PROSES SEMUA ITEM ---------- */\nconst titleMax = 140;\nconst snippetMax = 260;\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  // 1) SUBJECT (multi-jalur)\n  const subject =\n    j.subject ??\n    j.headers?.subject ??\n    j.message?.subject ??\n    '(no subject)';\n\n  // 2) BODY (multi-jalur) — dukung hasil HTML node juga (mis. bodyText)\n  const htmlCandidates = [\n    j.bodyTextHtml,   // beberapa node\n    j.textHtml,\n    j.html,\n    j.body?.html,\n    j.message?.html,\n  ];\n  const textCandidates = [\n    j.bodyText,       // kalau HTML node menaruh hasil disini\n    j.textPlain,\n    j.text,\n    j.body?.text,\n    j.message?.text,\n  ];\n\n  const htmlSrc = htmlCandidates.find(v => v && String(v).trim().length > 0);\n  let text = textCandidates.find(v => v && String(v).trim().length > 0) || '';\n\n  if (htmlSrc) {\n    text = stripHtmlToText(htmlSrc, text);\n  } else {\n    text = String(text).replace(/\\s+/g, ' ').trim();\n  }\n\n  // 3) DATE (multi-jalur)\n  const rawDate =\n    j.date ??\n    j.internalDate ??\n    j.headers?.date ??\n    j.message?.date ??\n    new Date().toISOString();\n  const sentAt = toISO(rawDate);\n\n  // 4) FROM (multi-jalur)\n  const fromCandidate =\n    j.from ??\n    j.headers?.from ??\n    j.message?.from ??\n    '';\n  const fromAddress = extractFromAddress(fromCandidate);\n\n  // 5) Title/Slug/Snippet\n  const title = limit(String(subject || '(no subject)').trim(), titleMax);\n  const slug = slugify(title);\n  const snippet = limit(text, snippetMax);\n\n  // 6) URL pertama (opsional)\n  const sourceUrl = firstUrlFrom(text) || '';\n\n  // 7) messageId (fallback hash)\n  let messageId =\n    (j.messageId ?? j.headers?.['message-id'] ?? j.message?.messageId ?? '').toString().trim();\n  if (!messageId) {\n    messageId = simpleHash(`${title}__${sentAt}`);\n  }\n\n  // 8) Debug asal field (opsional, boleh dihapus nanti)\n  const debugFields = {\n    subjectPickedFrom: subject === j.subject ? 'subject'\n      : subject === j.headers?.subject ? 'headers.subject'\n      : subject === j.message?.subject ? 'message.subject'\n      : '(default)',\n    bodyPickedFrom: htmlSrc ? 'HTML→text'\n      : (textCandidates.find(v => v && String(v).trim().length > 0) ? 'plain text' : '(none)'),\n    datePickedFrom: rawDate === j.date ? 'date'\n      : rawDate === j.internalDate ? 'internalDate'\n      : rawDate === j.headers?.date ? 'headers.date'\n      : rawDate === j.message?.date ? 'message.date'\n      : '(default now)',\n    fromPickedFrom: typeof fromCandidate === 'string' ? '(string)'\n      : fromCandidate?.value ? 'from.value[0]'\n      : fromCandidate?.text ? 'from.text'\n      : '(unknown)',\n  };\n\n  return {\n    json: {\n      title,\n      snippet,\n      bodyText: text,\n      slug,\n      messageId,\n      sentAt,\n      fromAddress,\n      sourceUrl,\n      debugFields, // hapus kalau sudah yakin\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0878f213-7ea0-4c16-98fd-2f680aa72254",
      "name": "SQL 쿼리 실행3",
      "type": "n8n-nodes-base.postgres",
      "disabled": true,
      "position": [
        -5712,
        2064
      ],
      "parameters": {
        "query": "SELECT EXISTS(\n  SELECT 1\n  FROM email_kb_index\n  WHERE message_id = '{{ $json.messageId }}'\n) AS exists;\n",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "e0b2d5c4-c3bf-4025-a56f-bd8123eaf6c2",
      "name": "조건문3",
      "type": "n8n-nodes-base.if",
      "disabled": true,
      "position": [
        -5712,
        2224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "086a03b2-23c7-4a90-904e-420bbd480a0a",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.exists }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "56809d81-55f3-45b5-baaa-116378c4bd25",
      "name": "데이터베이스 페이지 생성3",
      "type": "n8n-nodes-base.notion",
      "disabled": true,
      "position": [
        -5024,
        1840
      ],
      "parameters": {
        "title": "={{ $('Code2').item.json.title }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": "Email Knowledge Base"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "date|date",
              "date": "={{ $('Code2').item.json.sentAt }}"
            },
            {
              "key": "summary|rich_text",
              "textContent": "={{ $('Code2').item.json.snippet }}"
            },
            {
              "key": "sourceUrl|url",
              "urlValue": "={{ $('Code2').item.json.sourceUrl }}",
              "ignoreIfEmpty": true
            },
            {
              "key": "From|rich_text",
              "textContent": "={{ $('Code2').item.json.fromAddress }}"
            },
            {
              "key": "Slug|rich_text",
              "textContent": "={{ $('Code2').item.json.slug }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "48463d2d-bba3-4d85-99a5-4ca217c91a3a",
      "name": "텍스트 메시지 전송3",
      "type": "n8n-nodes-base.telegram",
      "disabled": true,
      "position": [
        -5040,
        2080
      ],
      "webhookId": "",
      "parameters": {
        "additionalFields": {}
      },
      "typeVersion": 1.2
    }
  ],
  "pinData": {},
  "connections": {
    "39cbc53e-23e4-426f-b879-e1a2fc81c4b8": {
      "main": [
        [
          {
            "node": "ee6394e8-31f1-4962-9c56-c7fa924e162c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "481d4a86-c36a-4ced-abfa-b9aa1313f2a7": {
      "main": [
        [
          {
            "node": "0db94227-30d5-4cd5-8e00-1c538ebee9d7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2ebe795e-796c-4996-a6d2-29c463bbfe06": {
      "main": [
        [
          {
            "node": "7b18f198-1e29-4cf7-b5c4-ca483e4f4486",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1dd93fa5-e5c8-4dd2-963e-1130ff968a73": {
      "main": [
        []
      ]
    },
    "5339fdb6-b7aa-4da0-82f5-1f6e07d96246": {
      "main": [
        [
          {
            "node": "6aaf8200-35c7-419a-9480-a54f49b43493",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7b18f198-1e29-4cf7-b5c4-ca483e4f4486": {
      "main": [
        [
          {
            "node": "39cbc53e-23e4-426f-b879-e1a2fc81c4b8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f69ede00-31fd-4c5f-b4ac-1dc3ed08d9c0": {
      "main": [
        [
          {
            "node": "2ebe795e-796c-4996-a6d2-29c463bbfe06",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "de0849c9-a638-42ce-b436-9f50000f5b0c": {
      "main": [
        []
      ]
    },
    "6aaf8200-35c7-419a-9480-a54f49b43493": {
      "main": [
        [
          {
            "node": "481d4a86-c36a-4ced-abfa-b9aa1313f2a7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "eb42b744-cf6a-49d5-9c5f-5109d823e2af": {
      "main": [
        []
      ]
    },
    "8071d1b9-8da3-4720-b1ee-0537f2c05262": {
      "main": [
        [
          {
            "node": "5339fdb6-b7aa-4da0-82f5-1f6e07d96246",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ee6394e8-31f1-4962-9c56-c7fa924e162c": {
      "main": [
        [
          {
            "node": "7d3d8ece-4dc4-484c-a6ba-11a706933de5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3de38f0e-371a-4e52-b6e4-2fa4aafa0306": {
      "main": [
        [
          {
            "node": "055677f0-bea4-4a65-b603-b918068e3fb2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0db94227-30d5-4cd5-8e00-1c538ebee9d7": {
      "main": [
        [
          {
            "node": "069bafd2-b669-4890-9dd5-417310aecfa5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "56809d81-55f3-45b5-baaa-116378c4bd25": {
      "main": [
        [
          {
            "node": "48463d2d-bba3-4d85-99a5-4ca217c91a3a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

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

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

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

고급 - 내부 위키, 멀티모달 AI

유료인가요?

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

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

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

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34