게시물 export 작업 기반 추가 v1.5.20
This commit is contained in:
@@ -20,6 +20,7 @@ const savingSpam = ref(false)
|
||||
const uploadingLogo = ref(false)
|
||||
const uploadingHomeCover = ref(false)
|
||||
const uploadingHomeCoverDark = ref(false)
|
||||
const requestingPostExport = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const toast = ref(null)
|
||||
const logoInputRef = ref(null)
|
||||
@@ -80,6 +81,12 @@ let toastTimer = null
|
||||
let scrollSpyFrame = null
|
||||
|
||||
const { data: settings } = await useFetch('/admin/api/settings')
|
||||
const {
|
||||
data: postExportJobs,
|
||||
refresh: refreshPostExportJobs
|
||||
} = await useFetch('/admin/api/posts/export-jobs', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
title: settings.value?.title || 'sori.studio',
|
||||
@@ -158,6 +165,62 @@ const hasAnnouncementChanges = computed(() => customizeAnnouncement.value && (
|
||||
const hasSpamChanges = computed(() => editSpam.value
|
||||
&& JSON.stringify(form.signupBlockedUsernames) !== JSON.stringify(spamSnapshot.signupBlockedUsernames))
|
||||
|
||||
/**
|
||||
* 최신 게시물 export 작업 목록
|
||||
* @returns {Array} export 작업 목록
|
||||
*/
|
||||
const normalizedPostExportJobs = computed(() => Array.isArray(postExportJobs.value) ? postExportJobs.value : [])
|
||||
|
||||
/**
|
||||
* export 상태 라벨 조회
|
||||
* @param {string} status - export 상태
|
||||
* @returns {string} 상태 라벨
|
||||
*/
|
||||
const getPostExportStatusLabel = (status) => ({
|
||||
queued: '대기 중',
|
||||
processing: '생성 중',
|
||||
ready: '준비 완료',
|
||||
failed: '실패',
|
||||
expired: '만료'
|
||||
}[status] || '알 수 없음')
|
||||
|
||||
/**
|
||||
* export 상태 배지 클래스 조회
|
||||
* @param {string} status - export 상태
|
||||
* @returns {string} Tailwind 클래스
|
||||
*/
|
||||
const getPostExportStatusClass = (status) => ({
|
||||
queued: 'bg-[#eef4ff] text-[#2f5fbb] ring-[#c9dafd]',
|
||||
processing: 'bg-[#fff7e8] text-[#9a6200] ring-[#f3d39b]',
|
||||
ready: 'bg-[#eaf8f0] text-[#147a45] ring-[#b9e7cd]',
|
||||
failed: 'bg-[#fff0f0] text-[#c53232] ring-[#f3c2c2]',
|
||||
expired: 'bg-[#f1f3f5] text-[#657080] ring-[#dce0e5]'
|
||||
}[status] || 'bg-[#f1f3f5] text-[#657080] ring-[#dce0e5]')
|
||||
|
||||
/**
|
||||
* 바이트 값을 읽기 쉬운 용량으로 변환한다.
|
||||
* @param {number} value - 바이트 값
|
||||
* @returns {string} 용량 라벨
|
||||
*/
|
||||
const formatExportFileSize = (value) => {
|
||||
const bytes = Number(value || 0)
|
||||
|
||||
if (!bytes) {
|
||||
return '생성 대기'
|
||||
}
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
|
||||
return `${size.toFixed(size >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 가입 금지 닉네임 textarea 바인딩
|
||||
*/
|
||||
@@ -325,6 +388,37 @@ const showToast = (type, message) => {
|
||||
}, 3200)
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 export 작업을 요청한다.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const requestPostExport = async () => {
|
||||
if (requestingPostExport.value) {
|
||||
return
|
||||
}
|
||||
|
||||
requestingPostExport.value = true
|
||||
errorMessage.value = ''
|
||||
showToast('info', '게시물 Export 작업을 등록하는 중입니다.')
|
||||
|
||||
try {
|
||||
await $fetch('/admin/api/posts/export-jobs', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
chunkSize: 100,
|
||||
retentionDays: 100
|
||||
}
|
||||
})
|
||||
await refreshPostExportJobs()
|
||||
showToast('success', '게시물 Export 작업이 등록되었습니다.')
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '게시물 Export 작업을 등록하지 못했습니다.'
|
||||
showToast('error', errorMessage.value)
|
||||
} finally {
|
||||
requestingPostExport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로고 파일 선택창을 연다.
|
||||
* @returns {void}
|
||||
@@ -1629,11 +1723,111 @@ onBeforeUnmount(() => {
|
||||
게시물 Import/Export
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-relaxed text-[#657080]">
|
||||
마크다운 등 형식으로 게시물을 가져오거나 보냅니다. (준비 중)
|
||||
게시물 백업을 서버 작업으로 등록하고, 준비된 분할 파일을 내려받습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="admin-settings-screen__placeholder mt-4 rounded-lg border border-dashed border-[#dce0e5] bg-[#f7f8fa] px-4 py-8 text-center text-sm text-[#657080]">
|
||||
이후 버전에서 일괄 가져오기·보내기 도구를 제공합니다.
|
||||
|
||||
<div class="admin-settings-screen__export-actions mt-5 flex flex-col gap-3 rounded-lg border border-[#e6e8eb] bg-[#fbfcfd] p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-[#15171a]">
|
||||
Obsidian 호환 백업 준비
|
||||
</p>
|
||||
<p class="mt-1 text-sm leading-relaxed text-[#657080]">
|
||||
100개 단위 분할 zip 계획을 만들고, 산출물은 최대 100일 동안 보관합니다.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="admin-settings-screen__export-request inline-flex h-10 shrink-0 cursor-pointer items-center justify-center rounded-md bg-[#15171a] px-4 text-sm font-semibold text-white transition hover:bg-black disabled:cursor-not-allowed disabled:bg-[#c7cdd4]"
|
||||
type="button"
|
||||
:disabled="requestingPostExport"
|
||||
@click="requestPostExport"
|
||||
>
|
||||
{{ requestingPostExport ? '요청 중...' : 'Export 요청' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-settings-screen__export-list mt-5">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold text-[#15171a]">
|
||||
최근 Export 작업
|
||||
</h3>
|
||||
<button
|
||||
class="inline-flex h-8 cursor-pointer items-center justify-center rounded px-3 text-xs font-semibold text-[#5d6673] transition hover:bg-[#eceff2] hover:text-[#15171a]"
|
||||
type="button"
|
||||
@click="refreshPostExportJobs"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="normalizedPostExportJobs.length === 0"
|
||||
class="admin-settings-screen__export-empty rounded-lg border border-dashed border-[#dce0e5] bg-[#f7f8fa] px-4 py-8 text-center text-sm text-[#657080]"
|
||||
>
|
||||
아직 등록된 Export 작업이 없습니다.
|
||||
</div>
|
||||
<div v-else class="admin-settings-screen__export-items grid gap-3">
|
||||
<article
|
||||
v-for="job in normalizedPostExportJobs"
|
||||
:key="job.id"
|
||||
class="admin-settings-screen__export-item rounded-lg border border-[#e6e8eb] bg-white p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold ring-1 ring-inset"
|
||||
:class="getPostExportStatusClass(job.status)"
|
||||
>
|
||||
{{ getPostExportStatusLabel(job.status) }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-[#15171a]">
|
||||
게시물 {{ job.postCount }}개
|
||||
</span>
|
||||
<span class="text-sm text-[#9aa3ad]">
|
||||
{{ job.files.length }}개 파일
|
||||
</span>
|
||||
</div>
|
||||
<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]"
|
||||
type="button"
|
||||
disabled
|
||||
>
|
||||
일괄 다운로드 준비 중
|
||||
</button>
|
||||
</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"
|
||||
:key="file.id"
|
||||
class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b border-[#edf0f3] px-3 py-2 last:border-b-0"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium text-[#15171a]">
|
||||
{{ file.fileName }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-[#9aa3ad]">
|
||||
{{ file.postStart }}-{{ file.postEnd }} · {{ formatExportFileSize(file.fileSizeBytes) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex h-8 cursor-not-allowed items-center justify-center rounded border border-[#e1e5ea] px-3 text-xs font-semibold text-[#a6b0bb]"
|
||||
type="button"
|
||||
disabled
|
||||
>
|
||||
다운로드 대기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user