{
  "name": "AI 블로그 글 생성 워크플로우 (aipost.4men.kr)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      },
      "id": "26be8289-73e5-4e9b-ad07-0dc9b897b4be",
      "name": "1분마다 실행",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        -656,
        112
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM blog_generation_jobs WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1",
        "options": {}
      },
      "id": "3226aabd-e4bc-483e-9e93-b14d9be45d31",
      "name": "작업 조회",
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        -448,
        112
      ],
      "credentials": {
        "mySql": {
          "id": "YZxfpCazoF1S1gy0",
          "name": "MySQL account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "condition-check",
              "leftValue": "={{ Array.isArray($json) ? $json.length : ($json ? 1 : 0) }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "97d55f86-3f88-403c-8c22-9236b27a290b",
      "name": "작업이 있는가?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -224,
        112
      ]
    },
    {
      "parameters": {
        "jsCode": "// 작업 정보 추출\nconst inputData = $input.all();\nif (inputData.length === 0 || !inputData[0].json) {\n  return [];\n}\n\nconst job = Array.isArray(inputData[0].json) ? inputData[0].json[0] : inputData[0].json;\nif (!job || !job.id) {\n  return [];\n}\n\nconst bookIds = typeof job.book_ids === 'string' ? JSON.parse(job.book_ids) : job.book_ids;\n\nreturn {\n  json: {\n    id: job.id,\n    job_id: job.id,\n    book_ids: bookIds,\n    topic_keywords: job.topic_keywords,\n    target_blog_id: job.target_blog_id\n  }\n};"
      },
      "id": "d45db85a-45d5-42c7-ba33-9dd80f83dacc",
      "name": "작업 정보 준비",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "=UPDATE blog_generation_jobs SET status = 'processing', updated_at = NOW() WHERE id = {{ parseInt($json.job_id) || parseInt($json.id) }}",
        "options": {}
      },
      "id": "86e6d69f-3212-4116-8c0b-14613614b82a",
      "name": "상태 업데이트 (처리중)",
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        224,
        0
      ],
      "credentials": {
        "mySql": {
          "id": "YZxfpCazoF1S1gy0",
          "name": "MySQL account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 작업 정보 전달 (상태 업데이트 노드는 데이터를 반환하지 않으므로 이전 노드 데이터 전달)\nconst prevNode = $('작업 정보 준비');\nif (prevNode && prevNode.item && prevNode.item.json) {\n  return [prevNode.item.json];\n}\n// 폴백: 현재 입력 데이터 사용\nconst inputData = $input.all();\nif (inputData.length > 0 && inputData[0].json) {\n  return [inputData[0].json];\n}\nreturn [{ json: {} }];"
      },
      "id": "2f0bcb1d-29c5-453b-a57e-8910b1535356",
      "name": "데이터 전달",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "=SELECT id, title, author, full_text FROM books WHERE id IN ({{ Array.isArray($json.book_ids) && $json.book_ids.length > 0 ? $json.book_ids.map(id => parseInt(id) || id).join(',') : '0' }}) AND full_text IS NOT NULL AND full_text != ''",
        "options": {}
      },
      "id": "a3e6f706-da6e-4f8a-b71a-581e377e44dd",
      "name": "도서 텍스트 조회",
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        672,
        0
      ],
      "alwaysOutputData": true,
      "credentials": {
        "mySql": {
          "id": "JmUu7rzRGby1ac67",
          "name": "MySQL account 2"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 도서 텍스트 결합 및 프롬프트 생성\nconst books = $input.all();\n\n// 첫 번째 아이템에서 작업 정보 가져오기\nlet jobInfo = null;\nif (books.length > 0 && books[0].json.job_id) {\n  jobInfo = books[0].json;\n} else {\n  // 이전 노드에서 작업 정보 가져오기\n  const previousNode = $('작업 정보 준비');\n  if (previousNode && previousNode.item) {\n    jobInfo = previousNode.item.json;\n  }\n}\n\nif (!jobInfo) {\n  return { json: { error: '작업 정보를 찾을 수 없습니다.' } };\n}\n\n// 모든 도서의 full_text 결합 (토큰 제한을 위해 각 도서당 최대 5천자로 제한)\nlet combinedText = '';\nconst maxCharsPerBook = 5000; // 도서당 최대 5천자\nconst totalMaxChars = 10000; // 전체 최대 1만자 (약 3,000-4,000 토큰)\nlet totalChars = 0;\n\nbooks.forEach((book, index) => {\n  if (book.json.full_text && totalChars < totalMaxChars) {\n    const bookText = book.json.full_text.substring(0, maxCharsPerBook);\n    const remainingChars = totalMaxChars - totalChars;\n    const textToAdd = bookText.substring(0, Math.min(bookText.length, remainingChars - 200)); // 헤더 공간 확보\n    \n    if (textToAdd.length > 0) {\n      combinedText += `\\n\\n=== 도서 ${index + 1}: ${book.json.title} ===\\n\\n`;\n      combinedText += textToAdd;\n      totalChars += textToAdd.length + 50; // 헤더 길이 포함\n    }\n  }\n});\n\n// 프롬프트 생성 (간결하게)\nconst prompt = `다음 도서 내용을 바탕으로 \"${jobInfo.topic_keywords}\" 주제로 블로그 글을 작성해주세요.\n\n## 요구사항:\n- 제목: 매력적이고 SEO 친화적인 제목 (30-50자)\n- 본문: 2,500-3,500자, 서론-본론(2-3섹션)-결론 구조\n- 스타일: 전문적이면서 친근한 톤, 마크다운 형식 사용\n- 내용: 도서 핵심을 바탕으로 새로운 관점과 통찰 제공\n\n## 도서 내용:\n${combinedText}\n\n위 내용을 바탕으로 \"${jobInfo.topic_keywords}\" 주제로 완성도 높은 블로그 글을 작성해주세요.`;\n\nreturn {\n  json: {\n    job_id: jobInfo.job_id,\n    topic_keywords: jobInfo.topic_keywords,\n    target_blog_id: jobInfo.target_blog_id,\n    prompt: prompt,\n    book_count: books.length\n  }\n};"
      },
      "id": "87048f45-bcb2-42cf-850d-1a9af3e57bdc",
      "name": "프롬프트 생성",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        672,
        352
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer sk-proj-je15spt7GagLLCQpgj8niFN************"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  model: 'gpt-4',\n  messages: [{\n    role: 'user',\n    content: $json.prompt || ''\n  }],\n  temperature: 0.7,\n  max_tokens: 3000\n} }}",
        "options": {}
      },
      "id": "6a05df0f-85e8-4838-8711-ef6f68d4f9d1",
      "name": "OpenAI - 글 생성",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// OpenAI 응답에서 글 추출\nconst response = $input.item.json;\nconst previousNode = $('프롬프트 생성');\nconst previousData = previousNode && previousNode.item ? previousNode.item.json : {};\n\n// 디버깅: 전체 응답 확인\nconsole.log('OpenAI API 응답:', JSON.stringify(response, null, 2));\n\nlet title = '';\nlet content = '';\n\n// OpenAI API 응답 구조 확인\nif (response.choices && response.choices[0]) {\n  const choice = response.choices[0];\n  \n  if (choice.message && choice.message.content) {\n    const text = choice.message.content || '';\n    \n    if (text) {\n      // 제목과 본문 분리 (첫 번째 줄을 제목으로 가정)\n      const lines = text.split('\\n');\n      title = lines[0].replace(/^#+\\s*/, '').trim(); // 마크다운 헤더 제거\n      content = text;\n      \n      // 제목이 너무 길면 첫 100자로 제한\n      if (title.length > 100) {\n        title = title.substring(0, 100);\n      }\n      \n      // 제목이 없으면 기본 제목 생성\n      if (!title || title.length < 5) {\n        title = `AI가 생성한 블로그 글 - ${previousData.topic_keywords || '주제'}`;\n      }\n    }\n  }\n}\n\n// 응답이 예상과 다를 경우 에러 메시지 포함\nif (!content) {\n  return {\n    json: {\n      error: 'OpenAI API 응답에서 텍스트를 추출할 수 없습니다.',\n      raw_response: response,\n      job_id: previousData.job_id,\n      topic_keywords: previousData.topic_keywords,\n      target_blog_id: previousData.target_blog_id,\n      title: title || `에러 - ${previousData.topic_keywords || '주제'}`,\n      content: '응답을 파싱할 수 없습니다.'\n    }\n  };\n}\n\nreturn {\n  json: {\n    job_id: previousData.job_id,\n    topic_keywords: previousData.topic_keywords,\n    target_blog_id: previousData.target_blog_id,\n    title: title,\n    content: content\n  }\n};"
      },
      "id": "9c79d3be-4ef1-4b0a-8f24-baf5c59ff9c8",
      "name": "글 추출",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://aipost.4men.kr/api/n8n-callback.php",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-API-Key",
              "value": "=980a30ff663e7e631c76c38ff0e183d***********"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"job_id\": {{ $json.job_id }},\n  \"title\": \"{{ $json.title }}\",\n  \"content\": {{ JSON.stringify($json.content) }},\n  \"thumbnail_url\": \"{{ $json.thumbnail_url || '' }}\",\n  \"audio_url\": \"{{ $json.audio_url || '' }}\"\n}",
        "options": {}
      },
      "id": "74782d7a-8a9b-4206-b064-a47e91ed9d79",
      "name": "결과 콜백",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1312,
        0
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "table": "blog_generation_jobs",
        "options": {}
      },
      "id": "fd02f73e-6223-4f8c-baef-e126a85153af",
      "name": "상태 업데이트 (완료)",
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        1488,
        0
      ],
      "credentials": {
        "mySql": {
          "id": "JmUu7rzRGby1ac67",
          "name": "MySQL account 2"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "1분마다 실행": {
      "main": [
        [
          {
            "node": "작업 조회",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "작업 조회": {
      "main": [
        [
          {
            "node": "작업이 있는가?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "작업이 있는가?": {
      "main": [
        [
          {
            "node": "작업 정보 준비",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "작업 정보 준비": {
      "main": [
        [
          {
            "node": "상태 업데이트 (처리중)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "상태 업데이트 (처리중)": {
      "main": [
        [
          {
            "node": "데이터 전달",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "데이터 전달": {
      "main": [
        [
          {
            "node": "도서 텍스트 조회",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "도서 텍스트 조회": {
      "main": [
        [
          {
            "node": "프롬프트 생성",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "글 추출": {
      "main": [
        [
          {
            "node": "결과 콜백",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "결과 콜백": {
      "main": [
        [
          {
            "node": "상태 업데이트 (완료)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI - 글 생성": {
      "main": [
        [
          {
            "node": "글 추출",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "프롬프트 생성": {
      "main": [
        [
          {
            "node": "OpenAI - 글 생성",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1dcb6f0b-9aae-498f-aa68-b544ada30579",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "96b15fe35a88e4384ebb5727d5ce7ae0a39a7fc9afbb9615848fa8dffab7f0d3"
  },
  "id": "SXLS4VmobquAUa9z",
  "tags": []
}