v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.

세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 11:05:40 +09:00
parent 9bc0f0a90e
commit de767095ba
116 changed files with 8128 additions and 2933 deletions

View File

@@ -1,6 +1,6 @@
# 배포 가이드
> 로컬 기준 v1.10.83에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
> v1.10.88은 단위 테스트와 프로덕션 빌드 외에 격리 PostgreSQL 기반 Playwright API 10건·Chromium E2E 3건을 제공한다. 운영 배포 전 `068_csp_violation_aggregates.sql`, `069_media_catalog.sql`을 순서대로 적용하고 미디어 카탈로그는 reconcile → (선택) shadow 관찰 → `MEDIA_CATALOG_MODE=database` 고정 순서를 지킨다. 게시물 Export·Import·일반 업로드는 디스크·임시 디렉터리 스트리밍을 사용하므로 NAS 앱 컨테이너의 임시 저장 공간과 `public/uploads/exports` 쓰기 권한을 확인한다.
## 빌드 유형
@@ -8,10 +8,51 @@
|------|--------|------|
| 개발 | `npm run dev` | 로컬 테스트, 개발 서버 |
| 프로덕션 | `npm run build` | NAS 배포, 운영 서버 |
| 검증 | `npm run verify` | JavaScript 문법 점검 + 프로덕션 빌드 |
| 단위 검증 | `npm run verify` | JavaScript 문법 점검 + Node 단위 테스트 + 프로덕션 빌드 |
| CI 전체 검증 | `npm run verify:ci` | 문법 + 단위 + Playwright API/E2E + 프로덕션 빌드 |
> `npm run dev`는 프로젝트 전용 실행 스크립트를 통해 개발 서버, Admin, Tailwind Viewer 링크만 요약 출력한다.
## v1.10.88 배포 전 검증
Playwright API 테스트도 브라우저 바이너리는 필요하지 않지만 `verify:ci`에는 Chromium E2E가 포함된다. 최초 1회 의존성과 Chromium을 설치하고 Docker daemon이 실행 중인지 확인한다.
```bash
npm install
npx playwright install chromium
npm run test:unit
npm run test:api
npm run test:e2e
npm run verify:ci
```
Linux CI 이미지에서 브라우저 시스템 의존성도 함께 준비해야 하면 `npx playwright install --with-deps chromium`을 사용한다. API/E2E 명령은 `docker-compose.test.yml`의 `sori_studio_test` DB와 임시 업로드 루트만 사용하고 종료 시 제거한다. 테스트 중 Export worker와 추천 사이트 파비콘 cron은 비활성화된다.
### 운영 마이그레이션과 미디어 카탈로그 전환
```bash
sh scripts/migrate-production-db.sh status
sh scripts/migrate-production-db.sh migrate
sh scripts/migrate-production-db.sh status
```
- `068_csp_violation_aggregates.sql`은 CSP 보고 집계 테이블을 만든다.
- `069_media_catalog.sql`은 미디어 카탈로그·참조·상태 계약을 만든다. 최초 상태는 `ready=false`이므로 마이그레이션 직후 곧바로 `MEDIA_CATALOG_MODE=database`만 켠 채 운영하지 않는다. 반드시 reconcile을 먼저 끝낸다.
- 배포 직후(또는 `filesystem`) 상태에서 관리자 인증 후 `POST /admin/api/media/reconcile`을 1회 실행한다. 이 API는 파일을 삭제하거나 이동하지 않고 발견 파일 upsert, 참조 백필, 사라진 파일 `missing` 표시만 수행한다.
- `media_catalog_state`의 `ready=true`, 파일시스템/카탈로그 `count`와 `bytes` 일치, `staging`·`error` 행 0을 확인한다.
- 필요하면 짧게 `MEDIA_CATALOG_MODE=shadow`로 재기동해 관리자 미디어를 탐색하고 로그의 `count`, `bytes`, 종류별 차이가 0인지 본다.
- 확인이 끝나면 운영 env를 `MEDIA_CATALOG_MODE=database`로 고정하고 재기동한다. 개발 환경도 동일하게 `database`로 고정한다. 준비 미완료·불일치·DB 조회 오류가 발생하면 애플리케이션은 자동으로 파일시스템 목록으로 fallback한다.
- 관리자 화면의 임시 전환 버튼·런타임 모드 덮어쓰기 API는 제거했다. 모드 변경은 환경 변수만 사용한다.
- `npm run media:reconcile:dev`는 loopback 비운영 DB 전용 안전장치가 있으므로 개발 DB 확인에만 사용한다. 운영에서는 관리자 API를 사용한다.
- 개발 DB에서는 124개·77,019,754 bytes, 개수·용량 차이 0으로 reconcile을 확인했다. 운영 볼륨은 운영 수치로 별도 확인해야 한다.
### CSP 관찰
v1.10.88은 `base-uri`, `object-src`, `frame-ancestors`만 강제한다. 광고 코드, 사용자 사이트 코드, 외부 이미지·미디어·iframe에 영향을 줄 수 있는 출처 정책은 계속 Report-Only이므로 기존 광고와 사용자 코드 실행을 차단하지 않는다. 전체 HTML 문서 `/pages/**`는 강제 CSP에서 제외하고 Report-Only 보고만 유지한다.
배포 후 `csp_violation_aggregates`의 `policy_version='compat-v1'`, `effective_directive`, `blocked_origin`, `document_path`, `user_agent_family`, `violation_count`, `last_seen_at`을 관찰한다. URL 쿼리·fragment·차단 자산 경로와 User-Agent 원문은 저장되지 않는다. 반복 위반이 광고·사이트 코드의 정상 출처인지 확인한 뒤에만 강제 정책 확대를 별도 배포한다.
---
## 로컬 개발
@@ -68,7 +109,7 @@ docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
```
> 지난 버전 마이그레이션을 아직 적용하지 않았다면 `062_site_settings_display_counts.sql`부터 `065_public_search_index.sql`까지 순차 적용되어야 한다. 일반 배포에서는 `npm run db:migrate:prod` 또는 NAS 컨테이너 내부 마이그레이션 명령이 이 순서를 자동 처리한다.
> 지난 버전 마이그레이션을 아직 적용하지 않았다면 `062_site_settings_display_counts.sql`부터 `069_media_catalog.sql`까지 순차 적용되어야 한다. 앱 재기동 전에 운영 DB 마이그레이션을 먼저 완료한다. 일반 배포에서는 `npm run db:migrate:prod` 또는 NAS 컨테이너 내부 마이그레이션 명령이 이 순서를 자동 처리한다.
### 확인 주소
@@ -213,7 +254,7 @@ docker compose --env-file .env.production up -d --build
- 로컬 개발: 43080
- NAS Docker 외부: 43080
- 컨테이너 내부: 3000
- PostgreSQL 외부: 43081
- PostgreSQL 호스트 로컬 전용: `127.0.0.1:43081`
- Docker 내부 네트워크 기본값: `10.250.50.0/24`
- HTTPS: 3001 (SSL 설정 시)
@@ -232,6 +273,7 @@ docker compose --env-file .env.production up -d --build
- NAS 운영: `.env.production`의 `DATABASE_URL`
- 로컬 개발 예시: `postgres://sori_studio:비밀번호@127.0.0.1:43081/sori_studio`
- NAS Docker 예시: `postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio`
- `DB_BIND_ADDRESS` 기본값은 `127.0.0.1`이다. 특별한 관리망 요구가 없다면 `0.0.0.0`으로 변경하지 않는다.
- `.env.example`에는 실제 비밀번호나 개인 이메일을 기록하지 않음
- 개발/운영 DB 비밀번호와 관리자 비밀번호는 서로 다른 랜덤 값을 사용
- `ADMIN_EMAIL`/`ADMIN_PASSWORD`는 운영 DB에 owner/admin이 없는 최초 관리자 생성에만 사용한다. 같은 이메일의 일반 회원이 이미 있으면 owner로 승격하고 비밀번호를 `ADMIN_PASSWORD` 기준으로 갱신한다. 첫 owner 계정이 DB에 생성된 뒤에는 관리자 로그인도 DB의 bcrypt 비밀번호를 기준으로 검증한다.
@@ -246,6 +288,7 @@ docker compose --env-file .env.production up -d --build
- 스키마 변경은 `db/migrations/`의 SQL 파일로 관리한다. 버전별 변경 내역은 `docs/update.md`를 참고한다.
- 로컬: `npm run db:migrate:dev`
- NAS 운영: `sh scripts/migrate-production-db.sh status` → `migrate`
- 각 SQL 파일 실행과 `schema_migrations` 이력 기록은 같은 `psql --single-transaction`에서 처리되며 오류 시 함께 롤백된다.
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `migrate`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `sh scripts/migrate-production-db.sh baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `sh scripts/migrate-production-db.sh baseline 031`.
### 이메일 인증(Resend, 선택)