diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 5e0a86d..bc72fc1 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -25,12 +25,14 @@ import { buildMediaPickerPageItems, buildMediaPickerRangeLabel, createMediaBlockMarkdown, + createDefaultMediaPickerPagination, findUploadSizeLimitError, getGallerySelectionOrder as getGallerySelectionOrderFromSelection, getMediaItemKind, getMediaPickerPreviewUrl, isMediaItemSelectableForTarget, isUploadFileAllowedForMediaTarget, + normalizeMediaPickerResponse, resolveSelectedMediaItems, resolveUploadFetchErrorMessage, toggleMediaSelectionState @@ -119,12 +121,7 @@ const { } = useImeSearchInput(mediaSearchQuery, { debounce: 250 }) const mediaPickerPage = ref(1) const mediaPickerPageSize = 50 -const mediaPickerPagination = ref({ - page: 1, - pageSize: mediaPickerPageSize, - total: 0, - pageCount: 1 -}) +const mediaPickerPagination = ref(createDefaultMediaPickerPagination(mediaPickerPageSize)) const selectedMediaUrls = ref([]) const selectedMediaItemMap = ref({}) const lastSelectionState = ref({ @@ -2985,13 +2982,9 @@ const fetchMediaItems = async () => { } }) - mediaItems.value = response?.items || [] - mediaPickerPagination.value = response?.pagination || { - page: 1, - pageSize: mediaPickerPageSize, - total: mediaItems.value.length, - pageCount: 1 - } + const normalizedResponse = normalizeMediaPickerResponse(response, mediaPickerPageSize) + mediaItems.value = normalizedResponse.items + mediaPickerPagination.value = normalizedResponse.pagination mediaPickerPage.value = mediaPickerPagination.value.page || 1 } finally { isLoadingMedia.value = false @@ -3012,12 +3005,7 @@ const openMediaPicker = async (target) => { activeMediaPickerTab.value = 'upload' mediaSearchQuery.value = '' mediaPickerPage.value = 1 - mediaPickerPagination.value = { - page: 1, - pageSize: mediaPickerPageSize, - total: 0, - pageCount: 1 - } + mediaPickerPagination.value = createDefaultMediaPickerPagination(mediaPickerPageSize) selectedMediaUrls.value = [] selectedMediaItemMap.value = {} isMediaPickerOpen.value = true @@ -3059,12 +3047,7 @@ const closeMediaPicker = () => { mediaSearchQuery.value = '' mediaPickerPage.value = 1 mediaItems.value = [] - mediaPickerPagination.value = { - page: 1, - pageSize: mediaPickerPageSize, - total: 0, - pageCount: 1 - } + mediaPickerPagination.value = createDefaultMediaPickerPagination(mediaPickerPageSize) liveSlashInsertLine.value = null } diff --git a/docs/map.md b/docs/map.md index 52780d8..67ad1c8 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 fecfd52..3f1d00a 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.73 + +- 관리자 Markdown 에디터 미디어 선택 모달의 기본 페이지네이션 상태 생성과 API 응답 정규화 로직을 공통 helper로 분리. +- 미디어 선택 응답 정규화 단위 테스트 추가. + ## v1.11.72 - 관리자 Markdown 에디터 미디어 선택 모달의 페이지 범위 문구와 페이지 번호·말줄임 항목 계산을 공통 helper로 분리. diff --git a/lib/markdown-editor-media.js b/lib/markdown-editor-media.js index a0c37ca..9fae33f 100644 --- a/lib/markdown-editor-media.js +++ b/lib/markdown-editor-media.js @@ -375,3 +375,39 @@ export const buildMediaPickerPageItems = ({ return items }, []) } + +/** + * 미디어 선택 모달의 기본 페이지네이션 상태를 만든다. + * @param {number} pageSize - 기본 페이지 크기 + * @param {number} [total=0] - 전체 항목 수 + * @returns {{ page: number, pageSize: number, total: number, pageCount: number }} 페이지네이션 상태 + */ +export const createDefaultMediaPickerPagination = (pageSize, total = 0) => ({ + page: 1, + pageSize, + total, + pageCount: 1 +}) + +/** + * 미디어 선택 API 응답을 에디터 상태용 목록과 페이지네이션으로 정규화한다. + * @param {Object|null|undefined} response - 미디어 API 응답 + * @param {number} pageSize - 기본 페이지 크기 + * @returns {{ items: Object[], pagination: { page: number, pageSize: number, total: number, pageCount: number } }} 정규화된 응답 + */ +export const normalizeMediaPickerResponse = (response, pageSize) => { + const items = Array.isArray(response?.items) ? response.items : [] + const pagination = response?.pagination && typeof response.pagination === 'object' + ? response.pagination + : createDefaultMediaPickerPagination(pageSize, items.length) + + return { + items, + pagination: { + page: Math.max(1, Number(pagination.page) || 1), + pageSize: Math.max(1, Number(pagination.pageSize) || pageSize), + total: Math.max(0, Number(pagination.total) || 0), + pageCount: Math.max(1, Number(pagination.pageCount) || 1) + } + } +} diff --git a/package-lock.json b/package-lock.json index 3e484a8..93ece9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.72", + "version": "1.11.73", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.72", + "version": "1.11.73", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index a98f010..31a6703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.72", + "version": "1.11.73", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-editor-media.test.js b/tests/markdown-editor-media.test.js index 6a8307d..7dcd446 100644 --- a/tests/markdown-editor-media.test.js +++ b/tests/markdown-editor-media.test.js @@ -4,6 +4,7 @@ import { buildMediaPickerPageItems, buildMediaPickerRangeLabel, createMediaBlockMarkdown, + createDefaultMediaPickerPagination, findUploadSizeLimitError, formatMediaFileSize, getGallerySelectionOrder, @@ -11,6 +12,7 @@ import { getMediaPickerPreviewUrl, isMediaItemSelectableForTarget, isUploadFileAllowedForMediaTarget, + normalizeMediaPickerResponse, resolveSelectedMediaItems, resolveUploadFetchErrorMessage, toggleMediaSelectionState @@ -237,3 +239,69 @@ test('미디어 선택 페이지 번호와 말줄임 항목을 계산한다', () pageCount: 12 }), [1, 'ellipsis-1-9', 9, 10, 11, 12]) }) + +test('미디어 선택 기본 페이지네이션 상태를 만든다', () => { + assert.deepEqual(createDefaultMediaPickerPagination(50), { + page: 1, + pageSize: 50, + total: 0, + pageCount: 1 + }) + + assert.deepEqual(createDefaultMediaPickerPagination(20, 3), { + page: 1, + pageSize: 20, + total: 3, + pageCount: 1 + }) +}) + +test('미디어 선택 API 응답을 상태용 값으로 정규화한다', () => { + assert.deepEqual(normalizeMediaPickerResponse({ + items: [{ url: '/a.png' }], + pagination: { + page: 2, + pageSize: 50, + total: 101, + pageCount: 3 + } + }, 50), { + items: [{ url: '/a.png' }], + pagination: { + page: 2, + pageSize: 50, + total: 101, + pageCount: 3 + } + }) + + assert.deepEqual(normalizeMediaPickerResponse({ + items: [{ url: '/a.png' }] + }, 50), { + items: [{ url: '/a.png' }], + pagination: { + page: 1, + pageSize: 50, + total: 1, + pageCount: 1 + } + }) + + assert.deepEqual(normalizeMediaPickerResponse({ + items: 'invalid', + pagination: { + page: -2, + pageSize: 0, + total: -10, + pageCount: 0 + } + }, 50), { + items: [], + pagination: { + page: 1, + pageSize: 50, + total: 0, + pageCount: 1 + } + }) +})