게시물 export 진행도 표시 추가 v1.5.21

This commit is contained in:
2026-06-01 12:45:56 +09:00
parent 11203ba251
commit 7c8245c4e9
11 changed files with 139 additions and 8 deletions

View File

@@ -79,6 +79,7 @@ const spamSnapshot = reactive({
})
let toastTimer = null
let scrollSpyFrame = null
let postExportRefreshTimer = null
const { data: settings } = await useFetch('/admin/api/settings')
const {
@@ -171,6 +172,14 @@ const hasSpamChanges = computed(() => editSpam.value
*/
const normalizedPostExportJobs = computed(() => Array.isArray(postExportJobs.value) ? postExportJobs.value : [])
/**
* 진행 중인 게시물 export 작업이 있는지 확인한다.
* @returns {boolean} 진행 중 작업 여부
*/
const hasActivePostExportJobs = computed(() => normalizedPostExportJobs.value.some((job) => (
job.status === 'queued' || job.status === 'processing'
)))
/**
* export 상태 라벨 조회
* @param {string} status - export 상태
@@ -221,6 +230,65 @@ const formatExportFileSize = (value) => {
return `${size.toFixed(size >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`
}
/**
* export 작업 진행률을 계산한다.
* @param {Object} job - export 작업
* @returns {number} 진행률
*/
const getPostExportProgressPercent = (job) => {
const total = Number(job?.postCount || 0)
const processed = Number(job?.processedCount || 0)
if (!total) {
return job?.status === 'ready' ? 100 : 0
}
return Math.min(Math.max(Math.round((processed / total) * 100), 0), 100)
}
/**
* export 작업 진행 숫자 라벨을 만든다.
* @param {Object} job - export 작업
* @returns {string} 진행 숫자 라벨
*/
const getPostExportProgressLabel = (job) => {
const total = Number(job?.postCount || 0)
const processed = Math.min(Number(job?.processedCount || 0), total)
if (!total) {
return '0 / 0'
}
return `${processed.toLocaleString()} / ${total.toLocaleString()}`
}
/**
* export 작업 진행 보조 설명을 만든다.
* @param {Object} job - export 작업
* @returns {string} 진행 보조 설명
*/
const getPostExportProgressDescription = (job) => {
if (job.progressMessage) {
return job.progressMessage
}
if (job.status === 'queued') {
return '작업 대기열에 등록되었습니다. 생성 워커가 시작되면 진행 숫자가 갱신됩니다.'
}
if (job.status === 'processing') {
return job.currentPartIndex
? `${job.currentPartIndex}번째 분할 파일을 생성하는 중입니다.`
: '분할 파일을 생성하는 중입니다.'
}
if (job.status === 'ready') {
return '생성이 완료되었습니다.'
}
return job.message || ''
}
/**
* 가입 금지 닉네임 textarea 바인딩
*/
@@ -906,6 +974,11 @@ const onGlobalKeydown = (event) => {
onMounted(() => {
if (import.meta.client) {
window.addEventListener('keydown', onGlobalKeydown)
postExportRefreshTimer = window.setInterval(() => {
if (hasActivePostExportJobs.value) {
refreshPostExportJobs()
}
}, 5000)
nextTick(() => {
updateActiveSectionFromScroll()
})
@@ -914,6 +987,7 @@ onMounted(() => {
onBeforeUnmount(() => {
window.clearTimeout(toastTimer)
window.clearInterval(postExportRefreshTimer)
if (scrollSpyFrame) {
cancelAnimationFrame(scrollSpyFrame)
}
@@ -1790,9 +1864,6 @@ onBeforeUnmount(() => {
<p class="mt-2 text-xs text-[#657080]">
요청: {{ formatPostDateTime(job.createdAt) }} · 만료: {{ formatPostDateTime(job.expiresAt) }}
</p>
<p v-if="job.message" class="mt-2 text-sm text-[#657080]">
{{ job.message }}
</p>
</div>
<button
class="inline-flex h-9 shrink-0 cursor-not-allowed items-center justify-center rounded-md border border-[#dce0e5] px-3 text-xs font-semibold text-[#9aa3ad]"
@@ -1803,6 +1874,27 @@ onBeforeUnmount(() => {
</button>
</div>
<div class="admin-settings-screen__export-progress mt-4 rounded-lg border border-[#edf0f3] bg-[#fbfcfd] p-3">
<div class="mb-2 flex items-center justify-between gap-3 text-xs">
<span class="font-semibold text-[#15171a]">
진행도 {{ getPostExportProgressLabel(job) }}
</span>
<span class="text-[#657080]">
{{ getPostExportProgressPercent(job) }}%
</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-[#e8edf2]">
<div
class="h-full rounded-full bg-[#15171a] transition-[width] duration-500"
:style="{ width: `${getPostExportProgressPercent(job)}%` }"
/>
</div>
<div class="mt-2 flex flex-col gap-1 text-xs text-[#657080] md:flex-row md:items-center md:justify-between">
<span>{{ getPostExportProgressDescription(job) }}</span>
<span>마지막 갱신: {{ formatPostDateTime(job.updatedAt) }}</span>
</div>
</div>
<div v-if="job.files.length > 0" class="admin-settings-screen__export-files mt-4 overflow-hidden rounded-md border border-[#edf0f3]">
<div
v-for="file in job.files"