Files
sunrise/docs/threads-automation.md

119 lines
8.0 KiB
Markdown

# 매일 일출 촬영 및 Threads 게시
systemd가 `sunrise` 계정으로 예약 워커를 실행합니다. 워커는 매일 03:00 KST에 준비하고 SunCalc와 실제 페이지의 관측 위치로 일출을 계산합니다. 일출 5분 전에 브라우저를 열고 **일출 5분 뒤** 두 앵글을 촬영합니다. 사진 두 장을 한 캐러셀로 게시하고 원본을 즉시 삭제한 뒤 링크 댓글을 게시합니다. 본문에는 실제 일출 시각을 표시합니다.
## 설정과 저장 위치
- `.env.threads.json`: accessToken, siteUrl, stateDir, imageDir, imageBaseUrl 설정. 최초에만 샘플을 복사합니다.
- `.threads-captions.json`: 사용자 본문/댓글 문구. 최초에만 샘플을 복사합니다.
- `data/threads`: 갱신 토큰, 게시 기록, 댓글 기록, 캡처 메타데이터. 공개하면 안 됩니다.
- `dist/threads-captures`: 공개용 임시 JPEG. 게시 확인 후 삭제합니다.
- `public/og-sunrise.jpg`: 버튼 없는 메인 화면 일출 이미지. 빌드 때 dist로 복사되는 고정 링크 미리보기입니다.
빌드 시 임시 촬영 이미지는 삭제되어도 됩니다. 토큰·게시 기록은 data/threads, 설정·문구는 프로젝트 루트에 있으므로 빌드로 초기화되지 않습니다. 촬영·게시 도중에는 빌드하지 마세요.
## PM2에서 systemd로 전환
아래는 root에서 실행합니다. 경로 `/opt/sunrise`, 계정 `sunrise` 기준입니다.
### 1. 중복 실행 중지
root와 sunrise의 `pm2 list`를 확인하고 sunrise-threads가 등록된 계정에서만 delete/save를 실행하세요. pm2가 없는 계정에서는 생략합니다. 다른 웹 서버 프로세스는 삭제하지 않습니다.
```bash
pm2 list
pm2 delete sunrise-threads
pm2 save --force
sudo -iu sunrise pm2 list
sudo -iu sunrise pm2 delete sunrise-threads
sudo -iu sunrise pm2 save --force
systemctl disable --now sunrise-threads.timer
systemctl stop sunrise-threads.service
```
없는 프로세스/유닛 오류는 그 항목에 한해서 무시해도 됩니다. 새 서비스는 상시 워커라 별도 timer를 켜지 않습니다.
### 2. 코드 반영, 빌드, 권한 수정
root에서 실행합니다. 캡처 저장 위치는 기존 dist/threads-captures를 유지하며 저장소 이전은 필요 없습니다.
```bash
cd /opt/sunrise
git pull --ff-only
npm ci
# 최초 서비스 등록 전에는 직접 빌드합니다.
npm run build
install -d -o sunrise -g "$(id -gn sunrise)" -m 755 dist/threads-captures
install -d -o sunrise -g "$(id -gn sunrise)" -m 700 data/threads
chown -R sunrise:"$(id -gn sunrise)" data/threads dist/threads-captures
chmod -R u+rwX,go-rwx data/threads
chown sunrise:"$(id -gn sunrise)" .env.threads.json
chmod 600 .env.threads.json
if [ -f .threads-captions.json ]; then
chown sunrise:"$(id -gn sunrise)" .threads-captions.json
chmod 600 .threads-captions.json
fi
```
기존 Node 정적 서버의 /threads-captures/ 경로를 그대로 사용합니다. Nginx 경로 변경도 필요 없습니다. 앞선 저장소 이전 명령을 이미 서버에서 실행했다면 .env.threads.json의 imageDir를 dist/threads-captures로 되돌리세요. 토큰·문구·게시 기록은 지우지 않습니다.
### 3. sunrise 계정으로 systemd 실행
Node 22.18 이상이 필요합니다. 기존 계정의 Node 경로와 HOME을 사용합니다.
```bash
NODE_BIN=$(sudo -iu sunrise sh -lc 'command -v node')
SUNRISE_HOME=$(getent passwd sunrise | cut -d: -f6)
test -n "$NODE_BIN" && test -n "$SUNRISE_HOME" || exit 1
install -m 644 deploy/sunrise-threads.service /etc/systemd/system/sunrise-threads.service
mkdir -p /etc/systemd/system/sunrise-threads.service.d
printf '[Service]\nExecStart=\nExecStart=%s /opt/sunrise/server/threads/worker.ts\nEnvironment=HOME=%s\n' "$NODE_BIN" "$SUNRISE_HOME" > /etc/systemd/system/sunrise-threads.service.d/runtime.conf
systemctl daemon-reload
systemctl cat sunrise-threads.service
systemd-analyze verify /etc/systemd/system/sunrise-threads.service
systemctl enable --now sunrise-threads.service
systemctl status sunrise-threads.service --no-pager
journalctl -u sunrise-threads.service -n 50 --no-pager
```
기존 drop-in이 있으면 User/ExecStart/쓰기 제한 설정과 충돌하지 않는지 확인하세요. 기존 `PLAYWRIGHT_BROWSERS_PATH`가 있었다면 같은 Environment 설정을 유지하세요. 기존 sunrise 계정의 Chromium을 사용하며, 버전 업데이트로 브라우저가 없을 때만 같은 계정에서 `npx playwright install chromium`을 실행합니다. OS 라이브러리는 root에서 `npx playwright install-deps chromium`으로 설치할 수 있습니다.
### 4. 테스트와 이후 배포
```bash
# 실제 게시 없이 현재 시각 촬영
sudo -iu sunrise sh -lc 'cd /opt/sunrise && npm run threads:preview'
# 실제 테스트 게시 + 댓글
sudo -iu sunrise sh -lc 'cd /opt/sunrise && npm run threads:post-now'
```
별도 브라우저 경로를 사용하면 테스트 터미널에도 동일한 환경변수를 적용하세요. post-now는 실행할 때마다 새 테스트 게시물을 만듭니다. preview 이미지는 확인용으로 보존합니다.
이후 배포는 git pull, 필요 시 npm ci, ./build.sh, 웹 서버 재시작 순서입니다. ./build.sh가 빌드와 권한 설정에 성공한 뒤 sunrise-threads.service까지 재시작합니다. 설정/문구는 다시 복사하지 않습니다. root에서 ./build.sh를 실행하면 빌드 후 캡처 폴더 생성·권한 설정·서비스 재시작까지 처리합니다. 서비스가 아직 등록되지 않았다면 최초 등록 절차를 먼저 진행하세요. npm run build를 직접 실행했다면 아래 명령이 필요합니다.
```bash
install -d -o sunrise -g "$(id -gn sunrise)" -m 755 /opt/sunrise/dist/threads-captures
```
정지는 `systemctl stop sunrise-threads.service`입니다.
## 문구 편집
기존 `.threads-captions.json`을 직접 편집하세요. 파일이 없을 때만 `cp -n .threads-captions.sample.json .threads-captions.json`을 사용합니다.
- template: 본문 줄 배열. 기본은 소개, 날짜·일출, 날씨입니다.
- replyTemplate: 링크 댓글 줄 배열. 기존 파일에 이 항목이 없으면 댓글을 보내지 않습니다.
- messages: 날씨별 문구. 우선순위는 눈 → 비 → 낮은 시정 → 흐림 → 구름 조금 → 맑음입니다. 데이터가 없으면 unknown입니다.
- thresholds: 낮은 시정(m), 흐림과 구름 조금의 운량(%) 기준.
- testPrefix: 즉시 테스트 본문에 붙이는 표시.
- 치환값: {message}, {date}, {dateDots}, {time}, {sunrise}, {url}, {hostname}.
`npm run threads:captions`로 미리 확인합니다. 문구만 바꾸면 빌드/재시작이 필요 없습니다. 본문과 댓글 각각 500자 제한입니다. 댓글 link_attachment는 siteUrl의 origin을 사용하며 사이트 Open Graph 정보를 요청합니다. 최종 미리보기 표시와 캐시는 Threads가 결정합니다.
## 게시 기록과 재시도
토큰은 Meta 사용자 토큰 생성기의 장기 액세스 토큰을 accessToken에 넣습니다. 갱신 토큰은 `data/threads/token.json`에 저장되며 설정 토큰보다 우선합니다. 토큰 교체 시 워커를 멈추고 기존 token.json을 안전하게 백업한 뒤 제거하고 설정 토큰을 바꾸세요. 토큰 철회/만료는 재인증이 필요합니다.
작업 실패 시 최대 5회, 60초 간격으로 재시도합니다. 저장된 사진은 같은 날짜 재실행에 재사용하며 일출+5분 시각을 놓치고 저장 사진도 없으면 건너뜁니다. 본문 게시 성공 직후 원본을 삭제하고 댓글을 게시합니다. 삭제 실패는 다음 실행에 재시도합니다.
본문 기록은 날짜.json, 캡처 기록은 날짜.capture.json, 댓글은 날짜.json.reply.json입니다. 즉시 테스트는 test-날짜-UUID 형식입니다. published 기록과 게시 ID는 중복 방지용이므로 지우지 마세요. publishing 상태는 응답 유실 가능성이 있어 자동 재게시하지 않습니다. 실제 계정과 로그를 확인한 뒤 복구하세요. 과거 댓글 문구가 없는 기록에는 소급 댓글을 달지 않습니다.