ChatGPT와 Codex로 워드프레스 블로그 자동 업로드 파이프라인 만들기

··설명 약 9분·작성: 해비·블로그 소개

WordPress 자동 업로드에서 초안·검사·발행·공개 확인 단계를 어떻게 설계하나요?

WordPress 자동 업로드는 초안·검사·발행·공개 확인 상태를 나눠 실패 지점에서 재개하게 설계합니다. 로컬 예제는 요청 계획 파일을 만드는 모의 흐름입니다. 파일 생성과 실제 게시를 구분하고 원격 중복·공개 권한을 따로 확인합니다.

적용 조건·범위: 실제 WordPress 계정 없이 입력 JSON → 검증 → 요청 계획 JSON 생성의 로컬 모의 흐름을 재현합니다. 결과 파일은 게시 성공 응답이 아니며 실제 API 전송 전에 사람이 검토할 자료입니다.

본문 근거: developer.wordpress.org

핵심 기준

WordPress 자동 업로드는 글 생성보다 상태 전이가 중요합니다. 초안 생성, 품질검사, 이미지, 슬러그, 카테고리, 공개 발행, 결과 검증을 분리하고 실패 시 어느 단계부터 재개할지 기록해야 합니다.

콘텐츠 원본
→ 본문 생성
→ 품질 검사
→ 이미지/SEO
→ WordPress write
→ 공개 URL 확인
→ 시트 상태 갱신

AI 도구는 결과를 대신 책임지는 주체가 아닙니다. 사람이 작업 목표와 승인 기준을 정하고, 도구는 그 범위 안에서 탐색·수정·검증을 수행하는 구조로 보는 편이 안전합니다.

실무에 적용하는 순서

실제 WordPress 계정 없이 입력 JSON → 검증 → 요청 계획 JSON 생성의 로컬 모의 흐름을 재현합니다. 결과 파일은 게시 성공 응답이 아니며 실제 API 전송 전에 사람이 검토할 자료입니다.

작업을 시작할 때는 다음 정보를 짧게 고정합니다.

목표:
현재 상태:
허용 범위:
금지 범위:
완료 조건:
검증 방법:
실패 시 복구점:

이 문서가 있으면 대화가 길어져도 에이전트가 현재 위치를 다시 확인할 수 있습니다.

직접 해볼 일 · 입력과 검토 기준

독자 질문: 업로드 자동화에서 중복 게시를 어떻게 막을까?

실습: 실제 계정 없이 draft.json 한 건을 검증하고 모의 게시 결과를 만드는 로컬 흐름을 설계하세요. WordPress에는 요청을 보내지 않습니다.

입력 예시: {“source_id”:”ai-10-demo”,”title”:”작업 기록”,”content”:”검토할 초안”,”status”:”draft”}. source_id는 내부 중복 확인 키이며 WordPress Posts API 필드라고 가정하지 마세요. 제목·본문 공백 검사, 승인 전 중단, 성공 응답 후 ID 기록을 계획해 주세요.

결과 검토: 같은 source_id를 두 번 처리할 때 두 번째는 재전송보다 기존 결과 조회/대조를 요구하는지 봅니다. 실패·응답 유실·성공을 구분하고, 공개 전 사람 승인을 둡니다. WordPress의 status와 content는 공식 Posts API 필드입니다.

수정 요청: 내부 상태만 성공으로 바뀌었다면 완료 표시를 취소하고 응답·게시 ID·공개 URL을 조회해 대조한 뒤 재시도 여부를 결정합니다.

다음 학습: 11회차에서 같은 상태·검증·승인 구조를 코드 변경 한 건에 적용합니다.

확인 범위: 공식 자료 developer.wordpress.org를 대조했습니다. 로컬 모의 코드의 기존 실행 기록과 실제 WordPress 게시 검증을 구분합니다. 실제 API·인증·게시 요청은 실행하지 않았습니다. 독자는 자신의 환경에서 결과를 기록해 주세요.

독립 실습의 준비

Node.js에서 동작하는 로컬 모의 예제로 입력 검증·승인 전 중단·중복 방지를 확인합니다. 실제 WordPress 인증정보나 네트워크 요청은 필요하지 않습니다. 아래 로컬 상태는 교육용이며 여러 프로세스가 동시에 쓰는 운영 멱등성 저장소를 대체하지 않습니다.

자가 확인: prepared-request.json이 만들어졌다고 실제 게시 ID가 생긴 것은 아닙니다. 같은 입력으로 재실행했을 때 unchanged를 반환하는 것은 로컬 결과 파일 중복 방지이며, 원격 WordPress의 중복 게시 방지는 별도로 설계해야 합니다.

실행 방법과 확인한 결과

새 실습 폴더에 아래 파일 8개를 저장합니다. Node.js 24.x에서 외부 패키지 설치 없이 실행하며 TaskFlow 앱 폴더와 분리합니다.

node --version
node --test pipeline.test.mjs
node prepare.mjs input.json prepared-request.json
node prepare.mjs input.json prepared-request.json
node prepare.mjs invalid-input.json rejected-request.json

원문의 기존 오프라인 검증 기록에서는 테스트 11개가 통과했고, 정상 입력은 created, 동일 입력의 재실행은 unchanged를 반환했습니다. 잘못된 입력은 종료 코드 1이며 결과 파일을 만들지 않았고 코드 구문 검사도 통과한 것으로 기록되어 있습니다. 이번 원고 검토에서 이 명령을 다시 실행한 것은 아니며, 이 기록은 WordPress API·인증·게시 성공 증거가 아닙니다.

prepared-request.json에는 HTTP 메서드·경로·본문을 설명하는 로컬 객체만 저장됩니다. mode·fingerprint·request는 이 예제의 내부 필드이며 WordPress API에 그대로 보내는 필드가 아닙니다. 실제 Posts API는 title·slug·content·status 등의 본문 필드를 받습니다. WordPress Posts API 공식 문서

자주 발생하는 오류와 해결

입력 오류는 재시도해도 해결되지 않으므로 JSON 필드와 slug부터 수정합니다. 기존 출력과 다른 내용을 쓰려 하면 덮어쓰기를 거절하므로 내용을 검토하고 새 출력 파일명을 사용합니다. 로컬 fingerprint는 실제 서버의 중복 게시를 막아 주지 않습니다. 운영에서는 sourceId와 실제 postId 대응을 별도로 저장하고, 응답 유실 시 서버 상태를 조회한 뒤 재시도 여부를 결정해야 합니다.

선택 실습: 같은 프로젝트로 연습하기

이 선택 실습은 TaskFlow 작업 관리 앱의 변경 요청을 AI와 함께 검토하는 연습입니다. 요청·코드·검증 결과를 구분해 기록합니다.

이번 주제: ChatGPT와 Codex로 워드프레스 블로그 자동 업로드 파이프라인 만들기.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 누적 적용

이어서 읽기: Claude와 GPT 비교 코딩 글쓰기 문서 작업 평가 기준. 누적 작업 기록은 docs/ai-log.md에 회차별로 추가합니다. 새로 진입하면 실제 개발환경 시작 ZIP을 별도 폴더에 풉니다. 이 앱에는 상태 문구와 API URL 표시가 있으며 작업 목록·검색·완료 필터는 없습니다. 상단의 WordPress 모의 파이프라인은 별도 Node.js 폴더에서 실행합니다. 하단 스타터에 업로드 기능이 추가된 것은 아니며, 검증·중복 방지·승인 경계를 작업 로그로 옮기는 과제입니다.

동일 원리를 다음 회차의 코드 작업 파이프라인으로 변환합니다.
작업 계약 → 코드 수정 → 테스트 → diff 검토 → 승인 → commit.

변경 전후에는 Git을 기준으로 실제 결과를 확인합니다.

git status --short
git diff --stat
git diff

테스트나 build를 실행했다면 성공 여부뿐 아니라 어떤 명령을 실행했는지도 로그에 남깁니다. 외부 서비스가 필요한 검증은 실제로 실행하지 못했다면 미검증으로 표시합니다.

평가와 중단 기준

  • 자동 재시도로 중복 게시
  • 성공 응답 전에 내부 상태만 완료 처리
  • API 키를 프롬프트·로그에 출력
  • 외부 시스템 쓰기를 사람 승인 없이 실행
  • WordPress 전용 구현을 일반 AI 원리로 과장

에이전트가 더 많은 작업을 할 수 있다는 이유만으로 계속 진행하지 않습니다. 완료 조건을 충족했다면 멈추고 사람이 diff를 확인하는 것이 다음 단계입니다.

완료 체크리스트

  • 실제 API 필드와 내부 manifest 필드 구분
  • 멱등성과 재시도 설명 정확성
  • 외부 쓰기 승인 경계 명확
  • 마지막 TaskFlow 통합 회차에 직접 연결

누적 기록과 실행 결과

이전 회차 ai-step-09-evaluation에 남긴 기록을 시작점으로 사용합니다.

현재 구조는 기존 TaskFlow 소스와 package.json을 유지하고, 학습 기록은 새로 만드는 docs/ai-log.md에 누적합니다. 이 경로는 앱 실행 파일이 아닌 실습용 기록 위치입니다. 기존 동명 파일이 있으면 덮어쓰지 않고 회차 항목을 추가합니다. 앱 수정 파일은 저장소 탐색 후 계약에 확정하며 문서의 예시 경로를 실제 파일로 가정하지 않습니다.

Before는 해당 회차의 기록 항목이 없는 상태이고, 적용은 위 실습을 수행하며 관찰 내용을 채우는 과정입니다. After는 아래 항목의 근거와 미검증 범위가 남은 상태입니다. 문서 제목을 바꾸는 것만으로 기능 구현이 완료된 것은 아닙니다.

아래는 docs/ai-log.md에 추가하는 회차 항목 전체입니다. 앱 전체 코드가 아닌 개발 로그 양식이며, 빈칸은 직접 확인한 결과로 채웁니다.

체크포인트: ai-step-10-automation
작업 요청 / 목표 / 비목표:
시작 상태 / 적용 지침:
허용 파일 / 실제 변경 파일:
계획 / diff 요약:
실행 명령 / 실제 결과 / 증거 위치:
미검증 항목 / 필요한 조건:
사람이 확인·수정한 내용:
승인 여부 / 승인 대상:
commit: 미생성 또는 실제 확인한 식별자
실패 지점 / 복구점 / 다음 행동:

검증란은 성공·실패·미실행을 구분합니다. 이 양식 자체는 실행 성공이나 사람 승인 증거가 아닙니다. 앱을 바꿨다면 diff → pnpm lint·pnpm build → 필요한 브라우저 확인 → 사람 검토 → 승인 → commit 순서로 기록합니다. 로그만 편집한 단계는 내용 대조가 검증이며 앱 테스트 통과로 부풀리지 않습니다.

완료 체크포인트

ai-step-10-automation
전체 파일·코드 보기

실습 프로젝트 전체 코드

아래 8개 파일은 독립 실행하는 오프라인 교육 예제입니다. 네트워크 요청이나 실제 워드프레스 게시를 실행하지 않으며, 기존 TaskFlow 프로젝트의 파일과 테스트 스크립트를 변경하지 않습니다.

README.md

# AI 10편: WordPress 요청 명세를 준비하는 오프라인 예제

Node.js 24.x의 기본 모듈만 사용합니다. 설치할 패키지나 실제 사이트 주소·계정·키가 없습니다. 기존 TaskFlow 앱과 별개인 작은 실습 폴더입니다.

## 실행

이 폴더에서 다음 명령을 한 줄씩 실행합니다.

1. `node --test pipeline.test.mjs`: 정상 입력, 잘못된 입력, 재실행과 덮어쓰기 방지 검사
2. `node prepare.mjs input.json prepared-request.json`: `created` 출력
3. 같은 명령을 다시 실행: `unchanged` 출력, 기존 결과 유지
4. `node prepare.mjs invalid-input.json rejected-request.json`: 검증 오류, 종료 코드 1, 출력 파일 생성 안 함

`example-prepared-request.json`은 정상 입력의 예상 결과입니다. 프로그램은 자동으로 폴더를 만들거나 기존의 다른 내용 파일을 덮어쓰지 않습니다. 입력을 수정해 다른 결과를 만들려면 새 출력 파일명을 사용하세요.

## 검증하는 것

- title, slug, content 세 필드만 허용
- 비어 있지 않은 제목과 본문
- 소문자 영문·숫자·단어 사이 하이픈으로 구성된 slug
- 같은 입력으로 동일한 요청 명세 생성
- 같은 결과로 재실행하면 파일 내용 유지
- 다른 결과가 이미 있는 파일의 덮어쓰기 거부

## 검증하지 않는 것

네트워크·WordPress API·인증·게시·초안 저장을 실행하지 않습니다. 결과의 POST 경로와 status: draft는 로컬 요청 명세에 적힌 값입니다. 실제 초안도 생성되지 않습니다. fingerprint는 로컬 파일 비교용 내용 해시이며 WordPress 서버의 중복 게시 방지를 보장하지 않습니다.

본문 검사는 문자열의 존재 여부에 한정됩니다. HTML 정화, 저작권·개인정보 검토, SEO 검수, 실제 업로드 권한과 게시 결과 검증은 포함하지 않습니다. 실제 전송 코드를 덧붙이기 전에 별도 설계와 승인이 필요합니다.

테스트는 운영체제의 임시 폴더에 작은 JSON 파일을 만듭니다. 자동 삭제 기능은 포함하지 않습니다. 이 예제의 Node 테스트는 기존 TaskFlow 프로젝트에 없는 test 스크립트를 추가했다는 뜻이 아닙니다.

package.json

{
  "name": "blogflow-offline-wordpress-request",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": {
    "test": "node --test pipeline.test.mjs",
    "prepare": "node prepare.mjs input.json prepared-request.json"
  }
}

pipeline.mjs

import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';

export function prepareRequest(input) {
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
    throw new TypeError('입력은 JSON 객체여야 합니다.');
  }
  const keys = Object.keys(input).sort();
  if (keys.join(',') !== 'content,slug,title') {
    throw new TypeError('title, slug, content 세 필드만 입력하세요.');
  }
  const { title, slug, content } = input;
  if (typeof title !== 'string' || !title.trim() || title.length > 200) {
    throw new TypeError('title은 비어 있지 않은 200자 이하 문자열이어야 합니다.');
  }
  if (typeof slug !== 'string' || slug.length > 200 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
    throw new TypeError('slug에는 소문자 영문, 숫자, 단어 사이 하이픈만 사용하세요.');
  }
  if (typeof content !== 'string' || !content.trim()) {
    throw new TypeError('content는 비어 있지 않은 문자열이어야 합니다.');
  }
  const body = { title, slug, content, status: 'draft' };
  return {
    mode: 'offline-only',
    fingerprint: createHash('sha256').update(JSON.stringify(body)).digest('hex'),
    request: { method: 'POST', path: '/wp-json/wp/v2/posts', body },
  };
}

export async function prepareFile(inputPath, outputPath) {
  let input;
  try {
    input = JSON.parse(await readFile(inputPath, 'utf8'));
  } catch {
    throw new Error('입력 JSON을 읽거나 해석할 수 없습니다.');
  }
  const serialized = JSON.stringify(prepareRequest(input), null, 2) + '\n';
  try {
    await writeFile(outputPath, serialized, { encoding: 'utf8', flag: 'wx' });
    return 'created';
  } catch (error) {
    if (error.code !== 'EEXIST') throw error;
    if (await readFile(outputPath, 'utf8') === serialized) return 'unchanged';
    throw new Error('출력 파일에 다른 내용이 있습니다. 검토 후 새 출력 파일명을 사용하세요.');
  }
}

prepare.mjs

import { prepareFile } from './pipeline.mjs';

const [input, output, ...extra] = process.argv.slice(2);
if (!input || !output || extra.length > 0) {
  console.error('사용법: node prepare.mjs input.json prepared-request.json');
  process.exitCode = 1;
} else {
  try {
    const result = await prepareFile(input, output);
    console.log(`${result}: 로컬 요청 명세만 준비했습니다. 네트워크 요청이나 게시를 수행하지 않았습니다.`);
  } catch (error) {
    console.error(error.message);
    process.exitCode = 1;
  }
}

pipeline.test.mjs

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile, writeFile, access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { prepareRequest, prepareFile } from './pipeline.mjs';

const article = { title: '오프라인 워드프레스 요청 연습', slug: 'offline-wordpress-preview', content: '<p>이 파일은 실제로 게시되지 않습니다.</p>' };

test('valid input creates a local draft request description', () => {
  const result = prepareRequest(article);
  assert.equal(result.mode, 'offline-only');
  assert.deepEqual(result.request, { method: 'POST', path: '/wp-json/wp/v2/posts', body: { ...article, status: 'draft' } });
  assert.match(result.fingerprint, /^[a-f0-9]{64}$/);
});

test('the same input always creates the same request', () => {
  assert.deepEqual(prepareRequest(article), prepareRequest({ content: article.content, title: article.title, slug: article.slug }));
});

for (const [name, input] of [
  ['blank title', { ...article, title: '   ' }],
  ['non-English slug', { ...article, slug: '한글-슬러그' }],
  ['uppercase slug', { ...article, slug: 'Invalid-Slug' }],
  ['empty content', { ...article, content: '' }],
  ['missing field', { title: article.title, slug: article.slug }],
  ['additional publish status', { ...article, status: 'publish' }],
  ['null input', null],
]) {
  test(`rejects ${name}`, () => assert.throws(() => prepareRequest(input)));
}

test('rerun preserves identical output and rejects overwriting different content', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'blogflow-offline-test-'));
  const input = join(dir, 'input.json');
  const output = join(dir, 'request.json');
  await writeFile(input, JSON.stringify(article));
  assert.equal(await prepareFile(input, output), 'created');
  const original = await readFile(output, 'utf8');
  assert.equal(await prepareFile(input, output), 'unchanged');
  assert.equal(await readFile(output, 'utf8'), original);
  await writeFile(input, JSON.stringify({ ...article, title: '변경된 제목' }));
  await assert.rejects(prepareFile(input, output), /다른 내용/);
  assert.equal(await readFile(output, 'utf8'), original);
});

test('invalid input writes no output file', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'blogflow-offline-invalid-'));
  const input = join(dir, 'input.json');
  const output = join(dir, 'request.json');
  await writeFile(input, JSON.stringify({ ...article, slug: '잘못된 값' }));
  await assert.rejects(prepareFile(input, output));
  await assert.rejects(access(output));
});

input.json

{
  "title": "오프라인 워드프레스 요청 연습",
  "slug": "offline-wordpress-preview",
  "content": "<p>이 파일은 실제로 게시되지 않습니다.</p>"
}

invalid-input.json

{
  "title": "검증 실패 예제",
  "slug": "한글 슬러그",
  "content": "<p>유효하지 않은 입력은 출력 파일을 만들지 않습니다.</p>"
}

example-prepared-request.json

{
  "mode": "offline-only",
  "fingerprint": "a501ff3ff2f3726e2d4d6a91f6d4e21e7e6bf57ac38be796ea04f0e08b50b851",
  "request": {
    "method": "POST",
    "path": "/wp-json/wp/v2/posts",
    "body": {
      "title": "오프라인 워드프레스 요청 연습",
      "slug": "offline-wordpress-preview",
      "content": "<p>이 파일은 실제로 게시되지 않습니다.</p>",
      "status": "draft"
    }
  }
}

다음 학습

11회차 AI 코딩 에이전트 실전: TaskFlow 기능 요청부터 검증·승인·커밋까지

제품 기능이나 명칭은 바뀔 수 있으므로 발행 시점에는 해당 도구의 공식 문서와 실제 UI를 다시 확인합니다.

실습 파일 다운로드

로컬 모의 파이프라인 전체 파일 ZIP

네트워크 요청이나 실제 WordPress 발행 없이 입력 검증과 로컬 결과물 생성을 연습합니다.

글 소개·학습 준비·전체 목차
AI가 준비한 블로그 문서를 사람이 검토하고 승인하는 자동화 표지

AI 코딩 도구 · 10 / 11

ChatGPT와 Codex로 워드프레스 블로그 자동 업로드 파이프라인 만들기

AI가 단일 코드 수정이 아니라 여러 단계의 실제 자동화 작업을 수행할 때 필요한 입력 계약·상태·중복 방지·승인 경계를 WordPress 업로드 파이프라인 사례로 학습합니다. TaskFlow와 별도 사례이지만 에이전트 자동화 설계를 심화하는 회차입니다.

선수 지식

Node.js에서 파일을 실행하고 JSON의 객체·배열을 읽을 수 있어야 합니다.

이번 글에서 배우는 내용

  • 입력 manifest 또는 작업 계약
  • 생성/검증/업로드 단계 분리
  • API 응답과 내부 상태 구분
  • 중복 실행 방지와 멱등성
  • 응답 유실과 실제 실패 구분
  • 재시도 기준
  • 외부 공개 전 승인 지점
  • API 키와 인증정보 비공개 처리

목차

  1. 핵심 기준
  2. 실무에 적용하는 순서
  3. 독립 실습의 준비
  4. 실행 방법과 확인한 결과
  5. 자주 발생하는 오류와 해결
  6. 실습 프로젝트 전체 코드
  7. TaskFlow 누적 적용
  8. 평가와 중단 기준
  9. 완료 체크리스트
  10. 누적 기록과 실행 결과
  11. 완료 체크포인트
  12. 다음 학습
  13. 단독 실습과 결과 확인

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 11개 · 필수 9개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기