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) }}개 +
+