Files
behind_glass/docs/arrival-backend-operations.md
T
horoli e358e24436 Replace project with current server release and Windows setup
Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
2026-10-09 16:43:35 +09:00

9.7 KiB

방문·도착 집계 운영 및 이전 안내

이 안내는 Fastify + SQLite 서버 운영 절차다. 이번 ZIP에는 현재 소스와 집계 활성 서버용 빌드가 포함되어 있다. 실행 방법과 검증 범위는 루트의 README-서버실행.md를 확인한다. 사용자의 서버에 실제 배포한 기록은 아니다.

데이터와 집계 기준

  • 방문은 브라우저 UUID와 서버가 계산한 한국 날짜(Asia/Seoul)당 한 행이다. MAU는 해당 한국 달력 월의 서로 다른 UUID 수이며 일별 합계가 아니다. 페이지 진입 때 기록하고 열린 탭을 자정마다 새 방문으로 만들지 않는다.
  • 도착은 현재 개인 사진의 유효한 AI 성공 때만 기록한다. 샘플·영상·직접 올린 깊이·실패·평면 보기는 제외한다. 샘플 방문도 MAU에는 남는다.
  • 이름은 브라우저에서 NFC/공백 정리/20 Unicode code point 제한 후 사용하고 빈 이름만 멍멍이로 취급한다. UUID와 이름을 사진 시작 때 고정한 뒤 버전이 포함된 JSON 배열을 SHA-256으로 해시한다. 서버에는 방문 UUID 또는 도착 해시만 전송하며 사진·파일명·이름 원문을 보내지 않는다.
  • 같은 UUID·같은 이름의 도착은 중복되지 않는다. 이름 변경·저장소 초기화·다른 기기는 추가 집계될 수 있다. 실제 사람이나 반려견의 정확한 고유 수가 아니다. 해시를 완전한 익명화로 보지 않는다.
  • 요청은 3초 제한, 일시 장애·429·5xx만 최대 두 번 재시도한다. 페이지 종료나 오프라인으로 유실될 수 있으며 영구 오프라인 큐는 없다. 서버 장애가 사진 체험을 막지 않는다.

하나의 프런트엔드 소스와 두 가지 빌드

사진 체험·화면·기능은 같은 src 소스를 사용한다. 기능 수정은 한 번만 하고, 정적 Site와 Fastify 서버용으로 각각 빌드한다. 두 개의 프런트엔드 복사본이나 별도 기능 브랜치를 장기 운영하지 않는다. 빌드 차이는 집계 활성 여부이며, 한 곳에 배포했다고 다른 곳에 자동 배포되는 것은 아니다. 이번 배포본은 현재 테스트 Site와 같은 소스 커밋에서 서버용으로 빌드했다.

소스 및 package-lock.json을 함께 옮긴다. 지원 대상은 Node 24이며 .node-version의 검증된 패치를 사용한다. 대상 서버 OS/CPU에서 better-sqlite3 네이티브 모듈이 실제 로드되는지 먼저 확인한다. 이 작업에서 만든 node_modules를 다른 서버로 복사하지 않는다.

npm ci
node -p "process.version + ' ' + process.platform + ' ' + process.arch"
node --input-type=module -e "import Database from 'better-sqlite3'; const db = new Database(':memory:'); console.log(db.prepare('select sqlite_version() as version').get()); db.close()"
npm run test:arrival

기존 정적 Site용 기본 빌드는 집계를 끈다. 집계 UUID 생성·저장소 읽기와 API 요청이 없다.

npm run build

Fastify 서버와 함께 사용할 때만 집계 활성 빌드를 만든다.

npm run build:server

두 명령 모두 빌드 출력은 dist다. 기존 Site에 집계 활성 결과물을 덮어 올리지 않는다. 빌드/타입 확인과 실제 브라우저·휴대폰 확인은 다르며 이후 배포 단계에서 별도로 검사한다.

서버 환경과 데이터 위치

  • DB_PATH: 반드시 명시한 절대경로. 소스·dist·public 밖의 영구 로컬 디스크에 둔다. 예: /var/lib/behind-glass/analytics.sqlite.
  • PORT: 기본 3000.
  • HOST: 기본 127.0.0.1. 외부 공개와 네트워크 설정 변경은 배포 승인 뒤 진행한다.
  • PUBLIC_ORIGIN: 운영 환경에서 필수. 경로·쿼리·계정정보 없는 HTTP(S) origin. 예: https://your-approved-host.example.
  • NODE_ENV=production: 운영 Origin 필수 검사를 사용한다.

설치 경로·데이터 경로와 서비스 계정은 미래 서버에서 확정한다. 데이터 폴더와 파일은 해당 서비스 계정만 사용할 수 있게 하며, 디렉터리 0700/DB 0600 수준을 유지한다. 기존 권한을 넓히지 않는다. DB 본체와 WAL/SHM, 백업을 공개 정적 경로나 소스/빌드에 넣지 않는다.

DB_PATH=/var/lib/behind-glass/analytics.sqlite \
PUBLIC_ORIGIN=https://your-approved-host.example \
NODE_ENV=production npm start

한 프로세스·한 SQLite 연결·한 서버 로컬 디스크를 전제로 한다. 클러스터 워커나 여러 컨테이너 복제본, 공유 네트워크 파일시스템으로 확장하지 않는다. 시작 시 버전 마이그레이션을 트랜잭션으로 적용하고 WAL과 제한된 busy timeout을 사용한다. SIGTERM/SIGINT로 정상 종료한 후 서비스 관리자에서 재시작한다. 새 릴리스로 바꾸거나 빌드 출력을 지워도 DB_PATH는 그대로 유지한다.

공개 API와 운영 조회

공개 API는 다음 세 가지뿐이다. 관리자·식별자 목록·MAU HTTP 경로는 없다.

  • POST /api/visits JSON { "visitor_id": "UUID-v4" } → 204
  • POST /api/arrivals JSON { "dog_id": "64자리 소문자 SHA-256" } → 204 (신규와 중복 동일)
  • GET /api/arrivals/count → { "count": 0 } 같은 실제 누적 정수

1 KiB 본문 제한과 엄격한 형식·추가 필드 거부를 적용한다. 없는 API는 JSON 404다. 공개 화면의 숫자는 마지막 정상값을 유지하며 아직 성공한 조회가 없으면 숨긴다.

MAU는 서버 터미널에서만 조회한다. 기존 DB를 읽기 전용으로 열며 오타 경로에 빈 DB를 만들지 않는다.

DB_PATH=/var/lib/behind-glass/analytics.sqlite npm run --silent metrics -- --month 2026-10

--silent는 npm의 실행 안내를 숨겨 JSON만 출력한다. 명령 결과는 월·MAU·누적 도착 합계뿐이다. 월은 한국 달력 기준 YYYY-MM이다. 방문 UUID·도착 해시를 출력하거나 운영 로그에 기록하지 않는다.

일관된 백업과 복원

운영 중에는 SQLite backup API를 사용한다. WAL 모드에서 DB 본체 파일 하나만 복사하면 최신 기록을 잃을 수 있다. 백업 경로도 공개 루트 밖의 절대경로로 지정한다. 같은 경로와 기존 파일 덮어쓰기를 허용하지 않는다.

DB_PATH=/var/lib/behind-glass/analytics.sqlite npm run --silent backup -- \
  --output /var/backups/behind-glass/analytics-2026-10-08.sqlite

복원 절차:

  1. 해당 DB를 사용하는 서버와 운영 명령을 모두 종료한다. 자동 재시작도 잠시 멈춘다.
  2. 현재 DB/WAL/SHM을 덮어쓰지 말고 안전한 별도 보관 위치로 옮겨 복구 지점을 남긴다.
  3. 검증할 백업을 새로운 비공개 데이터 경로로 복사하고 서비스 계정 소유·제한된 권한을 유지한다. 과거 WAL/SHM 파일과 섞지 않는다.
  4. 다음처럼 복원된 파일의 무결성과 합계를 확인한다. 결과가 맞지 않으면 재시작하지 않는다.
  5. DB_PATH를 검증된 복원 파일로 지정해 재시작한다. 집계/체험 동작을 확인한다.
DB_PATH=/var/lib/behind-glass-restored/analytics.sqlite node --input-type=module -e \
  "import Database from 'better-sqlite3'; const db = new Database(process.env.DB_PATH, {readonly:true,fileMustExist:true}); console.log(db.pragma('integrity_check')); db.close()"
DB_PATH=/var/lib/behind-glass-restored/analytics.sqlite npm run --silent metrics -- --month 2026-10

integrity_check가 ok인지 확인하고 백업 당시 합계와 비교한다. 백업·복원 성공은 QA에서 실제 임시 SQLite 파일을 사용해 확인한 범위와 미래 운영 데이터 복원을 구분한다. 정기 백업 스케줄과 보존 정책은 배포 단계에서 별도 결정한다.

향후 HTTPS 프록시와 정적 파일 점검

이번 구현은 프록시나 방화벽을 설정하지 않는다. 배포 승인 뒤 같은 HTTPS origin에서 정적 앱과 /api가 함께 동작하도록 연결한다. PUBLIC_ORIGIN에는 사용자가 실제 접속하는 정확한 origin을 넣는다.

Origin 검사와 메모리 기반 요청 제한은 허위 집계를 완전히 막는 인증 기능이 아니다. Origin 없는 비브라우저 요청도 가능하다. 제한 카운터는 프로세스 재시작 시 초기화한다. trustProxy:false를 유지하므로 프록시 뒤에서는 연결 IP를 공유해 제한을 함께 받을 수 있다. 임의 forwarded header를 신뢰하도록 즉석 변경하지 말고 배포 구조와 신뢰 경계를 검토한다.

앱은 본문·UUID·해시·IP를 로그에 남기지 않는다. 프록시·호스팅의 기본 접속 로그에는 IP가 남을 수 있으므로 배포 전에 기록 범위·보존·접근 권한을 확인한다.

HTML/API는 오래 캐시하지 않고, 해시 자산은 immutable 장기 캐시, 일반 정적 자산은 재검증한다. AI worker·모델·외부 ONNX WASM 다운로드 경로는 기존 설정을 유지한다. 별도 분석 서비스나 사진 업로드를 추가하지 않는다. CSP/COEP/COOP를 일괄 추가하면 기존 외부 AI 자산이 차단될 수 있으므로 이번 작업에서 변경하지 않는다.

배포 전 남은 실제 확인

  • 미래 서버의 Node/네이티브 의존성 설치, 데이터 권한·영구 저장·프로세스 재시작
  • 승인된 HTTPS 주소에서 실제 API와 정적 파일, 외부 모델/WASM 로딩
  • 실제 휴대폰/WebGL, 사진 선택·성공·실패·평면 보기·취소·이름 수정·샘플 흐름
  • 도착 숫자 표시와 스크린리더/IME 등 브라우저 상호작용
  • 프록시의 Origin 전달·캐시·요청 제한과 로그 정책

현재 클라우드 작업 공간을 상시 운영 서버로 약속하지 않으며, 실제 접속 주소는 배포가 확인될 때 별도로 제공한다.