게시물 작성 통계 표시 추가

This commit is contained in:
2026-06-05 16:54:41 +09:00
parent 629ef8c4c6
commit ccb6db5f89
9 changed files with 125 additions and 7 deletions

View File

@@ -85,6 +85,8 @@ const { data: adminTags } = useFetch('/admin/api/tags', {
}) })
const defaultTagColor = '#15171a' const defaultTagColor = '#15171a'
/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
const READING_CHARS_PER_MINUTE = 600
/** /**
* ISO 날짜를 datetime-local 입력값으로 변환 * ISO 날짜를 datetime-local 입력값으로 변환
@@ -165,6 +167,84 @@ const form = reactive({
tagsText: props.initialPost.tags?.join(', ') || '' tagsText: props.initialPost.tags?.join(', ') || ''
}) })
/**
* 숫자를 한국어 로케일 문자열로 변환한다.
* @param {number} value - 숫자 값
* @returns {string} 표시 문자열
*/
const formatStatNumber = (value) => Number(value || 0).toLocaleString('ko-KR')
/**
* 통계 계산용 마크다운 텍스트를 정리한다.
* @param {string} value - 마크다운 본문
* @returns {string} 표시 텍스트에 가까운 문자열
*/
const normalizePostStatisticsText = (value) => {
return String(value || '')
.replace(/<!--sori:blank-paragraph-->/g, ' ')
.replace(/^```.*$/gm, ' ')
.replace(/^:::\w*.*$/gm, ' ')
.replace(/^> ?(?:\[![^\]]+\]|\{[^}]+\})$/gm, ' ')
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1 ')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[`*_~>#-]/g, ' ')
}
/**
* 본문 단어 수를 계산한다.
* @param {string} value - 통계용 텍스트
* @returns {number} 단어 수
*/
const countPostWords = (value) => {
const tokens = String(value || '').trim().match(/\S+/g)
return tokens ? tokens.length : 0
}
/**
* 본문 블록 수를 계산한다.
* @param {string} value - 마크다운 본문
* @returns {number} 블록 수
*/
const countPostBlocks = (value) => {
const lines = String(value || '').split('\n')
return lines.filter((line) => {
const trimmed = line.trim()
return trimmed
&& trimmed !== ':::'
&& trimmed !== '```'
&& trimmed !== '<!--sori:blank-paragraph-->'
}).length
}
/**
* 본문 이미지 수를 계산한다.
* @param {string} value - 마크다운 본문
* @returns {number} 이미지 수
*/
const countPostImages = (value) => {
const matches = String(value || '').match(/!\[[^\]]*]\([^)]+\)/g)
return matches ? matches.length : 0
}
/**
* 게시물 작성 통계를 계산한다.
* @returns {{ words: number, characters: number, spaces: number, readingMinutes: number, blocks: number, images: number }} 통계 값
*/
const postWritingStats = computed(() => {
const visibleText = normalizePostStatisticsText(form.content)
const characters = Array.from(visibleText.replace(/\s/g, '')).length
const spaces = (visibleText.match(/\s/g) || []).length
return {
words: countPostWords(visibleText),
characters,
spaces,
readingMinutes: characters > 0 ? Math.max(1, Math.ceil(characters / READING_CHARS_PER_MINUTE)) : 0,
blocks: countPostBlocks(form.content),
images: countPostImages(form.content)
}
})
/** /**
* 서버에 반영된 게시 형태(툴바·자동 저장·슬러그 자동 연동 분기) * 서버에 반영된 게시 형태(툴바·자동 저장·슬러그 자동 연동 분기)
* @param {Object} post - 게시물 * @param {Object} post - 게시물
@@ -1862,8 +1942,27 @@ defineExpose({
</span> </span>
</label> </label>
</div> </div>
<div v-if="showDelete" class="admin-post-form__settings-bottom shrink-0 border-t border-[#e3e6e8] px-8 py-6"> <div class="admin-post-form__settings-bottom grid shrink-0 gap-4 border-t border-[#e3e6e8] px-8 py-6">
<section class="admin-post-form__writing-stats grid gap-2 text-xs text-[#657080]" aria-label="본문 통계">
<div class="admin-post-form__writing-stats-row flex items-center justify-between gap-3">
<span class="admin-post-form__writing-stats-label">단어</span>
<span class="admin-post-form__writing-stats-value font-medium text-[#394047]">{{ formatStatNumber(postWritingStats.words) }}</span>
</div>
<div class="admin-post-form__writing-stats-row flex items-center justify-between gap-3">
<span class="admin-post-form__writing-stats-label">문자</span>
<span class="admin-post-form__writing-stats-value font-medium text-[#394047]">{{ formatStatNumber(postWritingStats.characters) }} <span class="text-[#8e9cac]">({{ formatStatNumber(postWritingStats.spaces) }} 공백)</span></span>
</div>
<div class="admin-post-form__writing-stats-row flex items-center justify-between gap-3">
<span class="admin-post-form__writing-stats-label">읽기 시간</span>
<span class="admin-post-form__writing-stats-value font-medium text-[#394047]">{{ postWritingStats.readingMinutes ? `${formatStatNumber(postWritingStats.readingMinutes)}` : '0분' }}</span>
</div>
<div class="admin-post-form__writing-stats-row flex items-center justify-between gap-3">
<span class="admin-post-form__writing-stats-label">구성</span>
<span class="admin-post-form__writing-stats-value font-medium text-[#394047]">블록 {{ formatStatNumber(postWritingStats.blocks) }} · 이미지 {{ formatStatNumber(postWritingStats.images) }}</span>
</div>
</section>
<button <button
v-if="showDelete"
class="admin-post-form__delete-post flex h-10 w-full items-center justify-center gap-2 rounded border border-[#d7dde2] bg-white text-sm font-bold text-[#394047] transition-colors hover:border-[#d21a26] hover:bg-red-50 hover:text-[#d21a26] disabled:opacity-50" class="admin-post-form__delete-post flex h-10 w-full items-center justify-center gap-2 rounded border border-[#d7dde2] bg-white text-sm font-bold text-[#394047] transition-colors hover:border-[#d21a26] hover:bg-red-50 hover:text-[#d21a26] disabled:opacity-50"
type="button" type="button"
:disabled="deleting" :disabled="deleting"

View File

@@ -1,5 +1,9 @@
# 업데이트 요약 # 업데이트 요약
## v1.5.75
- 게시물 작성 화면에서 단어 수, 문자 수, 공백 수, 읽기 시간, 블록 수, 이미지 수를 확인할 수 있게 했다.
## v1.5.70 ## v1.5.70
- 라이브 모드 마지막 줄에서 `!!!`로 콜아웃을 만들 때 본문 줄을 안정적으로 확보하도록 수정했다. - 라이브 모드 마지막 줄에서 `!!!`로 콜아웃을 만들 때 본문 줄을 안정적으로 확보하도록 수정했다.

View File

@@ -1,6 +1,6 @@
# 배포 가이드 # 배포 가이드
> 로컬 기준 v1.5.74에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. > 로컬 기준 v1.5.75에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형 ## 빌드 유형
@@ -16,6 +16,12 @@
## 로컬 개발 ## 로컬 개발
### v1.5.75 참고
- 추가 DB 마이그레이션은 없다.
- 게시물 작성·수정 화면 오른쪽 설정 패널 하단에 본문 통계가 표시되는지 확인한다.
- 본문 입력 시 단어 수, 공백 제외 문자 수, 공백 수, 읽기 시간, 블록 수, 이미지 수가 갱신되는지 확인한다.
### v1.5.74 참고 ### v1.5.74 참고
- DB 마이그레이션 `053_site_settings_post_sidebar_ad.sql` 적용 필요. - DB 마이그레이션 `053_site_settings_post_sidebar_ad.sql` 적용 필요.

View File

@@ -1,5 +1,9 @@
# 의사결정 이력 # 의사결정 이력
## 2026-06-05 v1.5.75 — 작성 통계는 광고 기준과 같은 문자 기준을 보여준다
게시물 인아티클 광고가 본문 길이를 기준으로 노출되므로 작성 화면에서도 길이 감각을 바로 확인할 수 있어야 한다. 작성 흐름을 방해하지 않도록 본문 위에 떠 있는 표시 대신 오른쪽 설정 패널 하단에 작은 통계 영역을 두고, 문자 수는 광고 기준과 같은 공백 제외 값을 우선 표시한다. 단어 수와 읽기 시간, 블록·이미지 수는 글의 분량과 구성을 빠르게 파악하는 보조 정보로 함께 둔다.
## 2026-06-05 v1.5.74 — 게시물 상세 광고는 목차와 분리한다 ## 2026-06-05 v1.5.74 — 게시물 상세 광고는 목차와 분리한다
게시물 상세 오른쪽 사이드바는 본문 목차가 핵심 기능이므로 공통 사이드 광고와 함께 두면 폭과 클릭 맥락이 어색해질 수 있다. 따라서 공통 오른쪽 사이드 광고는 게시물 상세가 아닌 화면에만 유지하고, 게시물 상세에는 별도 왼쪽 사이드 광고 슬롯을 둔다. 인아티클 광고는 상단·하단 광고와 함께 쓰이므로 짧은 글에서는 생략하고, 충분히 긴 글에서만 본문 흐름을 해치지 않도록 1회 또는 최대 2회로 제한한다. 게시물 상세 오른쪽 사이드바는 본문 목차가 핵심 기능이므로 공통 사이드 광고와 함께 두면 폭과 클릭 맥락이 어색해질 수 있다. 따라서 공통 오른쪽 사이드 광고는 게시물 상세가 아닌 화면에만 유지하고, 게시물 상세에는 별도 왼쪽 사이드 광고 슬롯을 둔다. 인아티클 광고는 상단·하단 광고와 함께 쓰이므로 짧은 글에서는 생략하고, 충분히 긴 글에서만 본문 흐름을 해치지 않도록 1회 또는 최대 2회로 제한한다.

View File

@@ -94,7 +94,7 @@
| components/admin/AdminAdsSettingsCard.vue | 관리자 사이트 설정의 위치별 Ads 코드 카드(메인 피드·메인 인피드·오른쪽 사이드·게시물 왼쪽 사이드·게시물 본문 상단·인아티클·하단) | | components/admin/AdminAdsSettingsCard.vue | 관리자 사이트 설정의 위치별 Ads 코드 카드(메인 피드·메인 인피드·오른쪽 사이드·게시물 왼쪽 사이드·게시물 본문 상단·인아티클·하단) |
| components/admin/AdminPostExportFileRow.vue | 관리자 사이트 설정 내보내기 작업의 분할 파일 선택 행 | | components/admin/AdminPostExportFileRow.vue | 관리자 사이트 설정 내보내기 작업의 분할 파일 선택 행 |
| components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 | | components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 |
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·발행·예약·멤버십·비공개 상태 저장, 발행 모달(중앙 배치), 좌우 설정 패널(작은 화면은 오른쪽 고정 오버레이), 오른쪽 `View Post` 링크, 미리보기 emit·미저장 이탈 가드, 추천 글 토글, 태그 색상 배지 다중 입력·메인 태그 드롭다운·부분 검색 추천 | | components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·발행·예약·멤버십·비공개 상태 저장, 발행 모달(중앙 배치), 좌우 설정 패널(작은 화면은 오른쪽 고정 오버레이), 오른쪽 `View Post` 링크, 오른쪽 하단 본문 통계(단어·문자·공백·읽기 시간·블록·이미지), 미리보기 emit·미저장 이탈 가드, 추천 글 토글, 태그 색상 배지 다중 입력·메인 태그 드롭다운·부분 검색 추천 |
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 페이지 공개 상태 선택, HTML 문서 기본 모드, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입 | | components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 페이지 공개 상태 선택, HTML 문서 기본 모드, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입 |
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령·미디어 모달(이미지·갤러리·비디오·오디오·파일), 커서 블록 컨텍스트·`block-panel` emit, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭 닫기·미디어 모달 중 유지, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연 | | components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령·미디어 모달(이미지·갤러리·비디오·오디오·파일), 커서 블록 컨텍스트·`block-panel` emit, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭 닫기·미디어 모달 중 유지, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연 |
| components/admin/AdminEditorBlockPanel.vue | 게시물 설정 사이드바 오버레이 블록 설정(이미지·갤러리·임베드·인용 배경색·콜아웃 제목·아이콘·배경색·코드·토글), 갤러리 선택 이미지 강조 | | components/admin/AdminEditorBlockPanel.vue | 게시물 설정 사이드바 오버레이 블록 설정(이미지·갤러리·임베드·인용 배경색·콜아웃 제목·아이콘·배경색·코드·토글), 갤러리 선택 이미지 강조 |

View File

@@ -157,7 +157,7 @@ layouts/
- 게시글 메뉴 라벨은 `게시글`로 표시하고, 우측 `+` 아이콘은 `/admin/posts/new`로 바로 이동한다. - 게시글 메뉴 라벨은 `게시글`로 표시하고, 우측 `+` 아이콘은 `/admin/posts/new`로 바로 이동한다.
- 관리자 글 목록의 태그 컬럼은 게시물 태그 배열의 첫 번째 항목만 대표 태그로 표시하며, 배지는 태그 고유 색상을 반영한다. - 관리자 글 목록의 태그 컬럼은 게시물 태그 배열의 첫 번째 항목만 대표 태그로 표시하며, 배지는 태그 고유 색상을 반영한다.
- 관리자 글쓰기의 Tags 입력은 배지형 다중 입력을 유지하며, 오른쪽 트리거로 메인 태그를 드롭다운에서 추가할 수 있다. 입력 중에는 기존 태그 이름·슬러그 부분 일치 결과를 추천하고, 방향키와 Enter로 선택할 수 있다. 선택된 태그 배지는 태그 고유 색상을 반영한다. - 관리자 글쓰기의 Tags 입력은 배지형 다중 입력을 유지하며, 오른쪽 트리거로 메인 태그를 드롭다운에서 추가할 수 있다. 입력 중에는 기존 태그 이름·슬러그 부분 일치 결과를 추천하고, 방향키와 Enter로 선택할 수 있다. 선택된 태그 배지는 태그 고유 색상을 반영한다.
- 관리자 글쓰기 상단 왼쪽 상태 영역은 `Published`, `Scheduled`, `Members`, `Private`, 초안 저장 상태 등 현재 상태 텍스트만 표시하며, 공개 게시물 이동은 오른쪽 설정 패널의 `View Post` 링크에서만 제공한다. - 관리자 글쓰기 상단 왼쪽 상태 영역은 `Published`, `Scheduled`, `Members`, `Private`, 초안 저장 상태 등 현재 상태 텍스트만 표시하며, 공개 게시물 이동은 오른쪽 설정 패널의 `View Post` 링크에서만 제공한다. 오른쪽 설정 패널 하단에는 본문 기준 단어 수, 공백 제외 문자 수와 공백 수, 예상 읽기 시간, 블록 수, 이미지 수를 작은 통계로 표시한다.
- 관리자 미디어 검색창은 글·멤버 목록과 같은 돋보기 아이콘 포함 입력 스타일을 사용한다. 미디어 라이브러리 탭에서는 파일 추가 버튼으로 `/admin/api/uploads`에 직접 업로드할 수 있고, 현재 폴더를 보고 있으면 업로드 후 해당 폴더로 배치한다. 전체 선택은 현재 검색·필터 결과만 대상으로 하며, 선택 삭제는 사용 중이거나 회원 프로필에 연결된 잠금 항목을 제외하고 삭제한다. - 관리자 미디어 검색창은 글·멤버 목록과 같은 돋보기 아이콘 포함 입력 스타일을 사용한다. 미디어 라이브러리 탭에서는 파일 추가 버튼으로 `/admin/api/uploads`에 직접 업로드할 수 있고, 현재 폴더를 보고 있으면 업로드 후 해당 폴더로 배치한다. 전체 선택은 현재 검색·필터 결과만 대상으로 하며, 선택 삭제는 사용 중이거나 회원 프로필에 연결된 잠금 항목을 제외하고 삭제한다.
- 메뉴 관리 항목은 `네비게이션`으로 표시한다. - 메뉴 관리 항목은 `네비게이션`으로 표시한다.
- 게시글·페이지·태그·미디어·네비게이션·멤버·설정 메뉴는 전용 SVG 아이콘을 사용한다. - 게시글·페이지·태그·미디어·네비게이션·멤버·설정 메뉴는 전용 SVG 아이콘을 사용한다.

View File

@@ -1,5 +1,10 @@
# 업데이트 이력 # 업데이트 이력
## v1.5.75
- 게시물 글쓰기: 오른쪽 설정 패널 하단에 단어 수·공백 제외 문자 수·공백 수·예상 읽기 시간·블록 수·이미지 수 통계 추가.
- 게시물 글쓰기: 본문 통계를 삭제 버튼 위의 작은 정보 영역으로 표시하도록 추가.
## v1.5.74 ## v1.5.74
- 사이트 설정 Ads: 게시물 왼쪽 사이드 광고 슬롯 추가. - 사이트 설정 Ads: 게시물 왼쪽 사이드 광고 슬롯 추가.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "1.5.74", "version": "1.5.75",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sori.studio", "name": "sori.studio",
"version": "1.5.74", "version": "1.5.75",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "1.5.74", "version": "1.5.75",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "imports": {