Files
sunrise/docs/threads-automation.md
T

13 KiB
Raw Blame History

매일 일출 촬영 및 Threads 게시

동작

PM2가 상시 실행하는 워커가 매일 한국 시간 03:00에 작업을 시작합니다. 기존 calculateSun / SunCalc 2.x로 당일 관악산 일출을 계산하고, 일출 5분 전에 Chromium을 실행합니다. 배포 페이지의 실제 관측 위치·고도로 계산한 일출 시각에서 5분 더 기다린 뒤, UI 없는 1080×1728 세로 JPEG 두 장을 같은 시각의 장면에서 촬영하고 한 개의 Threads 캐러셀 게시물로 올립니다. 게시 문구는 별도 JSON 파일에서 수정하며, 촬영에 사용된 날씨에 따라 선택합니다. 날씨는 페이지의 기존 기상자료를 사용합니다.

사진 순서는 ① 일출 방향을 바라보는 캐릭터 뒷모습과 풍경 ② 캐릭터 얼굴과 하늘입니다. 캐릭터는 일출을 향한 자세를 유지하고 두 번째 카메라만 방위각 +200°, 위쪽 20°로 회전합니다. 기존 3인칭 카메라의 지형 충돌 보정을 사용합니다. 구도와 해상도는 src/constants/threads.ts의 angles, viewport에서 조정합니다. 두 사진 모두 준비된 뒤 캐러셀을 만들며, 한 장이라도 실패하면 일부만 게시하지 않습니다.

이전 단일 사진 버전의 .capture.json 기록은 재사용하지 않습니다. 해당 기록이 남아 있다면 보관 위치로 옮긴 뒤 다음 일출부터 실행하세요. 이미 게시했거나 게시 결과가 불명확한 날짜의 .json 게시 기록은 유지해야 중복을 방지할 수 있습니다.

SunCalc의 일출은 관측 고도를 반영한 천문학적 일출입니다. 주변 산 능선 뒤에서 태양이 실제로 드러나는 시각과 다를 수 있습니다. 서버 시계 동기화(NTP)를 켜 두세요. 문구에는 계산된 일출 시각을 표시하고 촬영은 그로부터 5분 뒤에 합니다(예: 일출 06:53 → 촬영 06:58). 캡처 후 업로드·Meta 처리 시간이 추가됩니다. 촬영 지연은 src/constants/threads.ts의 captureDelayMs에서 조정합니다. 즉시 테스트 게시와 preview는 이 지연 없이 바로 촬영합니다.

변경된 프런트엔드를 먼저 배포해야 합니다. 작업은 ?mode=solo&site=gwanaksan&capture=threads로 열며, 해당 모드에서만 브라우저 내 촬영 인터페이스가 노출됩니다. 인증 토큰은 브라우저로 전달하지 않습니다.

1. 서버 설치

아래는 프로젝트 /opt/sunrise, 서비스 사용자 sunrise, Node 실행 파일 /usr/bin/node 예시입니다. 실제 경로·기존 서비스 사용자에 맞게 수정하세요. Node.js는 프로젝트 요구사항인 22.18 이상을 사용합니다.

cd /opt/sunrise
npm ci
npm run prepare:buildings
npm run prepare:summit
npm run build
# OS 라이브러리 설치는 sudo 권한이 필요합니다.
sudo npx playwright install-deps chromium
# 브라우저는 작업을 실행할 사용자로 설치해야 합니다.
sudo -u sunrise npx playwright install chromium
cp -n .env.threads.sample.json .env.threads.json
chmod 600 .env.threads.json

.env.threads.json와 프로젝트의 이미지/상태 저장 경로는 sunrise 사용자가 읽고 쓸 수 있어야 합니다. root로 Chromium을 실행하지 않습니다. Linux에서 Chromium sandbox를 지원해야 하며, 지원하지 않는 환경은 OS 사용자 namespace/sandbox 설정을 점검하세요.

설정 파일은 프로젝트 루트의 .env.threads.json을 읽습니다. 샘플을 복사한 뒤 accessToken에 발급받은 토큰을 넣으세요. 상대 디렉터리 경로는 실행 작업 디렉터리를 기준으로 하므로 프로젝트 루트에서 실행합니다. 실제 설정 파일은 Git에서 제외되고, 토큰이 비어 있는 샘플만 버전 관리됩니다. 기존 .env.threads 환경변수 파일은 더 이상 읽지 않습니다.

2. Meta 계정 연결

Meta 개발자 콘솔에서 Threads 용도의 앱을 만들고, 자신의 계정에 threads_basic, threads_content_publish 권한을 부여합니다. 개발 모드에서는 앱의 테스터/역할 계정 설정 및 초대 수락을 확인하세요. 발급한 토큰이 장기 토큰인지 확인하고(단기 토큰이면 장기 토큰으로 교환), .env.threads.json의 accessToken에 넣습니다. 초기 토큰은 새로 발급한 것을 사용하세요.

작업은 토큰을 비공개 data/threads/token.json에 저장하고 7일 간격으로 갱신합니다. 이후에는 저장된 토큰이 JSON 설정의 accessToken보다 우선합니다. 재인증 시 서비스를 중지하고 token.json을 별도 보관/삭제한 다음 JSON 설정의 accessToken에 새 long-lived 토큰을 설정하세요. 토큰 철회·만료나 권한 변경은 재인증이 필요하며 자동으로 해결할 수 없습니다. 토큰 파일과 상태 디렉터리는 정적 웹 루트에 두지 마세요.

3. 촬영 검증 (게시하지 않음)

npm run threads:preview

현재 시각의 장면을 즉시 촬영하고 파일 경로를 출력합니다. 토큰 없이 실행할 수 있으며 Threads API 호출이나 게시 기록 생성은 없습니다. 다음 일출에 촬영만 검증하려면:

npm run threads:run -- --dry-run

--now는 --dry-run과 함께만 허용됩니다. 위 촬영 검증 명령은 실제 계정에 게시하지 않습니다.

지금 실제 테스트 게시하기

npm run threads:post-now

이 명령은 일출까지 기다리지 않고 현재 시각의 두 앵글을 실제 Threads 계정에 게시합니다. 게시 문구에는 설정된 testPrefix(기본 [테스트])를 붙입니다. 촬영 시각은 템플릿에 {time}을 넣으면 표시됩니다. 실행할 때마다 새 테스트 게시물이 생성되며, test-날짜-UUID.json 기록을 사용하므로 당일 일출 예약 게시를 막지 않습니다. 예약 서비스와 동일한 일반 사용자로 실행하세요. root에서는 Chromium sandbox가 동작하지 않습니다.

게시 ID를 받은 즉시 두 원본 JPEG를 삭제합니다. Threads published: ... 다음 Deleted both source images after publication 로그를 확인하세요. 삭제 실패 시 게시 기록은 유지하고 다음 게시 작업 시작 때 삭제를 재시도합니다. 게시 실패 또는 결과 불명확 상태에서는 이미지를 보관하며, 촬영 전용 preview 파일도 확인용으로 유지합니다. 서버 원본 삭제는 Threads에 게시된 사진을 삭제하는 작업이 아닙니다. 별도 CDN을 사용한다면 캐시 사본은 CDN의 만료 정책을 따릅니다.

4. 이미지 공개 URL

기본 저장 위치는 dist/threads-captures/, 공개 URL은 https://sunrise.horoli.kr/threads-captures/입니다. 기존 Node 정적 파일 서버가 이미지를 제공합니다. 저장된 JPEG의 공개 URL을 외부에서 열어 확인하세요. 게시 직전에 작업도 실제 다운로드한 바이트가 저장된 이미지와 같은지 확인합니다. Meta 서버가 로그인 없이 이미지를 읽을 수 있어야 합니다.

npm run build는 dist를 다시 생성합니다. 운영에서는 배포와 독립적인 이미지 디렉터리를 권장합니다. 예를 들어 "imageDir": "/var/lib/sunrise/captures"로 지정하고 Nginx에서 /threads-captures/를 해당 디렉터리에 연결하세요. Nginx 사용자에게 디렉터리 탐색·파일 읽기 권한을 부여하세요. imageBaseUrl도 실제 URL과 일치해야 합니다. 이미지에는 토큰이 포함되지 않습니다. 예약 게시와 테스트 게시 모두 성공 후 원본 이미지를 즉시 삭제합니다.

5. 문구 수정과 날씨 조건

프로젝트 루트에서 샘플을 복사합니다. 실제 문구 파일은 Git에서 제외되므로 배포로 덮어쓰지 않습니다. 파일이 없으면 샘플을 사용합니다. 기존 .threads-captions.json은 자동으로 덮어쓰지 않습니다. 새 기본 문구로 바꾸려면 기존 파일을 백업한 뒤 .threads-captions.sample.json 내용으로 교체하세요.

cp -n .threads-captions.sample.json .threads-captions.json
nano .threads-captions.json
npm run threads:captions
  • messages: 날씨별 문구입니다. 배열의 각 항목은 한 줄이며, 여러 줄을 쓰면 모두 순서대로 표시됩니다(무작위 선택 아님).
  • template: 전체 게시글의 줄 구성입니다. 기본은 “오늘의 관악산 일출이에요”, 날짜·일출 시각, 날씨, 사이트 주소의 네 줄입니다.
  • testPrefix: 즉시 테스트 게시 앞에 붙일 표시입니다.
  • 치환값: {message}, {date}(한국 날짜 YYYY-MM-DD), {dateDots}(YYYY.MM.DD), {time}(촬영 HH:mm), {sunrise}(일출 HH:mm), {url}(전체 URL), {hostname}(도메인만).
  • thresholds: 낮은 시정 기준(m), 흐림·구름 조금 운량 기준(%)입니다.

날씨 우선순위는 눈 → 비 → 낮은 시정 → 흐림 → 구름 조금 → 맑음입니다. 기본 시정 기준은 1000m 미만, 흐림은 운량 80% 이상, 구름 조금은 30% 이상입니다. 강수 정보가 없거나 날씨 로딩/조회가 실패하면 unknown을 사용합니다. 낮은 시정은 안개로 단정하지 않도록 기본 문구에 표현했습니다. 웹페이지에 적용된 기상 데이터를 캡처 기록에 함께 저장하며, 재시도도 그 사진의 날씨를 사용합니다.

문구는 게시 직전에 다시 읽으므로 수정 후 빌드나 PM2 재시작이 필요 없습니다. npm run threads:captions는 모든 날씨별 문구를 출력만 하며 게시하지 않습니다. 실제 촬영 시 선택된 문구도 threads:preview 로그로 확인할 수 있습니다. 완성된 문구가 500자를 넘거나 JSON이 잘못됐으면 게시를 중단합니다.

6. PM2로 자동 실행

기존 systemd 작업을 설정했다면 먼저 중지합니다. 두 예약 작업을 동시에 실행하지 마세요.

sudo systemctl disable --now sunrise-threads.timer
sudo systemctl stop sunrise-threads.service

이전 systemd 작업이 없다면 위 명령은 생략합니다. PM2는 Chromium을 설치한 동일한 일반 사용자로 실행하세요. PM2가 없다면 관리자 권한으로 npm install -g pm2를 한 번 실행합니다. 이후 일반 사용자 터미널에서:

cd /opt/sunrise
pm2 start deploy/ecosystem.config.cjs
pm2 save
pm2 status
pm2 logs sunrise-threads --lines 100

설정은 프로젝트 위치를 자동으로 계산하고 워커를 단일 fork 프로세스로 실행합니다. 기존 웹 서버 PM2 프로세스는 건드리지 않습니다. 날짜 계산은 서버 시간대와 무관하게 Asia/Seoul 기준이며, 03:00 이후 새로 시작하면 오늘 작업을 바로 확인합니다. 이미 게시했다면 중복 게시하지 않고, 일출+5분 촬영 시간을 놓쳤고 저장된 사진도 없다면 다음 날짜를 기다립니다. 실행 중에는 일출 5분 전에 Chromium을 열고 일출 5분 뒤 촬영합니다.

pm2 restart sunrise-threads    # 코드/실행 설정 변경 후
pm2 stop sunrise-threads       # 중지
pm2 delete sunrise-threads     # PM2에서 제거
pm2 save                      # 변경된 목록 저장

재부팅 후 복구는 일반 사용자로 pm2 startup을 실행하고, 출력되는 관리자 명령을 그대로 실행한 뒤 pm2 save합니다. Linux 배포판에 따라 PM2 자체 부팅 등록에는 systemd가 사용될 수 있지만, 일출 예약과 프로세스 관리는 PM2 워커가 담당합니다.

게시 기록과 재시도

data/threads/YYYY-MM-DD.json에 게시 기록을 저장합니다. Meta에 실제 게시 요청을 보내기 직전 파일을 배타적으로 생성해 동시 실행도 중복 게시하지 않게 합니다. 성공 시 published와 게시 ID가 저장됩니다.

publishing 상태가 남았다면 요청이 성공했지만 응답이 유실됐을 수 있습니다. 이때 자동으로 재게시하지 않습니다. 계정과 기록의 컨테이너 ID를 확인한 뒤 수동으로 처리하세요. 기존 기록을 무조건 지우면 중복 게시될 수 있습니다.

촬영 성공 시 별도 YYYY-MM-DD.capture.json을 저장합니다. 토큰 갱신·공개 URL 확인·컨테이너 준비가 실패하면 워커가 60초 뒤 재시도합니다(하루 작업 최대 5회 시도). 같은 날 재실행은 저장된 일출 사진을 재사용하므로 게시 준비 중 오류가 나도 촬영을 잃지 않습니다. 일출+5분의 촬영 시간을 놓쳤다면 그날은 건너뜁니다. 다음 날짜에 전날 사진을 자동 게시하지 않습니다.

정지: pm2 stop sunrise-threads. 게시 결과가 불명확한 작업은 재실행 전 계정과 기록을 확인하세요.