From f4ea2f30b73524e1ef55c7970971148772381a12 Mon Sep 17 00:00:00 2001 From: jschoi Date: Thu, 21 Aug 2025 19:39:54 +0900 Subject: [PATCH] =?UTF-8?q?Workflow=20API=20=EC=B6=94=EA=B0=80(=EC=88=98?= =?UTF-8?q?=EC=A0=95=20x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/organisms/WorkflowsCreateDialog.vue | 59 +++- src/components/common/LayoutComponent.vue | 21 +- src/components/models/management/Autoflow.ts | 8 + src/components/service/index.ts | 9 - .../service/management/AutoflowService.ts | 12 + .../service/management/userManagerService.ts | 2 +- src/components/workflow/ListComponent.vue | 259 +++++++++--------- src/pages/LoginView.vue | 8 +- src/stores/autoflowStore.ts | 15 + src/utils/CommonUtils.ts | 96 +++++++ src/views/Select.vue | 10 +- vite.config.mjs | 2 +- 12 files changed, 335 insertions(+), 166 deletions(-) create mode 100644 src/components/models/management/Autoflow.ts create mode 100644 src/components/service/management/AutoflowService.ts create mode 100644 src/stores/autoflowStore.ts create mode 100644 src/utils/CommonUtils.ts diff --git a/src/components/atoms/organisms/WorkflowsCreateDialog.vue b/src/components/atoms/organisms/WorkflowsCreateDialog.vue index 21395c8..45e5c49 100644 --- a/src/components/atoms/organisms/WorkflowsCreateDialog.vue +++ b/src/components/atoms/organisms/WorkflowsCreateDialog.vue @@ -4,7 +4,11 @@ import IconArrowUp from "@/components/button/IconArrowUp.vue"; import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import { ref, watch } from "vue"; - +import { AutoflowService } from "@/components/service/management/AutoflowService"; +import { storage } from "@/utils/storage"; +import { Workflow } from "@/components/models/management/Autoflow"; +const saving = ref(false); +const errorMsg = ref(""); const steps = ref([ { order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" }, { @@ -27,7 +31,7 @@ const props = defineProps({ userOption: Array, }); -const emit = defineEmits(["handle-data", "close-modal"]); +const emit = defineEmits(["handle-data", "close-modal", "saved"]); const visible = ref(true); @@ -36,8 +40,55 @@ const form = ref({ description: "", }); -const submit = () => { - emit("handle-data", form.value); +const nowLocalIso = (): string => { + const t = new Date(Date.now() - new Date().getTimezoneOffset() * 60000); + return t.toISOString().slice(0, 23); // 밀리초까지 포함, 끝의 'Z' 제거 +}; +const now = nowLocalIso(); +const submit = async () => { + errorMsg.value = ""; + if (!form.value.name.trim()) { + errorMsg.value = "Workflow Name은 필수입니다."; + return; + } + + const authObj = + (typeof storage?.getAuth === "function" ? storage.getAuth() : null) ?? + JSON.parse(localStorage.getItem("autoflow-auth") || "{}"); + + // 2) username 추출 (userinfo / userInfo 모두 대응) + const regUserId = + authObj?.userInfo?.username ?? + authObj?.userinfo?.username ?? + authObj?.username ?? + authObj?.userId ?? + ""; + + if (!regUserId) { + errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다."; + return; + } + + const payload: Workflow = { + workflowName: form.value.name.trim(), + workflowDescription: form.value.description?.trim() || "", + uploadYn: "Y", + regUserId, + regDt: now, + modDt: now, + }; + + try { + saving.value = true; + const { data } = await AutoflowService.add(payload); + emit("saved", data); + emit("close-modal"); + } catch (e) { + console.error("워크플로우 저장 실패:", e); + errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요."; + } finally { + saving.value = false; + } }; diff --git a/src/components/common/LayoutComponent.vue b/src/components/common/LayoutComponent.vue index 8529562..6c95356 100644 --- a/src/components/common/LayoutComponent.vue +++ b/src/components/common/LayoutComponent.vue @@ -5,6 +5,15 @@ import DrawerComponent from "@/components/common/DrawerComponent.vue"; import { watchEffect } from "vue"; import Select from "@/views/Select.vue"; import { UserManagerService } from "@/components/service/management/userManagerService"; + +import { storeToRefs } from "pinia"; +import { useAutoflowStore } from "@/stores/autoflowStore"; + +const autoflow = useAutoflowStore(); +const { ProjectName } = storeToRefs(autoflow); +const showPasswordModal = ref(false); +const selectedUserData = ref({}); + const route = useRoute(); const router = useRouter(); @@ -62,15 +71,12 @@ const goSelect = () => { }; const logOut = () => { - // 1) 서버에 로그아웃 API 호출 UserManagerService.signOut() .catch((err) => { - // 403 Forbidden 이든 네트워크 에러든, - // 일단 콘솔에 찍어 보고 console.error("logout API failed:", err); }) .finally(() => { - // 2) 로컬 토큰 삭제 & 로그인 페이지 이동 + autoflow.setProjectName(""); storage.clearAuth(); router.push("/login"); }); @@ -79,9 +85,6 @@ const goHome = () => { router.push("/main"); }; -const showPasswordModal = ref(false); -const selectedUserData = ref({}); - watchEffect(() => { // const auth = storage.getAuth().auth; // if (auth === "ADMIN") { @@ -130,7 +133,9 @@ watchEffect(() => {
ADMIN_001
-
No Project Selected
+
+ {{ ProjectName || "No Project Selected" }} +
mdi-arrow-down-drop-circle-outline diff --git a/src/components/models/management/Autoflow.ts b/src/components/models/management/Autoflow.ts new file mode 100644 index 0000000..8fc7695 --- /dev/null +++ b/src/components/models/management/Autoflow.ts @@ -0,0 +1,8 @@ +export interface Workflow { + workflowName: string; + workflowDescription?: string; + uploadYn: "Y" | "N"; + regUserId: string; + regDt: string; + modDt: string; +} diff --git a/src/components/service/index.ts b/src/components/service/index.ts index 7d677ea..d923c50 100644 --- a/src/components/service/index.ts +++ b/src/components/service/index.ts @@ -74,17 +74,8 @@ export const request = { }); }, postNoParam: (uri: string): any => { - // axios.defaults.withCredentials=true 이 켜져 있으니 - // Set-Cookie 헤더로 내려오는 쿠키를 자동 저장합니다. return axios.post(`${API_URL}${uri}`, null); }, - // postOptimization: (uri: string, param: any): any => { - // return axios.post(`${PYTHON_API_URL}${uri}`, param); - // }, - - // postPython: (uri: string, param: any): Promise => { - // return axios.post(`${PYTHON_API_URL}${uri}`, param); - // }, }; // axios.defaults.withCredentials = true; diff --git a/src/components/service/management/AutoflowService.ts b/src/components/service/management/AutoflowService.ts new file mode 100644 index 0000000..e677e18 --- /dev/null +++ b/src/components/service/management/AutoflowService.ts @@ -0,0 +1,12 @@ +import { Workflow } from "@/components/models/management/Autoflow"; +import { request } from "@/components/service/index"; +export const AutoflowService = { + add: (payload: Workflow) => { + return request.post("/api/workflows", payload); + }, + getAll: () => request.get("/api/workflows", {}), + + delete: (id: Number) => { + return request.delete(`/api/workflows/${id}`, {}); + }, +}; diff --git a/src/components/service/management/userManagerService.ts b/src/components/service/management/userManagerService.ts index 4b1bae9..51407e4 100644 --- a/src/components/service/management/userManagerService.ts +++ b/src/components/service/management/userManagerService.ts @@ -22,6 +22,6 @@ export const UserManagerService = { return request.post("/management/user/update", param); }, delete: (param: User) => { - return request.post("/management/user/delete", param); + return request.delete("/management/user/delete", param); }, }; diff --git a/src/components/workflow/ListComponent.vue b/src/components/workflow/ListComponent.vue index a0beba8..22e46ef 100644 --- a/src/components/workflow/ListComponent.vue +++ b/src/components/workflow/ListComponent.vue @@ -7,7 +7,9 @@ import { onMounted, ref, watch } from "vue"; import ViewComponent from "@/components/workflow/ViewComponent.vue"; import WorkflowsCreateDialog from "@/components/atoms/organisms/WorkflowsCreateDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; -// const store = commonStore(); +import { AutoflowService } from "../service/management/AutoflowService"; +import { commonStore } from "@/stores/commonStore"; +const store = commonStore(); const openView = ref(false); const openModify = ref(false); @@ -120,91 +122,56 @@ const getCodeList = () => { // }); }; +const toRow = (w: any, i: number, offset: number) => ({ + no: offset + i + 1, + name: w.workflowName, + version: w.version ?? "v1.0", + stepCount: w.stepCount ?? w.steps?.length ?? 0, + configProgress: `${w.configDone ?? 0}/${w.configTotal ?? w.steps?.length ?? 0}`, + kubeflowStatus: w.uploadYn === "Y" ? "Uploaded" : "Not Uploaded", + registDt: w.regDt ?? w.regDate ?? "-", + deviceKey: w.id ?? w.workflowId ?? offset + i, +}); + const getData = () => { - const params = { ...data.value.params }; - if (params.searchType === "" || params.searchText === "") { - delete params.searchType; - delete params.searchText; - } - data.value.results = [ - { - no: 5, - name: "sentiment-analysis", - version: "v2.0", - stepCount: 2, - configProgress: "0/2", - kubeflowStatus: "Not Uploaded", - registDt: "2025-06-10T00:00:00Z", - }, - { - no: 4, - name: "image-classfier", - version: "v2.0", - stepCount: 3, - configProgress: "1/3", - kubeflowStatus: "Not Uploaded", - registDt: "2025-06-09T00:00:00Z", - }, - { - no: 3, - name: "user-clustering", - version: "v1.0", - stepCount: 3, - configProgress: "0/3", - kubeflowStatus: "Not Uploaded", - registDt: "2025-06-01T00:00:00Z", - }, - { - no: 2, - name: "time-series-train", - version: "v1.0", - stepCount: 2, - configProgress: "1/3", - kubeflowStatus: "Not Uploaded", - registDt: "2025-05-29T00:00:00Z", - }, - { - no: 1, - name: "customer-churn-pred", - version: "v1.0", - stepCount: 3, - configProgress: "0/3", - kubeflowStatus: "Not Uploaded", - registDt: "2025-05-31T00:00:00Z", - }, - ]; - data.value.totalDataLength = 5; - // DeviceService.search(params).then((d) => { - // if (d.status === 200) { - // data.value.results = d.data.deviceList; - // data.value.totalDataLength = d.data.totalCount; - // setTimeout(() => { - // setPaginationLength(); - // }, 200); - // } else { - // store.setSnackbarMsg({ - // text: "디바이스 조회 실패", - // color: "error", - // }); - // } - // }); - // DeviceService.search().then((d) => { - // data.value.totalDataLength = d.data.totalCount; - // setTimeout(() => { - // setPaginationLength(); - // }, 200); - // }); -}; + const params = data.value.params; + const pageNum = params.pageNum; + const pageSize = params.pageSize; + const startIndex = (pageNum - 1) * pageSize; + AutoflowService.getAll() + .then((res) => { + if (res.status !== 200) { + console.error("워크플로우 조회 실패:", res); + + return; + } + const result = res.data; + console.log(result); + const rawList = Array.isArray(result) ? result : (result.content ?? []); + const totalCount = Array.isArray(result) + ? rawList.length + : (result.totalElements ?? rawList.length); + const currentPageList = Array.isArray(result) + ? rawList.slice(startIndex, startIndex + pageSize) + : rawList; + + data.value.results = currentPageList.map((w: any, i: number) => + toRow(w, i, startIndex), + ); + data.value.totalDataLength = totalCount; + + setPaginationLength(); + }) + .catch((err) => { + console.error("워크플로우 조회 에러:", err); + }); +}; const setPaginationLength = () => { - if (data.value.totalDataLength % data.value.params.pageSize === 0) { - data.value.pageLength = - data.value.totalDataLength / data.value.params.pageSize; - } else { - data.value.pageLength = Math.ceil( - data.value.totalDataLength / data.value.params.pageSize, - ); - } + const total = data.value.totalDataLength || 0; + const pageSize = data.value.params.pageSize || 10; + data.value.pageLength = + total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize); }; const saveData = (formData) => { @@ -244,42 +211,68 @@ const saveData = (formData) => { }; const removeData = (value) => { - let removeList = value ? value : data.value.selected; - const remove = (code) => { - // return DeviceService.delete(code).then((d) => { - // if (d.status !== 200) { - // store.setSnackbarMsg({ - // text: d, - // result: 500, - // }); - // } - // }); - }; + const removeList = value ?? data.value.selected; + if (!removeList || removeList.length === 0) return; - if (removeList.length === 1) { - remove(removeList[0].deviceKey).then(() => { - // store.setSnackbarMsg({ - // text: "삭제되었습니다.", - // result: 200, - // }); - changePageNum(); - data.value.isConfirmDialogVisible = false; - data.value.selected = []; - data.value.allSelected = false; + const ids = removeList.map((x) => x.deviceKey); + + const remove = (id) => + AutoflowService.delete(id).then((res) => { + 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; + } + getData(); + + data.value.isConfirmDialogVisible = false; + data.value.selected = []; + data.value.allSelected = false; + }; + + if (ids.length === 1) { + remove(ids[0]) + .then(() => { + store.setSnackbarMsg({ + color: "success", + text: "삭제되었습니다.", + result: 200, + }); + after(); + }) + .catch((err) => { + store.setSnackbarMsg({ + color: "warning", + text: "삭제 실패", + result: 500, + }); + console.error(err); + }); } else { - Promise.all(removeList.map((item) => remove(item.deviceKey))).finally( - () => { - // store.setSnackbarMsg({ - // text: "모두 삭제되었습니다.", - // result: 200, - // }); - changePageNum(); - data.value.isConfirmDialogVisible = false; - data.value.selected = []; - data.value.allSelected = false; - }, - ); + Promise.all(ids.map(remove)) + .then(() => { + store.setSnackbarMsg({ + color: "success", + text: "모두 삭제되었습니다.", + result: 200, + }); + }) + .catch((err) => { + store.setSnackbarMsg({ + color: "warning", + text: "일부 삭제 실패", + result: 500, + }); + console.error(err); + }) + .finally(after); } }; @@ -331,11 +324,11 @@ const openUploadModal = () => { }; const closeCreateModal = () => { data.value.isModalVisible = false; - data.value.isCreateVisible = null; + data.value.isCreateVisible = false; }; const closeUploadModal = () => { data.value.isModalVisible = false; - data.value.isUploadVisible = null; + data.value.isUploadVisible = false; }; const getSelectedAllData = () => { @@ -347,7 +340,19 @@ const getSelectedAllData = () => { }) : []; }; +watch( + () => data.value.isCreateVisible, + (now, prev) => { + if (prev && !now) getData(); + }, +); +watch( + () => data.value.isUploadVisible, + (now, prev) => { + if (prev && !now) getData(); + }, +); onMounted(() => { getData(); getCodeList(); @@ -356,22 +361,6 @@ onMounted(() => {