# 매일 일출 촬영 및 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 이상을 사용합니다. ```bash 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`에 넣습니다. 초기 토큰은 새로 발급한 것을 사용하세요. - [Meta 공식 Threads API / 계정 연결](https://www.postman.com/meta/threads/collection/dht3nzz/threads-api) - [토큰 교환·갱신 및 이미지 게시 API](https://www.postman.com/meta/threads/documentation/dht3nzz/threads-api) 작업은 토큰을 비공개 `data/threads/token.json`에 저장하고 7일 간격으로 갱신합니다. 이후에는 저장된 토큰이 JSON 설정의 accessToken보다 우선합니다. 재인증 시 서비스를 중지하고 `token.json`을 별도 보관/삭제한 다음 JSON 설정의 accessToken에 새 long-lived 토큰을 설정하세요. 토큰 철회·만료나 권한 변경은 재인증이 필요하며 자동으로 해결할 수 없습니다. 토큰 파일과 상태 디렉터리는 정적 웹 루트에 두지 마세요. ## 3. 촬영 검증 (게시하지 않음) ```bash npm run threads:preview ``` 현재 시각의 장면을 즉시 촬영하고 파일 경로를 출력합니다. 토큰 없이 실행할 수 있으며 Threads API 호출이나 게시 기록 생성은 없습니다. 다음 일출에 촬영만 검증하려면: ```bash npm run threads:run -- --dry-run ``` `--now`는 `--dry-run`과 함께만 허용됩니다. 테스트 때문에 실제 계정에 게시하지 않습니다. ## 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 경로를 수정한 뒤: ```bash 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`).