관리자 설정 사이트 용량 카드를 간결한 통계형 UI로 정리
백업 현황을 사이트 용량으로 재정의하고 파일 집계를 이미지·영상·오디오·첨부 파일·보내기로 명확히 분리해 합계 불일치를 해소했다. 설정 화면의 통계 카드 스타일을 통일해 가독성을 높였다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -524,14 +524,14 @@ const formatExportFileSize = (value) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 백업 현황 숫자를 쉼표 포함 문자열로 변환한다.
|
||||
* 사이트 용량 숫자를 쉼표 포함 문자열로 변환한다.
|
||||
* @param {number} value - 숫자 값
|
||||
* @returns {string} 숫자 라벨
|
||||
*/
|
||||
const formatBackupNumber = (value) => Number(value || 0).toLocaleString()
|
||||
|
||||
/**
|
||||
* 백업 현황 용량을 읽기 쉬운 문자열로 변환한다.
|
||||
* 사이트 용량 바이트를 읽기 쉬운 문자열로 변환한다.
|
||||
* @param {number} value - 바이트 값
|
||||
* @returns {string} 용량 라벨
|
||||
*/
|
||||
@@ -555,7 +555,7 @@ const formatBackupSize = (value) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 백업 현황 기준 시각을 표시한다.
|
||||
* 사이트 용량 기준 시각을 표시한다.
|
||||
* @param {string} value - ISO 날짜 문자열
|
||||
* @returns {string} 날짜 라벨
|
||||
*/
|
||||
@@ -571,7 +571,7 @@ const formatBackupGeneratedAt = (value) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 백업 현황 콘텐츠 지표 목록
|
||||
* 사이트 용량 콘텐츠 지표 목록
|
||||
* @returns {Array<{ label: string, value: string, helper: string }>} 콘텐츠 지표
|
||||
*/
|
||||
const backupContentMetrics = computed(() => {
|
||||
@@ -612,59 +612,82 @@ const backupContentMetrics = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 백업 현황 저장소 지표 목록
|
||||
* @returns {Array<{ label: string, value: string, helper: string }>} 저장소 지표
|
||||
* 사이트 용량 저장소 행 값을 포맷한다.
|
||||
* @param {number} count - 파일 개수
|
||||
* @param {number} sizeBytes - 파일 용량
|
||||
* @returns {string} 개수·용량 라벨
|
||||
*/
|
||||
const backupStorageMetrics = computed(() => {
|
||||
const formatBackupStatValue = (count, sizeBytes) => `${formatBackupNumber(count)}개 · ${formatBackupSize(sizeBytes)}`
|
||||
|
||||
/**
|
||||
* 사이트 용량 저장소 섹션 목록
|
||||
* @returns {{ imageRows: Array<{ label: string, value: string, child?: boolean }>, mainRows: Array<{ label: string, value: string }>, totalRow: { label: string, value: string } }} 저장소 섹션
|
||||
*/
|
||||
const backupStorageSections = computed(() => {
|
||||
const storage = backupSummary.value?.storage || {}
|
||||
const total = storage.total || {}
|
||||
const originals = storage.originals || {}
|
||||
const derivatives = storage.derivatives || {}
|
||||
const profiles = storage.profiles || {}
|
||||
const exports = storage.exports || {}
|
||||
const byKind = storage.byKind || {}
|
||||
|
||||
return [
|
||||
const images = byKind.images || {}
|
||||
const videos = byKind.videos || {}
|
||||
const audio = byKind.audio || {}
|
||||
const files = byKind.files || {}
|
||||
const otherCount = Number(audio.count || 0) + Number(files.count || 0)
|
||||
const otherSize = Number(audio.sizeBytes || 0) + Number(files.sizeBytes || 0)
|
||||
const imageOriginalCount = Math.max(0, Number(images.count || 0) - Number(derivatives.count || 0) - Number(profiles.count || 0))
|
||||
const imageOriginalSize = Math.max(0, Number(images.sizeBytes || 0) - Number(derivatives.sizeBytes || 0) - Number(profiles.sizeBytes || 0))
|
||||
const totalCount = Number(images.count || 0) + Number(videos.count || 0) + otherCount + Number(exports.count || 0)
|
||||
const totalSize = Number(images.sizeBytes || 0) + Number(videos.sizeBytes || 0) + otherSize + Number(exports.sizeBytes || 0)
|
||||
const mainRows = [
|
||||
{
|
||||
label: '업로드 미디어',
|
||||
value: `${formatBackupNumber(total.count)}개`,
|
||||
helper: `누적 ${formatBackupSize(total.sizeBytes)}`
|
||||
label: '영상',
|
||||
value: formatBackupStatValue(videos.count, videos.sizeBytes)
|
||||
},
|
||||
{
|
||||
label: '원본 미디어',
|
||||
value: `${formatBackupNumber(originals.count)}개`,
|
||||
helper: formatBackupSize(originals.sizeBytes)
|
||||
},
|
||||
{
|
||||
label: '자동 생성 이미지',
|
||||
value: `${formatBackupNumber(derivatives.count)}개`,
|
||||
helper: formatBackupSize(derivatives.sizeBytes)
|
||||
},
|
||||
{
|
||||
label: '프로필 이미지',
|
||||
value: `${formatBackupNumber(profiles.count)}개`,
|
||||
helper: formatBackupSize(profiles.sizeBytes)
|
||||
},
|
||||
{
|
||||
label: '내보내기 파일',
|
||||
value: `${formatBackupNumber(exports.count)}개`,
|
||||
helper: formatBackupSize(exports.sizeBytes)
|
||||
},
|
||||
{
|
||||
label: '이미지·영상',
|
||||
value: `${formatBackupNumber(byKind.images?.count)} / ${formatBackupNumber(byKind.videos?.count)}개`,
|
||||
helper: `이미지 ${formatBackupSize(byKind.images?.sizeBytes)} · 영상 ${formatBackupSize(byKind.videos?.sizeBytes)}`
|
||||
label: '보내기',
|
||||
value: formatBackupStatValue(exports.count, exports.sizeBytes)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 백업 현황 큰 파일 목록
|
||||
* @returns {Array<Object>} 큰 파일 목록
|
||||
*/
|
||||
const backupLargestFiles = computed(() => Array.isArray(backupSummary.value?.storage?.largestFiles)
|
||||
? backupSummary.value.storage.largestFiles
|
||||
: [])
|
||||
if (Number(audio.count || 0) > 0) {
|
||||
mainRows.push({
|
||||
label: '오디오',
|
||||
value: formatBackupStatValue(audio.count, audio.sizeBytes)
|
||||
})
|
||||
}
|
||||
|
||||
if (Number(files.count || 0) > 0) {
|
||||
mainRows.push({
|
||||
label: '첨부 파일',
|
||||
value: formatBackupStatValue(files.count, files.sizeBytes)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
imageRows: [
|
||||
{
|
||||
label: '이미지',
|
||||
value: formatBackupStatValue(imageOriginalCount, imageOriginalSize)
|
||||
},
|
||||
{
|
||||
label: '자동생성',
|
||||
value: formatBackupStatValue(derivatives.count, derivatives.sizeBytes),
|
||||
child: true
|
||||
},
|
||||
{
|
||||
label: '프로필',
|
||||
value: formatBackupStatValue(profiles.count, profiles.sizeBytes),
|
||||
child: true
|
||||
}
|
||||
],
|
||||
mainRows,
|
||||
totalRow: {
|
||||
label: '합계',
|
||||
value: formatBackupStatValue(totalCount, totalSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* export 작업 진행률을 계산한다.
|
||||
@@ -767,7 +790,7 @@ const settingsNavGroups = [
|
||||
{
|
||||
heading: '콘텐츠·안전',
|
||||
items: [
|
||||
{ id: 'admin-settings-section-backup-summary', label: '백업 현황', keywords: 'backup storage media count volume 용량 통계', iconId: 'post-export' },
|
||||
{ id: 'admin-settings-section-backup-summary', label: '사이트 용량', keywords: 'storage media count volume 용량 통계 사이트', iconId: 'post-export' },
|
||||
{ id: 'admin-settings-section-export', label: '게시물 내보내기', keywords: 'export backup 내보내기', iconId: 'post-export' },
|
||||
{ id: 'admin-settings-section-import', label: '게시물 가져오기', keywords: 'import restore 가져오기', iconId: 'post-import' },
|
||||
{ id: 'admin-settings-section-spam', label: '스팸 필터', keywords: 'spam moderation comments', iconId: 'spam' }
|
||||
@@ -3472,10 +3495,10 @@ onBeforeUnmount(() => {
|
||||
<div class="admin-settings-screen__card-head flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-lg font-semibold text-[#15171a]">
|
||||
백업 현황
|
||||
사이트 용량
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-relaxed text-[#657080]">
|
||||
콘텐츠 수량과 업로드 용량을 기준으로 현재 백업 규모를 확인합니다.
|
||||
콘텐츠 수량과 업로드 파일 용량을 한눈에 확인합니다.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -3494,67 +3517,62 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-3">
|
||||
<h3 class="text-sm font-bold text-[#15171a]">
|
||||
콘텐츠
|
||||
</h3>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="mt-5 grid gap-3 border-t border-[#eceff2] pt-5">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<span class="font-bold text-[#15171a]">콘텐츠</span>
|
||||
<span class="text-xs text-[#657080]">DB 기준</span>
|
||||
</div>
|
||||
<div class="grid gap-2 text-xs text-[#657080] sm:grid-cols-2">
|
||||
<div
|
||||
v-for="metric in backupContentMetrics"
|
||||
:key="`backup-content-${metric.label}`"
|
||||
class="admin-settings-screen__backup-metric rounded-lg border border-[#edf0f2] bg-[#fbfcfd] px-4 py-3"
|
||||
class="rounded-md border border-[#edf0f2] px-3 py-2"
|
||||
:class="metric.span ? 'sm:col-span-2' : ''"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="text-sm font-semibold text-[#657080]">{{ metric.label }}</span>
|
||||
<strong class="text-sm font-bold text-[#15171a]">{{ metric.value }}</strong>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ metric.value }}</strong>
|
||||
</div>
|
||||
<p class="mt-2 text-xs leading-relaxed text-[#657080]">
|
||||
<p
|
||||
v-if="metric.helper"
|
||||
class="mt-1 text-[11px] leading-relaxed text-[#657080]"
|
||||
>
|
||||
{{ metric.helper }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-3 border-t border-[#eceff2] pt-5">
|
||||
<h3 class="text-sm font-bold text-[#15171a]">
|
||||
파일·용량
|
||||
</h3>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div
|
||||
v-for="metric in backupStorageMetrics"
|
||||
:key="`backup-storage-${metric.label}`"
|
||||
class="admin-settings-screen__backup-metric rounded-lg border border-[#edf0f2] bg-[#fbfcfd] px-4 py-3"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="text-sm font-semibold text-[#657080]">{{ metric.label }}</span>
|
||||
<strong class="text-sm font-bold text-[#15171a]">{{ metric.value }}</strong>
|
||||
</div>
|
||||
<p class="mt-2 text-xs leading-relaxed text-[#657080]">
|
||||
{{ metric.helper }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-3 border-t border-[#eceff2] pt-5">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<span class="font-bold text-[#15171a]">파일·용량</span>
|
||||
<span class="text-xs text-[#657080]">public/uploads 기준</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="backupLargestFiles.length"
|
||||
class="mt-6 grid gap-3 border-t border-[#eceff2] pt-5"
|
||||
>
|
||||
<h3 class="text-sm font-bold text-[#15171a]">
|
||||
큰 파일
|
||||
</h3>
|
||||
<div class="grid gap-2">
|
||||
<div
|
||||
v-for="file in backupLargestFiles"
|
||||
:key="file.url"
|
||||
class="admin-settings-screen__backup-file grid gap-1 rounded-lg border border-[#edf0f2] px-4 py-3 text-sm md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-4"
|
||||
>
|
||||
<span class="min-w-0 truncate font-semibold text-[#15171a]">
|
||||
{{ file.name }}
|
||||
</span>
|
||||
<span class="font-mono text-xs font-semibold text-[#657080]">
|
||||
{{ formatBackupSize(file.sizeBytes) }}
|
||||
</span>
|
||||
<div class="grid gap-2 text-xs text-[#657080]">
|
||||
<div class="admin-settings-screen__backup-image-group grid gap-2">
|
||||
<div
|
||||
v-for="row in backupStorageSections.imageRows"
|
||||
:key="`backup-storage-image-${row.label}`"
|
||||
class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2"
|
||||
:class="row.child ? 'border-dashed' : ''"
|
||||
>
|
||||
<span>{{ row.label }}</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ row.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div
|
||||
v-for="row in backupStorageSections.mainRows"
|
||||
:key="`backup-storage-main-${row.label}`"
|
||||
class="flex items-center justify-between gap-3 rounded-md border border-[#edf0f2] px-3 py-2"
|
||||
>
|
||||
<span>{{ row.label }}</span>
|
||||
<strong class="font-mono text-[#15171a]">{{ row.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-[#dce0e5] bg-[#fbfcfd] px-3 py-2">
|
||||
<span class="font-bold text-[#15171a]">{{ backupStorageSections.totalRow.label }}</span>
|
||||
<strong class="font-mono font-bold text-[#15171a]">{{ backupStorageSections.totalRow.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user