v1.12.21 관리자 미디어 조작 개선 및 CSP 관찰 안내 정리

This commit is contained in:
2026-09-28 16:52:14 +09:00
parent 5482318b84
commit 6886c57d04
18 changed files with 488 additions and 60 deletions

View File

@@ -68,7 +68,7 @@
- 호환 정책은 모든 경로에서 `Content-Security-Policy-Report-Only`로 유지한다. 사용자 사이트 코드·광고·외부 미디어를 차단하지 않은 채 script/style/img/font/media/frame/connect/worker/manifest 위반을 `/api/csp-report`로 보고하며 `Reporting-Endpoints`와 `Report-To`도 같은 origin 수집 주소를 가리킨다.
- `POST /api/csp-report`는 `application/csp-report`, `application/reports+json`, `application/json`, `application/*+json`만 허용하고 요청 본문은 32KB, Reporting API 배열은 최대 20건으로 제한한다. IP 원문을 보관하지 않는 해시 키 기준으로 분당 60회까지만 받는다.
- CSP 위반은 정책 버전, 쿼리·fragment를 제거한 문서 경로, 지시어, 경로·쿼리를 제거한 차단 origin, 브라우저 계열만 `csp_violation_aggregates`에 누적한다. 원문 URL과 User-Agent는 저장하지 않으며 저장 실패가 일반 응답을 실패시키지 않는다. 차단 출처가 비어 있으면 `empty`, `about:blank` 계열은 `about:`, 브라우저 확장 프로그램은 `chrome-extension:`·`moz-extension:`·`safari-web-extension:` 같은 scheme 단위로 분류한다.
- 관리자 대시보드는 CSP Report-Only 위반 집계가 1건 이상 있을 때만 `CSP 관찰` 표를 표시한다. 표는 최근 30일 기준 상위 위반의 지시어, 차단 출처, 문서 경로, 브라우저 계열, 횟수, 최근 관찰 시각을 보여준다. `script-src-elem`, `style-src-attr`, `child-src` 같은 세부 지시어는 각각 `script-src`, `style-src`, `frame-src` 후보로 묶고, `http(s)` origin만 허용 후보로 별도 표시한다.
- 관리자 대시보드는 CSP Report-Only 위반 집계가 1건 이상 있을 때만 `CSP 관찰` 표를 표시한다. 표는 최근 30일 내 보고된 상위 항목의 지시어, 보고된 출처, 문서 경로, 브라우저 계열, 누적 횟수, 최근 관찰 시각을 보여준다. 횟수는 최초 수집부터 누적이며 실제 최근 30일 발생 수가 아니다. 관찰 목적과 차단 없이 보고만 수집한다는 설명, `empty`·`unknown`의 의미, HTTPS 출처를 폭넓게 허용하는 현재 정책의 관찰 한계를 안내한다. `script-src-elem`, `style-src-attr`, `child-src` 같은 세부 지시어는 각각 `script-src`, `style-src`, `frame-src` 후보로 묶고, `http(s)` origin만 허용 후보로 별도 표시한다. 후보가 없어도 안전성이나 정책 강화 가능성을 확정하지 않는다.
### 홈 Featured (인덱스)
@@ -685,7 +685,7 @@ components/content/
- `GET /admin/api/analytics/realtime?limit=20` - 현재 접속자 요약·목록(로그인 사용자 닉네임·아바타, IP, 열린 페이지 수와 열린 페이지 목록 포함)
- `GET /admin/api/analytics/traffic?days=30` - 기간 내 유입원·디바이스·유입 키워드 통계
- `GET /admin/api/analytics/range-detail?startDay=YYYY-MM-DD&endDay=YYYY-MM-DD&limit=5` - 통계 추이 막대 상세(요약·유입·디바이스·인기 게시물·인기 페이지). 인기 콘텐츠는 조회·읽음/방문자·최근 30일 조회·작성일·스크롤 도달 지표를 포함한다. 최대 31일 범위로 제한한다.
- `GET /admin/api/security/csp-violations?days=30&limit=8` - CSP Report-Only 위반 집계 관찰 요약. `days`는 1~90일, `limit`은 1~20개로 제한하며 전체 누적 횟수, 최근 기간 횟수, 마지막 관찰 시각, 상위 위반 목록, 지시어별 외부 origin 허용 후보를 반환한다.
- `GET /admin/api/security/csp-violations?days=30&limit=8` - CSP Report-Only 위반 집계 관찰 요약. `days`는 1~90일, `limit`은 1~20개로 제한하며 전체 누적 횟수, 최근 관찰 항목의 누적 횟수(`recentViolations`: 기간 내 `last_seen_at`을 가진 행의 `violation_count` 합계), 마지막 관찰 시각, 상위 위반 목록, 지시어별 외부 origin 허용 후보를 반환한다. 후보는 최근 관찰 상위 200행에서 계산한다. 일별 원장이 없어 기간 내 실제 발생 횟수는 반환하지 않는다.
- `GET /admin/api/analytics/posts/:id?period=daily&days=14` - 게시물별 통계(누적·기간 요약, 일간/주간/월간 조회·방문자 추이, 게시물별 유입·디바이스·키워드)
- `GET /admin/api/analytics/pages/:id?period=daily&days=14` - 페이지별 통계(누적·기간 요약, 일간/주간/월간 조회·방문자 추이, 체류·스크롤 구간)
- `GET /admin/api/posts` - 글 목록. 기본은 본문 없는 배열 응답이며, `paged=true&page=1&pageSize=50&q=검색어&status=published&tag=slug&featured=featured&sort=newest|oldest` 쿼리를 보내면 서버 필터링된 `{ items, pagination, counts }` 페이지 응답을 반환한다.
@@ -999,8 +999,10 @@ components/content/
- 관리자 미디어 화면 상단에 **미디어 라이브러리**·**대표 이미지**·**본문 이미지**·**프로필 이미지** 탭을 두어, 라이브러리 탭에서는 원본 업로드 파일만, 대표 이미지 탭에서는 `/posts/YYYY/MM/thumbs/*-card.webp`와 `*-page-cover.webp` 파일만, 본문 이미지 탭에서는 `/posts/YYYY/MM/thumbs/*-body-1600.webp`, `*-body-720.webp`, `*-body-2560.webp` 파일과 레거시 `*-body.webp`, `*-lightbox.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 본문 이미지 탭의 상세 정보는 1600px, 720px, 2560px 용도를 구분해 표시한다. 라이브러리 탭의 파일 종류 필터는 `전체/이미지/영상/음악/파일`을 표시하고 사용 상태 필터는 중복 `전체` 배지를 피하기 위해 `미사용` 토글만 표시한다. 시스템 파생 탭에서는 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 대표 이미지 탭과 본문 이미지 탭에서는 모든 항목이 해당 파생 파일이므로 각각 `카드`, `본문` 역할 배지는 표시하지 않으며, 탭 전환 중 이전 응답 항목이 잠깐 남아도 파생 파일 URL 기준으로 역할 배지를 숨긴다. 미디어 그리드는 서버 페이지 응답 기준 50·100·200개 단위 페이지네이션을 사용하며, 페이지당 표시 개수 셀렉트는 다른 관리자 셀렉트와 같은 SVG chevron 화살표를 사용한다. 원본 이미지 항목도 가능한 경우 720px 본문 파생 이미지 또는 카드·Pages 커버 썸네일을 우선 표시하고 실패 시 원본으로 되돌린다. 파생 이미지 목록·상세 미리보기는 파일 수정 시각과 용량을 캐시 버전으로 붙여 재생성 직후에도 새 파일을 확인할 수 있게 한다.
- 미디어 라이브러리 탭은 왼쪽 폴더 트리와 오른쪽 고밀도 썸네일 갤러리, 검색, 파일명 변경, 개별 삭제를 제공한다.
- 관리자는 폴더 추가 버튼으로 모달에서 새 폴더·하위 폴더 이름을 입력해 만들 수 있으며, `미분류`·`썸네일`(및 그 하위)을 제외한 폴더는 목록에서 삭제할 수 있다. 폴더 삭제 시 해당 경로 및 하위 경로로 분류돼 있던 미디어 메타는 모두 `미분류`로 되돌린다.
- 썸네일 본문(이미지·파일명) 한 번 클릭 시 상세(미리보기) 모달이 열리고, 썸네일 좌측 상단 **선택 토글**로 개별 선택한다. Shift+클릭으로 현재 페이지 안의 범위 선택이 가능하다. 현재 페이지 선택은 화면에 보이는 항목 중 사용 중이 아니어서 삭제 가능한 항목만 대상으로 한다.
- 미디어 라이브러리 탭에서만 선택한 미디어를 폴더로 드래그하면 해당 미디어들의 폴더 경로가 일괄 변경된다.
- 썸네일 본문(이미지·파일명) 한 번 클릭 시 상세(미리보기) 모달이 열리고, 썸네일 좌측 상단 **선택 토글**로 개별 선택한다. Shift+클릭으로 현재 페이지 안의 범위 선택이 가능하다. **전체 선택 / 전체 선택 해제**는 현재 페이지에 표시된 모든 항목에 적용하며 사용 중인 항목도 포함한다. 선택은 카드 테두리와 체크로 표시한다. 선택 삭제는 사용 중인 항목을 제외하고 가능한 개수를 확인받는다.
- 미디어 라이브러리 탭에서만 선택한 미디어를 폴더로 드래그하면 해당 미디어들의 논리 폴더 경로가 일괄 변경된다. 드래그 미리보기와 안내에 이동 개수를 표시하고, 이동 대상 카드는 흐리게 표시하며, 폴더 위에서는 강조와 `놓으면 N개 이동`을 표시한다. 전체 미디어에 드롭하면 `미분류`로 이동한다는 안내를 별도로 표시한다. 이미 대상 폴더에 있는 항목과 시스템 관리 항목은 제외한다. 이동 중에는 중복 이동·선택·삭제를 막고 결과를 토스트로 안내한다. 성공 후 현재 탐색 위치를 유지하고 선택을 해제하며, 실패 시 선택을 유지한다. 취소·종료 시 드래그 상태를 정리하고 외부 파일 드롭으로 기존 선택 항목이 이동하지 않게 한다.
- 선택 항목이 있는 상태에서 폴더를 생성하면 현재 폴더와 선택을 유지해 새 폴더로 바로 드래그할 수 있다. 선택이 없으면 생성한 폴더를 연다.
- 폴더·탭·검색·필터·페이지 전환과 목록 갱신 중에는 이전 그리드·범위 수치·페이지네이션을 숨기고 로딩 안내·스켈레톤을 표시한다. 최신 조회가 앞선 요청을 취소하며, 조회 실패는 빈 폴더와 구분된 오류 및 **다시 시도** 버튼으로 표시한다.
- 관리자 미디어 화면 검색은 **저장 파일명**과 게시물·페이지 **사용처 제목**만 대상으로 한다(URL 경로·논리 폴더명 문자열은 검색에 쓰지 않는다).
- 미디어 파일 경로, 사용 현황(라이브러리), 연결 회원(프로필 이미지 탭), 용량 등 세부 정보는 상세 모달에서 표시한다.
- API 실패·클라이언트 검증 실패 등 사용자 피드백은 본문 상단 고정 배너가 아니라 `useAdminToast` 우측 상단 토스트로 표시해 모달에 가리지 않는다.