아이템 관리 자산 분류와 기본 필터를 정리한다

This commit is contained in:
2026-04-03 13:27:33 +09:00
parent 953837137a
commit 426e7de177
9 changed files with 87 additions and 22 deletions

View File

@@ -16,7 +16,15 @@ const props = defineProps({
<div v-if="!props.customItems.length" class="hint">조건에 맞는 관리 대상 아이템이 없어요.</div>
<div v-else class="customItemGrid">
<button v-for="item in props.customItems" :key="item.id" type="button" class="customItemCard" @click="props.openCustomItemModal(item)">
<span class="customItemCard__badge" :class="{ 'customItemCard__badge--template': item.sourceType === 'template' }">{{ item.sourceLabel }}</span>
<span
class="customItemCard__badge"
:class="{
'customItemCard__badge--template': item.sourceType === 'template',
'customItemCard__badge--asset': item.sourceType === 'asset',
}"
>
{{ item.sourceLabel }}
</span>
<img class="customItemCard__image" :src="toApiUrl(item.src)" :alt="item.label" />
<div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
</button>

View File

@@ -119,9 +119,19 @@ export function useAdminCustomItems({
closeCustomItemDeleteModal()
closeCustomItemModal()
await refreshCustomItems()
success.value = item.sourceType === 'template' ? '선택한 템플릿 아이템을 제거했어요.' : '사용자 업로드 이미지를 삭제했어요.'
success.value =
item.sourceType === 'template'
? '선택한 템플릿 아이템을 제거했어요.'
: item.sourceType === 'asset'
? '선택한 이미지 자산을 삭제했어요.'
: '사용자 업로드 이미지를 삭제했어요.'
} catch (e) {
error.value = item.sourceType === 'template' ? '템플릿 아이템 제거에 실패했어요.' : '사용자 업로드 이미지 삭제에 실패했어요.'
error.value =
item.sourceType === 'template'
? '템플릿 아이템 제거에 실패했어요.'
: item.sourceType === 'asset'
? '이미지 자산 삭제에 실패했어요.'
: '사용자 업로드 이미지 삭제에 실패했어요.'
}
}

View File

@@ -44,7 +44,7 @@ const customItemQuery = ref('')
const customItemPage = ref(1)
const customItemLimit = ref(50)
const customItemTotal = ref(0)
const customItemFilter = ref('all')
const customItemFilter = ref('library')
const customItemModalTargetTemplateId = ref('')
const adminTierLists = ref([])
@@ -242,7 +242,12 @@ const activeTabDescription = computed(() => {
})
const adminOverviewStats = computed(() => {
const pendingRequests = templateRequests.value.length
const orphanItems = customItems.value.filter((item) => item.usageCount === 0).length
const orphanItems = customItems.value.filter(
(item) =>
item.sourceType === 'user' &&
Number(item.usageCount || 0) === 0 &&
!(Array.isArray(item.linkedTemplates) && item.linkedTemplates.length > 0)
).length
const adminCount = users.value.filter((user) => user.isAdmin).length
if (activeTab.value === 'featured') {
@@ -264,8 +269,9 @@ const adminOverviewStats = computed(() => {
if (activeTab.value === 'items') {
return [
{ label: '검색 결과', value: `${customItemTotal.value}` },
{ label: '미사용', value: `${orphanItems}` },
{ label: '미사용 사용자 아이템', value: `${orphanItems}` },
{ label: '템플릿 아이템', value: `${customItems.value.filter((item) => item.sourceType === 'template').length}` },
{ label: '이미지 자산', value: `${customItems.value.filter((item) => item.sourceType === 'asset').length}` },
]
}
if (activeTab.value === 'tierlists') {
@@ -490,7 +496,7 @@ watch(
if (tab === 'items') {
customItemQuery.value = ''
customItemFilter.value = 'all'
customItemFilter.value = 'library'
customItemPage.value = 1
await refreshCustomItems()
return
@@ -599,10 +605,11 @@ function formatImageJobStatus(status) {
function customItemDeleteImpactText(item) {
if (!item) return ''
if (item.sourceType === 'asset' || item.isAssetLibraryItem) {
return `"${item.label}" ${item.sourceLabel || '이미지 자산'} 항목을 정리할까요? 라이브러리 항목만 제거되고, 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.`
}
if (item.sourceType === 'template') {
return item.isAssetLibraryItem
? `"${item.label}" 보관 자산 항목을 정리할까요? 라이브러리 항목만 제거되고, 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.`
: `"${item.label}" 템플릿 항목을 정리할까요? 연결된 템플릿과 같은 주제의 저장된 티어표에서 이 항목이 함께 제거될 수 있어요.`
return `"${item.label}" 템플릿 항목을 정리할까요? 연결된 템플릿과 같은 주제의 저장된 티어표에서 이 항목이 함께 제거될 수 있어요.`
}
return `"${item.label}" 사용자 업로드 이미지를 삭제할까요? 현재 항목만 정리됩니다.`
@@ -747,7 +754,7 @@ function setTab(tab) {
}
if (tab === 'items') {
customItemQuery.value = ''
customItemFilter.value = 'all'
customItemFilter.value = 'library'
customItemPage.value = 1
refreshCustomItems()
}
@@ -1389,7 +1396,7 @@ function buildModalItemFromTierListItem(item, tierList) {
id,
label: item?.label || matchedItem?.label || '이름 없음',
src: item?.src || matchedItem?.src || '',
sourceType: matchedItem?.sourceType || (String(id).startsWith('asset:') ? 'template' : 'user'),
sourceType: matchedItem?.sourceType || (String(id).startsWith('asset:') ? 'asset' : 'user'),
sourceLabel: matchedItem?.sourceLabel || '티어표 추가 아이템',
ownerName: matchedItem?.ownerName || tierListAuthorDisplayName(tierList),
linkedTemplates: Array.isArray(matchedItem?.linkedTemplates) ? matchedItem.linkedTemplates : [],
@@ -2289,12 +2296,13 @@ function openUserProfile(user) {
<option :value="200">200개씩 보기</option>
</select>
<select :value="customItemFilter" class="select" @change="changeCustomItemFilter($event.target.value)">
<option value="library">아이템만 (템플릿+사용자)</option>
<option value="all">전체 이미지</option>
<option value="user">사용자 업로드</option>
<option value="template">템플릿 사용 이미지</option>
<option value="asset">관리자 보관 자산</option>
<option value="template">관리자 템플릿 아이템</option>
<option value="asset">썸네일·프로필 이미지</option>
<option value="unused-user">미사용 사용자 업로드</option>
<option value="unused-admin">미사용 관리자 자산</option>
<option value="unused-admin">미사용 썸네일·프로필 이미지</option>
</select>
</div>
<div class="adminSidebar__actions">
@@ -3480,6 +3488,9 @@ function openUserProfile(user) {
.adminUiScope .customItemCard__badge--template {
background: rgba(96, 165, 250, 0.18);
}
.adminUiScope .customItemCard__badge--asset {
background: rgba(251, 191, 36, 0.18);
}
.adminUiScope .customItemCard:hover {
border-color: rgba(126, 162, 255, 0.42);
background: rgba(255, 255, 255, 0.06);