에디터 미디어 선택 상태 로직 분리
This commit is contained in:
@@ -23,10 +23,13 @@ import {
|
|||||||
} from '../../lib/markdown-editor-history.js'
|
} from '../../lib/markdown-editor-history.js'
|
||||||
import {
|
import {
|
||||||
createMediaBlockMarkdown,
|
createMediaBlockMarkdown,
|
||||||
|
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
|
||||||
getMediaItemKind,
|
getMediaItemKind,
|
||||||
getMediaPickerPreviewUrl,
|
getMediaPickerPreviewUrl,
|
||||||
isMediaItemSelectableForTarget,
|
isMediaItemSelectableForTarget,
|
||||||
isUploadFileAllowedForMediaTarget
|
isUploadFileAllowedForMediaTarget,
|
||||||
|
resolveSelectedMediaItems,
|
||||||
|
toggleMediaSelectionState
|
||||||
} from '../../lib/markdown-editor-media.js'
|
} from '../../lib/markdown-editor-media.js'
|
||||||
import {
|
import {
|
||||||
filterSlashCommands,
|
filterSlashCommands,
|
||||||
@@ -3131,8 +3134,7 @@ const getGallerySelectionOrder = (url) => {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = selectedMediaUrls.value.indexOf(url)
|
return getGallerySelectionOrderFromSelection(selectedMediaUrls.value, url)
|
||||||
return index >= 0 ? index + 1 : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3169,27 +3171,15 @@ watch(activeMediaPickerTab, async (tab) => {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const toggleMediaSelection = (mediaItem) => {
|
const toggleMediaSelection = (mediaItem) => {
|
||||||
if (!isGalleryMediaPicker.value) {
|
const nextSelection = toggleMediaSelectionState({
|
||||||
selectedMediaUrls.value = [mediaItem.url]
|
selectedUrls: selectedMediaUrls.value,
|
||||||
selectedMediaItemMap.value = {
|
selectedItemMap: selectedMediaItemMap.value,
|
||||||
[mediaItem.url]: mediaItem
|
mediaItem,
|
||||||
}
|
isGallery: isGalleryMediaPicker.value
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedMediaUrls.value.includes(mediaItem.url)) {
|
selectedMediaUrls.value = nextSelection.selectedUrls
|
||||||
selectedMediaUrls.value = selectedMediaUrls.value.filter((url) => url !== mediaItem.url)
|
selectedMediaItemMap.value = nextSelection.selectedItemMap
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3268,9 +3258,11 @@ const insertSelectedMediaItems = (selectedItems) => {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const applyMediaSelection = () => {
|
const applyMediaSelection = () => {
|
||||||
const selectedItems = selectedMediaUrls.value
|
const selectedItems = resolveSelectedMediaItems(
|
||||||
.map((url) => selectedMediaItemMap.value[url] || mediaItems.value.find((item) => item.url === url))
|
selectedMediaUrls.value,
|
||||||
.filter(Boolean)
|
selectedMediaItemMap.value,
|
||||||
|
mediaItems.value
|
||||||
|
)
|
||||||
|
|
||||||
insertSelectedMediaItems(selectedItems)
|
insertSelectedMediaItems(selectedItems)
|
||||||
closeMediaPicker()
|
closeMediaPicker()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
|
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
|
||||||
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
||||||
| lib/markdown-editor-history.js | 관리자 Markdown-first 에디터 undo/redo 히스토리 항목 생성, 레거시 문자열 항목 본문 추출, 선택 범위 보정 |
|
| 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-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 |
|
||||||
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
||||||
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |
|
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
|
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
|
||||||
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
|
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
|
||||||
| tests/markdown-editor-history.test.js | 관리자 Markdown 에디터 undo/redo 히스토리 항목과 선택 범위 보정 단위 회귀 테스트 |
|
| 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-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 |
|
||||||
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
||||||
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.70
|
||||||
|
|
||||||
|
- 관리자 Markdown 에디터 미디어 선택 모달의 선택 순서·단일/갤러리 선택 토글·선택 URL 기반 항목 해석 로직을 공통 helper로 분리.
|
||||||
|
- 미디어 선택 상태 토글과 선택 항목 해석 단위 테스트 추가.
|
||||||
|
|
||||||
## v1.11.69
|
## v1.11.69
|
||||||
|
|
||||||
- 관리자 Markdown 에디터의 미디어 선택 모달 종류 판별·대상별 선택 가능 여부·미리보기 URL·업로드 MIME 판별·미디어 블록 생성 로직을 공통 helper로 분리.
|
- 관리자 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",
|
"name": "sori.studio",
|
||||||
"version": "1.11.69",
|
"version": "1.11.70",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.69",
|
"version": "1.11.70",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.69",
|
"version": "1.11.70",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import test from 'node:test'
|
|||||||
import {
|
import {
|
||||||
createMediaBlockMarkdown,
|
createMediaBlockMarkdown,
|
||||||
formatMediaFileSize,
|
formatMediaFileSize,
|
||||||
|
getGallerySelectionOrder,
|
||||||
getMediaItemKind,
|
getMediaItemKind,
|
||||||
getMediaPickerPreviewUrl,
|
getMediaPickerPreviewUrl,
|
||||||
isMediaItemSelectableForTarget,
|
isMediaItemSelectableForTarget,
|
||||||
isUploadFileAllowedForMediaTarget
|
isUploadFileAllowedForMediaTarget,
|
||||||
|
resolveSelectedMediaItems,
|
||||||
|
toggleMediaSelectionState
|
||||||
} from '../lib/markdown-editor-media.js'
|
} from '../lib/markdown-editor-media.js'
|
||||||
|
|
||||||
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
|
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
|
||||||
@@ -62,3 +65,65 @@ test('미디어 항목으로 fenced 미디어 블록을 만든다', () => {
|
|||||||
size: 2048
|
size: 2048
|
||||||
}), [':::file', 'url=/uploads/a.pdf', 'title=a.pdf', 'description=', 'name=a.pdf', 'size=2 KB', ':::'])
|
}), [':::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