diff --git a/.env.dev b/.env.dev index 7373427..42b5c38 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,3 @@ NODE_ENV = "development" -VITE_APP_API_SERVER_URL = "http://localhost:80" +VITE_APP_API_SERVER_URL = "http://localhost:8080" VITE_ROOT_PATH = "" \ No newline at end of file diff --git a/components.d.ts b/components.d.ts index f638129..e95cea5 100644 --- a/components.d.ts +++ b/components.d.ts @@ -10,6 +10,7 @@ declare module 'vue' { export interface GlobalComponents { AppFooter: typeof import('./src/components/AppFooter.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'] DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.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'] IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.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'] RouterView: typeof import('vue-router')['RouterView'] SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default'] diff --git a/src/components/atoms/organisms/DatagroupBaseDoalog.vue b/src/components/atoms/organisms/DatagroupBaseDoalog.vue new file mode 100644 index 0000000..7ac1510 --- /dev/null +++ b/src/components/atoms/organisms/DatagroupBaseDoalog.vue @@ -0,0 +1,210 @@ + + + + + + {{ isEdit ? "Edit DataGroup" : "Create DataGroup" }} + + + + + DataGroup Information + + + + + + DataGroup Name + + + + + + Description + + + + {{ errorMsg }} + + + + + + {{ isEdit ? "Update" : "Save" }} + + Close + + + diff --git a/src/components/atoms/organisms/DatasetBaseDoalog.vue b/src/components/atoms/organisms/DatasetBaseDoalog.vue index cd335c6..f78f1ce 100644 --- a/src/components/atoms/organisms/DatasetBaseDoalog.vue +++ b/src/components/atoms/organisms/DatasetBaseDoalog.vue @@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) { form.value.description = (d?.description ?? "") + ""; } +const getRefId = () => String(props.editData?.refId ?? "0"); onMounted(() => { if (isEdit.value) hydrateFormFromEdit(props.editData); }); @@ -39,7 +40,7 @@ watch( ); 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 = "프로젝트가 선택되지 않았습니다."); const fd = new FormData(); - fd.append("refId", "0"); + fd.append("refId", getRefId()); fd.append("refType", "DATASET"); fd.append("title", title); fd.append("description", desc); diff --git a/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue b/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue index a616696..02754e3 100644 --- a/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue +++ b/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue @@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) { form.value.description = (d?.description ?? "") + ""; } +const getRefId = () => String(props.editData?.refId ?? "0"); onMounted(() => { if (isEdit.value) hydrateFormFromEdit(props.editData); }); @@ -59,7 +60,6 @@ const regUserId = (() => { } })(); -// ✅ 여기만 변경 async function submit() { errorMsg.value = ""; @@ -95,7 +95,7 @@ async function submit() { return (errorMsg.value = "프로젝트가 선택되지 않았습니다."); const fd = new FormData(); - fd.append("refId", "0"); + fd.append("refId", getRefId()); fd.append("refType", "TRAINING_SCRIPT"); fd.append("title", title); fd.append("description", desc); diff --git a/src/components/atoms/organisms/WorkflowsBaseDialog.vue b/src/components/atoms/organisms/WorkflowsBaseDialog.vue index 068830b..038aa27 100644 --- a/src/components/atoms/organisms/WorkflowsBaseDialog.vue +++ b/src/components/atoms/organisms/WorkflowsBaseDialog.vue @@ -169,14 +169,10 @@ async function submit() { const authObj = (typeof storage?.getAuth === "function" ? storage.getAuth() : null) ?? 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 = "로그인 사용자 정보를 찾을 수 없습니다."; return; } @@ -198,28 +194,21 @@ async function submit() { errorMsg.value = "수정할 ID가 없습니다."; return; } - - // ① 기존 값 조회 const viewRes = await WorkflowService.view(id); const current = (viewRes?.data ?? viewRes) || {}; - // ② name/description만 변경, 그 외는 기존 값 유지해서 null 덮어쓰기 방지 const updatePayload = cleanUndefined({ id, - name, // 변경 - description: form.value.description?.trim() || "", // 변경 - - // ===== 기존 유지 필드 ===== + name, + description: form.value.description?.trim() || "", displayName: current.displayName, namespace: current.namespace, pipelineId: current.pipelineId, kubeflowStatus: current.kubeflowStatus, version: current.version, - - regUserId: current.regUserId ?? regUserId, - projectId: current.projectId ?? projectId.value, + regUserId: current.regUserId, regDt: current.regDt, - modDt: now, + projectId: current.projectId ?? projectId.value, }); const { data } = await WorkflowService.update(id, updatePayload); @@ -236,7 +225,7 @@ async function submit() { display_name: name, description: form.value.description?.trim() || "", namespace: "default", - regUserId, + regUserId: userId, projectId: projectId.value!, uploadfile: form.value.file, }; diff --git a/src/components/common/LayoutComponent.vue b/src/components/common/LayoutComponent.vue index b4cc1a2..a9a6049 100644 --- a/src/components/common/LayoutComponent.vue +++ b/src/components/common/LayoutComponent.vue @@ -12,12 +12,9 @@ const router = useRouter(); const username = ref(""); const projectName = ref(localStorage.getItem("projectName") || ""); -// ---------------------- -// Admin 판별 + Admin 모드 -// ---------------------- -const isAdmin = ref(false); // 관리자 계정 여부 -const adminMode = ref(false); // 설정 버튼으로 토글되는 관리자 메뉴 모드 -const lastNonAdminPath = ref("/home"); // 마지막 일반 경로 저장 +const isAdmin = ref(false); +const adminMode = ref(false); +const lastNonAdminPath = ref("/home"); function computeIsAdmin() { try { @@ -79,11 +76,19 @@ function updateUsername() { const auth = storage.getAuth?.() ?? null; 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() { const v = localStorage.getItem("projectName"); projectName.value = v ? v : ""; } function goSelect() { + adminMode.value = false; router.push("/select"); } function logOut() { @@ -103,13 +108,17 @@ function logOut() { // storage 변경 반영 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") { updateUsername(); computeIsAdmin(); } } const goMain = () => { + adminMode.value = false; router.push("/home"); }; @@ -120,6 +129,7 @@ watch( refreshProjectName(); const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin); if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home"; + syncAdminModeWithRoute(); }, { immediate: true }, ); @@ -130,6 +140,7 @@ onMounted(() => { refreshProjectName(); menu.value = menuItems; window.addEventListener("storage", onStorage); + syncAdminModeWithRoute(); }); onBeforeUnmount(() => { window.removeEventListener("storage", onStorage); diff --git a/src/components/models/management/Kubeflow.ts b/src/components/models/management/Kubeflow.ts index 9d3be98..f2edec4 100644 --- a/src/components/models/management/Kubeflow.ts +++ b/src/components/models/management/Kubeflow.ts @@ -3,13 +3,11 @@ export type KubeflowUploadDto = { display_name?: string; description?: string; namespace?: string; - regUserId: string; + regUserId: string | number; // number도 허용 projectId: number | string; uploadfile: File | Blob; }; -export type kubeflow = FormData; - export function toKubeflowForm(dto: KubeflowUploadDto): FormData { const fd = new FormData(); fd.append("name", dto.name); diff --git a/src/components/service/management/DataGroupService.ts b/src/components/service/management/DataGroupService.ts index 53ed3ea..c89360d 100644 --- a/src/components/service/management/DataGroupService.ts +++ b/src/components/service/management/DataGroupService.ts @@ -2,6 +2,7 @@ import { DataGroup, DataGroupSearch, } from "@/components/models/management/DataGroup"; + import { request } from "@/components/service/index"; export const DataGroupService = { add: (payload: DataGroup) => { @@ -16,8 +17,8 @@ export const DataGroupService = { view: (id: Number) => { return request.get(`/api/datagroup/${id}`, {}); }, - update: (id: number, payload: DataGroup) => { - return request.put(`/api/datagroup/${id}`, payload); + update: (id: number, updatePayload: DataGroup) => { + return request.put(`/api/datagroup/${id}`, updatePayload); }, search: (payload: DataGroupSearch) => { return request.get("/api/datagroup/search", payload); diff --git a/src/components/service/management/kubeflowService.ts b/src/components/service/management/kubeflowService.ts index dabdf08..d27d7a7 100644 --- a/src/components/service/management/kubeflowService.ts +++ b/src/components/service/management/kubeflowService.ts @@ -1,4 +1,4 @@ -import { kubeflow } from "@/components/models/management/Kubeflow"; +import { Kubeflow } from "@/components/models/management/Kubeflow"; import { request } from "@/components/service/index"; export const KubeflowService = { upload: (payload: kubeflow) => { diff --git a/src/components/templates/Datasets/ListComponent.vue b/src/components/templates/Datasets/ListComponent.vue index 7b8db49..98c6c30 100644 --- a/src/components/templates/Datasets/ListComponent.vue +++ b/src/components/templates/Datasets/ListComponent.vue @@ -2,13 +2,18 @@ import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.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 ViewComponent from "@/components/templates/Datasets/ViewComponent.vue"; import DatasetBaseDoalog from "@/components/atoms/organisms/DatasetBaseDoalog.vue"; import { AttachmentsService } from "@/components/service/management/AttachmentsService"; import { commonStore } from "@/stores/commonStore"; +import { useRoute, useRouter } from "vue-router"; +const route = useRoute(); +const router = useRouter(); +const activeRefId = ref(null); +const activeRefName = computed(() => String(route.query.refName)); const store = commonStore(); const openView = ref(false); const openModify = ref(false); @@ -69,6 +74,12 @@ const data = ref({ 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 { try { @@ -140,6 +151,7 @@ const fetchList = async () => { sortField: "id", sortDirection: "DESC", refType: "DATASET", + refId: activeRefId.value, }; try { @@ -286,6 +298,7 @@ const openCreateModal = () => { data.value.selectedData = { username: username.value, projectId: getProjectId(), + refId: activeRefId.value, }; data.value.isCreateVisible = true; }; @@ -318,8 +331,18 @@ const getSelectedAllData = () => { }; onMounted(() => { username.value = readUsernameFromStorage(); + activeRefId.value = getRefIdFromRoute(route.query); fetchList(); }); + +watch( + () => route.query.refId, + () => { + activeRefId.value = getRefIdFromRoute(route.query); + data.value.params.pageNum = 1; + fetchList(); + }, +); @@ -413,6 +436,26 @@ onMounted(() => { /> + + + Filter: DataGroup {{ activeRefName }} + + + 필터 해제 + + diff --git a/src/components/templates/datagroup/ListComponent.vue b/src/components/templates/datagroup/ListComponent.vue index 1679e9d..478ec53 100644 --- a/src/components/templates/datagroup/ListComponent.vue +++ b/src/components/templates/datagroup/ListComponent.vue @@ -3,55 +3,80 @@ 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 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 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(tz); const KST = "Asia/Seoul"; -const store = commonStore(); -const openView = ref(false); -type SearchType = "전체" | "제목" | "작성자"; - -const isRunVisible = ref(false); -const selectedRun = ref(null); +const router = useRouter(); -const tableHeader = [ - { label: "No", width: "5%", style: "word-break: keep-all;" }, - { label: "Workflow Name", width: "18%", style: "word-break: keep-all;" }, - { label: "Description", width: "28%", style: "word-break: keep-all;" }, - { label: "Version", width: "10%", style: "word-break: keep-all;" }, - { label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" }, - { label: "Created DateTime", width: "15%", style: "word-break: keep-all;" }, - { label: "Action", width: "12%", style: "word-break: keep-all;" }, -]; -const searchOptions = [ +/* ------------------------- + * 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 }, + { label: "작성자", value: "작성자" as SearchType }, ]; -const pageSizeOptions = [ +const PAGE_SIZE_OPTIONS = [ { text: "10 페이지", value: 10 }, { text: "50 페이지", value: 50 }, { text: "100 페이지", value: 100 }, ]; -const SEARCH_TYPE_MAP: Record = { +const SEARCH_TYPE_MAP: Record = { "": "ALL", 전체: "ALL", 제목: "TITLE", 작성자: "AUTHOR", }; +/* ------------------------- + * Store & Local State + * ------------------------*/ +const store = commonStore(); +const openView = ref(false); + +const isRunVisible = ref(false); // 유지 (다른 곳에서 열릴 수 있음) +const selectedRun = ref(null); + const data = ref({ params: { pageNum: 1, @@ -59,7 +84,7 @@ const data = ref({ searchType: "전체" as SearchType, searchText: "", }, - results: [] as any[], + results: [] as DataGroupRow[], totalElements: 0, pageLength: 0, modalMode: "" as "create" | "edit" | "upload" | "", @@ -73,137 +98,144 @@ const data = ref({ 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 = (w: any, no: number) => ({ +const toRow = (g: any, no: number): DataGroupRow => ({ no, - name: w.name, - description: w.description, - version: w.version, - kubeflowStatus: w.kubeflowStatus, - registDt: w.regDt, - deviceKey: w.id, - pipelineId: w.pipelineId ?? w.pipeline_id ?? "", + name: g.dsNm, + description: g.dsDesc, + author: g.regUserNm, + registDt: g.regDate, + deviceKey: g.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")); if (!projectId) { - console.warn("[Workflows] projectId 없음 — 프로젝트 먼저 선택"); + console.warn("[DataGroup] projectId 없음 — 프로젝트 먼저 선택"); data.value.results = []; data.value.totalElements = 0; data.value.pageLength = 0; return; } - const { pageNum, searchText, searchType } = data.value.params; - - // 매핑 및 로컬 필터링 필요 여부 판단 + 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; - let reqPage = data.value.params.pageNum; - let reqSize = data.value.params.pageSize; - if (needLocalFilter) { - reqPage = 0; - reqSize = 1000; - } - const payload = { - projectId, - page: reqPage, - size: reqSize, - keyword, - searchType: mapped, - }; - - DataGroupService.search(payload) - .then((res: any) => { - if (res.status !== 200) return; - - const result = res.data; - let list = 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?.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), + // 서버 요청 크기 설정 (로컬 필터가 필요하면 넉넉히 가져옴) + 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), ); - data.value.totalElements = totalElements; - data.value.pageLength = totalPages; - return; } - const totalElements = result.totalElements; - const totalPages = result.totalPages; - const serverPage = result.pageable.pageNumber; - const serverSize = result.pageable.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) => + 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; - }) - .catch((err: any) => console.error("워크플로우 조회 에러:", err)); -}; + return; + } -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; fetchList(); -}; - -const changePageNum = (page: number) => { +} +function changePageNum(page: number) { data.value.params.pageNum = page; fetchList(); -}; - -const changePageSize = (size: number) => { +} +function changePageSize(size: number) { data.value.params.pageSize = size; data.value.params.pageNum = 1; fetchList(); -}; +} -const removeData = (value?: Array<{ deviceKey: number }>) => { +function removeData(value?: Array<{ deviceKey: number }>) { const removeList = value ?? data.value.selected; - if (!removeList || removeList.length === 0) return; + if (!removeList?.length) return; const ids = removeList.map((x) => x.deviceKey); - const remove = (id: number) => - DataGroupService.delete(id).then((res) => { + const removeOnce = (id: number) => + DataGroupService.delete(id).then((res: any) => { 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.allSelected = false; }; - console.log(ids.length); if (ids.length === 1) { - remove(ids[0]) - .then(() => { + removeOnce(ids[0]) + .then(() => store.setSnackbarMsg({ color: "success", text: "삭제되었습니다.", result: 200, - }); - after(); - }) + }), + ) .catch((err) => { + console.error(err); store.setSnackbarMsg({ color: "warning", text: "삭제 실패", result: 500, }); - console.error(err); - }); + }) + .finally(after); } else { - Promise.all(ids.map(remove)) - .then(() => { + 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, }); - console.error(err); }) .finally(after); } -}; +} -const handleRemoveData = () => { +function handleRemoveData() { if (data.value.selected.length === 0) return; if (data.value.allSelected || data.value.selected.length !== 1) { data.value.isConfirmDialogVisible = true; return; } removeData(); -}; +} -const getSelectedAllData = () => { +function getSelectedAllData() { data.value.selected = data.value.allSelected ? data.value.results.map((item) => ({ deviceKey: item.deviceKey })) : []; -}; +} -const openDetailModal = (selectedItem: any) => { +function openDetailModal(selectedItem: DataGroupRow) { data.value.selectedData = selectedItem; openView.value = true; -}; - +} const closeDetail = () => (openView.value = false); -const closeRunModal = () => (isRunVisible.value = false); -const openRunModal = (item: any) => { +function openRunModal(item: any) { selectedRun.value = item; isRunVisible.value = true; -}; +} -const openModifyModal = (item: any) => { +function openModifyModal(item: DataGroupRow) { data.value.selectedData = { id: item.deviceKey, workflowName: item.name, @@ -296,47 +325,42 @@ const openModifyModal = (item: any) => { }; data.value.modalMode = "edit"; data.value.isCreateVisible = true; -}; - -const openCreateModal = () => { +} +function openCreateModal() { data.value.selectedData = null; data.value.modalMode = "create"; data.value.isCreateVisible = true; -}; - -const openUploadModal = () => { +} +function openUploadModal() { data.value.selectedData = null; data.value.modalMode = "upload"; data.value.isUploadVisible = true; -}; - -const closeCreateModal = () => { +} +function closeCreateModal() { data.value.isModalVisible = false; data.value.isCreateVisible = false; -}; - -const closeUploadModal = () => { +} +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(); -}); +onMounted(fetchList); @@ -353,6 +377,8 @@ onMounted(() => { + + @@ -365,13 +391,14 @@ onMounted(() => { v-model="data.params.searchType" label="검색조건" density="compact" - :items="searchOptions" + :items="SEARCH_OPTIONS" item-title="label" item-value="value" hide-details @update:model-value="doSearch" /> + { class="mt-3 mb-3" hide-details @keyup.enter="doSearch" - > + /> @@ -392,12 +419,13 @@ onMounted(() => { :rounded="5" @click="doSearch" > - mdi-magnify + mdi-magnify + @@ -414,27 +442,25 @@ onMounted(() => { + /> - Create Workflow - + >Create DataGroup + @@ -447,69 +473,52 @@ onMounted(() => { overflow-x-auto > - - - - - {{ item.label }} + {{ h.label }} - - - - {{ item.no }} - {{ item.name }} - {{ item.description }} - {{ item.version }} - {{ item.kubeflowStatus }} - {{ formatDateTime(item.registDt) }} - - - - + {{ row.no }} + {{ row.name }} + {{ row.description }} + {{ row.author || "-" }} + {{ formatDateTime(row.registDt) }} + + + + { color="primary" rounded="circle" @update:model-value="changePageNum" - > + /> + + - - - - - - - @@ -559,4 +555,16 @@ onMounted(() => { - + diff --git a/src/components/templates/home/ListComponent.vue b/src/components/templates/home/ListComponent.vue index 4becb98..9aaef62 100644 --- a/src/components/templates/home/ListComponent.vue +++ b/src/components/templates/home/ListComponent.vue @@ -1,129 +1,159 @@ + - 배터리 상태 예측 모델 프로젝트 + {{ currentProjectName }} + + @@ -512,72 +640,48 @@ loadKubeflowRuns(); v-for="i in 4" :key="i" /> - + - {{ - run.status === "success" - ? "mdi-check" - : run.status === "failed" - ? "mdi-close" - : run.status === "running" - ? "mdi-progress-clock" - : "mdi-clock-outline" - }} + {{ avatarIconByUiStatus(runRow.status) }} - {{ run.status }} + {{ STATUS_LABEL[runRow.status] }} - + - {{ run.name }} + {{ runRow.name }} - {{ run.time }} + {{ runRow.time }} - + @@ -590,6 +694,7 @@ loadKubeflowRuns(); + @@ -597,22 +702,28 @@ loadKubeflowRuns(); Recently Registered Workflow + - + {{ - item.title + wfRow.name + }} + {{ + fmtYmdHm(wfRow.modDt) }} - - {{ formatToYmdHm(item.timestamp) }} - - + 최근 등록/수정된 워크플로우가 없습니다. @@ -625,163 +736,260 @@ loadKubeflowRuns(); + + - - Kubeflow Runs + + + + Kubeflow Runs + + + Total {{ kubeflowStats.total }} + Succeeded {{ kubeflowStats.succeeded }} + Failed {{ kubeflowStats.failed }} + + + Filter: {{ kubeflowStatusFilter }} + + + - - - + + + + + - - - - - - - - {{ r.state }} - - - - {{ r.name }} - - - {{ fmtYmdHm(r.createdAt) }} - - → {{ fmtYmdHm(r.finishedAt) }} - - - + + + + + + + - - - - Experiment: - {{ - r.experimentId || "-" - }} - - - Pipeline: - {{ - r.pipelineName || "-" - }} - - + + + + Clear + - - - - - 표시할 Kubeflow Run 데이터가 없습니다. - - - - + + + + + + + {{ + (kfRunRow.state || "") + .toUpperCase() + .includes("SUCCEED") + ? "Succeeded" + : "Failed" + }} + + + {{ kfRunRow.name }} + + + + {{ fmtYmdHm(kfRunRow.createdAt) }} + + + + + + + + 표시할 Run이 없습니다. + + + + + + + + + - - - - - Dataset Update Activity - - + + + + + Dataset Update Activity + + - + + - - - - {{ it.name }} - - v{{ it.version }} - - - + + { + if (e.value) await loadDatasetsForGroup(g.id, g.name); + } + " + > + - - Last: {{ fmtYmd(it.last) }} - + {{ g.name }} + + {{ (datasetsByGroup[g.id] || []).length }} datasets + + + + + + + - - - - {{ it.version }} Update{{ it.version > 1 ? "s" : "" }} - - • {{ it.rows }} Rows 0" + density="compact" > - - + + + + {{ row.name }} + v{{ row.version }} + + Last: {{ fmtLocalDateTime(row.last) }} + + + + + {{ row.version }} Update{{ row.version > 1 ? "s" : "" }} + + • {{ row.rows }} Rows + + + - - - - 표시할 데이터셋이 없습니다. + + 이 그룹에 표시할 데이터셋이 없습니다. - - - - + + + + + 표시할 데이터그룹이 없습니다. + + + + @@ -796,56 +1004,62 @@ loadKubeflowRuns(); Go to Model Deploy + - + + + + + - - + + + Model Name - - {{ item.label }} + + Version + + + Deployed At + + + Status + + + Download - - - - - {{ item.name }} - {{ item.version }} - {{ item.time }} - {{ item.status }} - {{ item.download }} + + + LaneDetectionModel + v1.2.0 + 2025-05-13 14:32 + Active + Finished + + + + TrafficSignClassifier + v0.9.3 + 2025-05-13 09:00 + Pending + - + + + + PathPlannerModel + v2.0.1 + 2025-05-12 17:44 + Failed + Failed diff --git a/src/components/templates/run/executions/ListComponent.vue b/src/components/templates/run/executions/ListComponent.vue index 2342b91..cfa2f75 100644 --- a/src/components/templates/run/executions/ListComponent.vue +++ b/src/components/templates/run/executions/ListComponent.vue @@ -11,19 +11,11 @@ import { KubeflowRunService } from "@/components/service/management/KubeflowRunS import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue"; const store = commonStore(); + const openView = ref(false); -const username = ref(""); -const openCompare = ref(false); const execSelected = ref(null); -const selectedExperiment = ref<{ - name: string; - description: string; - createdDate: string; - createdID: string; - deviceKey: number; -} | null>(null); - -// ===== 헤더 ===== +const username = ref(""); + const tableHeader = [ { label: "No", width: "5%", 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: "Action", width: "10%", style: "word-break: keep-all;" }, ]; -// ===== 검색/페이지네이션 (데이터셋/워크플로우와 '동일') ===== -type SearchType = "전체" | "제목" | "작성자"; +type SearchType = "전체" | "제목" | "작성자"; const searchOptions = [ { label: "전체", value: "전체" as SearchType }, { label: "제목", value: "제목" as SearchType }, { label: "작성자", value: "작성자" as SearchType }, ]; - const SEARCH_TYPE_MAP: Record = { "": "ALL", 전체: "ALL", @@ -57,7 +47,6 @@ const pageSizeOptions = [ { text: "100 페이지", value: 100 }, ]; -// ===== 상태 ===== const data = ref({ params: { pageNum: 1, @@ -70,15 +59,9 @@ const data = ref({ pageLength: 0, modalMode: "" as "create" | "edit" | "", selectedData: null as any, - allSelected: false, - selected: [] as any[], isCreateVisible: false, - isModalVisible: false, - isConfirmDialogVisible: false, - userOption: [] as any[], }); -// ===== 유틸 ===== function readUsernameFromStorage(): string { try { const raw = @@ -100,11 +83,7 @@ const getProjectId = (): number => { const v = Number(localStorage.getItem("projectId")); 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 fmtStart = (start?: string) => { if (!start) return "-"; @@ -155,7 +134,6 @@ const toRow = (r: any, idx: number) => { status: toUiStatus(r.state), duration: fmtDuration(r.createdAt, r.finishedAt), experiment: r.experimentId ?? "-", - workflow: r.pipelineId ?? r.pipelineVersionId ?? "-", startTime: fmtStart(r.createdAt), 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() { const { pageNum, pageSize, searchType, searchText } = data.value.params; const mapped = SEARCH_TYPE_MAP[searchType] || "ALL"; @@ -264,7 +153,7 @@ async function fetchList() { size: pageSize, keyword, searchType: mapped, - sortField: "id", + sortField: "createdAt", sortDirection: "DESC", }; @@ -272,29 +161,44 @@ async function fetchList() { const res = await KubeflowRunService.search(payload as any); const result = res?.data ?? res; - // 1) 응답 정규화 let list: any[] = []; let totalElements: number | undefined; let totalPages: number | undefined; let isServerPaged = false; - if (Array.isArray(result)) { - // 루트 배열 - list = result; - } else if (Array.isArray(result?.data)) { - // data 배열 - list = result.data; - } else if (Array.isArray(result?.content)) { - // 서버 페이징 (Page) + if (Array.isArray(result)) list = result; + else if (Array.isArray(result?.data)) list = result.data; + else if (Array.isArray(result?.content)) { list = result.content; totalElements = result.totalElements; totalPages = result.totalPages; isServerPaged = true; - } else if (Array.isArray(result?.runs)) { - list = result.runs; - } else { - list = []; - } + } else if (Array.isArray(result?.runs)) list = result.runs; + + // 클라 보정 정렬 (서버 미적용 대비) + 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) { const total = list.length; @@ -307,8 +211,7 @@ async function fetchList() { data.value.totalElements = total; data.value.pageLength = pages; } else { - // 3) 서버 페이징 결과 그대로 적용 - data.value.results = (list as any[]).map((r, i) => toRow(r, i)); + data.value.results = list.map((r, i) => toRow(r, i)); data.value.totalElements = typeof totalElements === "number" ? totalElements : list.length; data.value.pageLength = @@ -323,7 +226,8 @@ async function fetchList() { data.value.pageLength = 1; } } -// ===== 검색/페이지 조작 ===== + +// 검색/페이지 조작 const doSearch = () => { data.value.params.pageNum = 1; fetchList(); @@ -338,12 +242,11 @@ const changePageSize = (size: number) => { fetchList(); }; -// 삭제/수정 버튼 등(기존 로직 유지) -const removeData = (value?: Array<{ deviceKey: number }>) => { - const removeList = value ?? data.value.selected; - if (!removeList || removeList.length === 0) return; +// 삭제 +const removeData = (value: Array<{ deviceKey: number }>) => { + const ids = (value || []).map((x) => x.deviceKey); + if (ids.length === 0) return; - const ids = removeList.map((x) => x.deviceKey); const removeOne = (id: number) => ExperimentService.delete(id).then((res) => { if (res.status < 200 || res.status >= 300) return Promise.reject(res); @@ -353,27 +256,20 @@ const removeData = (value?: Array<{ deviceKey: number }>) => { 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) { removeOne(ids[0]) - .then(() => { + .then(() => store.setSnackbarMsg({ color: "success", text: "삭제되었습니다.", result: 200, - }); - after(); - }) + }), + ) .catch((err) => { console.error("삭제 실패:", err); store.setSnackbarMsg({ @@ -381,16 +277,17 @@ const removeData = (value?: Array<{ deviceKey: number }>) => { text: "삭제 실패", result: 500, }); - }); + }) + .finally(after); } else { Promise.all(ids.map(removeOne)) - .then(() => { + .then(() => store.setSnackbarMsg({ color: "success", text: "모두 삭제되었습니다.", result: 200, - }); - }) + }), + ) .catch((err) => { console.error("일부 삭제 실패:", err); store.setSnackbarMsg({ @@ -403,29 +300,15 @@ const removeData = (value?: Array<{ deviceKey: number }>) => { } }; -// ===== 상세 & 생성 모달 (기존 그대로) ===== -const closeDetail = () => { - openView.value = false; - selectedExperiment.value = null; -}; +// 상세 모달 const openInfoModal = (item: any) => { execSelected.value = item; - console.log("[Parent] 선택된 실행:", item); openView.value = true; - openCompare.value = false; }; function closeView() { openView.value = false; } 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 = () => { data.value.modalMode = "create"; data.value.selectedData = { @@ -534,12 +417,6 @@ onMounted(() => { - - @@ -586,10 +463,28 @@ onMounted(() => { mdi-close-circle - mdi-loading + + + + mdi-loading + + + + mdi-help-circle {{ item.duration }} {{ item.experiment }} + {{ item.workflow }} {{ item.startTime }} {{ item.registryStatus }} @@ -640,7 +535,7 @@ onMounted(() => { diff --git a/src/components/templates/run/executions/ViewComponent.vue b/src/components/templates/run/executions/ViewComponent.vue index a65bba9..258a75d 100644 --- a/src/components/templates/run/executions/ViewComponent.vue +++ b/src/components/templates/run/executions/ViewComponent.vue @@ -92,11 +92,6 @@ const segWidthPct = () => 100 / (nSteps - 1); // 유틸 const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—"); - -// 콘솔로 확인 -onMounted(() => { - console.log("[Child] 받은 데이터:", props.experimentInfo); -}); diff --git a/src/components/templates/run/experiment/ViewComponent.vue b/src/components/templates/run/experiment/ViewComponent.vue index 80ab707..08080cc 100644 --- a/src/components/templates/run/experiment/ViewComponent.vue +++ b/src/components/templates/run/experiment/ViewComponent.vue @@ -3,7 +3,7 @@ import { ref, computed, onMounted, watch } from "vue"; import { ExperimentService } from "@/components/service/management/ExperimentService"; 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 loading = ref(false); @@ -19,27 +19,67 @@ const experimentInfo = ref({ mlFlowId: "-", }); -const formatIso = (s?: string) => - s ? String(s).replace("T", " ").slice(0, 19) : "-"; +function formatIso(s?: string) { + return s ? String(s).replace("T", " ").slice(0, 19) : "-"; +} -const mapToViewModel = (raw: any) => ({ - experimentName: raw.displayName ?? raw.name ?? "-", - projectName: "-", - createdDate: formatIso(raw.lastUpdateTime), - createdId: raw.regUserId ?? "-", - description: raw.description ?? "-", - kubeFlowId: raw.kubeFlowId ?? "-", - mlFlowId: raw.mlFlowId ?? "-", -}); +function mapToViewModel(raw: any) { + const hasRaw = !!raw; + const created = hasRaw && (raw.createdAt || raw.lastUpdateTime); + const createdId = + (hasRaw && (raw.regUserId || raw.createdBy || raw.serviceAccount)) || "-"; + + return { + 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 || {})); +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) { - if (!projectId && projectId !== 0) return; + if (projectId === undefined || projectId === null) return; try { const res = await ProjectService.fetchProjectById(projectId as number); - const prj = res?.data ?? res; - experimentInfo.value.projectName = prj?.prjNm ?? prj?.name ?? "-"; + const prj = res && res.data ? res.data : res; + const name = prj && (prj.prjNm || prj.name) ? prj.prjNm || prj.name : "-"; + experimentInfo.value.projectName = name; } catch (e) { console.warn("[Experiment/View] project fetch fail:", e); } @@ -52,13 +92,20 @@ async function fetchDetail(id: number | string) { loading.value = true; try { 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); experimentInfo.value = { ...experimentInfo.value, ...vm }; - // 프로젝트명 별도 조회 - await fetchProjectName(detailRaw.value?.projectId); + const hasProjectId = + detailRaw.value && + detailRaw.value.projectId !== undefined && + detailRaw.value.projectId !== null; + + if (hasProjectId) { + await fetchProjectName(Number(detailRaw.value.projectId)); + } } catch (e) { console.error("[Experiment/View] fetch detail error:", e); } 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( - () => props.id, - (nv) => { - if (nv !== undefined && nv !== null && nv !== "") fetchDetail(nv); + () => props.experimentInfo, + function (nv) { + if (nv === null || nv === undefined || nv === "") return; + if (typeof nv === "object") { + bindFromProp(); + } else { + fetchDetail(nv as any); + } }, ); diff --git a/src/components/templates/stepconfig/ViewComponent.vue b/src/components/templates/stepconfig/ViewComponent.vue index d3ecb4b..582cffd 100644 --- a/src/components/templates/stepconfig/ViewComponent.vue +++ b/src/components/templates/stepconfig/ViewComponent.vue @@ -74,9 +74,6 @@ async function fetchDetail(id: number | string) { // ⬇️ 여기서 이름 해석 await resolveWorkflowName(raw); - - console.log("[ViewComponent] raw:", raw); - console.log("[ViewComponent] info:", info.value); } catch (e) { console.error("[ViewComponent] fetch detail error:", e); } finally { diff --git a/src/components/templates/trainingscript/ListComponent.vue b/src/components/templates/trainingscript/ListComponent.vue index 77e737e..f132052 100644 --- a/src/components/templates/trainingscript/ListComponent.vue +++ b/src/components/templates/trainingscript/ListComponent.vue @@ -2,13 +2,18 @@ import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.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 ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue"; import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue"; import { AttachmentsService } from "@/components/service/management/AttachmentsService"; import { commonStore } from "@/stores/commonStore"; +import { useRoute, useRouter } from "vue-router"; +const route = useRoute(); +const router = useRouter(); +const activeRefId = ref(null); +const activeRefName = computed(() => String(route.query.refName ?? "")); const store = commonStore(); const openView = ref(false); const openModify = ref(false); @@ -68,6 +73,12 @@ const data = ref({ 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 { try { @@ -139,6 +150,7 @@ const fetchList = async () => { sortField: "id", sortDirection: "DESC", refType: "TRAINING_SCRIPT", + refId: activeRefId.value, }; try { @@ -286,6 +298,7 @@ const openCreateModal = () => { data.value.selectedData = { username: username.value, projectId: getProjectId(), + refId: activeRefId.value, }; data.value.isCreateVisible = true; }; @@ -318,8 +331,17 @@ const getSelectedAllData = () => { }; onMounted(() => { username.value = readUsernameFromStorage(); + activeRefId.value = getRefIdFromRoute(route.query); fetchList(); }); +watch( + () => route.query.refId, + () => { + activeRefId.value = getRefIdFromRoute(route.query); + data.value.params.pageNum = 1; + fetchList(); + }, +); @@ -413,6 +435,26 @@ onMounted(() => { /> + + + Filter: DataGroup {{ activeRefName }} + + + 필터 해제 + + diff --git a/src/components/templates/trainingscriptgroup/ListComponent.vue b/src/components/templates/trainingscriptgroup/ListComponent.vue new file mode 100644 index 0000000..bdaff10 --- /dev/null +++ b/src/components/templates/trainingscriptgroup/ListComponent.vue @@ -0,0 +1,570 @@ + + + + + + + + + + TrainingScriptGroup + + + + + + + + + + + + + + + + + + + mdi-magnify + + + + + + + + + + 총 {{ data.totalElements.toLocaleString() }}개 + + + + + + + + + Create DataGroup + + + + + + + + + + + + + + + {{ h.label }} + + + + + + {{ row.no }} + {{ row.name }} + {{ row.description }} + {{ row.author || "-" }} + {{ formatDateTime(row.registDt) }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/templates/workflow/ListComponent.vue b/src/components/templates/workflow/ListComponent.vue index 8005e61..6c9c5f1 100644 --- a/src/components/templates/workflow/ListComponent.vue +++ b/src/components/templates/workflow/ListComponent.vue @@ -25,13 +25,13 @@ const isRunVisible = ref(false); const selectedRun = ref(null); const tableHeader = [ - { label: "No", width: "5%", style: "word-break: keep-all;" }, - { label: "Workflow Name", width: "18%", style: "word-break: keep-all;" }, - { label: "Description", width: "28%", style: "word-break: keep-all;" }, - { label: "Version", width: "10%", style: "word-break: keep-all;" }, - { label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" }, - { label: "Created DateTime", width: "15%", style: "word-break: keep-all;" }, - { label: "Action", width: "12%", style: "word-break: keep-all;" }, + { label: "No", width: "6%", style: "white-space: nowrap;" }, + { label: "Workflow Name", width: "22%", style: "white-space: nowrap;" }, + { label: "Description", width: "30%", style: "white-space: nowrap;" }, + { label: "Version", width: "10%", style: "white-space: nowrap;" }, + { label: "Kubeflow Status", width: "12%", style: "white-space: nowrap;" }, + { label: "Created DateTime", width: "12%", style: "white-space: nowrap;" }, + { label: "Action", width: "8%", style: "white-space: nowrap;" }, ]; const searchOptions = [ { label: "전체", value: "전체" as SearchType }, @@ -125,6 +125,7 @@ const fetchList = () => { if (res.status !== 200) return; const result = res.data; + console.log("Workflows", result); let list = result?.content ?? []; if (needLocalFilter) { @@ -447,7 +448,6 @@ onMounted(() => { overflow-x-auto > - { - - - { :key="i" class="text-center" > - - - {{ item.no }} {{ item.name }} {{ item.description }} diff --git a/src/pages/Datagroup.vue b/src/pages/DatagroupView.vue similarity index 100% rename from src/pages/Datagroup.vue rename to src/pages/DatagroupView.vue diff --git a/src/pages/SignupView.vue b/src/pages/SignupView.vue index 4fb0039..f58c7c1 100644 --- a/src/pages/SignupView.vue +++ b/src/pages/SignupView.vue @@ -47,7 +47,6 @@ const signUp = () => { role: data.value.role ? [data.value.role] : [], password: data.value.password, }; - console.log("회원가입 호출 payload:", payload); UserManagerService.signUp(payload) .then((res) => { diff --git a/src/pages/TrainingscriptgroupView.vue b/src/pages/TrainingscriptgroupView.vue new file mode 100644 index 0000000..c9e08de --- /dev/null +++ b/src/pages/TrainingscriptgroupView.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/router/index.js b/src/router/index.js index 4f29c50..8ef6ab1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -44,7 +44,13 @@ const routes = [ name: "dataGroup", path: "/datagroup", 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", diff --git a/src/utils/menuUtils.js b/src/utils/menuUtils.js index c6a6373..2e5b344 100644 --- a/src/utils/menuUtils.js +++ b/src/utils/menuUtils.js @@ -12,12 +12,7 @@ export const menuUtils = { value: "workflows", icon: "mdi-code-braces", }, - { - title: "DataGroup", - path: "/DataGroup", - value: "DataGroup", - icon: "mdi-hammer-wrench", - }, + { title: "Run", path: "/run", @@ -34,19 +29,31 @@ export const menuUtils = { value: "deployment", icon: "mdi-folder-search", }, - ], - adminMenuItem: [ { - title: "Training Script", - path: "/training-script", - value: "training-script", + title: "TrainingScriptGroup", + path: "/TrainingScriptGroup", + value: "TrainingScriptGroup", icon: "mdi-file-code-outline", }, { - title: "Datasets", - path: "/datasets", - value: "datasets", + title: "DataSetGroup", + path: "/DataGroup", + value: "DataGroup", 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", + // }, + ], }; diff --git a/typed-router.d.ts b/typed-router.d.ts index 2232ca6..116a2e4 100644 --- a/typed-router.d.ts +++ b/typed-router.d.ts @@ -19,7 +19,7 @@ declare module 'vue-router/auto-routes' { */ export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, - '/Datagroup': RouteRecordInfo<'/Datagroup', '/Datagroup', Record, Record>, + '/DatagroupView': RouteRecordInfo<'/DatagroupView', '/DatagroupView', Record, Record>, '/DatasetView': RouteRecordInfo<'/DatasetView', '/DatasetView', Record, Record>, '/DeploymentView': RouteRecordInfo<'/DeploymentView', '/DeploymentView', Record, Record>, '/ExecutionsView': RouteRecordInfo<'/ExecutionsView', '/ExecutionsView', Record, Record>, @@ -29,6 +29,7 @@ declare module 'vue-router/auto-routes' { '/MainView': RouteRecordInfo<'/MainView', '/MainView', Record, Record>, '/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record, Record>, '/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record, Record>, + '/TrainingscriptgroupView': RouteRecordInfo<'/TrainingscriptgroupView', '/TrainingscriptgroupView', Record, Record>, '/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record, Record>, '/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record, Record>, '/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record, Record>,