관리자 미디어 선택 상태 분리
This commit is contained in:
271
composables/useAdminMarkdownMediaPicker.js
Normal file
271
composables/useAdminMarkdownMediaPicker.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import { computed, ref, unref } from 'vue'
|
||||
import {
|
||||
buildMediaPickerApplyState,
|
||||
buildMediaPickerEmptyMessage,
|
||||
buildMediaPickerPageItems,
|
||||
buildMediaPickerRangeLabel,
|
||||
buildMediaPickerUploadHint,
|
||||
createDefaultMediaPickerPagination,
|
||||
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
|
||||
getMediaPickerAcceptForTarget,
|
||||
getMediaPickerKindForTarget,
|
||||
getMediaPickerTitleForTarget,
|
||||
isGalleryMediaPickerTarget,
|
||||
isMediaItemSelectableForTarget,
|
||||
isUploadFileAllowedForMediaTarget,
|
||||
normalizeMediaPickerPage,
|
||||
normalizeMediaPickerResponse,
|
||||
resolveSelectedMediaItems,
|
||||
toggleMediaSelectionState
|
||||
} from '../lib/markdown-editor-media.js'
|
||||
import { useImeSearchInput } from './useImeSearchInput.js'
|
||||
|
||||
export const ADMIN_MARKDOWN_MEDIA_PICKER_PAGE_SIZE = 50
|
||||
|
||||
/**
|
||||
* 관리자 Markdown 에디터의 미디어 선택 모달 상태를 관리한다.
|
||||
* @param {Object} options - 상태 옵션
|
||||
* @param {import('vue').Ref<Object>|import('vue').ComputedRef<Object>} options.uploadSizeLimits - 업로드 한도
|
||||
* @param {number} [options.pageSize=50] - 페이지 크기
|
||||
* @returns {Object} 미디어 선택 모달 상태와 액션
|
||||
*/
|
||||
export const useAdminMarkdownMediaPicker = ({
|
||||
uploadSizeLimits,
|
||||
pageSize = ADMIN_MARKDOWN_MEDIA_PICKER_PAGE_SIZE
|
||||
}) => {
|
||||
const mediaItems = ref([])
|
||||
const isMediaPickerOpen = ref(false)
|
||||
const isLoadingMedia = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const mediaPickerTarget = ref('image')
|
||||
const activeMediaPickerTab = ref('upload')
|
||||
const mediaSearchQuery = ref('')
|
||||
const mediaPickerPage = ref(1)
|
||||
const mediaPickerPagination = ref(createDefaultMediaPickerPagination(pageSize))
|
||||
const selectedMediaUrls = ref([])
|
||||
const selectedMediaItemMap = ref({})
|
||||
const {
|
||||
displayValue: mediaSearchInput,
|
||||
onInput: onMediaSearchInput,
|
||||
onCompositionEnd: onMediaSearchCompositionEnd
|
||||
} = useImeSearchInput(mediaSearchQuery, { debounce: 250 })
|
||||
|
||||
const mediaPickerAccept = computed(() => getMediaPickerAcceptForTarget(mediaPickerTarget.value))
|
||||
const mediaPickerKind = computed(() => getMediaPickerKindForTarget(mediaPickerTarget.value))
|
||||
const mediaPickerTitle = computed(() => getMediaPickerTitleForTarget(mediaPickerTarget.value))
|
||||
const isGalleryMediaPicker = computed(() => isGalleryMediaPickerTarget(mediaPickerTarget.value))
|
||||
const mediaPickerPageCount = computed(() => Math.max(1, Number(mediaPickerPagination.value.pageCount) || 1))
|
||||
const filteredMediaItems = computed(() => mediaItems.value.filter(isMediaItemSelectableForCurrentTarget))
|
||||
const mediaPickerRangeLabel = computed(() => buildMediaPickerRangeLabel({
|
||||
pagination: mediaPickerPagination.value,
|
||||
currentPage: mediaPickerPage.value,
|
||||
pageCount: mediaPickerPageCount.value,
|
||||
defaultPageSize: pageSize
|
||||
}))
|
||||
const mediaPickerPageItems = computed(() => buildMediaPickerPageItems({
|
||||
currentPage: mediaPickerPage.value,
|
||||
pageCount: mediaPickerPageCount.value
|
||||
}))
|
||||
const mediaPickerUploadHint = computed(() => buildMediaPickerUploadHint(
|
||||
mediaPickerTarget.value,
|
||||
unref(uploadSizeLimits)
|
||||
))
|
||||
const mediaPickerEmptyMessage = computed(() => buildMediaPickerEmptyMessage(mediaSearchQuery.value))
|
||||
const mediaPickerApplyState = computed(() => buildMediaPickerApplyState({
|
||||
selectedUrls: selectedMediaUrls.value,
|
||||
isUploading: isUploading.value,
|
||||
isGallery: isGalleryMediaPicker.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* 미디어 항목이 현재 선택 대상과 맞는지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 선택 가능 여부
|
||||
*/
|
||||
function isMediaItemSelectableForCurrentTarget(item) {
|
||||
return isMediaItemSelectableForTarget(item, mediaPickerTarget.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 파일이 현재 선택 대상에 맞는지 확인한다.
|
||||
* @param {File} file - 브라우저 파일
|
||||
* @returns {boolean} 허용 여부
|
||||
*/
|
||||
const isUploadFileAllowedForPicker = (file) => {
|
||||
return isUploadFileAllowedForMediaTarget(file, mediaPickerTarget.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 모달 상태를 연다.
|
||||
* @param {string} target - 삽입 대상
|
||||
* @returns {void}
|
||||
*/
|
||||
const openMediaPickerState = (target) => {
|
||||
mediaPickerTarget.value = target
|
||||
activeMediaPickerTab.value = 'upload'
|
||||
mediaSearchQuery.value = ''
|
||||
mediaPickerPage.value = 1
|
||||
mediaPickerPagination.value = createDefaultMediaPickerPagination(pageSize)
|
||||
selectedMediaUrls.value = []
|
||||
selectedMediaItemMap.value = {}
|
||||
isMediaPickerOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 모달 상태를 닫는다.
|
||||
* @returns {boolean} 닫힘 처리 여부
|
||||
*/
|
||||
const closeMediaPickerState = () => {
|
||||
if (isUploading.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
isMediaPickerOpen.value = false
|
||||
selectedMediaUrls.value = []
|
||||
selectedMediaItemMap.value = {}
|
||||
activeMediaPickerTab.value = 'upload'
|
||||
mediaSearchQuery.value = ''
|
||||
mediaPickerPage.value = 1
|
||||
mediaItems.value = []
|
||||
mediaPickerPagination.value = createDefaultMediaPickerPagination(pageSize)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 API 응답을 현재 상태에 반영한다.
|
||||
* @param {Object} response - 미디어 API 응답
|
||||
* @returns {void}
|
||||
*/
|
||||
const applyMediaPickerResponse = (response) => {
|
||||
const normalizedResponse = normalizeMediaPickerResponse(response, pageSize)
|
||||
mediaItems.value = normalizedResponse.items
|
||||
mediaPickerPagination.value = normalizedResponse.pagination
|
||||
mediaPickerPage.value = mediaPickerPagination.value.page || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 페이지를 변경한다.
|
||||
* @param {number} page - 이동할 페이지
|
||||
* @returns {void}
|
||||
*/
|
||||
const setMediaPickerPage = (page) => {
|
||||
mediaPickerPage.value = normalizeMediaPickerPage(page, mediaPickerPageCount.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 상태를 토글한다.
|
||||
* @param {Object} mediaItem - 미디어 항목
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleMediaSelection = (mediaItem) => {
|
||||
const nextSelection = toggleMediaSelectionState({
|
||||
selectedUrls: selectedMediaUrls.value,
|
||||
selectedItemMap: selectedMediaItemMap.value,
|
||||
mediaItem,
|
||||
isGallery: isGalleryMediaPicker.value
|
||||
})
|
||||
|
||||
selectedMediaUrls.value = nextSelection.selectedUrls
|
||||
selectedMediaItemMap.value = nextSelection.selectedItemMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 미디어 항목을 현재 목록과 선택 맵에서 해석한다.
|
||||
* @returns {Object[]} 선택된 미디어 항목
|
||||
*/
|
||||
const resolveCurrentSelectedMediaItems = () => resolveSelectedMediaItems(
|
||||
selectedMediaUrls.value,
|
||||
selectedMediaItemMap.value,
|
||||
mediaItems.value
|
||||
)
|
||||
|
||||
/**
|
||||
* 업로드 직후 선택 상태를 갱신한다.
|
||||
* @param {Object[]} uploadedFiles - 업로드된 파일 목록
|
||||
* @returns {string[]} 선택된 URL 목록
|
||||
*/
|
||||
const selectUploadedMediaFiles = (uploadedFiles) => {
|
||||
const uploadedUrls = (Array.isArray(uploadedFiles) ? uploadedFiles : [])
|
||||
.filter(isMediaItemSelectableForCurrentTarget)
|
||||
.map((file) => file.url)
|
||||
|
||||
selectedMediaItemMap.value = (Array.isArray(uploadedFiles) ? uploadedFiles : []).reduce((map, file) => {
|
||||
if (uploadedUrls.includes(file.url)) {
|
||||
map[file.url] = file
|
||||
}
|
||||
|
||||
return map
|
||||
}, {})
|
||||
|
||||
selectedMediaUrls.value = isGalleryMediaPicker.value
|
||||
? uploadedUrls
|
||||
: uploadedUrls.slice(0, 1)
|
||||
|
||||
return uploadedUrls
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드된 파일을 목록 앞에 임시로 합친다.
|
||||
* @param {Object[]} uploadedFiles - 업로드된 파일 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const prependUploadedMediaItems = (uploadedFiles) => {
|
||||
mediaItems.value = [
|
||||
...(Array.isArray(uploadedFiles) ? uploadedFiles : []),
|
||||
...mediaItems.value
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 미디어 선택 순서를 반환한다.
|
||||
* @param {string} url - 미디어 URL
|
||||
* @returns {number} 1부터 시작하는 선택 순서
|
||||
*/
|
||||
const getGallerySelectionOrder = (url) => {
|
||||
if (!isGalleryMediaPicker.value) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return getGallerySelectionOrderFromSelection(selectedMediaUrls.value, url)
|
||||
}
|
||||
|
||||
return {
|
||||
mediaItems,
|
||||
isMediaPickerOpen,
|
||||
isLoadingMedia,
|
||||
isUploading,
|
||||
mediaPickerTarget,
|
||||
activeMediaPickerTab,
|
||||
mediaSearchQuery,
|
||||
mediaSearchInput,
|
||||
onMediaSearchInput,
|
||||
onMediaSearchCompositionEnd,
|
||||
mediaPickerPage,
|
||||
mediaPickerPageSize: pageSize,
|
||||
mediaPickerPagination,
|
||||
selectedMediaUrls,
|
||||
selectedMediaItemMap,
|
||||
mediaPickerAccept,
|
||||
mediaPickerKind,
|
||||
mediaPickerTitle,
|
||||
isGalleryMediaPicker,
|
||||
mediaPickerPageCount,
|
||||
filteredMediaItems,
|
||||
mediaPickerRangeLabel,
|
||||
mediaPickerPageItems,
|
||||
mediaPickerUploadHint,
|
||||
mediaPickerEmptyMessage,
|
||||
mediaPickerApplyState,
|
||||
isMediaItemSelectableForCurrentTarget,
|
||||
isUploadFileAllowedForPicker,
|
||||
openMediaPickerState,
|
||||
closeMediaPickerState,
|
||||
applyMediaPickerResponse,
|
||||
setMediaPickerPage,
|
||||
toggleMediaSelection,
|
||||
resolveCurrentSelectedMediaItems,
|
||||
selectUploadedMediaFiles,
|
||||
prependUploadedMediaItems,
|
||||
getGallerySelectionOrder
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user