diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 9e3faf0..b6b0aef 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -5,6 +5,11 @@ const props = defineProps({ modelValue: { type: [String, Array, Object], default: '' + }, + /** 헤더 아래 고정 툴바 슬롯(AdminPostForm `#admin-post-form-editor-toolbar-host`) */ + toolbarTeleportTo: { + type: String, + default: '#admin-post-form-editor-toolbar-host' } }) @@ -22,20 +27,14 @@ const isMediaPickerOpen = ref(false) const isLoadingMedia = ref(false) const isUploading = ref(false) const mediaPickerTarget = ref('image') +const activeMediaPickerTab = ref('library') const selectedMediaUrls = ref([]) -const selectedImageWidth = ref('regular') const lastSelectionState = ref({ start: 0, end: 0, scrollTop: 0 }) -const imageWidthOptions = [ - { value: 'regular', label: '기본' }, - { value: 'wide', label: '와이드' }, - { value: 'full', label: '풀사이즈' } -] - const markdownValue = computed({ get: () => normalizeMarkdownContent(props.modelValue), set: (value) => emit('update:modelValue', value) @@ -500,12 +499,7 @@ const insertCodeBlock = () => { * @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보 * @returns {string} 이미지 마크다운 */ -const createImageMarkdown = (image) => { - const width = image.width && image.width !== 'regular' - ? `{width=${image.width}}` - : '' - return `![${image.alt || ''}](${image.url})${width}` -} +const createImageMarkdown = (image) => `![${image.alt || ''}](${image.url})` /** * 지정 줄 범위를 새 줄 목록으로 교체한다. @@ -680,6 +674,7 @@ const fetchMediaItems = async () => { */ const openMediaPicker = async (target) => { mediaPickerTarget.value = target + activeMediaPickerTab.value = 'library' selectedMediaUrls.value = [] isMediaPickerOpen.value = true await fetchMediaItems() @@ -692,6 +687,7 @@ const openMediaPicker = async (target) => { const closeMediaPicker = () => { isMediaPickerOpen.value = false selectedMediaUrls.value = [] + activeMediaPickerTab.value = 'library' } /** @@ -724,8 +720,7 @@ const applyMediaSelection = () => { if (item) { insertImage({ url: item.url, - alt: item.name || '', - width: selectedImageWidth.value + alt: item.name || '' }) } } else if (mediaPickerTarget.value === 'active-gallery') { @@ -779,8 +774,7 @@ const uploadAndInsert = async (files, target = 'image') => { const uploadedFiles = await uploadImages(files) const images = uploadedFiles.map((file) => ({ url: file.url, - alt: file.name || '', - width: target === 'image' ? selectedImageWidth.value : 'regular' + alt: file.name || '' })) if (target === 'gallery') { @@ -931,6 +925,59 @@ const handleFileInput = async (event, target) => { event.target.value = '' } +/** + * 미디어 모달 업로드 탭에서 파일을 삽입한다. + * @param {FileList|Array} files - 업로드 파일 목록 + * @returns {Promise} + */ +const uploadFromMediaModal = async (files) => { + if (!files?.length) { + return + } + + const target = mediaPickerTarget.value === 'gallery' || mediaPickerTarget.value === 'active-gallery' + ? 'gallery' + : 'image' + + await uploadAndInsert(files, target) + closeMediaPicker() +} + +/** + * 미디어 모달 업로드 영역에 파일을 드롭한다. + * @param {DragEvent} event - 드롭 이벤트 + * @returns {Promise} + */ +const handleMediaModalDrop = async (event) => { + const files = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/')) + + if (!files.length) { + return + } + + event.preventDefault() + await uploadFromMediaModal(files) +} + +/** + * 미디어 모달의 삽입 대상 라벨을 반환한다. + * @returns {string} 모달 제목 + */ +const mediaPickerTitle = computed(() => { + if (mediaPickerTarget.value === 'gallery' || mediaPickerTarget.value === 'active-gallery') { + return '갤러리 이미지 선택' + } + + return '이미지 선택' +}) + +/** + * 미디어 모달에서 다중 선택 여부를 반환한다. + * @returns {boolean} 다중 선택 여부 + */ +const isGalleryMediaPicker = computed(() => mediaPickerTarget.value === 'gallery' + || mediaPickerTarget.value === 'active-gallery') + /** * 붙여넣은 이미지 파일을 업로드한다. * @param {ClipboardEvent} event - 붙여넣기 이벤트 @@ -1002,7 +1049,12 @@ const handleKeydown = (event) => {