에디터 미디어 삽입 실행 분리

This commit is contained in:
2026-07-29 12:44:23 +09:00
parent 92e37f3ae8
commit ebc4a95b37
10 changed files with 394 additions and 153 deletions

View File

@@ -0,0 +1,190 @@
import { serializeImageMarkdown } from '../lib/markdown-image.js'
import { createSelectedMediaInsertPlan } from '../lib/markdown-editor-media.js'
import { insertMarkdownLinesAtSlashTarget } from '../lib/markdown-slash-commands.js'
/**
* 관리자 Markdown 에디터의 미디어 삽입 실행 함수를 만든다.
* @param {Object} options - 실행 옵션
* @param {{ value: string }} options.value - 마크다운 본문 ref
* @param {{ value: string }} options.mediaPickerTarget - 미디어 선택 대상 ref
* @param {{ value: number|null }} options.liveSlashInsertLine - 슬래시 미디어 삽입 대기 줄 ref
* @param {{ value: boolean }} options.isUploading - 업로드 중 여부 ref
* @param {{ value: boolean }} options.isMediaPickerOpen - 미디어 선택 창 열림 여부 ref
* @param {{ value: string }} options.activeMediaPickerTab - 미디어 선택 창 탭 ref
* @param {() => Array<Object>} options.resolveCurrentSelectedMediaItems - 현재 선택 미디어 반환 함수
* @param {(files: FileList|Array<File>) => Promise<Array<Object>>} options.uploadMediaFilesWithState - 파일 업로드 함수
* @param {(files: FileList|Array<File>) => Promise<Array<Object>>} options.uploadFilesIntoMediaPicker - 미디어 선택 창 업로드 함수
* @param {() => Promise<void>} options.fetchMediaItems - 미디어 목록 조회 함수
* @param {() => void} options.closeMediaPicker - 미디어 선택 창 닫기 함수
* @param {(snippet: string) => void} options.insertBlockSnippet - 블록 조각 삽입 함수
* @param {(images: Array<Object>) => void} options.appendImagesToActiveGallery - 활성 갤러리 이미지 추가 함수
* @param {(type: 'success'|'error'|'info', message: string) => void} options.showToast - 토스트 표시 함수
* @param {(error: unknown) => string} options.resolveUploadFetchErrorMessage - 업로드 오류 문구 변환 함수
* @returns {Object} 미디어 삽입 실행 함수 모음
*/
export const useAdminMarkdownMediaActions = ({
value,
mediaPickerTarget,
liveSlashInsertLine,
isUploading,
isMediaPickerOpen,
activeMediaPickerTab,
resolveCurrentSelectedMediaItems,
uploadMediaFilesWithState,
uploadFilesIntoMediaPicker,
fetchMediaItems,
closeMediaPicker,
insertBlockSnippet,
appendImagesToActiveGallery,
showToast,
resolveUploadFetchErrorMessage
}) => {
/**
* 이미지 마크다운 문자열을 생성한다.
* @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
* @returns {string} 이미지 마크다운
*/
const createImageMarkdown = (image) => serializeImageMarkdown(image)
/**
* 지정 줄에 마크다운 줄을 삽입한다.
* @param {number} lineIndex - 삽입 위치
* @param {string[]} insertLines - 삽입 줄
* @returns {void}
*/
const insertMarkdownAtLine = (lineIndex, insertLines) => {
value.value = insertMarkdownLinesAtSlashTarget(value.value, lineIndex, insertLines)
}
/**
* 이미지 마크다운을 삽입한다.
* @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
* @returns {void}
*/
const insertImage = (image) => {
insertBlockSnippet(createImageMarkdown(image))
}
/**
* 갤러리 마크다운을 삽입한다.
* @param {Array<{ url: string, alt?: string, width?: string }>} images - 이미지 목록
* @returns {void}
*/
const insertGallery = (images) => {
if (!images.length) {
return
}
insertBlockSnippet([':::gallery', ...images.map(createImageMarkdown), ':::'].join('\n'))
}
/**
* 미디어 삽입 계획을 본문에 적용한다.
* @param {{ kind: string, line?: number, lines?: string[], image?: Object, images?: Object[] }} plan - 삽입 계획
* @returns {void}
*/
const applyMediaInsertPlan = (plan) => {
if (plan.kind === 'insert-at-line') {
insertMarkdownAtLine(plan.line, plan.lines)
} else if (plan.kind === 'image') {
insertImage(plan.image)
} else if (plan.kind === 'active-gallery') {
appendImagesToActiveGallery(plan.images)
} else if (plan.kind === 'gallery') {
insertGallery(plan.images)
} else if (plan.kind === 'block') {
insertBlockSnippet(plan.lines.join('\n'))
}
}
/**
* 미디어 항목 목록을 마크다운에 삽입한다.
* @param {Array<Object>} selectedItems - 선택 또는 업로드된 미디어 항목
* @param {string} [target] - 삽입 대상
* @returns {void}
*/
const insertSelectedMediaItems = (selectedItems, target = mediaPickerTarget.value) => {
const pendingSlashLine = liveSlashInsertLine.value
const plan = createSelectedMediaInsertPlan({
target,
selectedItems,
liveSlashLine: pendingSlashLine
})
applyMediaInsertPlan(plan)
if (pendingSlashLine !== null) {
liveSlashInsertLine.value = null
}
}
/**
* 선택한 미디어를 마크다운에 삽입한다.
* @returns {void}
*/
const applyMediaSelection = () => {
insertSelectedMediaItems(resolveCurrentSelectedMediaItems())
closeMediaPicker()
}
/**
* 업로드 파일을 현재 커서 위치에 삽입한다.
* @param {FileList|Array<File>} files - 파일 목록
* @param {'image'|'gallery'|'video'|'audio'|'file'} [target] - 삽입 대상
* @returns {Promise<void>}
*/
const uploadAndInsert = async (files, target = 'image') => {
if (!files?.length || isUploading.value) {
return
}
try {
const uploadedFiles = await uploadMediaFilesWithState(files)
if (!uploadedFiles.length) {
return
}
insertSelectedMediaItems(uploadedFiles, target)
if (isMediaPickerOpen.value && activeMediaPickerTab.value === 'library') {
await fetchMediaItems()
}
} catch (error) {
showToast('error', resolveUploadFetchErrorMessage(error))
}
}
/**
* 미디어 모달 업로드 탭에서 파일을 업로드하고 라이브러리 목록을 갱신한다.
* @param {FileList|Array<File>} files - 업로드 파일 목록
* @returns {Promise<void>}
*/
const uploadFromMediaModal = async (files) => {
if (!files?.length || isUploading.value) {
return
}
try {
const uploadedFiles = await uploadFilesIntoMediaPicker(files)
if (!uploadedFiles.length) {
return
}
showToast('success', '업로드가 완료되어 파일이 선택되었습니다.')
} catch (error) {
showToast('error', resolveUploadFetchErrorMessage(error))
}
}
return {
insertMarkdownAtLine,
insertImage,
insertGallery,
insertSelectedMediaItems,
applyMediaSelection,
uploadAndInsert,
uploadFromMediaModal
}
}