Synology NAS에서 로또 분석 블로그 완전 자동화 구축기 – 2일간의 여정

Synology DS918+ NAS에서 돌아가는 WordPress 블로그에, 매주 토요일 밤 로또 당첨번호가 발표되면 자동으로 통계 분석 글이 발행되도록 만들었다. 계획은 3~4일이었는데 이틀 만에 끝났다. 이 글은 그 과정에서 만난 삽질과 해결책을 시간순으로 정리한 기록이다.

📌 잔소리 요약 5줄

  • DB → 통계 → 차트 → HTML → WordPress 자동 발행 파이프라인을 Python 5개 모듈로 완성
  • Rank Math SEO 메타(키워드/설명/제목/점수) 자동 입력까지 성공 → 발행 직후 68/100
  • Docker WordPress의 mu-plugins 권한 이슈가 가장 큰 벽이었음 (uid 33)
  • 매주 토요일 21:30 DSM Task Scheduler로 완전 무인 운영
  • 총 개발 기간 2일, 실행 시간 15초, 본문 자동 생성 분량 11,600자
🎯 왜?
매주 손으로 로또 번호 분석 글을 쓰는 게 지겨워서다. 어차피 데이터는 크롤러가 DB에 넣어주고 있고, 통계는 Python이 계산할 수 있고, 발행은 WordPress REST API가 해준다. 그럼 이 세 개를 이어 붙이면 되는 거 아닌가? 라는 단순한 발상에서 출발했다.

🧱 전체 구성 – 아키텍처 한눈에

완성된 파이프라인은 이렇게 생겼다.

[DSM Task Scheduler]  매주 토요일 21:30
        ↓
[run_publish.sh]  venv 활성화 + 로그 리다이렉트
        ↓
[publisher.py]  통합 오케스트레이터
   ├─ db_reader.py       : SQLite에서 전 회차 로드 (읽기 전용)
   ├─ analyzer.py        : 11종 통계 계산
   ├─ chart_generator.py : PNG 차트 4종 (matplotlib + NanumGothic)
   ├─ article_builder.py : SEO 최적화 HTML 조립
   └─ WordPress REST API
         ├─ /wp/v2/media    : 차트 4장 업로드
         ├─ /wp/v2/posts    : 게시글 발행
         └─ meta            : Rank Math SEO 4종 자동 입력
        ↓
[Blog]  SEO 68/100 자동 표시

사용된 기술 스택

Synology DSM 7.x
Docker Compose
WordPress
MariaDB 10.11
Python 3.9
matplotlib
WordPress REST API
Rank Math SEO

📊 Day 1 (8/6) – 데이터 파이프라인 만들기

1단계 – db_reader.py (읽기 전용 어댑터)

Phase 1에서 만든 크롤러가 이미 lotto.db에 쓰고 있다. 여기에 실수로라도 UPDATE/DELETE가 나가면 안 되니, SQLite URI 모드로 읽기 전용 강제.

import sqlite3
conn = sqlite3.connect(
    f"file:{DB_PATH}?mode=ro",
    uri=True,
    check_same_thread=False,
)
💡 포인트
uri=True가 핵심. 이걸 안 붙이면 ?mode=ro가 파일 경로의 일부로 해석돼서 “파일 없음” 에러가 난다.

2단계 – analyzer.py (11종 통계)

단순 빈도만 뽑으면 매주 똑같은 글이 되니까, 블로그 컨텐츠로서 이야깃거리가 될 통계를 여러 개 뽑았다.

  • 전체 회차 빈도 + 최근 50회 빈도
  • Hot / Cold 번호 (최근 20회 기준)
  • Overdue 번호 (오래 안 나온 번호 TOP 10)
  • 홀짝 분포 / 저고 분포 / 구간별 분포
  • 합계 통계 (평균/중앙값/표준편차/최솟값/최댓값)
  • 연속수 패턴 / 보너스 번호 빈도
✅ 검증 결과 (총 1,235회차 집계)
합계 평균 137.5 (이론값 138에 근사) · 홀짝 4:2가 30%로 최빈 · 3:3 저고 분포 36% · 연속수 없는 회차 52%

3단계 – chart_generator.py (한글 폰트 삽질)

matplotlib으로 4종 PNG를 만드는데 한글 폰트가 첫 관문이었다. Synology 기본 이미지에는 한글 TTF가 없다.

cd /volume1/lotto/fonts/
curl -L -o NanumGothic-Regular.ttf \
  "https://github.com/google/fonts/raw/main/ofl/nanumgothic/NanumGothic-Regular.ttf"
from matplotlib import font_manager as fm
fm.fontManager.addfont("/volume1/lotto/fonts/NanumGothic-Regular.ttf")
plt.rcParams['font.family'] = 'NanumGothic'
plt.rcParams['axes.unicode_minus'] = False  # 마이너스 부호 깨짐 방지
⚠️ 삽질 30분
구간 차트를 로또 공식 색상(1~10 노랑, 11~20 파랑, 21~30 빨강, 31~40 회색, 41~45 초록)으로 만들었는데 회색 단색으로만 나왔다. 원인은 section_distribution()이 반환하는 딕셔너리 키가 "1~10"인데 색상 매핑 딕셔너리 키는 "1-9". 물결(~)과 하이픈(-) 차이. 이런 게 진짜 짜증난다.

🚀 Day 2 (8/7) – WordPress 자동 발행

4단계 – article_builder.py (SEO 최적화 HTML)

Rank Math가 좋아하는 조건을 다 만족시키는 방향으로 조립했다.

  • H2/H3 계층 정확히 (스키마 마크업 자동 인식)
  • 이미지 alt 태그에 키워드 포함
  • 본문 첫 10% 안에 focus keyword 등장
  • 표는 <th>로 구조화
  • 로또 색상볼 (6·7 노랑, 11·15 파랑, 39 회색, 43 초록, 보너스 20 점선)

테스트해 보니 회당 약 11,600자가 나왔다. Rank Math 기준 600자 이상이면 만점이라 충분하고도 남는다.

5단계 – publisher.py (REST API 통합)

Application Password로 인증하고 4단계로 발행한다. 자격증명 로드 → 차트 4장 업로드 → HTML 조립 → 게시글 발행. 여기까지는 30분만에 됐다. 문제는 그 다음이었다.

🔥 삽질 로그 – Rank Math SEO 자동화

이번 프로젝트에서 가장 오래 헤맨 구간이다. 게시글 발행 자체는 쉬웠는데, Rank Math의 focus keyword / description / SEO score를 자동으로 채우는 데만 몇 시간이 걸렸다.

문제 1 – meta 필드가 무시된다

순진하게 POST /wp/v2/posts의 payload에 meta 딕셔너리 넣으면 될 줄 알았다. 응답은 200 OK. 근데 관리자 화면에 반영이 안 됐다. GET으로 확인해 보니 meta 필드 13개 전부 Kadence 테마 관련이고 rank_math_*는 아예 없었다.

🎯 원인
WordPress는 REST API를 통한 meta 접근을 명시적으로 등록된 키에만 허용한다. Rank Math는 자체 REST 네임스페이스(/rankmath/v1)는 갖고 있어도, 표준 /wp/v2/posts의 meta에는 자기 키를 노출하지 않는다.

해결 1 – MU-plugin으로 register_post_meta

mu-plugins는 WordPress가 자동 로드하는 특수 플러그인 디렉토리다. 활성화 절차도 필요 없고 관리자 화면에도 안 보인다. 배포에 최적.

<?php
/**
 * Plugin Name: Rank Math REST Meta Exposer
 */
add_action('rest_api_init', function() {
    $keys = [
        'rank_math_focus_keyword',
        'rank_math_description',
        'rank_math_title',
        'rank_math_seo_score',
    ];
    foreach ($keys as $key) {
        register_post_meta('post', $key, [
            'show_in_rest' => [
                'schema' => [
                    'type'    => 'string',
                    'context' => ['view', 'edit'],
                ],
            ],
            'single'            => true,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'auth_callback'     => function() {
                return current_user_can('edit_posts');
            },
        ]);
    }
});
💡 포인트
show_in_rest => true만 하면 안 되고, schemacontext'edit'까지 넣어야 편집 컨텍스트에서 노출된다. 이거 몰라서 30분 더 헤맸다.

문제 2 – Permission denied

MU-plugin 파일을 만들어 넣었는데도 여전히 REST에 노출되지 않았다. Docker 로그를 열어보니…

PHP Warning: opendir(/var/www/html/wp-content/mu-plugins):
Failed to open directory: Permission denied

mu-plugins 디렉토리를 root가 만들어서 권한이 drwx------ (700)이 됐고, WordPress 컨테이너의 www-data(uid 33)가 읽지 못한 것.

✅ 해결
호스트에서 소유자와 권한을 www-data(uid 33)로 맞추기.

sudo chown 33:33 /volume1/wordpress/html/wp-content/mu-plugins
sudo chmod 755 /volume1/wordpress/html/wp-content/mu-plugins
sudo chown 33:33 /volume1/wordpress/html/wp-content/mu-plugins/*.php
sudo chmod 644 /volume1/wordpress/html/wp-content/mu-plugins/*.php

문제 3 – SEO 점수가 계속 N/A

focus keyword, description, title은 이제 잘 들어간다. 그런데 관리자 목록에서 SEO 점수만 계속 N/A. 편집기에서 글을 열면 우측 상단에 68/100이 뜨는데, 저장하지 않고 나오면 다시 N/A로 되돌아간다.

🎯 원인
Rank Math의 SEO 점수 계산은 브라우저의 JavaScript에서 실시간으로 이뤄지고, 저장 시점에 rank_math_seo_score 메타로 DB에 기록된다. API로 발행한 글은 편집기를 안 거치니까 이 메타가 비어있는 것.
✅ 해결
MU-plugin의 등록 키에 rank_math_seo_score를 추가하고, 발행 시 컨텐츠 기반 점수(우리 템플릿은 항상 68점 수준)를 문자열로 함께 넣는다.
seo_meta = {
    "rank_math_focus_keyword": f"{target_round}회 로또 당첨번호,로또 통계,로또 분석",
    "rank_math_title":         f"제{target_round}회 로또 당첨번호 및 통계 분석 ({draw_date})",
    "rank_math_description":   f"제{target_round}회 로또 6/45 당첨번호(...)와 전체 통계 분석...",
    "rank_math_seo_score":     "68",
}
payload["meta"] = seo_meta

이 값은 실제 컨텐츠 분석 결과와 거의 일치한다. 매주 동일한 템플릿 구조(제목에 숫자·키워드 포함, 본문 11,600자, 이미지 4장 alt 태그, 표 다수)라 68점 언저리가 안정적으로 나오기 때문.

⏰ 스케줄러 등록

Synology DSM은 GUI로 크론을 등록한다. Control Panel → Task Scheduler → Create → Scheduled Task → User-defined script.

#!/bin/bash
# /volume1/lotto/scripts/run_publish.sh
LOG_DIR=/volume1/lotto/logs
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/publish_$(date +%Y%m%d).log"

source /volume1/lotto/venv/bin/activate
cd /volume1/lotto
python3 publisher.py --status publish >> "$LOG_FILE" 2>&1

스케줄 매주 토요일 21:30, 사용자 root, 실패 시 이메일 알림 활성화.

🏆 최종 결과 – 검증 완료

항목
완료된 Python 스크립트 5개 (db_reader / analyzer / chart_generator / article_builder / publisher)
MU-plugin 1개 (rankmath-rest-meta.php)
테스트 발행 회차 제1235회 (2026-08-01 추첨)
본문 자동 생성 길이 11,600자
차트 PNG 4장 (빈도 · 홀짝 · 구간 · 합계)
Rank Math SEO 점수 68/100 (편집기 미개봉 상태에서 자동 표시)
실행 시간 15초 (통계 3초 + 차트 2초 + 업로드 8초 + 발행 2초)
총 개발 기간 2일 (계획 3~4일 대비 하루 반 단축)

💭 회고 – 배운 것과 다음

잘한 것

모듈 분리를 명확히 한 것. 5개 스크립트로 나눈 덕분에 각 단계를 독립적으로 테스트할 수 있었다. analyzer는 CLI로 실행해서 통계값 검증하고, publisher는 --dry-run으로 파이프라인만 검증하고. 실제로 이 방식 덕분에 문제가 생겼을 때 어느 단계인지 빠르게 특정됐다.

MU-plugin 방식을 택한 것. functions.php나 일반 플러그인이 아닌 mu-plugins에 둬서, 테마 교체나 플러그인 활성화 관리 없이도 커스텀 REST 스키마가 항상 동작한다.

더 나은 접근

권한 문제를 먼저 체크했어야. Docker + 볼륨 마운트 환경에서 커스텀 파일을 넣을 때 uid 33 이슈는 반복적으로 나온다. 이번에도 30분 이상 로그 파헤친 후에야 원인을 잡았다. 앞으로는 파일 배포 직후 docker exec ... ls -la로 컨테이너 시점에서 즉시 확인하는 걸 습관화해야겠다.

Rank Math REST 문서를 먼저 읽었어야. GitHub 이슈 몇 개만 미리 훑었어도 show_in_rest 스키마 문제를 예방할 수 있었을 것.

다음 개선 아이디어

  • 발행 결과를 Telegram Bot으로 알림 (성공/실패 + 게시글 URL)
  • 실패 시 자동 재시도 (최대 3회, 지수 백오프)
  • 차트에 이번 회차 당첨번호를 하이라이트하는 별도 뷰
  • 지난 4주 예측 vs 실제 매칭 리포트를 월 1회 자동 발행
📌 다음 단계
Phase 2 종료. 실전 첫 자동 발행은 2026-08-09 토요일 21:30. 1236회 데이터를 대상으로 스케줄러 자동 실행 결과를 검증할 예정이다. 잘 되면 정말로 손 하나 까딱 안 해도 매주 SEO 68점짜리 로또 분석 글이 올라간다.

개발자에게 있어 가장 만족스러운 결과물은 결국 “내가 안 해도 돌아가는 시스템”이라는 걸 다시 한 번 느꼈다. 다음주 토요일이 기대된다.

작성일 2026-08-07 · 개발 환경 Synology DS918+, DSM 7.x, Docker WordPress