You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
autoflow-web-console/src/components/templates/Datasets/ViewComponent.vue

275 lines
8.1 KiB

<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { ProjectService } from "@/components/service/project/projectService";
// ✅ 부모에서 넘겨준 id 수신 + 닫기 이벤트
const props = defineProps<{ id: number | string }>();
const emit = defineEmits<{ (e: "close"): void }>();
// -------- state --------
const loading = ref(false);
const detailRaw = ref<any | null>(null);
const experimentInfo = ref({
datasetTitle: "-",
projectName: "-",
version: "-",
createdDate: "-",
createdId: "-",
modifiedDate: "-",
modifiedId: "-",
description: "-",
fileName: "-",
fileSize: "-",
});
const downloadObjectName = computed(() => detailRaw.value?.storagePath || "");
const canDownload = computed(() => !!downloadObjectName.value);
async function handleDownload() {
const key = downloadObjectName.value.trim();
if (!key) return;
const res = await AttachmentsService.downloadFile(key);
const ct = String(res.headers["content-type"] || "").toLowerCase();
if (ct.includes("application/json")) {
const text = await (res.data as Blob).text();
try {
const json = JSON.parse(text);
throw new Error(json.message || text);
} catch {
throw new Error(text);
}
}
const cd = res.headers["content-disposition"] || "";
let filename: string | undefined;
const mUtf8 = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
const mStd = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i);
if (mUtf8?.[1]) {
try {
filename = decodeURIComponent(mUtf8[1].trim());
} catch {
filename = mUtf8[1].trim();
}
} else if (mStd) {
filename = (mStd[1] || mStd[2])?.trim();
}
// 2) 헤더가 없거나 못 읽으면: objectName의 basename으로 강제(fallback)
if (!filename) {
// key 예: "11fc4121-...-mlflow_pipeline.yaml" 또는 "dir/subdir/11fc...yaml"
const parts = key.split(/[\\/]/);
filename = parts[parts.length - 1] || "download.bin";
}
const blob = new Blob([res.data]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", filename);
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
// -------- utils --------
const formatIso = (s?: string) =>
s ? String(s).replace("T", " ").slice(0, 19) : "-";
const formatBytes = (b?: number) => {
const n = Number(b);
if (!Number.isFinite(n) || n < 0) return "-";
if (n < 1024) return `${n} B`;
const u = ["KB", "MB", "GB", "TB"];
let i = -1,
v = n;
do {
v /= 1024;
i++;
} while (v >= 1024 && i < u.length - 1);
return `${v.toFixed(1)} ${u[i]}`;
};
// 백엔드 응답 → 화면 모델
const mapToViewModel = (raw: any) => {
const projectName =
raw?.projectName ?? raw?.project?.name ?? raw?.prjNm ?? "-";
const size = raw?.fileSize ?? raw?.size ?? raw?.length ?? undefined;
return {
datasetTitle: raw?.title ?? "-",
projectName,
version: raw?.version ?? "-",
createdDate: formatIso(raw?.regDt),
createdId: raw?.regUserId ?? "-",
modifiedDate: formatIso(raw?.modDt),
modifiedId: raw?.modUserId ?? "-",
description: raw?.description ?? "-",
fileName: raw?.originalName ?? "-",
fileSize: formatBytes(size),
};
};
async function fetchProjectName(projectId?: number) {
if (!projectId && projectId !== 0) return;
try {
const res = await ProjectService.fetchProjectById(projectId as number);
const prj = res?.data ?? res;
experimentInfo.value.projectName = prj?.prjNm ?? prj?.name ?? "-";
} catch (e) {
console.warn("[Experiment/View] project fetch fail:", e);
}
}
const info = computed(() => mapToViewModel(detailRaw.value || {}));
async function fetchDetail(id: number | string) {
const idNum = typeof id === "string" ? Number(id) : id;
if (!Number.isFinite(idNum as number)) {
console.warn("[Datasets/View] invalid id:", id);
return;
}
loading.value = true;
try {
const res = await AttachmentsService.view(idNum as number);
detailRaw.value = res?.data ?? res;
experimentInfo.value = mapToViewModel(detailRaw.value);
await fetchProjectName(detailRaw.value?.projectId);
} catch (e) {
console.error("[Datasets/View] fetch detail error:", e);
} finally {
loading.value = false;
}
}
// -------- lifecycle --------
onMounted(() => {
fetchDetail(props.id);
});
watch(
() => props.id,
(nv) => {
if (nv !== undefined && nv !== null && nv !== "") fetchDetail(nv);
},
);
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div>View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-text class="px-6 pb-6 pt-4">
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Dataset Title
</v-col>
<v-col cols="3" class="pa-2">{{
experimentInfo.datasetTitle
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Version </v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.version }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Project Name
</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.projectName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Created Date
</v-col>
<v-col cols="3" class="pa-2">{{
experimentInfo.createdDate
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Created ID </v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.description
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">File</v-col>
<v-col cols="9" class="pa-2 d-flex align-center">
<span class="text-truncate">{{ experimentInfo.fileName }}</span>
<v-btn
icon
variant="text"
size="small"
class="ml-2"
:disabled="!canDownload"
@click="handleDownload"
aria-label="download"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>