Strapi V4 Docker 설치 가이드 — WordPress 자동 발행 연동까지 완전 정복 (n8n)

-

Strapi Headless CMS Docker WordPress 연동 ComfyUI SNS 자동화
Strapi 설치부터 스키마 설계, WordPress·ComfyUI·SNS 자동 발행까지 — 건너뛰는 단계 없이 클릭 하나하나 따라갈 수 있게 처음부터 끝까지 다시 정리했습니다. 실제로 구축하며 겪은 모든 문제(DB 드라이버 누락, Production 모드 제한, HTML 콘텐츠 깨짐, tmux로 안전하게 작업하는 법)와 해결책이 전부 포함되어 있습니다. n8n 워크플로우는 본문이 아니라 별도 JSON 파일로 분리해서 그대로 Import할 수 있게 제공합니다.
📌
이 가이드 기준 버전 & 환경

Strapi v4.24.2 (엔트리 식별자는 documentId가 아니라 숫자 id) · [서버 혹은 PC] 서버 192.168.0.100 · 경로 /mnt/data/02_automation/strapi · DB 컨테이너명 strapi-pg-db

🗂️
종합 분석 — 실제 구축 과정에서 발견·수정된 전체 이력

이 가이드는 한 번에 완성된 게 아니라, 실제로 부딫힌 문제를 그때그때 반영해서 다듬어진 결과물입니다. 처음 따라가실 때 “왜 이렇게 되어있나” 싶은 부분이 있다면 아래 표를 참고하세요.

분류발견된 문제반영된 해결책
설치DB 드라이버 누락, 호스트/컨테이너 아키텍처 불일치스캐폴딩을 컨테이너 안에서, Debian 계열 베이스 이미지로 고정 (STEP 03)
스키마 설계Production 모드에서 Content-Type Builder 메뉴 자체가 비활성tmux + 임시 dev 컨테이너(1338)로 작업 후 재배포 (STEP 06~09)
본문 필드content를 Rich text로 만들면 raw HTML/CSS가 깨짐Text(Long text)로 변경해 원본 그대로 보존 (STEP 07)
카테고리 필드Strapi Enumeration은 GraphQL Name 규칙상 한글·비ASCII 입력이 원천적으로 불가한글 대신 영문 슬러그(ai-lab, home-lab 등)를 Enum 값으로 직접 사용, 한글 표시는 categoryPath(Text)에 별도 보관
n8n 연동워크플로우 JSON 안의 placeholder 9곳을 어디서 어떻게 채우는지 안내 부재STEP 15에 노드별 체크리스트 표 + Credential 등록 절차 추가
WordPress 발행Rank Math 커스텀 메타가 REST API로 기본 차단됨functions.php에 register_post_meta 등록 (STEP 14)
네트워크 주소[서버 혹은 PC] IP를 .100으로 잘못 기록(실제는 .253) + n8n→Strapi/ComfyUI를 호스트 IP로 호출 시 타임아웃[서버 혹은 PC]은 .253으로 전체 수정, n8n↔Strapi·ComfyUI 내부 호출은 컨테이너명(strapi:1337, comfyui:8188)으로 통일
ComfyUI 이미지 가져오기history 응답 구조를 단순 $json.outputs.filename으로 잘못 가정prompt_id → outputs[SaveImage번호] → images[0] → filename까지 정확히 탐색하는 표현식으로 수정 (STEP 17)
Strapi 인증Header Auth로 안내했으나 실제로는 Generic Credential Type의 Bearer Auth가 더 정확하고 간단함10·11·17번 노드 전부 Bearer Auth Credential 방식으로 전환 (STEP 16 ③)
STEP01

Strapi란 무엇인가

WordPress는 콘텐츠 관리와 화면 출력이 하나로 묶인 CMS입니다. Strapi는 콘텐츠 모델링·저장·API 제공만 담당하는 헤드리스 CMS로, 이 가이드에서는 “콘텐츠 작성·검수 허브”로 쓰고, WordPress를 “최종 게시 채널”로 씁니다. Strapi에서 글을 쓰고 메타정보까지 채운 뒤 Publish를 누르면, 대표 이미지 생성 → WordPress 발행 → SNS 게시까지 자동으로 이어집니다.

STEP02

사전 준비 — 디렉토리 구조 & PostgreSQL

bash
mkdir -p /mnt/data/02_automation/strapi
mkdir -p /mnt/data/02_automation/strapi-postgres/data
mkdir -p /mnt/data/02_automation/strapi-uploads
cd /mnt/data/02_automation/strapi
🚨
절대경로 volumes — 데이터 손실 방지

상대경로(./strapi-uploads)는 docker compose 실행 위치에 따라 마운트 폴더가 달라져 재빌드 시 데이터가 사라질 수 있습니다. 이 가이드는 예외 없이 /mnt/data/... 절대경로만 씁니다.

STEP03

Strapi 프로젝트 생성 & Dockerfile 작성

🚨
실제로 겪은 문제 ① — esbuild 바이너리 아키텍처 불일치

호스트에서 직접 npx create-strapi-app을 실행하면 호스트(glibc) 기준 node_modules가 생성되고, 이게 Alpine(musl) 컨테이너에 섞이면 esbuild가 실행되지 않습니다. 스캐폴딩 자체를 컨테이너 안에서 실행해 이 문제를 원천 차단합니다.

bash — 프로젝트 생성
cd /mnt/data/02_automation
mkdir -p strapi && cd strapi

docker run --rm -it \
  -v "$(pwd):/opt/app" -w /opt/app \
  node:20-bookworm-slim \
  npx create-strapi-app@latest . \
  --typescript --dbclient=postgres \
  --dbhost=strapi-pg-db --dbport=5432 \
  --dbname=strapi --dbusername=strapi \
  --dbpassword=ChangeThisPassword123! \
  --no-run --skip-cloud
⚠️
실제로 겪은 문제 ② — Error: Cannot find module ‘pg’

플래그를 줘도 npx 캐시 문제로 SQLite 기본값(pg 드라이버 없음)으로 생성되는 경우가 있습니다. 생성 직후 반드시 확인하세요.

bash — pg 드라이버 확인 & 강제 추가
grep '"pg"' package.json || echo "❌ pg 누락 — 아래로 추가"

docker run --rm -it -v "$(pwd):/opt/app" -w /opt/app \
  node:20-bookworm-slim npm install pg --save

grep '"pg"' package.json   # 반드시 있어야 함

.dockerignore

bash
node_modules
.tmp
.cache
build
.git
.env
*.md
.editorconfig

Dockerfile.prod — Debian 계열로 고정

Dockerfile.prod
# Stage 1: 빌드
FROM node:20-bookworm-slim AS builder
WORKDIR /opt/app
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential python3 git libvips-dev && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
RUN npm ci
RUN npm install pg --save
COPY . .
ENV NODE_ENV=production
RUN npm run build

# Stage 2: 운영 실행 (경량)
FROM node:20-bookworm-slim AS production
WORKDIR /opt/app
RUN apt-get update && apt-get install -y --no-install-recommends \
    libvips && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /opt/app/build ./build
COPY --from=builder /opt/app/config ./config
COPY --from=builder /opt/app/database ./database
COPY --from=builder /opt/app/public ./public
COPY --from=builder /opt/app/src ./src
COPY --from=builder /opt/app/favicon.png ./favicon.png
ENV NODE_ENV=production
EXPOSE 1337
CMD ["npm", "run", "start"]
STEP04

docker-compose 통합 & 실행

yaml — 메인 docker-compose.yml services: 하위에 추가
  strapi-pg-db:
    image: postgres:16-alpine
    container_name: strapi-pg-db
    restart: always
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: ${STRAPI_DB_PASSWORD}
    volumes:
      - /mnt/data/02_automation/strapi-postgres/data:/var/lib/postgresql/data
    networks: [ai-common-net]
    logging: *default-logging

  strapi:
    build:
      context: /mnt/data/02_automation/strapi
      dockerfile: Dockerfile.prod
    container_name: strapi
    restart: always
    env_file: /mnt/data/.env
    environment:
      - DATABASE_CLIENT=postgres
      - DATABASE_HOST=strapi-pg-db
      - DATABASE_PORT=5432
      - DATABASE_NAME=strapi
      - DATABASE_USERNAME=strapi
      - DATABASE_PASSWORD=${STRAPI_DB_PASSWORD}
      - NODE_ENV=production
    ports: ["1337:1337"]
    volumes:
      - /mnt/data/02_automation/strapi-uploads:/opt/app/public/uploads
    depends_on: [strapi-pg-db]
    networks: [ai-common-net]
    logging: *default-logging
bash — 보안 키 & 실행
cd /mnt/data/02_automation/strapi
cat >> .env << EOF
APP_KEYS=$(openssl rand -base64 32),$(openssl rand -base64 32)
API_TOKEN_SALT=$(openssl rand -base64 32)
ADMIN_JWT_SECRET=$(openssl rand -base64 32)
TRANSFER_TOKEN_SALT=$(openssl rand -base64 32)
JWT_SECRET=$(openssl rand -base64 32)
EOF
echo "STRAPI_DB_PASSWORD=ChangeThisPassword123!" >> /mnt/data/.env

cd /mnt/data
docker compose build strapi
docker compose up -d strapi-pg-db strapi
docker logs strapi -f
정상 실행 확인

로그에 To manage your project 🚀가 보이면 성공입니다. http://192.168.0.100:1337/admin 접속.

STEP05

최초 관리자 계정 설정

1

http://192.168.0.100:1337/admin 접속

2

이름·이메일·강력한 비밀번호 입력 → 계정 생성

Super Admin — 안전하게 보관

STEP06

개발자 모드 진입 — tmux로 안전하게 띄우기

스키마(Content-Type) 작업은 production이 아니라 임시 dev 컨테이너에서 해야 합니다(이유는 STEP 07에서). 이 컨테이너는 --rm -it로 떠서 터미널 연결이 끊기면 그대로 죽습니다. SSH가 끊기거나 노트북이 잠자기 모드에 들어가면 작업 중이던 스키마가 날아갈 수 있으므로, tmux로 띄워서 연결이 끊겨도 안전하게 유지합니다.

① tmux 설치 (없는 경우만)

bash
which tmux || sudo apt install -y tmux

② 헬퍼 스크립트 준비

bash — ~/strapi-dev-mode.sh
#!/bin/bash
set -e
ENV_FILE="/mnt/data/.env"
STRAPI_DIR="/mnt/data/02_automation/strapi"
DEV_PORT=1338

STRAPI_DB_PASSWORD=$(grep -E '^STRAPI_DB_PASSWORD=' "$ENV_FILE" | head -1 | cut -d '=' -f2-)
[[ -z "$STRAPI_DB_PASSWORD" ]] && { echo "❌ STRAPI_DB_PASSWORD 못 찾음"; exit 1; }

docker inspect strapi-pg-db &>/dev/null || { echo "❌ strapi-pg-db가 안 떠있음"; exit 1; }
NETWORK_NAME=$(docker inspect strapi-pg-db --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')
docker rm -f strapi-dev-temp &>/dev/null || true

echo "Dev 모드 시작 — http://192.168.0.100:${DEV_PORT}/admin"
cd "$STRAPI_DIR"

docker run --rm -it \
  --name strapi-dev-temp \
  --network "$NETWORK_NAME" \
  -v "$(pwd):/opt/app" -w /opt/app \
  -p "${DEV_PORT}:1337" \
  -e NODE_ENV=development \
  -e DATABASE_CLIENT=postgres \
  -e DATABASE_HOST=strapi-pg-db \
  -e DATABASE_PORT=5432 \
  -e DATABASE_NAME=strapi \
  -e DATABASE_USERNAME=strapi \
  -e DATABASE_PASSWORD="${STRAPI_DB_PASSWORD}" \
  node:20-bookworm-slim \
  bash -c "npm install && npm run develop"
bash
chmod +x ~/strapi-dev-mode.sh

③ tmux 세션 생성 후 그 안에서 실행

bash
# 1. 이름이 있는 tmux 세션 새로 생성
tmux new -s strapi-dev

# 2. (tmux 안으로 들어온 화면에서) 스크립트 실행
~/strapi-dev-mode.sh
# npm install 3~7분 소요. "To manage your project" 메시지 나오면 준비 완료

④ 세션에서 빠져나오기 (컨테이너는 계속 살아있음)

키 입력의미
Ctrl + B 누른 뒤 Dtmux 세션에서 “빠져나가기”(detach). 안의 프로세스는 계속 실행됨

이제 SSH 연결을 끊거나 터미널 창을 닫아도 dev 컨테이너는 계속 살아있습니다. 브라우저로 http://192.168.0.100:1338/admin 접속해서 STEP 07~09 작업을 이어가시면 됩니다.

⑤ 나중에 다시 들어가기

bash
# 세션 목록 확인 (이름이 기억 안 날 때)
tmux ls

# 해당 세션으로 재접속
tmux attach -t strapi-dev
⚠️
SSH가 갑자기 끊겼다면

detach(Ctrl+B → D)를 못 하고 연결이 끊겨도 tmux 세션 자체는 서버에 남아있습니다. 다시 SSH로 들어가서 tmux attach -t strapi-dev만 치면 끊기기 전 화면 그대로 돌아옵니다. (단, 진짜 컨테이너가 죽었는지는 docker ps -a --filter "name=strapi-dev-temp"로 별도 확인하세요)

⑥ 스키마 작업이 다 끝나면 — 개발자 모드 종료

STEP 09에서 Save를 누르고 Strapi 재시작까지 확인했다면, 이제 dev 컨테이너를 정리합니다.

1

tmux 세션으로 재접속 (위 ⑤ 참고)

tmux attach -t strapi-dev

2

컨테이너 로그가 보이는 화면에서 Ctrl + C

npm run develop 프로세스 종료 신호

3

컨테이너가 --rm 옵션 덕분에 자동 삭제됨

확인: docker ps -a --filter "name=strapi-dev-temp" → 결과 없어야 정상

4

tmux 세션 자체도 종료 (선택)

세션 안에서 exit 입력 — 더 이상 안 쓸 거면 정리

💡
tmux 세션은 그대로 두고 컨테이너만 끄는 것도 가능

나중에 스키마를 또 고칠 일이 있다면, tmux 세션(exit 안 친 상태)은 그대로 두고 컨테이너만 Ctrl+C로 끈 뒤, 필요할 때 tmux attach -t strapi-dev~/strapi-dev-mode.sh 다시 실행하면 됩니다.

STEP07

Content-Type Builder ① — Blog Post 기본 필드 (클릭 단위)

여기서부터는 STEP 06에서 띄운 1338 화면(개발자 모드)에서 진행합니다. http://192.168.0.100:1338/admin으로 접속한 상태에서 시작하세요.

🚨
“Create new collection type” 메뉴가 1337(production)에는 없습니다

STEP 06에서 1338 dev 컨테이너를 띄운 이유가 바로 이것입니다. Strapi는 NODE_ENV=production에서 스키마 생성·수정을 의도적으로 막습니다(Strapi 팀이 “버그가 아닌 의도된 기능”이라고 공식 확인). 지금부터의 모든 작업은 반드시 1338에서 진행하세요.

① Collection Type 생성

1

왼쪽 메뉴 CONTENT-TYPE BUILDER 클릭

2

상단 “Create new collection type” 클릭

3

팝업의 Display name 입력란에 Blog Post 입력

API ID가 자동으로 blog-post로 생성됨

4

“Continue” 클릭 → 필드 추가 화면으로 이동

② title 필드

1

필드 타입 목록에서 Text 클릭

2

Name: title 입력

3

Type 선택에서 Short text 라디오 버튼 선택 (기본값)

4

상단 ADVANCED SETTINGS 탭 클릭 → Required field 체크

5

오른쪽 아래 파란색 Finish 클릭

③ slug 필드

1

필드 목록 맨 아래 “Add another field” 클릭

2

필드 타입에서 UID 클릭

3

Name: slug 입력

4

“Attached field” 드롭다운에서 title 선택

title을 입력하면 slug가 자동으로 채워짐

5

Finish 클릭

④ content 필드 — ⚠️ Rich text가 아니라 Long text로!

🚨
실제로 겪은 문제 — Rich text로 만들면 HTML 코드가 깨집니다

실제 블로그 글은 <div style="..."> 같은 raw HTML/CSS 코드 그 자체입니다. Rich text(Blocks) 타입은 내용을 Strapi 전용 블록 JSON 구조로 변환해서 저장하기 때문에, 입력한 HTML 태그가 그대로 보존되지 않고 깨지거나 escape됩니다. Text(Long text)로 만들어야 입력한 HTML 문자열이 그대로 저장되고, n8n을 거쳐 WordPress로 보낼 때도 코드 유실 없이 발행됩니다.

1

Add another field → 필드 타입에서 Text 클릭

2

Name: content 입력

3

Type 선택에서 Long text 라디오 버튼 클릭 (Short text 아님!)

4

ADVANCED SETTINGS 탭 → Required field 체크

5

Finish 클릭

⚠️
이미 Rich text로 만들어버렸다면

필드 목록에서 content 옆의 연필 아이콘(Edit) 클릭 → 팝업 상단에서 타입을 다시 Text로 선택 → 세부 옵션에서 Long text 선택 → ADVANCED SETTINGS에서 Required 재체크 → Finish. 이미 입력된 내용이 있다면 타입 변경 시 내용이 사라질 수 있으니, 초기 스키마 설계 단계(아직 글을 안 쓴 상태)에서 고치는 걸 추천합니다.

⑤ excerpt 필드

1

Add another field → Text 클릭

2

Name: excerpt, Type: Long text 선택

3

ADVANCED SETTINGS → Maximum length: 200 입력

4

Finish 클릭

⑥ coverImage 필드

1

Add another field → Media 클릭

2

Name: coverImage

3

“Single media” 선택 (Multiple media 아님)

4

ADVANCED SETTINGS → “Select allowed types of media” → Images만 체크

5

Finish 클릭

⑦ category & subCategory 필드 — 대/소분류 슬러그 체계

카테고리를 단일 값이 아니라 대분류(category) + 소분류(subCategory) 2단계로 나눕니다. WordPress에도 동일한 부모-자식 카테고리 구조를 미리 만들어두고, 아래 슬러그로 1:1 매핑합니다.

대분류슬러그소분류슬러그
AI랩ai-lab로컬 AIlocal-ai
워크플로우workflow
음악/노래 생성music-gen
이미지/영상 생성image-media-gen
프롬프트prompt
재테크랩finance일일 시황 리포트daily-stock-reports
자동화 작업investment-automation
주식/ETFstock_etf
홈랩home-lab나스nas
데브옵스devops
보안security
서버/네트워크server-network
1

Add another field → Enumeration 클릭

2

Name: category

3

Values 입력창에 한 줄씩: AI랩 / 재테크랩 / 홈랩

Enter로 한 줄씩 추가

4

Finish 클릭

5

다시 Add another field → Enumeration → Name: subCategory

6

Values에 위 표의 소분류 12개를 한 줄씩 전부 입력

로컬 AI, 워크플로우, 음악/노래 생성, 이미지/영상 생성, 프롬프트, 일일 시황 리포트, 자동화 작업, 주식/ETF, 나스, 데브옵스, 보안, 서버/네트워크

7

Finish 클릭

⑧ wordpressPostId 필드

1

Add another field → Number 클릭

2

Name: wordpressPostId, Number format: integer

3

ADVANCED SETTINGS → Private field 체크

API 응답에 안 보이게 — 내부 추적용

4

Finish 클릭

⚠️
아직 Save 누르지 마세요

여기서 바로 Save해도 되지만, STEP 08의 Component 3개까지 다 만들고 한 번에 Save하면 서버 재시작이 한 번만 일어나서 더 빠릅니다. 이어서 진행하세요.

⑨ Draft & Publish 활성화

1

필드 목록 화면 상단의 ADVANCED SETTINGS 탭 클릭 (필드 팝업이 아니라 Blog Post 전체 설정)

2

“Draft & Publish” 토글을 ON으로 켜기

이게 켜져 있어야 Publish 시점에만 webhook이 트리거됩니다

STEP08

Content-Type Builder ② — Component 3개 생성 (클릭 단위)

대표 이미지·SEO·SNS·티스토리 메타정보를 Component 3개로 묶습니다. Blog Post 화면은 그대로 두고, 왼쪽 메뉴로 이동합니다.

① image-meta 만들기

1

왼쪽 메뉴 COMPONENTS 옆의 “+”(Create new component) 클릭

2

Display name: image-meta 입력

3

Category 입력란에 meta 입력 → 새 카테고리로 생성됨

4

“Configure the component” (또는 Continue) 클릭

필드 추가 화면에서 아래 5개를 하나씩 추가합니다 (각각: 타입 클릭 → Name 입력 → 세부 타입 선택 → Finish):

순서Name필드 타입 클릭세부 타입
1fileNameTextShort text
2altTextTextLong text
3imageTitleTextShort text
4captionTextLong text
5descriptionTextLong text

5개를 모두 추가한 뒤, 팝업창이 아니라 이 component 편집 화면 우측 상단의 Finish를 클릭해 component 생성을 완료합니다.

② seo-meta 만들기

1

다시 COMPONENTS → Create new component 클릭

2

Display name: seo-meta

3

Category: 드롭다운에서 방금 만든 meta 선택 (새로 안 만듦)

4

Configure the component 클릭

순서Name필드 타입세부 타입
1seoTitleTextShort text
2focusKeywordTextShort text
3secondaryKeywordsTextLong text
4metaDescriptionTextLong text
5wpTagsTextLong text
6snsTagsTextLong text
7categoryPathTextShort text

7개 추가 후 Finish 클릭.

③ social-meta 만들기

1

COMPONENTS → Create new component 클릭

2

Display name: social-meta, Category: meta 선택

3

Configure the component 클릭

순서Name필드 타입세부 타입
1snsSummaryTextLong text (Rich text 아님!)
2snsCommentTextLong text
3tistorySummaryTextLong text (Rich text 아님!)
4tistoryUrlTextShort text
5tistoryPublishedBoolean(타입 자체가 참/거짓 스위치)

5개 추가 후 Finish 클릭. 이것으로 Component 3개가 모두 준비됐습니다.

STEP09

Component를 Blog Post에 조립 → 저장 → production 재배포

① Blog Post에 Component 3개 연결

1

왼쪽 메뉴 COLLECTION TYPES → Blog Post 클릭

STEP 07에서 만들던 화면으로 돌아옴

2

필드 목록 맨 아래 “Add another field to this collection type” 클릭

3

필드 타입 목록 가장 아래 Component 클릭

4

팝업 상단 두 번째 탭 “Use an existing component” 클릭 (Create a new component 아님)

5

목록에서 meta.image-meta 선택

6

Name 입력란에 imageMeta 입력

7

타입 선택에서 Single component 라디오 버튼 선택

글 하나당 메타정보는 1세트면 충분하므로

8

Finish 클릭

같은 방식으로 2번 더 반복합니다:

선택할 ComponentName 입력값타입
meta.seo-metaseoMetaSingle component
meta.social-metasocialMetaSingle component

② 최종 저장

1

3개 Component 연결이 모두 끝나면, 화면 우측 상단의 초록색 Save 버튼 클릭

2

Strapi가 스키마 파일(.json)을 생성하며 자동 재시작

화면에 로딩 표시가 사라지고 다시 정상 화면이 보이면 완료

③ 개발자 모드 종료 (STEP 06-⑥ 참고)

bash
# tmux 세션으로 들어가서
tmux attach -t strapi-dev

# 로그 화면에서 Ctrl+C로 종료 → --rm이라 컨테이너 자동 삭제

# 확인 (결과 없어야 정상)
docker ps -a --filter "name=strapi-dev-temp"

④ 스키마 파일 생성 확인 & production 재배포

bash
cd /mnt/data/02_automation/strapi
ls src/api/blog-post/content-types/blog-post/schema.json
ls src/components/meta/

cd /mnt/data
docker compose build strapi
docker compose up -d strapi
production(1337)에서 확인

http://192.168.0.100:1337/admin 접속 → Content Manager에 Blog Post가 보이고, 그 안에 “Create new entry” 버튼이 보이면 정상입니다. (구조를 바꾸는 것만 막혀있고, 글을 쓰는 건 production에서도 항상 가능합니다)

STEP10

API 토큰 · CORS 설정 (파일 위치 + 재배포까지)

① API 토큰 발급

1

1337 화면에서 Settings → API Tokens → Create new API Token

2

Name: n8n-integration / Token duration: Unlimited / Token type: Full access

3

Save 클릭 → 생성된 토큰 즉시 복사 (재확인 불가)

② CORS 설정 파일 위치 및 수정

Strapi v4에서 CORS는 별도 화면이 아니라 코드 파일로 설정합니다.

📂 /mnt/data/02_automation/strapi/config/middlewares.ts
1

터미널에서 해당 파일 열기

nano /mnt/data/02_automation/strapi/config/middlewares.ts

2

기존에 'strapi::cors'라고 단순 문자열로 되어있는 줄을 찾아서, 아래 객체 형태로 교체

TypeScript — config/middlewares.ts 전체
module.exports = [
  'strapi::logger',
  'strapi::errors',
  {
    name: 'strapi::cors',
    config: {
      origin: [
        'https://agibop.com',
        'https://n8n.agibop.com',
        'http://192.168.0.100:1337',
      ],
    },
  },
  'strapi::security',
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
];
⚠️
주소는 예시입니다 — 본인 도메인으로 교체하세요

https://agibop.com 등은 예시이므로, 실제 본인의 워드프레스·n8n 도메인(또는 IP)으로 정확히 바꿔야 합니다.

③ 파일 수정은 코드 수정일 뿐 — 반드시 재배포까지 해야 적용됩니다

bash
# 저장(Ctrl+O → Enter → Ctrl+X) 후
cd /mnt/data
docker compose build strapi
docker compose up -d strapi
🚨
파일만 고치고 재배포를 안 하면 적용 안 됩니다

middlewares.ts는 production 이미지 안에 빌드되어 들어가는 코드입니다. 파일을 수정한 것만으로는 지금 떠있는 컨테이너에 반영되지 않으니, 반드시 build → up -d 까지 실행해야 CORS 설정이 실제로 적용됩니다.

STEP11

사용 방법 — 콘텐츠 작성 & Draft/Publish

1

Content Manager → Blog Post → Create new entry

title 입력 시 slug 자동 생성

2

content에 본문 HTML 통째로 붙여넣기

Long text 필드라 그대로 보존됩니다

3

excerpt, category, subCategory 입력

coverImage는 비워둠 — STEP 17에서 자동으로 채워짐

4

imageMeta, seoMeta, socialMeta 6개 섹션 전부 채우기

이게 곧 예전에 따로 관리하던 meta.md 내용입니다

5

Save(Draft) → 검수 → Publish

Publish 순간에만 자동 파이프라인이 시작됩니다

STEP12

Obsidian + n8n SFTP 연동 — 초안을 자동으로 Strapi에 가져오기

Strapi 관리자 화면에 직접 타이핑하는 대신, 익숙한 Obsidian에서 글을 쓰고, [NAS]에 모인 파일을 n8n이 SFTP로 직접 읽어서 Strapi에 Draft로 자동 생성하는 방식입니다. obsidian-vault Vault의 1. blog-content 폴더 구조를 그대로 활용합니다.

⚠️
Obsidian과 Strapi는 원래 서로를 모릅니다

Obsidian에서 글을 쓰고 Synology Drive로 동기화해도, 그건 [NAS] 디스크에 파일이 저장되는 것뿐입니다. Strapi는 그 폴더의 존재 자체를 모르고, 누군가 API를 호출해줘야만 데이터가 들어갑니다. 이 STEP은 그 “다리” 역할을 — 파이썬 스크립트 대신 n8n으로 만듭니다. [NAS]에 Python·pip를 따로 설치할 필요가 없습니다.

① 기존 Vault 구조 그대로 사용

기존 Vault 구조
obsidian-vault/
├── 0. system/          ← Obsidian 설정·플러그인 등
├── 1. blog-content/     ← 이 STEP에서 사용
│   ├── attachments/     ← 본문에 넣는 이미지 등 첨부파일
│   ├── logs/             ← (선택) 처리 이력 기록용
│   ├── posts/            ← 작성 중인(아직 미완성) 글
│   ├── published/        ← Strapi 전송 완료 후 보관
│   ├── ready/             ← n8n이 주기적으로 감시하는 폴더
│   ├── scripts/           ← (n8n 방식에서는 사용 안 함)
│   └── templates/         ← 새 글 작성용 frontmatter 템플릿
├── 2. work/              ← 모바일/PC 업무용 (이 파이프라인과 무관)
└── 3. life/              ← (이 파이프라인과 무관)

② Synology Drive로 서버([서버 혹은 PC])·모바일·PC 전부 연동

Obsidian을 어디서 쓰든(휴대폰, 노트북, 또는 [서버 혹은 PC] 서버에 직접 SSH로 들어가서) 결국 같은 [NAS] 폴더로 모이게 만드는 게 핵심입니다.

환경연동 방식
PC/노트북Synology Drive Client 설치 → obsidian-vault 동기화 항목 추가
휴대폰Synology Drive 모바일 앱 → 같은 Vault 동기화 (오프라인 작성 후 자동 업로드)
[서버 혹은 PC] 서버아래 ③ 참고 — Drive Client 대신 NFS/SMB 마운트로 직접 연결

③ [서버 혹은 PC] 서버에서 [NAS] Vault에 직접 연결 (서버 연동)

서버는 Synology Drive Client(GUI 프로그램)를 설치할 수 없으므로, [NAS] 공유 폴더를 네트워크 파일시스템으로 직접 마운트합니다. 이렇게 하면 [서버 혹은 PC]에서도 마치 로컬 폴더처럼 Vault에 접근할 수 있습니다(이 가이드의 n8n SFTP 방식에서는 필수는 아니지만, [서버 혹은 PC]에서 직접 파일을 열어보거나 백업할 때 유용합니다).

1

[NAS] DSM → 제어판 → 파일 서비스 → NFS 탭 → NFS 서비스 활성화

2

제어판 → 공유 폴더 → obsidian-vault 선택 → 편집 → “NFS 권한” 탭 → 생성

Hostname/IP: [서버 혹은 PC]의 IP(192.168.0.100) · Privilege: 읽기/쓰기 · Squash: No mapping(기본값이 다르면 권한이 있어도 거부될 수 있음)

3

[서버 혹은 PC]에서 마운트

bash — [서버 혹은 PC]에서 [NAS] Vault 마운트
sudo apt install -y nfs-common
sudo mkdir -p /mnt/obsidian-vault
sudo mount -t nfs 192.168.0.150:obsidian-vault /mnt/obsidian-vault

# 재부팅 후에도 유지되게 fstab에 등록
echo "192.168.0.150:obsidian-vault /mnt/obsidian-vault nfs defaults 0 0" | sudo tee -a /etc/fstab
💡
이 STEP의 핵심 파이프라인은 NFS 마운트가 아니라 SFTP입니다

위 NFS 마운트는 [서버 혹은 PC]에서 Vault 파일을 직접 들여다보고 싶을 때를 위한 선택 사항입니다. 아래 ④ n8n 워크플로우는 NFS 마운트 없이도 SFTP로 곧바로 [NAS]에 접근하므로, 이 ③번 단계를 건너뛰셔도 무방합니다.

④ [NAS]에서 SFTP 활성화

1

DSM → 제어판 → 파일 서비스 → FTP 탭

2

“SFTP 서비스 활성화” 체크 → 적용

SSH와 같은 계정·포트(기본 22)를 그대로 씁니다 — 추가 계정 생성 불필요

🚨
기본 포트(22)는 반드시 변경을 권장합니다

SFTP는 SSH와 같은 22번 포트를 그대로 씁니다. 이 포트가 외부에 노출되어 있다면(공유기 포트포워딩 등) 22번은 전 세계에서 가장 많이 자동 스캔·무차별 대입 공격을 받는 포트입니다. 외부 노출 여부와 무관하게, 다음을 적용하시길 권장합니다.

  • DSM → 제어판 → 터미널 및 SNMP → SSH 서비스 → 포트 번호를 22가 아닌 다른 값(예: 2222, 22022 등)으로 변경
  • 외부에서 접속할 계획이 없다면 공유기에서 22/SFTP 포트포워딩 자체를 열지 않기 — 이 가이드의 n8n↔[NAS] 통신은 같은 내부망이라 포트포워딩이 필요 없습니다
  • 포트를 바꾸면 STEP 12 ⑦의 SFTP Credential과 STEP 12 ③의 NFS 마운트 예시에서도 Port 값을 동일하게 수정해야 합니다

⑤ 템플릿 만들기 — templates/blog-post-template.md

Markdown — 1. blog-content/templates/blog-post-template.md
---
title: 
category: 
subCategory: 
excerpt: 
seoTitle: 
focusKeyword: 
secondaryKeywords: 
metaDescription: 
categoryPath: 
wpTags: 
snsTags: 
fileName: 
altText: 
imageTitle: 
caption: 
description: 
snsSummary: 
snsComment: 
tistorySummary: 
---

여기부터 본문 HTML 작성...

⑥ n8n 워크플로우 — SFTP로 직접 읽어서 Strapi에 전송

Webhook 대신 Schedule Trigger로 시작하는 별도의 작은 워크플로우입니다(STEP 16의 메인 발행 워크플로우와는 완전히 분리된 워크플로우입니다).

📦
파일로 한 번에 가져오기 — 8개 노드 전부 포함

아래 ⑦~⑪에서 설명하는 노드 8개가 obsidian-sftp-to-strapi-workflow.json 파일 하나에 전부 들어있습니다. n8n에서 Workflows → Import from File로 통째로 가져오시면, 직접 하나씩 만들 필요 없이 Credential 연결과 placeholder 교체만 하면 됩니다.

⑥-1. 파일 Import & 설정 (클릭 단위)

1

n8n → Workflows → “…” 메뉴 → Import from File

2

obsidian-sftp-to-strapi-workflow.json 선택

8개 노드가 한 번에 캔버스에 펼쳐짐

3

Save (아직 Active는 켜지 않음)

노드해야 할 일
1) SFTP 목록 조회Authentication → ⑦에서 만들 [NAS] SFTP Credential 연결
2) 파일 있을 때만 진행그대로 둠 — ready/ 폴더가 비어있을 때 SFTP 에러가 발생하는 것을 방지하는 IF 노드. 파일이 없으면 조용히 종료됩니다
2) .md 파일만 필터그대로 둠 — .md가 아닌 파일(임시파일 등)이 섞여도 자동으로 건너뜀
3) SFTP 파일 다운로드위와 동일한 [NAS] SFTP Credential 연결
4) frontmatter 파싱그대로 둠 — title이 비어있는 미완성 글은 자동으로 건너뜀
5) Strapi에 Draft 생성Authentication → Generic Credential Type → Bearer Auth → STEP 16 ③의 Strapi 토큰 Credential 그대로 재사용
6) published로 이동 (성공)Credential 연결 (1번과 동일) — 경로는 1) SFTP 목록 조회의 실제 결과값을 직접 참조하므로 수정 불필요
6) 실패 시 FAILED_ 접두사 표시Credential 연결 (1번과 동일)
🚨
rename 경로를 직접 수정하지 마세요 — 특히 $json.fileName 절대 금지

6번 rename 노드의 경로(oldPath/newPath)를 손으로 고치다가 생기는 에러가 실제로 가장 흔한 문제입니다. $json.fileName은 rename 노드 시점에서 Strapi API 응답({ id, title, … })을 가리키기 때문에 항상 undefined가 됩니다. 이 파일의 두 rename 노드는 1) SFTP 목록 조회의 결과($json.path, $json.name)를 직접 참조하도록 이미 설정되어 있습니다 — 그대로 두세요.

💡
FAILED_ 파일을 발견하면

ready/ 폴더에 FAILED_제목.md 형태의 파일이 보이면, frontmatter 작성 실수(필수 필드 누락 등)나 Strapi 연결 문제가 있었다는 신호입니다. n8n Executions에서 해당 실행 기록을 열어 5번 노드의 에러를 확인한 뒤, 파일명에서 FAILED_를 지우고 다시 ready/에 두면 다음 주기에 재시도됩니다.

⑥-2. 아래는 위 파일에 포함된 각 노드의 상세 동작 설명입니다

이미 파일을 Import하셨다면 이 부분은 참고용입니다 — 노드를 처음부터 직접 만들고 싶으신 분만 따라가시면 됩니다.

전체 흐름
Schedule Trigger (10분마다)
    ↓
SFTP: ready/ 폴더 목록 조회 (List)
    ↓
SFTP: 파일별로 다운로드 (Download)
    ↓
Code: frontmatter 파싱 (순수 JavaScript, 외부 라이브러리 불필요)
    ↓
HTTP Request: Strapi에 Draft 생성 (POST /api/blog-posts)
    ↓
SFTP: 성공한 파일을 published/로 이동 (Rename)

⑦ SFTP Credential 등록

1

n8n Credentials → Create new → “SFTP” 검색

2

Host: 192.168.0.150 / Port: ④에서 변경한 포트 번호(예: 2222) / Username·Password: [NAS] 로그인 계정

기본값 22를 그대로 쓰셨다면 22 입력 — 단, ④의 보안 권고대로 변경하셨다면 그 값을 정확히 입력

3

Save → 이름 예: [NAS] SFTP

⑧ SFTP 목록 조회 + 다운로드 노드

n8n 노드 설정
# 1) SFTP 노드 — List
Operation: List
Path: obsidian-vault/1. blog-content/ready

# 2) SFTP 노드 — Download (위 결과의 각 파일에 대해 반복)
Operation: Download
Path: ={{ $json.path }}

⑨ frontmatter 파싱 — Python 없이 순수 JavaScript로

🚨
binaryDataMode: filesystem 환경에서 흔히 겪는 에러 — 아래 코드로 해결됩니다

n8n 설정이 binaryDataMode: filesystem(Self Hosted 기본값)이면, SFTP로 받은 파일이 디스크 참조 객체({ id: "uuid" })로 전달됩니다. 예전 코드의 .binary.data.toString('utf-8')은 이 참조 객체에 직접 toString()을 호출하는 거라 실제 내용이 아니라 깨진 바이트 배열이 반환됩니다. 반드시 this.helpers.getBinaryDataBuffer()로 읽어야 합니다. Sublime Text의 UTF-8 with BOM(~)^ 형태로 오염)과 Windows 줄바꿈(\r\n) 문제도 같이 처리했습니다.

JavaScript — Code 노드 (filesystem & memory 모드 자동 대응)
const item = $input.first();
const binary = item.binary?.data;

if (!binary) return [];

let raw;

try {
  if (binary.id !== undefined) {
    // binaryDataMode: filesystem — 디스크 파일을 helpers로 읽음
    // (직접 .toString() 하면 참조 객체만 오고 내용이 읽히지 않음)
    const buffer = await this.helpers.getBinaryDataBuffer(0, 'data');
    raw = buffer.toString('utf-8');
  } else if (binary.data) {
    // binaryDataMode: default(memory) — base64 인코딩 상태
    raw = Buffer.from(binary.data, 'base64').toString('utf-8');
  } else {
    return [];
  }
} catch(e) {
  // 읽기 실패 → 조용히 건너뜀 (다음 주기에 재시도 가능)
  return [];
}

// BOM 제거 (Sublime Text UTF-8 with BOM → \uFEFF 로 시작)
// Windows 줄바꿈 정규화 (\r\n → \n, 단독 \r → \n)
raw = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');

// frontmatter 파싱
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return [];

const meta = {};
match[1].split('\n').forEach(line => {
  const idx = line.indexOf(':');
  if (idx === -1) return;
  const key = line.slice(0, idx).trim();
  let val = line.slice(idx + 1).trim();
  if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
  meta[key] = val;
});

const body = match[2];
if (!meta.title) return [];

return [{ json: { meta, body, fileName: $('1) SFTP 목록 조회').item.json.name } }];

⑩ Strapi에 Draft 생성

HTTP Request — POST /api/blog-posts
Authentication: Bearer Auth (STEP 16 ③과 동일한 Strapi 토큰 Credential 재사용)

Body (JSON):
{
  "data": {
    "title": {{ JSON.stringify($json.meta.title) }},
    "content": {{ JSON.stringify($json.body) }},
    "excerpt": {{ JSON.stringify($json.meta.excerpt) }},
    "category": {{ JSON.stringify($json.meta.category) }},
    "subCategory": {{ JSON.stringify($json.meta.subCategory) }},
    "imageMeta": {
      "fileName": {{ JSON.stringify($json.meta.fileName) }},
      "altText": {{ JSON.stringify($json.meta.altText) }},
      "imageTitle": {{ JSON.stringify($json.meta.imageTitle) }},
      "caption": {{ JSON.stringify($json.meta.caption) }},
      "description": {{ JSON.stringify($json.meta.description) }}
    },
    "seoMeta": {
      "seoTitle": {{ JSON.stringify($json.meta.seoTitle) }},
      "focusKeyword": {{ JSON.stringify($json.meta.focusKeyword) }},
      "secondaryKeywords": {{ JSON.stringify($json.meta.secondaryKeywords) }},
      "metaDescription": {{ JSON.stringify($json.meta.metaDescription) }},
      "categoryPath": {{ JSON.stringify($json.meta.categoryPath) }},
      "wpTags": {{ JSON.stringify($json.meta.wpTags) }},
      "snsTags": {{ JSON.stringify($json.meta.snsTags) }}
    },
    "socialMeta": {
      "snsSummary": {{ JSON.stringify($json.meta.snsSummary) }},
      "snsComment": {{ JSON.stringify($json.meta.snsComment) }},
      "tistorySummary": {{ JSON.stringify($json.meta.tistorySummary) }}
    }
  }
}
⚠️
publishedAt을 지정하지 않아야 Draft로 생성됩니다

위 Body에 publishedAt 필드가 없으므로 Strapi에 Draft 상태로만 생성됩니다. Publish는 여전히 Strapi 화면에서 사람이 검수 후 직접 누릅니다. Obsidian에서 “자동 발행”까지 건너뛰지 않도록 하는 안전장치입니다.

⑪ 성공한 파일을 published로 이동

n8n SFTP 노드 — Rename
Operation: Rename
Old Path: =obsidian-vault/1. blog-content/ready/{{ $json.fileName }}
New Path: =obsidian-vault/1. blog-content/published/{{ $json.fileName }}

⑫ 실제 사용 흐름

🚨
n8n은 파일 수정을 감지하는 게 아닙니다 — ready/ 폴더에 직접 옮겨야 처리됩니다

워크플로우는 “파일이 변경됐는지”를 전혀 감시하지 않습니다. 단순히 10분마다 ready/ 폴더 안에 .md 파일이 있는지를 확인할 뿐입니다. 따라서 templates/posts/에서 아무리 글을 써도, 직접 ready/ 폴더로 옮기기 전까지는 아무 일도 일어나지 않습니다.

폴더n8n이 처리하는가용도
templates/❌ 절대 안 봄빈 양식 보관 — 새 글 시작할 때 복사해서 쓰는 용도
posts/❌ 안 봄작성 중인 미완성 글 보관
ready/✅ 10분마다 감시여기 넣는 순간 자동 처리 대상이 됨
published/❌ 안 봄전송 완료된 글 보관 (n8n이 자동 이동)
전체 사용 흐름
① templates/blog-post-template.md 복사
        ↓
② posts/내글제목.md 로 이름 바꾸고 그 안에서 작성 (작성 중, 안전)
        ↓
③ 완성되면 Obsidian에서 ready/내글제목.md 로 이동
  (Synology Drive가 [NAS]에 즉시 동기화)
        ↓
④ n8n Schedule Trigger(10분마다 자동 실행)
   → SFTP로 ready/ 스캔 → .md 파일 발견
   → frontmatter 파싱 → Strapi에 Draft 생성
   → 성공하면 published/로 자동 이동 / 실패하면 FAILED_ 접두사 표시
        ↓
⑤ Strapi(1337) 접속 → 생성된 Draft 확인·검수
   → coverImage 등 Obsidian으로 못 채운 부분 보완 → Publish
        ↓
(이후는 STEP 16~23과 동일한 자동 파이프라인)
[NAS]에는 아무것도 설치하지 않아도 됩니다

이 방식은 [NAS]에 Python·pip가 없어도 그대로 동작합니다. [NAS]는 SFTP 서비스만 켜두면 되고, 실제 파일을 읽고 파싱하고 전송하는 모든 작업은 이미 [서버 혹은 PC]에서 돌고 있는 n8n이 처리합니다.

💡
이 단계는 선택사항입니다

Strapi 화면에서 직접 쓰는 게 더 편하시면 STEP 11 방식 그대로 쓰셔도 전혀 문제없습니다. 이 STEP은 “이동 중 휴대폰으로 초안만 빠르게 써두고 싶을 때”를 위한 보조 경로입니다.

STEP13

REST API 직접 호출 테스트

bash — v4 기준, id 사용
curl -s "http://192.168.0.100:1337/api/blog-posts?populate=*" \
  -H "Authorization: Bearer YOUR_API_TOKEN" | jq

curl -s "http://192.168.0.100:1337/api/blog-posts/1?populate=*" \
  -H "Authorization: Bearer YOUR_API_TOKEN" | jq
STEP14

WordPress 측 사전 준비 — Application Password & Rank Math & 카테고리

① Application Password 발급

1

WordPress 관리자 → 사용자 → 프로필 편집

2

“Application Passwords” 섹션 → 이름 입력 → Add New

3

생성된 비밀번호 공백 포함 그대로 복사 (재확인 불가)

② WordPress에 동일한 부모-자식 카테고리 생성

STEP 07에서 만든 슬러그 체계와 정확히 일치하게, WordPress 카테고리도 미리 만들어둡니다.

1

글 → 카테고리 → 새 카테고리 추가

2

이름: AI랩, 슬러그: ai-lab, 상위 카테고리: 없음

재테크랩(finance), 홈랩(home-lab)도 동일하게 부모 카테고리로 생성

3

소분류 12개를 각각 알맞은 부모 아래 자식 카테고리로 생성

예: 이름 “로컬 AI”, 슬러그 “local-ai”, 상위 카테고리 “AI랩”

💡
슬러그를 정확히 맞춰야 하는 이유

STEP 18의 n8n 워크플로우는 슬러그로 WordPress 카테고리 ID를 자동 조회합니다. 슬러그가 다르면 조회에 실패해 미분류로 발행됩니다.

③ Rank Math 커스텀 필드 REST API 등록

Strapi의 seoTitle·focusKeyword·metaDescription을 WordPress REST API가 기본적으로 막아두기 때문에, functions.php에 1회 등록해야 실제로 Rank Math 패널에 반영됩니다.

PHP — Child Theme functions.php
add_action('rest_api_init', function() {
    $fields = [
        'rank_math_title'             => 'string',
        'rank_math_description'       => 'string',
        'rank_math_focus_keyword'     => 'string',
        'rank_math_secondary_keyword' => 'string',
    ];
    foreach ($fields as $key => $type) {
        register_post_meta('post', $key, [
            'show_in_rest'  => true,
            'single'        => true,
            'type'          => $type,
            'auth_callback' => function() { return current_user_can('edit_posts'); }
        ]);
    }
});
STEP15

Strapi Webhook 설정

1

1337 화면 → Settings → Webhooks → Create new webhook

2

Name: wordpress-auto-publish

3

URL: https://n8n.agibop.com/webhook/strapi-publish

4

Headers 추가: X-Webhook-Secret = 임의의 강력한 문자열

5

Events에서 Entry → Publish만 체크 → Save

STEP16

n8n 워크플로우 Import & 전체 설정 (클릭 단위)

이 가이드와 함께 제공하는 strapi-full-pipeline-workflow.json에는 Webhook 수신부터 페이스북 댓글까지 19개 노드가 전부 들어있습니다. 직접 하나씩 만들 필요 없이 통째로 가져온 뒤, 표시된 placeholder만 실제 값으로 교체하면 됩니다.

① 파일 가져오기

1

n8n 접속 → 좌측 상단 Workflows 클릭

2

우측 상단 “…” (More) 메뉴 → Import from File 클릭

3

strapi-full-pipeline-workflow.json 선택

19개 노드가 캔버스에 한 번에 펼쳐짐

4

우측 상단 Save 클릭 (아직 Active는 켜지 않음)

⚠️
아직 Active 토글을 켜지 마세요

placeholder를 다 채우고 테스트까지 끝낸 뒤(이 STEP 마지막 ⑤번)에 켜는 게 안전합니다. 미리 켜두면 빈 토큰으로 실제 글이 잘못 발행될 수 있습니다.

② 노드별 설정 전체 체크리스트 — 실제 검증된 최종판

아래 표 순서대로, 해당 노드를 캔버스에서 클릭 → 값 수정 → 빠져나오기를 반복하세요. URL은 이미 컨테이너명(strapi:1337, comfyui:8188)으로 채워져 있어 손댈 필요 없습니다 — 인증(Authentication)만 연결하시면 됩니다.

노드 번호호출 대상인증 방식해야 할 일
3. IF 시크릿 검증YOUR_WEBHOOK_SECRET를 STEP 15에서 만든 X-Webhook-Secret 문자열로 교체
5. ComfyUI 작업 등록ComfyUI없음이미 실제 워크플로우 JSON이 채워져 있음 — 본인 ComfyUI에 체크포인트 모델명만 일치하는지 확인
7. 생성 완료 확인ComfyUI없음그대로 둠
9. 이미지 가져오기ComfyUI없음그대로 둠 (history 응답 구조 탐색 표현식 이미 포함됨)
10, 11, 17StrapiBearer Auth③ 참고
12. WP 카테고리 ID 조회WordPressBasic Auth④ 참고
13. 이미지 다시 받기Strapi(정적 파일)없음그대로 둠 — 손댈 필요 없음
14. WP 미디어 업로드WordPressBasic Auth④ 참고, URL 도메인을 본인 워드프레스로 교체
15. WP alt/caption 채우기WordPressBasic Auth④ 참고
20. WP 부모 카테고리 조회WordPressBasic Auth④ 참고 — 14·15·16과 동일 Credential
21. 태그 이름 분리없음그대로 둠
22. WP 태그 생성WordPressBasic Auth④ 참고 + Options → Response → Never Error를 반드시 ON (STEP 18 ⑤)
23. 태그ID+카테고리 집계없음그대로 둠
16. WordPress 글 발행WordPressBasic Auth④ 참고, URL 도메인 교체
18. 페이스북 페이지 게시FacebookBody에 토큰 직접 입력YOUR_PAGE_ID, YOUR_PAGE_ACCESS_TOKEN 교체 (STEP 19 “사전 준비”)
19. 페이스북 댓글 작성FacebookBody에 토큰 직접 입력YOUR_PAGE_ACCESS_TOKEN 교체 (위와 동일)

③ Strapi — Bearer Auth Credential 등록 (10·11·17번 공통)

1

10번 노드 클릭 → Authentication 드롭다운 → Generic Credential Type 선택

2

Generic Auth Type 드롭다운 → Bearer Auth 선택

3

“Set up credential” 클릭 → Token 입력란에 토큰값만 붙여넣기

“Bearer ” 글자는 직접 적지 마세요 — n8n이 자동으로 붙여줍니다

4

Save → 이름 예: Strapi N8N Token

5

11번, 17번 노드도 각각 Authentication → Generic Credential Type → Bearer Auth → 방금 만든 Credential 선택

새로 만들지 말고 드롭다운에서 기존 것 선택

🚨
“Bearer ” 중복 입력 주의

Token 칸에 Bearer 토큰값처럼 직접 “Bearer “를 같이 넣으시면, 실제 요청은 Authorization: Bearer Bearer 토큰값이 되어 Strapi가 거부합니다. 토큰 문자열만 넣으세요.

④ WordPress — Basic Auth Credential 등록 (12·14·15·16번 공통)

1

12번 노드 클릭 → Authentication 드롭다운 → Generic Credential Type 선택

2

Generic Auth Type 드롭다운 → Basic Auth 선택

3

“Set up credential” 클릭 → User: 워드프레스 로그인 계정명 / Password: STEP 14에서 발급한 Application Password (공백 포함 그대로)

4

Save → 이름 예: WordPress Application Password

5

14·15·16번 노드도 각각 같은 방식으로 → 드롭다운에서 방금 만든 Credential 선택

13번은 건드리지 않음 (인증 불필요)

⑤ 테스트 — 실제로 한 번 끝까지 돌려보기

1

Strapi(1337)에서 테스트용 글 작성 → 6개 메타 섹션 전부 입력 → Publish

2

n8n 좌측 메뉴 Executions 클릭 → 방금 실행 항목 열기

3

노드를 순서대로 클릭하며 각각 초록색 체크인지 확인

빨간색이면 그 노드를 클릭 → 우측 패널의 에러 메시지로 원인 파악

4

19번 노드까지 전부 성공하면 → 워크플로우 우측 상단 Active 토글 ON

이제부터는 자동입니다

Active를 켠 이후로는 Strapi에서 Publish만 누르면, 위 19개 노드가 자동으로 순서대로 실행됩니다. 다음 STEP 17~19은 각 단계의 동작 원리를 더 깊이 설명합니다 — 이미 ②~④에서 설정을 마쳤다면 참고용으로 읽으시면 됩니다.

STEP17

대표 이미지 자동 생성 — ComfyUI 연동

STEP 16에서 워크플로우를 Import하고 설정까지 마치셨다면, 여기서는 그 중 이미지 생성 부분(5~11번 노드)이 내부적으로 어떻게 동작하는지 자세히 설명합니다. 5번 노드의 ComfyUI 워크플로우 JSON을 아직 안 채우셨다면 아래 내용을 먼저 참고하세요.

이미지 생성 흐름
title/category로 프롬프트 작성 (Code 노드)
    ↓ ComfyUI POST /prompt (작업 등록)
    ↓ GET /history/{id} 반복 확인 (완료 대기, Wait+IF 루프)
    ↓ GET /view (완성 이미지 가져오기)
    ↓ Strapi POST /api/upload (Media Library 업로드)
    ↓ Strapi PUT /api/blog-posts/:id (coverImage 연결)

노드별 핵심 설정

노드핵심 설정
Code: 프롬프트 생성category별 스타일 키워드를 매핑해 ComfyUI 프롬프트 문자열 생성
HTTP: ComfyUI 작업등록POST http://comfyui:8188/prompt — Body에 “Save (API Format)”으로 내보낸 워크플로우 JSON 통째로, 프롬프트 텍스트 노드 값만 교체
Wait5초 대기
HTTP: 완료 확인GET /history/{{prompt_id}} — 결과 있을 때까지 Wait와 함께 루프
HTTP: 이미지 가져오기GET http://comfyui:8188/view?filename={{ ComfyUI history 응답에서 prompt_id→outputs['SaveImage노드번호']→images[0]→filename까지 탐색 }}&type=output, Response Format: File
HTTP: Strapi 업로드POST /api/upload, Body Type: Form-Data, Authorization: Bearer 토큰
HTTP: coverImage 연결PUT /api/blog-posts/{{id}}, Body: {"data":{"coverImage": 업로드결과ID}}
⚠️
ComfyUI 워크플로우 JSON은 본인 환경에 맞게 다릅니다

ComfyUI 화면에서 설정 → “Enable Dev mode Options” 켜면 “Save (API Format)” 버튼이 생깁니다. 그 형식으로 내보낸 그대로 사용하고, 프롬프트 텍스트가 들어가는 노드의 text 값만 n8n 변수로 교체하세요.

💡
생성 실패 대비 — 안전장치

폴링이 일정 횟수(예: 30회=2분30초)를 넘으면 타임아웃으로 처리하고, 이미지 없이 본문 발행을 계속 진행하도록 분기를 만드세요. 별도로 만들어둔 텔레그램 알림 워크플로우에 “이미지 생성 실패: {title}” 메시지를 보내면 나중에 수동 보완할 수 있습니다.

⚠️
Webhook 응답은 즉시, 처리는 비동기로

이미지 생성까지 포함하면 파이프라인이 수십 초~수 분 걸립니다. Strapi Webhook이 타임아웃 나지 않도록 n8n Webhook 노드의 Response Mode를 “Immediately respond”로 설정하세요.

STEP18

n8n — WordPress 자동 발행 (카테고리·태그 자동 매핑 + Rank Math + 이미지)

① 자식 카테고리 ID 조회 (12번 노드)

STEP 14에서 만든 슬러그를 활용해, 하드코딩된 ID 대신 실시간으로 조회합니다 — 카테고리 ID가 바뀌어도 워크플로우를 안 고쳐도 됩니다.

HTTP Request — 자식 카테고리 ID 조회 (subCategory 슬러그 사용)
GET https://agibop.com/wp-json/wp/v2/categories?slug={{ entry.subCategory }}
응답: [{ "id": 23, ... }]
💡
슬러그를 직접 사용 — 별도 매핑 테이블 불필요

STEP 07에서 category/subCategory를 처음부터 영문 슬러그(home-lab, devops 등)로 저장해뒀기 때문에, 한글→슬러그 변환 코드가 따로 필요 없습니다. 4번 Code 노드가 만든 값을 그대로 씁니다.

② 이미지를 WordPress 미디어로 업로드 (13~15번 노드)

HTTP Request 3개
# 1) Strapi 이미지 다시 받기 (populate=coverImage로 채워진 URL 사용)
GET http://strapi:1337{{ coverImage URL }}

# 2) WP 미디어 업로드
POST https://agibop.com/wp-json/wp/v2/media
Authentication: Basic Auth (Application Password)
Content-Disposition: attachment; filename="{{ imageMeta.fileName }}.png"
Content-Type: image/png
Body: (위 binary, Input Data Field Name: data)
응답: { "id": 1234 }

# 3) alt/caption/title 채우기
POST https://agibop.com/wp-json/wp/v2/media/{{ wpMediaId }}
Body: { "alt_text": JSON.stringify(imageMeta.altText),
        "caption": JSON.stringify(imageMeta.caption),
        "title": JSON.stringify(imageMeta.imageTitle) }

③ 부모 카테고리 ID 조회 (20번 노드) — 신규

②까지 했을 때 자식 카테고리만 보내면 워드프레스 카테고리 목록에서 부모가 체크 안 된 상태로 발행됩니다. 부모도 별도로 조회해서 같이 보내야 합니다.

HTTP Request — 부모 카테고리 ID 조회 (category 슬러그 사용)
GET https://agibop.com/wp-json/wp/v2/categories?slug={{ entry.category }}
응답: [{ "id": 5, ... }]

④ 태그 이름 분리 (21번 노드) — 신규

seoMeta.wpTags(쉼표로 구분된 문자열, 예: "Obsidian, Strapi, 자동화")를 태그 하나당 1개 item으로 쪼갭니다.

JavaScript — Code 노드
const entry = $('4. Code: 프롬프트 생성 + 슬러그 매핑').item.json;
const tagNames = (entry.seoMeta.wpTags || '').split(',').map(s => s.trim()).filter(Boolean);

// 태그가 없으면 빈 배열 — 다음 노드(22번)가 아예 실행 안 되고 자연스럽게 건너뜀
return tagNames.map(name => ({ json: { tagName: name } }));

⑤ WordPress 태그 생성 (22번 노드) — 신규, 핵심 함정 주의

태그 이름마다 WordPress에 생성을 시도합니다. 문제는 이미 존재하는 태그 이름으로 또 만들려고 하면 WordPress가 400(term_exists) 에러로 거부한다는 점입니다 — 실제로는 거의 모든 발행에서 이런 일이 일어납니다(같은 태그를 여러 글에서 재사용하니까요).

🚨
실제로 겪은 문제 — 태그가 한 번도 안 들어가던 원인

WordPress의 400 거부 응답 본문 안에는 사실 이미 존재하는 그 태그의 ID(data.term_id)가 들어있어서, 정상적으로는 이걸 꺼내 쓰면 됩니다. 그런데 n8n의 HTTP Request 노드는 4xx/5xx 응답을 “실패”로 처리하는 게 기본값이라, 옵션을 안 켜두면 이 본문이 그대로 안 넘어오고 n8n이 자체적으로 만든 에러 래퍼만 넘어옵니다. 결과적으로 같은 태그를 두 번째 쓰는 순간부터 그 태그의 ID를 영영 못 찾고, 매번 빈 태그 목록으로 발행되고 있었습니다.

해결책은 “Never Error” 옵션을 켜서, 4xx 응답도 정상 응답처럼 본문을 그대로 받아오게 만드는 것입니다.

1

22번 노드 클릭 → Options 섹션 펼치기

2

“Add option” → Response 항목 추가

3

그 안의 “Never Error” 토글을 ON으로 켜기

이걸 켜야 400 응답도 정상 응답처럼 본문이 그대로 넘어옵니다

HTTP Request — 태그 생성
POST https://agibop.com/wp-json/wp/v2/tags
Authentication: Basic Auth
Body: { "name": "{{ $json.tagName }}" }
Options → Response → Never Error: ON

성공 시 응답:  { "id": 99, "name": "...", ... }
이미 있을 때 응답(이제는 정상으로 받아짐):
  { "code": "term_exists", "data": { "status": 400, "term_id": 99 } }

⑥ 태그ID + 카테고리 집계 (23번 노드) — 신규

22번이 만든 N개의 결과(새 태그든 기존 태그든)를 하나의 ID 배열로 합치고, 부모·자식 카테고리 ID까지 한 item으로 정리합니다.

JavaScript — Code 노드 (Run Once for All Items)
const items = $input.all();
const tagIds = [];
for (const item of items) {
  const d = item.json;
  if (d.id) tagIds.push(d.id);                                    // 새로 생성된 태그
  else if (d.data && d.data.term_id) tagIds.push(d.data.term_id);  // 이미 존재하던 태그
}

const entry = $('4. Code: 프롬프트 생성 + 슬러그 매핑').item.json;
const parentCategoryId = $('20. WP 부모 카테고리 ID 조회').item.json.id;
const childCategoryId = $('12. HTTP: WP 카테고리 ID 조회').item.json.id;

return [{ json: { ...entry, tagIds, parentCategoryId, childCategoryId } }];

⑦ 글 발행 — 부모+자식 카테고리, 태그, Primary Term 전부 포함 (16번 노드)

HTTP Request — POST /wp-json/wp/v2/posts
{
  "title": JSON.stringify(title),
  "slug": JSON.stringify(slug),
  "content": JSON.stringify(content),
  "excerpt": JSON.stringify(excerpt),
  "status": "publish",
  "categories": [parentCategoryId, childCategoryId],
  "tags": [tagIds 전체],
  "featured_media": wpMediaId,
  "meta": {
    "rank_math_title": JSON.stringify(seoMeta.seoTitle),
    "rank_math_description": JSON.stringify(seoMeta.metaDescription),
    "rank_math_focus_keyword": JSON.stringify(seoMeta.focusKeyword),
    "rank_math_secondary_keyword": JSON.stringify(seoMeta.secondaryKeywords),
    "rank_math_primary_category": childCategoryId
  }
}
응답: { "id": 5678, "link": "https://agibop.com/..." }
⚠️
왜 모든 텍스트 필드에 JSON.stringify를 쓰는가

본문·제목 등에 줄바꿈이 포함되면, {{ }}로 그냥 끼워넣을 경우 JSON 문법 자체가 깨집니다(Bad control character 에러). JSON.stringify()로 감싸면 줄바꿈이 안전하게 이스케이프되어 항상 유효한 JSON으로 만들어집니다.

💡
rank_math_primary_category란

워드프레스 글이 여러 카테고리에 속할 때, Rank Math가 어떤 카테고리를 “주” 카테고리(URL 구조·breadcrumb 기준)로 쓸지 지정하는 메타 필드입니다. 자식 카테고리(childCategoryId)를 Primary로 지정해, 가장 구체적인 분류가 우선되도록 설정했습니다.

⑧ Strapi에 역기록 (17번 노드)

HTTP Request
PUT http://strapi:1337/api/blog-posts/{{ strapiId }}
Body: { "data": { "wordpressPostId": {{ wpPostId }} } }
STEP19

SNS 게시 & 댓글 자동화

미리 알아두기 — Instagram은 며칠 만에 안 됩니다

인스타그램 게시·댓글에는 instagram_business_content_publish 권한이 필요하고, Meta 앱 심사(2~4주, 화면녹화 제출 포함)를 통과해야 발급됩니다. 심사 끝나기 전까지는 인스타그램만 socialMeta를 참고해 수동으로 게시하는 하이브리드 방식을 권장합니다.

사전 준비 — 페이지 액세스 토큰

1

developers.facebook.com에서 앱 생성 (Business 유형)

2

Graph API Explorer에서 pages_manage_posts, pages_read_engagement 권한으로 토큰 발급 → 장기 토큰(60일)으로 교환

3

n8n Credential로 저장, 60일마다 갱신 워크플로우 별도 구성

HTTP Request 2개
# 1) 페이지 게시
POST https://graph.facebook.com/v22.0/{PAGE_ID}/feed
Body: { "message": "={{ $json.socialMeta.snsSummary }}", "access_token": "YOUR_PAGE_TOKEN" }
응답: { "id": "PAGEID_POSTID" }

# 2) 댓글 작성 (워드프레스 링크 포함)
POST https://graph.facebook.com/v22.0/{{ $json.postId }}/comments
Body: { "message": "={{ $json.socialMeta.snsComment }}", "access_token": "YOUR_PAGE_TOKEN" }

티스토리는 공식 게시 API가 제한적이라, tistorySummary를 참고해 수동 게시 후 Strapi의 tistoryUrl·tistoryPublished에 결과를 기록하는 방식을 추천합니다.

STEP20

전체 파이프라인 통합 그림

Strapi Publish 한 번 → 끝까지
Strapi에서 글 작성 + 6개 메타 섹션 입력 → Publish 클릭
        ↓ (webhook, X-Webhook-Secret 검증, 즉시 200 응답)
n8n 워크플로우 시작
        ↓
① 프롬프트 생성 → ComfyUI 이미지 생성 → Strapi coverImage 연결
        ↓
② 슬러그로 WP 카테고리 ID 조회 + 같은 이미지를 WP 미디어로 업로드
        ↓
③ WordPress 글 발행 (Rank Math 메타, featured_media 포함)
        ↓
④ wordpressPostId를 Strapi에 역기록
        ↓
⑤ 페이스북 페이지에 snsSummary로 게시 → ⑥ snsComment로 댓글 작성
        ↓
(인스타그램 — 앱 심사 완료 후 동일 구조로 추가)
(티스토리 — tistorySummary 참고 수동 게시 → tistoryUrl 기록)
STEP21

처음부터 끝까지 — 실제 발행 테스트 (단계별)

여기까지 전부 설정하셨다면, 이제 진짜 글 하나를 끝까지 발행해보면서 각 구간이 정확히 어디서 성공/실패하는지 직접 확인합니다.

사전 점검 — 시작 전 5가지

  • STEP 15의 워크플로우 Active 토글이 켜져 있는가
  • STEP 15 placeholder 9곳 전부 실제 값으로 교체했는가
  • STEP 14 Rank Math functions.php가 적용되어 사이트가 정상 로딩되는가
  • STEP 14 WordPress 카테고리(부모/자식)가 슬러그까지 정확히 생성되어 있는가
  • ComfyUI(8188)가 평소처럼 정상 응답하는가 — curl http://192.168.0.100:8188/system_stats

① 테스트 글 작성 (의도적으로 알아보기 쉬운 제목으로)

1

Strapi(1337) 또는 Obsidian(STEP 12)으로 글 작성

title 예: “파이프라인 테스트 글 — 삭제 예정”

2

category/subCategory를 STEP 07 슬러그 중 하나로 정확히 입력

예: home-lab / devops

3

6개 메타 섹션(imageMeta·seoMeta·socialMeta) 전부 채우기

비어있는 채로 두면 그 항목만 빈 값으로 발행되어 어디가 비었는지 헷갈림

4

Publish 클릭

② n8n Executions에서 실시간 추적

1

Publish 누른 직후 n8n 좌측 메뉴 Executions 클릭

몇 초 안에 새 실행 항목이 나타나야 함 — 안 나타나면 STEP 15 Webhook URL/Secret부터 재확인

2

실행 항목 클릭 → 19개 노드가 순서대로 초록색으로 바뀌는지 관찰

이미지 생성 구간(5~9번)에서 가장 오래 걸림 — 정상입니다, 보통 10~30초

③ 구간별 확인 포인트 — 어디서 멈췄는지 바로 판단하는 법

몇 번 노드에서 멈췄나이 시점까지는 성공한 것다음에 확인할 곳
3번(시크릿 검증)에서 멈춤Webhook 수신 자체는 성공STEP 15의 X-Webhook-Secret 값 재대조
5~9번(ComfyUI)에서 멈춤n8n→ComfyUI 연결까지 정상ComfyUI 워크플로우 JSON 안의 체크포인트 모델명이 실제 설치된 것과 일치하는지
10~11번(Strapi 업로드)에서 멈춤이미지 생성까지 정상 완료Strapi API 토큰, STEP 15 ③ 권장 방법 재확인
12번(WP 카테고리 조회)에서 빈 배열 반환WP 인증까지는 정상STEP 14에서 만든 카테고리 슬러그와 글의 subCategory 값이 정확히 일치하는지
14~16번(WP 발행)에서 멈춤이미지·카테고리까지 전부 정상Application Password 공백 포함 정확성, 도메인 주소 오타
18~19번(페이스북)에서 멈춤WordPress 발행까지 전부 성공페이지 액세스 토큰 만료(60일) 여부

④ 최종 결과물 — 5곳 전부 확인

  • agibop.com에 글이 실제로 publish 상태로 보이는가
  • 대표 이미지가 ComfyUI로 생성된 그림으로 채워져 있고, 이미지 클릭 시 alt 텍스트가 STEP07 imageMeta 값과 일치하는가 (브라우저 개발자도구 → Elements에서 alt= 확인)
  • Rank Math 플러그인 패널에 포커스 키워드·설명이 채워져 있는가 (워드프레스 글 편집 화면에서 직접 확인)
  • 페이스북 페이지에 글이 올라오고, 그 글에 댓글(워드프레스 링크)이 달려있는가
  • Strapi(1337)로 돌아가서 해당 글의 wordpressPostId가 빈 값이 아니라 실제 숫자로 채워졌는가
🎉
5곳 전부 확인되면 — 완전히 끝났습니다

이제 테스트 글은 WordPress·Strapi 양쪽에서 삭제하시고, 진짜 글부터는 이 STEP 21 과정 없이 그냥 Publish만 누르면 됩니다. 문제가 생기면 그때 다시 이 STEP의 표로 어느 구간인지 빠르게 짚어보시면 됩니다.

STEP22

트러블슈팅 & 최종 운영 체크리스트

증상원인해결
Cannot find module 'pg'quickstart로 생성되어 SQLite 드라이버만 포함STEP 03 — pg 직접 설치 + Dockerfile 보강
esbuild 바이너리 실행 오류호스트 node_modules가 Alpine 컨테이너로 유입STEP 03 — 컨테이너 안에서 스캐폴딩 + Debian 이미지
Content-Type Builder 메뉴 없음NODE_ENV=production 의도적 제한STEP 06 — tmux + strapi-dev-mode.sh로 1338 사용
본문 HTML이 깨져서 발행됨content가 Rich text로 설정됨STEP 07 — Text(Long text)로 변경
CORS 에러로 n8n 호출 실패middlewares.ts 수정 후 재배포 누락STEP 10 — build && up -d 까지 반드시 실행
Rank Math 패널이 항상 비어있음functions.php 등록 누락STEP 14 — register_post_meta 코드 추가
글이 미분류로 발행됨WP 카테고리 슬러그 불일치STEP 14 — 슬러그를 표와 정확히 동일하게
strapi-dev-temp가 갑자기 사라짐SSH 연결 끊김 → –rm으로 자동 삭제STEP 06 — 항상 tmux 안에서 실행
워크플로우 Execution에서 특정 노드가 빨간색(에러)placeholder(YOUR_…)를 안 바꾼 채로 Active함STEP 16의 체크리스트 표로 9곳 전부 교체했는지 재확인
발행된 글에 태그가 하나도 안 들어감22번 노드의 “Never Error” 옵션 누락 — 이미 존재하는 태그를 재사용할 때마다 400 에러 본문이 제대로 전달되지 않아 ID를 못 찾음STEP 18 ⑤ — 22번 노드 Options → Response → Never Error를 ON으로 설정
워드프레스 카테고리 목록에서 부모만 비어있음발행 요청에 자식 카테고리 ID만 보내고 부모는 안 보냄STEP 18 ③⑦ — 20번 노드(부모 조회) 추가 후 categories 배열에 부모+자식 둘 다 포함
페이스북 댓글 작성 실패페이지 토큰 만료(60일)STEP 19 — 장기 토큰 재발급, 자동 갱신 구성

최종 체크리스트

  • 스키마 작업은 항상 tmux + 1338에서, production(1337)에서는 시도하지 않기
  • content 필드는 Long text — Rich text 아님
  • WP 카테고리 슬러그가 STEP 07 표와 정확히 일치
  • .env 5개 보안 키 모두 기본값에서 변경
  • Rank Math functions.php 등록 완료
  • Webhook Response Mode = Immediately respond
  • !strapi-pg-db 정기 백업 (DB 덤프 + uploads 폴더)
  • !페이스북 페이지 토큰 60일 갱신 알림 구성
  • in8n 워크플로우는 별도 JSON 파일로 제공 — STEP 16의 9곳 placeholder 체크리스트 표대로 교체 후 Active

📋 최종 요약: 설치 → tmux로 안전하게 스키마 설계(Production 모드 함정 + HTML 보존을 위한 content 타입까지) → WordPress 카테고리/Rank Math 사전 준비 → Webhook → ComfyUI 이미지 생성 → 슬러그 기반 카테고리 자동 매핑 → WordPress 발행 → 페이스북 게시·댓글까지, 처음부터 끝까지 빈틈없이 이어지는 파이프라인을 완성했습니다.

Leave A Reply

Please enter your comment!
Please enter your name here

Related Stories