230 lines
9.2 KiB
Vue
230 lines
9.2 KiB
Vue
<script setup>
|
|
/**
|
|
* 게시물 내보내기 작업 목록 카드
|
|
* @property {Array} jobs - Export 작업 목록
|
|
* @property {Function} formatDateTime - 날짜 표시 함수
|
|
* @property {Function} getStatusLabel - 상태 라벨 함수
|
|
* @property {Function} getStatusClass - 상태 배지 클래스 함수
|
|
* @property {Function} shouldShowProgress - 진행도 표시 여부 함수
|
|
* @property {Function} getProgressLabel - 진행 숫자 라벨 함수
|
|
* @property {Function} getProgressPercent - 진행률 함수
|
|
* @property {Function} getProgressDescription - 진행 보조 설명 함수
|
|
* @property {Function} isRetrying - 재시도 중 여부 함수
|
|
* @property {Function} isDeleting - 삭제 중 여부 함수
|
|
* @property {Function} isDownloading - 다운로드 중 여부 함수
|
|
* @property {Function} areAllReadyFilesSelected - 전체 선택 여부 함수
|
|
* @property {Function} getReadyFiles - 준비 완료 파일 목록 함수
|
|
* @property {Function} getSelectedReadyFiles - 선택된 준비 완료 파일 목록 함수
|
|
* @property {Function} isFileSelected - 파일 선택 여부 함수
|
|
* @property {Function} isFileSelectionDisabled - 파일 선택 비활성 여부 함수
|
|
*/
|
|
defineProps({
|
|
jobs: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
formatDateTime: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getStatusLabel: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getStatusClass: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
shouldShowProgress: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getProgressLabel: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getProgressPercent: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getProgressDescription: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
isRetrying: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
isDeleting: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
isDownloading: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
areAllReadyFilesSelected: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getReadyFiles: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
getSelectedReadyFiles: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
isFileSelected: {
|
|
type: Function,
|
|
required: true
|
|
},
|
|
isFileSelectionDisabled: {
|
|
type: Function,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
defineEmits(['refresh', 'retry', 'delete', 'toggle-all-files', 'download-files', 'toggle-file'])
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
v-if="jobs.length > 0"
|
|
id="admin-settings-section-export-results"
|
|
class="admin-post-export-job-list admin-settings-screen__card admin-settings-screen__card--export-results rounded-xl border border-[#e6e8eb] bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,0.04)]"
|
|
>
|
|
<div class="admin-settings-screen__export-list">
|
|
<div class="mb-3 flex items-center justify-between gap-3">
|
|
<h3 class="text-sm font-semibold text-[#15171a]">
|
|
최근 내보내기 작업
|
|
</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="$emit('refresh')"
|
|
>
|
|
새로고침
|
|
</button>
|
|
</div>
|
|
<div class="admin-settings-screen__export-items grid gap-3">
|
|
<article
|
|
v-for="job in jobs"
|
|
: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
|
|
v-if="job.status !== 'ready'"
|
|
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold ring-1 ring-inset"
|
|
:class="getStatusClass(job.status)"
|
|
>
|
|
{{ getStatusLabel(job.status) }}
|
|
</span>
|
|
<span class="text-sm font-semibold text-[#15171a]">
|
|
게시물 {{ job.postCount }}개
|
|
</span>
|
|
<span class="text-sm text-[#9aa3ad]">
|
|
{{ job.files.length }}개 파일
|
|
</span>
|
|
<span class="text-sm text-[#657080]">
|
|
{{ job.rangeLabel || '전체' }}
|
|
</span>
|
|
</div>
|
|
<p class="mt-2 text-xs text-[#657080]">
|
|
만료: {{ formatDateTime(job.expiresAt) }}
|
|
</p>
|
|
</div>
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
<button
|
|
v-if="job.status === 'failed'"
|
|
class="inline-flex h-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-[#dce0e5] px-3 text-xs font-semibold text-[#15171a] transition hover:bg-[#f4f6f8] disabled:cursor-not-allowed disabled:text-[#a6b0bb]"
|
|
type="button"
|
|
:disabled="isRetrying(job.id)"
|
|
@click="$emit('retry', job)"
|
|
>
|
|
{{ isRetrying(job.id) ? '재시도 중' : '재시도' }}
|
|
</button>
|
|
<button
|
|
class="inline-flex h-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-[#ffd5d5] px-3 text-xs font-semibold text-[#d64545] transition hover:bg-[#fff3f3] disabled:cursor-not-allowed disabled:border-[#e1e5ea] disabled:text-[#a6b0bb]"
|
|
type="button"
|
|
:disabled="job.status === 'queued' || job.status === 'processing' || isDeleting(job.id)"
|
|
@click="$emit('delete', job)"
|
|
>
|
|
{{ isDeleting(job.id) ? '삭제 중' : '삭제' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="shouldShowProgress(job)"
|
|
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]">
|
|
진행도 {{ getProgressLabel(job) }}
|
|
</span>
|
|
<span class="text-[#657080]">
|
|
{{ getProgressPercent(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: `${getProgressPercent(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>{{ getProgressDescription(job) }}</span>
|
|
<span>마지막 갱신: {{ formatDateTime(job.updatedAt) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<details
|
|
v-if="job.status === 'failed' && job.errorDetail"
|
|
class="admin-settings-screen__export-error mt-4 rounded-lg border border-[#ffd5d5] bg-[#fff7f7] p-3 text-xs text-[#8f2d2d]"
|
|
>
|
|
<summary class="cursor-pointer font-semibold">
|
|
실패 원인 보기
|
|
</summary>
|
|
<pre class="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">{{ job.errorDetail }}</pre>
|
|
</details>
|
|
|
|
<div v-if="job.files.length > 0" class="admin-settings-screen__export-files mt-4 overflow-hidden rounded-md border border-[#edf0f3]">
|
|
<div class="flex flex-col gap-3 border-b border-[#edf0f3] bg-[#fbfcfd] px-3 py-3 md:flex-row md:items-center md:justify-between">
|
|
<label class="inline-flex cursor-pointer items-center gap-2 text-xs font-semibold text-[#394047] has-[:disabled]:cursor-not-allowed has-[:disabled]:text-[#a6b0bb]">
|
|
<input
|
|
class="size-4 rounded border-[#cfd6de] text-[#15171a] focus:ring-[#15171a] disabled:cursor-not-allowed"
|
|
type="checkbox"
|
|
:checked="areAllReadyFilesSelected(job)"
|
|
:disabled="getReadyFiles(job).length === 0 || isDownloading(job.id)"
|
|
@change="$emit('toggle-all-files', job)"
|
|
/>
|
|
전체 선택
|
|
</label>
|
|
<button
|
|
class="inline-flex h-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-[#15171a] bg-[#15171a] px-3 text-xs font-semibold text-white transition hover:bg-black disabled:cursor-not-allowed disabled:border-[#dce0e5] disabled:bg-white disabled:text-[#9aa3ad]"
|
|
type="button"
|
|
:disabled="getSelectedReadyFiles(job).length === 0 || isDownloading(job.id)"
|
|
@click="$emit('download-files', job)"
|
|
>
|
|
{{ isDownloading(job.id) ? '다운로드 중' : `선택 파일 다운로드 ${getSelectedReadyFiles(job).length || ''}` }}
|
|
</button>
|
|
</div>
|
|
<AdminPostExportFileRow
|
|
v-for="file in job.files"
|
|
:key="file.id"
|
|
:file="file"
|
|
:selected="isFileSelected(job, file)"
|
|
:disabled="isFileSelectionDisabled(job, file)"
|
|
@toggle="$emit('toggle-file', job, file)"
|
|
/>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|