v1.3.3: 자체 최소 통계 및 스플래시 localStorage 정리

- 일별 익명 방문자 해시·사이트/게시물 통계(030 마이그레이션)
- POST /api/analytics/pageview, 관리자 analytics API, 클라이언트 트래커
- 관리자 대시보드 통계 카드·인기 게시물 Top 5
- 스플래시: SITE_BRAND_LOGO_TEXT localStorage 제거
This commit is contained in:
2026-05-20 12:15:13 +09:00
parent b6a3228b09
commit 3623305119
18 changed files with 831 additions and 37 deletions

View File

@@ -145,6 +145,7 @@ docker compose --env-file .env.production exec sori-studio-db psql -U sori_studi
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/027_site_settings_home_cover.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/028_site_settings_announcement.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/029_site_settings_signup_blocked_usernames.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/030_analytics_daily_stats.sql
```
### Docker 네트워크 충돌 대응

View File

@@ -26,6 +26,7 @@
| lib/external-favicon-url.js | 외부 URL 호스트 기준 Google `s2/favicons` 썸네일 URL 생성(내부 경로는 빈 문자열) |
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
| lib/analytics.js | 통계 추적 경로 필터·봇 UA·일별 visitor hash |
## Nuxt 모듈
@@ -114,7 +115,7 @@
| 파일 | 화면 |
|------|------|
| pages/admin/index.vue | 대시보드 |
| pages/admin/index.vue | 대시보드(오늘·7일 방문, 30일 조회, 인기 게시물 Top 5) |
| pages/admin/login.vue | 관리자 로그인(이메일·비밀번호 모두 입력 시에만 제출 버튼 활성) |
| pages/admin/posts/index.vue | 글 목록, 헤더 우측에 필터(상태·태그·정렬)와 «새 글»을 한 줄 배치, 예약/초안/발행 텍스트 상태, 제목 옆 댓글 수, 읽기 전용 태그 배지, 행 끝 more vert 메뉴(추천·삭제) |
| pages/admin/posts/new.vue | 글 작성, Ghost 스타일 작성 폼, 초안 `POST` 디바운스 자동 저장·이탈 직전 플러시, 저장 토스트 |
@@ -241,6 +242,12 @@
| server/repositories/content-repository.js | 콘텐츠 조회 저장소 |
| server/repositories/member-repository.js | 회원 조회/생성 저장소 |
| server/repositories/comment-repository.js | 댓글 조회/생성 저장소 |
| server/repositories/analytics-repository.js | 방문·게시물 통계 집계·관리자 요약 조회 |
| server/utils/analytics-pageview-input.js | `POST /api/analytics/pageview` 검증·기록 |
| server/api/analytics/pageview.post.js | 공개 통계 수집 API |
| server/routes/admin/api/analytics/summary.get.js | 관리자 통계 요약 API |
| server/routes/admin/api/analytics/posts.get.js | 관리자 인기 게시물 API |
| plugins/site-analytics.client.js | 공개 라우트 pageview·게시물 read 클라이언트 전송 |
## 데이터베이스
@@ -265,6 +272,7 @@
| db/migrations/014_add_user_role_levels.sql | 회원 권한 3단계(owner/admin/member) 컬럼 추가 |
| db/migrations/013_add_user_admin_role.sql | 회원 관리자 권한 컬럼 추가 및 첫 사용자 관리자 승격 |
| db/migrations/015_add_tag_type_and_reorder_support.sql | 태그 유형(`managed`/`general`) 컬럼 추가 |
| db/migrations/030_analytics_daily_stats.sql | 사이트·게시물 일별 통계·일별 방문자 해시 테이블 |
## 설정/배포

View File

@@ -45,7 +45,7 @@
- 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리
- 라이트 모드 기본 배경은 `#fcfcfc`로 통일하고 패널 구분은 보더로 처리
- 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응
- 사용자 수동 테마 전환은 `html[data-theme]``localStorage.SITE_THEME`로 관리한다. 첫 페인트 전 `lib/site-theme-init.js` 인라인 스크립트가 테마를 적용해 시스템 다크·저장 라이트 불일치 시 깜빡임을 줄인다. 공개 페이지 로딩 중에는 `#site-splash`에 캐시된 로고(`SITE_BRAND_LOGO_URL`) 또는 로고 텍스트를 잠깐 표시하고, 앱 마운트 후 `site-app-ready`로 숨긴다.
- 사용자 수동 테마 전환은 `html[data-theme]``localStorage.SITE_THEME`로 관리한다. 첫 페인트 전 `lib/site-theme-init.js` 인라인 스크립트가 테마를 적용해 시스템 다크·저장 라이트 불일치 시 깜빡임을 줄인다. 공개 페이지 로딩 중에는 `#site-splash`에 캐시된 로고 이미지 URL(`SITE_BRAND_LOGO_URL`, localStorage) 또는 사이트 제목(`NUXT_PUBLIC_SITE_TITLE`)을 잠깐 표시하고, 앱 마운트 후 `site-app-ready`로 숨긴다. `site_settings.logo_text`(기본 `井`)는 **이미지 로고가 없을 때** 헤더·사이드바에 쓰는 짧은 기호이며 localStorage·스플래시와는 별개다.
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
### 홈 Featured (인덱스)
@@ -351,6 +351,20 @@ components/content/
| tag_id | UUID | FK → Tags |
| created_at | DateTime | 생성일 |
### Analytics (자체 최소 통계)
> 마이그레이션 `030_analytics_daily_stats.sql`. 원문 IP·User-Agent·쿠키 ID는 저장하지 않는다. 서버는 `date + IP + User-Agent + secret`으로 일 단위 `visitor_hash`만 생성·저장한다.
| 테이블 | 필드 | 설명 |
|--------|------|------|
| site_analytics_daily | day, page_views, visitors | 사이트 일별 페이지뷰·방문자(중복 제거) |
| post_analytics_daily | day, post_id, views, reads, visitors | 게시물 일별 조회·읽음·방문자 |
| analytics_daily_visitors | day, scope(`site`/`post`), post_id?, visitor_hash | 일별 방문자 해시 등록(중복 방문 제거용) |
- 추적 대상: 공개 경로만. `/admin`, `/signin`, `/signup`, `/forgot-password`, `/settings`는 제외.
- 봇 User-Agent는 서버에서 무시.
- 게시물 `reads`는 클라이언트에서 15초 이상 체류·50% 이상 스크롤 후 별도 전송.
---
## API 구조
@@ -379,6 +393,7 @@ components/content/
- `GET /api/tags` - 태그 목록
- `GET /api/search?q=` - 통합 검색(태그 `name`·`slug`, 게시물 `title`·`excerpt`·`content` 부분 일치, 각 최대 12건, 발행 게시물만)
- `GET /api/site-settings` - 공개 사이트 설정(어나운스 바·홈 커버 등 포함)
- `POST /api/analytics/pageview` - 공개 방문·게시물 조회/읽음 집계. 본문: `path`(필수), `postSlug`(게시물일 때), `read`(읽음 이벤트). 발행된 게시물만 `postSlug` 집계. 응답 `{ ok: true }`.
- `GET /api/navigation` - 공개 네비게이션(`primary`는 트리·`footer`·`recommended`는 평면, 상세는 위 메뉴/네비게이션 절)
- `POST /api/auth/signup` - 회원 가입. 본문: `username`, `email`, `password`, 선택 `emailOtp`(6자리 숫자). **`GET /api/auth/bootstrap-status``emailOtpConfigured`가 true이고 `needsAdminSetup`이 false일 때**(서버에 Resend·pepper 설정됨) `emailOtp`가 필수이며, 직전에 `POST /api/auth/email-otp/request`로 같은 이메일·`purpose: "signup"` 발송분과 일치해야 한다. 최초 관리자 부트스트랩(`needsAdminSetup: true`)에서는 `emailOtp` 없이 가입 가능. 이메일은 저장 시 소문자로 정규화한다.
- `GET /api/auth/bootstrap-status` - 최초 관리자 등록 필요 여부(`hasUsers`, `needsAdminSetup`) 및 **이메일 OTP 사용 가능 여부** `emailOtpConfigured`(서버 `RESEND_API_KEY`·`RESEND_FROM_EMAIL` 및 pepper: 비어 있지 않은 `EMAIL_OTP_PEPPER` **또는** `MEMBER_SESSION_SECRET`)를 반환한다. pepper는 OTP 6자리를 DB에 해시해 둘 때만 쓰이는 서버 비밀(긴 난문자열 권장)이다.
@@ -410,6 +425,8 @@ components/content/
- `POST /admin/api/auth/login` - 로그인
- `POST /admin/api/auth/logout` - 로그아웃
- `GET /admin/api/auth/me` - 현재 관리자 세션 조회
- `GET /admin/api/analytics/summary?days=30` - 통계 요약(오늘 방문, 최근 7일 방문 합, 최근 30일 페이지뷰 합)
- `GET /admin/api/analytics/posts?days=30&limit=5` - 기간 내 인기 게시물(조회·읽음·방문자 합, 조회수 내림차순)
- `GET /admin/api/posts` - 글 목록
- `POST /admin/api/posts` - 글 작성
- `GET /admin/api/posts/:id` - 글 상세
@@ -677,6 +694,7 @@ DB_PORT=43119
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=replace-with-random-password
MEMBER_SESSION_SECRET=replace-with-random-password
ANALYTICS_HASH_SECRET=replace-with-random-password
# Upload
UPLOAD_DIR=/uploads

View File

@@ -1,7 +1,14 @@
# 업데이트 이력
## v1.3.3
- 자체 최소 통계: 일별 익명 방문자 해시·사이트/게시물 일별 집계. 마이그레이션 `030_analytics_daily_stats.sql`. 원문 IP·User-Agent 미저장.
- `POST /api/analytics/pageview`, `plugins/site-analytics.client.js`(pageview·15초+50% 스크롤 read).
- 관리자 대시보드: 오늘/7일 방문·30일 조회·인기 게시물 Top 5. `GET /admin/api/analytics/summary`, `GET /admin/api/analytics/posts`.
## v1.3.2
- 스플래시: `SITE_BRAND_LOGO_TEXT` localStorage 사용 중단·레거시 키 제거. 스플래시 문구는 사이트 제목, 이미지는 `SITE_BRAND_LOGO_URL`만 캐시.
- 관리자 설정 내비: 타임존·메인 화면·어나운스 바·Import/Export·스팸 필터 아이콘 추가.
- 어나운스 바: 클라이언트에서 숨김 여부 확인 후 아래로 슬라이드 인. 닫기·7일간 보지 않기 시 위로 슬라이드 아웃 후 제거(깜빡임 방지).
- 어나운스 바: X는 이번 방문(세션)만 숨김, `7일간 보지 않기` 텍스트 버튼으로 localStorage 7일 스누즈.