|
|
|
|
@@ -56,6 +56,7 @@ const userDeleteModalOpen = ref(false)
|
|
|
|
|
const userRoleModalOpen = ref(false)
|
|
|
|
|
const customItemModalOpen = ref(false)
|
|
|
|
|
const customItemDeleteModalOpen = ref(false)
|
|
|
|
|
const customItemModalHistoryActive = ref(false)
|
|
|
|
|
const modalTargetUser = ref(null)
|
|
|
|
|
const modalPasswordDraft = ref('')
|
|
|
|
|
const modalRoleNextAdmin = ref(false)
|
|
|
|
|
@@ -194,13 +195,26 @@ const adminOverviewStats = computed(() => {
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function handleAdminPopState() {
|
|
|
|
|
if (customItemDeleteModalOpen.value) {
|
|
|
|
|
customItemDeleteModalOpen.value = false
|
|
|
|
|
if (customItemModalOpen.value) pushCustomItemModalHistoryState()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (customItemModalOpen.value) {
|
|
|
|
|
closeCustomItemModal({ fromPopState: true })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
if (typeof window !== 'undefined') window.addEventListener('popstate', handleAdminPopState)
|
|
|
|
|
await auth.refresh()
|
|
|
|
|
await Promise.all([refreshGames(), refreshCustomItems(), refreshAdminTierLists(), refreshUsers(), refreshTemplateRequests(), refreshImageDiagnostics()])
|
|
|
|
|
await syncFeaturedSortable()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (typeof window !== 'undefined') window.removeEventListener('popstate', handleAdminPopState)
|
|
|
|
|
clearPreviewUrl('item')
|
|
|
|
|
clearPreviewUrl('thumb')
|
|
|
|
|
destroyFeaturedSortable()
|
|
|
|
|
@@ -1048,26 +1062,44 @@ function moveCustomItemPage(direction) {
|
|
|
|
|
refreshCustomItems()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pushCustomItemModalHistoryState() {
|
|
|
|
|
if (typeof window === 'undefined') return
|
|
|
|
|
window.history.pushState({ ...(window.history.state || {}), adminCustomItemModal: true }, '', window.location.href)
|
|
|
|
|
customItemModalHistoryActive.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCustomItemModal(item) {
|
|
|
|
|
modalTargetCustomItem.value = item || null
|
|
|
|
|
customItemModalTargetGameId.value = ''
|
|
|
|
|
customItemModalGameQuery.value = ''
|
|
|
|
|
customItemModalGameSort.value = 'recent'
|
|
|
|
|
customItemModalOpen.value = true
|
|
|
|
|
pushCustomItemModalHistoryState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeCustomItemModal() {
|
|
|
|
|
function closeCustomItemModal({ fromPopState = false } = {}) {
|
|
|
|
|
customItemModalOpen.value = false
|
|
|
|
|
customItemDeleteModalOpen.value = false
|
|
|
|
|
modalTargetCustomItem.value = null
|
|
|
|
|
customItemModalTargetGameId.value = ''
|
|
|
|
|
customItemModalGameQuery.value = ''
|
|
|
|
|
customItemModalGameSort.value = 'recent'
|
|
|
|
|
|
|
|
|
|
if (fromPopState) {
|
|
|
|
|
customItemModalHistoryActive.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (customItemModalHistoryActive.value && typeof window !== 'undefined') {
|
|
|
|
|
customItemModalHistoryActive.value = false
|
|
|
|
|
window.history.back()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCustomItemDeleteModal(item) {
|
|
|
|
|
if (!item) return
|
|
|
|
|
if (item.usageCount > 0) {
|
|
|
|
|
error.value = '사용 중인 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
if (item.sourceType === 'user' && (item.usageCount > 0 || item.linkedGames.length > 0)) {
|
|
|
|
|
error.value = '사용 중이거나 템플릿에 연결된 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
modalTargetCustomItem.value = item
|
|
|
|
|
@@ -1081,8 +1113,8 @@ function closeCustomItemDeleteModal() {
|
|
|
|
|
async function removeCustomItem(item = modalTargetCustomItem.value) {
|
|
|
|
|
resetMessages()
|
|
|
|
|
if (!item) return
|
|
|
|
|
if (item.usageCount > 0) {
|
|
|
|
|
error.value = '사용 중인 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
if (item.sourceType === 'user' && (item.usageCount > 0 || item.linkedGames.length > 0)) {
|
|
|
|
|
error.value = '사용 중이거나 템플릿에 연결된 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1091,9 +1123,9 @@ async function removeCustomItem(item = modalTargetCustomItem.value) {
|
|
|
|
|
closeCustomItemDeleteModal()
|
|
|
|
|
closeCustomItemModal()
|
|
|
|
|
await refreshCustomItems()
|
|
|
|
|
success.value = '미사용 사용자 업로드 이미지를 삭제했어요.'
|
|
|
|
|
success.value = item.sourceType === 'template' ? '선택한 템플릿 아이템을 제거했어요.' : '사용자 업로드 이미지를 삭제했어요.'
|
|
|
|
|
} catch (e) {
|
|
|
|
|
error.value = '커스텀 이미지 삭제에 실패했어요.'
|
|
|
|
|
error.value = item.sourceType === 'template' ? '템플릿 아이템 제거에 실패했어요.' : '사용자 업로드 이미지 삭제에 실패했어요.'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1939,7 +1971,7 @@ async function saveFeaturedOrder() {
|
|
|
|
|
<div v-if="modalTargetCustomItem" class="customItemModal">
|
|
|
|
|
<aside class="customItemModal__pickerPanel">
|
|
|
|
|
<div class="customItemModal__pickerHead">
|
|
|
|
|
<div class="customItemModal__pickerEyebrow">GAME LIBRARY</div>
|
|
|
|
|
<div class="customItemModal__pickerEyebrow">GAME PICKER</div>
|
|
|
|
|
<div class="customItemModal__pickerTitle">템플릿으로 추가할 게임</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemModal__pickerControls">
|
|
|
|
|
@@ -1993,7 +2025,7 @@ async function saveFeaturedOrder() {
|
|
|
|
|
<button class="btn btn--ghost customItemModal__action" :disabled="!customItemModalTargetGameId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
|
|
|
|
|
{{ modalTargetCustomItem.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button v-if="modalTargetCustomItem.canDelete" class="btn btn--danger customItemModal__action" :disabled="modalTargetCustomItem.usageCount > 0 || visibleLinkedGames.length > 0" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
|
|
|
|
|
<button v-if="modalTargetCustomItem.canDelete" class="btn btn--danger customItemModal__action" :disabled="modalTargetCustomItem.sourceType === 'user' && (modalTargetCustomItem.usageCount > 0 || visibleLinkedGames.length > 0)" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -2002,8 +2034,8 @@ async function saveFeaturedOrder() {
|
|
|
|
|
|
|
|
|
|
<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.label + '" 이미지를 삭제할까요? 사용자 업로드이면서 어디에도 연결되지 않은 이미지에만 삭제를 허용합니다.' : '' }}</div>
|
|
|
|
|
<div class="modalCard__title">아이템 삭제</div>
|
|
|
|
|
<div class="modalCard__desc">{{ !modalTargetCustomItem ? '' : modalTargetCustomItem.sourceType === 'template' ? '"' + modalTargetCustomItem.label + '" 항목을 현재 템플릿에서 제거할까요? 이미 저장된 같은 게임의 티어표에서도 함께 빠질 수 있어요.' : '"' + modalTargetCustomItem.label + '" 이미지를 삭제할까요? 사용자 업로드이면서 어디에도 연결되지 않은 이미지에만 삭제를 허용합니다.' }}</div>
|
|
|
|
|
<div class="modalCard__actions">
|
|
|
|
|
<button class="btn btn--ghost" @click="closeCustomItemDeleteModal">취소</button>
|
|
|
|
|
<button class="btn btn--danger" @click="removeCustomItem()">삭제</button>
|
|
|
|
|
@@ -2894,6 +2926,7 @@ async function saveFeaturedOrder() {
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
color: var(--theme-text-soft);
|
|
|
|
|
background: var(--theme-card-bg);
|
|
|
|
|
}
|
|
|
|
|
.selectedThumb--sidebar {
|
|
|
|
|
width: 100%;
|
|
|
|
|
@@ -3134,12 +3167,12 @@ async function saveFeaturedOrder() {
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
color: var(--theme-text);
|
|
|
|
|
}
|
|
|
|
|
.customItemModal {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
|
|
|
|
gap: 18px;
|
|
|
|
|
grid-template-columns: minmax(280px, 320px) minmax(0, 1fr);
|
|
|
|
|
gap: 24px;
|
|
|
|
|
align-items: start;
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__pickerPanel {
|
|
|
|
|
@@ -3275,7 +3308,7 @@ async function saveFeaturedOrder() {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
.modalCard--customItem {
|
|
|
|
|
width: min(980px, 100%);
|
|
|
|
|
width: min(1160px, calc(100vw - 48px));
|
|
|
|
|
}
|
|
|
|
|
.pager {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|