미디어 그리드 렌더링 분리

This commit is contained in:
2026-07-24 13:47:34 +09:00
parent b2537d4d14
commit 5b6e4d33dd
7 changed files with 161 additions and 86 deletions

View File

@@ -0,0 +1,135 @@
<script setup>
const props = defineProps({
activeTab: {
type: String,
default: 'library'
},
getMediaBadgeClass: {
type: Function,
required: true
},
getMediaBadgeLabel: {
type: Function,
required: true
},
getMediaGridPreviewUrl: {
type: Function,
required: true
},
getMediaItemKind: {
type: Function,
required: true
},
getMediaUsageBadgeCount: {
type: Function,
required: true
},
items: {
type: Array,
default: () => []
},
selectedUrls: {
type: Array,
default: () => []
}
})
const emit = defineEmits([
'drag-start',
'image-error',
'open-detail',
'toggle-selection'
])
/**
* 미디어 선택 여부를 확인한다.
* @param {Object} item - 미디어 항목
* @returns {boolean} 선택 여부
*/
const isMediaSelected = (item) => props.selectedUrls.includes(item?.url)
</script>
<template>
<div class="admin-media-grid 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 props.items"
: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="props.activeTab === 'library'"
@dragstart="emit('drag-start', $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="emit('toggle-selection', 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 flex w-full flex-col text-left outline-none"
type="button"
@click="emit('open-detail', item)"
>
<span class="admin-media__thumb-frame relative block w-full overflow-hidden">
<img
v-if="props.getMediaItemKind(item) === 'image'"
class="admin-media__image aspect-square w-full bg-surface object-cover"
:src="props.getMediaGridPreviewUrl(item)"
:alt="item.title"
loading="lazy"
decoding="async"
@error="emit('image-error', $event, item)"
>
<AdminMediaVideoThumbnail
v-else-if="props.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"
>
{{ props.getMediaItemKind(item) }}
</span>
<span
v-if="props.getMediaUsageBadgeCount(item)"
class="admin-media__usage-count-badge pointer-events-none absolute right-1.5 top-1.5 z-[1] rounded bg-[#15171a] px-1.5 py-0.5 text-[10px] font-semibold text-white"
>
{{ props.getMediaUsageBadgeCount(item) }}
</span>
<span
v-if="props.getMediaBadgeLabel(item)"
class="admin-media__usage-badge pointer-events-none absolute right-1.5 z-[1] rounded px-1.5 py-0.5 text-[10px] font-semibold"
:class="[
props.getMediaBadgeClass(item),
props.getMediaUsageBadgeCount(item) ? 'top-8' : 'top-1.5'
]"
>
{{ props.getMediaBadgeLabel(item) }}
</span>
</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>
</template>