릴리스: v1.3.64 관리자 필터와 게임 설정 패널 정리
This commit is contained in:
@@ -12,6 +12,20 @@ const props = defineProps({
|
||||
isGameLoading: { type: Boolean, required: true },
|
||||
hasSelectedGame: { type: Boolean, required: true },
|
||||
selectedGame: { type: Object, default: null },
|
||||
displayThumbnailUrl: { type: String, default: '' },
|
||||
canApplyThumbnail: { type: Boolean, required: true },
|
||||
gameVisibilitySaving: { type: Boolean, required: true },
|
||||
thumbFileInputRef: { type: Function, required: true },
|
||||
openThumbFilePicker: { type: Function, required: true },
|
||||
onThumb: { type: Function, required: true },
|
||||
onThumbDragEnter: { type: Function, required: true },
|
||||
onThumbDragOver: { type: Function, required: true },
|
||||
onThumbDragLeave: { type: Function, required: true },
|
||||
onThumbDrop: { type: Function, required: true },
|
||||
isThumbDragOver: { type: Boolean, required: true },
|
||||
uploadThumbnail: { type: Function, required: true },
|
||||
removeGame: { type: Function, required: true },
|
||||
toggleSelectedGameVisibility: { type: Function, required: true },
|
||||
itemFileInputRef: { type: Function, required: true },
|
||||
onFile: { type: Function, required: true },
|
||||
isItemDragOver: { type: Boolean, required: true },
|
||||
@@ -36,6 +50,10 @@ const props = defineProps({
|
||||
function setGameItemListElement(el) {
|
||||
props.gameItemListRef(el)
|
||||
}
|
||||
|
||||
function setThumbFileElement(el) {
|
||||
props.thumbFileInputRef(el)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -91,13 +109,43 @@ function setGameItemListElement(el) {
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.hasSelectedGame" class="panel">
|
||||
<div class="detailHead">
|
||||
<div>
|
||||
<div class="panel__title">선택된 게임 정보</div>
|
||||
<div class="selectedGame__name">{{ props.selectedGame.game.name }}</div>
|
||||
<div class="selectedGame__id">{{ props.selectedGame.game.id }}</div>
|
||||
<section class="adminCard gameSettingsCard">
|
||||
<div class="gameSettingsCard__media">
|
||||
<input :ref="setThumbFileElement" type="file" accept="image/*" class="srOnlyInput" @change="props.onThumb" />
|
||||
<button
|
||||
class="thumbDropZone"
|
||||
:class="{ 'thumbDropZone--active': props.isThumbDragOver }"
|
||||
type="button"
|
||||
@click="props.openThumbFilePicker"
|
||||
@dragenter="props.onThumbDragEnter"
|
||||
@dragover="props.onThumbDragOver"
|
||||
@dragleave="props.onThumbDragLeave"
|
||||
@drop="props.onThumbDrop"
|
||||
>
|
||||
<img v-if="props.displayThumbnailUrl" class="selectedThumb selectedThumb--sidebar" :src="props.displayThumbnailUrl" :alt="props.selectedGame.game.name" />
|
||||
<div v-else class="selectedThumb selectedThumb--empty selectedThumb--sidebar">대표 썸네일</div>
|
||||
<div class="thumbDropZone__copy">
|
||||
<div class="thumbDropZone__iconWrap">
|
||||
<SvgIcon :src="addPhotoAlternateIcon" alt="" class="thumbDropZone__icon" />
|
||||
</div>
|
||||
<div class="thumbDropZone__title">{{ props.displayThumbnailUrl ? '썸네일 변경' : '클릭 & 드래그' }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gameSettingsCard__body">
|
||||
<div class="panel__title">게임 설정</div>
|
||||
<div class="gameSettingsCard__meta">{{ props.selectedGame.game.name }} · {{ props.selectedGame.game.id }}</div>
|
||||
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': props.gameVisibilitySaving }">
|
||||
<input :checked="!!props.selectedGame.game.isPublic" type="checkbox" @change="props.toggleSelectedGameVisibility($event.target.checked)" />
|
||||
<span class="toggleSwitch__label">{{ props.selectedGame.game.isPublic ? '템플릿 공개중' : '비공개 상태' }}</span>
|
||||
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||||
</label>
|
||||
<div class="gameSettingsCard__actions">
|
||||
<button class="btn" :disabled="!props.canApplyThumbnail" @click="props.uploadThumbnail">썸네일 적용</button>
|
||||
<button class="btn btn--danger" @click="props.removeGame">게임 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section">
|
||||
<section class="adminCard">
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useAdminCustomItems({
|
||||
customItemLimit,
|
||||
customItemPageCount,
|
||||
customItemQuery,
|
||||
customItemOrphanOnly,
|
||||
customItemFilter,
|
||||
customItemModalOpen,
|
||||
customItemDeleteModalOpen,
|
||||
customItemModalHistoryActive,
|
||||
@@ -33,7 +33,8 @@ export function useAdminCustomItems({
|
||||
refreshCustomItems()
|
||||
}
|
||||
|
||||
function toggleCustomItemOrphanOnly() {
|
||||
function changeCustomItemFilter(filter) {
|
||||
customItemFilter.value = filter
|
||||
customItemPage.value = 1
|
||||
refreshCustomItems()
|
||||
}
|
||||
@@ -186,7 +187,7 @@ export function useAdminCustomItems({
|
||||
|
||||
return {
|
||||
submitCustomItemSearch,
|
||||
toggleCustomItemOrphanOnly,
|
||||
changeCustomItemFilter,
|
||||
changeCustomItemLimit,
|
||||
moveCustomItemPage,
|
||||
pushCustomItemModalHistoryState,
|
||||
|
||||
@@ -41,9 +41,9 @@ export const api = {
|
||||
request(`/api/admin/games/${encodeURIComponent(gameId)}`, { method: 'PATCH', body: payload }),
|
||||
updateAdminGameItem: (gameId, itemId, payload) =>
|
||||
request(`/api/admin/games/${encodeURIComponent(gameId)}/items/${encodeURIComponent(itemId)}`, { method: 'PATCH', body: payload }),
|
||||
listAdminCustomItems: ({ q = '', page = 1, limit = 50, orphanOnly = false } = {}) =>
|
||||
listAdminCustomItems: ({ q = '', page = 1, limit = 50, filter = 'all' } = {}) =>
|
||||
request(
|
||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&orphanOnly=${encodeURIComponent(orphanOnly)}`
|
||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
|
||||
),
|
||||
listAdminTierLists: ({ q = '', page = 1, limit = 50 } = {}) =>
|
||||
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||
|
||||
@@ -5,7 +5,6 @@ import { api } from '../lib/api'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import lockResetIcon from '../assets/icons/lock_reset.svg'
|
||||
import deleteIcon from '../assets/icons/delete.svg'
|
||||
import addPhotoAlternateIcon from '../assets/icons/add_photo_alternate.svg'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import AdminFeaturedSection from '../components/admin/AdminFeaturedSection.vue'
|
||||
import AdminGamesSection from '../components/admin/AdminGamesSection.vue'
|
||||
@@ -43,7 +42,7 @@ const customItemQuery = ref('')
|
||||
const customItemPage = ref(1)
|
||||
const customItemLimit = ref(50)
|
||||
const customItemTotal = ref(0)
|
||||
const customItemOrphanOnly = ref(false)
|
||||
const customItemFilter = ref('all')
|
||||
const customItemModalTargetGameId = ref('')
|
||||
const customItemModalGameQuery = ref('')
|
||||
const customItemModalGameSort = ref('recent')
|
||||
@@ -128,6 +127,10 @@ function setItemFileInputRef(el) {
|
||||
itemFileInput.value = el
|
||||
}
|
||||
|
||||
function setThumbFileInputRef(el) {
|
||||
thumbFileInput.value = el
|
||||
}
|
||||
|
||||
function scheduleGameItemSortableSync() {
|
||||
if (gameItemSortableSyncTimer) {
|
||||
clearTimeout(gameItemSortableSyncTimer)
|
||||
@@ -445,7 +448,7 @@ watch(
|
||||
|
||||
if (tab === 'items') {
|
||||
customItemQuery.value = ''
|
||||
customItemOrphanOnly.value = false
|
||||
customItemFilter.value = 'all'
|
||||
customItemPage.value = 1
|
||||
customItemModalGameQuery.value = ''
|
||||
await refreshCustomItems()
|
||||
@@ -682,7 +685,7 @@ function setTab(tab) {
|
||||
}
|
||||
if (tab === 'items') {
|
||||
customItemQuery.value = ''
|
||||
customItemOrphanOnly.value = false
|
||||
customItemFilter.value = 'all'
|
||||
customItemPage.value = 1
|
||||
refreshCustomItems()
|
||||
}
|
||||
@@ -743,7 +746,7 @@ async function refreshCustomItems() {
|
||||
q: customItemQuery.value,
|
||||
page: customItemPage.value,
|
||||
limit: customItemLimit.value,
|
||||
orphanOnly: customItemOrphanOnly.value,
|
||||
filter: customItemFilter.value,
|
||||
})
|
||||
customItems.value = data.items || []
|
||||
customItemTotal.value = data.total || 0
|
||||
@@ -900,7 +903,7 @@ const {
|
||||
|
||||
const {
|
||||
submitCustomItemSearch,
|
||||
toggleCustomItemOrphanOnly,
|
||||
changeCustomItemFilter,
|
||||
changeCustomItemLimit,
|
||||
moveCustomItemPage,
|
||||
openCustomItemModal,
|
||||
@@ -920,7 +923,7 @@ const {
|
||||
customItemLimit,
|
||||
customItemPageCount,
|
||||
customItemQuery,
|
||||
customItemOrphanOnly,
|
||||
customItemFilter,
|
||||
customItemModalOpen,
|
||||
customItemDeleteModalOpen,
|
||||
customItemModalHistoryActive,
|
||||
@@ -1483,6 +1486,20 @@ function userAvatarFallback(user) {
|
||||
:is-game-loading="isGameLoading"
|
||||
:has-selected-game="hasSelectedGame"
|
||||
:selected-game="selectedGame"
|
||||
:display-thumbnail-url="displayThumbnailUrl"
|
||||
:can-apply-thumbnail="canApplyThumbnail"
|
||||
:game-visibility-saving="gameVisibilitySaving"
|
||||
:thumb-file-input-ref="setThumbFileInputRef"
|
||||
:open-thumb-file-picker="openThumbFilePicker"
|
||||
:on-thumb="onThumb"
|
||||
:on-thumb-drag-enter="onThumbDragEnter"
|
||||
:on-thumb-drag-over="onThumbDragOver"
|
||||
:on-thumb-drag-leave="onThumbDragLeave"
|
||||
:on-thumb-drop="onThumbDrop"
|
||||
:is-thumb-drag-over="isThumbDragOver"
|
||||
:upload-thumbnail="uploadThumbnail"
|
||||
:remove-game="removeGame"
|
||||
:toggle-selected-game-visibility="toggleSelectedGameVisibility"
|
||||
:item-file-input-ref="setItemFileInputRef"
|
||||
:on-file="onFile"
|
||||
:is-item-drag-over="isItemDragOver"
|
||||
@@ -1944,39 +1961,6 @@ function userAvatarFallback(user) {
|
||||
</div>
|
||||
<div v-if="selectedGameId && !hasSelectedGame && !isGameLoading" class="hint hint--tight">선택된 게임 ID: {{ selectedGameId }}</div>
|
||||
</div>
|
||||
<div v-if="hasSelectedGame" class="adminSidebar__group">
|
||||
<div class="selectedGameSidebar__name">{{ selectedGame.game.name }}</div>
|
||||
<div class="selectedGameSidebar__id">{{ selectedGame.game.id }}</div>
|
||||
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': gameVisibilitySaving }">
|
||||
<input :checked="!!selectedGame.game.isPublic" type="checkbox" @change="toggleSelectedGameVisibility($event.target.checked)" />
|
||||
<span class="toggleSwitch__label">{{ selectedGame.game.isPublic ? '템플릿 공개중' : '비공개 상태' }}</span>
|
||||
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||||
</label>
|
||||
<input ref="thumbFileInput" type="file" accept="image/*" class="srOnlyInput" @change="onThumb" />
|
||||
<button
|
||||
class="thumbDropZone"
|
||||
:class="{ 'thumbDropZone--active': isThumbDragOver }"
|
||||
type="button"
|
||||
@click="openThumbFilePicker"
|
||||
@dragenter="onThumbDragEnter"
|
||||
@dragover="onThumbDragOver"
|
||||
@dragleave="onThumbDragLeave"
|
||||
@drop="onThumbDrop"
|
||||
>
|
||||
<img v-if="displayThumbnailUrl" class="selectedThumb selectedThumb--sidebar" :src="displayThumbnailUrl" :alt="selectedGame.game.name" />
|
||||
<div v-else class="selectedThumb selectedThumb--empty selectedThumb--sidebar">대표 썸네일</div>
|
||||
<div class="thumbDropZone__copy">
|
||||
<div class="thumbDropZone__iconWrap">
|
||||
<SvgIcon :src="addPhotoAlternateIcon" alt="" class="thumbDropZone__icon" />
|
||||
</div>
|
||||
<div class="thumbDropZone__title">{{ displayThumbnailUrl ? '썸네일 변경' : '클릭 & 드래그' }}</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="adminSidebar__actions adminSidebar__actions--stack">
|
||||
<button class="btn" :disabled="!canApplyThumbnail" @click="uploadThumbnail">썸네일 적용</button>
|
||||
<button class="btn btn--danger" @click="removeGame">게임 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'items'" class="adminSidebar__panel">
|
||||
@@ -1990,14 +1974,18 @@ function userAvatarFallback(user) {
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
<label class="checkRow checkRow--compact">
|
||||
<input v-model="customItemOrphanOnly" type="checkbox" @change="toggleCustomItemOrphanOnly" />
|
||||
<span>미사용 사용자 업로드만 보기</span>
|
||||
</label>
|
||||
<select :value="customItemFilter" class="select" @change="changeCustomItemFilter($event.target.value)">
|
||||
<option value="all">전체 이미지</option>
|
||||
<option value="user">사용자 업로드</option>
|
||||
<option value="template">템플릿 사용 이미지</option>
|
||||
<option value="asset">관리자 보관 자산</option>
|
||||
<option value="unused-user">미사용 사용자 업로드</option>
|
||||
<option value="unused-admin">미사용 관리자 자산</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshCustomItems">새로고침</button>
|
||||
<button class="btn btn--danger" :disabled="!customItems.length" @click="removeUnusedCustomItems">미사용 이미지 일괄 삭제</button>
|
||||
<button class="btn btn--danger" :disabled="customItemFilter !== 'unused-user' || !customItems.length" @click="removeUnusedCustomItems">미사용 사용자 이미지 일괄 삭제</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
@@ -2112,8 +2100,14 @@ function userAvatarFallback(user) {
|
||||
<strong>{{ formatImageJobSourceCategory(job.sourceCategory) }}</strong>
|
||||
<span class="imageJobRow__status">{{ formatImageJobStatus(job.status) }}</span>
|
||||
</div>
|
||||
<div class="hint hint--tight">{{ formatBytes(job.originalByteSize) }} → {{ formatBytes(job.optimizedByteSize) }}</div>
|
||||
<div v-if="job.reusedAsset" class="hint hint--tight">기존 최적화 파일 재사용</div>
|
||||
<div class="hint hint--tight">
|
||||
{{
|
||||
job.reusedAsset
|
||||
? `이번 업로드 ${formatBytes(job.originalByteSize)} · 재사용 자산 ${formatBytes(job.optimizedByteSize)}`
|
||||
: `${formatBytes(job.originalByteSize)} → ${formatBytes(job.optimizedByteSize)}`
|
||||
}}
|
||||
</div>
|
||||
<div v-if="job.reusedAsset" class="hint hint--tight">동일한 최적화 결과가 이미 있어 새 파일을 다시 만들지 않았어요.</div>
|
||||
<div class="hint hint--tight">{{ fmt(job.queuedAt) }}</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -2720,6 +2714,31 @@ function userAvatarFallback(user) {
|
||||
opacity: 0.72;
|
||||
word-break: break-all;
|
||||
}
|
||||
.adminUiScope .gameSettingsCard {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
.adminUiScope .gameSettingsCard__media {
|
||||
min-width: 0;
|
||||
}
|
||||
.adminUiScope .gameSettingsCard__body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
align-content: center;
|
||||
}
|
||||
.adminUiScope .gameSettingsCard__meta {
|
||||
color: var(--theme-text-soft);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
.adminUiScope .gameSettingsCard__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.adminUiScope .selectedThumb {
|
||||
width: min(100%, 256px);
|
||||
aspect-ratio: 16 / 9;
|
||||
@@ -4122,6 +4141,7 @@ function userAvatarFallback(user) {
|
||||
.adminUiScope .featuredOrderPanel,
|
||||
.adminUiScope .section--topGrid,
|
||||
.adminUiScope .gameManagerGrid,
|
||||
.adminUiScope .gameSettingsCard,
|
||||
.adminUiScope .toolbar,
|
||||
.adminUiScope .itemComposer,
|
||||
.adminUiScope .tierAdminCard,
|
||||
|
||||
Reference in New Issue
Block a user