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

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

@@ -1,9 +1,6 @@
<script setup>
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
import { resolveActiveBlockContext } from '../../lib/markdown-block-context.js'
import {
serializeImageMarkdown
} from '../../lib/markdown-image.js'
import {
convertHtmlToMarkdown,
readEditableTextFromElement
@@ -26,7 +23,6 @@ import {
resolvePreviewFocusPosition as resolveMarkdownPreviewFocusPosition
} from '../../lib/markdown-editor-focus.js'
import {
createSelectedMediaInsertPlan,
filterImageUploadFiles,
resolveImageUploadInsertTarget,
resolveUploadFetchErrorMessage
@@ -35,7 +31,6 @@ import { parseEditorImageLine } from '../../lib/markdown-editor-preview-blocks.j
import {
getNextSlashHighlightedIndex,
getSlashMenuKeyboardAction,
insertMarkdownLinesAtSlashTarget,
parseSlashInput,
shouldCaptureSlashMenuKeyboardAction
} from '../../lib/markdown-slash-commands.js'
@@ -46,6 +41,7 @@ import { useAdminMarkdownHistory } from '../../composables/useAdminMarkdownHisto
import { useAdminMarkdownEditorFocusActions } from '../../composables/useAdminMarkdownEditorFocusActions.js'
import { useAdminMarkdownSourceActions } from '../../composables/useAdminMarkdownSourceActions.js'
import { useAdminMarkdownMediaPicker } from '../../composables/useAdminMarkdownMediaPicker.js'
import { useAdminMarkdownMediaActions } from '../../composables/useAdminMarkdownMediaActions.js'
import { useAdminMarkdownFocusBridge } from '../../composables/useAdminMarkdownFocusBridge.js'
import { useAdminMarkdownPreviewFocus } from '../../composables/useAdminMarkdownPreviewFocus.js'
import { useAdminMarkdownTextareaFocus } from '../../composables/useAdminMarkdownTextareaFocus.js'
@@ -850,13 +846,6 @@ const {
setTextareaSelection
})
/**
* 이미지 마크다운 문자열을 생성한다.
* @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
* @returns {string} 이미지 마크다운
*/
const createImageMarkdown = (image) => serializeImageMarkdown(image)
/**
* 미디어 선택 모달 파생 이미지 로드 실패 시 원본 URL로 되돌린다.
* @param {Event} event - 이미지 오류 이벤트
@@ -1048,37 +1037,28 @@ const onLiveSlashUpdate = (payload) => {
nextTick(updateLiveSlashMenuPosition)
}
/**
* 지정 줄에 마크다운 줄을 삽입한다.
* @param {number} lineIndex - 삽입 위치(0-based)
* @param {string[]} insertLines - 삽입 줄
* @returns {void}
*/
const insertMarkdownAtLine = (lineIndex, insertLines) => {
markdownValue.value = insertMarkdownLinesAtSlashTarget(markdownValue.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'))
}
const {
insertGallery,
applyMediaSelection,
uploadAndInsert,
uploadFromMediaModal
} = useAdminMarkdownMediaActions({
value: markdownValue,
mediaPickerTarget,
liveSlashInsertLine,
isUploading,
isMediaPickerOpen,
activeMediaPickerTab,
resolveCurrentSelectedMediaItems,
uploadMediaFilesWithState,
uploadFilesIntoMediaPicker,
fetchMediaItems,
closeMediaPicker: () => closeMediaPicker(),
insertBlockSnippet,
appendImagesToActiveGallery: (images) => appendImagesToActiveGallery(images),
showToast,
resolveUploadFetchErrorMessage
})
const {
appendImagesToActiveGallery,
@@ -1198,87 +1178,6 @@ watch(activeMediaPickerTab, async (tab) => {
await fetchMediaItems()
})
/**
* 미디어 항목 목록을 마크다운에 삽입한다.
* @param {Array<Object>} selectedItems - 선택 또는 업로드된 미디어 항목
* @returns {void}
*/
const insertSelectedMediaItems = (selectedItems) => {
const plan = createSelectedMediaInsertPlan({
target: mediaPickerTarget.value,
selectedItems,
liveSlashLine: liveSlashInsertLine.value
})
if (liveSlashInsertLine.value !== null) {
if (plan.kind === 'insert-at-line') {
insertMarkdownAtLine(plan.line, plan.lines)
}
liveSlashInsertLine.value = null
return
}
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'))
}
}
/**
* 선택한 미디어를 마크다운에 삽입한다.
* @returns {void}
*/
const applyMediaSelection = () => {
const selectedItems = resolveCurrentSelectedMediaItems()
insertSelectedMediaItems(selectedItems)
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
}
const plan = createSelectedMediaInsertPlan({
target,
selectedItems: uploadedFiles,
liveSlashLine: null
})
if (plan.kind === 'gallery') {
insertGallery(plan.images)
} else if (plan.kind === 'image') {
insertImage(plan.image)
} else if (plan.kind === 'block') {
insertBlockSnippet(plan.lines.join('\n'))
}
if (isMediaPickerOpen.value && activeMediaPickerTab.value === 'library') {
await fetchMediaItems()
}
} catch (error) {
showToast('error', resolveUploadFetchErrorMessage(error))
}
}
/**
* 파일 입력 변경 처리
* @param {Event} event - 파일 입력 이벤트
@@ -1290,29 +1189,6 @@ const handleFileInput = async (event, target) => {
event.target.value = ''
}
/**
* 미디어 모달 업로드 탭에서 파일을 업로드하고 라이브러리 목록을 갱신한다.
* @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))
}
}
/**
* 미디어 모달 업로드 영역에 파일을 드롭한다.
* @param {DragEvent} event - 드롭 이벤트