diff --git a/components/admin/AdminPostForm.vue b/components/admin/AdminPostForm.vue
index ff4cd75..385f822 100644
--- a/components/admin/AdminPostForm.vue
+++ b/components/admin/AdminPostForm.vue
@@ -85,6 +85,8 @@ const { data: adminTags } = useFetch('/admin/api/tags', {
})
const defaultTagColor = '#15171a'
+/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
+const READING_CHARS_PER_MINUTE = 600
/**
* ISO 날짜를 datetime-local 입력값으로 변환
@@ -165,6 +167,84 @@ const form = reactive({
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(//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 !== ''
+ }).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 - 게시물
@@ -1862,8 +1942,27 @@ defineExpose({
-
+
+
+
+ 단어
+ {{ formatStatNumber(postWritingStats.words) }}개
+
+
+ 문자
+ {{ formatStatNumber(postWritingStats.characters) }}개 ({{ formatStatNumber(postWritingStats.spaces) }} 공백)
+
+
+ 읽기 시간
+ {{ postWritingStats.readingMinutes ? `약 ${formatStatNumber(postWritingStats.readingMinutes)}분` : '0분' }}
+
+
+ 구성
+ 블록 {{ formatStatNumber(postWritingStats.blocks) }}개 · 이미지 {{ formatStatNumber(postWritingStats.images) }}개
+
+