9.5 KiB
매일 일출 촬영 및 Threads 게시
동작
Linux systemd 타이머가 매일 한국 시간 03:00에 작업을 시작합니다. 기존 calculateSun / SunCalc 2.x로 당일 관악산 일출을 계산하고, 5분 전에 Chromium을 실행합니다. 배포 페이지의 실제 관측 위치·고도로 계산한 일출 시각까지 기다린 뒤, UI 없는 1080×1728 세로 JPEG 두 장을 같은 시각의 장면에서 촬영하고 한 개의 Threads 캐러셀 게시물로 올립니다. 사진은 실제 풍경이 아니라 시뮬레이션임을 게시 문구에 표시합니다. 날씨는 페이지의 기존 기상자료를 사용합니다.
사진 순서는 ① 일출 방향을 바라보는 캐릭터 뒷모습과 풍경 ② 캐릭터 얼굴과 하늘입니다. 캐릭터는 일출을 향한 자세를 유지하고 두 번째 카메라만 방위각 +200°, 위쪽 20°로 회전합니다. 기존 3인칭 카메라의 지형 충돌 보정을 사용합니다. 구도와 해상도는 src/constants/threads.ts의 angles, viewport에서 조정합니다. 두 사진 모두 준비된 뒤 캐러셀을 만들며, 한 장이라도 실패하면 일부만 게시하지 않습니다.
이전 단일 사진 버전의 .capture.json 기록은 재사용하지 않습니다. 해당 기록이 남아 있다면 보관 위치로 옮긴 뒤 다음 일출부터 실행하세요. 이미 게시했거나 게시 결과가 불명확한 날짜의 .json 게시 기록은 유지해야 중복을 방지할 수 있습니다.
SunCalc의 일출은 관측 고도를 반영한 천문학적 일출입니다. 주변 산 능선 뒤에서 태양이 실제로 드러나는 시각과 다를 수 있습니다. 서버 시계 동기화(NTP)를 켜 두세요. 캡처 후 업로드·Meta 처리 때문에 게시 시각은 일출보다 늦습니다.
변경된 프런트엔드를 먼저 배포해야 합니다. 작업은 ?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 계정에 게시합니다. 게시 문구에는 테스트 게시임과 실제 촬영 시각을 표시합니다. 실행할 때마다 새 테스트 게시물이 생성되며, 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. 자동 실행 활성화
deploy/sunrise-threads.service의 사용자, 작업 디렉터리, Node 경로를 수정한 뒤:
sudo cp deploy/sunrise-threads.service deploy/sunrise-threads.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now sunrise-threads.timer
systemctl list-timers sunrise-threads.timer
journalctl -u sunrise-threads.service -n 100
타이머는 03:00에 준비 작업을 시작하고 프로세스가 계산된 일출까지 기다립니다. 서버의 기본 시간대와 관계없이 Asia/Seoul 기준입니다. 일출을 놓친 뒤 서버가 켜지면 그날 촬영은 건너뜁니다. 촬영 허용 지연은 최대 30초이며, 로딩 지연이나 절전 등으로 이를 초과하면 게시하지 않습니다. 장애 시 journal에 오류가 남습니다. 별도 알림 채널은 연결하지 않았습니다.
게시 기록과 재시도
data/threads/YYYY-MM-DD.json에 게시 기록을 저장합니다. Meta에 실제 게시 요청을 보내기 직전 파일을 배타적으로 생성해 동시 실행도 중복 게시하지 않게 합니다. 성공 시 published와 게시 ID가 저장됩니다.
publishing 상태가 남았다면 요청이 성공했지만 응답이 유실됐을 수 있습니다. 이때 자동으로 재게시하지 않습니다. 계정과 기록의 컨테이너 ID를 확인한 뒤 수동으로 처리하세요. 기존 기록을 무조건 지우면 중복 게시될 수 있습니다.
촬영 성공 시 별도 YYYY-MM-DD.capture.json을 저장합니다. 토큰 갱신·공개 URL 확인·컨테이너 준비가 실패하면 systemd가 60초 뒤 재시도합니다(1시간 내 최대 5회 시작). 같은 날 재실행은 저장된 일출 사진을 재사용하므로 게시 준비 중 오류가 나도 촬영을 잃지 않습니다. 촬영 자체를 놓쳤다면 그날은 건너뜁니다. 다음 날짜에 전날 사진을 자동 게시하지 않습니다.
정지: sudo systemctl disable --now sunrise-threads.timer (이미 실행 중인 작업도 중지하려면 sudo systemctl stop sunrise-threads.service).