에디터 미디어 선택 상태 로직 분리
This commit is contained in:
@@ -23,10 +23,13 @@ import {
|
||||
} from '../../lib/markdown-editor-history.js'
|
||||
import {
|
||||
createMediaBlockMarkdown,
|
||||
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
|
||||
getMediaItemKind,
|
||||
getMediaPickerPreviewUrl,
|
||||
isMediaItemSelectableForTarget,
|
||||
isUploadFileAllowedForMediaTarget
|
||||
isUploadFileAllowedForMediaTarget,
|
||||
resolveSelectedMediaItems,
|
||||
toggleMediaSelectionState
|
||||
} from '../../lib/markdown-editor-media.js'
|
||||
import {
|
||||
filterSlashCommands,
|
||||
@@ -3131,8 +3134,7 @@ const getGallerySelectionOrder = (url) => {
|
||||
return 0
|
||||
}
|
||||
|
||||
const index = selectedMediaUrls.value.indexOf(url)
|
||||
return index >= 0 ? index + 1 : 0
|
||||
return getGallerySelectionOrderFromSelection(selectedMediaUrls.value, url)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3169,27 +3171,15 @@ watch(activeMediaPickerTab, async (tab) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleMediaSelection = (mediaItem) => {
|
||||
if (!isGalleryMediaPicker.value) {
|
||||
selectedMediaUrls.value = [mediaItem.url]
|
||||
selectedMediaItemMap.value = {
|
||||
[mediaItem.url]: mediaItem
|
||||
}
|
||||
return
|
||||
}
|
||||
const nextSelection = toggleMediaSelectionState({
|
||||
selectedUrls: selectedMediaUrls.value,
|
||||
selectedItemMap: selectedMediaItemMap.value,
|
||||
mediaItem,
|
||||
isGallery: isGalleryMediaPicker.value
|
||||
})
|
||||
|
||||
if (selectedMediaUrls.value.includes(mediaItem.url)) {
|
||||
selectedMediaUrls.value = selectedMediaUrls.value.filter((url) => url !== mediaItem.url)
|
||||
const nextMap = { ...selectedMediaItemMap.value }
|
||||
delete nextMap[mediaItem.url]
|
||||
selectedMediaItemMap.value = nextMap
|
||||
return
|
||||
}
|
||||
|
||||
selectedMediaUrls.value = [...selectedMediaUrls.value, mediaItem.url]
|
||||
selectedMediaItemMap.value = {
|
||||
...selectedMediaItemMap.value,
|
||||
[mediaItem.url]: mediaItem
|
||||
}
|
||||
selectedMediaUrls.value = nextSelection.selectedUrls
|
||||
selectedMediaItemMap.value = nextSelection.selectedItemMap
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3268,9 +3258,11 @@ const insertSelectedMediaItems = (selectedItems) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const applyMediaSelection = () => {
|
||||
const selectedItems = selectedMediaUrls.value
|
||||
.map((url) => selectedMediaItemMap.value[url] || mediaItems.value.find((item) => item.url === url))
|
||||
.filter(Boolean)
|
||||
const selectedItems = resolveSelectedMediaItems(
|
||||
selectedMediaUrls.value,
|
||||
selectedMediaItemMap.value,
|
||||
mediaItems.value
|
||||
)
|
||||
|
||||
insertSelectedMediaItems(selectedItems)
|
||||
closeMediaPicker()
|
||||
|
||||
@@ -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` 권한으로 정규화 |
|
||||
@@ -120,7 +120,7 @@
|
||||
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
|
||||
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
|
||||
| tests/markdown-editor-history.test.js | 관리자 Markdown 에디터 undo/redo 히스토리 항목과 선택 범위 보정 단위 회귀 테스트 |
|
||||
| tests/markdown-editor-media.test.js | 관리자 Markdown 에디터 미디어 선택 helper의 종류·선택 가능 여부·미리보기·블록 생성 단위 회귀 테스트 |
|
||||
| tests/markdown-editor-media.test.js | 관리자 Markdown 에디터 미디어 선택 helper의 종류·선택 가능 여부·미리보기·선택 상태·블록 생성 단위 회귀 테스트 |
|
||||
| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 |
|
||||
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
||||
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.70
|
||||
|
||||
- 관리자 Markdown 에디터 미디어 선택 모달의 선택 순서·단일/갤러리 선택 토글·선택 URL 기반 항목 해석 로직을 공통 helper로 분리.
|
||||
- 미디어 선택 상태 토글과 선택 항목 해석 단위 테스트 추가.
|
||||
|
||||
## v1.11.69
|
||||
|
||||
- 관리자 Markdown 에디터의 미디어 선택 모달 종류 판별·대상별 선택 가능 여부·미리보기 URL·업로드 MIME 판별·미디어 블록 생성 로직을 공통 helper로 분리.
|
||||
|
||||
@@ -151,3 +151,85 @@ export const createMediaBlockMarkdown = (blockType, item) => {
|
||||
':::'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 미디어 선택 순서를 반환한다.
|
||||
* @param {string[]} selectedUrls - 선택된 URL 목록
|
||||
* @param {string} url - 미디어 URL
|
||||
* @returns {number} 1부터 시작하는 선택 순서, 선택되지 않았으면 0
|
||||
*/
|
||||
export const getGallerySelectionOrder = (selectedUrls, url) => {
|
||||
const index = Array.isArray(selectedUrls) ? selectedUrls.indexOf(url) : -1
|
||||
return index >= 0 ? index + 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 상태를 토글한다.
|
||||
* @param {Object} options - 토글 옵션
|
||||
* @param {string[]} options.selectedUrls - 선택된 URL 목록
|
||||
* @param {Record<string, Object>} options.selectedItemMap - 선택된 미디어 항목 맵
|
||||
* @param {Object} options.mediaItem - 토글 대상 미디어 항목
|
||||
* @param {boolean} options.isGallery - 갤러리 다중 선택 여부
|
||||
* @returns {{ selectedUrls: string[], selectedItemMap: Record<string, Object> }} 다음 선택 상태
|
||||
*/
|
||||
export const toggleMediaSelectionState = ({
|
||||
selectedUrls,
|
||||
selectedItemMap,
|
||||
mediaItem,
|
||||
isGallery
|
||||
}) => {
|
||||
const url = mediaItem?.url
|
||||
|
||||
if (!url) {
|
||||
return {
|
||||
selectedUrls: Array.isArray(selectedUrls) ? selectedUrls : [],
|
||||
selectedItemMap: selectedItemMap && typeof selectedItemMap === 'object' ? selectedItemMap : {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isGallery) {
|
||||
return {
|
||||
selectedUrls: [url],
|
||||
selectedItemMap: {
|
||||
[url]: mediaItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentUrls = Array.isArray(selectedUrls) ? selectedUrls : []
|
||||
const currentMap = selectedItemMap && typeof selectedItemMap === 'object' ? selectedItemMap : {}
|
||||
|
||||
if (currentUrls.includes(url)) {
|
||||
const nextMap = { ...currentMap }
|
||||
delete nextMap[url]
|
||||
|
||||
return {
|
||||
selectedUrls: currentUrls.filter((selectedUrl) => selectedUrl !== url),
|
||||
selectedItemMap: nextMap
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedUrls: [...currentUrls, url],
|
||||
selectedItemMap: {
|
||||
...currentMap,
|
||||
[url]: mediaItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 URL 순서에 맞춰 미디어 항목 목록을 반환한다.
|
||||
* @param {string[]} selectedUrls - 선택된 URL 목록
|
||||
* @param {Record<string, Object>} selectedItemMap - 선택된 미디어 항목 맵
|
||||
* @param {Object[]} mediaItems - 현재 미디어 목록
|
||||
* @returns {Object[]} 선택된 미디어 항목 목록
|
||||
*/
|
||||
export const resolveSelectedMediaItems = (selectedUrls, selectedItemMap, mediaItems) => {
|
||||
const itemMap = selectedItemMap && typeof selectedItemMap === 'object' ? selectedItemMap : {}
|
||||
const items = Array.isArray(mediaItems) ? mediaItems : []
|
||||
|
||||
return (Array.isArray(selectedUrls) ? selectedUrls : [])
|
||||
.map((url) => itemMap[url] || items.find((item) => item?.url === url))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.69",
|
||||
"version": "1.11.70",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.69",
|
||||
"version": "1.11.70",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.69",
|
||||
"version": "1.11.70",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -3,10 +3,13 @@ import test from 'node:test'
|
||||
import {
|
||||
createMediaBlockMarkdown,
|
||||
formatMediaFileSize,
|
||||
getGallerySelectionOrder,
|
||||
getMediaItemKind,
|
||||
getMediaPickerPreviewUrl,
|
||||
isMediaItemSelectableForTarget,
|
||||
isUploadFileAllowedForMediaTarget
|
||||
isUploadFileAllowedForMediaTarget,
|
||||
resolveSelectedMediaItems,
|
||||
toggleMediaSelectionState
|
||||
} from '../lib/markdown-editor-media.js'
|
||||
|
||||
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
|
||||
@@ -62,3 +65,65 @@ test('미디어 항목으로 fenced 미디어 블록을 만든다', () => {
|
||||
size: 2048
|
||||
}), [':::file', 'url=/uploads/a.pdf', 'title=a.pdf', 'description=', 'name=a.pdf', 'size=2 KB', ':::'])
|
||||
})
|
||||
|
||||
test('갤러리 선택 순서를 선택 URL 순서로 반환한다', () => {
|
||||
assert.equal(getGallerySelectionOrder(['/a.png', '/b.png'], '/b.png'), 2)
|
||||
assert.equal(getGallerySelectionOrder(['/a.png'], '/b.png'), 0)
|
||||
})
|
||||
|
||||
test('단일 미디어 선택은 기존 선택을 새 항목 하나로 교체한다', () => {
|
||||
assert.deepEqual(toggleMediaSelectionState({
|
||||
selectedUrls: ['/old.png'],
|
||||
selectedItemMap: {
|
||||
'/old.png': { url: '/old.png' }
|
||||
},
|
||||
mediaItem: { url: '/new.png', name: 'new' },
|
||||
isGallery: false
|
||||
}), {
|
||||
selectedUrls: ['/new.png'],
|
||||
selectedItemMap: {
|
||||
'/new.png': { url: '/new.png', name: 'new' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('갤러리 미디어 선택은 선택 추가와 제거를 토글한다', () => {
|
||||
const selected = toggleMediaSelectionState({
|
||||
selectedUrls: ['/a.png'],
|
||||
selectedItemMap: {
|
||||
'/a.png': { url: '/a.png' }
|
||||
},
|
||||
mediaItem: { url: '/b.png' },
|
||||
isGallery: true
|
||||
})
|
||||
|
||||
assert.deepEqual(selected.selectedUrls, ['/a.png', '/b.png'])
|
||||
assert.deepEqual(Object.keys(selected.selectedItemMap), ['/a.png', '/b.png'])
|
||||
|
||||
assert.deepEqual(toggleMediaSelectionState({
|
||||
selectedUrls: selected.selectedUrls,
|
||||
selectedItemMap: selected.selectedItemMap,
|
||||
mediaItem: { url: '/a.png' },
|
||||
isGallery: true
|
||||
}), {
|
||||
selectedUrls: ['/b.png'],
|
||||
selectedItemMap: {
|
||||
'/b.png': { url: '/b.png' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('선택된 URL 순서대로 미디어 항목을 해석한다', () => {
|
||||
assert.deepEqual(resolveSelectedMediaItems(
|
||||
['/a.png', '/b.png', '/missing.png'],
|
||||
{
|
||||
'/a.png': { url: '/a.png', source: 'map' }
|
||||
},
|
||||
[
|
||||
{ url: '/b.png', source: 'list' }
|
||||
]
|
||||
), [
|
||||
{ url: '/a.png', source: 'map' },
|
||||
{ url: '/b.png', source: 'list' }
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user