7.7 KiB
매일 일출 촬영 및 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가 없는 계정에서는 생략합니다. 다른 웹 서버 프로세스는 삭제하지 않습니다.
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를 유지하며 저장소 이전은 필요 없습니다.
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을 사용합니다.
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. 테스트와 이후 배포
# 실제 게시 없이 현재 시각 촬영
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, npm run build, 웹 서버 재시작, systemctl restart sunrise-threads.service 순서입니다. 설정/문구는 다시 복사하지 않습니다. root로 빌드한 뒤에는 아래 명령으로 캡처 폴더를 다시 만들고 권한을 맞추세요.
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 상태는 응답 유실 가능성이 있어 자동 재게시하지 않습니다. 실제 계정과 로그를 확인한 뒤 복구하세요. 과거 댓글 문구가 없는 기록에는 소급 댓글을 달지 않습니다.