Synology NAS에서 로또 분석 블로그 완전 자동화 구축기 – 2일간의 여정
Synology DS918+ NAS에서 돌아가는 WordPress 블로그에, 매주 토요일 밤 로또 당첨번호가 발표되면 자동으로 통계 분석 글이 발행되도록 만들었다. 계획은 3~4일이었는데 이틀 만에 끝났다. 이 글은 그 과정에서 만난 삽질과 해결책을 시간순으로 정리한 기록이다.
- 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)
- 홀짝 분포 / 저고 분포 / 구간별 분포
- 합계 통계 (평균/중앙값/표준편차/최솟값/최댓값)
- 연속수 패턴 / 보너스 번호 빈도
합계 평균 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 # 마이너스 부호 깨짐 방지
구간 차트를 로또 공식 색상(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만 하면 안 되고, schema의 context에 '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
