feat: DataGroup 컴포넌트 추가, Executions Status 수정, 대시보드 데이터 바인딩

main
jschoi 9 months ago
parent 4686379184
commit 8114ca58c5

@ -1,3 +1,3 @@
NODE_ENV = "development" NODE_ENV = "development"
VITE_APP_API_SERVER_URL = "http://localhost:80" VITE_APP_API_SERVER_URL = "http://localhost:8080"
VITE_ROOT_PATH = "" VITE_ROOT_PATH = ""

4
components.d.ts vendored

@ -10,6 +10,7 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppFooter: typeof import('./src/components/AppFooter.vue')['default'] AppFooter: typeof import('./src/components/AppFooter.vue')['default']
CompareComponent: typeof import('./src/components/templates/run/executions/CompareComponent.vue')['default'] CompareComponent: typeof import('./src/components/templates/run/executions/CompareComponent.vue')['default']
DatagroupBaseDoalog: typeof import('./src/components/atoms/organisms/DatagroupBaseDoalog.vue')['default']
DatasetBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetBaseDoalog.vue')['default'] DatasetBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetBaseDoalog.vue')['default']
DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default'] DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default']
DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default'] DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default']
@ -26,7 +27,8 @@ declare module 'vue' {
IconRunBtn: typeof import('./src/components/atoms/button/IconRunBtn.vue')['default'] IconRunBtn: typeof import('./src/components/atoms/button/IconRunBtn.vue')['default']
IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.vue')['default'] IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.vue')['default']
LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default'] LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default']
ListComponent: typeof import('./src/components/templates/Datasets/ListComponent.vue')['default'] ListComponent: typeof import('./src/components/templates/datagroup/ListComponent.vue')['default']
ListComponentback: typeof import('./src/components/templates/run/executions/ListComponentback.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default'] SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default']

@ -0,0 +1,210 @@
<script setup lang="ts">
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { computed, onBeforeUnmount, onMounted, watch, ref } from "vue";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import { storage } from "@/utils/storage";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const { projectId } = storeToRefs(useAutoflowStore());
const props = defineProps<{ editData: any; mode: "create" | "edit" }>();
const emit = defineEmits<{
(e: "close-modal"): void;
(e: "saved", v: any): void;
}>();
const isEdit = computed(() => props.mode === "edit");
const saving = ref(false);
const errorMsg = ref("");
const form = ref({ name: "", description: "" });
//
function hydrateFormFromEdit(data: any) {
if (!data) return;
form.value.name = data.workflowName ?? data.dsNm ?? data.name ?? "";
form.value.description =
data.workflowDescription ?? data.dsDesc ?? data.description ?? "";
}
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
watch(
() => props.editData,
(v) => {
if (isEdit.value) hydrateFormFromEdit(v);
},
);
// /
function getAuthUser() {
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
const ui = authObj?.userInfo ?? authObj?.userinfo ?? authObj ?? {};
return { id: Number(ui.id), username: String(ui.username ?? "").trim() };
}
function cleanUndefined<T extends Record<string, any>>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined),
) as T;
}
async function submit() {
errorMsg.value = "";
const name = (form.value.name ?? "").trim(); // (/ OK)
if (!name) {
errorMsg.value = "이름을 입력하세요.";
return;
}
const { id: userId, username } = getAuthUser();
if (!userId || !username) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
if (!projectId.value) {
errorMsg.value = "프로젝트가 선택되지 않았습니다.";
return;
}
try {
saving.value = true;
if (isEdit.value) {
const rawId = props.editData?.id ?? props.editData?.deviceKey;
const id = Number(rawId);
if (!id) {
errorMsg.value = "수정할 ID가 없습니다.";
return;
}
const viewRes = await DataGroupService.view(id);
const current = (viewRes?.data ?? viewRes) || {};
const updatePayload = {
id,
dsNm: name,
dsDesc: form.value.description ?? "",
projectId: current.projectId,
regUserId: current.regUserId,
regUserNm: current.regUserNm,
modUserId: userId,
modUserNm: username,
};
console.log(id);
const { data } = await DataGroupService.update(id, updatePayload);
emit("saved", data);
emit("close-modal");
} else {
const createPayload = {
dsNm: name,
dsDesc: form.value.description ?? "",
regUserId: userId,
regUserNm: username,
projectId: projectId.value!,
};
const { data } = await DataGroupService.add(createPayload);
emit("saved", data);
emit("close-modal");
}
} catch (e: any) {
console.error("데이터그룹 저장 실패:", e);
const status = e?.response?.status;
const raw =
(typeof e?.response?.data === "string"
? e?.response?.data
: e?.response?.data?.message || e?.response?.data?.error) ||
e?.message ||
"";
if (status === 409)
errorMsg.value = "같은 이름의 데이터그룹이 이미 존재합니다.";
else if (status === 400) errorMsg.value = "요청 형식이 올바르지 않습니다.";
else if (status === 401 || status === 403)
errorMsg.value = "권한이 없거나 로그인 정보가 만료되었습니다.";
else errorMsg.value = raw || `요청 실패 (HTTP ${status ?? "Error"})`;
} finally {
saving.value = false;
}
}
function onEsc(e: KeyboardEvent) {
if (e.key === "Escape") emit("close-modal");
}
onMounted(() => window.addEventListener("keydown", onEsc));
onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</script>
<template>
<v-card>
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
{{ isEdit ? "Edit DataGroup" : "Create DataGroup" }}
</v-card-title>
<v-card-text class="pa-6">
<div class="text-subtitle-1 font-weight-medium mb-4">
DataGroup Information
</div>
<v-form @submit.prevent="submit">
<!-- Name: 제한 없음 -->
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>DataGroup Name</label
>
<v-text-field
v-model="form.name"
variant="outlined"
:disabled="saving"
dense
hide-details="auto"
required
/>
</div>
<!-- Description: 제한 없음 -->
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Description</label
>
<v-textarea
v-model="form.description"
variant="outlined"
:disabled="saving"
rows="3"
dense
hide-details="auto"
/>
</div>
<div v-if="errorMsg" class="mt-3 text-error">{{ errorMsg }}</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" :loading="saving" @click="submit">
{{ isEdit ? "Update" : "Save" }}
</v-btn>
<v-btn
text
class="white--text"
:disabled="saving"
@click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) {
form.value.description = (d?.description ?? "") + ""; form.value.description = (d?.description ?? "") + "";
} }
const getRefId = () => String(props.editData?.refId ?? "0");
onMounted(() => { onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData); if (isEdit.value) hydrateFormFromEdit(props.editData);
}); });
@ -39,7 +40,7 @@ watch(
); );
const dialogTitle = computed(() => const dialogTitle = computed(() =>
isEdit.value ? "Edit Training Script" : "Create Training Script", isEdit.value ? "Edit DataSet Script" : "Create DataSet Script",
); );
// //
@ -94,7 +95,7 @@ async function submit() {
return (errorMsg.value = "프로젝트가 선택되지 않았습니다."); return (errorMsg.value = "프로젝트가 선택되지 않았습니다.");
const fd = new FormData(); const fd = new FormData();
fd.append("refId", "0"); fd.append("refId", getRefId());
fd.append("refType", "DATASET"); fd.append("refType", "DATASET");
fd.append("title", title); fd.append("title", title);
fd.append("description", desc); fd.append("description", desc);

@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) {
form.value.description = (d?.description ?? "") + ""; form.value.description = (d?.description ?? "") + "";
} }
const getRefId = () => String(props.editData?.refId ?? "0");
onMounted(() => { onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData); if (isEdit.value) hydrateFormFromEdit(props.editData);
}); });
@ -59,7 +60,6 @@ const regUserId = (() => {
} }
})(); })();
//
async function submit() { async function submit() {
errorMsg.value = ""; errorMsg.value = "";
@ -95,7 +95,7 @@ async function submit() {
return (errorMsg.value = "프로젝트가 선택되지 않았습니다."); return (errorMsg.value = "프로젝트가 선택되지 않았습니다.");
const fd = new FormData(); const fd = new FormData();
fd.append("refId", "0"); fd.append("refId", getRefId());
fd.append("refType", "TRAINING_SCRIPT"); fd.append("refType", "TRAINING_SCRIPT");
fd.append("title", title); fd.append("title", title);
fd.append("description", desc); fd.append("description", desc);

@ -169,14 +169,10 @@ async function submit() {
const authObj = const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ?? (typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}"); JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
const regUserId =
authObj?.userInfo?.username ??
authObj?.userinfo?.username ??
authObj?.username ??
authObj?.userId ??
"";
if (!regUserId) { const ui = authObj?.userInfo ?? authObj?.userinfo ?? authObj ?? {};
const userId = Number(ui.id); // ID
if (!userId) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다."; errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return; return;
} }
@ -198,28 +194,21 @@ async function submit() {
errorMsg.value = "수정할 ID가 없습니다."; errorMsg.value = "수정할 ID가 없습니다.";
return; return;
} }
//
const viewRes = await WorkflowService.view(id); const viewRes = await WorkflowService.view(id);
const current = (viewRes?.data ?? viewRes) || {}; const current = (viewRes?.data ?? viewRes) || {};
// name/description , null
const updatePayload = cleanUndefined({ const updatePayload = cleanUndefined({
id, id,
name, // name,
description: form.value.description?.trim() || "", // description: form.value.description?.trim() || "",
// ===== =====
displayName: current.displayName, displayName: current.displayName,
namespace: current.namespace, namespace: current.namespace,
pipelineId: current.pipelineId, pipelineId: current.pipelineId,
kubeflowStatus: current.kubeflowStatus, kubeflowStatus: current.kubeflowStatus,
version: current.version, version: current.version,
regUserId: current.regUserId,
regUserId: current.regUserId ?? regUserId,
projectId: current.projectId ?? projectId.value,
regDt: current.regDt, regDt: current.regDt,
modDt: now, projectId: current.projectId ?? projectId.value,
}); });
const { data } = await WorkflowService.update(id, updatePayload); const { data } = await WorkflowService.update(id, updatePayload);
@ -236,7 +225,7 @@ async function submit() {
display_name: name, display_name: name,
description: form.value.description?.trim() || "", description: form.value.description?.trim() || "",
namespace: "default", namespace: "default",
regUserId, regUserId: userId,
projectId: projectId.value!, projectId: projectId.value!,
uploadfile: form.value.file, uploadfile: form.value.file,
}; };

@ -12,12 +12,9 @@ const router = useRouter();
const username = ref(""); const username = ref("");
const projectName = ref(localStorage.getItem("projectName") || ""); const projectName = ref(localStorage.getItem("projectName") || "");
// ---------------------- const isAdmin = ref(false);
// Admin + Admin const adminMode = ref(false);
// ---------------------- const lastNonAdminPath = ref("/home");
const isAdmin = ref(false); //
const adminMode = ref(false); //
const lastNonAdminPath = ref("/home"); //
function computeIsAdmin() { function computeIsAdmin() {
try { try {
@ -79,11 +76,19 @@ function updateUsername() {
const auth = storage.getAuth?.() ?? null; const auth = storage.getAuth?.() ?? null;
username.value = auth?.userInfo?.username ?? auth?.username ?? ""; username.value = auth?.userInfo?.username ?? auth?.username ?? "";
} }
function syncAdminModeWithRoute() {
const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
if (!isAdminRoute && adminMode.value) {
adminMode.value = false;
}
}
function refreshProjectName() { function refreshProjectName() {
const v = localStorage.getItem("projectName"); const v = localStorage.getItem("projectName");
projectName.value = v ? v : ""; projectName.value = v ? v : "";
} }
function goSelect() { function goSelect() {
adminMode.value = false;
router.push("/select"); router.push("/select");
} }
function logOut() { function logOut() {
@ -103,13 +108,17 @@ function logOut() {
// storage // storage
function onStorage(e) { function onStorage(e) {
if (!e.key || e.key === "projectName") refreshProjectName(); if (!e.key || e.key === "projectName") {
refreshProjectName();
adminMode.value = false;
}
if (!e.key || e.key === "autoflow-auth" || e.key === "auth") { if (!e.key || e.key === "autoflow-auth" || e.key === "auth") {
updateUsername(); updateUsername();
computeIsAdmin(); computeIsAdmin();
} }
} }
const goMain = () => { const goMain = () => {
adminMode.value = false;
router.push("/home"); router.push("/home");
}; };
@ -120,6 +129,7 @@ watch(
refreshProjectName(); refreshProjectName();
const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin); const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home"; if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home";
syncAdminModeWithRoute();
}, },
{ immediate: true }, { immediate: true },
); );
@ -130,6 +140,7 @@ onMounted(() => {
refreshProjectName(); refreshProjectName();
menu.value = menuItems; menu.value = menuItems;
window.addEventListener("storage", onStorage); window.addEventListener("storage", onStorage);
syncAdminModeWithRoute();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage); window.removeEventListener("storage", onStorage);

@ -3,13 +3,11 @@ export type KubeflowUploadDto = {
display_name?: string; display_name?: string;
description?: string; description?: string;
namespace?: string; namespace?: string;
regUserId: string; regUserId: string | number; // number도 허용
projectId: number | string; projectId: number | string;
uploadfile: File | Blob; uploadfile: File | Blob;
}; };
export type kubeflow = FormData;
export function toKubeflowForm(dto: KubeflowUploadDto): FormData { export function toKubeflowForm(dto: KubeflowUploadDto): FormData {
const fd = new FormData(); const fd = new FormData();
fd.append("name", dto.name); fd.append("name", dto.name);

@ -2,6 +2,7 @@ import {
DataGroup, DataGroup,
DataGroupSearch, DataGroupSearch,
} from "@/components/models/management/DataGroup"; } from "@/components/models/management/DataGroup";
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
export const DataGroupService = { export const DataGroupService = {
add: (payload: DataGroup) => { add: (payload: DataGroup) => {
@ -16,8 +17,8 @@ export const DataGroupService = {
view: (id: Number) => { view: (id: Number) => {
return request.get(`/api/datagroup/${id}`, {}); return request.get(`/api/datagroup/${id}`, {});
}, },
update: (id: number, payload: DataGroup) => { update: (id: number, updatePayload: DataGroup) => {
return request.put(`/api/datagroup/${id}`, payload); return request.put(`/api/datagroup/${id}`, updatePayload);
}, },
search: (payload: DataGroupSearch) => { search: (payload: DataGroupSearch) => {
return request.get("/api/datagroup/search", payload); return request.get("/api/datagroup/search", payload);

@ -1,4 +1,4 @@
import { kubeflow } from "@/components/models/management/Kubeflow"; import { Kubeflow } from "@/components/models/management/Kubeflow";
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
export const KubeflowService = { export const KubeflowService = {
upload: (payload: kubeflow) => { upload: (payload: kubeflow) => {

@ -2,13 +2,18 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { storage } from "@/utils/storage"; import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue"; import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue";
import DatasetBaseDoalog from "@/components/atoms/organisms/DatasetBaseDoalog.vue"; import DatasetBaseDoalog from "@/components/atoms/organisms/DatasetBaseDoalog.vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService"; import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { commonStore } from "@/stores/commonStore"; import { commonStore } from "@/stores/commonStore";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const activeRefId = ref<number | null>(null);
const activeRefName = computed(() => String(route.query.refName));
const store = commonStore(); const store = commonStore();
const openView = ref(false); const openView = ref(false);
const openModify = ref(false); const openModify = ref(false);
@ -69,6 +74,12 @@ const data = ref({
userOption: [] as any[], userOption: [] as any[],
}); });
function getRefIdFromRoute(q: any): number | null {
const raw = q?.refId;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
// //
function readUsernameFromStorage(): string { function readUsernameFromStorage(): string {
try { try {
@ -140,6 +151,7 @@ const fetchList = async () => {
sortField: "id", sortField: "id",
sortDirection: "DESC", sortDirection: "DESC",
refType: "DATASET", refType: "DATASET",
refId: activeRefId.value,
}; };
try { try {
@ -286,6 +298,7 @@ const openCreateModal = () => {
data.value.selectedData = { data.value.selectedData = {
username: username.value, username: username.value,
projectId: getProjectId(), projectId: getProjectId(),
refId: activeRefId.value,
}; };
data.value.isCreateVisible = true; data.value.isCreateVisible = true;
}; };
@ -318,8 +331,18 @@ const getSelectedAllData = () => {
}; };
onMounted(() => { onMounted(() => {
username.value = readUsernameFromStorage(); username.value = readUsernameFromStorage();
activeRefId.value = getRefIdFromRoute(route.query);
fetchList(); fetchList();
}); });
watch(
() => route.query.refId,
() => {
activeRefId.value = getRefIdFromRoute(route.query);
data.value.params.pageNum = 1;
fetchList();
},
);
</script> </script>
<template> <template>
@ -413,6 +436,26 @@ onMounted(() => {
/> />
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip
v-if="activeRefId"
class="ml-2"
color="secondary"
variant="tonal"
>
Filter: DataGroup {{ activeRefName }}
</v-chip>
<v-btn
v-if="activeRefId"
size="small"
variant="text"
@click="router.replace({ path: '/DataGroup' })"
>
필터 해제
</v-btn>
</v-sheet>
</v-sheet> </v-sheet>
<v-sheet class="justify-end mb-2"> <v-sheet class="justify-end mb-2">

@ -3,55 +3,80 @@ import { onMounted, ref, watch } from "vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone"; import tz from "dayjs/plugin/timezone";
import { useRouter } from "vue-router";
import { commonStore } from "@/stores/commonStore"; import { commonStore } from "@/stores/commonStore";
import { DataGroupService } from "@/components/service/management/DataGroupService"; import { DataGroupService } from "@/components/service/management/DataGroupService";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue"; import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
import WorkflowsRunDialog from "@/components/atoms/organisms/WorkflowsRunDialog.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconRunBtn from "@/components/atoms/button/IconRunBtn.vue"; import DatagroupBaseDoalog from "@/components/atoms/organisms/DatagroupBaseDoalog.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
/* -------------------------
* Dayjs & Router
* ------------------------*/
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(tz); dayjs.extend(tz);
const KST = "Asia/Seoul"; const KST = "Asia/Seoul";
const store = commonStore(); const router = useRouter();
const openView = ref(false);
type SearchType = "전체" | "제목" | "작성자";
const isRunVisible = ref(false);
const selectedRun = ref<any | null>(null);
const tableHeader = [ /* -------------------------
{ label: "No", width: "5%", style: "word-break: keep-all;" }, * Types
{ label: "Workflow Name", width: "18%", style: "word-break: keep-all;" }, * ------------------------*/
{ label: "Description", width: "28%", style: "word-break: keep-all;" }, type SearchType = "전체" | "제목" | "작성자";
{ label: "Version", width: "10%", style: "word-break: keep-all;" }, type ApiSearchType = "ALL" | "TITLE" | "AUTHOR";
{ label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "15%", style: "word-break: keep-all;" }, interface DataGroupRow {
{ label: "Action", width: "12%", style: "word-break: keep-all;" }, no: number;
]; name: string;
const searchOptions = [ description: string;
author: string;
registDt: string | number | Date;
deviceKey: number;
}
/* -------------------------
* Constants
* ------------------------*/
const TABLE_HEADERS = [
{ label: "No", width: "6%", style: "word-break: keep-all;" },
{ label: "DataGroup Name", width: "24%", style: "word-break: keep-all;" },
{ label: "Description", width: "32%", style: "word-break: keep-all;" },
{ label: "Author", width: "14%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "16%", style: "word-break: keep-all;" },
{ label: "Action", width: "8%", style: "word-break: keep-all;" },
] as const;
const SEARCH_OPTIONS = [
{ label: "전체", value: "전체" as SearchType }, { label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType }, { label: "제목", value: "제목" as SearchType },
{ label: "사용자", value: "작성자" as SearchType }, { label: "작성자", value: "작성자" as SearchType },
]; ];
const pageSizeOptions = [ const PAGE_SIZE_OPTIONS = [
{ text: "10 페이지", value: 10 }, { text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 }, { text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 }, { text: "100 페이지", value: 100 },
]; ];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = { const SEARCH_TYPE_MAP: Record<SearchType | "", ApiSearchType> = {
"": "ALL", "": "ALL",
전체: "ALL", 전체: "ALL",
제목: "TITLE", 제목: "TITLE",
작성자: "AUTHOR", 작성자: "AUTHOR",
}; };
/* -------------------------
* Store & Local State
* ------------------------*/
const store = commonStore();
const openView = ref(false);
const isRunVisible = ref(false); // ( )
const selectedRun = ref<any | null>(null);
const data = ref({ const data = ref({
params: { params: {
pageNum: 1, pageNum: 1,
@ -59,7 +84,7 @@ const data = ref({
searchType: "전체" as SearchType, searchType: "전체" as SearchType,
searchText: "", searchText: "",
}, },
results: [] as any[], results: [] as DataGroupRow[],
totalElements: 0, totalElements: 0,
pageLength: 0, pageLength: 0,
modalMode: "" as "create" | "edit" | "upload" | "", modalMode: "" as "create" | "edit" | "upload" | "",
@ -73,137 +98,144 @@ const data = ref({
userOption: [] as any[], userOption: [] as any[],
}); });
/* -------------------------
* Utils
* ------------------------*/
const formatDateTime = ( const formatDateTime = (
v?: string | number | Date, v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss", fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : ""); ) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const toRow = (w: any, no: number) => ({ const toRow = (g: any, no: number): DataGroupRow => ({
no, no,
name: w.name, name: g.dsNm,
description: w.description, description: g.dsDesc,
version: w.version, author: g.regUserNm,
kubeflowStatus: w.kubeflowStatus, registDt: g.regDate,
registDt: w.regDt, deviceKey: g.id,
deviceKey: w.id,
pipelineId: w.pipelineId ?? w.pipeline_id ?? "",
}); });
const fetchList = () => { function onRowClick(row: DataGroupRow) {
const id = Number(row?.deviceKey);
if (!Number.isFinite(id)) return;
router.push({
path: "/datasets",
query: { refId: String(id), refName: row.name },
});
}
/* -------------------------
* Data Loaders
* ------------------------*/
async function fetchList() {
const projectId = Number(localStorage.getItem("projectId")); const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) { if (!projectId) {
console.warn("[Workflows] projectId 없음 — 프로젝트 먼저 선택"); console.warn("[DataGroup] projectId 없음 — 프로젝트 먼저 선택");
data.value.results = []; data.value.results = [];
data.value.totalElements = 0; data.value.totalElements = 0;
data.value.pageLength = 0; data.value.pageLength = 0;
return; return;
} }
const { pageNum, searchText, searchType } = data.value.params; const { pageNum, pageSize, searchType, searchText } = data.value.params;
//
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL"; const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
const keyword = (searchText || "").trim(); const keyword = (searchText || "").trim();
const needLocalFilter = mapped !== "ALL" && keyword.length > 0; const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
let reqPage = data.value.params.pageNum; // ( )
let reqSize = data.value.params.pageSize; const reqPage = needLocalFilter ? 0 : pageNum - 1; // 0-based
if (needLocalFilter) { const reqSize = needLocalFilter ? 1000 : pageSize;
reqPage = 0;
reqSize = 1000; try {
} const payload = {
const payload = { projectId,
projectId, page: reqPage,
page: reqPage, size: reqSize,
size: reqSize, keyword,
keyword, searchType: mapped,
searchType: mapped, };
}; const res: any = await DataGroupService.search(payload);
if (res?.status !== 200) return;
DataGroupService.search(payload)
.then((res: any) => { const result = res.data;
if (res.status !== 200) return; let list: any[] = result?.content ?? [];
const result = res.data; //
let list = result?.content ?? []; if (needLocalFilter) {
const kw = keyword.toLowerCase();
if (needLocalFilter) { if (mapped === "TITLE") {
const kw = keyword.toLowerCase(); list = list.filter((w: any) =>
String(w?.name ?? "")
if (mapped === "TITLE") { .toLowerCase()
list = list.filter((w: any) => .includes(kw),
String(w?.name ?? "") );
.toLowerCase() } else if (mapped === "AUTHOR") {
.includes(kw), list = list.filter((w: any) =>
); String(w?.regUserNm ?? "")
} else if (mapped === "AUTHOR") { .toLowerCase()
list = list.filter((w: any) => .includes(kw),
String(w?.regUserId ?? "")
.toLowerCase()
.includes(kw),
);
}
const uiSize = data.value.params.pageSize;
const totalElements = list.length;
const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const safePage = Math.min(Math.max(1, pageNum), totalPages);
const start = (safePage - 1) * uiSize;
const pageSlice = list.slice(start, start + uiSize);
// ()
const firstNo = totalElements - start;
data.value.results = pageSlice.map((w: any, i: number) =>
toRow(w, firstNo - i),
); );
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
return;
} }
const totalElements = result.totalElements; const uiSize = pageSize;
const totalPages = result.totalPages; const totalElements = list.length;
const serverPage = result.pageable.pageNumber; const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const serverSize = result.pageable.pageSize; const safePage = Math.min(Math.max(1, pageNum), totalPages);
const offset = const start = (safePage - 1) * uiSize;
typeof result.pageable.offset === "number" const pageSlice = list.slice(start, start + uiSize);
? result.pageable.offset const firstNo = totalElements - start;
: serverPage * serverSize;
const firstNo = totalElements - offset; data.value.results = pageSlice.map((w: any, i: number) =>
data.value.results = list.map((w: any, i: number) =>
toRow(w, firstNo - i), toRow(w, firstNo - i),
); );
data.value.totalElements = totalElements; data.value.totalElements = totalElements;
data.value.pageLength = totalPages; data.value.pageLength = totalPages;
}) return;
.catch((err: any) => console.error("워크플로우 조회 에러:", err)); }
};
const doSearch = () => { //
const totalElements = result.totalElements ?? list.length;
const totalPages = result.totalPages ?? 1;
const serverPage = result.pageable?.pageNumber ?? 0;
const serverSize = result.pageable?.pageSize ?? pageSize;
const offset =
typeof result.pageable?.offset === "number"
? result.pageable.offset
: serverPage * serverSize;
const firstNo = totalElements - offset;
data.value.results = list.map((w: any, i: number) => toRow(w, firstNo - i));
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
} catch (err) {
console.error("[DataGroup] 조회 에러:", err);
}
}
/* -------------------------
* Actions
* ------------------------*/
function doSearch() {
data.value.params.pageNum = 1; data.value.params.pageNum = 1;
fetchList(); fetchList();
}; }
function changePageNum(page: number) {
const changePageNum = (page: number) => {
data.value.params.pageNum = page; data.value.params.pageNum = page;
fetchList(); fetchList();
}; }
function changePageSize(size: number) {
const changePageSize = (size: number) => {
data.value.params.pageSize = size; data.value.params.pageSize = size;
data.value.params.pageNum = 1; data.value.params.pageNum = 1;
fetchList(); fetchList();
}; }
const removeData = (value?: Array<{ deviceKey: number }>) => { function removeData(value?: Array<{ deviceKey: number }>) {
const removeList = value ?? data.value.selected; const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return; if (!removeList?.length) return;
const ids = removeList.map((x) => x.deviceKey); const ids = removeList.map((x) => x.deviceKey);
const remove = (id: number) => const removeOnce = (id: number) =>
DataGroupService.delete(id).then((res) => { DataGroupService.delete(id).then((res: any) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res); if (res.status < 200 || res.status >= 300) return Promise.reject(res);
}); });
@ -219,76 +251,73 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
data.value.selected = []; data.value.selected = [];
data.value.allSelected = false; data.value.allSelected = false;
}; };
console.log(ids.length);
if (ids.length === 1) { if (ids.length === 1) {
remove(ids[0]) removeOnce(ids[0])
.then(() => { .then(() =>
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "success", color: "success",
text: "삭제되었습니다.", text: "삭제되었습니다.",
result: 200, result: 200,
}); }),
after(); )
})
.catch((err) => { .catch((err) => {
console.error(err);
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "warning", color: "warning",
text: "삭제 실패", text: "삭제 실패",
result: 500, result: 500,
}); });
console.error(err); })
}); .finally(after);
} else { } else {
Promise.all(ids.map(remove)) Promise.all(ids.map(removeOnce))
.then(() => { .then(() =>
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "success", color: "success",
text: "모두 삭제되었습니다.", text: "모두 삭제되었습니다.",
result: 200, result: 200,
}); }),
}) )
.catch((err) => { .catch((err) => {
console.error(err);
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "warning", color: "warning",
text: "일부 삭제 실패", text: "일부 삭제 실패",
result: 500, result: 500,
}); });
console.error(err);
}) })
.finally(after); .finally(after);
} }
}; }
const handleRemoveData = () => { function handleRemoveData() {
if (data.value.selected.length === 0) return; if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) { if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true; data.value.isConfirmDialogVisible = true;
return; return;
} }
removeData(); removeData();
}; }
const getSelectedAllData = () => { function getSelectedAllData() {
data.value.selected = data.value.allSelected data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey })) ? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: []; : [];
}; }
const openDetailModal = (selectedItem: any) => { function openDetailModal(selectedItem: DataGroupRow) {
data.value.selectedData = selectedItem; data.value.selectedData = selectedItem;
openView.value = true; openView.value = true;
}; }
const closeDetail = () => (openView.value = false); const closeDetail = () => (openView.value = false);
const closeRunModal = () => (isRunVisible.value = false);
const openRunModal = (item: any) => { function openRunModal(item: any) {
selectedRun.value = item; selectedRun.value = item;
isRunVisible.value = true; isRunVisible.value = true;
}; }
const openModifyModal = (item: any) => { function openModifyModal(item: DataGroupRow) {
data.value.selectedData = { data.value.selectedData = {
id: item.deviceKey, id: item.deviceKey,
workflowName: item.name, workflowName: item.name,
@ -296,47 +325,42 @@ const openModifyModal = (item: any) => {
}; };
data.value.modalMode = "edit"; data.value.modalMode = "edit";
data.value.isCreateVisible = true; data.value.isCreateVisible = true;
}; }
function openCreateModal() {
const openCreateModal = () => {
data.value.selectedData = null; data.value.selectedData = null;
data.value.modalMode = "create"; data.value.modalMode = "create";
data.value.isCreateVisible = true; data.value.isCreateVisible = true;
}; }
function openUploadModal() {
const openUploadModal = () => {
data.value.selectedData = null; data.value.selectedData = null;
data.value.modalMode = "upload"; data.value.modalMode = "upload";
data.value.isUploadVisible = true; data.value.isUploadVisible = true;
}; }
function closeCreateModal() {
const closeCreateModal = () => {
data.value.isModalVisible = false; data.value.isModalVisible = false;
data.value.isCreateVisible = false; data.value.isCreateVisible = false;
}; }
function closeUploadModal() {
const closeUploadModal = () => {
data.value.isModalVisible = false; data.value.isModalVisible = false;
data.value.isUploadVisible = false; data.value.isUploadVisible = false;
}; }
/* -------------------------
* Watchers & Lifecycle
* ------------------------*/
watch( watch(
() => data.value.isCreateVisible, () => data.value.isCreateVisible,
(now, prev) => { (now, prev) => {
if (prev && !now) fetchList(); if (prev && !now) fetchList();
}, },
); );
watch( watch(
() => data.value.isUploadVisible, () => data.value.isUploadVisible,
(now, prev) => { (now, prev) => {
if (prev && !now) fetchList(); if (prev && !now) fetchList();
}, },
); );
onMounted(fetchList);
onMounted(() => {
fetchList();
});
</script> </script>
<template> <template>
@ -353,6 +377,8 @@ onMounted(() => {
</div> </div>
</v-card-item> </v-card-item>
</v-card> </v-card>
<!-- Search -->
<v-card flat class="bg-shades-transparent w-100"> <v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4"> <v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center"> <div class="d-flex justify-center flex-wrap align-center">
@ -365,13 +391,14 @@ onMounted(() => {
v-model="data.params.searchType" v-model="data.params.searchType"
label="검색조건" label="검색조건"
density="compact" density="compact"
:items="searchOptions" :items="SEARCH_OPTIONS"
item-title="label" item-title="label"
item-value="value" item-value="value"
hide-details hide-details
@update:model-value="doSearch" @update:model-value="doSearch"
/> />
</v-responsive> </v-responsive>
<v-responsive min-width="540" max-width="540"> <v-responsive min-width="540" max-width="540">
<v-text-field <v-text-field
v-model="data.params.searchText" v-model="data.params.searchText"
@ -382,7 +409,7 @@ onMounted(() => {
class="mt-3 mb-3" class="mt-3 mb-3"
hide-details hide-details
@keyup.enter="doSearch" @keyup.enter="doSearch"
></v-text-field> />
</v-responsive> </v-responsive>
<div class="ml-3"> <div class="ml-3">
@ -392,12 +419,13 @@ onMounted(() => {
:rounded="5" :rounded="5"
@click="doSearch" @click="doSearch"
> >
<v-icon> mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
</div> </div>
</div> </div>
</v-card> </v-card>
<!-- Toolbar -->
<v-sheet <v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2" class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
> >
@ -414,27 +442,25 @@ onMounted(() => {
<v-select <v-select
v-model="data.params.pageSize" v-model="data.params.pageSize"
density="compact" density="compact"
:items="pageSizeOptions" :items="PAGE_SIZE_OPTIONS"
item-title="text" item-title="text"
item-value="value" item-value="value"
variant="outlined" variant="outlined"
color="primary" color="primary"
hide-details hide-details
@update:model-value="changePageSize" @update:model-value="changePageSize"
></v-select> />
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
</v-sheet> </v-sheet>
<v-sheet class="justify-end mb-2"> <v-sheet class="justify-end mb-2">
<!-- <v-btn color="info" class="mr-4" @click="openUploadModal"
>Upload Workflow
</v-btn> -->
<v-btn color="info" @click="openCreateModal" <v-btn color="info" @click="openCreateModal"
>Create Workflow >Create DataGroup</v-btn
</v-btn> >
</v-sheet> </v-sheet>
</v-sheet> </v-sheet>
<!-- Table -->
<v-card class="rounded-lg pa-8"> <v-card class="rounded-lg pa-8">
<v-col cols="12"> <v-col cols="12">
<v-sheet> <v-sheet>
@ -447,69 +473,52 @@ onMounted(() => {
overflow-x-auto overflow-x-auto
> >
<colgroup> <colgroup>
<col style="width: 5%" />
<col <col
v-for="(item, i) in tableHeader" v-for="(h, i) in TABLE_HEADERS"
:key="i" :key="i"
:style="`width:${item.width}`" :style="`width:${h.width}`"
/> />
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th <th
v-for="(item, i) in tableHeader" v-for="(h, i) in TABLE_HEADERS"
:key="i" :key="i"
class="text-center font-weight-bold" class="text-center font-weight-bold"
:style="`${item.style}`" :style="h.style"
> >
{{ item.label }} {{ h.label }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="text-body-2"> <tbody class="text-body-2">
<tr <tr
v-for="(item, i) in data.results" v-for="(row, i) in data.results"
:key="i" :key="i"
class="text-center" class="text-center row-hover"
@click="onRowClick(row)"
> >
<td> <td>{{ row.no }}</td>
<v-checkbox <td>{{ row.name }}</td>
v-model="data.selected" <td>{{ row.description }}</td>
hide-details <td>{{ row.author || "-" }}</td>
:value="{ <td>{{ formatDateTime(row.registDt) }}</td>
deviceKey: item.deviceKey, <td
}" style="white-space: nowrap"
></v-checkbox> @click.stop
</td> @mousedown.stop
<td>{{ item.no }}</td> >
<td>{{ item.name }}</td> <IconInfoBtn @on-click="openDetailModal(row)" />
<td>{{ item.description }}</td> <IconModifyBtn @on-click="openModifyModal(row)" />
<td>{{ item.version }}</td>
<td>{{ item.kubeflowStatus }}</td>
<td>{{ formatDateTime(item.registDt) }}</td>
<td style="white-space: nowrap">
<IconRunBtn @on-click="openRunModal(item)" />
<IconInfoBtn @on-click="openDetailModal(item)" />
<!-- <IconModifyBtn @on-click="openModifyModal(item)" /> -->
<IconDeleteBtn <IconDeleteBtn
@on-click=" @on-click="removeData([{ deviceKey: row.deviceKey }])"
removeData([{ deviceKey: item.deviceKey }])
"
/> />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>
</v-sheet> </v-sheet>
<v-card-actions class="text-center mt-8 justify-center"> <v-card-actions class="text-center mt-8 justify-center">
<v-pagination <v-pagination
v-model="data.params.pageNum" v-model="data.params.pageNum"
@ -518,36 +527,23 @@ onMounted(() => {
color="primary" color="primary"
rounded="circle" rounded="circle"
@update:model-value="changePageNum" @update:model-value="changePageNum"
></v-pagination> />
</v-card-actions> </v-card-actions>
</v-col> </v-col>
</v-card> </v-card>
</v-card> </v-card>
</v-card> </v-card>
</v-container> </v-container>
<!-- Create/Edit Dialog -->
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent> <v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
<WorkflowsBaseDialog <DatagroupBaseDoalog
:key="data.modalMode + String(data.selectedData?.deviceKey ?? '')" :key="data.modalMode + String(data.selectedData?.deviceKey ?? '')"
:edit-data="data.selectedData" :edit-data="data.selectedData"
:mode="data.modalMode" :mode="data.modalMode"
@close-modal="closeCreateModal" @close-modal="closeCreateModal"
/> />
</v-dialog> </v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
<WorkflowsUploadDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeUploadModal"
/>
</v-dialog>
<v-dialog v-model="isRunVisible" max-width="600" persistent>
<WorkflowsRunDialog
:pipeline-id="selectedRun?.pipelineId"
:display-name="`Run of ${selectedRun?.name ?? 'Pipeline'} (${new Date().toLocaleString()})`"
:description="selectedRun?.description || ''"
@close-modal="closeRunModal"
/>
</v-dialog>
</div> </div>
<div class="w-100" v-else> <div class="w-100" v-else>
@ -559,4 +555,16 @@ onMounted(() => {
</div> </div>
</template> </template>
<style scoped></style> <style scoped>
/* 행 Hover 효과 */
.row-hover {
cursor: pointer;
transition: background-color 120ms ease;
}
tbody tr.row-hover:hover {
background-color: rgba(255, 255, 255, 0.06);
}
tbody tr.row-hover:active {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

File diff suppressed because it is too large Load Diff

@ -11,19 +11,11 @@ import { KubeflowRunService } from "@/components/service/management/KubeflowRunS
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue"; import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
const store = commonStore(); const store = commonStore();
const openView = ref(false); const openView = ref(false);
const username = ref<string>("");
const openCompare = ref(false);
const execSelected = ref<any>(null); const execSelected = ref<any>(null);
const selectedExperiment = ref<{ const username = ref<string>("");
name: string;
description: string;
createdDate: string;
createdID: string;
deviceKey: number;
} | null>(null);
// ===== =====
const tableHeader = [ const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" }, { label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Execution Name", width: "20%", style: "word-break: keep-all;" }, { label: "Execution Name", width: "20%", style: "word-break: keep-all;" },
@ -35,15 +27,13 @@ const tableHeader = [
{ label: "Registry Status", width: "10%", style: "word-break: keep-all;" }, { label: "Registry Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Action", width: "10%", style: "word-break: keep-all;" }, { label: "Action", width: "10%", style: "word-break: keep-all;" },
]; ];
// ===== / (/ '') =====
type SearchType = "전체" | "제목" | "작성자";
type SearchType = "전체" | "제목" | "작성자";
const searchOptions = [ const searchOptions = [
{ label: "전체", value: "전체" as SearchType }, { label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType }, { label: "제목", value: "제목" as SearchType },
{ label: "작성자", value: "작성자" as SearchType }, { label: "작성자", value: "작성자" as SearchType },
]; ];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = { const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
"": "ALL", "": "ALL",
전체: "ALL", 전체: "ALL",
@ -57,7 +47,6 @@ const pageSizeOptions = [
{ text: "100 페이지", value: 100 }, { text: "100 페이지", value: 100 },
]; ];
// ===== =====
const data = ref({ const data = ref({
params: { params: {
pageNum: 1, pageNum: 1,
@ -70,15 +59,9 @@ const data = ref({
pageLength: 0, pageLength: 0,
modalMode: "" as "create" | "edit" | "", modalMode: "" as "create" | "edit" | "",
selectedData: null as any, selectedData: null as any,
allSelected: false,
selected: [] as any[],
isCreateVisible: false, isCreateVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [] as any[],
}); });
// ===== =====
function readUsernameFromStorage(): string { function readUsernameFromStorage(): string {
try { try {
const raw = const raw =
@ -100,11 +83,7 @@ const getProjectId = (): number => {
const v = Number(localStorage.getItem("projectId")); const v = Number(localStorage.getItem("projectId"));
return Number.isFinite(v) ? v : 0; return Number.isFinite(v) ? v : 0;
}; };
const fmtDate = (v?: string) =>
v ? String(v).replace("T", " ").slice(0, 19) : "";
// Row
// Execution Row
const toRow = (r: any, idx: number) => { const toRow = (r: any, idx: number) => {
const fmtStart = (start?: string) => { const fmtStart = (start?: string) => {
if (!start) return "-"; if (!start) return "-";
@ -155,7 +134,6 @@ const toRow = (r: any, idx: number) => {
status: toUiStatus(r.state), status: toUiStatus(r.state),
duration: fmtDuration(r.createdAt, r.finishedAt), duration: fmtDuration(r.createdAt, r.finishedAt),
experiment: r.experimentId ?? "-", experiment: r.experimentId ?? "-",
workflow: r.pipelineId ?? r.pipelineVersionId ?? "-", workflow: r.pipelineId ?? r.pipelineVersionId ?? "-",
startTime: fmtStart(r.createdAt), startTime: fmtStart(r.createdAt),
registryStatus: r.storageState ?? "-", registryStatus: r.storageState ?? "-",
@ -164,95 +142,6 @@ const toRow = (r: any, idx: number) => {
}; };
}; };
// async function fetchList() {
// const { pageNum, pageSize, searchType, searchText } = data.value.params;
// const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
// const keyword = (searchText || "").trim();
// const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
// // (: 1000) +
// // 0-based
// const reqPage = needLocalFilter ? 0 : pageNum ;
// const reqSize = needLocalFilter ? 1000 : pageSize;
// const payload = {
// projectId: getProjectId(),
// page: reqPage, // : reqPage
// size: reqSize, // : reqSize
// keyword,
// searchType: mapped,
// sortField: "id",
// sortDirection: "DESC",
// };
// try {
// const res = await kubeflowRunService.search(payload as any);
// const result = res?.data ?? res;
// // : content | data | runs | []
// let list: any[] = Array.isArray(result)
// ? result
// : Array.isArray(result?.data)
// ? result.data
// : Array.isArray(result?.content)
// ? result.content
// : Array.isArray(result?.runs)
// ? result.runs
// : [];
// if (needLocalFilter) {
// const kw = keyword.toLowerCase();
// if (mapped === "TITLE") {
// list = list.filter((r: any) =>
// String(r?.displayName ?? r?.name ?? r?.runId ?? "")
// .toLowerCase()
// .includes(kw),
// );
// } else if (mapped === "AUTHOR") {
// list = list.filter((r: any) =>
// String(r?.regUserId ?? r?.createdBy ?? r?.serviceAccount ?? "")
// .toLowerCase()
// .includes(kw),
// );
// }
// // ( )
// const total = list.length;
// const pages = Math.max(1, Math.ceil(total / pageSize));
// const safePage = Math.min(Math.max(1, pageNum), pages);
// const start = (safePage - 1) * pageSize;
// const slice = list.slice(start, start + pageSize);
// data.value.params.pageNum = safePage;
// data.value.results = slice.map((r, i) => toRow(r, i));
// data.value.totalElements = total;
// data.value.pageLength = pages;
// return;
// }
// //
// const totalElements =
// typeof result?.totalElements === "number"
// ? result.totalElements
// : list.length;
// const totalPages =
// typeof result?.totalPages === "number"
// ? Math.max(1, result.totalPages)
// : Math.max(1, Math.ceil(totalElements / pageSize));
// data.value.results = list.map((r, i) => toRow(r, i));
// data.value.totalElements = totalElements;
// data.value.pageLength = totalPages;
// } catch (err) {
// console.error("[Executions] :", err);
// data.value.results = [];
// data.value.totalElements = 0;
// data.value.pageLength = 1;
// }
// }
async function fetchList() { async function fetchList() {
const { pageNum, pageSize, searchType, searchText } = data.value.params; const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL"; const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
@ -264,7 +153,7 @@ async function fetchList() {
size: pageSize, size: pageSize,
keyword, keyword,
searchType: mapped, searchType: mapped,
sortField: "id", sortField: "createdAt",
sortDirection: "DESC", sortDirection: "DESC",
}; };
@ -272,29 +161,44 @@ async function fetchList() {
const res = await KubeflowRunService.search(payload as any); const res = await KubeflowRunService.search(payload as any);
const result = res?.data ?? res; const result = res?.data ?? res;
// 1)
let list: any[] = []; let list: any[] = [];
let totalElements: number | undefined; let totalElements: number | undefined;
let totalPages: number | undefined; let totalPages: number | undefined;
let isServerPaged = false; let isServerPaged = false;
if (Array.isArray(result)) { if (Array.isArray(result)) list = result;
// else if (Array.isArray(result?.data)) list = result.data;
list = result; else if (Array.isArray(result?.content)) {
} else if (Array.isArray(result?.data)) {
// data
list = result.data;
} else if (Array.isArray(result?.content)) {
// (Page)
list = result.content; list = result.content;
totalElements = result.totalElements; totalElements = result.totalElements;
totalPages = result.totalPages; totalPages = result.totalPages;
isServerPaged = true; isServerPaged = true;
} else if (Array.isArray(result?.runs)) { } else if (Array.isArray(result?.runs)) list = result.runs;
list = result.runs;
} else { // ( )
list = []; list.sort((a, b) => {
} const ta = new Date(
a.createdAt ||
a.lastUpdateTime ||
a.scheduledAt ||
a.finishedAt ||
a.startTime ||
0,
).getTime();
const tb = new Date(
b.createdAt ||
b.lastUpdateTime ||
b.scheduledAt ||
b.finishedAt ||
b.startTime ||
0,
).getTime();
if (tb !== ta) return tb - ta; //
const aid = a.id ?? a.runId ?? a.run_id ?? a.name ?? "";
const bid = b.id ?? b.runId ?? b.run_id ?? b.name ?? "";
return String(bid).localeCompare(String(aid)); //
});
if (!isServerPaged) { if (!isServerPaged) {
const total = list.length; const total = list.length;
@ -307,8 +211,7 @@ async function fetchList() {
data.value.totalElements = total; data.value.totalElements = total;
data.value.pageLength = pages; data.value.pageLength = pages;
} else { } else {
// 3) data.value.results = list.map((r, i) => toRow(r, i));
data.value.results = (list as any[]).map((r, i) => toRow(r, i));
data.value.totalElements = data.value.totalElements =
typeof totalElements === "number" ? totalElements : list.length; typeof totalElements === "number" ? totalElements : list.length;
data.value.pageLength = data.value.pageLength =
@ -323,7 +226,8 @@ async function fetchList() {
data.value.pageLength = 1; data.value.pageLength = 1;
} }
} }
// ===== / =====
// /
const doSearch = () => { const doSearch = () => {
data.value.params.pageNum = 1; data.value.params.pageNum = 1;
fetchList(); fetchList();
@ -338,12 +242,11 @@ const changePageSize = (size: number) => {
fetchList(); fetchList();
}; };
// / ( ) //
const removeData = (value?: Array<{ deviceKey: number }>) => { const removeData = (value: Array<{ deviceKey: number }>) => {
const removeList = value ?? data.value.selected; const ids = (value || []).map((x) => x.deviceKey);
if (!removeList || removeList.length === 0) return; if (ids.length === 0) return;
const ids = removeList.map((x) => x.deviceKey);
const removeOne = (id: number) => const removeOne = (id: number) =>
ExperimentService.delete(id).then((res) => { ExperimentService.delete(id).then((res) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res); if (res.status < 200 || res.status >= 300) return Promise.reject(res);
@ -353,27 +256,20 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
if ( if (
ids.length >= data.value.results.length && ids.length >= data.value.results.length &&
data.value.params.pageNum > 1 data.value.params.pageNum > 1
) { )
data.value.params.pageNum -= 1; data.value.params.pageNum -= 1;
}
fetchList(); fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
}; };
// /
if (ids.length === 1) { if (ids.length === 1) {
removeOne(ids[0]) removeOne(ids[0])
.then(() => { .then(() =>
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "success", color: "success",
text: "삭제되었습니다.", text: "삭제되었습니다.",
result: 200, result: 200,
}); }),
after(); )
})
.catch((err) => { .catch((err) => {
console.error("삭제 실패:", err); console.error("삭제 실패:", err);
store.setSnackbarMsg({ store.setSnackbarMsg({
@ -381,16 +277,17 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
text: "삭제 실패", text: "삭제 실패",
result: 500, result: 500,
}); });
}); })
.finally(after);
} else { } else {
Promise.all(ids.map(removeOne)) Promise.all(ids.map(removeOne))
.then(() => { .then(() =>
store.setSnackbarMsg({ store.setSnackbarMsg({
color: "success", color: "success",
text: "모두 삭제되었습니다.", text: "모두 삭제되었습니다.",
result: 200, result: 200,
}); }),
}) )
.catch((err) => { .catch((err) => {
console.error("일부 삭제 실패:", err); console.error("일부 삭제 실패:", err);
store.setSnackbarMsg({ store.setSnackbarMsg({
@ -403,29 +300,15 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
} }
}; };
// ===== & ( ) ===== //
const closeDetail = () => {
openView.value = false;
selectedExperiment.value = null;
};
const openInfoModal = (item: any) => { const openInfoModal = (item: any) => {
execSelected.value = item; execSelected.value = item;
console.log("[Parent] 선택된 실행:", item);
openView.value = true; openView.value = true;
openCompare.value = false;
}; };
function closeView() { function closeView() {
openView.value = false; openView.value = false;
} }
const onSaved = () => fetchList(); const onSaved = () => fetchList();
const openDetailModal = (selectedItem: any) => {
console.log("[Experiment/List] row clicked:", selectedItem);
if (!selectedItem?.deviceKey) {
console.warn("[Experiment/List] deviceKey 없음!", selectedItem);
}
data.value.selectedData = selectedItem;
openView.value = true;
};
const openCreateModal = () => { const openCreateModal = () => {
data.value.modalMode = "create"; data.value.modalMode = "create";
data.value.selectedData = { data.value.selectedData = {
@ -534,12 +417,6 @@ onMounted(() => {
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
</v-sheet> </v-sheet>
<!-- <v-sheet class="justify-end mb-2">
<v-btn color="primary" @click="openCreateModal"
>Create Experiment</v-btn
>
</v-sheet> -->
</v-sheet> </v-sheet>
<!-- 목록 --> <!-- 목록 -->
@ -586,10 +463,28 @@ onMounted(() => {
<v-icon v-else-if="item.status === 'Failed'" color="red" <v-icon v-else-if="item.status === 'Failed'" color="red"
>mdi-close-circle</v-icon >mdi-close-circle</v-icon
> >
<v-icon v-else color="grey">mdi-loading</v-icon> <v-progress-circular
v-else-if="item.status === 'Running'"
indeterminate
size="18"
width="2"
color="info"
/>
<v-icon
v-else-if="item.status === 'Pending'"
color="grey"
class="mdi-spin"
>
mdi-loading
</v-icon>
<!-- -->
<v-icon v-else color="grey">mdi-help-circle</v-icon>
</td> </td>
<td>{{ item.duration }}</td> <td>{{ item.duration }}</td>
<td>{{ item.experiment }}</td> <td>{{ item.experiment }}</td>
<td>{{ item.workflow }}</td> <td>{{ item.workflow }}</td>
<td>{{ item.startTime }}</td> <td>{{ item.startTime }}</td>
<td>{{ item.registryStatus }}</td> <td>{{ item.registryStatus }}</td>
@ -640,7 +535,7 @@ onMounted(() => {
<div class="w-100" v-else> <div class="w-100" v-else>
<ViewComponent <ViewComponent
v-if="openView" v-if="openView"
:id="execSelected.deviceKey" :experiment-info="execSelected"
:onClose="closeView" :onClose="closeView"
/> />
</div> </div>

@ -92,11 +92,6 @@ const segWidthPct = () => 100 / (nSteps - 1);
// //
const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—"); const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
//
onMounted(() => {
console.log("[Child] 받은 데이터:", props.experimentInfo);
});
</script> </script>
<template> <template>

@ -3,7 +3,7 @@ import { ref, computed, onMounted, watch } from "vue";
import { ExperimentService } from "@/components/service/management/ExperimentService"; import { ExperimentService } from "@/components/service/management/ExperimentService";
import { ProjectService } from "@/components/service/project/projectService"; import { ProjectService } from "@/components/service/project/projectService";
const props = defineProps<{ id: number | string }>(); const props = defineProps<{ experimentInfo: any }>();
const emit = defineEmits<{ (e: "close"): void }>(); const emit = defineEmits<{ (e: "close"): void }>();
const loading = ref(false); const loading = ref(false);
@ -19,27 +19,67 @@ const experimentInfo = ref({
mlFlowId: "-", mlFlowId: "-",
}); });
const formatIso = (s?: string) => function formatIso(s?: string) {
s ? String(s).replace("T", " ").slice(0, 19) : "-"; return s ? String(s).replace("T", " ").slice(0, 19) : "-";
}
const mapToViewModel = (raw: any) => ({ function mapToViewModel(raw: any) {
experimentName: raw.displayName ?? raw.name ?? "-", const hasRaw = !!raw;
projectName: "-", const created = hasRaw && (raw.createdAt || raw.lastUpdateTime);
createdDate: formatIso(raw.lastUpdateTime), const createdId =
createdId: raw.regUserId ?? "-", (hasRaw && (raw.regUserId || raw.createdBy || raw.serviceAccount)) || "-";
description: raw.description ?? "-",
kubeFlowId: raw.kubeFlowId ?? "-", return {
mlFlowId: raw.mlFlowId ?? "-", experimentName: (hasRaw && (raw.displayName || raw.name)) || "-",
}); projectName: "-",
createdDate: formatIso(created as string | undefined),
createdId: createdId,
description: (hasRaw && raw.description) || "-",
kubeFlowId: (hasRaw && (raw.runId || raw.run_id || raw.id)) || "-",
mlFlowId: (hasRaw && raw.mlFlowId) || "-",
};
}
const info = computed(() => mapToViewModel(detailRaw.value || {})); const info = computed(() => mapToViewModel(detailRaw.value || {}));
function bindFromProp() {
const hasProp =
props.experimentInfo !== null && props.experimentInfo !== undefined;
if (!hasProp) return;
const raw =
hasProp && props.experimentInfo.raw
? props.experimentInfo.raw
: props.experimentInfo;
if (!raw) return;
detailRaw.value = raw;
const vm = mapToViewModel(detailRaw.value);
experimentInfo.value = { ...experimentInfo.value, ...vm };
const prjName = localStorage.getItem("projectName");
if (prjName) {
experimentInfo.value.projectName = prjName;
}
if (
detailRaw.value &&
detailRaw.value.projectId !== undefined &&
detailRaw.value.projectId !== null
) {
fetchProjectName(Number(detailRaw.value.projectId)).catch(function () {});
}
}
async function fetchProjectName(projectId?: number) { async function fetchProjectName(projectId?: number) {
if (!projectId && projectId !== 0) return; if (projectId === undefined || projectId === null) return;
try { try {
const res = await ProjectService.fetchProjectById(projectId as number); const res = await ProjectService.fetchProjectById(projectId as number);
const prj = res?.data ?? res; const prj = res && res.data ? res.data : res;
experimentInfo.value.projectName = prj?.prjNm ?? prj?.name ?? "-"; const name = prj && (prj.prjNm || prj.name) ? prj.prjNm || prj.name : "-";
experimentInfo.value.projectName = name;
} catch (e) { } catch (e) {
console.warn("[Experiment/View] project fetch fail:", e); console.warn("[Experiment/View] project fetch fail:", e);
} }
@ -52,13 +92,20 @@ async function fetchDetail(id: number | string) {
loading.value = true; loading.value = true;
try { try {
const res = await ExperimentService.view(idNum as number); const res = await ExperimentService.view(idNum as number);
detailRaw.value = res?.data ?? res; const payload = res && res.data ? res.data : res;
detailRaw.value = payload;
const vm = mapToViewModel(detailRaw.value); const vm = mapToViewModel(detailRaw.value);
experimentInfo.value = { ...experimentInfo.value, ...vm }; experimentInfo.value = { ...experimentInfo.value, ...vm };
// const hasProjectId =
await fetchProjectName(detailRaw.value?.projectId); detailRaw.value &&
detailRaw.value.projectId !== undefined &&
detailRaw.value.projectId !== null;
if (hasProjectId) {
await fetchProjectName(Number(detailRaw.value.projectId));
}
} catch (e) { } catch (e) {
console.error("[Experiment/View] fetch detail error:", e); console.error("[Experiment/View] fetch detail error:", e);
} finally { } finally {
@ -66,11 +113,25 @@ async function fetchDetail(id: number | string) {
} }
} }
onMounted(() => fetchDetail(props.id)); onMounted(function () {
const hasObj =
props.experimentInfo && typeof props.experimentInfo === "object";
if (hasObj) {
bindFromProp();
} else {
fetchDetail(props.experimentInfo as any);
}
});
watch( watch(
() => props.id, () => props.experimentInfo,
(nv) => { function (nv) {
if (nv !== undefined && nv !== null && nv !== "") fetchDetail(nv); if (nv === null || nv === undefined || nv === "") return;
if (typeof nv === "object") {
bindFromProp();
} else {
fetchDetail(nv as any);
}
}, },
); );
</script> </script>

@ -74,9 +74,6 @@ async function fetchDetail(id: number | string) {
// //
await resolveWorkflowName(raw); await resolveWorkflowName(raw);
console.log("[ViewComponent] raw:", raw);
console.log("[ViewComponent] info:", info.value);
} catch (e) { } catch (e) {
console.error("[ViewComponent] fetch detail error:", e); console.error("[ViewComponent] fetch detail error:", e);
} finally { } finally {

@ -2,13 +2,18 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { storage } from "@/utils/storage"; import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue"; import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue";
import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue"; import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService"; import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { commonStore } from "@/stores/commonStore"; import { commonStore } from "@/stores/commonStore";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const activeRefId = ref<number | null>(null);
const activeRefName = computed(() => String(route.query.refName ?? ""));
const store = commonStore(); const store = commonStore();
const openView = ref(false); const openView = ref(false);
const openModify = ref(false); const openModify = ref(false);
@ -68,6 +73,12 @@ const data = ref({
userOption: [] as any[], userOption: [] as any[],
}); });
function getRefIdFromRoute(q: any): number | null {
const raw = q?.refId;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
// //
function readUsernameFromStorage(): string { function readUsernameFromStorage(): string {
try { try {
@ -139,6 +150,7 @@ const fetchList = async () => {
sortField: "id", sortField: "id",
sortDirection: "DESC", sortDirection: "DESC",
refType: "TRAINING_SCRIPT", refType: "TRAINING_SCRIPT",
refId: activeRefId.value,
}; };
try { try {
@ -286,6 +298,7 @@ const openCreateModal = () => {
data.value.selectedData = { data.value.selectedData = {
username: username.value, username: username.value,
projectId: getProjectId(), projectId: getProjectId(),
refId: activeRefId.value,
}; };
data.value.isCreateVisible = true; data.value.isCreateVisible = true;
}; };
@ -318,8 +331,17 @@ const getSelectedAllData = () => {
}; };
onMounted(() => { onMounted(() => {
username.value = readUsernameFromStorage(); username.value = readUsernameFromStorage();
activeRefId.value = getRefIdFromRoute(route.query);
fetchList(); fetchList();
}); });
watch(
() => route.query.refId,
() => {
activeRefId.value = getRefIdFromRoute(route.query);
data.value.params.pageNum = 1;
fetchList();
},
);
</script> </script>
<template> <template>
@ -413,6 +435,26 @@ onMounted(() => {
/> />
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip
v-if="activeRefId"
class="ml-2"
color="secondary"
variant="tonal"
>
Filter: DataGroup {{ activeRefName }}
</v-chip>
<v-btn
v-if="activeRefId"
size="small"
variant="text"
@click="router.replace({ path: '/TrainingScriptGroup' })"
>
필터 해제
</v-btn>
</v-sheet>
</v-sheet> </v-sheet>
<v-sheet class="justify-end mb-2"> <v-sheet class="justify-end mb-2">

@ -0,0 +1,570 @@
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone";
import { useRouter } from "vue-router";
import { commonStore } from "@/stores/commonStore";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import DatagroupBaseDoalog from "@/components/atoms/organisms/DatagroupBaseDoalog.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
/* -------------------------
* Dayjs & Router
* ------------------------*/
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const router = useRouter();
/* -------------------------
* Types
* ------------------------*/
type SearchType = "전체" | "제목" | "작성자";
type ApiSearchType = "ALL" | "TITLE" | "AUTHOR";
interface DataGroupRow {
no: number;
name: string;
description: string;
author: string;
registDt: string | number | Date;
deviceKey: number;
}
/* -------------------------
* Constants
* ------------------------*/
const TABLE_HEADERS = [
{ label: "No", width: "6%", style: "word-break: keep-all;" },
{ label: "DataGroup Name", width: "24%", style: "word-break: keep-all;" },
{ label: "Description", width: "32%", style: "word-break: keep-all;" },
{ label: "Author", width: "14%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "16%", style: "word-break: keep-all;" },
{ label: "Action", width: "8%", style: "word-break: keep-all;" },
] as const;
const SEARCH_OPTIONS = [
{ label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType },
{ label: "작성자", value: "작성자" as SearchType },
];
const PAGE_SIZE_OPTIONS = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const SEARCH_TYPE_MAP: Record<SearchType | "", ApiSearchType> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
/* -------------------------
* Store & Local State
* ------------------------*/
const store = commonStore();
const openView = ref(false);
const isRunVisible = ref(false); // ( )
const selectedRun = ref<any | null>(null);
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "전체" as SearchType,
searchText: "",
},
results: [] as DataGroupRow[],
totalElements: 0,
pageLength: 0,
modalMode: "" as "create" | "edit" | "upload" | "",
selectedData: null as any,
allSelected: false,
selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [] as any[],
});
/* -------------------------
* Utils
* ------------------------*/
const formatDateTime = (
v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const toRow = (g: any, no: number): DataGroupRow => ({
no,
name: g.dsNm,
description: g.dsDesc,
author: g.regUserNm,
registDt: g.regDate,
deviceKey: g.id,
});
function onRowClick(row: DataGroupRow) {
const id = Number(row?.deviceKey);
if (!Number.isFinite(id)) return;
router.push({
path: "/training-script",
query: { refId: String(id), refName: row.name },
});
}
/* -------------------------
* Data Loaders
* ------------------------*/
async function fetchList() {
const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) {
console.warn("[TrainingscriptGroup] projectId 없음 — 프로젝트 먼저 선택");
data.value.results = [];
data.value.totalElements = 0;
data.value.pageLength = 0;
return;
}
const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
const keyword = (searchText || "").trim();
const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
// ( )
const reqPage = needLocalFilter ? 0 : pageNum - 1; // 0-based
const reqSize = needLocalFilter ? 1000 : pageSize;
try {
const payload = {
projectId,
page: reqPage,
size: reqSize,
keyword,
searchType: mapped,
};
const res: any = await DataGroupService.search(payload);
if (res?.status !== 200) return;
const result = res.data;
let list: any[] = result?.content ?? [];
//
if (needLocalFilter) {
const kw = keyword.toLowerCase();
if (mapped === "TITLE") {
list = list.filter((w: any) =>
String(w?.name ?? "")
.toLowerCase()
.includes(kw),
);
} else if (mapped === "AUTHOR") {
list = list.filter((w: any) =>
String(w?.regUserNm ?? "")
.toLowerCase()
.includes(kw),
);
}
const uiSize = pageSize;
const totalElements = list.length;
const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const safePage = Math.min(Math.max(1, pageNum), totalPages);
const start = (safePage - 1) * uiSize;
const pageSlice = list.slice(start, start + uiSize);
const firstNo = totalElements - start;
data.value.results = pageSlice.map((w: any, i: number) =>
toRow(w, firstNo - i),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
return;
}
//
const totalElements = result.totalElements ?? list.length;
const totalPages = result.totalPages ?? 1;
const serverPage = result.pageable?.pageNumber ?? 0;
const serverSize = result.pageable?.pageSize ?? pageSize;
const offset =
typeof result.pageable?.offset === "number"
? result.pageable.offset
: serverPage * serverSize;
const firstNo = totalElements - offset;
data.value.results = list.map((w: any, i: number) => toRow(w, firstNo - i));
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
} catch (err) {
console.error("[TrainingscriptGroup] 조회 에러:", err);
}
}
/* -------------------------
* Actions
* ------------------------*/
function doSearch() {
data.value.params.pageNum = 1;
fetchList();
}
function changePageNum(page: number) {
data.value.params.pageNum = page;
fetchList();
}
function changePageSize(size: number) {
data.value.params.pageSize = size;
data.value.params.pageNum = 1;
fetchList();
}
function removeData(value?: Array<{ deviceKey: number }>) {
const removeList = value ?? data.value.selected;
if (!removeList?.length) return;
const ids = removeList.map((x) => x.deviceKey);
const removeOnce = (id: number) =>
DataGroupService.delete(id).then((res: any) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
const after = () => {
if (
ids.length >= data.value.results.length &&
data.value.params.pageNum > 1
) {
data.value.params.pageNum -= 1;
}
fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
if (ids.length === 1) {
removeOnce(ids[0])
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "삭제 실패",
result: 500,
});
})
.finally(after);
} else {
Promise.all(ids.map(removeOnce))
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
})
.finally(after);
}
}
function handleRemoveData() {
if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
removeData();
}
function getSelectedAllData() {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: [];
}
function openDetailModal(selectedItem: DataGroupRow) {
data.value.selectedData = selectedItem;
openView.value = true;
}
const closeDetail = () => (openView.value = false);
function openRunModal(item: any) {
selectedRun.value = item;
isRunVisible.value = true;
}
function openModifyModal(item: DataGroupRow) {
data.value.selectedData = {
id: item.deviceKey,
workflowName: item.name,
workflowDescription: item.description,
};
data.value.modalMode = "edit";
data.value.isCreateVisible = true;
}
function openCreateModal() {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
}
function openUploadModal() {
data.value.selectedData = null;
data.value.modalMode = "upload";
data.value.isUploadVisible = true;
}
function closeCreateModal() {
data.value.isModalVisible = false;
data.value.isCreateVisible = false;
}
function closeUploadModal() {
data.value.isModalVisible = false;
data.value.isUploadVisible = false;
}
/* -------------------------
* Watchers & Lifecycle
* ------------------------*/
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
watch(
() => data.value.isUploadVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
onMounted(fetchList);
</script>
<template>
<div class="w-100" v-if="!openView">
<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 align-center 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 class="text-primary">TrainingScriptGroup</div>
</div>
</v-card-item>
</v-card>
<!-- Search -->
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="SEARCH_OPTIONS"
item-title="label"
item-value="value"
hide-details
@update:model-value="doSearch"
/>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="doSearch"
/>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="doSearch"
>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<!-- Toolbar -->
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalElements.toLocaleString() }}</v-chip
>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="PAGE_SIZE_OPTIONS"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageSize"
/>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create DataGroup</v-btn
>
</v-sheet>
</v-sheet>
<!-- Table -->
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(h, i) in TABLE_HEADERS"
:key="i"
:style="`width:${h.width}`"
/>
</colgroup>
<thead>
<tr>
<th
v-for="(h, i) in TABLE_HEADERS"
:key="i"
class="text-center font-weight-bold"
:style="h.style"
>
{{ h.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(row, i) in data.results"
:key="i"
class="text-center row-hover"
@click="onRowClick(row)"
>
<td>{{ row.no }}</td>
<td>{{ row.name }}</td>
<td>{{ row.description }}</td>
<td>{{ row.author || "-" }}</td>
<td>{{ formatDateTime(row.registDt) }}</td>
<td
style="white-space: nowrap"
@click.stop
@mousedown.stop
>
<IconInfoBtn @on-click="openDetailModal(row)" />
<IconModifyBtn @on-click="openModifyModal(row)" />
<IconDeleteBtn
@on-click="removeData([{ deviceKey: row.deviceKey }])"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="changePageNum"
/>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<!-- Create/Edit Dialog -->
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
<DatagroupBaseDoalog
:key="data.modalMode + String(data.selectedData?.deviceKey ?? '')"
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent
v-if="data.selectedData"
:id="data.selectedData.deviceKey"
@close="closeDetail"
/>
</div>
</template>
<style scoped>
/* 행 Hover 효과 */
.row-hover {
cursor: pointer;
transition: background-color 120ms ease;
}
tbody tr.row-hover:hover {
background-color: rgba(255, 255, 255, 0.06);
}
tbody tr.row-hover:active {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

@ -25,13 +25,13 @@ const isRunVisible = ref(false);
const selectedRun = ref<any | null>(null); const selectedRun = ref<any | null>(null);
const tableHeader = [ const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" }, { label: "No", width: "6%", style: "white-space: nowrap;" },
{ label: "Workflow Name", width: "18%", style: "word-break: keep-all;" }, { label: "Workflow Name", width: "22%", style: "white-space: nowrap;" },
{ label: "Description", width: "28%", style: "word-break: keep-all;" }, { label: "Description", width: "30%", style: "white-space: nowrap;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" }, { label: "Version", width: "10%", style: "white-space: nowrap;" },
{ label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" }, { label: "Kubeflow Status", width: "12%", style: "white-space: nowrap;" },
{ label: "Created DateTime", width: "15%", style: "word-break: keep-all;" }, { label: "Created DateTime", width: "12%", style: "white-space: nowrap;" },
{ label: "Action", width: "12%", style: "word-break: keep-all;" }, { label: "Action", width: "8%", style: "white-space: nowrap;" },
]; ];
const searchOptions = [ const searchOptions = [
{ label: "전체", value: "전체" as SearchType }, { label: "전체", value: "전체" as SearchType },
@ -125,6 +125,7 @@ const fetchList = () => {
if (res.status !== 200) return; if (res.status !== 200) return;
const result = res.data; const result = res.data;
console.log("Workflows", result);
let list = result?.content ?? []; let list = result?.content ?? [];
if (needLocalFilter) { if (needLocalFilter) {
@ -447,7 +448,6 @@ onMounted(() => {
overflow-x-auto overflow-x-auto
> >
<colgroup> <colgroup>
<col style="width: 5%" />
<col <col
v-for="(item, i) in tableHeader" v-for="(item, i) in tableHeader"
:key="i" :key="i"
@ -456,15 +456,6 @@ onMounted(() => {
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th <th
v-for="(item, i) in tableHeader" v-for="(item, i) in tableHeader"
:key="i" :key="i"
@ -481,15 +472,6 @@ onMounted(() => {
:key="i" :key="i"
class="text-center" class="text-center"
> >
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td>
<td>{{ item.no }}</td> <td>{{ item.no }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.description }}</td> <td>{{ item.description }}</td>

@ -47,7 +47,6 @@ const signUp = () => {
role: data.value.role ? [data.value.role] : [], role: data.value.role ? [data.value.role] : [],
password: data.value.password, password: data.value.password,
}; };
console.log("회원가입 호출 payload:", payload);
UserManagerService.signUp(payload) UserManagerService.signUp(payload)
.then((res) => { .then((res) => {

@ -0,0 +1,9 @@
<script setup>
import ListComponent from "@/components/templates/trainingscriptgroup/ListComponent.vue";
</script>
<template>
<ListComponent />
</template>
<style scoped lang="sass"></style>

@ -44,7 +44,13 @@ const routes = [
name: "dataGroup", name: "dataGroup",
path: "/datagroup", path: "/datagroup",
meta: { title: "DataGroup", requiresAuth: false }, meta: { title: "DataGroup", requiresAuth: false },
component: () => import("@/pages/Datagroup.vue"), component: () => import("@/pages/DatagroupView.vue"),
},
{
name: "TrainingScriptGroup",
path: "/trainingscriptgroup",
meta: { title: "TrainingScriptGroup", requiresAuth: false },
component: () => import("@/pages/TrainingscriptgroupView.vue"),
}, },
{ {
name: "run", name: "run",

@ -12,12 +12,7 @@ export const menuUtils = {
value: "workflows", value: "workflows",
icon: "mdi-code-braces", icon: "mdi-code-braces",
}, },
{
title: "DataGroup",
path: "/DataGroup",
value: "DataGroup",
icon: "mdi-hammer-wrench",
},
{ {
title: "Run", title: "Run",
path: "/run", path: "/run",
@ -34,19 +29,31 @@ export const menuUtils = {
value: "deployment", value: "deployment",
icon: "mdi-folder-search", icon: "mdi-folder-search",
}, },
],
adminMenuItem: [
{ {
title: "Training Script", title: "TrainingScriptGroup",
path: "/training-script", path: "/TrainingScriptGroup",
value: "training-script", value: "TrainingScriptGroup",
icon: "mdi-file-code-outline", icon: "mdi-file-code-outline",
}, },
{ {
title: "Datasets", title: "DataSetGroup",
path: "/datasets", path: "/DataGroup",
value: "datasets", value: "DataGroup",
icon: "mdi-database-outline", icon: "mdi-database-outline",
}, },
], ],
adminMenuItem: [
// {
// title: "Training Script",
// path: "/training-script",
// value: "training-script",
// icon: "mdi-file-code-outline",
// },
// {
// title: "Datasets",
// path: "/datasets",
// value: "datasets",
// icon: "mdi-database-outline",
// },
],
}; };

3
typed-router.d.ts vendored

@ -19,7 +19,7 @@ declare module 'vue-router/auto-routes' {
*/ */
export interface RouteNamedMap { export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>, '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/Datagroup': RouteRecordInfo<'/Datagroup', '/Datagroup', Record<never, never>, Record<never, never>>, '/DatagroupView': RouteRecordInfo<'/DatagroupView', '/DatagroupView', Record<never, never>, Record<never, never>>,
'/DatasetView': RouteRecordInfo<'/DatasetView', '/DatasetView', Record<never, never>, Record<never, never>>, '/DatasetView': RouteRecordInfo<'/DatasetView', '/DatasetView', Record<never, never>, Record<never, never>>,
'/DeploymentView': RouteRecordInfo<'/DeploymentView', '/DeploymentView', Record<never, never>, Record<never, never>>, '/DeploymentView': RouteRecordInfo<'/DeploymentView', '/DeploymentView', Record<never, never>, Record<never, never>>,
'/ExecutionsView': RouteRecordInfo<'/ExecutionsView', '/ExecutionsView', Record<never, never>, Record<never, never>>, '/ExecutionsView': RouteRecordInfo<'/ExecutionsView', '/ExecutionsView', Record<never, never>, Record<never, never>>,
@ -29,6 +29,7 @@ declare module 'vue-router/auto-routes' {
'/MainView': RouteRecordInfo<'/MainView', '/MainView', Record<never, never>, Record<never, never>>, '/MainView': RouteRecordInfo<'/MainView', '/MainView', Record<never, never>, Record<never, never>>,
'/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>, '/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>, '/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>,
'/TrainingscriptgroupView': RouteRecordInfo<'/TrainingscriptgroupView', '/TrainingscriptgroupView', Record<never, never>, Record<never, never>>,
'/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>, '/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>,
'/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record<never, never>, Record<never, never>>, '/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record<never, never>, Record<never, never>>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>, '/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>,

Loading…
Cancel
Save