<script setup>
definePageMeta({
  layout: 'admin'
})

/** 서버 `MEDIA_THUMBNAIL_ROOT`와 동일한 썸네일 논리 폴더 라벨 */
const MEDIA_THUMBNAIL_ROOT = '썸네일'

/**
 * 논리 폴더 경로가 썸네일 전용 루트인지 확인한다.
 * @param {string} folder - 폴더 경로
 * @returns {boolean} 썸네일 전용이면 true
 */
const isThumbnailFolderPath = (folder) => folder === MEDIA_THUMBNAIL_ROOT || String(folder).startsWith(`${MEDIA_THUMBNAIL_ROOT}/`)

const activeTab = ref('library')
const searchText = ref('')
const activeFolder = ref('')
const activeMediaKind = ref('all')
const showUnusedOnly = ref(false)
const isCreateFolderModalOpen = ref(false)
const createFolderModalName = ref('')
const deletingFolder = ref('')
const editingUrl = ref('')
const editingName = ref('')
const editingCategory = ref('')
const deletingUrl = ref('')
const selectedMediaUrl = ref('')
const selectedMediaUrls = ref([])
const lastSelectedIndex = ref(-1)
const draggingUrls = ref([])
const uploadInputRef = ref(null)
const uploadingFiles = ref(false)
const deletingSelected = ref(false)

const { toast, showToast } = useAdminToast()

const { openMenuId, closeMenu } = useAdminRowMenu()

/**
 * 폴더 행 메뉴 id
 * @param {string} folder - 폴더 경로
 * @returns {string}
 */
const getFolderMenuId = (folder) => `folder:${folder}`

const { data: mediaItems, refresh } = await useFetch('/admin/api/media', {
  default: () => []
})

/**
 * 썸네일 디스크 경로 여부
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 회원 아바타 경로이면 true
 */
const isThumbnailDiskItem = (item) => Boolean(item?.url?.includes('/members/avatars/'))

/**
 * 게시물 카드 썸네일 디스크 경로 여부
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 게시물 카드 썸네일이면 true
 */
const isPostCardThumbnailDiskItem = (item) => Boolean(item?.url?.includes('/posts/') && item?.url?.includes('/thumbs/') && item?.url?.endsWith('-card.webp'))

/**
 * 미디어 항목이 시스템 파생 파일인지 확인한다.
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 시스템 관리 항목이면 true
 */
const isSystemManagedMediaItem = (item) => Boolean(item?.avatarOwner) || isPostCardThumbnailDiskItem(item)

/**
 * 파일명 변경·삭제·드래그 이동이 제한되는지 여부
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 잠금이면 true
 */
const isMediaItemLocked = (item) => Boolean(item?.usage?.length) || Boolean(item?.avatarOwner)

/**
 * 미디어 파일명을 변경할 수 있는지 확인한다.
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 변경 가능 여부
 */
const canRenameMediaItem = (item) => !isMediaItemLocked(item) && !isPostCardThumbnailDiskItem(item)

/**
 * 미디어 논리 폴더를 변경할 수 있는지 확인한다.
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 변경 가능 여부
 */
const canEditMediaCategory = (item) => !isSystemManagedMediaItem(item)

/**
 * 미디어 카드 배지 라벨을 반환한다.
 * @param {Object} item - 미디어 항목
 * @returns {string} 배지 라벨
 */
const getMediaBadgeLabel = (item) => {
  if (item?.avatarOwner) {
    return '회원'
  }

  if (item?.thumbnailStatus?.role === 'card') {
    return '카드'
  }

  if (item?.thumbnailStatus?.isFallbackActive) {
    return '원본'
  }

  if (item?.usage?.length) {
    return String(item.usage.length)
  }

  return ''
}

/**
 * 미디어 카드 배지 클래스를 반환한다.
 * @param {Object} item - 미디어 항목
 * @returns {string} Tailwind 클래스
 */
const getMediaBadgeClass = (item) => {
  if (item?.thumbnailStatus?.isFallbackActive) {
    return 'bg-amber-600 text-white'
  }

  if (item?.thumbnailStatus?.role === 'card') {
    return 'bg-sky-700 text-white'
  }

  if (item?.avatarOwner) {
    return 'bg-emerald-800 text-white'
  }

  return 'bg-[#15171a] text-white'
}

/**
 * 미디어 항목 종류를 반환한다.
 * @param {Object} item - 미디어 항목
 * @returns {'image'|'video'|'audio'|'file'} 미디어 종류
 */
const getMediaItemKind = (item) => item?.kind || 'image'

const libraryMediaItems = computed(() => (mediaItems.value || []).filter((item) => !isThumbnailDiskItem(item) && !isPostCardThumbnailDiskItem(item)))

const postThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostCardThumbnailDiskItem(item)))

const thumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isThumbnailDiskItem(item)))

const scopeItems = computed(() => {
  if (activeTab.value === 'thumbnails') {
    return thumbnailMediaItems.value
  }

  if (activeTab.value === 'postThumbnails') {
    return postThumbnailMediaItems.value
  }

  return libraryMediaItems.value
})

const systemManagedTab = computed(() => activeTab.value === 'thumbnails' || activeTab.value === 'postThumbnails')

const activeSystemMediaCount = computed(() => activeTab.value === 'postThumbnails'
  ? postThumbnailMediaItems.value.length
  : thumbnailMediaItems.value.length)

const activeSystemMediaTitle = computed(() => activeTab.value === 'postThumbnails' ? '카드 썸네일' : '프로필 이미지')

const activeSystemMediaHint = computed(() => {
  if (activeTab.value === 'postThumbnails') {
    return '게시물 목록 카드에서 쓰는 자동 생성 이미지입니다. 원본 대표 이미지가 사용 중이고 이 썸네일 파일이 있으면 목록에서는 원본 대신 이 파일을 불러옵니다. 원본에 연결된 썸네일은 사용 중으로 표시되며, 썸네일이 없는 원본은 일반 미디어 상세에서 다시 생성 필요 상태로 구분합니다.'
  }

  return '회원 프로필에서 쓰는 이미지는 연결 회원이 있을 때만 삭제·이름 변경이 막힙니다. 프로필에서 바꾸거나 해제해도 디스크 파일은 삭제되지 않으며 이 목록에 남습니다. 목록이 바로 안 바뀌면 페이지를 새로고침하세요. 관리자는 필요 시 삭제·다운로드로 정리할 수 있습니다.'
})

const mediaKindFilterOptions = computed(() => {
  const baseItems = scopeItems.value.filter((item) => {
    const folder = activeFolder.value

    return systemManagedTab.value
      ? true
      : (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
  })
  const countByKind = baseItems.reduce((counts, item) => {
    const kind = getMediaItemKind(item)
    counts[kind] = (counts[kind] || 0) + 1
    return counts
  }, {})

  return [
    { id: 'all', label: '전체', count: baseItems.length },
    { id: 'image', label: '이미지', count: countByKind.image || 0 },
    { id: 'video', label: '영상', count: countByKind.video || 0 },
    { id: 'audio', label: '음악', count: countByKind.audio || 0 },
    { id: 'file', label: '파일', count: countByKind.file || 0 }
  ]
})

const unusedMediaCount = computed(() => scopeItems.value.filter((item) => {
  const folder = activeFolder.value
  const matchesFolder = systemManagedTab.value
    ? true
    : (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))

  return matchesFolder && !isMediaItemLocked(item)
}).length)

/**
 * 상단 탭 전환 시 목록 상태를 초기화한다.
 * @param {'library' | 'postThumbnails' | 'thumbnails'} tab - 선택 탭
 * @returns {void}
 */
const setActiveTab = (tab) => {
  if (activeTab.value === tab) {
    return
  }

  activeTab.value = tab
  activeFolder.value = ''
  searchText.value = ''
  activeMediaKind.value = 'all'
  showUnusedOnly.value = false
  clearMediaSelection()
  closeMediaDetail()
}

/**
 * 미디어 종류 필터를 선택한다.
 * @param {'all'|'image'|'video'|'audio'|'file'} kind - 선택할 미디어 종류
 * @returns {void}
 */
const setMediaKindFilter = (kind) => {
  activeMediaKind.value = kind
  clearMediaSelection()
}

/**
 * 미사용 미디어만 보기 필터를 토글한다.
 * @returns {void}
 */
const toggleUnusedMediaFilter = () => {
  showUnusedOnly.value = !showUnusedOnly.value
  clearMediaSelection()
}

/**
 * ISO 시각을 짧은 로캘 문자열로 표시한다.
 * @param {string | null} iso - ISO 시각
 * @returns {string} 표시 문자열
 */
const formatDateTime = (iso) => {
  if (!iso) {
    return '—'
  }

  try {
    return new Date(iso).toLocaleString('ko-KR', {
      dateStyle: 'medium',
      timeStyle: 'short'
    })
  } catch {
    return '—'
  }
}

/**
 * 상세 모달에서 선택된 미디어를 브라우저로 내려받는다.
 * @returns {void}
 */
const downloadSelectedMedia = () => {
  const item = selectedMedia.value

  if (!item?.url || !import.meta.client) {
    return
  }

  const anchor = document.createElement('a')
  anchor.href = item.url
  anchor.download = item.name || 'image'
  anchor.rel = 'noopener noreferrer'
  anchor.target = '_blank'
  document.body.appendChild(anchor)
  anchor.click()
  document.body.removeChild(anchor)
}

const { data: mediaFolders, refresh: refreshMediaFolders } = await useFetch('/admin/api/media-folders', {
  default: () => ['미분류']
})

const selectedMedia = computed(() => mediaItems.value.find((item) => item.url === selectedMediaUrl.value) || null)

const normalizedFolders = computed(() => {
  const folderSet = new Set(['미분류'])

  mediaFolders.value.forEach((folder) => {
    String(folder || '').split('/').filter(Boolean).reduce((parentPath, segment) => {
      const nextPath = parentPath ? `${parentPath}/${segment}` : segment
      folderSet.add(nextPath)
      return nextPath
    }, '')
  })

  libraryMediaItems.value.forEach((item) => {
    String(item.category || '미분류').split('/').filter(Boolean).reduce((parentPath, segment) => {
      const nextPath = parentPath ? `${parentPath}/${segment}` : segment
      folderSet.add(nextPath)
      return nextPath
    }, '')
  })

  return [...folderSet].sort((left, right) => left.localeCompare(right))
})

const folderMediaCounts = computed(() => normalizedFolders.value.reduce((counts, folder) => {
  counts[folder] = libraryMediaItems.value.filter((item) => item.category === folder || item.category?.startsWith(`${folder}/`)).length
  return counts
}, {}))

const filteredMediaItems = computed(() => {
  const query = searchText.value.trim().toLowerCase()
  const folder = activeFolder.value
  const mediaKind = activeMediaKind.value
  const unusedOnly = showUnusedOnly.value
  const base = scopeItems.value

  return base.filter((item) => {
    const matchesFolder = systemManagedTab.value
      ? true
      : (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
    const usageTitles = item.usage?.map((usage) => usage.title) || []
    const matchesQuery = !query || [
      item.name,
      ...usageTitles
    ].some((value) => String(value || '').toLowerCase().includes(query))
    const matchesKind = mediaKind === 'all' || getMediaItemKind(item) === mediaKind
    const matchesUsage = !unusedOnly || !isMediaItemLocked(item)

    return matchesFolder && matchesQuery && matchesKind && matchesUsage
  })
})

const filteredMediaUrls = computed(() => filteredMediaItems.value.map((item) => item.url))

const isAllFilteredMediaSelected = computed(() => {
  if (!filteredMediaUrls.value.length) {
    return false
  }

  return filteredMediaUrls.value.every((url) => selectedMediaUrls.value.includes(url))
})

const selectedDeletableMediaItems = computed(() => {
  const selectedUrlSet = new Set(selectedMediaUrls.value)

  return filteredMediaItems.value.filter((item) => selectedUrlSet.has(item.url) && !isMediaItemLocked(item))
})

/**
 * 파일 크기 표시 문자열 생성
 * @param {number} size - 파일 크기
 * @returns {string} 표시 문자열
 */
const formatFileSize = (size) => {
  if (size < 1024) {
    return `${size} B`
  }

  if (size < 1024 * 1024) {
    return `${(size / 1024).toFixed(1)} KB`
  }

  return `${(size / 1024 / 1024).toFixed(1)} MB`
}

/**
 * 폴더 경로의 표시 이름 조회
 * @param {string} folder - 폴더 경로
 * @returns {string} 표시 이름
 */
const getFolderName = (folder) => folder.split('/').filter(Boolean).pop() || '미분류'

/**
 * 폴더 깊이 조회
 * @param {string} folder - 폴더 경로
 * @returns {number} 폴더 깊이
 */
const getFolderDepth = (folder) => Math.max(folder.split('/').filter(Boolean).length - 1, 0)

/**
 * 미디어 선택 여부 확인
 * @param {Object} item - 미디어 항목
 * @returns {boolean} 선택 여부
 */
const isMediaSelected = (item) => selectedMediaUrls.value.includes(item.url)

/**
 * 폴더 선택
 * @param {string} folder - 폴더 경로
 * @returns {void}
 */
const selectFolder = (folder) => {
  activeFolder.value = folder
  activeMediaKind.value = 'all'
  showUnusedOnly.value = false
  selectedMediaUrls.value = []
  lastSelectedIndex.value = -1
}

/**
 * 체크박스로 미디어 선택 토글(Shift 시 범위 추가 선택)
 * @param {Object} item - 미디어 항목
 * @param {number} index - 필터 목록 내 순서
 * @param {MouseEvent} event - 클릭 이벤트
 * @returns {void}
 */
const toggleMediaSelection = (item, index, event) => {
  if (event.shiftKey && lastSelectedIndex.value >= 0) {
    const startIndex = Math.min(lastSelectedIndex.value, index)
    const endIndex = Math.max(lastSelectedIndex.value, index)
    const rangeUrls = filteredMediaItems.value.slice(startIndex, endIndex + 1).map((mediaItem) => mediaItem.url)
    const merged = new Set([...selectedMediaUrls.value, ...rangeUrls])
    selectedMediaUrls.value = [...merged]
    lastSelectedIndex.value = index
    return
  }

  selectedMediaUrls.value = isMediaSelected(item)
    ? selectedMediaUrls.value.filter((url) => url !== item.url)
    : [...selectedMediaUrls.value, item.url]
  lastSelectedIndex.value = index
}

/**
 * 선택된 미디어 해제
 * @returns {void}
 */
const clearMediaSelection = () => {
  selectedMediaUrls.value = []
  lastSelectedIndex.value = -1
}

/**
 * 현재 필터링된 미디어 전체 선택을 토글한다.
 * @returns {void}
 */
const toggleFilteredMediaSelection = () => {
  if (!filteredMediaUrls.value.length) {
    return
  }

  if (isAllFilteredMediaSelected.value) {
    const filteredUrlSet = new Set(filteredMediaUrls.value)
    selectedMediaUrls.value = selectedMediaUrls.value.filter((url) => !filteredUrlSet.has(url))
    lastSelectedIndex.value = -1
    return
  }

  selectedMediaUrls.value = [...new Set([
    ...selectedMediaUrls.value,
    ...filteredMediaUrls.value
  ])]
  lastSelectedIndex.value = filteredMediaItems.value.length - 1
}

/**
 * 파일 직접 업로드 선택창을 연다.
 * @returns {void}
 */
const openUploadFilePicker = () => {
  if (activeTab.value !== 'library') {
    return
  }

  uploadInputRef.value?.click()
}

/**
 * 선택된 파일을 미디어 라이브러리에 업로드한다.
 * @param {Event} event - 파일 입력 변경 이벤트
 * @returns {Promise<void>}
 */
const uploadSelectedFiles = async (event) => {
  if (activeTab.value !== 'library') {
    return
  }

  const input = event.target

  if (!(input instanceof HTMLInputElement) || !input.files?.length) {
    return
  }

  const files = Array.from(input.files)
  const formData = new FormData()

  files.forEach((file) => {
    formData.append('files', file)
  })

  uploadingFiles.value = true

  try {
    const result = await $fetch('/admin/api/uploads', {
      method: 'POST',
      body: formData
    })
    const uploadedUrls = Array.isArray(result?.files)
      ? result.files.map((file) => file.url).filter(Boolean)
      : []

    if (activeFolder.value && uploadedUrls.length) {
      await $fetch('/admin/api/media', {
        method: 'PUT',
        body: {
          urls: uploadedUrls,
          category: activeFolder.value
        }
      })
    }

    await Promise.all([
      refresh(),
      refreshMediaFolders()
    ])
    selectedMediaUrls.value = uploadedUrls
    showToast('success', `${files.length}개 파일을 추가했습니다.`)
  } catch (error) {
    showToast('error', error?.data?.message || '파일을 업로드하지 못했습니다.')
  } finally {
    uploadingFiles.value = false
    input.value = ''
  }
}

/**
 * 미디어 상세 모달 열기
 * @param {Object} item - 미디어 항목
 * @returns {void}
 */
const openMediaDetail = (item) => {
  selectedMediaUrl.value = item.url
  editingUrl.value = item.url
  editingName.value = item.title
  editingCategory.value = item.category
}

/**
 * 미디어 상세 모달 닫기
 * @returns {void}
 */
const closeMediaDetail = () => {
  selectedMediaUrl.value = ''
  cancelRename()
}

/**
 * 미디어 파일명 변경 취소
 * @returns {void}
 */
const cancelRename = () => {
  editingUrl.value = ''
  editingName.value = ''
}

/**
 * 폴더 추가 모달을 연다
 * @returns {void}
 */
const openCreateFolderModal = () => {
  createFolderModalName.value = ''
  isCreateFolderModalOpen.value = true
}

/**
 * 폴더 추가 모달을 닫는다
 * @returns {void}
 */
const closeCreateFolderModal = () => {
  isCreateFolderModalOpen.value = false
  createFolderModalName.value = ''
}

/**
 * 미디어 폴더 생성(모달에서 확인 시)
 * @returns {Promise<void>}
 */
const submitCreateFolderModal = async () => {
  if (activeTab.value !== 'library') {
    return
  }

  const folderName = createFolderModalName.value.trim()

  if (!folderName) {
    return
  }

  try {
    const folderPath = activeFolder.value ? `${activeFolder.value}/${folderName}` : folderName
    const createdFolder = await $fetch('/admin/api/media-folders', {
      method: 'POST',
      body: {
        path: folderPath
      }
    })

    closeCreateFolderModal()
    activeFolder.value = createdFolder.path
    await refreshMediaFolders()
  } catch (error) {
    showToast('error', error?.data?.message || '폴더를 만들지 못했습니다.')
  }
}

/**
 * 미디어 폴더 삭제
 * @param {string} folder - 삭제할 폴더 경로
 * @returns {Promise<void>}
 */
const removeMediaFolder = async (folder) => {
  closeMenu()

  if (!folder || folder === '미분류' || folder === MEDIA_THUMBNAIL_ROOT || folder.startsWith(`${MEDIA_THUMBNAIL_ROOT}/`)) {
    return
  }

  if (!confirm(`"${folder}" 폴더를 삭제할까요? 이 폴더(및 하위 경로)에 속한 미디어는 모두 "미분류"로 옮겨집니다.`)) {
    return
  }

  deletingFolder.value = folder

  try {
    await $fetch('/admin/api/media-folders', {
      method: 'DELETE',
      body: {
        path: folder
      }
    })
    if (activeFolder.value === folder || activeFolder.value.startsWith(`${folder}/`)) {
      activeFolder.value = ''
    }
    selectedMediaUrls.value = []
    lastSelectedIndex.value = -1
    await Promise.all([
      refresh(),
      refreshMediaFolders()
    ])
  } catch (error) {
    showToast('error', error?.data?.message || '폴더를 삭제하지 못했습니다.')
  } finally {
    deletingFolder.value = ''
  }
}

/**
 * 선택한 미디어를 폴더로 이동
 * @param {string} folder - 폴더 경로
 * @param {Array<string>} urls - 이동할 미디어 URL 목록
 * @returns {Promise<void>}
 */
const moveMediaToFolder = async (folder, urls = selectedMediaUrls.value) => {
  if (activeTab.value !== 'library') {
    return
  }

  const targetUrls = [...new Set(urls.filter(Boolean))]

  if (!targetUrls.length) {
    return
  }

  try {
    await $fetch('/admin/api/media', {
      method: 'PUT',
      body: {
        urls: targetUrls,
        category: folder || '미분류'
      }
    })
    await Promise.all([
      refresh(),
      refreshMediaFolders()
    ])
    activeFolder.value = folder || '미분류'
    clearMediaSelection()
  } catch (error) {
    showToast('error', error?.data?.message || '미디어 폴더를 변경하지 못했습니다.')
  }
}

/**
 * 미디어 드래그 시작
 * @param {DragEvent} event - 드래그 이벤트
 * @param {Object} item - 미디어 항목
 * @returns {void}
 */
const startMediaDrag = (event, item) => {
  if (activeTab.value !== 'library') {
    return
  }

  if (!isMediaSelected(item)) {
    selectedMediaUrls.value = [item.url]
  }

  draggingUrls.value = [...selectedMediaUrls.value]

  if (event.dataTransfer) {
    event.dataTransfer.effectAllowed = 'move'
    event.dataTransfer.setData('text/plain', draggingUrls.value.join('\n'))
  }
}

/**
 * 폴더로 미디어 드롭
 * @param {string} folder - 폴더 경로
 * @returns {Promise<void>}
 */
const dropMediaOnFolder = async (folder) => {
  if (activeTab.value !== 'library') {
    draggingUrls.value = []
    return
  }

  const urls = draggingUrls.value.length ? draggingUrls.value : selectedMediaUrls.value
  draggingUrls.value = []
  await moveMediaToFolder(folder, urls)
}

/**
 * 미디어 카테고리 저장
 * @returns {Promise<void>} 저장 결과
 */
const saveMediaCategory = async () => {
  if (!canEditMediaCategory(selectedMedia.value)) {
    return
  }

  try {
    await $fetch('/admin/api/media', {
      method: 'PUT',
      body: {
        url: selectedMedia.value.url,
        category: editingCategory.value
      }
    })
    await Promise.all([
      refresh(),
      refreshMediaFolders()
    ])
  } catch (error) {
    showToast('error', error?.data?.message || '카테고리를 저장하지 못했습니다.')
  }
}

/**
 * 미디어 파일명 변경
 * @returns {Promise<void>}
 */
const renameMedia = async () => {
  const editingItem = mediaItems.value.find((item) => item.url === editingUrl.value)

  if (editingItem && !canRenameMediaItem(editingItem)) {
    showToast('error', '사용 중인 미디어 또는 카드 썸네일은 파일명을 바꿀 수 없습니다.')
    return
  }

  try {
    const renamedItem = await $fetch('/admin/api/media', {
      method: 'PUT',
      body: {
        url: editingUrl.value,
        name: editingName.value
      }
    })
    cancelRename()
    await refresh()
    selectedMediaUrl.value = renamedItem.url
  } catch (error) {
    showToast('error', error?.data?.message || '파일명을 변경하지 못했습니다.')
  }
}

/**
 * 미디어 삭제
 * @param {Object} item - 미디어 항목
 * @returns {Promise<void>}
 */
const deleteMedia = async (item) => {
  if (isMediaItemLocked(item)) {
    showToast('error', '게시물·페이지에서 쓰이거나, 회원 프로필에 연결된 썸네일은 삭제할 수 없습니다.')
    return
  }

  if (!confirm(`"${item.name}" 파일을 삭제할까요?`)) {
    return
  }

  deletingUrl.value = item.url

  try {
    await $fetch('/admin/api/media', {
      method: 'DELETE',
      body: {
        url: item.url
      }
    })
    closeMediaDetail()
    await refresh()
  } catch (error) {
    showToast('error', error?.data?.message || '파일을 삭제하지 못했습니다.')
  } finally {
    deletingUrl.value = ''
  }
}

/**
 * 선택한 미디어 중 삭제 가능한 항목을 한 번에 삭제한다.
 * @returns {Promise<void>}
 */
const deleteSelectedMedia = async () => {
  const items = selectedDeletableMediaItems.value
  const lockedCount = selectedMediaUrls.value.length - items.length

  if (!items.length) {
    showToast('error', '삭제할 수 있는 선택 항목이 없습니다.')
    return
  }

  const confirmMessage = lockedCount > 0
    ? `${items.length}개 파일을 삭제할까요? 잠긴 ${lockedCount}개 항목은 제외됩니다.`
    : `${items.length}개 파일을 삭제할까요?`

  if (!confirm(confirmMessage)) {
    return
  }

  deletingSelected.value = true

  try {
    for (const item of items) {
      await $fetch('/admin/api/media', {
        method: 'DELETE',
        body: {
          url: item.url
        }
      })
    }

    closeMediaDetail()
    clearMediaSelection()
    await Promise.all([
      refresh(),
      refreshMediaFolders()
    ])
    showToast('success', `${items.length}개 파일을 삭제했습니다.`)
  } catch (error) {
    showToast('error', error?.data?.message || '선택한 파일을 삭제하지 못했습니다.')
  } finally {
    deletingSelected.value = false
  }
}

watch(filteredMediaUrls, (urls) => {
  const visibleUrlSet = new Set(urls)
  selectedMediaUrls.value = selectedMediaUrls.value.filter((url) => visibleUrlSet.has(url))

  if (!selectedMediaUrls.value.length) {
    lastSelectedIndex.value = -1
  }
})
</script>

<template>
  <section class="admin-media bg-paper p-6">
    <div class="admin-media__header flex flex-col gap-4">
      <div class="flex flex-wrap items-center justify-between gap-4">
        <div>
          <p class="admin-media__eyebrow text-xs font-semibold uppercase text-muted">
            Media
          </p>
          <h1 class="admin-media__title mt-2 text-3xl font-semibold">
            미디어
          </h1>
          <div class="admin-media__tabs mt-3 inline-flex rounded-lg border border-line bg-surface p-0.5">
            <button
              class="admin-media__tab rounded-md px-3 py-1.5 text-xs font-semibold transition"
              :class="activeTab === 'library' ? 'bg-[#15171a] text-white shadow-sm' : 'text-muted hover:text-ink'"
              type="button"
              @click="setActiveTab('library')"
            >
              미디어 라이브러리
            </button>
            <button
              class="admin-media__tab rounded-md px-3 py-1.5 text-xs font-semibold transition"
              :class="activeTab === 'postThumbnails' ? 'bg-[#15171a] text-white shadow-sm' : 'text-muted hover:text-ink'"
              type="button"
              @click="setActiveTab('postThumbnails')"
            >
              카드 썸네일
            </button>
            <button
              class="admin-media__tab rounded-md px-3 py-1.5 text-xs font-semibold transition"
              :class="activeTab === 'thumbnails' ? 'bg-[#15171a] text-white shadow-sm' : 'text-muted hover:text-ink'"
              type="button"
              @click="setActiveTab('thumbnails')"
            >
              프로필 이미지
            </button>
          </div>
        </div>
        <div class="admin-media__header-actions flex min-w-0 flex-wrap items-center gap-2">
          <label class="admin-media__search relative">
            <span class="sr-only">미디어 검색</span>
            <svg class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[#8e9cac]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
              <path d="m21 21-4.34-4.34" />
              <circle cx="11" cy="11" r="8" />
            </svg>
            <input
              v-model="searchText"
              class="admin-media__search-input h-10 w-full rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac] md:w-72"
              type="search"
              placeholder="미디어 검색"
            >
          </label>
          <button
            v-if="activeTab === 'library'"
            class="admin-media__upload-button h-10 shrink-0 rounded bg-[#15171a] px-4 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50"
            type="button"
            :disabled="uploadingFiles"
            @click="openUploadFilePicker"
          >
            {{ uploadingFiles ? '추가 중' : '파일 추가' }}
          </button>
          <input
            ref="uploadInputRef"
            class="hidden"
            type="file"
            multiple
            @change="uploadSelectedFiles"
          >
        </div>
      </div>
      <div
        v-if="activeTab === 'library'"
        class="admin-media__filters flex flex-wrap gap-1.5"
      >
        <button
          v-for="option in mediaKindFilterOptions"
          :key="option.id"
          class="admin-media__kind-filter rounded-full border px-3 py-1.5 text-xs font-semibold transition"
          :class="activeMediaKind === option.id ? 'border-[#15171a] bg-[#15171a] text-white' : 'border-line bg-white text-muted hover:text-ink'"
          type="button"
          @click="setMediaKindFilter(option.id)"
        >
          {{ option.label }}
          <span class="ml-1 opacity-70">{{ option.count }}</span>
        </button>
        <button
          class="admin-media__unused-filter rounded-full border px-3 py-1.5 text-xs font-semibold transition"
          :class="showUnusedOnly ? 'border-[#15171a] bg-[#15171a] text-white' : 'border-line bg-white text-muted hover:text-ink'"
          type="button"
          :aria-pressed="showUnusedOnly"
          @click="toggleUnusedMediaFilter"
        >
          미사용
          <span class="ml-1 opacity-70">{{ unusedMediaCount }}</span>
        </button>
      </div>
    </div>

    <div class="admin-media__layout mt-8 grid gap-5 lg:grid-cols-[16rem_minmax(0,1fr)]">
      <aside
        v-if="activeTab === 'library'"
        class="admin-media__folders rounded border border-line bg-white p-3"
      >
        <button
          class="admin-media__folder-button flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm font-semibold hover:bg-surface"
          :class="!activeFolder ? 'bg-[#15171a] text-white hover:bg-[#15171a]' : 'text-ink'"
          type="button"
          @click="selectFolder('')"
          @dragover.prevent
          @drop.prevent="dropMediaOnFolder('미분류')"
        >
          <span>전체 미디어</span>
          <span>{{ libraryMediaItems.length }}</span>
        </button>

        <div class="admin-media__folder-list mt-3 grid gap-1">
          <div
            v-for="folder in normalizedFolders"
            :key="folder"
            class="admin-media__folder-row flex items-stretch rounded"
            :class="activeFolder === folder ? 'bg-[#15171a] text-white' : 'text-ink hover:bg-surface'"
          >
            <button
              class="admin-media__folder-button flex min-w-0 flex-1 items-center justify-between rounded py-2 pr-3 text-left text-sm"
              :style="{ paddingLeft: `${12 + getFolderDepth(folder) * 14}px` }"
              type="button"
              @click="selectFolder(folder)"
              @dragover.prevent
              @drop.prevent="dropMediaOnFolder(folder)"
            >
              <span class="admin-media__folder-name min-w-0 truncate">{{ getFolderName(folder) }}</span>
              <span class="admin-media__folder-count shrink-0 text-xs opacity-70">{{ folderMediaCounts[folder] || 0 }}</span>
            </button>
            <AdminRowMoreMenu
              v-if="folder !== '미분류' && !isThumbnailFolderPath(folder)"
              v-model:open-menu-id="openMenuId"
              :item-id="getFolderMenuId(folder)"
              menu-label="폴더 메뉴"
              size="sm"
              class="mr-0.5 shrink-0"
              :inverse="activeFolder === folder"
              :busy="deletingFolder === folder"
            >
              <button
                class="admin-row-more-menu__item admin-row-more-menu__item--danger"
                type="button"
                role="menuitem"
                :disabled="deletingFolder === folder"
                @click="removeMediaFolder(folder)"
              >
                폴더 삭제
              </button>
            </AdminRowMoreMenu>
          </div>
        </div>

        <div v-if="activeTab === 'library'" class="admin-media__folder-actions mt-4 border-t border-line pt-4">
          <button
            class="admin-media__folder-add w-full rounded bg-[#15171a] px-3 py-2 text-xs font-semibold text-white"
            type="button"
            @click="openCreateFolderModal"
          >
            폴더 추가
          </button>
        </div>
      </aside>

      <aside
        v-else
        class="admin-media__folders admin-media__folders--thumbnails rounded border border-line bg-white p-3"
      >
        <button
          class="admin-media__folder-button flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm font-semibold bg-[#15171a] text-white hover:bg-[#15171a]"
          type="button"
          @click="selectFolder('')"
        >
          <span>{{ activeSystemMediaTitle }}</span>
          <span>{{ activeSystemMediaCount }}</span>
        </button>
        <p class="admin-media__thumb-hint mt-4 text-xs leading-relaxed text-muted">
          {{ activeSystemMediaHint }}
        </p>
      </aside>

      <div class="admin-media__content min-w-0">
        <div class="admin-media__toolbar flex flex-wrap items-center justify-between gap-3">
          <div>
            <h2 class="admin-media__folder-title text-lg font-semibold text-ink">
              {{ systemManagedTab ? activeSystemMediaTitle : (activeFolder || '전체 미디어') }}
            </h2>
            <p class="admin-media__folder-summary mt-1 text-xs text-muted">
              {{ filteredMediaItems.length }}개 표시
            </p>
          </div>
          <div class="admin-media__selection-actions flex flex-wrap items-center justify-end gap-2">
            <button
              v-if="filteredMediaItems.length"
              class="admin-media__select-all rounded border border-line bg-white px-3 py-2 text-xs font-semibold text-ink transition hover:border-[#c8ced3] disabled:cursor-not-allowed disabled:opacity-50"
              type="button"
              :aria-pressed="isAllFilteredMediaSelected"
              @click="toggleFilteredMediaSelection"
            >
              {{ isAllFilteredMediaSelected ? '전체 해제' : '전체 선택' }}
            </button>
            <div v-if="selectedMediaUrls.length" class="admin-media__selection flex flex-wrap items-center gap-2 rounded border border-line bg-white px-3 py-2 text-xs">
              <strong class="admin-media__selection-count text-ink">{{ selectedMediaUrls.length }}개 선택됨</strong>
              <button
                class="admin-media__selection-delete font-semibold text-red-600 hover:text-red-700 disabled:cursor-not-allowed disabled:opacity-50"
                type="button"
                :disabled="deletingSelected"
                @click="deleteSelectedMedia"
              >
                {{ deletingSelected ? '삭제 중' : '선택 삭제' }}
              </button>
              <button class="admin-media__selection-clear font-semibold text-muted hover:text-ink" type="button" @click="clearMediaSelection">
                선택 해제
              </button>
            </div>
          </div>
        </div>

        <div v-if="filteredMediaItems.length" class="admin-media__grid mt-5 grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
          <div
            v-for="(item, index) in filteredMediaItems"
            :key="item.url"
            class="admin-media__item group relative overflow-hidden border border-line bg-white text-left outline-none transition hover:border-[#15171a]"
            :draggable="activeTab === 'library'"
            @dragstart="startMediaDrag($event, item)"
          >
            <button
              type="button"
              class="admin-media__select-toggle absolute left-1.5 top-1.5 z-10 grid size-7 place-items-center rounded-md border-2 shadow-md outline-none transition focus-visible:ring-2 focus-visible:ring-[#15171a] focus-visible:ring-offset-1"
              :class="isMediaSelected(item)
                ? 'border-white bg-[#15171a] text-white'
                : 'border-[#394047] bg-white/95 text-[#15171a] hover:border-[#15171a] hover:bg-white'"
              :aria-label="`${item.name} 선택`"
              :aria-pressed="isMediaSelected(item)"
              @click.stop="toggleMediaSelection(item, index, $event)"
            >
              <svg
                v-if="isMediaSelected(item)"
                class="size-4 shrink-0"
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2.5"
                stroke-linecap="round"
                stroke-linejoin="round"
                aria-hidden="true"
              >
                <path d="M20 6 9 17l-5-5" />
              </svg>
            </button>
            <button
              class="admin-media__thumb relative flex w-full flex-col text-left outline-none"
              type="button"
              @click="openMediaDetail(item)"
            >
              <img
                v-if="getMediaItemKind(item) === 'image'"
                class="admin-media__image aspect-square w-full bg-surface object-cover"
                :src="item.url"
                :alt="item.title"
              >
              <AdminMediaVideoThumbnail
                v-else-if="getMediaItemKind(item) === 'video'"
                :src="item.url"
                :alt="item.title"
              />
              <span
                v-else
                class="admin-media__image flex aspect-square w-full items-center justify-center bg-surface text-xs font-bold uppercase tracking-[0.18em] text-muted"
              >
                {{ getMediaItemKind(item) }}
              </span>
              <span
                v-if="getMediaBadgeLabel(item)"
                class="admin-media__usage-badge pointer-events-none absolute right-1.5 top-1.5 rounded px-1.5 py-0.5 text-[10px] font-semibold"
                :class="getMediaBadgeClass(item)"
              >
                {{ getMediaBadgeLabel(item) }}
              </span>
              <span class="admin-media__name block truncate px-2 py-1.5 text-xs font-semibold text-ink">
                {{ item.name }}
              </span>
            </button>
          </div>
        </div>

        <p v-else class="admin-media__empty mt-5 border border-dashed border-line bg-white p-8 text-center text-sm text-muted">
          표시할 미디어가 없습니다.
        </p>
      </div>
    </div>

    <div
      v-if="isCreateFolderModalOpen"
      class="admin-media__folder-create-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8"
      role="dialog"
      aria-modal="true"
      aria-labelledby="media-folder-modal-title"
      @click.self="closeCreateFolderModal"
    >
      <section class="admin-media__folder-create-panel w-full max-w-sm rounded border border-line bg-white p-5 shadow-xl">
        <h3 id="media-folder-modal-title" class="text-lg font-semibold text-ink">
          새 폴더
        </h3>
        <p class="mt-1 text-xs text-muted">
          {{ activeFolder ? `${activeFolder} 아래에 하위 폴더를 만듭니다.` : '최상위 폴더를 만듭니다.' }}
        </p>
        <label class="mt-4 grid gap-1.5 text-sm">
          <span class="text-xs font-semibold text-muted">폴더 이름</span>
          <input
            v-model="createFolderModalName"
            class="rounded border border-line px-3 py-2 text-sm outline-none focus:border-[#8e9cac]"
            type="text"
            placeholder="폴더 이름"
            @keydown.enter.prevent="submitCreateFolderModal"
          >
        </label>
        <div class="mt-5 flex justify-end gap-2">
          <button class="rounded border border-line px-3 py-2 text-xs font-semibold text-ink" type="button" @click="closeCreateFolderModal">
            취소
          </button>
          <button
            class="rounded bg-[#15171a] px-3 py-2 text-xs font-semibold text-white disabled:opacity-50"
            type="button"
            :disabled="!createFolderModalName.trim()"
            @click="submitCreateFolderModal"
          >
            만들기
          </button>
        </div>
      </section>
    </div>

    <div
      v-if="selectedMedia"
      class="admin-media__modal fixed inset-0 z-50 grid place-items-center bg-black/40 px-5 py-8"
      role="dialog"
      aria-modal="true"
      @click.self="closeMediaDetail"
    >
      <section class="admin-media__modal-panel grid max-h-[86vh] w-full max-w-5xl overflow-hidden bg-white text-ink shadow-xl lg:grid-cols-[minmax(0,1fr)_22rem]">
        <div class="admin-media__preview grid min-h-[20rem] place-items-center bg-[#f5f5f2] p-5">
          <img
            v-if="getMediaItemKind(selectedMedia) === 'image'"
            class="admin-media__preview-image max-h-[72vh] max-w-full object-contain"
            :src="selectedMedia.url"
            :alt="selectedMedia.title"
          >
          <video
            v-else-if="getMediaItemKind(selectedMedia) === 'video'"
            class="admin-media__preview-image max-h-[72vh] max-w-full bg-black"
            :src="selectedMedia.url"
            controls
            preload="metadata"
          />
          <audio
            v-else-if="getMediaItemKind(selectedMedia) === 'audio'"
            class="w-full max-w-xl"
            :src="selectedMedia.url"
            controls
            preload="metadata"
          />
          <span
            v-else
            class="rounded border border-line bg-white px-5 py-4 text-sm font-semibold text-muted"
          >
            {{ selectedMedia.name }}
          </span>
        </div>

        <aside class="admin-media__detail grid max-h-[86vh] content-start gap-5 overflow-y-auto border-l border-line p-5">
          <div class="admin-media__detail-header flex flex-wrap items-start justify-between gap-3">
            <div class="min-w-0 flex-1">
              <p class="admin-media__detail-eyebrow text-xs font-semibold uppercase text-muted">
                Attachment
              </p>
              <h2 class="admin-media__detail-title mt-1 break-all text-xl font-semibold">
                {{ selectedMedia.name }}
              </h2>
            </div>
            <div class="admin-media__detail-header-actions flex shrink-0 flex-wrap items-center gap-2">
              <button
                class="admin-media__download rounded border border-line px-3 py-1.5 text-sm font-semibold text-ink hover:bg-surface"
                type="button"
                @click="downloadSelectedMedia"
              >
                다운로드
              </button>
              <button class="admin-media__detail-close rounded border border-line px-3 py-1.5 text-sm font-semibold" type="button" @click="closeMediaDetail">
                닫기
              </button>
            </div>
          </div>

          <dl class="admin-media__info grid gap-3 text-sm">
            <div class="admin-media__info-row">
              <dt class="admin-media__info-label text-xs font-semibold text-muted">경로</dt>
              <dd class="admin-media__info-value mt-1 break-all">{{ selectedMedia.url }}</dd>
            </div>
            <div class="admin-media__info-row">
              <dt class="admin-media__info-label text-xs font-semibold text-muted">용량</dt>
              <dd class="admin-media__info-value mt-1">{{ formatFileSize(selectedMedia.size) }}</dd>
            </div>
            <div v-if="selectedMedia.thumbnailStatus" class="admin-media__info-row">
              <dt class="admin-media__info-label text-xs font-semibold text-muted">카드 썸네일</dt>
              <dd class="admin-media__info-value mt-1 grid gap-1 break-all">
                <span v-if="selectedMedia.thumbnailStatus.role === 'card'">
                  자동 생성된 목록 카드용 썸네일입니다.
                </span>
                <template v-else>
                  <span v-if="selectedMedia.thumbnailStatus.hasThumbnail">
                    생성됨: {{ selectedMedia.thumbnailStatus.thumbnailUrl }}
                  </span>
                  <span v-else-if="selectedMedia.thumbnailStatus.isFallbackActive" class="font-semibold text-amber-700">
                    썸네일 없음. 현재 목록에서 원본 이미지를 불러옵니다.
                  </span>
                  <span v-else>
                    썸네일 없음. 대표 이미지로 사용되면 원본으로 대체됩니다.
                  </span>
                </template>
                <span v-if="selectedMedia.thumbnailStatus.originalUrl" class="text-muted">
                  원본: {{ selectedMedia.thumbnailStatus.originalUrl }}
                </span>
              </dd>
            </div>
          </dl>

          <div class="admin-media__category grid gap-2">
            <label class="admin-media__category-label text-xs font-semibold text-muted" for="media-category">
              폴더
            </label>
            <div class="admin-media__category-row flex gap-2">
              <input
                id="media-category"
                v-model="editingCategory"
                class="admin-media__category-input min-w-0 flex-1 rounded border border-line px-3 py-2 text-sm disabled:cursor-not-allowed disabled:bg-surface disabled:opacity-70"
                type="text"
                list="media-folder-options"
                placeholder="미분류"
                :disabled="!canEditMediaCategory(selectedMedia)"
                @keydown.enter.prevent="saveMediaCategory"
              >
              <button
                class="admin-media__category-save rounded border border-line px-3 py-2 text-xs font-semibold disabled:cursor-not-allowed disabled:opacity-50"
                type="button"
                :disabled="!canEditMediaCategory(selectedMedia)"
                @click="saveMediaCategory"
              >
                저장
              </button>
            </div>
            <p v-if="selectedMedia.avatarOwner" class="admin-media__category-hint text-xs text-muted">
              프로필 썸네일의 논리 폴더는 「{{ MEDIA_THUMBNAIL_ROOT }}」로 고정됩니다.
            </p>
            <p v-else-if="isPostCardThumbnailDiskItem(selectedMedia)" class="admin-media__category-hint text-xs text-muted">
              카드 썸네일은 원본 이미지에 연결된 자동 생성 파일이라 폴더를 직접 바꾸지 않습니다.
            </p>
            <datalist id="media-folder-options">
              <option v-for="folder in normalizedFolders" :key="folder" :value="folder" />
            </datalist>
          </div>

          <div
            v-if="selectedMedia.avatarOwner"
            class="admin-media__avatar-owner rounded border border-line bg-surface p-3 text-xs"
          >
            <strong class="admin-media__avatar-owner-title text-ink">연결된 회원</strong>
            <dl class="admin-media__avatar-owner-fields mt-2 grid gap-2 text-muted">
              <div class="admin-media__avatar-owner-row">
                <dt class="text-[11px] font-semibold uppercase tracking-wide text-muted">닉네임</dt>
                <dd class="mt-0.5 font-semibold text-ink">{{ selectedMedia.avatarOwner.username }}</dd>
              </div>
              <div class="admin-media__avatar-owner-row">
                <dt class="text-[11px] font-semibold uppercase tracking-wide text-muted">이메일</dt>
                <dd class="mt-0.5 break-all text-ink">{{ selectedMedia.avatarOwner.email }}</dd>
              </div>
              <div class="admin-media__avatar-owner-row">
                <dt class="text-[11px] font-semibold uppercase tracking-wide text-muted">마지막 접속</dt>
                <dd class="mt-0.5 text-ink">{{ formatDateTime(selectedMedia.avatarOwner.lastSeenAt) }}</dd>
              </div>
              <div v-if="selectedMedia.avatarOwner.lastSeenIp" class="admin-media__avatar-owner-row">
                <dt class="text-[11px] font-semibold uppercase tracking-wide text-muted">마지막 IP</dt>
                <dd class="mt-0.5 break-all font-mono text-[11px] text-ink">{{ selectedMedia.avatarOwner.lastSeenIp }}</dd>
              </div>
            </dl>
          </div>

          <div v-if="!selectedMedia.avatarOwner" class="admin-media__usage rounded bg-surface p-3 text-xs">
            <strong class="admin-media__usage-title text-ink">
              사용 현황 {{ selectedMedia.usage.length }}곳
            </strong>
            <ul v-if="selectedMedia.usage.length" class="admin-media__usage-list mt-2 grid gap-1.5 text-muted">
              <li v-for="usage in selectedMedia.usage" :key="`${selectedMedia.url}-${usage.type}-${usage.id}-${usage.location}`" class="admin-media__usage-item">
                <NuxtLink
                  v-if="usage.adminUrl"
                  class="admin-media__usage-link font-semibold text-ink hover:opacity-70"
                  :to="usage.adminUrl"
                >
                  {{ usage.title }}
                </NuxtLink>
                <span v-else class="admin-media__usage-name font-semibold text-ink">{{ usage.title }}</span>
                <span class="admin-media__usage-meta"> · {{ usage.typeLabel }} · {{ usage.label }}</span>
              </li>
            </ul>
            <p v-else class="admin-media__usage-empty mt-2 text-muted">
              사용 중인 곳이 없습니다.
            </p>
          </div>

          <div class="admin-media__rename grid gap-2">
            <label class="admin-media__rename-label text-xs font-semibold text-muted" for="media-name">
              파일명
            </label>
            <input
              id="media-name"
              v-model="editingName"
              class="admin-media__rename-input rounded border border-line px-3 py-2 text-sm disabled:opacity-50"
              type="text"
              :disabled="!canRenameMediaItem(selectedMedia)"
              :placeholder="selectedMedia.title"
              @keydown.enter.prevent="renameMedia"
            >
            <p v-if="isMediaItemLocked(selectedMedia)" class="admin-media__locked text-xs text-muted">
              게시물·페이지에서 사용 중이거나, 회원 프로필에 연결된 썸네일은 파일명 변경과 삭제가 잠깁니다.
            </p>
            <p v-else-if="isPostCardThumbnailDiskItem(selectedMedia)" class="admin-media__locked text-xs text-muted">
              카드 썸네일 파일명은 원본 이미지와의 연결을 유지하기 위해 변경할 수 없습니다.
            </p>
          </div>

          <div class="admin-media__actions flex flex-wrap gap-2">
            <button
              class="admin-media__rename-save rounded bg-[#15171a] px-3 py-1.5 text-xs font-semibold text-white disabled:opacity-50"
              type="button"
              :disabled="!canRenameMediaItem(selectedMedia) || !editingName"
              @click="renameMedia"
            >
              파일명 저장
            </button>
            <button
              class="admin-media__delete rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700 disabled:opacity-50"
              type="button"
              :disabled="deletingUrl === selectedMedia.url || isMediaItemLocked(selectedMedia)"
              @click="deleteMedia(selectedMedia)"
            >
              {{ deletingUrl === selectedMedia.url ? '삭제 중' : '삭제' }}
            </button>
          </div>
        </aside>
      </section>
    </div>

    <div
      v-if="toast"
      class="admin-media__toast fixed right-5 top-5 z-[100] max-w-[min(24rem,calc(100vw-2.5rem))] rounded border px-4 py-3 text-sm font-semibold shadow-lg"
      :class="{
        'border-green-200 bg-green-50 text-green-800': toast.type === 'success',
        'border-red-200 bg-red-50 text-red-800': toast.type === 'error',
        'border-line bg-white text-ink': toast.type === 'info'
      }"
      role="status"
    >
      {{ toast.message }}
    </div>
  </section>
</template>
