릴리스: v1.3.5 이미지 최적화 대시보드 기간 필터와 실사용 통계
This commit is contained in:
@@ -44,7 +44,13 @@ export const api = {
|
||||
listAdminTierLists: ({ q = '', page = 1, limit = 50 } = {}) =>
|
||||
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||
listAdminTemplateRequests: () => request('/api/admin/template-requests'),
|
||||
getAdminImageAssetStats: () => request('/api/admin/image-assets/stats'),
|
||||
getAdminImageAssetStats: ({ month = '', limit = 12 } = {}) => {
|
||||
const query = new URLSearchParams()
|
||||
if (month) query.set('month', month)
|
||||
query.set('limit', String(limit))
|
||||
return request(`/api/admin/image-assets/stats?${query.toString()}`)
|
||||
},
|
||||
resetAdminImageAssetStats: (payload) => request('/api/admin/image-assets/stats/reset', { method: 'POST', body: payload || {} }),
|
||||
listAdminUnusedImageAssets: ({ limit = 100, minAgeHours = 24 } = {}) => request(`/api/admin/image-assets/orphans?limit=${encodeURIComponent(limit)}&minAgeHours=${encodeURIComponent(minAgeHours)}`),
|
||||
cleanupAdminUnusedImageAssets: (payload) => request('/api/admin/image-assets/cleanup', { method: 'POST', body: payload || {} }),
|
||||
promoteAdminCustomItem: (itemId, payload) =>
|
||||
|
||||
@@ -60,6 +60,9 @@ const users = ref([])
|
||||
const imageStats = ref(null)
|
||||
const imageQueue = ref({ concurrency: 1, activeCount: 0, pendingCount: 0 })
|
||||
const imageRecentJobs = ref([])
|
||||
const imageStatsMonth = ref('')
|
||||
const imageStatsLimit = ref(12)
|
||||
const imageResetModalOpen = ref(false)
|
||||
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
@@ -246,16 +249,23 @@ const imageDiagnosticsCards = computed(() => {
|
||||
const stats = imageStats.value
|
||||
if (!stats) return []
|
||||
return [
|
||||
{ label: '총 자산', value: `${stats.assetCount || 0}` },
|
||||
{ label: '현재 용량', value: formatBytes(stats.totalByteSize) },
|
||||
{ label: '실사용 파일', value: `${stats.referencedCount || 0}` },
|
||||
{ label: '현재 용량', value: formatBytes(stats.referencedByteSize) },
|
||||
{ label: '추적 자산', value: `${stats.trackedReferencedCount || 0}` },
|
||||
{ label: '레거시 참조', value: `${stats.legacyReferencedCount || 0}` },
|
||||
{ label: '절감 용량', value: formatBytes(stats.savedByteSize) },
|
||||
{ label: '절감률', value: `${Math.round((stats.savingsRatio || 0) * 100)}%` },
|
||||
]
|
||||
})
|
||||
|
||||
const imageStatsPeriodLabel = computed(() => (imageStatsMonth.value ? `${imageStatsMonth.value} 기준` : '전체 기간'))
|
||||
|
||||
async function refreshImageDiagnostics() {
|
||||
try {
|
||||
const data = await api.getAdminImageAssetStats()
|
||||
const data = await api.getAdminImageAssetStats({
|
||||
month: imageStatsMonth.value || '',
|
||||
limit: imageStatsLimit.value,
|
||||
})
|
||||
imageStats.value = data.stats || null
|
||||
imageQueue.value = data.queue || { concurrency: 1, activeCount: 0, pendingCount: 0 }
|
||||
imageRecentJobs.value = data.recentJobs || []
|
||||
@@ -264,6 +274,27 @@ async function refreshImageDiagnostics() {
|
||||
}
|
||||
}
|
||||
|
||||
function openImageResetModal() {
|
||||
imageResetModalOpen.value = true
|
||||
}
|
||||
|
||||
function closeImageResetModal() {
|
||||
imageResetModalOpen.value = false
|
||||
}
|
||||
|
||||
async function confirmImageReset() {
|
||||
try {
|
||||
const data = await api.resetAdminImageAssetStats({ month: imageStatsMonth.value || null })
|
||||
success.value = imageStatsMonth.value
|
||||
? `${imageStatsMonth.value} 기록 ${data.deletedCount || 0}건을 정리했어요.`
|
||||
: `전체 최적화 기록 ${data.deletedCount || 0}건을 정리했어요.`
|
||||
closeImageResetModal()
|
||||
await refreshImageDiagnostics()
|
||||
} catch (e) {
|
||||
error.value = '이미지 최적화 기록을 정리하지 못했어요.'
|
||||
}
|
||||
}
|
||||
|
||||
function setTab(tab) {
|
||||
resetMessages()
|
||||
activeTab.value = tab
|
||||
@@ -1733,6 +1764,19 @@ async function saveFeaturedOrder() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="imageResetModalOpen" class="modalOverlay" @click.self="closeImageResetModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true">
|
||||
<div class="modalCard__title">최적화 기록 비우기</div>
|
||||
<div class="modalCard__desc">
|
||||
{{ imageStatsMonth ? `${imageStatsMonth} 기간의 최적화 기록만 삭제합니다.` : '전체 최적화 작업 기록을 비웁니다. 실제 이미지 파일은 삭제되지 않아요.' }}
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
<button class="btn btn--ghost" @click="closeImageResetModal">취소</button>
|
||||
<button class="btn btn--danger" @click="confirmImageReset">기록 비우기</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">
|
||||
@@ -1883,9 +1927,19 @@ async function saveFeaturedOrder() {
|
||||
|
||||
<section class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Image Optimization</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshImageDiagnostics">현황 새로고침</button>
|
||||
<div class="adminSidebar__group">
|
||||
<input v-model="imageStatsMonth" class="input" type="month" />
|
||||
<select v-model.number="imageStatsLimit" class="select">
|
||||
<option :value="6">최근 6건</option>
|
||||
<option :value="12">최근 12건</option>
|
||||
<option :value="24">최근 24건</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adminSidebar__actions adminSidebar__actions--split">
|
||||
<button class="btn btn--ghost" @click="refreshImageDiagnostics">현황 새로고침</button>
|
||||
<button class="btn btn--ghost" @click="openImageResetModal">기록 비우기</button>
|
||||
</div>
|
||||
<div class="hint hint--tight">{{ imageStatsPeriodLabel }}</div>
|
||||
<div v-if="imageDiagnosticsCards.length" class="adminSidebar__stats adminSidebar__stats--grid">
|
||||
<article v-for="stat in imageDiagnosticsCards" :key="stat.label" class="sidebarStat">
|
||||
<span class="sidebarStat__label">{{ stat.label }}</span>
|
||||
@@ -1898,16 +1952,21 @@ async function saveFeaturedOrder() {
|
||||
<strong class="sidebarStat__value">{{ imageQueue.activeCount }} 실행 / {{ imageQueue.pendingCount }} 대기</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">누적 작업</span>
|
||||
<span class="sidebarStat__label">작업 누적</span>
|
||||
<strong class="sidebarStat__value">{{ imageStats?.completedCount || 0 }} 완료 · {{ imageStats?.failedCount || 0 }} 실패</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">중복 재사용</span>
|
||||
<strong class="sidebarStat__value">{{ imageStats?.reusedCount || 0 }}건</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">누락 파일</span>
|
||||
<strong class="sidebarStat__value">{{ imageStats?.missingReferencedCount || 0 }}건</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adminSidebar__group">
|
||||
<div class="section__title">최근 최적화 작업</div>
|
||||
<div class="hint hint--tight">현재 {{ imageRecentJobs.length }}건 표시 중</div>
|
||||
<div v-if="!imageRecentJobs.length" class="hint hint--tight">아직 기록된 최적화 작업이 없어요.</div>
|
||||
<div v-else class="imageJobList">
|
||||
<article v-for="job in imageRecentJobs" :key="job.id" class="imageJobRow">
|
||||
@@ -1916,6 +1975,7 @@ async function saveFeaturedOrder() {
|
||||
<span class="imageJobRow__status">{{ job.status }}</span>
|
||||
</div>
|
||||
<div class="hint hint--tight">{{ formatBytes(job.originalByteSize) }} → {{ formatBytes(job.optimizedByteSize) }}</div>
|
||||
<div class="hint hint--tight">{{ fmt(job.queuedAt) }}</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2058,6 +2118,9 @@ async function saveFeaturedOrder() {
|
||||
.adminSidebar__actions--stack .btn {
|
||||
width: 100%;
|
||||
}
|
||||
.adminSidebar__actions--split {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.adminSidebar__groupTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
|
||||
Reference in New Issue
Block a user