120 lines
9.7 KiB
Markdown
120 lines
9.7 KiB
Markdown
# 방문·도착 집계 운영 및 이전 안내
|
|
|
|
이 안내는 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`를 다른 서버로 복사하지 않는다.
|
|
|
|
```sh
|
|
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 요청이 없다.
|
|
|
|
```sh
|
|
npm run build
|
|
```
|
|
|
|
Fastify 서버와 함께 사용할 때만 집계 활성 빌드를 만든다.
|
|
|
|
```sh
|
|
npm run build:server
|
|
```
|
|
|
|
두 명령 모두 빌드 출력은 `dist`다. 기존 Site에 집계 활성 결과물을 덮어 올리지 않는다. 빌드/타입 확인과 실제 브라우저·휴대폰 확인은 다르며 이후 배포 단계에서 별도로 검사한다.
|
|
|
|
## 서버 환경과 데이터 위치
|
|
|
|
- `DB_PATH`: 반드시 명시한 절대경로. 소스·dist·public 밖의 영구 로컬 디스크에 둔다. 예: `/var/lib/behind-glass/analytics.sqlite`.
|
|
- `PORT`: 기본 3600.
|
|
- `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, 백업을 공개 정적 경로나 소스/빌드에 넣지 않는다.
|
|
|
|
```sh
|
|
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를 만들지 않는다.
|
|
|
|
```sh
|
|
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 본체 파일 하나만 복사하면 최신 기록을 잃을 수 있다. 백업 경로도 공개 루트 밖의 절대경로로 지정한다. 같은 경로와 기존 파일 덮어쓰기를 허용하지 않는다.
|
|
|
|
```sh
|
|
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`를 검증된 복원 파일로 지정해 재시작한다. 집계/체험 동작을 확인한다.
|
|
|
|
```sh
|
|
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 전달·캐시·요청 제한과 로그 정책
|
|
|
|
현재 클라우드 작업 공간을 상시 운영 서버로 약속하지 않으며, 실제 접속 주소는 배포가 확인될 때 별도로 제공한다.
|