diff --git a/components/admin/AdminPageForm.vue b/components/admin/AdminPageForm.vue index 0065fe3..a503d8c 100644 --- a/components/admin/AdminPageForm.vue +++ b/components/admin/AdminPageForm.vue @@ -92,7 +92,7 @@ const romanizeHangulSyllable = (char) => { } /** - * 문자열을 영문 URL 슬러그로 변환 + * 문자열을 URL 슬러그로 변환 * @param {string} value - 원본 문자열 * @returns {string} 영문 슬러그 */ @@ -103,10 +103,10 @@ const toSlug = (value) => value .join('') .trim() .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') + .replace(/[^a-z0-9\s_-]/g, '') .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') + .replace(/[-_]+/g, (match) => match.includes('_') ? '_' : '-') + .replace(/^[-_]|[-_]$/g, '') const pageSlug = computed(() => toSlug(form.slug || form.title)) const viewPageUrl = computed(() => props.publicUrl || (pageSlug.value ? `/pages/${pageSlug.value}` : '')) diff --git a/components/admin/AdminPostForm.vue b/components/admin/AdminPostForm.vue index f5704ac..97effc0 100644 --- a/components/admin/AdminPostForm.vue +++ b/components/admin/AdminPostForm.vue @@ -405,10 +405,10 @@ const toSlug = (value) => value .join('') .trim() .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') + .replace(/[^a-z0-9\s_-]/g, '') .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') + .replace(/[-_]+/g, (match) => match.includes('_') ? '_' : '-') + .replace(/^[-_]|[-_]$/g, '') /** * 게시물 태그 입력 토큰 정규화(한글 유지, 공백은 하이픈으로) diff --git a/components/site/PostCard.vue b/components/site/PostCard.vue index 1e0d3eb..9d3784b 100644 --- a/components/site/PostCard.vue +++ b/components/site/PostCard.vue @@ -31,7 +31,17 @@ defineProps({ {{ post.excerpt }}
diff --git a/docs/changelog.md b/docs/changelog.md index b6ab7ba..b6b745d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,20 @@ # 업데이트 요약 +## v1.9.10 + +- 관리자 로그인 상태에서 공개 글·페이지를 열어도 조회수, 읽음, 체류, 스크롤, 실시간 접속 통계에 포함되지 않게 했다. + +## v1.9.9 + +- 관리자 글·페이지 목록의 공개 상태 표기를 `공개`로 맞췄다. +- 관리자 글·페이지 목록 주소 옆에 공개 화면으로 바로 여는 아이콘을 hover/focus 시 표시한다. + +## v1.9.8 + +- 게시물·페이지 슬러그 입력 규칙을 통일하고 언더바를 사용할 수 있게 했다. +- 게시물 본문 제목의 `~` 문자가 오른쪽 목차에서 사라지지 않도록 수정했다. +- 메인·목록·태그 화면의 태그 배지와 카드 화살표 링크 동작을 통일했다. + ## v1.9.0 - 관리자 대시보드 통계 추이를 일일·주간·월간·년간 기준으로 볼 수 있게 정리했다. diff --git a/docs/deploy.md b/docs/deploy.md index f97d471..de7f638 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,6 +1,6 @@ # 배포 가이드 -> 로컬 기준 v1.9.4에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. +> 로컬 기준 v1.9.10에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. ## 빌드 유형 @@ -264,6 +264,7 @@ docker compose --env-file .env.production up -d --build - `analytics_active_sessions`: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다. - 관리자 대시보드 차트는 최대 365일 범위를 조회하며, 차트 범위를 넘는 집계도 누적 통계 원본으로 보관한다. - 관리자 통계 집계 시작일은 `ANALYTICS_COLLECTED_SINCE` 환경 변수로 설정한다. 기본값은 `2026-06-01`이다. +- 관리자 세션으로 공개 글·페이지를 확인하는 요청은 통계 수집에서 제외된다. 운영자가 직접 방문자처럼 검증해야 할 때는 관리자 로그아웃 또는 별도 브라우저 프로필을 사용한다. ### 개발/운영 DB 분리 검증 절차 diff --git a/docs/history.md b/docs/history.md index 296d3de..bcc0d32 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,17 @@ # 의사결정 이력 +## 2026-06-30 v1.9.10 — 관리자 세션은 공개 통계에서 제외한다 + +운영자가 글 작성·수정·확인을 위해 공개 게시물과 페이지를 반복해서 열면 실제 방문자 행동보다 관리자 검수 동작이 조회수와 체류 통계에 크게 반영된다. 따라서 관리자 세션 쿠키가 있는 요청은 공개 경로라도 페이지뷰, 읽음, heartbeat, HTML 문서 페이지 직접 집계에서 제외해 통계가 방문자 기준에 더 가깝게 유지되도록 한다. + +## 2026-06-30 v1.9.9 — 관리자 글·페이지 목록의 공개 상태와 보기 액션을 맞춘다 + +게시물과 페이지는 공개 URL을 가진 같은 콘텐츠 관리 대상이므로 목록에서 상태 용어가 다르면 운영자가 서로 다른 의미로 오해할 수 있다. 따라서 공개 상태 표기는 `공개`로 통일하고, 공개 화면 확인은 제목 편집 링크와 분리해 주소 줄 오른쪽의 외부 링크 아이콘으로 제공한다. + +## 2026-06-30 v1.9.8 — 게시물과 페이지 슬러그 규칙을 통일한다 + +게시물과 페이지는 공개 URL 안에서 같은 성격의 식별자로 쓰이므로 입력 허용 범위가 다르면 운영자가 화면마다 다른 규칙을 기억해야 한다. 따라서 두 관리자 폼과 서버 검증 모두 영문 소문자, 숫자, 한글, 하이픈, 언더바를 같은 방식으로 허용한다. 하이픈과 언더바는 URL 가독성을 위한 구분자로 유지하되 앞뒤 끝에는 두지 않는 규칙을 유지한다. + ## 2026-06-29 v1.9.0 — 대시보드 추이는 서버 버킷 집계로 전환한다 대시보드 차트에서 30일 이상을 프론트엔드에서 임의 일수로 다시 묶으면 주간의 일요일~토요일 기준, 이번 주·이번 달·올해의 부분 집계 기준을 명확히 맞추기 어렵다. 따라서 `GET /admin/api/analytics/summary`가 `period`를 받아 일간·주간·월간·년간 버킷을 서버에서 직접 만들고, 화면은 응답된 `startDay`·`endDay`·`label`을 그대로 표시한다. 년간은 빈 과거 연도를 과하게 보여 주지 않도록 실제 집계 시작 연도부터 올해까지 최대 8년만 표시한다. diff --git a/docs/map.md b/docs/map.md index 5f599ee..fd0e0f1 100644 --- a/docs/map.md +++ b/docs/map.md @@ -101,7 +101,7 @@ | components/site/SiteAdSlot.vue | 사이트 설정 Ads HTML 코드 렌더링, 메인 피드·메인 인피드·오른쪽 사이드·게시물 오른쪽 사이드·게시물 본문 상단·인아티클·하단 광고 슬롯 | | components/site/MainColumn.vue | 메인 화면 중앙, `lg:max-w-[720px]`로 본문 상한 | | components/site/HomeHero.vue | 홈 상단 720px 커버 배너, 라이트·다크 테마별 이미지 교체, 왼쪽 하단 오버레이 제목·본문 | -| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 메타에 표시 | +| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 메타에 표시하고 태그 페이지 링크 제공 | | components/site/PostCardMedia.vue | 게시물 카드 썸네일(`featuredImageThumbnail` 우선, 없으면 대표 이미지 원본 사용. 대표 이미지 없으면 모바일 3줄·`sm` 이상 4줄 제목 placeholder 표시) | | components/site/TagHeader.vue | 태그 페이지 헤더 | | components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시, 입력값 기반 등록 버튼 활성화, 작성자 썸네일/좋아요/상대시간 표시 | @@ -169,12 +169,12 @@ |------|------| | pages/admin/index.vue | 대시보드(상단 요약: 현재 접속자·오늘 접속자·게시물 수, 일일·주간·월간·년간 선택형 방문자수·평균 체류·50% 스크롤 차트, 차트 막대 클릭 날짜·구간 상세 모달, 방문자 유입 정보·디바이스·유입 키워드, 접속자 목록, 인기 게시물 월간 조회수·작성일, 인기 페이지 참여 지표) | | pages/admin/login.vue | 관리자 로그인, 일반 로그인과 같은 다크 인증 스타일·우측 배치 및 내부 오른쪽 정렬 폼, 이메일·비밀번호 모두 입력 시에만 제출 버튼 활성 | -| pages/admin/posts/index.vue | 글 목록, 헤더 우측에 검색·필터(상태·태그·추천·정렬)와 «새 글»을 한 줄 배치, 예약/초안/발행/멤버십/비공개 텍스트 상태, 추천 표시와 제목 사이 대표 이미지 썸네일, 제목 옆 댓글 수, 별도 조회수 열의 누적 조회수·통계 모달 버튼, 적용 태그 전체 색상 배지, 화면 기준 행 more vert 메뉴(추천·삭제) | +| pages/admin/posts/index.vue | 글 목록, 헤더 우측에 검색·필터(상태·태그·추천·정렬)와 «새 글»을 한 줄 배치, 예약/초안/공개/멤버십/비공개 텍스트 상태, 추천 표시와 제목 사이 대표 이미지 썸네일, 제목 옆 댓글 수, 제목 아래 주소·공개 보기 hover 아이콘, 별도 조회수 열의 누적 조회수·통계 모달 버튼, 적용 태그 전체 색상 배지, 화면 기준 행 more vert 메뉴(추천·삭제) | | pages/admin/posts/new.vue | 글 작성, Ghost 스타일 작성 폼, 초안 `POST` 디바운스 자동 저장·이탈 직전 플러시, 저장 토스트 | | pages/admin/posts/[id].vue | 글 수정, `AdminPostForm`, 저장·초안 디바운스 자동 저장(`autoSaving`)·이탈 직전 플러시·삭제·토스트 | | pages/admin/posts/[id]/analytics.vue | 게시물별 통계, 누적 조회수·기간별 조회/방문자 차트·유입/디바이스/키워드·읽음/체류/스크롤 지표 | | pages/admin/posts/preview.vue | 글 미리보기(공개 상세와 동일한 중앙 컬럼·수평 패딩) | -| pages/admin/pages/index.vue | 페이지 목록, 조회수·통계 모달, 상태 표시, 화면 기준 행 more vert 메뉴(수정·삭제) | +| pages/admin/pages/index.vue | 페이지 목록, 제목 아래 주소·공개 보기 hover 아이콘, 조회수·통계 모달, 상태 표시, 화면 기준 행 more vert 메뉴(수정·삭제) | | pages/admin/pages/new.vue | 전체 화면 페이지 작성, HTML 문서 기본 모드 저장, 저장 토스트 | | pages/admin/pages/[id].vue | 전체 화면 페이지 수정, HTML 문서/일반 텍스트 모드 저장, 설정 패널 삭제, 저장/삭제 토스트 | | pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/카드 썸네일/프로필 이미지** 탭, 글·멤버 목록과 같은 검색창, 파일 직접 추가, 모든 탭 공통 전체/미사용 배지형 필터, 목록 카드 오른쪽 위 사용 개수 배지, 현재 필터 결과의 삭제 가능 항목 전체 선택·선택 삭제, 이미지·비디오·오디오·파일 종류 필터, 원본 fallback·카드 썸네일 사용 배지(카드 썸네일 탭의 카드 배지는 숨김), 대표 이미지 원본 카드 썸네일 생성·재생성, 비디오 프레임 썸네일, 폴더 트리 more vert(폴더 삭제), 검색·드래그·상세 모달 등 | @@ -194,13 +194,13 @@ | 파일 | 화면 | |------|------| -| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 태그 배지 표시, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 | -| pages/posts/index.vue | 게시물 전체 목록, 태그는 있을 때만 카드 메타에 표시 | +| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 링크 배지 표시, 카드 오른쪽 아래 화살표는 게시물 상세 링크, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 | +| pages/posts/index.vue | 게시물 전체 목록, 태그는 있을 때만 카드 메타에 링크로 표시 | | pages/posts/[slug].vue | `/post/:slug` 리다이렉트 | | pages/post/[slug].vue | 블로그 글 상세, 첫 태그가 있을 때만 메타 행에 태그 링크, H1~H3 본문 제목을 오른쪽 TOC 상태로 제공, 공개 회원 로그인 세션의 회원이 글쓴이(`author_id`)이면 공유 버튼 옆 새 탭 편집 링크 표시, 게시물 SEO/OG 메타 출력(요약 없으면 본문 fallback), 공유 모달(X/Bluesky/Facebook/LinkedIn/Email/링크복사), 회원 댓글 섹션 | | pages/tags/index.vue | 태그 전체 목록, 중앙 히어로와 3열 태그 카드, 좌측 컬러 보더/hover 오버레이 | | pages/tags/[slug].vue | `/tag/:slug` 리다이렉트 | -| pages/tag/[slug].vue | 태그별 글 목록, 태그 헤더 이미지가 있으면 상단 이미지 헤더 표시, 없으면 기본 태그명·설명 + 공통 섹션 패딩을 쓰는 리스트형 게시물 카드 | +| pages/tag/[slug].vue | 태그별 글 목록, 태그 헤더 이미지가 있으면 상단 이미지 헤더 표시, 없으면 기본 태그명·설명 + 공통 섹션 패딩을 쓰는 리스트형 게시물 카드, 카드 태그 배지와 오른쪽 아래 화살표 링크 | | pages/pages/[slug].vue | 고정 페이지 상세, HTML 문서 모드는 직접 진입 시 서버 미들웨어 원문 응답·클라이언트 내부 이동 시 해당 URL 재진입 | | pages/signup.vue | 회원가입 3단계, `emailOtpConfigured`일 때 이메일 OTP·인증번호 받기, 모바일 OTP 입력창 높이 보정, `POST /api/auth/email-otp/request` | | pages/signin.vue | 로그인, `/forgot-password` 링크 | @@ -296,6 +296,7 @@ | server/utils/content-schema.js | Zod 콘텐츠 스키마 | | server/utils/sample-content.js | 샘플 콘텐츠 저장소 | | server/utils/admin-auth.js | 관리자 세션 쿠키 인증 유틸리티 | +| server/utils/analytics-admin-exclusion.js | 관리자 세션 요청의 공개 통계 수집 제외 판별 | | server/utils/member-auth.js | 회원 세션 쿠키 인증 유틸리티 | | server/utils/request-ip.js | 프록시 헤더 포함 요청 IP 조회 유틸리티 | | server/utils/member-avatar.js | 회원 전용 썸네일 경로 검증·프로필 분리 시 `media_metadata`만 제거(디스크는 관리자 미디어에서 정리) | @@ -316,12 +317,12 @@ | 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/utils/analytics-pageview-input.js | `POST /api/analytics/pageview` 검증·관리자 세션 제외·기록 | | server/utils/zip-writer.js | 게시물 Export ZIP 생성 유틸리티 | | server/utils/zip-reader.js | 게시물 Import ZIP 읽기 유틸리티 | | server/api/analytics/pageview.post.js | 공개 통계 수집 API | | server/api/analytics/heartbeat.post.js | 공개 heartbeat·체류·스크롤 수집 API | -| server/utils/analytics-heartbeat-input.js | heartbeat 검증·기록 | +| server/utils/analytics-heartbeat-input.js | heartbeat 검증·관리자 세션 제외·기록 | | server/routes/admin/api/analytics/summary.get.js | 관리자 통계 요약 API | | server/routes/admin/api/analytics/posts.get.js | 관리자 인기 게시물 API | | server/routes/admin/api/analytics/posts/[id].get.js | 관리자 게시물별 통계 API | diff --git a/docs/spec.md b/docs/spec.md index 7b74c4c..74b38a8 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -81,6 +81,7 @@ - 댓글 정렬은 `인기순`(좋아요 우선), `최신순`, `오래된순`을 제공한다. - 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다. - 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성 +- 공개 게시물 오른쪽 목차는 H1~H3 제목 표시 텍스트에서 링크·굵게·기울임·인라인 코드 같은 마크다운 장식만 제거하고 `~` 같은 일반 제목 문자는 보존한다. - 표준 마크다운 표(`| 헤더 | ... |`, `| --- | ... |`)는 본문에서 가로 스크롤 가능한 HTML table로 렌더링한다. 정렬 구분선(`:---`, `:---:`, `---:`)은 각각 좌/중앙/우 정렬로 반영한다. - 인용문(`>`)은 왼쪽 세로 막대형 기본 스타일로 표시한다. 기본 인용 텍스트는 라이트·다크 모드 모두 사이트 본문 텍스트 색상(`--site-text`)을 따른다. 첫 줄 옵션 `> [!bg=blue]` 또는 `> {bg=blue}`는 인용 막대 색상으로 반영하며, 지원 값은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이다. - 관리자 Markdown-first 글쓰기의 오른쪽 블록 설정 패널은 인용·콜아웃·코드 블록·토글 설정과 선택 텍스트 색상 설정을 지원한다. 콜아웃은 제목·아이콘 표시 여부·아이콘·배경색, 코드 블록은 언어·줄번호 표시 여부, 토글은 기본 펼침·닫힘 상태를 선언 줄에 저장한다. 코드 블록 줄번호는 기본 숨김이며, 켠 경우에만 선언 줄에 `line-numbers`를 저장한다. 일반 텍스트 범위를 선택하면 텍스트 색상 패널이 열리고 회색·파랑·초록·노랑·빨강·보라 인라인 색상을 적용한다. 콜아웃 아이콘은 라이브·공개 화면 모두 왼쪽 상단에 배치하고, 아이콘·제목 헤더 아래에 본문을 줄바꿈해 표시한다. 아이콘 미사용 시 자리 표시자를 남기지 않는다. 라이브 문단에서는 `>` 입력으로 인용, ``` Enter로 코드 블록, `!!!` Enter로 콜아웃을 만들 수 있고, `/표` 또는 `/table` 슬래시 명령으로 기본 3열 표 마크다운을 삽입한다. `/html`은 `:::html`, `/link`는 `:::link`, `/iframe`은 `:::iframe` fenced 블록을 삽입한다. 소스·라이브 모드 모두 `Cmd/Ctrl+K`로 링크 마크다운을 삽입한다. 에디터 되돌리기·다시 실행은 본문 값과 함께 선택 위치를 복원해 줄 앞 이동을 줄인다. 라이브 모드에서 위·아래 방향키로 인접 블록을 이동하면 커서가 화면 밖으로 벗어날 때 편집 스크롤 영역도 함께 이동한다. 라이브 코드·인용·콜아웃·토글 블록은 맨 위/맨 아래 방향키로 외부 기본 문단을 만들며 빠져나올 수 있고, 인용 첫 글자 앞 Backspace는 일반 문단으로 되돌린다. 한글 등 IME 조합 입력 중에는 줄바꿈 직후 블록 판별이 일시적으로 비어도 마지막 블록 컨텍스트를 유지해 설정 패널이 닫히지 않게 한다. @@ -90,7 +91,7 @@ - 공유 모달은 게시물 썸네일/제목/요약 미리보기, X/Bluesky/Facebook/LinkedIn/Email 링크, 링크 복사 액션을 제공한다. - 공유·SEO 설명은 SEO 설명이 있으면 우선 사용하고, 없으면 게시물 요약, 요약도 없으면 본문에서 마크다운 기호를 제거한 짧은 텍스트를 사용한다. - 공개 페이지 `- /pages/{{ page.slug }} -
+- /post/{{ post.slug }} -
+