From 1ec71cc985f0e8c015adcb2290409f438b111152 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 24 Jul 2026 18:07:22 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EB=AF=B8?= =?UTF-8?q?=EB=94=94=EC=96=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminMarkdownEditor.vue | 55 ++++-------------- docs/map.md | 2 +- docs/update.md | 5 ++ lib/markdown-editor-media.js | 74 ++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- tests/markdown-editor-media.test.js | 52 +++++++++++++++++ 7 files changed, 147 insertions(+), 47 deletions(-) diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index a04e0fa..5e0a86d 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -22,6 +22,8 @@ import { getEditorHistoryEntryValue } from '../../lib/markdown-editor-history.js' import { + buildMediaPickerPageItems, + buildMediaPickerRangeLabel, createMediaBlockMarkdown, findUploadSizeLimitError, getGallerySelectionOrder as getGallerySelectionOrderFromSelection, @@ -3075,52 +3077,19 @@ const filteredMediaItems = computed(() => mediaItems.value.filter(isMediaItemSel const mediaPickerPageCount = computed(() => Math.max(1, Number(mediaPickerPagination.value.pageCount) || 1)) const mediaPickerRangeLabel = computed(() => { - const total = Number(mediaPickerPagination.value.total) || 0 - - if (total <= 0) { - return '0개' - } - - const page = Math.min(Math.max(Number(mediaPickerPagination.value.page) || mediaPickerPage.value, 1), mediaPickerPageCount.value) - const pageSize = Number(mediaPickerPagination.value.pageSize) || mediaPickerPageSize - const start = ((page - 1) * pageSize) + 1 - const end = Math.min(start + pageSize - 1, total) - - return `${total}개 중 ${start}-${end}개` + return buildMediaPickerRangeLabel({ + pagination: mediaPickerPagination.value, + currentPage: mediaPickerPage.value, + pageCount: mediaPickerPageCount.value, + defaultPageSize: mediaPickerPageSize + }) }) const mediaPickerPageItems = computed(() => { - const total = mediaPickerPageCount.value - const current = Math.min(Math.max(Number(mediaPickerPage.value) || 1, 1), total) - const pages = new Set([1, total, current]) - - for (let page = current - 2; page <= current + 2; page += 1) { - pages.add(page) - } - - if (current <= 3) { - pages.add(2) - pages.add(3) - pages.add(4) - } - - if (current >= total - 2) { - pages.add(total - 3) - pages.add(total - 2) - pages.add(total - 1) - } - - const orderedPages = [...pages].filter((page) => page >= 1 && page <= total).sort((left, right) => left - right) - return orderedPages.reduce((items, page, index) => { - const previousPage = orderedPages[index - 1] - - if (previousPage && page - previousPage > 1) { - items.push(`ellipsis-${previousPage}-${page}`) - } - - items.push(page) - return items - }, []) + return buildMediaPickerPageItems({ + currentPage: mediaPickerPage.value, + pageCount: mediaPickerPageCount.value + }) }) /** diff --git a/docs/map.md b/docs/map.md index 4f82d61..52780d8 100644 --- a/docs/map.md +++ b/docs/map.md @@ -48,7 +48,7 @@ | lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` | | lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 | | lib/markdown-editor-history.js | 관리자 Markdown-first 에디터 undo/redo 히스토리 항목 생성, 레거시 문자열 항목 본문 추출, 선택 범위 보정 | -| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, fenced 미디어 블록 생성 | +| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, 페이지 범위·페이지 번호 계산, fenced 미디어 블록 생성 | | lib/markdown-block-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 | | lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 | | lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 | diff --git a/docs/update.md b/docs/update.md index 250f486..fecfd52 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.72 + +- 관리자 Markdown 에디터 미디어 선택 모달의 페이지 범위 문구와 페이지 번호·말줄임 항목 계산을 공통 helper로 분리. +- 미디어 선택 페이지네이션 helper 단위 테스트 추가. + ## v1.11.71 - 관리자 Markdown 에디터 미디어 업로드 API 오류 메시지 변환과 파일 개수·총량·개별 크기 한도 검증 로직을 공통 helper로 분리. diff --git a/lib/markdown-editor-media.js b/lib/markdown-editor-media.js index 91f43f7..a0c37ca 100644 --- a/lib/markdown-editor-media.js +++ b/lib/markdown-editor-media.js @@ -301,3 +301,77 @@ export const findUploadSizeLimitError = ({ return null } + +/** + * 미디어 선택 모달의 페이지 범위 표시 문구를 만든다. + * @param {Object} options - 범위 표시 옵션 + * @param {Object} options.pagination - 서버 페이지네이션 응답 + * @param {number} options.currentPage - 현재 페이지 fallback + * @param {number} options.pageCount - 전체 페이지 수 + * @param {number} options.defaultPageSize - 기본 페이지 크기 + * @returns {string} 범위 표시 문구 + */ +export const buildMediaPickerRangeLabel = ({ + pagination, + currentPage, + pageCount, + defaultPageSize +}) => { + const total = Number(pagination?.total) || 0 + + if (total <= 0) { + return '0개' + } + + const normalizedPageCount = Math.max(1, Number(pageCount) || 1) + const page = Math.min(Math.max(Number(pagination?.page) || currentPage, 1), normalizedPageCount) + const pageSize = Number(pagination?.pageSize) || defaultPageSize + const start = ((page - 1) * pageSize) + 1 + const end = Math.min(start + pageSize - 1, total) + + return `${total}개 중 ${start}-${end}개` +} + +/** + * 미디어 선택 모달 페이지네이션 항목을 만든다. + * @param {Object} options - 페이지 항목 옵션 + * @param {number} options.currentPage - 현재 페이지 + * @param {number} options.pageCount - 전체 페이지 수 + * @returns {Array} 페이지 번호와 ellipsis 키 목록 + */ +export const buildMediaPickerPageItems = ({ + currentPage, + pageCount +}) => { + const total = Math.max(1, Number(pageCount) || 1) + const current = Math.min(Math.max(Number(currentPage) || 1, 1), total) + const pages = new Set([1, total, current]) + + for (let page = current - 2; page <= current + 2; page += 1) { + pages.add(page) + } + + if (current <= 3) { + pages.add(2) + pages.add(3) + pages.add(4) + } + + if (current >= total - 2) { + pages.add(total - 3) + pages.add(total - 2) + pages.add(total - 1) + } + + const orderedPages = [...pages].filter((page) => page >= 1 && page <= total).sort((left, right) => left - right) + return orderedPages.reduce((items, page, index) => { + const previousPage = orderedPages[index - 1] + + if (previousPage && page - previousPage > 1) { + items.push(`ellipsis-${previousPage}-${page}`) + } + + items.push(page) + return items + }, []) +} diff --git a/package-lock.json b/package-lock.json index b8fac7d..3e484a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.71", + "version": "1.11.72", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.71", + "version": "1.11.72", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 7fbf627..a98f010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.71", + "version": "1.11.72", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-editor-media.test.js b/tests/markdown-editor-media.test.js index ac62ffb..6a8307d 100644 --- a/tests/markdown-editor-media.test.js +++ b/tests/markdown-editor-media.test.js @@ -1,6 +1,8 @@ import assert from 'node:assert/strict' import test from 'node:test' import { + buildMediaPickerPageItems, + buildMediaPickerRangeLabel, createMediaBlockMarkdown, findUploadSizeLimitError, formatMediaFileSize, @@ -185,3 +187,53 @@ test('업로드 파일 한도 초과 메시지를 계산한다', () => { uploadSizeLimits }), null) }) + +test('미디어 선택 페이지 범위 문구를 계산한다', () => { + assert.equal(buildMediaPickerRangeLabel({ + pagination: { + page: 2, + pageSize: 50, + total: 231 + }, + currentPage: 2, + pageCount: 5, + defaultPageSize: 50 + }), '231개 중 51-100개') + + assert.equal(buildMediaPickerRangeLabel({ + pagination: { + page: 99, + pageSize: 50, + total: 231 + }, + currentPage: 99, + pageCount: 5, + defaultPageSize: 50 + }), '231개 중 201-231개') + + assert.equal(buildMediaPickerRangeLabel({ + pagination: { + total: 0 + }, + currentPage: 1, + pageCount: 1, + defaultPageSize: 50 + }), '0개') +}) + +test('미디어 선택 페이지 번호와 말줄임 항목을 계산한다', () => { + assert.deepEqual(buildMediaPickerPageItems({ + currentPage: 1, + pageCount: 5 + }), [1, 2, 3, 4, 5]) + + assert.deepEqual(buildMediaPickerPageItems({ + currentPage: 6, + pageCount: 12 + }), [1, 'ellipsis-1-4', 4, 5, 6, 7, 8, 'ellipsis-8-12', 12]) + + assert.deepEqual(buildMediaPickerPageItems({ + currentPage: 12, + pageCount: 12 + }), [1, 'ellipsis-1-9', 9, 10, 11, 12]) +})