|
|
|
|
@@ -48,9 +48,12 @@ const previewTierList = ref(null)
|
|
|
|
|
const userPasswordModalOpen = ref(false)
|
|
|
|
|
const userDeleteModalOpen = ref(false)
|
|
|
|
|
const userRoleModalOpen = ref(false)
|
|
|
|
|
const customItemModalOpen = ref(false)
|
|
|
|
|
const customItemDeleteModalOpen = ref(false)
|
|
|
|
|
const modalTargetUser = ref(null)
|
|
|
|
|
const modalPasswordDraft = ref('')
|
|
|
|
|
const modalRoleNextAdmin = ref(false)
|
|
|
|
|
const modalTargetCustomItem = ref(null)
|
|
|
|
|
|
|
|
|
|
const users = ref([])
|
|
|
|
|
|
|
|
|
|
@@ -855,18 +858,42 @@ function moveCustomItemPage(direction) {
|
|
|
|
|
refreshCustomItems()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeCustomItem(item) {
|
|
|
|
|
function openCustomItemModal(item) {
|
|
|
|
|
modalTargetCustomItem.value = item || null
|
|
|
|
|
customItemModalOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeCustomItemModal() {
|
|
|
|
|
customItemModalOpen.value = false
|
|
|
|
|
modalTargetCustomItem.value = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCustomItemDeleteModal(item) {
|
|
|
|
|
if (!item) return
|
|
|
|
|
if (item.usageCount > 0) {
|
|
|
|
|
error.value = '사용 중인 커스텀 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
modalTargetCustomItem.value = item
|
|
|
|
|
customItemDeleteModalOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeCustomItemDeleteModal() {
|
|
|
|
|
customItemDeleteModalOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeCustomItem(item = modalTargetCustomItem.value) {
|
|
|
|
|
resetMessages()
|
|
|
|
|
if (!item) return
|
|
|
|
|
if (item.usageCount > 0) {
|
|
|
|
|
error.value = '사용 중인 커스텀 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ok = window.confirm(`"${item.label}" 미사용 커스텀 이미지를 삭제할까요?`)
|
|
|
|
|
if (!ok) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await api.deleteAdminCustomItem(item.id)
|
|
|
|
|
closeCustomItemDeleteModal()
|
|
|
|
|
closeCustomItemModal()
|
|
|
|
|
await refreshCustomItems()
|
|
|
|
|
success.value = '미사용 커스텀 이미지를 삭제했어요.'
|
|
|
|
|
} catch (e) {
|
|
|
|
|
@@ -900,6 +927,7 @@ async function promoteCustomItem(item) {
|
|
|
|
|
await api.promoteAdminCustomItem(item.id, { gameId: customItemTargetGameId.value })
|
|
|
|
|
const targetGameName = games.value.find((game) => game.id === customItemTargetGameId.value)?.name || customItemTargetGameId.value
|
|
|
|
|
if (selectedGameId.value === customItemTargetGameId.value) await loadGame()
|
|
|
|
|
closeCustomItemModal()
|
|
|
|
|
success.value = `"${item.label}" 아이템을 ${targetGameName} 기본 템플릿으로 추가했어요.`
|
|
|
|
|
} catch (e) {
|
|
|
|
|
error.value = '커스텀 아이템을 기본 템플릿으로 가져오지 못했어요.'
|
|
|
|
|
@@ -1349,37 +1377,10 @@ async function saveFeaturedOrder() {
|
|
|
|
|
<div class="panel">
|
|
|
|
|
<div v-if="!customItems.length" class="hint">조건에 맞는 커스텀 아이템이 없어요.</div>
|
|
|
|
|
<div v-else class="customItemGrid">
|
|
|
|
|
<article v-for="item in customItems" :key="item.id" class="customItemCard">
|
|
|
|
|
<button v-for="item in customItems" :key="item.id" type="button" class="customItemCard" @click="openCustomItemModal(item)">
|
|
|
|
|
<img class="customItemCard__image" :src="toApiUrl(item.src)" :alt="item.label" />
|
|
|
|
|
<div class="customItemCard__body">
|
|
|
|
|
<div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
|
|
|
|
|
<div class="customItemCard__metaList">
|
|
|
|
|
<div class="customItemCard__metaRow">
|
|
|
|
|
<span>파일</span>
|
|
|
|
|
<strong :title="item.src.split('/').pop()">{{ item.src.split('/').pop() }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemCard__metaRow">
|
|
|
|
|
<span>업로더</span>
|
|
|
|
|
<strong :title="item.ownerName">{{ item.ownerName }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemCard__metaRow">
|
|
|
|
|
<span>사용 중</span>
|
|
|
|
|
<strong>{{ item.usageCount }}개 티어표</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemCard__metaRow">
|
|
|
|
|
<span>등록일</span>
|
|
|
|
|
<strong>{{ fmt(item.createdAt) }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemCard__actions">
|
|
|
|
|
<a class="btn btn--small btn--ghost" :href="toApiUrl(item.src)" :download="item.label">이미지 다운로드</a>
|
|
|
|
|
<button class="btn btn--small btn--ghost" :disabled="!customItemTargetGameId || item.isPromoting" @click="promoteCustomItem(item)">
|
|
|
|
|
{{ item.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn--small btn--danger" :disabled="item.usageCount > 0" @click="removeCustomItem(item)">개별 삭제</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
<div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pager">
|
|
|
|
|
@@ -1671,6 +1672,51 @@ async function saveFeaturedOrder() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="customItemModalOpen" class="modalOverlay" @click.self="closeCustomItemModal">
|
|
|
|
|
<div class="modalCard modalCard--customItem" role="dialog" aria-modal="true">
|
|
|
|
|
<div class="modalCard__titleRow">
|
|
|
|
|
<div class="modalCard__title">커스텀 아이템 상세</div>
|
|
|
|
|
<button class="btn btn--ghost btn--small" @click="closeCustomItemModal">닫기</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="modalTargetCustomItem" class="customItemModal">
|
|
|
|
|
<img class="customItemModal__image" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
|
|
|
|
|
<div class="customItemModal__body">
|
|
|
|
|
<div class="customItemModal__title">{{ modalTargetCustomItem.label }}</div>
|
|
|
|
|
<div class="customItemModal__metaList">
|
|
|
|
|
<div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div>
|
|
|
|
|
<div class="customItemModal__metaRow"><span>업로더</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div>
|
|
|
|
|
<div class="customItemModal__metaRow"><span>사용 중</span><strong>{{ modalTargetCustomItem.usageCount }}개 티어표</strong></div>
|
|
|
|
|
<div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modalCard__form">
|
|
|
|
|
<select v-model="customItemTargetGameId" class="select">
|
|
|
|
|
<option value="">기본 템플릿에 추가할 게임 선택</option>
|
|
|
|
|
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customItemModal__actions">
|
|
|
|
|
<a class="btn btn--ghost" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
|
|
|
|
|
<button class="btn btn--ghost" :disabled="!customItemTargetGameId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
|
|
|
|
|
{{ modalTargetCustomItem.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn--danger" :disabled="modalTargetCustomItem.usageCount > 0" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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__actions">
|
|
|
|
|
<button class="btn btn--ghost" @click="closeCustomItemDeleteModal">취소</button>
|
|
|
|
|
<button class="btn btn--danger" @click="removeCustomItem()">삭제</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="previewModalOpen" class="modalOverlay" @click.self="closePreviewModal">
|
|
|
|
|
<div class="modalCard modalCard--preview" role="dialog" aria-modal="true">
|
|
|
|
|
<div class="modalCard__titleRow">
|
|
|
|
|
@@ -2435,22 +2481,28 @@ async function saveFeaturedOrder() {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
.customItemGrid {
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(240px, 280px));
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
|
|
|
|
gap: 12px;
|
|
|
|
|
justify-content: start;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard {
|
|
|
|
|
appearance: none;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.04);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-rows: auto 1fr;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
padding: 14px;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
text-align: left;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard:hover {
|
|
|
|
|
border-color: rgba(126, 162, 255, 0.42);
|
|
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__image {
|
|
|
|
|
width: 100%;
|
|
|
|
|
@@ -2460,51 +2512,69 @@ async function saveFeaturedOrder() {
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
background: rgba(0, 0, 0, 0.18);
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__body {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
align-content: start;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__actions {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__actions > * {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__title {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__metaList {
|
|
|
|
|
.customItemModal {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
|
|
|
|
|
gap: 18px;
|
|
|
|
|
align-items: start;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__metaRow {
|
|
|
|
|
.customItemModal__image {
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 1 / 1;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.04);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__body {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 3px;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__metaRow span {
|
|
|
|
|
.customItemModal__title {
|
|
|
|
|
font-size: 19px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__metaList {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__metaRow {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__metaRow span {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.46);
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__metaRow strong {
|
|
|
|
|
.customItemModal__metaRow strong {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.82);
|
|
|
|
|
color: rgba(255, 255, 255, 0.84);
|
|
|
|
|
}
|
|
|
|
|
.customItemModal__actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.modalCard--customItem {
|
|
|
|
|
width: min(760px, 100%);
|
|
|
|
|
}
|
|
|
|
|
.pager {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
@@ -2991,6 +3061,9 @@ async function saveFeaturedOrder() {
|
|
|
|
|
.adminHero__stats {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
.customItemModal {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
.adminSidebar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
@@ -3034,19 +3107,22 @@ async function saveFeaturedOrder() {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
}
|
|
|
|
|
.thumbGrid,
|
|
|
|
|
.customItemGrid,
|
|
|
|
|
.userList {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
.customItemGrid {
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
|
|
|
}
|
|
|
|
|
.tierAdminCard__head {
|
|
|
|
|
display: grid;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard {
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
.customItemCard__image {
|
|
|
|
|
width: clamp(72px, 28vw, 120px);
|
|
|
|
|
flex-basis: 120px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 1 / 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|