{
  "createdAt": "2026-02-28T12:43:46.770Z",
  "updatedAt": "2026-02-28T13:29:30.697Z",
  "id": "ty52rqOEJ6ZjNF2L",
  "name": "Blog Automation Pipeline A \u2014 Content Generation",
  "active": true,
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 1 * * *"
            }
          ]
        }
      },
      "id": "node-1-schedule",
      "name": "Schedule Trigger (01:00 AM)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "1VbyEQNuIAKpmTfk_5pTIjuLb3xlS-kdUfWflmfnFJSA",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "id"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "detectAutomatically"
            }
          }
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "\uc0c1\ud0dc",
              "lookupValue": "\ub300\uae30"
            }
          ]
        },
        "combineFilters": "AND"
      },
      "id": "node-2-sheets-read",
      "name": "Sheets Read (Status=\ub300\uae30)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        680,
        300
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://serpapi.com/search.json",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json['\ud0a4\uc6cc\ub4dc'] }}"
            },
            {
              "name": "location",
              "value": "South Korea"
            },
            {
              "name": "hl",
              "value": "ko"
            },
            {
              "name": "gl",
              "value": "kr"
            },
            {
              "name": "num",
              "value": "10"
            },
            {
              "name": "api_key",
              "value": "={{ $env.SERPAPI_KEY }}"
            }
          ]
        },
        "options": {}
      },
      "id": "node-3-serpapi",
      "name": "SerpAPI Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Parse SERP Data \u2014 SerpAPI \uacb0\uacfc \uad6c\uc870\ud654 \ucd94\ucd9c\n * Mode: runOnceForEachItem\n * \uc785\ub825: SerpAPI JSON \uc751\ub2f5 (\uac01 \uc544\uc774\ud15c)\n * \ucd9c\ub825: \uad6c\uc870\ud654\ub41c SERP \ud14d\uc2a4\ud2b8 (\ud504\ub86c\ud504\ud2b8\uc5d0 \uc0bd\uc785\uc6a9)\n */\n\nconst serpResults = $input.item.json;\n\n// \uacf5\uc2dd \ubb38\uc11c \ub3c4\uba54\uc778 \ub9ac\uc2a4\ud2b8\nconst OFFICIAL_DOMAINS = [\n  'learn.microsoft.com', 'docs.aws.amazon.com', 'cloud.google.com',\n  'developer.mozilla.org', 'docs.docker.com', 'kubernetes.io/docs',\n  'docs.github.com', 'developer.hashicorp.com', 'docs.ansible.com',\n  'docs.oracle.com', 'docs.redhat.com', 'wiki.archlinux.org',\n  'man7.org', 'nginx.org/en/docs', 'docs.python.org',\n  'go.dev/doc', 'docs.microsoft.com', 'cloud.google.com/docs',\n  'docs.anthropic.com', 'platform.openai.com/docs',\n];\n\n// 1. Organic Results (\uc0c1\uc704 7\uac1c) \u2014 \uc81c\ubaa9, \uc2a4\ub2c8\ud3ab, URL\nlet organicText = '';\nconst serpUrls = [];\nconst officialUrls = [];\nif (serpResults.organic_results && serpResults.organic_results.length > 0) {\n  const top7 = serpResults.organic_results.slice(0, 7);\n  organicText = top7.map((r, i) =>\n    `${i + 1}. ${r.title}\\n   ${r.snippet || '(\uc2a4\ub2c8\ud3ab \uc5c6\uc74c)'}\\n   URL: ${r.link || ''}`\n  ).join('\\n');\n  for (const r of top7) {\n    if (r.link) {\n      serpUrls.push(r.link);\n      if (OFFICIAL_DOMAINS.some(d => r.link.includes(d))) {\n        officialUrls.push({ url: r.link, title: r.title, snippet: r.snippet });\n      }\n    }\n  }\n}\n\n// 2. People Also Ask (\uad00\ub828 \uc9c8\ubb38, 5\uac1c) \u2014 FAQ \uc7ac\ub8cc\nlet paaText = '';\nif (serpResults.related_questions && serpResults.related_questions.length > 0) {\n  const top5 = serpResults.related_questions.slice(0, 5);\n  paaText = top5.map((q, i) =>\n    `${i + 1}. ${q.question}${q.snippet ? '\\n   \u2192 ' + q.snippet : ''}`\n  ).join('\\n');\n}\n\n// 3. Related Searches (\uad00\ub828 \uac80\uc0c9\uc5b4, 8\uac1c) \u2014 internal_link_keywords \ud6c4\ubcf4\nlet relatedText = '';\nif (serpResults.related_searches && serpResults.related_searches.length > 0) {\n  const top8 = serpResults.related_searches.slice(0, 8);\n  relatedText = top8.map(r => r.query).join(', ');\n}\n\n// 4. Knowledge Graph (\uc788\uc73c\uba74) \u2014 \uc815\uc758 \uc575\ucee4\nlet kgText = '';\nif (serpResults.knowledge_graph) {\n  const kg = serpResults.knowledge_graph;\n  kgText = `${kg.title || ''}: ${kg.description || kg.snippet || ''}`;\n  if (kg.source) {\n    kgText += ` (\ucd9c\ucc98: ${kg.source.name || ''})`;\n  }\n  // Extract knowledge graph URL\n  if (kg.source && kg.source.link) {\n    serpUrls.push(kg.source.link);\n  }\n}\n\n// \uad6c\uc870\ud654\ub41c SERP \ud14d\uc2a4\ud2b8 \uc870\ud569\nconst sections = [];\n\nif (organicText) {\n  sections.push(`### \uac80\uc0c9 \uc0c1\uc704 \ucf58\ud150\uce20 (\uc0c1\uc704 7\uac1c)\\n${organicText}`);\n}\nif (paaText) {\n  sections.push(`### People Also Ask (\uc0ac\ub78c\ub4e4\uc774 \uc790\uc8fc \ubb3b\ub294 \uc9c8\ubb38)\\n${paaText}`);\n}\nif (relatedText) {\n  sections.push(`### \uad00\ub828 \uac80\uc0c9\uc5b4\\n${relatedText}`);\n}\nif (kgText) {\n  sections.push(`### \uc9c0\uc2dd \uadf8\ub798\ud504 \uc815\uc758\\n${kgText}`);\n}\n\nconst serpText = sections.length > 0\n  ? sections.join('\\n\\n')\n  : '(SERP \ub370\uc774\ud130 \uc5c6\uc74c)';\n\n// \uc2dc\ud2b8 \ub370\uc774\ud130\ub97c \ud568\uaed8 \uc804\ub2ec\nconst sheetData = $('Sheets Read (Status=\ub300\uae30)').item.json;\n\nreturn {\n  json: {\n    ...sheetData,\n    serp_text: serpText,\n    serp_urls: serpUrls,\n    official_urls: officialUrls.slice(0, 3),\n    serp_organic_count: serpResults.organic_results ? serpResults.organic_results.length : 0,\n    serp_paa_count: serpResults.related_questions ? serpResults.related_questions.length : 0,\n    serp_related_count: serpResults.related_searches ? serpResults.related_searches.length : 0,\n    serp_has_kg: !!serpResults.knowledge_graph,\n  }\n};\n"
      },
      "id": "node-3b-parse-serp",
      "name": "Parse SERP Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1450,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Fetch Official Docs \u2014 \uacf5\uc2dd\ubb38\uc11c \ubcf8\ubb38 \ud06c\ub864\ub9c1\n * Mode: runOnceForEachItem\n * \uc785\ub825: Parse SERP Data \ucd9c\ub825 (official_urls \ud3ec\ud568)\n * \ucd9c\ub825: official_docs_text \ud544\ub4dc \ucd94\uac00\n *\n * Firecrawl API \ud0a4\uac00 \uc788\uc73c\uba74 /v1/scrape\ub85c \ub9c8\ud06c\ub2e4\uc6b4 \ucd94\ucd9c,\n * \uc5c6\uc73c\uba74 \uc9c1\uc811 GET + HTML \ud14d\uc2a4\ud2b8 \ucd94\ucd9c (fallback)\n */\n\nconst FIRECRAWL_KEY = $env.FIRECRAWL_API_KEY || '';\nconst officialUrls = $input.item.json.official_urls || [];\nconst MAX_DOCS = 3;\nconst MAX_CHARS_PER_DOC = 3000;\n\n/**\n * HTML\uc5d0\uc11c \ubcf8\ubb38 \ud14d\uc2a4\ud2b8 \ucd94\ucd9c (fallback\uc6a9)\n * <main>, <article>, <div role=\"main\"> \uc911 \uccab \ub9e4\uce6d \ud0dc\uadf8\uc758 \ud14d\uc2a4\ud2b8 \ucd94\ucd9c\n */\nfunction extractMainText(html) {\n  if (typeof html !== 'string') return '';\n\n  // main/article/div[role=main] \ud0dc\uadf8 \ub0b4\uc6a9 \ucd94\ucd9c \uc2dc\ub3c4\n  const patterns = [\n    /<main[^>]*>([\\s\\S]*?)<\\/main>/i,\n    /<article[^>]*>([\\s\\S]*?)<\\/article>/i,\n    /<div[^>]*role\\s*=\\s*[\"']main[\"'][^>]*>([\\s\\S]*?)<\\/div>/i,\n  ];\n\n  let bodyText = '';\n  for (const pattern of patterns) {\n    const match = html.match(pattern);\n    if (match && match[1]) {\n      bodyText = match[1];\n      break;\n    }\n  }\n\n  // \ub9e4\uce6d \uc2e4\ud328 \uc2dc <body> \uc804\uccb4 \uc0ac\uc6a9\n  if (!bodyText) {\n    const bodyMatch = html.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/i);\n    bodyText = bodyMatch ? bodyMatch[1] : html;\n  }\n\n  // HTML \ud0dc\uadf8 \uc81c\uac70 + \uc815\ub9ac\n  return bodyText\n    .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n    .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, '')\n    .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, '')\n    .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, '')\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&amp;/g, '&')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nconst docs = [];\n\nfor (const entry of officialUrls.slice(0, MAX_DOCS)) {\n  try {\n    let content = '';\n\n    if (FIRECRAWL_KEY) {\n      // Firecrawl API v1/scrape\n      const resp = await this.helpers.httpRequest({\n        method: 'POST',\n        url: 'https://api.firecrawl.dev/v1/scrape',\n        headers: {\n          'Authorization': `Bearer ${FIRECRAWL_KEY}`,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          url: entry.url,\n          formats: ['markdown'],\n          onlyMainContent: true,\n        }),\n        returnFullResponse: false,\n        timeout: 15000,\n      });\n\n      const parsed = typeof resp === 'string' ? JSON.parse(resp) : resp;\n      content = (parsed.data?.markdown || '').slice(0, MAX_CHARS_PER_DOC);\n    } else {\n      // Fallback: \uc9c1\uc811 GET + \ud14d\uc2a4\ud2b8 \ucd94\ucd9c\n      const html = await this.helpers.httpRequest({\n        method: 'GET',\n        url: entry.url,\n        timeout: 10000,\n        encoding: 'utf-8',\n        returnFullResponse: false,\n        headers: {\n          'User-Agent': 'Mozilla/5.0 (compatible; BlogBot/1.0)',\n          'Accept': 'text/html',\n        },\n      });\n      content = extractMainText(html).slice(0, MAX_CHARS_PER_DOC);\n    }\n\n    if (content.length > 200) {\n      docs.push(`### ${entry.title}\\nURL: ${entry.url}\\n\\n${content}`);\n    }\n  } catch (e) {\n    // \ud06c\ub864\ub9c1 \uc2e4\ud328 \uc2dc skip (SERP snippet\uc740 \uc774\ubbf8 \uc788\uc73c\ubbc0\ub85c \uce58\uba85\uc801\uc774\uc9c0 \uc54a\uc74c)\n  }\n}\n\nconst officialDocsText = docs.length > 0\n  ? docs.join('\\n\\n---\\n\\n')\n  : '';\n\nreturn {\n  json: {\n    ...$input.item.json,\n    official_docs_text: officialDocsText,\n  }\n};\n"
      },
      "id": "node-fetch-official-docs",
      "name": "Fetch Official Docs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1555,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Node 4: Route Prompt \u2014 \uc2dc\ud2b8 \ub370\uc774\ud130 + SERP \ub370\uc774\ud130 \uacb0\ud569\nconst inputData = $input.item.json;\nconst keyword = inputData['\ud0a4\uc6cc\ub4dc'] || '';\nconst category = inputData['\ucf58\ud150\uce20\uc720\ud615'] || 'IT \uae30\ucd08 \uc6a9\uc5b4';\nconst rowIndex = inputData['__row_index'] || inputData['row_number'] || 0;\n\n// Parse SERP Data\uc5d0\uc11c \uc804\ub2ec\ub41c \uad6c\uc870\ud654\ub41c SERP \ud14d\uc2a4\ud2b8\nconst serpSummary = inputData.serp_text || '(SERP \ub370\uc774\ud130 \uc5c6\uc74c)';\n\n// \ud504\ub86c\ud504\ud2b8 \uc720\ud615 \ubd84\uae30\nconst PROMPT_A = `# \ud504\ub86c\ud504\ud2b8 A: IT \uae30\ucd08 \uc6a9\uc5b4 \uc815\uc758\n\n\ub2f9\uc2e0\uc740 B2B IT \uc778\ud504\ub77c \uc804\ubb38 \uae30\uc220 \ube14\ub85c\uac70\uc785\ub2c8\ub2e4.\n\uc544\ub798 \ud0a4\uc6cc\ub4dc\uc5d0 \ub300\ud574 \ud55c\uad6d\uc5b4 \uae30\uc220 \ube14\ub85c\uadf8 \uae00\uc744 \uc791\uc131\ud558\uc138\uc694.\n\n## \uc791\uc131 \uaddc\uce59\n\n1. **\ub300\uc0c1 \ub3c5\uc790**: IT \uc778\ud504\ub77c \ucd08\ubcf4~\uc911\uae09 \uc2e4\ubb34\uc790 (\uae30\uc5c5 \ud658\uacbd)\n2. **\ud1a4**: \uc804\ubb38\uc801\uc774\ub418 \uc27d\uac8c \ud480\uc5b4\uc4f4 \uc124\uba85\uccb4\n3. **\ubd84\ub7c9**: \ubcf8\ubb38 \ucd5c\uc18c 3,000\uc790 \uc774\uc0c1 (\ub9c8\ud06c\ub2e4\uc6b4 \uae30\uc900, 4,000\uc790 \uad8c\uc7a5)\n4. **\uad6c\uc870**: H2/H3 \ud5e4\ub529 \ud65c\uc6a9, \ub17c\ub9ac\uc801 \ud750\ub984\n5. **\ub2e4\uc774\uc5b4\uadf8\ub7a8**: \uac01 H2 \uc139\uc158(FAQ \uc81c\uc678) \uc911 \uc2dc\uac01\ud654\uac00 \uc720\uc6a9\ud55c \uacf3\uc5d0 Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc744 \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 2\uac1c). **\ubc18\ub4dc\uc2dc [MERMAID]...[/MERMAID] \ub9c8\ucee4\ub85c \uac10\uc2f8\uc138\uc694** (\ud2b8\ub9ac\ud50c \ubc31\ud2f1 \\`\\`\\`mermaid \uc0ac\uc6a9 \uae08\uc9c0). \ub2e4\uc774\uc5b4\uadf8\ub7a8 \uc720\ud615\uc740 graph TD, sequenceDiagram, flowchart LR \ub4f1 \uc790\uc720\ub86d\uac8c \uc120\ud0dd\ud558\ub418, \ub178\ub4dc \ub808\uc774\ube14\uc740 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694.\n6. **\ucf54\ub4dc \uc608\uc2dc**: \uc124\uc815 \ud30c\uc77c, CLI \uba85\ub839\uc5b4, \uc2a4\ud06c\ub9bd\ud2b8 \ub4f1 \uc2e4\ubb34 \ucf54\ub4dc \uc608\uc2dc\ub97c 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ud558\uc138\uc694. \ucf54\ub4dc\ube14\ub85d\uc5d0\ub294 \ubc18\ub4dc\uc2dc \uc5b8\uc5b4 \ud0dc\uadf8(\\`\\`\\`yaml, \\`\\`\\`bash, \\`\\`\\`powershell \ub4f1)\ub97c \ubd99\uc774\uc138\uc694\n7. **\uc139\uc158 \uc774\ubbf8\uc9c0**: Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 \uc5c6\ub294 H2 \uc139\uc158 \uc911 \uc2dc\uac01\uc801 \ubcf4\uac15\uc774 \ud544\uc694\ud55c \uacf3\uc5d0 \\`<!-- IMAGE: \uc601\ubb38 \uac80\uc0c9 \ud0a4\uc6cc\ub4dc -->\\` \ub9c8\ucee4\ub97c \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 1\uac1c, \ucd5c\ub300 2\uac1c). \ud0a4\uc6cc\ub4dc\ub294 \ud574\ub2f9 \uc139\uc158 \uc8fc\uc81c\ub97c \uc601\ubb38 2~4\ub2e8\uc5b4\ub85c \ud45c\ud604 (\uc608: \"server rack data center\", \"network security firewall\"). FAQ \uc139\uc158\uc5d0\ub294 \uc0bd\uc785 \uae08\uc9c0.\n8. **\ucd5c\uc2e0\uc131 \ud544\uc218**:\n   - user \uba54\uc2dc\uc9c0\uc758 \"\uc791\uc131 \uae30\uc900\uc77c\"\uc744 \ud655\uc778\ud558\uace0 \ud574\ub2f9 \uc2dc\uc810 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"2024\ub144\", \"2023\ub144\" \ub4f1 \uacfc\uac70 \uc5f0\ub3c4 \ub370\uc774\ud130\ub294 \uc0ac\uc6a9\ud558\uc9c0 \ub9c8\uc138\uc694. \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4(user \uba54\uc2dc\uc9c0 \ucc38\uace0) \uae30\uc900 \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n   - \ubc84\uc804 \uc815\ubcf4, \uac00\uaca9, \uae30\ub2a5 \ube44\uad50\ub294 \ucd5c\uc2e0 \ubc84\uc804 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"\uacf5\uc2dd \ubb38\uc11c \ubcf8\ubb38\" \uc139\uc158\uc774 \uc81c\uacf5\ub418\uba74 SERP \uc2a4\ub2c8\ud3ab\ubcf4\ub2e4 \uc6b0\uc120\ud558\uc5ec \ucc38\uace0\ud558\uc138\uc694. \uacf5\uc2dd \ubb38\uc11c\uc758 \uc815\ud655\ud55c \uc6a9\uc5b4, \ubc84\uc804, \uae30\ub2a5 \uc124\uba85\uc744 \uadf8\ub300\ub85c \ubc18\uc601\ud558\uc138\uc694\n   - \uc81c\ubaa9\uc5d0 \uc5f0\ub3c4\ub97c \ud3ec\ud568\ud560 \ub54c\ub294 \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n\n## \ud544\uc218 \uc139\uc158 \uad6c\uc131 (H2 \uae30\uc900)\n\n| \uc139\uc158 (H2) | \ucd5c\uc18c \uae00\uc790 \uc218 | \ud544\uc218 \ub0b4\uc6a9 |\n|---|---|---|\n| {\ud0a4\uc6cc\ub4dc}\ub780? | 300\uc790 | \ud575\uc2ec \uc815\uc758 \ud55c \ubb38\uc7a5 + \ubd80\uc5f0 \uc124\uba85 2~3\ubb38\uc7a5, \uc65c \uc911\uc694\ud55c\uc9c0 \ub9e5\ub77d |\n| \uc65c \uc911\uc694\ud55c\uc9c0 (Why) | 400\uc790 | \uc2e4\ubb34 \uc2dc\ub098\ub9ac\uc624 2\uac1c \uc774\uc0c1, \ub3c4\uc785 \uc804/\ud6c4 \ube44\uad50 \ub610\ub294 \ubbf8\uc0ac\uc6a9 \uc2dc \ub9ac\uc2a4\ud06c |\n| \uc791\ub3d9 \uc6d0\ub9ac | 500\uc790 | 3~5\ub2e8\uacc4\ub85c \uc124\uba85, \uac01 \ub2e8\uacc4\ub9c8\ub2e4 \uae30\uc220\uc801 \uc138\ubd80\uc0ac\ud56d \ud3ec\ud568 |\n| \uc2e4\ubb34 \uc801\uc6a9 \uac00\uc774\ub4dc | 400\uc790 | \uad6c\uccb4\uc801 \ub3c4\uad6c\uba85, \uc124\uc815\uac12, CLI \uba85\ub839\uc5b4, \ud658\uacbd\ubcc4(\uc628\ud504\ub808\ubbf8\uc2a4/\ud074\ub77c\uc6b0\ub4dc) \ucc28\uc774 |\n| \uae30\uc5c5 \ud658\uacbd \uc801\uc6a9 \uc0ac\ub840 | 300\uc790 | AD/Azure AD/AWS \ub4f1 \uad6c\uccb4\uc801 \uc2dc\ub098\ub9ac\uc624 2~3\uac1c, \uc2e4\ubb34 \uc608\uc2dc |\n| \uc7a5\uc810\uacfc \ud55c\uacc4 | 300\uc790 | \uc7a5\uc810 \ud14c\uc774\ube14\uacfc \ud55c\uacc4 \ud14c\uc774\ube14\uc744 **\ubcc4\ub3c4\ub85c \ubd84\ub9ac** (\uac01 3\ud589 \uc774\uc0c1), \ube44\uad50\ud45c \ub610\ub294 \uccb4\ud06c\ub9ac\uc2a4\ud2b8 1\uac1c \uc774\uc0c1 |\n| FAQ | 200\uc790 | 3\uac1c \uc774\uc0c1, \uac01 \ub2f5\ubcc0 80\uc790 \uc774\uc0c1 |\n\n## SERP \ub370\uc774\ud130 \ud65c\uc6a9 \uc9c0\uce68\n\n- \uc81c\uacf5\ub41c SERP \ub370\uc774\ud130(\uac80\uc0c9 \uc2a4\ub2c8\ud3ab, People Also Ask, \uad00\ub828 \uac80\uc0c9\uc5b4)\ub97c \ucc38\uace0\ud558\uc5ec \ucf58\ud150\uce20 \uae4a\uc774\ub97c \ud655\ubcf4\ud558\uc138\uc694\n- People Also Ask \uc9c8\ubb38\uc740 FAQ \uc139\uc158\uc5d0 \ubc18\uc601\ud558\uc138\uc694\n- \uad00\ub828 \uac80\uc0c9\uc5b4\ub294 \ubcf8\ubb38\uc5d0\uc11c \uc790\uc5f0\uc2a4\ub7fd\uac8c \uc5b8\uae09\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \ucf58\ud150\uce20\uc5d0\uc11c \ub2e4\ub8e8\ub294 \uc8fc\uc81c\ub97c \ube60\uc9d0\uc5c6\uc774 \ucee4\ubc84\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \uacb0\uacfc\ub97c \ucc38\uace0\ud558\ub418 \uadf8\ub300\ub85c \ubcf5\uc0ac\ud558\uc9c0 \ub9d0 \uac83\n- \uc778\ub77c\uc778 \ucd9c\ucc98\ub294 '[\ucd9c\ucc98\uba85](URL)' \ud615\ud0dc\ub85c \uc0bd\uc785\ud558\uc138\uc694\n\n## \ucc38\uc870 URL \uaddc\uce59 (\ud544\uc218)\n\n- **references \ud544\ub4dc**: \ubc18\ub4dc\uc2dc SERP \ub370\uc774\ud130\uc5d0 \ud3ec\ud568\ub41c \uc2e4\uc81c URL\ub9cc \uc0ac\uc6a9\ud558\uc138\uc694. URL\uc744 \ucd94\uce21\ud558\uac70\ub098 \uae30\uc5b5\uc5d0 \uc758\uc874\ud558\uc5ec \uc791\uc131\ud558\uc9c0 \ub9c8\uc138\uc694\n- **\uc778\ub77c\uc778 \ucd9c\ucc98 URL**: SERP \ub370\uc774\ud130\uc758 \"URL:\" \ud56d\ubaa9\uc5d0\uc11c \uac00\uc838\uc624\uc138\uc694. user \uba54\uc2dc\uc9c0\uc758 \"\ucc38\uc870 \uac00\ub2a5 URL \ud480\" \uc139\uc158 \ucc38\uace0\n- **\uacf5\uc2dd \ubb38\uc11c \uc6b0\uc120**: SERP URL \uc911 \uacf5\uc2dd \ubb38\uc11c \ub3c4\uba54\uc778(learn.microsoft.com, docs.aws.amazon.com, cloud.google.com, developer.mozilla.org \ub4f1)\uc774 \uc788\uc73c\uba74 \uc6b0\uc120 \uc0ac\uc6a9\ud558\uc138\uc694\n- **URL\uc744 \ubaa8\ub97c \ub54c**: URL \uc5c6\uc774 \ucd9c\ucc98\uba85\ub9cc \ud14d\uc2a4\ud2b8\ub85c \uae30\uc7ac\ud558\uc138\uc694 (\uc608: \"Microsoft Learn \uacf5\uc2dd \ubb38\uc11c \ucc38\uace0\"). \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 URL\uc744 \ub9cc\ub4e4\uc5b4\ub0b4\uc9c0 \ub9c8\uc138\uc694\n\n## \ud488\uc9c8 \uc790\uac00 \uac80\uc99d \uccb4\ud06c\ub9ac\uc2a4\ud2b8\n\n\uc791\uc131 \uc644\ub8cc \ud6c4 \uc544\ub798 \ud56d\ubaa9\uc744 \ud655\uc778\ud558\uc138\uc694:\n- [ ] H2 \ud5e4\ub529\uc774 4\uac1c \uc774\uc0c1\uc778\uac00?\n- [ ] \ub9c8\ud06c\ub2e4\uc6b4 \ud14c\uc774\ube14\uc774 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] FAQ\uac00 3\uac1c \uc774\uc0c1\uc774\uace0 \uac01 \ub2f5\ubcc0\uc774 80\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc804\uccb4 \ubcf8\ubb38\uc774 3,000\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc2e4\ubb34 \uc2dc\ub098\ub9ac\uc624\uac00 2\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uc2e4\ubb34 \uc801\uc6a9 \uac00\uc774\ub4dc\uc5d0 \uad6c\uccb4\uc801 \ub3c4\uad6c\uba85/\uba85\ub839\uc5b4\uac00 \uc788\ub294\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98\uac00 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uae30\uc5c5 \ud658\uacbd \uc801\uc6a9 \uc0ac\ub840\uac00 \uad6c\uccb4\uc801\uc778\uac00?\n- [ ] internal_link_keywords\uc5d0 \ud574\ub2f9\ud558\ub294 \ud0a4\uc6cc\ub4dc\uac00 \ubcf8\ubb38\uc5d0\uc11c **\uad75\uac8c** \ud45c\uc2dc\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] [MERMAID]...[/MERMAID] \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 2\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] Mermaid \ub178\ub4dc \ub808\uc774\ube14\uc774 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \ucf54\ub4dc\ube14\ub85d\uc774 1\uac1c \uc774\uc0c1\uc774\uace0, \ubaa8\ub450 \uc5b8\uc5b4 \ud0dc\uadf8\uac00 \uc788\ub294\uac00?\n- [ ] \uc7a5\uc810 \ud14c\uc774\ube14\uacfc \ud55c\uacc4 \ud14c\uc774\ube14\uc774 \ubd84\ub9ac\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \\`<!-- IMAGE: ... -->\\` \ub9c8\ucee4\uac00 1~2\uac1c \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00? (FAQ/Mermaid \uc139\uc158 \uc81c\uc678)\n- [ ] references \ud544\ub4dc\uc758 \ubaa8\ub4e0 URL\uc774 SERP \ub370\uc774\ud130\uc5d0\uc11c \uac00\uc838\uc628 \uac83\uc778\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98 URL\uc774 \uc2e4\uc81c \uc874\uc7ac\ud558\ub294 URL\uc778\uac00? (\ucd94\uce21 URL \uc0ac\uc6a9 \uae08\uc9c0)\n\n## \uae08\uc9c0 \uc0ac\ud56d\n\n- \uc2e4\uc81c \uae30\uc5c5\uba85\uc740 \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub418, \uac00\uc0c1 \uc2dc\ub098\ub9ac\uc624\uc5d0\uc11c\ub294 'example-corp.com' \ub4f1 \uc124\uba85\uc801 \ub3c4\uba54\uc778\uba85 \uc0ac\uc6a9\n- \ube44\ud655\uc778 \uc815\ubcf4 \uc791\uc131 \uae08\uc9c0\n\n## \ucd9c\ub825 \ud615\uc2dd\n\n\ubc18\ub4dc\uc2dc \uc544\ub798 JSON \ud615\uc2dd\uc73c\ub85c\ub9cc \uc751\ub2f5\ud558\uc138\uc694. JSON \uc678 \ud14d\uc2a4\ud2b8\ub97c \ud3ec\ud568\ud558\uc9c0 \ub9c8\uc138\uc694.\n\n\\`\\`\\`json\n{\n  \"title\": \"SEO \ucd5c\uc801\ud654\ub41c \uc81c\ubaa9 (50~60\uc790)\",\n  \"meta_description\": \"120~155\uc790 \uba54\ud0c0 \uc124\uba85 (\ud0a4\uc6cc\ub4dc \uc790\uc5f0 \ud3ec\ud568)\",\n  \"faq_schema\": [\n    {\"question\": \"\uc9c8\ubb381\", \"answer\": \"\ub2f5\ubcc01 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb382\", \"answer\": \"\ub2f5\ubcc02 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb383\", \"answer\": \"\ub2f5\ubcc03 (80\uc790 \uc774\uc0c1)\"}\n  ],\n  \"references\": [\"\ucc38\uace0\uc790\ub8cc URL \ub610\ub294 \ubb38\uc11c\uba85\"],\n  \"tags\": [\"\ud0dc\uadf81\", \"\ud0dc\uadf82\", \"\ud0dc\uadf83\", \"\ud0dc\uadf84\", \"\ud0dc\uadf85\"],\n  \"internal_link_keywords\": [\"\uad00\ub828 \ud0a4\uc6cc\ub4dc1\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc2\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc3\"],\n  \"content\": \"\ub9c8\ud06c\ub2e4\uc6b4 \ubcf8\ubb38 \uc804\uccb4 (H2/H3 \ud5e4\ub529, \ud45c, \ucf54\ub4dc \ube14\ub85d \ud3ec\ud568, \ucd5c\uc18c 3000\uc790 \uc774\uc0c1)\"\n}\n\\`\\`\\``;\nconst PROMPT_B = `# \ud504\ub86c\ud504\ud2b8 B: IT \ud2b8\ub80c\ub4dc/\ube44\uad50 \ubd84\uc11d\n\n\ub2f9\uc2e0\uc740 B2B IT \uc778\ud504\ub77c \uc804\ubb38 \uae30\uc220 \ube14\ub85c\uac70\uc785\ub2c8\ub2e4.\n\uc544\ub798 \ud0a4\uc6cc\ub4dc\uc5d0 \ub300\ud574 \ube44\uad50 \ubd84\uc11d \uae30\uc220 \ube14\ub85c\uadf8 \uae00\uc744 \uc791\uc131\ud558\uc138\uc694.\n\n## \uc791\uc131 \uaddc\uce59\n\n1. **\ub300\uc0c1 \ub3c5\uc790**: IT \uc778\ud504\ub77c \uc758\uc0ac\uacb0\uc815\uc790 \ubc0f \uc2e4\ubb34\uc790\n2. **\ud1a4**: \uac1d\uad00\uc801 \ube44\uad50 \ubd84\uc11d, \uc911\ub9bd\uc801 \uc2dc\uac01\n3. **\ubd84\ub7c9**: \ubcf8\ubb38 \ucd5c\uc18c 3,000\uc790 \uc774\uc0c1 (\ub9c8\ud06c\ub2e4\uc6b4 \uae30\uc900, 4,000\uc790 \uad8c\uc7a5)\n4. **\uad6c\uc870**: \uac1c\uc694 \u2192 \uac01 \uae30\uc220 \uc124\uba85 \u2192 \uc2ec\uce35 \ube44\uad50 \ubd84\uc11d \u2192 \uc120\ud0dd \uac00\uc774\ub4dc \u2192 \ub9c8\uc774\uadf8\ub808\uc774\uc158/\ub3c4\uc785 \uc2dc\ub098\ub9ac\uc624 \u2192 FAQ\n5. **\ub2e4\uc774\uc5b4\uadf8\ub7a8**: \uac01 H2 \uc139\uc158(FAQ \uc81c\uc678) \uc911 \uc2dc\uac01\ud654\uac00 \uc720\uc6a9\ud55c \uacf3\uc5d0 Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc744 \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 2\uac1c). **\ubc18\ub4dc\uc2dc [MERMAID]...[/MERMAID] \ub9c8\ucee4\ub85c \uac10\uc2f8\uc138\uc694** (\ud2b8\ub9ac\ud50c \ubc31\ud2f1 \\`\\`\\`mermaid \uc0ac\uc6a9 \uae08\uc9c0). \ub2e4\uc774\uc5b4\uadf8\ub7a8 \uc720\ud615\uc740 graph TD, sequenceDiagram, flowchart LR \ub4f1 \uc790\uc720\ub86d\uac8c \uc120\ud0dd\ud558\ub418, \ub178\ub4dc \ub808\uc774\ube14\uc740 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694.\n6. **\ucf54\ub4dc \uc608\uc2dc**: \uac01 \uae30\uc220\uc758 \uc124\uc815/\uc0ac\uc6a9 \uc608\uc2dc \ucf54\ub4dc\ub97c 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ud558\uc138\uc694. \ucf54\ub4dc\ube14\ub85d\uc5d0\ub294 \ubc18\ub4dc\uc2dc \uc5b8\uc5b4 \ud0dc\uadf8(\\`\\`\\`yaml, \\`\\`\\`bash \ub4f1)\ub97c \ubd99\uc774\uc138\uc694\n7. **\uc139\uc158 \uc774\ubbf8\uc9c0**: Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 \uc5c6\ub294 H2 \uc139\uc158 \uc911 \uc2dc\uac01\uc801 \ubcf4\uac15\uc774 \ud544\uc694\ud55c \uacf3\uc5d0 \\`<!-- IMAGE: \uc601\ubb38 \uac80\uc0c9 \ud0a4\uc6cc\ub4dc -->\\` \ub9c8\ucee4\ub97c \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 1\uac1c, \ucd5c\ub300 2\uac1c). \ud0a4\uc6cc\ub4dc\ub294 \ud574\ub2f9 \uc139\uc158 \uc8fc\uc81c\ub97c \uc601\ubb38 2~4\ub2e8\uc5b4\ub85c \ud45c\ud604 (\uc608: \"server rack data center\", \"network security firewall\"). FAQ \uc139\uc158\uc5d0\ub294 \uc0bd\uc785 \uae08\uc9c0.\n8. **\ucd5c\uc2e0\uc131 \ud544\uc218**:\n   - user \uba54\uc2dc\uc9c0\uc758 \"\uc791\uc131 \uae30\uc900\uc77c\"\uc744 \ud655\uc778\ud558\uace0 \ud574\ub2f9 \uc2dc\uc810 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"2024\ub144\", \"2023\ub144\" \ub4f1 \uacfc\uac70 \uc5f0\ub3c4 \ub370\uc774\ud130\ub294 \uc0ac\uc6a9\ud558\uc9c0 \ub9c8\uc138\uc694. \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4(user \uba54\uc2dc\uc9c0 \ucc38\uace0) \uae30\uc900 \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n   - \ubc84\uc804 \uc815\ubcf4, \uac00\uaca9, \uae30\ub2a5 \ube44\uad50\ub294 \ucd5c\uc2e0 \ubc84\uc804 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"\uacf5\uc2dd \ubb38\uc11c \ubcf8\ubb38\" \uc139\uc158\uc774 \uc81c\uacf5\ub418\uba74 SERP \uc2a4\ub2c8\ud3ab\ubcf4\ub2e4 \uc6b0\uc120\ud558\uc5ec \ucc38\uace0\ud558\uc138\uc694. \uacf5\uc2dd \ubb38\uc11c\uc758 \uc815\ud655\ud55c \uc6a9\uc5b4, \ubc84\uc804, \uae30\ub2a5 \uc124\uba85\uc744 \uadf8\ub300\ub85c \ubc18\uc601\ud558\uc138\uc694\n   - \uc81c\ubaa9\uc5d0 \uc5f0\ub3c4\ub97c \ud3ec\ud568\ud560 \ub54c\ub294 \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n\n## \ud544\uc218 \uc139\uc158 \uad6c\uc131 (H2 \uae30\uc900)\n\n| \uc139\uc158 (H2) | \ucd5c\uc18c \uae00\uc790 \uc218 | \ud544\uc218 \ub0b4\uc6a9 |\n|---|---|---|\n| \uac1c\uc694 | 300\uc790 | \ud575\uc2ec \ucc28\uc774 \uc694\uc57d 3~4\ubb38\uc7a5, \uc77d\uc5b4\uc57c \ud560 \uc774\uc720, \ube44\uad50 \ud310\ub2e8\uc5d0 \ud544\uc694\ud55c \uc0ac\uc804 \uc9c0\uc2dd |\n| {\uae30\uc220A} \uc0c1\uc138 | 500\uc790 | \uc815\uc758, \uc544\ud0a4\ud14d\ucc98 \uac1c\uc694, \ud575\uc2ec \uae30\ub2a5 3\uac00\uc9c0 \uc774\uc0c1, \ub300\ud45c \uc0ac\uc6a9 \uc0ac\ub840, \uc2e4\uc81c \uc124\uc815/\uad6c\uc131 \uc608\uc2dc |\n| {\uae30\uc220B} \uc0c1\uc138 | 500\uc790 | \uc815\uc758, \uc544\ud0a4\ud14d\ucc98 \uac1c\uc694, \ud575\uc2ec \uae30\ub2a5 3\uac00\uc9c0 \uc774\uc0c1, \ub300\ud45c \uc0ac\uc6a9 \uc0ac\ub840, \uc2e4\uc81c \uc124\uc815/\uad6c\uc131 \uc608\uc2dc |\n| \uc2ec\uce35 \ube44\uad50 \ubd84\uc11d | \u2014 | \ube44\uad50\ud45c 10\ud589 \uc774\uc0c1 (\uae30\ub2a5/\uc131\ub2a5/\ube44\uc6a9/\ud655\uc7a5\uc131/\ubcf4\uc548/\uad00\ub9ac \ud3b8\uc758\uc131/\ub3c4\uc785 \ub09c\uc774\ub3c4/\ud559\uc2b5\uace1\uc120/\uc0dd\ud0dc\uacc4/\ucee4\ubba4\ub2c8\ud2f0), \uac01 \ud56d\ubaa9\uc5d0 1~2\ubb38\uc7a5 \ud574\uc11d |\n| A\ub97c \uc120\ud0dd\ud574\uc57c \ud560 \ub54c vs B\ub97c \uc120\ud0dd\ud574\uc57c \ud560 \ub54c | 400\uc790 | \uad6c\uccb4\uc801 \uc2dc\ub098\ub9ac\uc624 \uae30\ubc18 \ucd94\ucc9c (SMB/Enterprise/\uc2a4\ud0c0\ud2b8\uc5c5), \uc6cc\ud06c\ub85c\ub4dc \uc720\ud615\ubcc4 \ucd94\ucc9c |\n| \ub9c8\uc774\uadf8\ub808\uc774\uc158/\ub3c4\uc785 \uc2dc\ub098\ub9ac\uc624 | 300\uc790 | A\u2192B \ub610\ub294 B\u2192A \uc804\ud658 \uc2dc \uace0\ub824\uc0ac\ud56d, \ub2e8\uacc4\ubcc4 \ub3c4\uc785 \ub85c\ub4dc\ub9f5, \uc608\uc0c1 \uc18c\uc694 \uae30\uac04 |\n| FAQ | 200\uc790 | 3\uac1c \uc774\uc0c1, \uac01 \ub2f5\ubcc0 80\uc790 \uc774\uc0c1 |\n\n## SERP \ub370\uc774\ud130 \ud65c\uc6a9 \uc9c0\uce68\n\n- \uc81c\uacf5\ub41c SERP \ub370\uc774\ud130(\uac80\uc0c9 \uc2a4\ub2c8\ud3ab, People Also Ask, \uad00\ub828 \uac80\uc0c9\uc5b4)\ub97c \ucc38\uace0\ud558\uc5ec \ucf58\ud150\uce20 \uae4a\uc774\ub97c \ud655\ubcf4\ud558\uc138\uc694\n- People Also Ask \uc9c8\ubb38\uc740 FAQ \uc139\uc158\uc5d0 \ubc18\uc601\ud558\uc138\uc694\n- \uad00\ub828 \uac80\uc0c9\uc5b4\ub294 \ubcf8\ubb38\uc5d0\uc11c \uc790\uc5f0\uc2a4\ub7fd\uac8c \uc5b8\uae09\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \ucf58\ud150\uce20\uc5d0\uc11c \ub2e4\ub8e8\ub294 \ube44\uad50 \uad00\uc810\uc744 \ube60\uc9d0\uc5c6\uc774 \ucee4\ubc84\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \uacb0\uacfc\ub97c \ucc38\uace0\ud558\ub418 \uadf8\ub300\ub85c \ubcf5\uc0ac\ud558\uc9c0 \ub9d0 \uac83\n- \uc778\ub77c\uc778 \ucd9c\ucc98\ub294 '[\ucd9c\ucc98\uba85](URL)' \ud615\ud0dc\ub85c \uc0bd\uc785\ud558\uc138\uc694\n\n## \ucc38\uc870 URL \uaddc\uce59 (\ud544\uc218)\n\n- **references \ud544\ub4dc**: \ubc18\ub4dc\uc2dc SERP \ub370\uc774\ud130\uc5d0 \ud3ec\ud568\ub41c \uc2e4\uc81c URL\ub9cc \uc0ac\uc6a9\ud558\uc138\uc694. URL\uc744 \ucd94\uce21\ud558\uac70\ub098 \uae30\uc5b5\uc5d0 \uc758\uc874\ud558\uc5ec \uc791\uc131\ud558\uc9c0 \ub9c8\uc138\uc694\n- **\uc778\ub77c\uc778 \ucd9c\ucc98 URL**: SERP \ub370\uc774\ud130\uc758 \"URL:\" \ud56d\ubaa9\uc5d0\uc11c \uac00\uc838\uc624\uc138\uc694. user \uba54\uc2dc\uc9c0\uc758 \"\ucc38\uc870 \uac00\ub2a5 URL \ud480\" \uc139\uc158 \ucc38\uace0\n- **\uacf5\uc2dd \ubb38\uc11c \uc6b0\uc120**: SERP URL \uc911 \uacf5\uc2dd \ubb38\uc11c \ub3c4\uba54\uc778(learn.microsoft.com, docs.aws.amazon.com, cloud.google.com, developer.mozilla.org \ub4f1)\uc774 \uc788\uc73c\uba74 \uc6b0\uc120 \uc0ac\uc6a9\ud558\uc138\uc694\n- **URL\uc744 \ubaa8\ub97c \ub54c**: URL \uc5c6\uc774 \ucd9c\ucc98\uba85\ub9cc \ud14d\uc2a4\ud2b8\ub85c \uae30\uc7ac\ud558\uc138\uc694 (\uc608: \"Microsoft Learn \uacf5\uc2dd \ubb38\uc11c \ucc38\uace0\"). \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 URL\uc744 \ub9cc\ub4e4\uc5b4\ub0b4\uc9c0 \ub9c8\uc138\uc694\n\n## \ud488\uc9c8 \uc790\uac00 \uac80\uc99d \uccb4\ud06c\ub9ac\uc2a4\ud2b8\n\n\uc791\uc131 \uc644\ub8cc \ud6c4 \uc544\ub798 \ud56d\ubaa9\uc744 \ud655\uc778\ud558\uc138\uc694:\n- [ ] H2 \ud5e4\ub529\uc774 6\uac1c \uc774\uc0c1\uc778\uac00?\n- [ ] \uc2ec\uce35 \ube44\uad50\ud45c\uac00 10\ud589 \uc774\uc0c1\uc778\uac00?\n- [ ] \uac01 \uae30\uc220 \uc124\uba85\uc774 500\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] FAQ\uac00 3\uac1c \uc774\uc0c1\uc774\uace0 \uac01 \ub2f5\ubcc0\uc774 80\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc804\uccb4 \ubcf8\ubb38\uc774 3,000\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc120\ud0dd \uac00\uc774\ub4dc\uc5d0 \uae30\uc5c5 \uaddc\ubaa8\ubcc4(SMB/Enterprise/\uc2a4\ud0c0\ud2b8\uc5c5) \ucd94\ucc9c\uc774 \uc788\ub294\uac00?\n- [ ] \ub9c8\uc774\uadf8\ub808\uc774\uc158/\ub3c4\uc785 \uc2dc\ub098\ub9ac\uc624\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98\uac00 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uc6cc\ud06c\ub85c\ub4dc \uc720\ud615\ubcc4 \ucd94\ucc9c\uc774 \uc788\ub294\uac00?\n- [ ] internal_link_keywords\uc5d0 \ud574\ub2f9\ud558\ub294 \ud0a4\uc6cc\ub4dc\uac00 \ubcf8\ubb38\uc5d0\uc11c **\uad75\uac8c** \ud45c\uc2dc\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] [MERMAID]...[/MERMAID] \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 2\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] Mermaid \ub178\ub4dc \ub808\uc774\ube14\uc774 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uac01 \uae30\uc220\ubcc4 \ucf54\ub4dc \uc608\uc2dc\uac00 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \\`<!-- IMAGE: ... -->\\` \ub9c8\ucee4\uac00 1~2\uac1c \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00? (FAQ/Mermaid \uc139\uc158 \uc81c\uc678)\n- [ ] references \ud544\ub4dc\uc758 \ubaa8\ub4e0 URL\uc774 SERP \ub370\uc774\ud130\uc5d0\uc11c \uac00\uc838\uc628 \uac83\uc778\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98 URL\uc774 \uc2e4\uc81c \uc874\uc7ac\ud558\ub294 URL\uc778\uac00? (\ucd94\uce21 URL \uc0ac\uc6a9 \uae08\uc9c0)\n\n## \uae08\uc9c0 \uc0ac\ud56d\n\n- \uc2e4\uc81c \uae30\uc5c5\uba85\uc740 \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub418, \uac00\uc0c1 \uc2dc\ub098\ub9ac\uc624\uc5d0\uc11c\ub294 'example-corp.com' \ub4f1 \uc124\uba85\uc801 \ub3c4\uba54\uc778\uba85 \uc0ac\uc6a9\n- \ud2b9\uc815 \ubca4\ub354 \ud3b8\ud5a5 \uae08\uc9c0\n\n## \ucd9c\ub825 \ud615\uc2dd\n\n\ubc18\ub4dc\uc2dc \uc544\ub798 JSON \ud615\uc2dd\uc73c\ub85c\ub9cc \uc751\ub2f5\ud558\uc138\uc694. JSON \uc678 \ud14d\uc2a4\ud2b8\ub97c \ud3ec\ud568\ud558\uc9c0 \ub9c8\uc138\uc694.\n\n\\`\\`\\`json\n{\n  \"title\": \"SEO \ucd5c\uc801\ud654\ub41c \ube44\uad50 \uc81c\ubaa9 (50~60\uc790, 'vs' \ub610\ub294 '\ube44\uad50' \ud3ec\ud568)\",\n  \"meta_description\": \"120~155\uc790 \uba54\ud0c0 \uc124\uba85 (\ub450 \uae30\uc220\uba85 + \ube44\uad50 \ud0a4\uc6cc\ub4dc \ud3ec\ud568)\",\n  \"faq_schema\": [\n    {\"question\": \"\uc9c8\ubb381\", \"answer\": \"\ub2f5\ubcc01 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb382\", \"answer\": \"\ub2f5\ubcc02 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb383\", \"answer\": \"\ub2f5\ubcc03 (80\uc790 \uc774\uc0c1)\"}\n  ],\n  \"references\": [\"\ucc38\uace0\uc790\ub8cc URL \ub610\ub294 \ubb38\uc11c\uba85\"],\n  \"tags\": [\"\ud0dc\uadf81\", \"\ud0dc\uadf82\", \"\ud0dc\uadf83\", \"\ud0dc\uadf84\", \"\ud0dc\uadf85\"],\n  \"internal_link_keywords\": [\"\uad00\ub828 \ud0a4\uc6cc\ub4dc1\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc2\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc3\"],\n  \"content\": \"\ub9c8\ud06c\ub2e4\uc6b4 \ubcf8\ubb38 \uc804\uccb4 (H2/H3 \ud5e4\ub529, \ube44\uad50\ud45c, \uc120\ud0dd \uac00\uc774\ub4dc \ud3ec\ud568, \ucd5c\uc18c 3000\uc790 \uc774\uc0c1)\"\n}\n\\`\\`\\``;\nconst PROMPT_C = `# \ud504\ub86c\ud504\ud2b8 C: \uc2e4\ubb34 \ud2b8\ub7ec\ube14\uc288\ud305 \uac00\uc774\ub4dc\n\n\ub2f9\uc2e0\uc740 B2B IT \uc778\ud504\ub77c \uc804\ubb38 \uae30\uc220 \ube14\ub85c\uac70\uc785\ub2c8\ub2e4.\n\uc544\ub798 \uc5d0\ub7ec/\uc774\uc288\uc5d0 \ub300\ud574 \ud2b8\ub7ec\ube14\uc288\ud305 \uac00\uc774\ub4dc \ube14\ub85c\uadf8 \uae00\uc744 \uc791\uc131\ud558\uc138\uc694.\n\n## \uc791\uc131 \uaddc\uce59\n\n1. **\ub300\uc0c1 \ub3c5\uc790**: \ud574\ub2f9 \uc5d0\ub7ec\ub97c \uac80\uc0c9\ud558\ub294 IT \uad00\ub9ac\uc790/\uc5d4\uc9c0\ub2c8\uc5b4\n2. **\ud1a4**: \uc2e4\ubb34 \uce5c\ud654\uc801, \ub2e8\uacc4\ubcc4 \ud574\uacb0 \uac00\uc774\ub4dc\n3. **\ubd84\ub7c9**: \ubcf8\ubb38 \ucd5c\uc18c 3,000\uc790 \uc774\uc0c1 (\ub9c8\ud06c\ub2e4\uc6b4 \uae30\uc900, 4,500\uc790 \uad8c\uc7a5)\n4. **\uad6c\uc870**: \uc5d0\ub7ec \ud604\uc0c1 \u2192 \uc6d0\uc778 \ubd84\uc11d \u2192 \ud574\uacb0 \ubc29\ubc95 (\ub2e8\uacc4\ubcc4) \u2192 \uc608\ubc29 \uc870\uce58 \u2192 \uad00\ub828 \uc5d0\ub7ec/\ucc38\uace0 \uc790\ub8cc \u2192 FAQ\n5. **\ub2e4\uc774\uc5b4\uadf8\ub7a8**: \uac01 H2 \uc139\uc158(FAQ \uc81c\uc678) \uc911 \uc2dc\uac01\ud654\uac00 \uc720\uc6a9\ud55c \uacf3\uc5d0 Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc744 \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 2\uac1c). **\ubc18\ub4dc\uc2dc [MERMAID]...[/MERMAID] \ub9c8\ucee4\ub85c \uac10\uc2f8\uc138\uc694** (\ud2b8\ub9ac\ud50c \ubc31\ud2f1 \\`\\`\\`mermaid \uc0ac\uc6a9 \uae08\uc9c0). \ub2e4\uc774\uc5b4\uadf8\ub7a8 \uc720\ud615\uc740 graph TD, sequenceDiagram, flowchart LR \ub4f1 \uc790\uc720\ub86d\uac8c \uc120\ud0dd\ud558\ub418, \ub178\ub4dc \ub808\uc774\ube14\uc740 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694.\n6. **\ucf54\ub4dc\ube14\ub85d \uc5b8\uc5b4 \ud0dc\uadf8**: \ubaa8\ub4e0 \ucf54\ub4dc\ube14\ub85d\uc5d0 \ubc18\ub4dc\uc2dc \uc5b8\uc5b4 \ud0dc\uadf8(\\`\\`\\`powershell, \\`\\`\\`bash, \\`\\`\\`yaml \ub4f1)\ub97c \ubd99\uc774\uc138\uc694. \uc2e4\ud589 \uc804\ud6c4 \ud655\uc778 \uba85\ub839\uc5b4\ub3c4 \ucf54\ub4dc\ube14\ub85d\uc73c\ub85c \uac10\uc2f8\uc138\uc694\n7. **\uc139\uc158 \uc774\ubbf8\uc9c0**: Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 \uc5c6\ub294 H2 \uc139\uc158 \uc911 \uc2dc\uac01\uc801 \ubcf4\uac15\uc774 \ud544\uc694\ud55c \uacf3\uc5d0 \\`<!-- IMAGE: \uc601\ubb38 \uac80\uc0c9 \ud0a4\uc6cc\ub4dc -->\\` \ub9c8\ucee4\ub97c \uc0bd\uc785\ud558\uc138\uc694 (\ucd5c\uc18c 1\uac1c, \ucd5c\ub300 2\uac1c). \ud0a4\uc6cc\ub4dc\ub294 \ud574\ub2f9 \uc139\uc158 \uc8fc\uc81c\ub97c \uc601\ubb38 2~4\ub2e8\uc5b4\ub85c \ud45c\ud604 (\uc608: \"server rack data center\", \"network security firewall\"). FAQ \uc139\uc158\uc5d0\ub294 \uc0bd\uc785 \uae08\uc9c0.\n8. **\ucd5c\uc2e0\uc131 \ud544\uc218**:\n   - user \uba54\uc2dc\uc9c0\uc758 \"\uc791\uc131 \uae30\uc900\uc77c\"\uc744 \ud655\uc778\ud558\uace0 \ud574\ub2f9 \uc2dc\uc810 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"2024\ub144\", \"2023\ub144\" \ub4f1 \uacfc\uac70 \uc5f0\ub3c4 \ub370\uc774\ud130\ub294 \uc0ac\uc6a9\ud558\uc9c0 \ub9c8\uc138\uc694. \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4(user \uba54\uc2dc\uc9c0 \ucc38\uace0) \uae30\uc900 \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n   - \ubc84\uc804 \uc815\ubcf4, \uac00\uaca9, \uae30\ub2a5 \ube44\uad50\ub294 \ucd5c\uc2e0 \ubc84\uc804 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694\n   - \"\uacf5\uc2dd \ubb38\uc11c \ubcf8\ubb38\" \uc139\uc158\uc774 \uc81c\uacf5\ub418\uba74 SERP \uc2a4\ub2c8\ud3ab\ubcf4\ub2e4 \uc6b0\uc120\ud558\uc5ec \ucc38\uace0\ud558\uc138\uc694. \uacf5\uc2dd \ubb38\uc11c\uc758 \uc815\ud655\ud55c \uc6a9\uc5b4, \ubc84\uc804, \uae30\ub2a5 \uc124\uba85\uc744 \uadf8\ub300\ub85c \ubc18\uc601\ud558\uc138\uc694\n   - \uc81c\ubaa9\uc5d0 \uc5f0\ub3c4\ub97c \ud3ec\ud568\ud560 \ub54c\ub294 \ubc18\ub4dc\uc2dc \ud604\uc7ac \uc5f0\ub3c4\ub97c \uc0ac\uc6a9\ud558\uc138\uc694\n\n## \ud544\uc218 \uc139\uc158 \uad6c\uc131 (H2 \uae30\uc900)\n\n| \uc139\uc158 (H2) | \ucd5c\uc18c \uae00\uc790 \uc218 | \ud544\uc218 \ub0b4\uc6a9 |\n|---|---|---|\n| \uc5d0\ub7ec \ud604\uc0c1 | 300\uc790 | \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub97c **\uc601\ubb38 \uc6d0\ubb38 + \ud55c\uae00 \ud574\uc11d** \ucf54\ub4dc\ube14\ub85d\uc73c\ub85c \uae30\uc7ac, \ubc1c\uc0dd \ud658\uacbd(OS/\ubc84\uc804/\uc0c1\ud669) \uc124\uba85, \uc2a4\ud06c\ub9b0\uc0f7 \ub300\uccb4 \ud14d\uc2a4\ud2b8 \uc124\uba85 |\n| \uc6d0\uc778 \ubd84\uc11d | 500\uc790 | H3\ub85c \uc6d0\uc778 3\uac00\uc9c0 \uc774\uc0c1, \uac01 \uc6d0\uc778 200\uc790 \uc774\uc0c1, \ube48\ub3c4\uc21c \uc815\ub82c, \uc6d0\uc778\ubcc4 \ud655\uc778 \uba85\ub839\uc5b4 \ud3ec\ud568 |\n| \ud574\uacb0 \ubc29\ubc95 | 800\uc790 | H3\ub85c \uac01 \uc6d0\uc778\ubcc4 \ud574\uacb0\ubc95, \ucf54\ub4dc\ube14\ub85d 4\uac1c \uc774\uc0c1, \uac01 \ud574\uacb0\ubc95\uc5d0 \uc2e4\ud589 \uc804 \ud655\uc778 \u2192 \uc870\uce58 \u2192 \uc2e4\ud589 \ud6c4 \uac80\uc99d 3\ub2e8\uacc4 \uad6c\uc870 |\n| \uc608\ubc29 \uc870\uce58 | 400\uc790 | \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud06c\ub9bd\ud2b8 \uc608\uc2dc, \uc790\ub3d9\ud654 \ud301, \uc54c\ub9bc \uc124\uc815 \ubc29\ubc95, \uc815\uae30 \uc810\uac80 \uccb4\ud06c\ub9ac\uc2a4\ud2b8 |\n| \uad00\ub828 \uc5d0\ub7ec/\ucc38\uace0 \uc790\ub8cc | 200\uc790 | \ub3d9\uc77c \uc81c\ud488\uad70\uc758 \uad00\ub828 \uc5d0\ub7ec 2~3\uac1c \uc5b8\uae09, \uacf5\uc2dd \ubb38\uc11c \ub9c1\ud06c \uc548\ub0b4 |\n| FAQ | 200\uc790 | 3\uac1c \uc774\uc0c1, \uac01 \ub2f5\ubcc0 80\uc790 \uc774\uc0c1 |\n\n## SERP \ub370\uc774\ud130 \ud65c\uc6a9 \uc9c0\uce68\n\n- \uc81c\uacf5\ub41c SERP \ub370\uc774\ud130(\uac80\uc0c9 \uc2a4\ub2c8\ud3ab, People Also Ask, \uad00\ub828 \uac80\uc0c9\uc5b4)\ub97c \ucc38\uace0\ud558\uc5ec \ucf58\ud150\uce20 \uae4a\uc774\ub97c \ud655\ubcf4\ud558\uc138\uc694\n- People Also Ask \uc9c8\ubb38\uc740 FAQ \uc139\uc158\uc5d0 \ubc18\uc601\ud558\uc138\uc694\n- \uad00\ub828 \uac80\uc0c9\uc5b4\ub294 \ubcf8\ubb38\uc5d0\uc11c \uc790\uc5f0\uc2a4\ub7fd\uac8c \uc5b8\uae09\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \ucf58\ud150\uce20\uc5d0\uc11c \ub2e4\ub8e8\ub294 \ud574\uacb0 \ubc29\ubc95\uc744 \ube60\uc9d0\uc5c6\uc774 \ucee4\ubc84\ud558\uc138\uc694\n- SERP \uc0c1\uc704 \uacb0\uacfc\ub97c \ucc38\uace0\ud558\ub418 \uadf8\ub300\ub85c \ubcf5\uc0ac\ud558\uc9c0 \ub9d0 \uac83\n- \uc778\ub77c\uc778 \ucd9c\ucc98\ub294 '[\ucd9c\ucc98\uba85](URL)' \ud615\ud0dc\ub85c \uc0bd\uc785\ud558\uc138\uc694\n\n## \ucc38\uc870 URL \uaddc\uce59 (\ud544\uc218)\n\n- **references \ud544\ub4dc**: \ubc18\ub4dc\uc2dc SERP \ub370\uc774\ud130\uc5d0 \ud3ec\ud568\ub41c \uc2e4\uc81c URL\ub9cc \uc0ac\uc6a9\ud558\uc138\uc694. URL\uc744 \ucd94\uce21\ud558\uac70\ub098 \uae30\uc5b5\uc5d0 \uc758\uc874\ud558\uc5ec \uc791\uc131\ud558\uc9c0 \ub9c8\uc138\uc694\n- **\uc778\ub77c\uc778 \ucd9c\ucc98 URL**: SERP \ub370\uc774\ud130\uc758 \"URL:\" \ud56d\ubaa9\uc5d0\uc11c \uac00\uc838\uc624\uc138\uc694. user \uba54\uc2dc\uc9c0\uc758 \"\ucc38\uc870 \uac00\ub2a5 URL \ud480\" \uc139\uc158 \ucc38\uace0\n- **\uacf5\uc2dd \ubb38\uc11c \uc6b0\uc120**: SERP URL \uc911 \uacf5\uc2dd \ubb38\uc11c \ub3c4\uba54\uc778(learn.microsoft.com, docs.aws.amazon.com, cloud.google.com, developer.mozilla.org \ub4f1)\uc774 \uc788\uc73c\uba74 \uc6b0\uc120 \uc0ac\uc6a9\ud558\uc138\uc694\n- **URL\uc744 \ubaa8\ub97c \ub54c**: URL \uc5c6\uc774 \ucd9c\ucc98\uba85\ub9cc \ud14d\uc2a4\ud2b8\ub85c \uae30\uc7ac\ud558\uc138\uc694 (\uc608: \"Microsoft Learn \uacf5\uc2dd \ubb38\uc11c \ucc38\uace0\"). \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 URL\uc744 \ub9cc\ub4e4\uc5b4\ub0b4\uc9c0 \ub9c8\uc138\uc694\n\n## \ud488\uc9c8 \uc790\uac00 \uac80\uc99d \uccb4\ud06c\ub9ac\uc2a4\ud2b8\n\n\uc791\uc131 \uc644\ub8cc \ud6c4 \uc544\ub798 \ud56d\ubaa9\uc744 \ud655\uc778\ud558\uc138\uc694:\n- [ ] H2 \ud5e4\ub529\uc774 5\uac1c \uc774\uc0c1\uc778\uac00?\n- [ ] \ucf54\ub4dc\ube14\ub85d\uc774 4\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \ucf54\ub4dc\ube14\ub85d\uc5d0 \ubaa8\ub450 \uc5b8\uc5b4 \ud0dc\uadf8\uac00 \uc788\ub294\uac00? (\\`\\`\\`powershell, \\`\\`\\`bash \ub4f1)\n- [ ] \uc5d0\ub7ec \uba54\uc2dc\uc9c0\uac00 \uc601\ubb38 \uc6d0\ubb38 + \ud55c\uae00 \ud574\uc11d\uc73c\ub85c \uae30\uc7ac\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uac01 \ud574\uacb0\ubc95\uc774 \uc2e4\ud589 \uc804 \ud655\uc778 \u2192 \uc870\uce58 \u2192 \uc2e4\ud589 \ud6c4 \uac80\uc99d 3\ub2e8\uacc4 \uad6c\uc870\uc778\uac00?\n- [ ] \uac01 \uc6d0\uc778\ubcc4 \ud574\uacb0\ubc95\uc5d0 \uc2e4\ud589 \uc804\ud6c4 \ud655\uc778 \uba85\ub839\uc5b4\uac00 \uc788\ub294\uac00?\n- [ ] FAQ\uac00 3\uac1c \uc774\uc0c1\uc774\uace0 \uac01 \ub2f5\ubcc0\uc774 80\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc804\uccb4 \ubcf8\ubb38\uc774 3,000\uc790 \uc774\uc0c1\uc778\uac00?\n- [ ] \uc608\ubc29 \uc870\uce58\uc5d0 \uad6c\uccb4\uc801\uc778 \uc2a4\ud06c\ub9bd\ud2b8\ub098 \uc790\ub3d9\ud654 \ud301\uc774 \uc788\ub294\uac00?\n- [ ] \uad00\ub828 \uc5d0\ub7ec/\ucc38\uace0 \uc790\ub8cc \uc139\uc158\uc774 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98\uac00 1\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] internal_link_keywords\uc5d0 \ud574\ub2f9\ud558\ub294 \ud0a4\uc6cc\ub4dc\uac00 \ubcf8\ubb38\uc5d0\uc11c **\uad75\uac8c** \ud45c\uc2dc\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] [MERMAID]...[/MERMAID] \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc774 2\uac1c \uc774\uc0c1 \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] Mermaid \ub178\ub4dc \ub808\uc774\ube14\uc774 \uc601\ubb38\uc73c\ub85c \uc791\uc131\ub418\uc5b4 \uc788\ub294\uac00?\n- [ ] \\`<!-- IMAGE: ... -->\\` \ub9c8\ucee4\uac00 1~2\uac1c \ud3ec\ud568\ub418\uc5b4 \uc788\ub294\uac00? (FAQ/Mermaid \uc139\uc158 \uc81c\uc678)\n- [ ] references \ud544\ub4dc\uc758 \ubaa8\ub4e0 URL\uc774 SERP \ub370\uc774\ud130\uc5d0\uc11c \uac00\uc838\uc628 \uac83\uc778\uac00?\n- [ ] \uc778\ub77c\uc778 \ucd9c\ucc98 URL\uc774 \uc2e4\uc81c \uc874\uc7ac\ud558\ub294 URL\uc778\uac00? (\ucd94\uce21 URL \uc0ac\uc6a9 \uae08\uc9c0)\n\n## \uae08\uc9c0 \uc0ac\ud56d\n\n- \uc2e4\uc81c \uae30\uc5c5\uba85\uc740 \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub418, \uac00\uc0c1 \uc2dc\ub098\ub9ac\uc624\uc5d0\uc11c\ub294 'example-corp.com' \ub4f1 \uc124\uba85\uc801 \ub3c4\uba54\uc778\uba85 \uc0ac\uc6a9\n- \ubbf8\ud655\uc778 \ud574\uacb0 \ubc29\ubc95 \uc791\uc131 \uae08\uc9c0\n- \ubcf4\uc548 \uc704\ud611\uc774 \ub418\ub294 \uba85\ub839\uc5b4 (\uc608: \ubc29\ud654\ubcbd \uc804\uccb4 \ud574\uc81c) \uae08\uc9c0\n\n## \ucd9c\ub825 \ud615\uc2dd\n\n\ubc18\ub4dc\uc2dc \uc544\ub798 JSON \ud615\uc2dd\uc73c\ub85c\ub9cc \uc751\ub2f5\ud558\uc138\uc694. JSON \uc678 \ud14d\uc2a4\ud2b8\ub97c \ud3ec\ud568\ud558\uc9c0 \ub9c8\uc138\uc694.\n\n\\`\\`\\`json\n{\n  \"title\": \"SEO \ucd5c\uc801\ud654\ub41c \uc81c\ubaa9 (\uc5d0\ub7ec\ucf54\ub4dc \ub610\ub294 \uc99d\uc0c1 \ud3ec\ud568, 50~60\uc790)\",\n  \"meta_description\": \"120~155\uc790 \uba54\ud0c0 \uc124\uba85 (\uc5d0\ub7ec\ucf54\ub4dc + \ud574\uacb0 \ud0a4\uc6cc\ub4dc \ud3ec\ud568)\",\n  \"faq_schema\": [\n    {\"question\": \"\uc9c8\ubb381\", \"answer\": \"\ub2f5\ubcc01 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb382\", \"answer\": \"\ub2f5\ubcc02 (80\uc790 \uc774\uc0c1)\"},\n    {\"question\": \"\uc9c8\ubb383\", \"answer\": \"\ub2f5\ubcc03 (80\uc790 \uc774\uc0c1)\"}\n  ],\n  \"references\": [\"Microsoft Learn URL\", \"\uacf5\uc2dd \ubb38\uc11c \ucc38\uace0\uc790\ub8cc\"],\n  \"tags\": [\"\ud0dc\uadf81\", \"\ud0dc\uadf82\", \"\ud0dc\uadf83\", \"\ud0dc\uadf84\", \"\ud0dc\uadf85\"],\n  \"internal_link_keywords\": [\"\uad00\ub828 \ud0a4\uc6cc\ub4dc1\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc2\", \"\uad00\ub828 \ud0a4\uc6cc\ub4dc3\"],\n  \"content\": \"\ub9c8\ud06c\ub2e4\uc6b4 \ubcf8\ubb38 \uc804\uccb4 (\uc5d0\ub7ec \ucf54\ub4dc\ube14\ub85d, \ud574\uacb0 \uba85\ub839\uc5b4, \ub2e8\uacc4\ubcc4 \uac00\uc774\ub4dc \ud3ec\ud568, \ucd5c\uc18c 3000\uc790 \uc774\uc0c1)\"\n}\n\\`\\`\\``;\n\nlet systemPrompt;\nlet promptType;\n\nif (category.includes('\ube44\uad50') || category.includes('\ud2b8\ub80c\ub4dc') || keyword.includes('vs')) {\n  systemPrompt = PROMPT_B;\n  promptType = 'B';\n} else if (category.includes('\uc5d0\ub7ec') || category.includes('\ud2b8\ub7ec\ube14\uc288\ud305') || category.includes('\ud574\uacb0')) {\n  systemPrompt = PROMPT_C;\n  promptType = 'C';\n} else {\n  systemPrompt = PROMPT_A;\n  promptType = 'A';\n}\n\n// JSON \ud615\uc2dd \uc548\uc804 \uc9c0\uc2dc \ucd94\uac00\nconst jsonSafetyNote = `\n\n## \uc911\uc694: JSON \ud615\uc2dd \uaddc\uce59\n- \ubc18\ub4dc\uc2dc \uc720\ud6a8\ud55c JSON\uc744 \ucd9c\ub825\ud558\uc138\uc694.\n- content \ud544\ub4dc\uc758 \uc904\ubc14\uafc8\uc740 \ubc18\ub4dc\uc2dc \\\\n\uc73c\ub85c \uc774\uc2a4\ucf00\uc774\ud504\ud558\uc138\uc694.\n- \ubb38\uc790\uc5f4 \ub0b4 \ud070\ub530\uc634\ud45c\ub294 \ubc18\ub4dc\uc2dc \\\\\"\uc73c\ub85c \uc774\uc2a4\ucf00\uc774\ud504\ud558\uc138\uc694.\n- \ubb38\uc790\uc5f4 \ub0b4 \ubc31\uc2ac\ub798\uc2dc\ub294 \ubc18\ub4dc\uc2dc \\\\\\\\\ub85c \uc774\uc2a4\ucf00\uc774\ud504\ud558\uc138\uc694.\n- \ucf54\ub4dc \ube14\ub85d\uc758 \ubc31\ud2f1(\\`)\uc740 \uadf8\ub300\ub85c \uc0ac\uc6a9 \uac00\ub2a5\ud558\uc9c0\ub9cc, \uc904\ubc14\uafc8\uc740 \\\\n\uc73c\ub85c \uc774\uc2a4\ucf00\uc774\ud504\ud558\uc138\uc694.\n`;\n\n// \uc791\uc131 \uae30\uc900\uc77c (\ucd5c\uc2e0\uc131 \uac15\ud654)\\nconst today = new Date();\\nconst yearMonth = `${today.getFullYear()}\ub144 ${today.getMonth() + 1}\uc6d4`;\\n\\n// \uacf5\uc2dd \ubb38\uc11c \ubcf8\ubb38 (Fetch Official Docs \ub178\ub4dc\uc5d0\uc11c \uc804\ub2ec)\\nconst officialDocsText = inputData.official_docs_text || '';\\nconst docsSection = officialDocsText\\n  ? `\\n\\n## \uacf5\uc2dd \ubb38\uc11c \ubcf8\ubb38 (\ucd5c\uc6b0\uc120 \ucc38\uace0 \uc790\ub8cc)\\n${officialDocsText}`\\n  : '';\\n\\n// SERP URL \ud480 \uad6c\uc131\nconst serpUrls = inputData.serp_urls || [];\nconst urlPoolText = serpUrls.length > 0\n  ? serpUrls.map((url, i) => `${i + 1}. ${url}`).join('\\n')\n  : '(SERP URL \uc5c6\uc74c)';\n\nreturn {\n  json: {\n    keyword,\n    category,\n    row_index: rowIndex,\n    prompt_type: promptType,\n    system_prompt: systemPrompt + jsonSafetyNote,\n    user_message: `\ud0a4\uc6cc\ub4dc: ${keyword}\\n\uc791\uc131 \uae30\uc900\uc77c: ${yearMonth} (\ucd5c\uc2e0 \uc815\ubcf4 \uae30\uc900\uc73c\ub85c \uc791\uc131\ud558\uc138\uc694)${docsSection}\\n\\n## SERP \uc778\ud154\ub9ac\uc804\uc2a4\\n${serpSummary}\\n\\n## \ucc38\uc870 \uac00\ub2a5 URL \ud480\\n\uc544\ub798 URL\ub9cc references\uc640 \uc778\ub77c\uc778 \ucd9c\ucc98\uc5d0 \uc0ac\uc6a9\ud558\uc138\uc694:\\n${urlPoolText}`,\n  }\n};\n"
      },
      "id": "node-4-route-prompt",
      "name": "Route Prompt (A/B/C)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1660,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Build LLM Request \u2014 provider\ubcc4 URL/headers/body \uc0dd\uc131\n * \uc785\ub825: system_prompt, user_message, _llm_purpose(optional)\n * \ucd9c\ub825: _llm_url, _llm_headers, _llm_body, _llm_provider + \uc6d0\ubcf8 \ub370\uc774\ud130 \ud328\uc2a4\uc2a4\ub8e8\n */\n\nconst provider = $env.LLM_PROVIDER || 'gemini';\nconst systemPrompt = $input.item.json.system_prompt;\nconst userMessage = $input.item.json.user_message;\nconst purpose = $input.item.json._llm_purpose || 'content_gen';\n\nconst PROVIDERS = {\n  gemini: {\n    url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${$env.GOOGLE_API_KEY}`,\n    headers: { 'content-type': 'application/json' },\n    body: (sys, user, opts) => ({\n      contents: [{ role: 'user', parts: [{ text: user }] }],\n      systemInstruction: { parts: [{ text: sys }] },\n      generationConfig: { maxOutputTokens: opts.maxTokens, temperature: opts.temperature }\n    }),\n  },\n  claude: {\n    url: 'https://api.anthropic.com/v1/messages',\n    headers: {\n      'x-api-key': $env.CLAUDE_API_KEY,\n      'anthropic-version': '2023-06-01',\n      'content-type': 'application/json',\n    },\n    body: (sys, user, opts) => ({\n      model: opts.model || 'claude-sonnet-4-5-20250514',\n      max_tokens: opts.maxTokens,\n      temperature: opts.temperature,\n      system: sys,\n      messages: [{ role: 'user', content: user }],\n    }),\n  },\n};\n\nconst config = PROVIDERS[provider];\nif (!config) throw new Error(`\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 LLM provider: ${provider}`);\n\nconst opts = purpose === 'verification'\n  ? { maxTokens: 800, temperature: 0, model: 'claude-haiku-4-5-20251001' }\n  : { maxTokens: 32768, temperature: 0.7 };\n\nreturn {\n  json: {\n    _llm_url: config.url,\n    _llm_headers: config.headers,\n    _llm_body: config.body(systemPrompt, userMessage, opts),\n    _llm_provider: provider,\n    ...Object.fromEntries(\n      Object.entries($input.item.json).filter(([k]) => !k.startsWith('_llm'))\n    ),\n  }\n};\n"
      },
      "id": "node-build-llm-content",
      "name": "Build LLM Request (Content Gen)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json._llm_url }}",
        "sendHeaders": true,
        "specifyHeaders": "json",
        "jsonHeaders": "={{ JSON.stringify($json._llm_headers) }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json._llm_body) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "node-5-llm-content",
      "name": "LLM Request (Content Gen)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1960,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 15000
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Normalize LLM Response \u2014 provider\ubcc4 \uc751\ub2f5\uc5d0\uc11c \ud14d\uc2a4\ud2b8 \ucd94\ucd9c\n * Gemini: candidates[0].content.parts[0].text\n * Claude: content[0].text\n */\n\nconst provider = $input.item.json._llm_provider\n  || $env.LLM_PROVIDER\n  || 'gemini';\n\nlet text;\nconst data = $input.item.json;\n\nif (provider === 'gemini') {\n  text = data.candidates[0].content.parts[0].text;\n} else if (provider === 'claude') {\n  text = data.content[0].text;\n} else {\n  throw new Error(`\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 provider \uc751\ub2f5: ${provider}`);\n}\n\nreturn { json: { text, _llm_provider: provider } };\n"
      },
      "id": "node-normalize-content",
      "name": "Normalize Response (Content Gen)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2140,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Node 6: LLM \uc751\ub2f5 JSON \ud30c\uc2f1 + \ud544\uc218 \ud544\ub4dc \uac80\uc99d\n * Mode: runOnceForEachItem\n * \uc785\ub825: \uc815\uaddc\ud654\ub41c LLM \uc751\ub2f5 (text)\n * \ucd9c\ub825: \ud30c\uc2f1\ub41c JSON \ub610\ub294 \uc5d0\ub7ec\n *\n * LLM\uc774 \uc0dd\uc131\ud55c JSON\uc758 \uc77c\ubc18\uc801 \ubb38\uc81c\ub97c \uc790\ub3d9 \ubcf5\uad6c:\n * - \ubb38\uc790\uc5f4 \ub0b4 \uc774\uc2a4\ucf00\uc774\ud504\ub418\uc9c0 \uc54a\uc740 \" (YAML, \ucf54\ub4dc\ube14\ub85d \ub4f1)\n * - \uc81c\uc5b4 \ubb38\uc790 (raw newline/tab)\n * - \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \uc774\uc2a4\ucf00\uc774\ud504 \uc2dc\ud000\uc2a4 (\\g, \\e \ub4f1)\n */\n\nconst raw = $input.item.json.text;\n\n// \uc54c\ub824\uc9c4 \ucd5c\uc0c1\uc704 JSON \ud544\ub4dc \ubaa9\ub85d (\uad6c\uc870\uc801 \ud0a4 \ud310\ubcc4\uc5d0 \uc0ac\uc6a9)\nconst TOP_LEVEL_KEYS = new Set([\n  'title', 'content', 'meta_description', 'faq_schema',\n  'references', 'internal_link_keywords', '_warning',\n]);\n\n// === Step 1: JSON \ube14\ub85d \ucd94\ucd9c ===\nlet jsonStr;\nconst fenceStart = raw.indexOf('```json');\nif (fenceStart !== -1) {\n  const contentStart = raw.indexOf('\\n', fenceStart);\n  const fenceEnd = raw.lastIndexOf('```');\n  if (contentStart !== -1 && fenceEnd > fenceStart + 7) {\n    jsonStr = raw.substring(contentStart + 1, fenceEnd).trim();\n  }\n}\nif (!jsonStr) {\n  const match = raw.match(/(\\{[\\s\\S]*\\})/);\n  if (!match) {\n    throw new Error(\"JSON \ucd94\ucd9c \uc2e4\ud328: \" + raw.substring(0, 200));\n  }\n  jsonStr = match[1];\n}\n\n// === Step 2: Lenient JSON repair ===\n// \uad6c\uc870\uc801 \ud30c\uc2f1\uc73c\ub85c \uc774\uc2a4\ucf00\uc774\ud504\ub418\uc9c0 \uc54a\uc740 \" \uc640 \uc81c\uc5b4 \ubb38\uc790\ub97c \uc790\ub3d9 \ubcf5\uad6c\nfunction repairJson(str) {\n  const VALID_ESCAPES = '\"\\\\\\/bfnrtu';\n  let result = '';\n  let i = 0;\n\n  function skipWS() {\n    while (i < str.length && /\\s/.test(str[i])) { result += str[i++]; }\n  }\n\n  function parseValue() {\n    skipWS();\n    if (i >= str.length) return;\n    if (str[i] === '\"') parseString();\n    else if (str[i] === '{') parseObject();\n    else if (str[i] === '[') parseArray();\n    else parseLiteral();\n  }\n\n  function parseString() {\n    result += str[i++]; // opening \"\n\n    while (i < str.length) {\n      const ch = str[i];\n\n      // \uc774\uc2a4\ucf00\uc774\ud504 \uc2dc\ud000\uc2a4\n      if (ch === '\\\\' && i + 1 < str.length) {\n        const next = str[i + 1];\n        if (VALID_ESCAPES.includes(next)) {\n          result += ch + next;\n        } else {\n          // \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \uc774\uc2a4\ucf00\uc774\ud504 \u2192 \ubc31\uc2ac\ub798\uc2dc \uc774\uc911 \uc774\uc2a4\ucf00\uc774\ud504\n          result += '\\\\\\\\' + next;\n        }\n        i += 2;\n        continue;\n      }\n\n      // \ud070\ub530\uc634\ud45c: \uad6c\uc870\uc801 \uc885\ub8cc\uc778\uc9c0 content \ub0b4 \uc778\uc6a9\ubd80\ud638\uc778\uc9c0 \ud310\ub2e8\n      if (ch === '\"') {\n        // \ub2e4\uc74c \ube44\uacf5\ubc31 \ubb38\uc790\ub85c \ud310\ub2e8\n        let peek = i + 1;\n        while (peek < str.length && /[\\s\\n\\r\\t]/.test(str[peek])) peek++;\n\n        if (peek >= str.length || ',:]}'.indexOf(str[peek]) !== -1) {\n          // \uad6c\uc870\uc801 \uc885\ub8cc: \ub4a4\uc5d0 , ] } : \ub610\ub294 EOF\n          result += '\"';\n          i++;\n          return;\n        } else if (str[peek] === '\"') {\n          // \ub2e4\uc74c\ub3c4 \" \u2192 \ub2e4\uc74c \"...\" \uc0ac\uc774\uac00 \uc720\ud6a8\ud55c JSON \ud0a4\uc778\uc9c0 \uac80\uc99d\n          let j = peek + 1;\n          let candidateKey = '';\n          while (j < str.length && str[j] !== '\"' && str[j] !== ':') {\n            candidateKey += str[j];\n            j++;\n          }\n          // \uc870\uac74 \uac15\ud654: (1) \" \ub85c \ub2eb\ud600\uc57c \ud558\uace0 (2) \uadf8 \ub4a4\uc5d0 : \uc774 \uc788\uace0\n          // (3) \ud0a4\uac00 \uc720\ud6a8\ud55c JSON \ud0a4 \ud328\ud134 (\uc9e7\uace0, \uc54c\ud30c\ubcb3/\uc5b8\ub354\uc2a4\ucf54\uc5b4)\n          // (4) \uc54c\ub824\uc9c4 \ucd5c\uc0c1\uc704 \ud544\ub4dc\uba85\uc774\uc5b4\uc57c \uad6c\uc870\uc801 \uc885\ub8cc\ub85c \ud310\ub2e8\n          if (j < str.length && str[j] === '\"') {\n            let k = j + 1;\n            while (k < str.length && /[\\s]/.test(str[k])) k++;\n            if (k < str.length && str[k] === ':'\n                && candidateKey.length > 0 && candidateKey.length < 30\n                && /^[a-zA-Z_][\\w_]*$/.test(candidateKey)\n                && TOP_LEVEL_KEYS.has(candidateKey)) {\n              // \"value\" \"known_key\": \u2192 \uc27c\ud45c \ub204\ub77d\uc774\uc9c0\ub9cc \uad6c\uc870\uc801 \uc885\ub8cc\n              result += '\"';\n              i++;\n              return;\n            }\n          }\n          // \uc720\ud6a8\ud55c \ud0a4\uac00 \uc544\ub2d8 \u2192 content \ub0b4 \uc778\uc6a9\ubd80\ud638\n          result += '\\\\\"';\n          i++;\n          continue;\n        } else {\n          // \ub4a4\uc5d0 \uc77c\ubc18 \ubb38\uc790 \u2192 content \ub0b4 \uc774\uc2a4\ucf00\uc774\ud504\ub418\uc9c0 \uc54a\uc740 \uc778\uc6a9\ubd80\ud638\n          result += '\\\\\"';\n          i++;\n          continue;\n        }\n      }\n\n      // \uc81c\uc5b4 \ubb38\uc790\n      const code = ch.charCodeAt(0);\n      if (code <= 0x1f) {\n        if (code === 0x0a) result += '\\\\n';\n        else if (code === 0x0d) result += '\\\\r';\n        else if (code === 0x09) result += '\\\\t';\n        i++;\n        continue;\n      }\n\n      result += ch;\n      i++;\n    }\n  }\n\n  function parseObject() {\n    result += str[i++]; // {\n    skipWS();\n    let first = true;\n    while (i < str.length && str[i] !== '}') {\n      if (!first) {\n        if (str[i] === ',') result += str[i++];\n        // \uc27c\ud45c \ub204\ub77d: \"}\" \uc544\ub2cc \ub2e4\ub978 \ubb38\uc790\uac00 \uc624\uba74 \uc27c\ud45c \uc0bd\uc785\n        else {\n          skipWS();\n          if (i < str.length && str[i] === '\"') result += ',';\n        }\n      }\n      first = false;\n      skipWS();\n      if (i >= str.length || str[i] === '}') break;\n      parseString(); // key\n      skipWS();\n      if (i < str.length && str[i] === ':') result += str[i++];\n      parseValue();   // value\n      skipWS();\n    }\n    if (i < str.length && str[i] === '}') result += str[i++];\n  }\n\n  function parseArray() {\n    result += str[i++]; // [\n    skipWS();\n    let first = true;\n    while (i < str.length && str[i] !== ']') {\n      if (!first) {\n        if (str[i] === ',') result += str[i++];\n        else {\n          skipWS();\n          if (i < str.length && str[i] !== ']') result += ',';\n        }\n      }\n      first = false;\n      skipWS();\n      if (i >= str.length || str[i] === ']') break;\n      parseValue();\n      skipWS();\n    }\n    if (i < str.length && str[i] === ']') result += str[i++];\n  }\n\n  function parseLiteral() {\n    // number, boolean, null\n    while (i < str.length && /[^\\s,\\]}\\[]/.test(str[i])) {\n      result += str[i++];\n    }\n  }\n\n  parseValue();\n  return result;\n}\n\n// === Step 2.5: \ud544\ub4dc \uacbd\uacc4 \uae30\ubc18 \ucd94\ucd9c (3\ucc28 \ud3f4\ubc31) ===\n// \ucd5c\uc0c1\uc704 \ud0a4 \uc704\uce58\ub97c \uc815\uaddc\uc2dd\uc73c\ub85c \ucc3e\uc544 \uac12\uc744 \uac1c\ubcc4 \ucd94\ucd9c \u2192 \uc548\uc804\ud558\uac8c \uc774\uc2a4\ucf00\uc774\ud504\nfunction extractByFieldBoundary(str) {\n  // \ucd5c\uc0c1\uc704 \ud0a4\uc758 \uc2dc\uc791 \uc704\uce58 \ucc3e\uae30: \"key\" \ub2e4\uc74c\uc5d0 : \uc774 \uc624\ub294 \ud328\ud134\n  // \ub4e4\uc5ec\uc4f0\uae30 \uae30\ubc18 \ud544\ud130: \ucd5c\uc0c1\uc704 \ud0a4\ub294 \ubcf4\ud1b5 2~4\uce78 \ub4e4\uc5ec\uc4f0\uae30 \ub610\ub294 \uc904 \uc2dc\uc791\n  const keyPattern = /(?:^|[\\n{,])\\s*\"(title|content|meta_description|faq_schema|references|internal_link_keywords)\"\\s*:/g;\n  const seen = new Set();\n  const positions = [];\n  let m;\n  while ((m = keyPattern.exec(str)) !== null) {\n    // \uac01 \ud0a4\uc758 \uccab \ubc88\uc9f8 \ub9e4\uce58\ub9cc \uc0ac\uc6a9 (\uc911\ubcf5 \ubc29\uc9c0)\n    if (seen.has(m[1])) continue;\n    seen.add(m[1]);\n    // \uc2e4\uc81c \ud0a4 \uc2dc\uc791 \uc704\uce58 \ubcf4\uc815 (prefix \ubb38\uc790 \uc81c\uc678)\n    const keyStart = str.indexOf('\"' + m[1] + '\"', m.index);\n    const fullMatch = '\"' + m[1] + '\"';\n    let vs = keyStart + fullMatch.length;\n    while (vs < str.length && /[\\s:]/.test(str[vs])) vs++;\n    positions.push({ key: m[1], start: keyStart, valueStart: vs });\n  }\n  if (positions.length === 0) {\n    throw new Error(\"\ud544\ub4dc \uacbd\uacc4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc74c\");\n  }\n  // \ud0a4 \uc704\uce58 \uc21c\uc11c\ub85c \uc815\ub82c\n  positions.sort((a, b) => a.start - b.start);\n\n  const obj = {};\n  for (let pi = 0; pi < positions.length; pi++) {\n    const pos = positions[pi];\n    // \uac12 \uc601\uc5ed: \ud604\uc7ac valueStart ~ \ub2e4\uc74c \ud0a4\uc758 start (\ub610\ub294 \ubb38\uc790\uc5f4 \ub05d)\n    const valueEnd = pi + 1 < positions.length ? positions[pi + 1].start : str.length;\n    let rawValue = str.substring(pos.valueStart, valueEnd).trim();\n\n    // \ud6c4\ud589 \uc27c\ud45c \uc81c\uac70\n    if (rawValue.endsWith(',')) rawValue = rawValue.slice(0, -1).trim();\n    // \ub9c8\uc9c0\ub9c9 \ud544\ub4dc\uc77c \ub54c \ub2eb\ub294 } \uc81c\uac70\n    if (pi === positions.length - 1 && rawValue.endsWith('}')) {\n      rawValue = rawValue.slice(0, -1).trim();\n      if (rawValue.endsWith(',')) rawValue = rawValue.slice(0, -1).trim();\n    }\n\n    // \ubc30\uc5f4/\uac1d\uccb4 \uac12\uc740 \uc9c1\uc811 \ud30c\uc2f1 \uc2dc\ub3c4\n    if (rawValue.startsWith('[') || rawValue.startsWith('{')) {\n      try {\n        obj[pos.key] = JSON.parse(rawValue);\n        continue;\n      } catch (_) {\n        // \ubc30\uc5f4/\uac1d\uccb4 \ud30c\uc2f1 \uc2e4\ud328 \u2192 \ubb38\uc790\uc5f4 repair \uc2dc\ub3c4\n        try {\n          obj[pos.key] = JSON.parse(repairJson(rawValue));\n          continue;\n        } catch (__) {\n          // \ubc30\uc5f4 \ud544\ub4dc\ub294 \ube48 \ubc30\uc5f4\ub85c \ucd08\uae30\ud654 (\ubb38\uc790\uc5f4\ub85c \uc800\uc7a5 \ubc29\uc9c0)\n          if (['faq_schema', 'references', 'internal_link_keywords'].includes(pos.key)) {\n            obj[pos.key] = [];\n            continue;\n          }\n          // \uadf8 \uc678 \u2192 \ubb38\uc790\uc5f4\ub85c \uc800\uc7a5\n        }\n      }\n    }\n\n    // \ubb38\uc790\uc5f4 \uac12: \uc55e\ub4a4 \" \uc81c\uac70 \ud6c4 \ub0b4\ubd80 \" \uc774\uc2a4\ucf00\uc774\ud504\n    if (rawValue.startsWith('\"')) rawValue = rawValue.slice(1);\n    if (rawValue.endsWith('\"')) rawValue = rawValue.slice(0, -1);\n    // \uc774\ubbf8 \uc774\uc2a4\ucf00\uc774\ud504\ub41c \\\" \ubcf4\uc874, \ub098\uba38\uc9c0 \" \uc774\uc2a4\ucf00\uc774\ud504\n    rawValue = rawValue.replace(/\\\\\"/g, '<<ESC_Q>>');\n    rawValue = rawValue.replace(/\"/g, '\\\\\"');\n    rawValue = rawValue.replace(/<<ESC_Q>>/g, '\\\\\"');\n    // \uc81c\uc5b4 \ubb38\uc790 \uc774\uc2a4\ucf00\uc774\ud504\n    rawValue = rawValue.replace(/[\\x00-\\x1f]/g, (c) => {\n      if (c === '\\n') return '\\\\n';\n      if (c === '\\r') return '\\\\r';\n      if (c === '\\t') return '\\\\t';\n      return '';\n    });\n\n    try {\n      obj[pos.key] = JSON.parse('\"' + rawValue + '\"');\n    } catch (_) {\n      obj[pos.key] = rawValue; // \ucd5c\ud6c4 \uc218\ub2e8: raw \ubb38\uc790\uc5f4 \uadf8\ub300\ub85c\n    }\n  }\n\n  return obj;\n}\n\n// === Step 3: \ud30c\uc2f1 ===\nlet parsed;\n\n// 1\ucc28: \uadf8\ub300\ub85c \ud30c\uc2f1\ntry {\n  parsed = JSON.parse(jsonStr);\n} catch (e1) {\n  // 2\ucc28: lenient repair \ud6c4 \ud30c\uc2f1\n  try {\n    const repaired = repairJson(jsonStr);\n    parsed = JSON.parse(repaired);\n  } catch (e2) {\n    // 3\ucc28: \ud544\ub4dc \uacbd\uacc4 \ucd94\ucd9c (content \ud544\ub4dc\uc758 unescaped quote \ub300\uc751)\n    try {\n      parsed = extractByFieldBoundary(jsonStr);\n    } catch (e3) {\n      throw new Error(\"JSON \ud30c\uc2f1 \uc2e4\ud328: \" + e1.message\n        + \" | repair \ud6c4: \" + e2.message\n        + \" | boundary \ud6c4: \" + e3.message);\n    }\n  }\n}\n\n// === Step 4: \ud544\uc218 \ud544\ub4dc \uac80\uc99d ===\nconst required = [\"title\", \"content\", \"meta_description\", \"faq_schema\",\n                  \"references\", \"internal_link_keywords\"];\nconst missing = required.filter(f => !parsed[f]);\nif (missing.length > 0) {\n  throw new Error(\"\ud544\uc218 \ud544\ub4dc \ub204\ub77d: \" + missing.join(\", \"));\n}\n\n// meta_description \uae38\uc774 \uac80\uc99d (120~155\uc790)\nconst metaLen = parsed.meta_description.length;\nif (metaLen < 120 || metaLen > 155) {\n  parsed._warning = `meta_description \uae38\uc774: ${metaLen}\uc790 (\uad8c\uc7a5 120~155\uc790)`;\n}\n\n// faq_schema \ud615\uc2dd \uac80\uc99d\nif (!Array.isArray(parsed.faq_schema) || parsed.faq_schema.length === 0) {\n  throw new Error(\"faq_schema\uac00 \ube44\uc5b4\uc788\uac70\ub098 \ubc30\uc5f4\uc774 \uc544\ub2d9\ub2c8\ub2e4\");\n}\nconst FAQ_PLACEHOLDER = /\ucc38\uc870|\ucc38\uace0\ud558\uc138\uc694|\ud655\uc778\ud558\uc138\uc694|\ubb38\uc11c\ub97c \ubcf4\uc138\uc694|\ud648\ud398\uc774\uc9c0\ub97c \ubc29\ubb38/;\nfor (const faq of parsed.faq_schema) {\n  if (!faq.question || !faq.answer) {\n    throw new Error(\"faq_schema \ud56d\ubaa9\uc5d0 question/answer \ub204\ub77d\");\n  }\n  if (faq.answer.length < 80) {\n    throw new Error(`FAQ \ub2f5\ubcc0 \uae38\uc774 \ubd80\uc871: ${faq.answer.length}\uc790 (\ucd5c\uc18c 80\uc790)`);\n  }\n  if (FAQ_PLACEHOLDER.test(faq.answer) && faq.answer.length < 120) {\n    throw new Error(`FAQ \ub2f5\ubcc0\uc774 \ud50c\ub808\uc774\uc2a4\ud640\ub354: \"${faq.answer.slice(0, 50)}...\"`);\n  }\n}\n\n// references \ud50c\ub808\uc774\uc2a4\ud640\ub354 \ud544\ud130\ub9c1\nif (Array.isArray(parsed.references)) {\n  parsed.references = parsed.references.filter(ref => {\n    if (typeof ref !== 'string') return false;\n    if (ref.length < 10) return false;\n    if (/^\ucc38\uc870|^\ucc38\uace0|^\ud655\uc778|^\uac80\uc0c9/.test(ref) && !ref.startsWith('http')) return false;\n    return true;\n  });\n}\n\n// \ubcf8\ubb38 \uae38\uc774 \uac80\uc99d (\ud504\ub86c\ud504\ud2b8 \uc720\ud615\ubcc4 \ucd5c\uc18c \uae00\uc790 \uc218)\nconst promptType = $('Route Prompt (A/B/C)').item.json.prompt_type || 'A';\nconst LENGTH_RULES = {\n  'A': { min: 1500 },\n  'B': { min: 2000 },\n  'C': { min: 2000 }\n};\nconst rule = LENGTH_RULES[promptType] || LENGTH_RULES['A'];\nif (parsed.content.length < rule.min) {\n  throw new Error(`\ubcf8\ubb38 \uae38\uc774 \ubd80\uc871: ${parsed.content.length}\uc790 (\ucd5c\uc18c ${rule.min}\uc790, \uc720\ud615: ${promptType})`);\n}\n\nreturn { json: parsed };\n"
      },
      "id": "node-6-parse-json",
      "name": "Parse JSON Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2320,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Inject Images \u2014 Mermaid \ub2e4\uc774\uc5b4\uadf8\ub7a8 \u2192 kroki.io URL + Unsplash Hero/\uc139\uc158 \uc774\ubbf8\uc9c0 + \uc378\ub124\uc77c\n * Mode: runOnceForEachItem\n * Parse JSON Response\uc640 Validate Structure \uc0ac\uc774\uc5d0 \ubc30\uce58\n * \uc785\ub825: \ud30c\uc2f1\ub41c JSON (content \ud544\ub4dc \ud3ec\ud568)\n * \ucd9c\ub825: [MERMAID]\u2192kroki.io <img> URL \ubcc0\ud658\ub41c content + <!-- IMAGE: -->\u2192Unsplash <figure>\n *       + Hero \uc774\ubbf8\uc9c0 + thumbnail_url + image_injection \uba54\ud0c0\ub370\uc774\ud130\n *\n * LLM\uc740 [MERMAID]...[/MERMAID] \ucee4\uc2a4\ud140 \ub9c8\ucee4\ub85c \ub2e4\uc774\uc5b4\uadf8\ub7a8\uc744 \uc0dd\uc131\ud55c\ub2e4.\n * (JSON \uc548\uc758 ```\uac00 fence \ud0d0\uc9c0\ub97c \uae68\ub728\ub9ac\uace0 Gemini \uc751\ub2f5 truncation\uc744 \uc720\ubc1c\ud558\ub294 \uac83\uc744 \ubc29\uc9c0)\n * SVG\ub97c \uc9c1\uc811 \uc0bd\uc785\ud558\uc9c0 \uc54a\uace0 kroki.io GET URL\uc744 \uc0ac\uc6a9\ud558\uc5ec Google Sheets 50K \uc140 \uc81c\ud55c\uc744 \ud68c\ud53c\ud55c\ub2e4.\n * \ub2e4\uc774\uc5b4\uadf8\ub7a8\ub2f9 URL ~200\uc790 vs \uc778\ub77c\uc778 SVG ~10,000\uc790+\n *\n * <!-- IMAGE: keyword --> \ub9c8\ucee4\ub294 LLM\uc774 \ucf58\ud150\uce20 \ub9e5\ub77d\uc5d0 \ub9de\uac8c \uc0dd\uc131\ud558\uba70,\n * Unsplash API\ub85c \uac80\uc0c9\ud558\uc5ec <figure> \ud0dc\uadf8\ub85c \uce58\ud658\ud55c\ub2e4.\n */\n\nconst zlib = require('zlib');\n\nconst content = $input.item.json.content || '';\nconst title = $input.item.json.title || '';\nconst UNSPLASH_KEY = $env.UNSPLASH_ACCESS_KEY;\nconst helpers = this.helpers;\n\nconst MAX_SECTION_IMAGES = 2;\n\n/**\n * IT \uae30\uc220 \ud0a4\uc6cc\ub4dc \u2192 Unsplash \uac80\uc0c9 \uce5c\ud654 \ud0a4\uc6cc\ub4dc \ub9e4\ud551\n * thumbnail/hero \uac80\uc0c9\uc6a9\uc73c\ub85c \uc720\uc9c0\n */\nconst KEYWORD_BROADENING = {\n  terraform: 'cloud infrastructure automation',\n  ansible: 'server automation terminal',\n  jenkins: 'software development pipeline',\n  kubernetes: 'cloud container technology',\n  docker: 'container technology server',\n  'ci/cd': 'software deployment automation',\n  cicd: 'software deployment automation',\n  ssl: 'cybersecurity encryption lock',\n  tls: 'cybersecurity encryption lock',\n  siem: 'cybersecurity monitoring dashboard',\n  splunk: 'data analytics dashboard',\n  elk: 'data analytics search',\n  prometheus: 'server monitoring dashboard',\n  grafana: 'monitoring dashboard visualization',\n  dns: 'network technology server',\n  vpn: 'network security connection',\n  saml: 'authentication security login',\n  oauth: 'authentication security login',\n  iam: 'identity access management security',\n  mfa: 'two factor authentication security',\n  'sd-wan': 'network infrastructure',\n  microservice: 'software architecture diagram',\n  'zero trust': 'cybersecurity network',\n  iac: 'infrastructure as code automation',\n};\n\n/**\n * Unsplash API\uc5d0\uc11c \uc774\ubbf8\uc9c0 \uac80\uc0c9\n * @param {string} keyword - \uac80\uc0c9 \ud0a4\uc6cc\ub4dc\n * @param {Set<string>} usedIds - \uac19\uc740 \uae00 \ub0b4 \uc0ac\uc9c4 \uc911\ubcf5 \ubc29\uc9c0\uc6a9 ID Set\n * @returns {object|null} { id, thumb, url, photographer, photographerUrl }\n */\nasync function searchUnsplash(keyword, usedIds) {\n  if (!UNSPLASH_KEY) return null;\n\n  try {\n    const data = await helpers.httpRequest({\n      method: 'GET',\n      url: 'https://api.unsplash.com/search/photos',\n      qs: {\n        query: keyword,\n        orientation: 'landscape',\n        per_page: '10',\n        client_id: UNSPLASH_KEY,\n      },\n    });\n    if (!data.results || data.results.length === 0) return null;\n\n    // usedIds\uc5d0 \uc5c6\ub294 \uc0ac\uc9c4 \uc911 \ub79c\ub364 \uc120\ud0dd\n    const available = data.results.filter(p => !usedIds.has(p.id));\n    if (available.length === 0) return null;\n\n    const idx = Math.floor(Math.random() * Math.min(available.length, 6));\n    const photo = available[idx];\n\n    return {\n      id: photo.id,\n      thumb: photo.urls.small,\n      url: photo.urls.regular,\n      photographer: photo.user.name || 'Unknown',\n      photographerUrl: photo.user.links?.html || 'https://unsplash.com',\n    };\n  } catch (e) {\n    return null;\n  }\n}\n\n/**\n * Unsplash \uc774\ubbf8\uc9c0\ub97c <figure> \ud0dc\uadf8\ub85c \uc0dd\uc131\n * @param {object} photo - searchUnsplash \ubc18\ud658\uac12\n * @param {string} altText - alt \uc18d\uc131 \ud14d\uc2a4\ud2b8\n * @param {boolean} isHero - Hero \uc774\ubbf8\uc9c0 \uc5ec\ubd80 (fetchpriority=\"high\")\n * @returns {string} <figure> HTML\n */\nfunction unsplashFigureTag(photo, altText, isHero) {\n  const safeAlt = altText.replace(/\"/g, '&quot;');\n  const priority = isHero ? ' fetchpriority=\"high\"' : ' loading=\"lazy\"';\n  const imgUrl = isHero ? photo.url : photo.url;\n  return (\n    `<figure style=\"max-width:100%;border-radius:8px;margin:24px 0;\">` +\n    `<img src=\"${imgUrl}\" alt=\"${safeAlt}\"${priority} decoding=\"async\" ` +\n    `style=\"max-width:100%;height:auto;border-radius:8px;\" />` +\n    `<!-- Photo by ${photo.photographer} on Unsplash -->` +\n    `</figure>`\n  );\n}\n\n/**\n * \uc81c\ubaa9\uc5d0\uc11c Unsplash \uac80\uc0c9\uc6a9 \ud0a4\uc6cc\ub4dc \ud6c4\ubcf4 \ubc30\uc5f4 \uc0dd\uc131 (hero/thumbnail\uc6a9)\n */\nfunction extractThumbnailKeywords() {\n  const candidates = [];\n\n  const titleEng = title.match(/[A-Za-z0-9][\\w./-]*/g) || [];\n  if (titleEng.length > 0) {\n    candidates.push(titleEng.join(' '));\n  }\n\n  const lowerTitle = title.toLowerCase();\n  for (const [tech, broad] of Object.entries(KEYWORD_BROADENING)) {\n    if (lowerTitle.includes(tech)) {\n      candidates.push(broad);\n      break;\n    }\n  }\n\n  candidates.push('technology software development');\n  return candidates;\n}\n\n/**\n * Mermaid \ucf54\ub4dc\ub97c kroki.io GET URL\ub85c \ubcc0\ud658\n * deflate \uc555\ucd95 + base64url \uc778\ucf54\ub529 \u2192 https://kroki.io/mermaid/svg/{encoded}\n * Google Sheets 50K \uc81c\ud55c \ud68c\ud53c\ub97c \uc704\ud574 SVG \uc784\ubca0\ub529 \ub300\uc2e0 URL \ucc38\uc870 \uc0ac\uc6a9\n * @param {string} code - Mermaid \ucf54\ub4dc (``` \uc81c\uc678)\n * @returns {string|null} kroki.io URL \ub610\ub294 null\n */\nfunction mermaidToKrokiUrl(code) {\n  try {\n    const deflated = zlib.deflateSync(Buffer.from(code, 'utf-8'));\n    const encoded = deflated.toString('base64url');\n    return `https://kroki.io/mermaid/svg/${encoded}`;\n  } catch (e) {\n    return null;\n  }\n}\n\n/**\n * Mermaid \ucf54\ub4dc\ub97c <img> \ud0dc\uadf8\ub85c \ubcc0\ud658 (kroki.io GET URL \ucc38\uc870)\n * @param {string} code - Mermaid \ucf54\ub4dc\n * @param {string} altText - alt \uc18d\uc131 \ud14d\uc2a4\ud2b8\n * @returns {string|null} <img> \ud0dc\uadf8 \ub610\ub294 null\n */\nfunction mermaidToImgTag(code, altText) {\n  const url = mermaidToKrokiUrl(code);\n  if (!url) return null;\n  const safeAlt = altText.replace(/\"/g, '&quot;');\n  return `<img src=\"${url}\" alt=\"${safeAlt}\" width=\"800\" height=\"auto\" style=\"max-width:100%;height:auto;margin:16px 0;\" />`;\n}\n\n// \uae00 \ub0b4 Unsplash \uc774\ubbf8\uc9c0 \uc911\ubcf5 \ubc29\uc9c0\uc6a9 Set\nconst usedPhotoIds = new Set();\nlet unsplashApiCalls = 0;\n\n// === Step 1: [MERMAID]...[/MERMAID] \ub9c8\ucee4 \ucd94\ucd9c ===\n// LLM\uc774 JSON \uc548\uc5d0\uc11c ```mermaid \ub300\uc2e0 [MERMAID]...[/MERMAID] \ub9c8\ucee4\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc9c0\uc2dc\ub428\n// (JSON fence \ucda9\ub3cc \ubc0f Gemini \uc751\ub2f5 truncation \ubc29\uc9c0)\nconst mermaidRegex = /\\[MERMAID\\]([\\s\\S]*?)\\[\\/MERMAID\\]/g;\nconst blocks = [];\nlet match;\n\nwhile ((match = mermaidRegex.exec(content)) !== null) {\n  blocks.push({\n    full: match[0],\n    code: match[1].trim(),\n    index: match.index,\n  });\n}\n\nlet updatedContent = content;\nlet renderedCount = 0;\nlet failedCount = 0;\n\n// === Step 2: \uac01 [MERMAID] \ube14\ub85d \u2192 kroki.io URL <img> \ud0dc\uadf8 (\uc5ed\uc21c \uce58\ud658) ===\nfor (let i = blocks.length - 1; i >= 0; i--) {\n  const block = blocks[i];\n  const firstLine = block.code.split('\\n')[0].trim();\n  const altText = `Mermaid diagram: ${firstLine}`;\n  const imgTag = mermaidToImgTag(block.code, altText);\n\n  if (imgTag) {\n    updatedContent =\n      updatedContent.substring(0, block.index) +\n      '\\n' + imgTag + '\\n' +\n      updatedContent.substring(block.index + block.full.length);\n    renderedCount++;\n  } else {\n    // URL \uc0dd\uc131 \uc2e4\ud328 \uc2dc \uc6d0\ubcf8 \ucf54\ub4dc\ube14\ub85d \uc720\uc9c0 (graceful degradation)\n    failedCount++;\n  }\n}\n\n// === Step 3: <!-- IMAGE: keyword --> \ub9c8\ucee4 \u2192 Unsplash <figure> (\uc5ed\uc21c \uce58\ud658) ===\nconst imageMarkerRegex = /<!-- IMAGE:\\s*(.+?)\\s*-->/g;\nconst imageMarkers = [];\n\nwhile ((match = imageMarkerRegex.exec(updatedContent)) !== null) {\n  imageMarkers.push({\n    full: match[0],\n    keyword: match[1].trim(),\n    index: match.index,\n  });\n}\n\nlet sectionImagesInjected = 0;\n\nfor (let i = imageMarkers.length - 1; i >= 0; i--) {\n  const marker = imageMarkers[i];\n\n  if (sectionImagesInjected >= MAX_SECTION_IMAGES) {\n    // \ucd08\uacfc\ubd84\uc740 \ub9c8\ucee4\ub9cc \uc81c\uac70\n    updatedContent =\n      updatedContent.substring(0, marker.index) +\n      updatedContent.substring(marker.index + marker.full.length);\n    continue;\n  }\n\n  unsplashApiCalls++;\n  const photo = await searchUnsplash(marker.keyword, usedPhotoIds);\n\n  if (photo) {\n    usedPhotoIds.add(photo.id);\n    const figureTag = unsplashFigureTag(photo, marker.keyword, false);\n    updatedContent =\n      updatedContent.substring(0, marker.index) +\n      '\\n' + figureTag + '\\n' +\n      updatedContent.substring(marker.index + marker.full.length);\n    sectionImagesInjected++;\n  } else {\n    // \uc2e4\ud328 \uc2dc \ub9c8\ucee4 \uc81c\uac70 (graceful degradation)\n    updatedContent =\n      updatedContent.substring(0, marker.index) +\n      updatedContent.substring(marker.index + marker.full.length);\n  }\n}\n\n// === Step 4: Hero \uc774\ubbf8\uc9c0 + thumbnail_url \u2014 Unsplash \uac80\uc0c9 ===\nlet thumbnailUrl = '';\nlet hasHeroImage = false;\n\nif (UNSPLASH_KEY) {\n  const keywords = extractThumbnailKeywords();\n  for (const kw of keywords) {\n    unsplashApiCalls++;\n    const result = await searchUnsplash(kw, usedPhotoIds);\n    if (result) {\n      usedPhotoIds.add(result.id);\n\n      // Hero: \uccab H2 \uc55e\uc5d0 <figure> \uc0bd\uc785\n      const firstH2 = updatedContent.match(/^## /m);\n      if (firstH2) {\n        const h2Index = updatedContent.indexOf(firstH2[0]);\n        const heroFigure = unsplashFigureTag(result, title, true);\n        updatedContent =\n          updatedContent.substring(0, h2Index) +\n          heroFigure + '\\n\\n' +\n          updatedContent.substring(h2Index);\n        hasHeroImage = true;\n      }\n\n      // Thumbnail: small URL (OG \uc774\ubbf8\uc9c0)\n      thumbnailUrl = result.thumb;\n      break;\n    }\n  }\n}\n\nreturn {\n  json: {\n    ...$input.item.json,\n    content: updatedContent,\n    thumbnail_url: thumbnailUrl,\n    image_injection: {\n      method: 'hybrid_mermaid_unsplash',\n      mermaid_found: blocks.length,\n      mermaid_rendered: renderedCount,\n      mermaid_failed: failedCount,\n      section_images_injected: sectionImagesInjected,\n      has_hero_image: hasHeroImage,\n      unsplash_api_calls: unsplashApiCalls,\n      has_unsplash_key: !!UNSPLASH_KEY,\n    },\n  },\n};\n"
      },
      "id": "node-6a-inject-images",
      "name": "Inject Images",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2500,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Validate Structure \u2014 \ub9c8\ud06c\ub2e4\uc6b4 \uad6c\uc870 \uc790\ub3d9 \uac80\uc99d\n * Mode: runOnceForEachItem\n * Parse JSON\uacfc Gemini Verification \uc0ac\uc774\uc5d0 \ubc30\uce58\n * \uc785\ub825: \ud30c\uc2f1\ub41c JSON (content \ud544\ub4dc \ud3ec\ud568)\n * \ucd9c\ub825: \uad6c\uc870 \uac80\uc99d \uacb0\uacfc \ucd94\uac00\n */\n\nconst content = $input.item.json.content || '';\nconst promptType = $('Route Prompt (A/B/C)').item.json.prompt_type || 'A';\n\nconst issues = [];\n\n// 0. Content length check\nconst MIN_CONTENT_LENGTH = 3000;\nif (content.length < MIN_CONTENT_LENGTH) {\n  issues.push(`\ubcf8\ubb38 \uae38\uc774 \ubd80\uc871: ${content.length}\uc790 (\ucd5c\uc18c ${MIN_CONTENT_LENGTH}\uc790)`);\n}\n\n// 1. H2 \ud5e4\ub529 \uce74\uc6b4\ud2b8\nconst h2Matches = content.match(/^## /gm) || [];\nconst h2Count = h2Matches.length;\n\nconst MIN_H2 = { 'A': 4, 'B': 5, 'C': 4 };\nconst minH2 = MIN_H2[promptType] || 4;\n\nif (h2Count < minH2) {\n  issues.push(`H2 \ud5e4\ub529 \ubd80\uc871: ${h2Count}\uac1c (\ucd5c\uc18c ${minH2}\uac1c)`);\n}\n\n// 2. H3 \ud5e4\ub529 \uce74\uc6b4\ud2b8\nconst h3Matches = content.match(/^### /gm) || [];\nconst h3Count = h3Matches.length;\n\n// 3. \ub9c8\ud06c\ub2e4\uc6b4 \ud14c\uc774\ube14 \ud589 \uc218\nconst tableRows = content.match(/^\\|.*\\|$/gm) || [];\nconst tableSeparators = content.match(/^\\|[\\s\\-:|]+\\|$/gm) || [];\nconst tableDataRows = tableRows.length - tableSeparators.length;\n\nif (promptType === 'A' || promptType === 'B') {\n  if (tableDataRows < 1) {\n    issues.push('\ub9c8\ud06c\ub2e4\uc6b4 \ud14c\uc774\ube14 \uc5c6\uc74c (\ucd5c\uc18c 1\uac1c \ud544\uc694)');\n  }\n  if (promptType === 'B' && tableDataRows < 7) {\n    issues.push(`\ube44\uad50\ud45c \ud589 \ubd80\uc871: ${tableDataRows}\ud589 (\ucd5c\uc18c 7\ud589)`);\n  }\n}\n\n// 4. \ucf54\ub4dc\ube14\ub85d \uc218\nconst codeBlocks = content.match(/```\\w*/g) || [];\nconst codeBlockCount = Math.floor(codeBlocks.length / 2);\n\nif (promptType === 'C' && codeBlockCount < 3) {\n  issues.push(`\ucf54\ub4dc\ube14\ub85d \ubd80\uc871: ${codeBlockCount}\uac1c (\ucd5c\uc18c 3\uac1c)`);\n}\n\n// 5. FAQ \uc139\uc158 \uc874\uc7ac \uc5ec\ubd80\nconst hasFAQ = /##\\s*FAQ/i.test(content) || /##.*\uc790\uc8fc\\s*\ubb3b\ub294/i.test(content);\nif (!hasFAQ) {\n  issues.push('FAQ \uc139\uc158 \uc5c6\uc74c');\n}\n\n// 6. faq_schema \uac80\uc99d\nconst faqSchema = $input.item.json.faq_schema || [];\nif (faqSchema.length < 3) {\n  issues.push(`FAQ \ud56d\ubaa9 \ubd80\uc871: ${faqSchema.length}\uac1c (\ucd5c\uc18c 3\uac1c)`);\n}\nconst shortAnswers = faqSchema.filter(f => f.answer && f.answer.length < 80);\nif (shortAnswers.length > 0) {\n  issues.push(`FAQ \ub2f5\ubcc0 80\uc790 \ubbf8\ub9cc: ${shortAnswers.length}\uac74`);\n}\n\n// 7. IMAGE \ub9c8\ucee4 \uc794\ub958 \uac80\uc0ac (inject_images.js \uc774\ud6c4\uc774\ubbc0\ub85c \uc794\ub958\ud558\uba74 \uc548\ub428)\nconst imageMarkers = content.match(/<!-- IMAGE:\\s*.+?\\s*-->/g) || [];\nif (imageMarkers.length > 0) {\n  issues.push(`\ubbf8\ucc98\ub9ac IMAGE \ub9c8\ucee4 ${imageMarkers.length}\uac1c \uc794\ub958`);\n}\n\n// 8. \uc774\ubbf8\uc9c0 \uac1c\uc218 (\ub9c8\ud06c\ub2e4\uc6b4 + HTML img \ud3ec\ud568)\nconst imageCount = (content.match(/!\\[.*?\\]\\(.*?\\)/g) || []).length;\nconst htmlImageCount = (content.match(/<img\\s/g) || []).length;\n\n// 8-1. image_injection \uba54\ud0c0\ub370\uc774\ud130\uc5d0\uc11c \uc139\uc158/\ud788\uc5b4\ub85c \uc815\ubcf4 \ucd94\ucd9c\nconst imageInjection = $input.item.json.image_injection || {};\nconst sectionImages = imageInjection.section_images_injected || 0;\nconst hasHeroImage = imageInjection.has_hero_image || false;\n\n// 9. Mermaid \ub9c8\ucee4 \uc794\ub958 \uac10\uc9c0 (WARNING only \u2014 hard fail \uc544\ub2d8)\n// [MERMAID] \uc794\ub958 > 0\uc774\uba74 inject_images.js\uac00 \ub80c\ub354\ub9c1 \uc2e4\ud328\ud55c \uac83\n// \ucf58\ud150\uce20 \uc790\uccb4\ub294 \uc720\ud6a8\ud558\ubbc0\ub85c warning only\nconst mermaidResidues = content.match(/\\[MERMAID\\]/g) || [];\nconst mermaidWarning = mermaidResidues.length > 0\n  ? `Mermaid \ub9c8\ucee4 ${mermaidResidues.length}\uac1c \ubbf8\ub80c\ub354\ub9c1 (\uacbd\uace0)`\n  : null;\n\nconst passed = issues.length === 0;\n\nreturn {\n  json: {\n    ...$input.item.json,\n    structure_validation: {\n      passed,\n      h2_count: h2Count,\n      h3_count: h3Count,\n      table_rows: tableDataRows,\n      code_block_count: codeBlockCount,\n      has_faq_section: hasFAQ,\n      faq_count: faqSchema.length,\n      image_count: imageCount,\n      html_image_count: htmlImageCount,\n      section_images: sectionImages,\n      has_hero_image: hasHeroImage,\n      mermaid_residue: mermaidResidues.length,\n      mermaid_warning: mermaidWarning,\n      issues,\n    }\n  }\n};\n"
      },
      "id": "node-6b-validate-structure",
      "name": "Validate Structure",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Node 7a: URL \uac80\uc99d \u2014 HTTP HEAD + SERP \uad50\ucc28 \ub300\uc870 + \uc8fd\uc740 URL \uc790\ub3d9 \uc81c\uac70\n * Mode: runOnceForEachItem\n *\n * \uc8fc\uc758: serp_urls\ub294 Normalize Response/Parse JSON\uc5d0\uc11c \uc18c\uc2e4\ub418\ubbc0\ub85c\n *       $('Parse SERP Data') \ub178\ub4dc\uc5d0\uc11c \uc9c1\uc811 \ucc38\uc870\ud55c\ub2e4.\n */\n\nconst helpers = this.helpers;\nconst item = $input.item.json;\nconst content = item.content || '';\nconst references = item.references || [];\n\n// serp_urls\ub294 \ud30c\uc774\ud504\ub77c\uc778 \uc911\uac04\uc5d0\uc11c \uc18c\uc2e4 \u2192 Parse SERP Data \ub178\ub4dc\uc5d0\uc11c \uc9c1\uc811 \ucc38\uc870\nlet serpUrls = [];\ntry {\n  serpUrls = $('Parse SERP Data').item.json.serp_urls || [];\n} catch { /* \ub178\ub4dc \ubbf8\uc874\uc7ac \uc2dc \ube48 \ubc30\uc5f4 */ }\n\n// URL \ucd94\ucd9c (trailing punctuation \uc81c\uac70)\nconst urlRegex = /https?:\\/\\/[^\\s\\)\\]\"'<>]+/g;\nconst rawUrls = content.match(urlRegex) || [];\nconst contentUrls = rawUrls.map(u => u.replace(/[.,;:!?)]+$/, ''));\n\n// \ubaa8\ub4e0 \uace0\uc720 URL \uc218\uc9d1 (content + references)\nconst allUrls = [...new Set([\n  ...contentUrls,\n  ...references.filter(r => r.startsWith('http')),\n])];\n\nconst issues = [];\nconst deadUrls = [];\nconst serpMatched = [];\nconst serpUnmatched = [];\n\n// SERP \ub3c4\uba54\uc778 \ucd94\ucd9c (\uad50\ucc28 \ub300\uc870\uc6a9)\nconst serpDomains = new Set();\nfor (const url of serpUrls) {\n  try { serpDomains.add(new URL(url).hostname); } catch {}\n}\n\n/**\n * HTTP \uac80\uc99d \u2014 try/catch \ud328\ud134 (axios \uae30\ubc18)\n * \uc131\uacf5(2xx) = \ubc18\ud658\uac12 \uc874\uc7ac, \uc2e4\ud328(4xx/5xx/\ud0c0\uc784\uc544\uc6c3) = \uc608\uc678 \ubc1c\uc0dd\n */\nasync function checkUrl(url) {\n  // \ud615\uc2dd \uac80\uc99d (HTTP \uc2a4\ud0b5)\n  if (url.includes('...') || url.includes(' ')) {\n    return { url, status: 'broken_format' };\n  }\n  if (url.includes('contoso.com') || url.includes('example.com')) {\n    return { url, status: 'placeholder' };\n  }\n\n  // HEAD \uc2dc\ub3c4\n  try {\n    await helpers.httpRequest({\n      method: 'HEAD',\n      url: url,\n      timeout: 5000,\n    });\n    return { url, status: 'ok' };\n  } catch {\n    // HEAD \uc2e4\ud328 \u2192 GET \ud3f4\ubc31 (\uc77c\ubd80 \uc11c\ubc84 HEAD \uac70\ubd80)\n    try {\n      await helpers.httpRequest({\n        method: 'GET',\n        url: url,\n        timeout: 5000,\n        maxContentLength: 1024,\n      });\n      return { url, status: 'ok' };\n    } catch {\n      return { url, status: 'dead' };\n    }\n  }\n}\n\n// \ubcd1\ub82c \uac80\uc99d (\ubc30\uce58 5\uac1c)\nconst results = [];\nfor (let i = 0; i < allUrls.length; i += 5) {\n  const batch = allUrls.slice(i, i + 5);\n  const batchResults = await Promise.all(batch.map(checkUrl));\n  results.push(...batchResults);\n}\n\n// \uacb0\uacfc \ubd84\ub958\nfor (const r of results) {\n  if (r.status === 'placeholder') continue;\n\n  if (r.status === 'broken_format' || r.status === 'dead') {\n    issues.push(`\uc811\uadfc \ubd88\uac00: ${r.url}`);\n    deadUrls.push(r.url);\n  }\n\n  // SERP \uad50\ucc28 \ub300\uc870\n  try {\n    const domain = new URL(r.url).hostname;\n    if (serpDomains.has(domain)) {\n      serpMatched.push(r.url);\n    } else {\n      serpUnmatched.push(r.url);\n    }\n  } catch {}\n}\n\n// \uc8fd\uc740 URL \uc81c\uac70: [\ud14d\uc2a4\ud2b8](deadUrl) \u2192 \ud14d\uc2a4\ud2b8\nlet cleanedContent = content;\nfor (const deadUrl of deadUrls) {\n  const escaped = deadUrl.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n  cleanedContent = cleanedContent.replace(\n    new RegExp(`\\\\[([^\\\\]]+)\\\\]\\\\(${escaped}\\\\)`, 'g'),\n    '$1'\n  );\n}\n\n// \uc8fd\uc740 URL\uc744 references\uc5d0\uc11c\ub3c4 \uc81c\uac70\nconst cleanedReferences = references.filter(ref => {\n  if (!ref.startsWith('http')) return true;\n  return !deadUrls.includes(ref);\n});\n\nconst passed = deadUrls.length === 0;\n\nreturn [{\n  json: {\n    ...item,\n    content: cleanedContent,\n    references: cleanedReferences,\n    url_validation: {\n      passed,\n      total_urls: allUrls.length,\n      reachable: results.filter(r => r.status === 'ok').length,\n      dead: deadUrls.length,\n      serp_matched: serpMatched.length,\n      serp_unmatched: serpUnmatched.length,\n      issues,\n    }\n  }\n}];\n"
      },
      "id": "node-7a-url-validate",
      "name": "URL Validation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2900,
        200
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Node 7b: \ucf54\ub4dc \ube14\ub85d \uae30\ucd08 \ud3ec\ub9f7 \uac80\uc0ac\n * \uc5b8\uc5b4 \ud0dc\uadf8 \uc874\uc7ac \uc5ec\ubd80, \ub2eb\ud788\uc9c0 \uc54a\uc740 \ube14\ub85d \uac10\uc9c0\n */\n\nconst content = $input.item.json.content;\n\nconst issues = [];\n\n// 1. \ucf54\ub4dc \ube14\ub85d \ucd94\ucd9c (\uc5f4\uae30/\ub2eb\uae30 \uc30d)\nconst openBlocks = (content.match(/```/g) || []).length;\nif (openBlocks % 2 !== 0) {\n  issues.push(\"\ub2eb\ud788\uc9c0 \uc54a\uc740 \ucf54\ub4dc \ube14\ub85d \uc874\uc7ac\");\n}\n\n// 2. \uc5b8\uc5b4 \ud0dc\uadf8 \uc5c6\ub294 \ucf54\ub4dc \ube14\ub85d \uac10\uc9c0\nconst codeBlockRegex = /```(\\w*)\\n/g;\nlet match;\nlet blockCount = 0;\nlet noTagCount = 0;\n\nwhile ((match = codeBlockRegex.exec(content)) !== null) {\n  blockCount++;\n  if (!match[1]) {\n    noTagCount++;\n  }\n}\n\nif (noTagCount > 0) {\n  issues.push(`\uc5b8\uc5b4 \ud0dc\uadf8 \uc5c6\ub294 \ucf54\ub4dc \ube14\ub85d: ${noTagCount}/${blockCount}\uac74`);\n}\n\n// 3. \ud5c8\uc6a9 \uc5b8\uc5b4 \ud0dc\uadf8 \uac80\uc99d\nconst allowedLangs = [\n  \"powershell\", \"bash\", \"cmd\", \"python\", \"json\", \"xml\", \"yaml\",\n  \"csharp\", \"javascript\", \"html\", \"sql\", \"text\", \"plaintext\",\n  \"shell\", \"ps1\", \"bat\",\n];\nconst langRegex = /```(\\w+)/g;\nlet langMatch;\nwhile ((langMatch = langRegex.exec(content)) !== null) {\n  const lang = langMatch[1].toLowerCase();\n  if (!allowedLangs.includes(lang)) {\n    issues.push(`\ube44\ud45c\uc900 \uc5b8\uc5b4 \ud0dc\uadf8: ${langMatch[1]}`);\n  }\n}\n\nconst passed = issues.length === 0;\n\nreturn {\n  json: {\n    ...$input.item.json,\n    code_lint: {\n      passed,\n      block_count: blockCount,\n      issues,\n    }\n  }\n};\n"
      },
      "id": "node-7b-code-lint",
      "name": "Code Block Lint",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2900,
        400
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Build Verify Request \u2014 \uac80\uc99d\uc6a9 system_prompt/user_message \uc0dd\uc131\n * \uc785\ub825: \ud30c\uc2f1\ub41c \ucf58\ud150\uce20 (title, content \ub4f1)\n * \ucd9c\ub825: system_prompt, user_message, _llm_purpose + \uc6d0\ubcf8 \ub370\uc774\ud130 \ud328\uc2a4\uc2a4\ub8e8\n */\n\nconst title = $input.item.json.title || '';\nconst content = $input.item.json.content || '';\n\nreturn {\n  json: {\n    ...$input.item.json,\n    system_prompt: '\ub2f9\uc2e0\uc740 B2B IT \uae30\uc220 \ucf58\ud150\uce20 \ud488\uc9c8 \uac80\uc99d AI\uc785\ub2c8\ub2e4. \uc544\ub798 \ube14\ub85c\uadf8 \uae00\uc744 \uac80\ud1a0\ud558\uace0, \uc815\ud655\ud788 \ub2e4\uc12f \ud56d\ubaa9\uc744 \ud3c9\uac00\ud558\uc138\uc694. \ubc18\ub4dc\uc2dc JSON \ud615\uc2dd\uc73c\ub85c\ub9cc \uc751\ub2f5: {\"is_accurate\": true/false, \"is_logical\": true/false, \"is_complete\": true/false, \"is_useful\": true/false, \"is_in_depth\": true/false, \"quality_score\": 0-100, \"reason\": \"\uc0ac\uc720 100\uc790 \uc774\ub0b4\"}',\n    user_message: `\uc81c\ubaa9: ${title}\\n\ubcf8\ubb38:\\n${content}`,\n    _llm_purpose: 'verification',\n  }\n};\n"
      },
      "id": "node-build-verify-request",
      "name": "Build Verify Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Build LLM Request (Verify) \u2014 provider\ubcc4 URL/headers/body \uc0dd\uc131\n * verification\uc6a9: maxTokens=800, temperature=0\n */\n\nconst provider = $env.LLM_PROVIDER || 'gemini';\nconst systemPrompt = $input.item.json.system_prompt;\nconst userMessage = $input.item.json.user_message;\nconst purpose = $input.item.json._llm_purpose || 'content_gen';\n\nconst PROVIDERS = {\n  gemini: {\n    url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${$env.GOOGLE_API_KEY}`,\n    headers: { 'content-type': 'application/json' },\n    body: (sys, user, opts) => ({\n      contents: [{ role: 'user', parts: [{ text: user }] }],\n      systemInstruction: { parts: [{ text: sys }] },\n      generationConfig: { maxOutputTokens: opts.maxTokens, temperature: opts.temperature }\n    }),\n  },\n  claude: {\n    url: 'https://api.anthropic.com/v1/messages',\n    headers: {\n      'x-api-key': $env.CLAUDE_API_KEY,\n      'anthropic-version': '2023-06-01',\n      'content-type': 'application/json',\n    },\n    body: (sys, user, opts) => ({\n      model: opts.model || 'claude-sonnet-4-5-20250514',\n      max_tokens: opts.maxTokens,\n      temperature: opts.temperature,\n      system: sys,\n      messages: [{ role: 'user', content: user }],\n    }),\n  },\n};\n\nconst config = PROVIDERS[provider];\nif (!config) throw new Error(`\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 LLM provider: ${provider}`);\n\nconst opts = purpose === 'verification'\n  ? { maxTokens: 800, temperature: 0, model: 'claude-haiku-4-5-20251001' }\n  : { maxTokens: 32768, temperature: 0.7 };\n\nreturn {\n  json: {\n    _llm_url: config.url,\n    _llm_headers: config.headers,\n    _llm_body: config.body(systemPrompt, userMessage, opts),\n    _llm_provider: provider,\n    ...Object.fromEntries(\n      Object.entries($input.item.json).filter(([k]) => !k.startsWith('_llm'))\n    ),\n  }\n};\n"
      },
      "id": "node-build-llm-verify",
      "name": "Build LLM Request (Verify)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3260,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json._llm_url }}",
        "sendHeaders": true,
        "specifyHeaders": "json",
        "jsonHeaders": "={{ JSON.stringify($json._llm_headers) }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json._llm_body) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "node-7c-llm-verify",
      "name": "LLM Request (Verify)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3440,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 15000
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Normalize LLM Response (Verify) \u2014 provider\ubcc4 \uc751\ub2f5\uc5d0\uc11c \ud14d\uc2a4\ud2b8 \ucd94\ucd9c\n * Gemini: candidates[0].content.parts[0].text\n * Claude: content[0].text\n */\n\nconst provider = $input.item.json._llm_provider\n  || $env.LLM_PROVIDER\n  || 'gemini';\n\nlet text;\nconst data = $input.item.json;\n\nif (provider === 'gemini') {\n  text = data.candidates[0].content.parts[0].text;\n} else if (provider === 'claude') {\n  text = data.content[0].text;\n} else {\n  throw new Error(`\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 provider \uc751\ub2f5: ${provider}`);\n}\n\nreturn { json: { text, _llm_provider: provider } };\n"
      },
      "id": "node-normalize-verify",
      "name": "Normalize Response (Verify)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3620,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "/**\n * Node 8: LLM \uad50\ucc28 \uac80\uc99d \uacb0\uacfc \ud30c\uc2f1\n * Mode: runOnceForEachItem\n * \uc785\ub825: \uc815\uaddc\ud654\ub41c LLM \uc751\ub2f5 (text)\n * \ucd9c\ub825: \uac80\uc99d \ud1b5\uacfc/\uc2e4\ud328 \ud310\uc815 (5\ud56d\ubaa9 + quality_score)\n */\n\nconst raw = $input.item.json.text;\n\n// JSON \ubb38\uc790\uc5f4 \uc815\ub9ac (\uc81c\uc5b4 \ubb38\uc790 + \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \uc774\uc2a4\ucf00\uc774\ud504 \ubcf5\uad6c)\nfunction sanitizeJsonStrings(str) {\n  const VALID_ESCAPES = '\"\\\\\\/bfnrtu';\n  let result = '';\n  let inString = false;\n  for (let i = 0; i < str.length; i++) {\n    const ch = str[i];\n    if (inString) {\n      if (ch === '\\\\' && i + 1 < str.length) {\n        const next = str[i + 1];\n        if (VALID_ESCAPES.includes(next)) {\n          result += ch + next;\n        } else {\n          result += '\\\\\\\\' + next;\n        }\n        i++;\n        continue;\n      }\n      if (ch === '\"') {\n        inString = false;\n        result += ch;\n        continue;\n      }\n      const code = ch.charCodeAt(0);\n      if (code <= 0x1f) {\n        if (code === 0x0a) result += '\\\\n';\n        else if (code === 0x0d) result += '\\\\r';\n        else if (code === 0x09) result += '\\\\t';\n        continue;\n      }\n      result += ch;\n    } else {\n      if (ch === '\"') inString = true;\n      result += ch;\n    }\n  }\n  return result;\n}\n\n// JSON \ucd94\ucd9c (\ubc29\uc5b4\uc801 \ud30c\uc2f1)\nlet result;\ntry {\n  const match = raw.match(/\\{[\\s\\S]*\\}/);\n  if (!match) {\n    throw new Error(\"JSON \uc5c6\uc74c\");\n  }\n  let jsonStr = sanitizeJsonStrings(match[0]);\n  // \uad6c\uc870 \ubcf5\uad6c: \ud6c4\ud589 \uc27c\ud45c \uc81c\uac70\n  jsonStr = jsonStr.replace(/,\\s*([}\\]])/g, '$1');\n  result = JSON.parse(jsonStr);\n} catch (e) {\n  // LLM \ud30c\uc2f1 \uc2e4\ud328 \uc2dc \uac80\uc218\ud544\uc694\ub85c \ubd84\ub958\n  return {\n    json: {\n      ...$input.item.json,\n      verification: {\n        passed: false,\n        is_accurate: null,\n        is_logical: null,\n        is_complete: null,\n        is_useful: null,\n        quality_score: 0,\n        reason: `LLM \uc751\ub2f5 \ud30c\uc2f1 \uc2e4\ud328: ${e.message}`,\n        raw_response: raw.substring(0, 300),\n      }\n    }\n  };\n}\n\n// \ud544\uc218 \ud544\ub4dc \uac80\uc99d (\uae30\uc874 2\ud56d\ubaa9\uc740 \ud544\uc218, \uc2e0\uaddc 2\ud56d\ubaa9\uc740 \uae30\ubcf8\uac12 \ucc98\ub9ac)\nif (typeof result.is_accurate !== \"boolean\" || typeof result.is_logical !== \"boolean\") {\n  return {\n    json: {\n      ...$input.item.json,\n      verification: {\n        passed: false,\n        is_accurate: result.is_accurate ?? null,\n        is_logical: result.is_logical ?? null,\n        is_complete: result.is_complete ?? null,\n        is_useful: result.is_useful ?? null,\n        quality_score: result.quality_score ?? 0,\n        reason: \"\uac80\uc99d \uacb0\uacfc \ud615\uc2dd \uc624\ub958 (boolean \uc544\ub2d8)\",\n      }\n    }\n  };\n}\n\n// \uc2e0\uaddc \ud56d\ubaa9 \uae30\ubcf8\uac12 (LLM\uc774 \ub204\ub77d\ud558\uba74 true\ub85c \uac04\uc8fc \u2014 \ud558\uc704 \ud638\ud658)\nconst isComplete = typeof result.is_complete === \"boolean\" ? result.is_complete : true;\nconst isUseful = typeof result.is_useful === \"boolean\" ? result.is_useful : true;\nconst isInDepth = typeof result.is_in_depth === \"boolean\" ? result.is_in_depth : true;\nconst qualityScore = typeof result.quality_score === \"number\" ? result.quality_score : 0;\n\nconst MIN_QUALITY_SCORE = 70;\nconst passed = result.is_accurate === true\n  && result.is_logical === true\n  && isComplete === true\n  && isUseful === true\n  && isInDepth === true\n  && qualityScore >= MIN_QUALITY_SCORE;\n\nreturn {\n  json: {\n    ...$input.item.json,\n    verification: {\n      passed,\n      is_accurate: result.is_accurate,\n      is_logical: result.is_logical,\n      is_complete: isComplete,\n      is_useful: isUseful,\n      is_in_depth: isInDepth,\n      quality_score: qualityScore,\n      reason: result.reason || \"\",\n    }\n  }\n};\n"
      },
      "id": "node-8-parse-verify",
      "name": "Parse Verification Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3800,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.verification.passed }}",
              "value2": true
            }
          ]
        }
      },
      "id": "node-9-if-passed",
      "name": "Verification Passed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3980,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1VbyEQNuIAKpmTfk_5pTIjuLb3xlS-kdUfWflmfnFJSA",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "\ud0a4\uc6cc\ub4dc": "={{ $('Sheets Read (Status=\ub300\uae30)').item.json['\ud0a4\uc6cc\ub4dc'] }}",
            "\uc0c1\ud0dc": "\ubc1c\ud589\ub300\uae30",
            "\uc81c\ubaa9": "={{ $('Parse JSON Response').item.json.title }}",
            "\uba54\ud0c0\uc124\uba85": "={{ $('Parse JSON Response').item.json.meta_description }}",
            "FAQ\uc2a4\ud0a4\ub9c8": "={{ JSON.stringify($('Parse JSON Response').item.json.faq_schema) }}",
            "\ucc38\uace0\uc790\ub8cc": "={{ JSON.stringify($('URL Validation').item.json.references) }}",
            "\ubcf8\ubb38\ub9c8\ud06c\ub2e4\uc6b4": "={{ $('URL Validation').item.json.content }}",
            "URL\uac80\uc99d": "={{ $('URL Validation').item.json.url_validation.reachable + '/' + $('URL Validation').item.json.url_validation.total_urls + ' (dead:' + $('URL Validation').item.json.url_validation.dead + ')' }}",
            "\ud504\ub86c\ud504\ud2b8\uc720\ud615": "={{ $('Route Prompt (A/B/C)').item.json.prompt_type || 'A' }}",
            "Haiku\uac80\uc99d": "={{ JSON.stringify($('Parse Verification Result').item.json.verification) }}",
            "\ub0b4\ubd80\ub9c1\ud06c\ud0a4\uc6cc\ub4dc": "={{ JSON.stringify($('Parse JSON Response').item.json.internal_link_keywords) }}",
            "\ud0dc\uadf8": "={{ ($('Parse JSON Response').item.json.tags || []).join(', ') }}",
            "\uc0dd\uc131\uc77c\uc2dc": "={{ new Date().toISOString().slice(0,19).replace('T',' ') }}",
            "\uc378\ub124\uc77cURL": "={{ $('Inject Images').item.json.thumbnail_url || '' }}"
          },
          "matchingColumns": [
            "\ud0a4\uc6cc\ub4dc"
          ]
        },
        "options": {}
      },
      "id": "node-10a-sheets-pass",
      "name": "Sheets Update (\ubc1c\ud589\ub300\uae30)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        4200,
        200
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1VbyEQNuIAKpmTfk_5pTIjuLb3xlS-kdUfWflmfnFJSA",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "\ud0a4\uc6cc\ub4dc": "={{ $('Sheets Read (Status=\ub300\uae30)').item.json['\ud0a4\uc6cc\ub4dc'] }}",
            "\uc0c1\ud0dc": "\uac80\uc218\ud544\uc694",
            "\uc5d0\ub7ec\uba54\uc2dc\uc9c0": "={{ $('Parse Verification Result').item.json.verification.reason || '\uac80\uc99d \uc2e4\ud328' }}",
            "Haiku\uac80\uc99d": "={{ JSON.stringify($('Parse Verification Result').item.json.verification) }}",
            "\uc0dd\uc131\uc77c\uc2dc": "={{ new Date().toISOString().slice(0,19).replace('T',' ') }}"
          },
          "matchingColumns": [
            "\ud0a4\uc6cc\ub4dc"
          ]
        },
        "options": {}
      },
      "id": "node-10b-sheets-fail",
      "name": "Sheets Update (\uac80\uc218\ud544\uc694)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        4200,
        400
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "id": "node-0-manual",
      "name": "Manual Trigger (Test)",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        60,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "1VbyEQNuIAKpmTfk_5pTIjuLb3xlS-kdUfWflmfnFJSA",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "id"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "detectAutomatically"
            }
          }
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "\uc0c1\ud0dc",
              "lookupValue": "\ubc1c\ud589\uc644\ub8cc"
            },
            {
              "lookupColumn": "\uc0c1\ud0dc",
              "lookupValue": "\ubc1c\ud589\ub300\uae30"
            }
          ]
        },
        "combineFilters": "OR"
      },
      "id": "node-dup-1-sheets-published",
      "name": "Sheets Read (Published Keywords)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "/**\n * Check Duplicate \u2014 \uae30\uc874 \ubc1c\ud589\uae00\uacfc \ud0a4\uc6cc\ub4dc \uc720\uc0ac\ub3c4 \ube44\uad50\n * Mode: runOnceForAllItems\n * \uc704\uce58: Sheets Read (Status=\ub300\uae30) \u2192 [Check Duplicate] \u2192 IF Not Duplicate?\n * \uc785\ub825: Sheets Read (\ub300\uae30) \uacb0\uacfc (\uac01 \uc544\uc774\ud15c\uc5d0 \ud0a4\uc6cc\ub4dc \ud3ec\ud568)\n * \ucc38\uc870: Sheets Read (Published Keywords) \ub178\ub4dc\uc5d0\uc11c \uae30\uc874 \ud0a4\uc6cc\ub4dc \uac00\uc838\uc634\n * \ucd9c\ub825: duplicate_check \ud544\ub4dc \ucd94\uac00\ub41c \uc544\uc774\ud15c\n */\n\n// \uc774\uc804\uc5d0 \uc2e4\ud589\ub41c \"Sheets Read (Published Keywords)\" \ub178\ub4dc\uc5d0\uc11c \ubc1c\ud589 \uc644\ub8cc/\ub300\uae30 \ud0a4\uc6cc\ub4dc \uc870\ud68c\nconst publishedItems = $('Sheets Read (Published Keywords)').all();\nconst existingKeywords = publishedItems\n  .map(item => item.json['\ud0a4\uc6cc\ub4dc'] || '')\n  .filter(kw => kw.length > 0);\n\nconst OVERLAP_THRESHOLD = 0.7;\n\nfunction keywordOverlap(kwA, kwB) {\n  const tokensA = new Set(kwA.toLowerCase().split(/\\s+/).filter(t => t.length > 0));\n  const tokensB = new Set(kwB.toLowerCase().split(/\\s+/).filter(t => t.length > 0));\n  // \ub2e8\uc77c \ud1a0\ud070 \ud0a4\uc6cc\ub4dc: \uc815\ud655 \uc77c\uce58\ub9cc \ud310\uc815\n  if (tokensA.size < 2 || tokensB.size < 2) {\n    return kwA.toLowerCase().trim() === kwB.toLowerCase().trim() ? 1.0 : 0;\n  }\n  const intersection = [...tokensA].filter(t => tokensB.has(t));\n  const smaller = Math.min(tokensA.size, tokensB.size);\n  return smaller > 0 ? intersection.length / smaller : 0;\n}\n\nconst results = [];\nfor (const item of $input.all()) {\n  const keyword = item.json['\ud0a4\uc6cc\ub4dc'] || '';\n  let isDuplicate = false;\n  let duplicateOf = '';\n  let maxOverlap = 0;\n\n  for (const existing of existingKeywords) {\n    const overlap = keywordOverlap(keyword, existing);\n    if (overlap > maxOverlap) {\n      maxOverlap = overlap;\n      duplicateOf = existing;\n    }\n    if (overlap >= OVERLAP_THRESHOLD) {\n      isDuplicate = true;\n      break;\n    }\n  }\n\n  results.push({\n    json: {\n      ...item.json,\n      duplicate_check: {\n        is_duplicate: isDuplicate,\n        duplicate_of: isDuplicate ? duplicateOf : '',\n        max_overlap: Math.round(maxOverlap * 100) / 100,\n        threshold: OVERLAP_THRESHOLD,\n      }\n    }\n  });\n}\n\nreturn results;\n"
      },
      "id": "node-dup-2-check",
      "name": "Check Duplicate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": ""
          },
          "conditions": [
            {
              "id": "dup-check-condition",
              "leftValue": "={{ $json.duplicate_check.is_duplicate }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "node-dup-3-if",
      "name": "IF Not Duplicate?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1VbyEQNuIAKpmTfk_5pTIjuLb3xlS-kdUfWflmfnFJSA",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "\ud0a4\uc6cc\ub4dc": "={{ $json[\"\ud0a4\uc6cc\ub4dc\"] }}",
            "\uc0c1\ud0dc": "\uc911\ubcf5\uc2a4\ud0b5",
            "\uc5d0\ub7ec\uba54\uc2dc\uc9c0": "={{ \"\uc911\ubcf5 \ud0a4\uc6cc\ub4dc: \" + $json.duplicate_check.duplicate_of + \" (\uc720\uc0ac\ub3c4: \" + ($json.duplicate_check.max_overlap * 100) + \"%)\" }}",
            "\uc0dd\uc131\uc77c\uc2dc": "={{ new Date().toISOString().slice(0,19).replace(\"T\",\" \") }}"
          },
          "matchingColumns": [
            "\ud0a4\uc6cc\ub4dc"
          ]
        },
        "options": {}
      },
      "id": "node-dup-4-sheets-skip",
      "name": "Sheets Update (\uc911\ubcf5\uc2a4\ud0b5)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1120,
        500
      ],
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Published Keywords \ub370\uc774\ud130\ub294 $(\"Sheets Read (Published Keywords)\").all()\ub85c \uc811\uadfc \uac00\ub2a5\n// Sheets Read (\ub300\uae30)\uac00 1\ubc88\ub9cc \uc2e4\ud589\ub418\ub3c4\ub85d \ub2e8\uc77c \ud2b8\ub9ac\uac70 \uc544\uc774\ud15c \ubc18\ud658\nreturn [{ json: { _trigger: true } }];"
      },
      "id": "node-dup-0-reduce",
      "name": "Reduce to Trigger",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        570,
        300
      ]
    }
  ],
  "connections": {
    "Schedule Trigger (01:00 AM)": {
      "main": [
        [
          {
            "node": "Sheets Read (Published Keywords)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets Read (Status=\ub300\uae30)": {
      "main": [
        [
          {
            "node": "Check Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SerpAPI Search": {
      "main": [
        [
          {
            "node": "Parse SERP Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse SERP Data": {
      "main": [
        [
          {
            "node": "Fetch Official Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Prompt (A/B/C)": {
      "main": [
        [
          {
            "node": "Build LLM Request (Content Gen)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LLM Request (Content Gen)": {
      "main": [
        [
          {
            "node": "LLM Request (Content Gen)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM Request (Content Gen)": {
      "main": [
        [
          {
            "node": "Normalize Response (Content Gen)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Response (Content Gen)": {
      "main": [
        [
          {
            "node": "Parse JSON Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON Response": {
      "main": [
        [
          {
            "node": "Inject Images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Structure": {
      "main": [
        [
          {
            "node": "URL Validation",
            "type": "main",
            "index": 0
          },
          {
            "node": "Code Block Lint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "URL Validation": {
      "main": [
        [
          {
            "node": "Build Verify Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Block Lint": {
      "main": [
        [
          {
            "node": "Build Verify Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Verify Request": {
      "main": [
        [
          {
            "node": "Build LLM Request (Verify)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LLM Request (Verify)": {
      "main": [
        [
          {
            "node": "LLM Request (Verify)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM Request (Verify)": {
      "main": [
        [
          {
            "node": "Normalize Response (Verify)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Response (Verify)": {
      "main": [
        [
          {
            "node": "Parse Verification Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Verification Result": {
      "main": [
        [
          {
            "node": "Verification Passed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verification Passed?": {
      "main": [
        [
          {
            "node": "Sheets Update (\ubc1c\ud589\ub300\uae30)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sheets Update (\uac80\uc218\ud544\uc694)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger (Test)": {
      "main": [
        [
          {
            "node": "Sheets Read (Published Keywords)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inject Images": {
      "main": [
        [
          {
            "node": "Validate Structure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets Read (Published Keywords)": {
      "main": [
        [
          {
            "node": "Reduce to Trigger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Duplicate": {
      "main": [
        [
          {
            "node": "IF Not Duplicate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Not Duplicate?": {
      "main": [
        [
          {
            "node": "SerpAPI Search",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sheets Update (\uc911\ubcf5\uc2a4\ud0b5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reduce to Trigger": {
      "main": [
        [
          {
            "node": "Sheets Read (Status=\ub300\uae30)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Official Docs": {
      "main": [
        [
          {
            "node": "Route Prompt (A/B/C)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Seoul"
  },
  "staticData": {
    "node:Schedule Trigger (01:00 AM)": {
      "recurrenceRules": []
    }
  },
  "meta": null,
  "versionId": "6f2117ca-302f-4716-98b2-02a41f9dcb3b",
  "triggerCount": 1,
  "shared": [
    {
      "createdAt": "2026-02-28T12:43:46.775Z",
      "updatedAt": "2026-02-28T12:43:46.775Z",
      "role": "workflow:owner",
      "workflowId": "ty52rqOEJ6ZjNF2L",
      "projectId": "uvFw6yAB9Rd7JtTW",
      "project": {
        "createdAt": "2026-02-28T12:42:59.143Z",
        "updatedAt": "2026-02-28T12:43:22.268Z",
        "id": "uvFw6yAB9Rd7JtTW",
        "name": "Admin User <admin@local.host>",
        "type": "personal",
        "icon": null,
        "projectRelations": [
          {
            "createdAt": "2026-02-28T12:42:59.143Z",
            "updatedAt": "2026-02-28T12:42:59.143Z",
            "role": "project:personalOwner",
            "userId": "1ff09d89-783c-4125-b2f7-add25d831eca",
            "projectId": "uvFw6yAB9Rd7JtTW",
            "user": {
              "createdAt": "2026-02-28T12:42:58.925Z",
              "updatedAt": "2026-02-28T12:46:53.944Z",
              "id": "1ff09d89-783c-4125-b2f7-add25d831eca",
              "email": "admin@local.host",
              "firstName": "Admin",
              "lastName": "User",
              "personalizationAnswers": null,
              "settings": {
                "userActivated": true,
                "firstSuccessfulWorkflowId": "fBPLKcSvPnWRUtMO",
                "userActivatedAt": 1772282806536
              },
              "role": "global:owner",
              "disabled": false,
              "mfaEnabled": false,
              "isPending": false,
              "isOwner": true
            }
          }
        ]
      }
    }
  ],
  "tags": []
}