릴리스: v1.3.67 아이템 참조 요약과 영향 표시

This commit is contained in:
2026-04-02 15:04:20 +09:00
parent 188576f8ac
commit c1dfea41a5
6 changed files with 173 additions and 3 deletions

View File

@@ -19,6 +19,10 @@ const props = defineProps({
<span class="customItemCard__badge" :class="{ 'customItemCard__badge--template': item.sourceType === 'template' }">{{ item.sourceLabel }}</span>
<img class="customItemCard__image" :src="toApiUrl(item.src)" :alt="item.label" />
<div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
<div class="customItemCard__stats">
<span class="customItemCard__stat">참조 {{ item.sharedReferenceCount || 1 }}</span>
<span class="customItemCard__stat">게임 {{ item.sharedLinkedGameCount || item.linkedGames?.length || 0 }}</span>
</div>
</button>
</div>

View File

@@ -557,6 +557,21 @@ function formatImageJobStatus(status) {
}
}
function customItemDeleteImpactText(item) {
if (!item) return ''
const sharedCount = Number(item.sharedReferenceCount || 1)
if (item.sourceType === 'template') {
const base = item.isAssetLibraryItem
? `"${item.label}" 보관 자산 항목을 정리할까요? 라이브러리 항목만 제거되고, 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.`
: `"${item.label}" 템플릿 항목을 정리할까요? 연결된 템플릿과 같은 게임의 저장된 티어표에서 이 항목이 함께 제거될 수 있어요.`
return sharedCount > 1 ? `${base} 현재 같은 이미지 참조 ${sharedCount}건 중 이 항목만 다룹니다.` : base
}
const base = `"${item.label}" 사용자 업로드 이미지를 삭제할까요? 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.`
return sharedCount > 1 ? `${base} 현재 같은 이미지 참조 ${sharedCount}건 중 이 항목만 다룹니다.` : base
}
const imageDiagnosticsCards = computed(() => {
const stats = imageStats.value
if (!stats) return []
@@ -572,6 +587,7 @@ const imageDiagnosticsCards = computed(() => {
const visibleLinkedGames = computed(() =>
(modalTargetCustomItem.value?.linkedGames || []).filter((game) => game?.id && game.id !== 'freeform')
)
const visibleSharedEntries = computed(() => modalTargetCustomItem.value?.sharedEntries || [])
const filteredCustomItemModalGames = computed(() => {
const query = customItemModalGameQuery.value.trim().toLowerCase()
const linkedIds = new Set(visibleLinkedGames.value.map((game) => game.id))
@@ -1245,6 +1261,12 @@ function buildModalItemFromTierListItem(item, tierList) {
sourceLabel: matchedItem?.sourceLabel || '티어표 추가 아이템',
ownerName: matchedItem?.ownerName || tierListAuthorDisplayName(tierList),
linkedGames: Array.isArray(matchedItem?.linkedGames) ? matchedItem.linkedGames : [],
sharedReferenceCount: matchedItem?.sharedReferenceCount || 1,
sharedUserReferenceCount: matchedItem?.sharedUserReferenceCount || 0,
sharedTemplateReferenceCount: matchedItem?.sharedTemplateReferenceCount || 0,
sharedAssetReferenceCount: matchedItem?.sharedAssetReferenceCount || 0,
sharedLinkedGameCount: matchedItem?.sharedLinkedGameCount || 0,
sharedEntries: Array.isArray(matchedItem?.sharedEntries) ? matchedItem.sharedEntries : [],
usageCount: matchedItem?.usageCount || 0,
canDelete: typeof matchedItem?.canDelete === 'boolean' ? matchedItem.canDelete : false,
isPromoting: false,
@@ -1769,6 +1791,18 @@ function userAvatarFallback(user) {
<div class="modalCard modalCard--customItem" role="dialog" aria-modal="true">
<div v-if="modalTargetCustomItem" class="customItemModal">
<aside class="customItemModal__pickerPanel">
<div class="customItemModal__selected">
<img class="customItemModal__selectedImage" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
<div class="customItemModal__selectedMeta">
<div class="customItemModal__selectedTitle">{{ modalTargetCustomItem.label }}</div>
<div class="customItemModal__selectedChips">
<span class="pill">{{ modalTargetCustomItem.sharedReferenceCount || 1 }} 참조</span>
<span class="pill" v-if="modalTargetCustomItem.sharedUserReferenceCount">사용자 {{ modalTargetCustomItem.sharedUserReferenceCount }}</span>
<span class="pill" v-if="modalTargetCustomItem.sharedTemplateReferenceCount">템플릿 {{ modalTargetCustomItem.sharedTemplateReferenceCount }}</span>
<span class="pill" v-if="modalTargetCustomItem.sharedAssetReferenceCount">보관 {{ modalTargetCustomItem.sharedAssetReferenceCount }}</span>
</div>
</div>
</div>
<div class="customItemModal__pickerHead">
<div class="customItemModal__pickerEyebrow">GAME PICKER</div>
<div class="customItemModal__pickerTitle">템플릿으로 추가할 게임</div>
@@ -1830,6 +1864,25 @@ function userAvatarFallback(user) {
<button v-for="game in visibleLinkedGames" :key="game.id" type="button" class="pill pill--link" @click="jumpToGameAdmin(game.id)">{{ game.name }}</button>
</div>
<div v-else class="hint hint--tight">아직 템플릿에 연결된 게임이 없어요.</div>
</div>
<div class="customItemModal__linked">
<span class="customItemModal__label">같은 이미지 참조</span>
<div class="customItemModal__metaList">
<div class="customItemModal__metaRow"><span> 참조</span><strong>{{ modalTargetCustomItem.sharedReferenceCount || 1 }}</strong></div>
<div class="customItemModal__metaRow"><span>사용자 업로드</span><strong>{{ modalTargetCustomItem.sharedUserReferenceCount || 0 }}</strong></div>
<div class="customItemModal__metaRow"><span>템플릿 항목</span><strong>{{ modalTargetCustomItem.sharedTemplateReferenceCount || 0 }}</strong></div>
<div class="customItemModal__metaRow"><span>보관 자산</span><strong>{{ modalTargetCustomItem.sharedAssetReferenceCount || 0 }}</strong></div>
</div>
</div>
<div class="customItemModal__linked">
<span class="customItemModal__label">같은 이미지 기록</span>
<div v-if="visibleSharedEntries.length" class="customItemModal__entryList">
<div v-for="entry in visibleSharedEntries" :key="entry.id" class="customItemModal__entry">
<div class="customItemModal__entryTitle">{{ entry.label }}</div>
<div class="customItemModal__entryMeta">{{ entry.sourceLabel }} · {{ entry.ownerName || '알 수 없음' }}</div>
</div>
</div>
<div v-else class="hint hint--tight">같은 이미지를 가리키는 다른 기록이 없어요.</div>
</div>
<div class="customItemModal__actions">
<a class="btn btn--ghost customItemModal__action" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
@@ -1847,7 +1900,7 @@ function userAvatarFallback(user) {
<div v-if="customItemDeleteModalOpen" class="modalOverlay" @click.self="closeCustomItemDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">아이템 삭제</div>
<div class="modalCard__desc">{{ !modalTargetCustomItem ? '' : modalTargetCustomItem.sourceType === 'template' ? '"' + modalTargetCustomItem.label + '" 항목을 정리할까요? 게임에 연결된 항목이면 해당 템플릿과 저장된 같은 게임의 티어표에서도 함께 빠질 수 있고, 보관 자산이면 라이브러리에서만 제거됩니다.' : '"' + modalTargetCustomItem.label + '" 이미지를 삭제할까요? 사용자 업로드이면서 어디에도 연결되지 않은 이미지에만 삭제를 허용합니다.' }}</div>
<div class="modalCard__desc">{{ customItemDeleteImpactText(modalTargetCustomItem) }}</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeCustomItemDeleteModal">취소</button>
<button class="btn btn--danger" @click="removeCustomItem()">삭제</button>
@@ -2766,8 +2819,9 @@ function userAvatarFallback(user) {
}
.adminUiScope .gameSettingsCard__actions {
display: flex;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
/* flex-wrap: wrap; */
}
.adminUiScope .selectedThumb {
width: min(100%, 256px);
@@ -3142,6 +3196,15 @@ function userAvatarFallback(user) {
line-height: 1.3;
color: var(--theme-text);
}
.adminUiScope .customItemCard__stats {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.adminUiScope .customItemCard__stat {
font-size: 11px;
color: var(--theme-text-soft);
}
.adminUiScope .customItemModal {
display: grid;
grid-template-columns: 340px minmax(0, 1fr);
@@ -3161,6 +3224,31 @@ function userAvatarFallback(user) {
display: grid;
gap: 10px;
}
.adminUiScope .customItemModal__selected {
display: grid;
gap: 12px;
}
.adminUiScope .customItemModal__selectedImage {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 18px;
object-fit: cover;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.adminUiScope .customItemModal__selectedMeta {
display: grid;
gap: 10px;
}
.adminUiScope .customItemModal__selectedTitle {
font-size: 18px;
font-weight: 900;
}
.adminUiScope .customItemModal__selectedChips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.adminUiScope .customItemModal__pickerEyebrow {
font-size: 11px;
letter-spacing: 0.12em;
@@ -3254,6 +3342,25 @@ function userAvatarFallback(user) {
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.adminUiScope .customItemModal__entryList {
display: grid;
gap: 8px;
}
.adminUiScope .customItemModal__entry {
padding: 10px 12px;
border-radius: 14px;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.adminUiScope .customItemModal__entryTitle {
font-size: 13px;
font-weight: 800;
}
.adminUiScope .customItemModal__entryMeta {
margin-top: 4px;
font-size: 11px;
color: var(--theme-text-soft);
}
.adminUiScope .customItemModal__close {
justify-self: end;
border: 0;