diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index de7e2cc..9f7eb7d 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -22,6 +22,7 @@ import { getEditorHistoryEntryValue } from '../../lib/markdown-editor-history.js' import { + buildMediaPickerApplyState, buildMediaPickerEmptyMessage, buildMediaPickerUploadHint, buildMediaPickerPageItems, @@ -3373,6 +3374,16 @@ const mediaPickerUploadHint = computed(() => buildMediaPickerUploadHint( */ const mediaPickerEmptyMessage = computed(() => buildMediaPickerEmptyMessage(mediaSearchQuery.value)) +/** + * 미디어 선택 적용 버튼 상태를 반환한다. + * @returns {{ disabled: boolean, label: string }} 적용 버튼 상태 + */ +const mediaPickerApplyState = computed(() => buildMediaPickerApplyState({ + selectedUrls: selectedMediaUrls.value, + isUploading: isUploading.value, + isGallery: isGalleryMediaPicker.value +})) + /** * 붙여넣은 이미지 파일을 업로드한다. * @param {ClipboardEvent} event - 붙여넣기 이벤트 @@ -3807,10 +3818,10 @@ const handleKeydown = (event) => { diff --git a/docs/map.md b/docs/map.md index 60d16e5..24ee161 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 허용, 선택 대상별 제목·accept·API kind·업로드 안내, 라이브러리 빈 상태 문구, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, 페이지 상태·범위·페이지 번호 계산, fenced 미디어 블록 생성 | +| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 대상별 제목·accept·API kind·업로드 안내, 라이브러리 빈 상태 문구, 적용 버튼 상태, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, 페이지 상태·범위·페이지 번호 계산, 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 2ddc09b..23b20c3 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.77 + +- 관리자 Markdown 에디터 미디어 선택 적용 버튼 상태 계산을 공통 helper로 분리. +- 미디어 선택 적용 버튼 상태 단위 테스트 추가. + ## v1.11.76 - 관리자 Markdown 에디터 미디어 선택 라이브러리의 빈 상태 문구 계산을 공통 helper로 분리. diff --git a/lib/markdown-editor-media.js b/lib/markdown-editor-media.js index 7c69c16..8fd4b07 100644 --- a/lib/markdown-editor-media.js +++ b/lib/markdown-editor-media.js @@ -241,6 +241,27 @@ export const buildMediaPickerEmptyMessage = (searchQuery) => { return '선택할 미디어가 없습니다. 업로드 탭에서 파일을 추가할 수 있습니다.' } +/** + * 미디어 선택 적용 버튼 상태를 만든다. + * @param {Object} options - 적용 버튼 상태 옵션 + * @param {string[]} options.selectedUrls - 선택된 URL 목록 + * @param {boolean} options.isUploading - 업로드 중 여부 + * @param {boolean} options.isGallery - 갤러리 다중 선택 여부 + * @returns {{ disabled: boolean, label: string }} 적용 버튼 상태 + */ +export const buildMediaPickerApplyState = ({ + selectedUrls, + isUploading, + isGallery +}) => { + const selectedCount = Array.isArray(selectedUrls) ? selectedUrls.length : 0 + + return { + disabled: Boolean(isUploading) || selectedCount <= 0, + label: isGallery && selectedCount > 0 ? `${selectedCount}개 삽입` : '삽입' + } +} + /** * 미디어 항목으로 Prose 미디어 블록을 만든다. * @param {'video'|'audio'|'file'} blockType - 블록 유형 diff --git a/package-lock.json b/package-lock.json index aa65718..5607d03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.76", + "version": "1.11.77", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.76", + "version": "1.11.77", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index a330fdf..5d37d99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.76", + "version": "1.11.77", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-editor-media.test.js b/tests/markdown-editor-media.test.js index aeeaa0f..7113870 100644 --- a/tests/markdown-editor-media.test.js +++ b/tests/markdown-editor-media.test.js @@ -1,6 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import { + buildMediaPickerApplyState, buildMediaPickerEmptyMessage, buildMediaPickerPageItems, buildMediaPickerRangeLabel, @@ -113,6 +114,35 @@ test('미디어 선택 라이브러리 빈 상태 문구를 계산한다', () => assert.equal(buildMediaPickerEmptyMessage(' car '), '「car」에 맞는 미디어가 없습니다.') }) +test('미디어 선택 적용 버튼 상태를 계산한다', () => { + assert.deepEqual(buildMediaPickerApplyState({ + selectedUrls: [], + isUploading: false, + isGallery: false + }), { + disabled: true, + label: '삽입' + }) + + assert.deepEqual(buildMediaPickerApplyState({ + selectedUrls: ['/a.png'], + isUploading: true, + isGallery: false + }), { + disabled: true, + label: '삽입' + }) + + assert.deepEqual(buildMediaPickerApplyState({ + selectedUrls: ['/a.png', '/b.png'], + isUploading: false, + isGallery: true + }), { + disabled: false, + label: '2개 삽입' + }) +}) + test('미디어 항목으로 fenced 미디어 블록을 만든다', () => { assert.deepEqual(createMediaBlockMarkdown('video', { url: '/uploads/a.mp4',