feat: 유저 페이지, 관리자 페이지 분리 및 Step 컴포넌트 api 추가

main
jschoi 9 months ago
parent d2f7ae15c5
commit c254117106

3
components.d.ts vendored

@ -28,12 +28,15 @@ declare module 'vue' {
ListComponent: typeof import('./src/components/home/ListComponent.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default']
StapComfigDialog: typeof import('./src/components/atoms/organisms/StapComfigDialog.vue')['default']
StepComfigDialog: typeof import('./src/components/atoms/organisms/StepComfigDialog.vue')['default']
TrainingScriptBaseDoalog: typeof import('./src/components/atoms/organisms/TrainingScriptBaseDoalog.vue')['default']
ViewComponent: typeof import('./src/components/templates/Datasets/ViewComponent.vue')['default']
WorkflowDialog: typeof import('./src/components/atoms/organisms/WorkflowDialog.vue')['default']
WorkflowsBaseDialog: typeof import('./src/components/atoms/organisms/WorkflowsBaseDialog.vue')['default']
WorkflowsCreateDialog: typeof import('./src/components/atoms/organisms/WorkflowsCreateDialog.vue')['default']
WorkflowsUploadDialog: typeof import('./src/components/atoms/organisms/WorkflowsUploadDialog.vue')['default']
WorklfowStepBaseDialog: typeof import('./src/components/atoms/organisms/WorklfowStepBaseDialog.vue')['default']
}
}

@ -1,99 +0,0 @@
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from "vue";
// Props
const props = defineProps<{
modelValue: boolean;
selectedData: { workflow: string; stepName: string } | null;
workflowList: string[];
}>();
// v-model save Emit
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "save", payload: { workflow: string; stepName: string }): void;
}>();
//
const internalWorkflow = ref(props.selectedData?.workflow || "");
const internalStepName = ref(props.selectedData?.stepName || "");
//
watch(
() => props.modelValue,
(open) => {
if (open && props.selectedData) {
internalWorkflow.value = props.selectedData.workflow;
internalStepName.value = props.selectedData.stepName;
}
},
);
// Save
const onSave = () => {
emit("save", {
workflow: internalWorkflow.value,
stepName: internalStepName.value,
});
emit("update:modelValue", false);
};
// Close
const onClose = () => {
emit("update:modelValue", false);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>Edit Workflow Step Config</v-card-title
>
<!-- 본문 -->
<v-card-text class="pt-6 px-6 pb-4">
<!-- Select Workflow -->
<v-row class="mb-6" dense>
<v-col cols="12" sm="4">
<div class="font-weight-medium white--text">Select Workflow</div>
</v-col>
<v-col cols="12">
<v-select
v-model="internalWorkflow"
:items="workflowList"
dense
hide-details
placeholder="Select Workflow"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Workflow Step Name -->
<v-row class="mb-6" dense>
<v-col cols="12">
<div class="font-weight-medium white--text mb-2">
Workflow Step Name
</div>
<v-text-field
v-model="internalStepName"
dense
hide-details
placeholder="Enter Workflow Step"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
</v-card-text>
<!-- 액션 버튼 -->
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="onSave">Save</v-btn>
<v-btn text class="white--text" @click="onClose">Close</v-btn>
</v-card-actions>
</v-card>
</template>

@ -4,9 +4,9 @@ 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 { AutoflowService } from "@/components/service/management/AutoflowService";
import { WorkflowService } from "@/components/service/management/workflowService";
import { storage } from "@/utils/storage";
import type { Workflow } from "@/components/models/management/Autoflow";
import type { Workflow } from "@/components/models/management/Workflow";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
@ -122,12 +122,12 @@ async function submit() {
return;
}
const { data } = await AutoflowService.update(id, payload);
const { data } = await WorkflowService.update(id, payload);
emit("saved", data);
emit("close-modal");
} else {
//
const { data } = await AutoflowService.add(payload);
const { data } = await WorkflowService.add(payload);
emit("saved", data);
emit("close-modal");
}

@ -0,0 +1,207 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { storage } from "@/utils/storage";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
import type { AxiosError } from "axios";
import { WorkflowStepService } from "@/components/service/management/workflowStepService";
const props = defineProps<{
editData: any;
mode: "create" | "edit";
}>();
const emit = defineEmits<{
(e: "close-modal"): void;
(e: "saved", value: any): void;
}>();
const isEdit = computed(() => props.mode === "edit");
const { projectId } = storeToRefs(useAutoflowStore());
const saving = ref(false);
const errorMsg = ref("");
// ( UI: stepName, status)
type StepStatus = "Running" | "Success" | "Fail";
const form = ref({
stepName: "",
status: "Running" as StepStatus,
});
function hydrateFormFromEdit(data: any) {
if (!data) return;
form.value.stepName = data?.stepName ?? data?.name ?? "";
form.value.status = (data?.status as StepStatus) ?? "Running";
}
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
watch(
() => props.editData,
(v) => {
if (isEdit.value) hydrateFormFromEdit(v);
},
);
const nowLocalIso = (): string => {
const t = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
return t.toISOString().slice(0, 23); // 'YYYY-MM-DDTHH:mm:ss.SSS'
};
const regUserId = (() => {
try {
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
return (
authObj?.userInfo?.username ??
authObj?.userinfo?.username ??
authObj?.username ??
authObj?.userId ??
""
);
} catch {
return "";
}
})();
async function submit() {
errorMsg.value = "";
//
const stepName = form.value.stepName.trim();
if (!stepName) {
errorMsg.value = "Step Name은 필수입니다.";
return;
}
if (!regUserId) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
if (!projectId.value) {
errorMsg.value = "프로젝트가 선택되지 않았습니다.";
return;
}
// payload
const now = nowLocalIso();
const payload = {
stepName,
status: form.value.status, // not null
regUserId, // not null
regDt: now, // not null ( )
version: 1, // not null ()
projectId: projectId.value, // not null
// (pipelineId = /)
// pipelineId: undefined,
// startTime: undefined,
// endTime: undefined,
// logPath: undefined,
};
try {
saving.value = true;
if (isEdit.value) {
// : id ( deviceKey id )
const rawId = props.editData?.id ?? props.editData?.deviceKey;
const id = Number(rawId);
if (!id) {
errorMsg.value = "수정할 ID가 없습니다.";
return;
}
const { data } = await WorkflowStepService.update(id, payload as any);
emit("saved", data);
emit("close-modal");
} else {
const { data } = await WorkflowStepService.add(payload as any);
emit("saved", data);
emit("close-modal");
}
} catch (e) {
const err = e as AxiosError;
console.error("워크플로우 스텝 저장 실패:", err);
errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요.";
} 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 Workflow Step" : "Create Workflow Step" }}
</v-card-title>
<!-- 영역 -->
<v-card-text class="pa-6">
<div class="text-subtitle-1 font-weight-medium mb-4">
Workflow Step Information
</div>
<v-form @submit.prevent="submit">
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block">
Step Name
</label>
<v-text-field
v-model="form.stepName"
variant="outlined"
:disabled="saving"
dense
hide-details
required
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block">
Status
</label>
<v-select
v-model="form.status"
:items="['Running', 'Success', 'Fail']"
variant="outlined"
:disabled="saving"
dense
hide-details
/>
</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>

@ -3,16 +3,21 @@ import { ref, onMounted, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { menuUtils } from "@/utils/menuUtils";
import { storage } from "@/utils/storage";
import logo from "@/assets/iteration (1).png";
import SidebarHeader from "@/components/common/SidebarHeader.vue";
const route = useRoute();
const router = useRouter();
const isAdminRoute = computed(() =>
route.matched.some((r) => r.meta?.requiresAdmin),
);
const menuItems = computed(() =>
isAdminRoute.value ? menuUtils.adminMenuItem : menuUtils.menuItem,
);
const isShowAuth = ref(false);
function readRolesFromStorage(): string[] {
try {
// storage.get(...) ,
const raw =
storage.get?.("autoflow-auth") ??
localStorage.getItem("autoflow-auth") ??
@ -20,8 +25,6 @@ function readRolesFromStorage(): string[] {
const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
let roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
// "ROLE_USER,ROLE_ADMIN"
if (typeof roles === "string") {
roles = roles.split(",").map((s: string) => s.trim());
}
@ -33,7 +36,6 @@ function readRolesFromStorage(): string[] {
}
}
// ADMIN (ROLE_ADMIN ADMIN )
const isAdmin = computed(() => {
const roles = readRolesFromStorage();
return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN");
@ -43,10 +45,6 @@ const isLinkActive = (link) => {
return route.path.includes(link);
};
const goMain = () => {
router.push("/home");
};
onMounted(() => {
isShowAuth.value = true;
//storage.getAuth().auth === "ADMIN";
@ -55,19 +53,6 @@ onMounted(() => {
<template>
<v-card flat class="mx-auto">
<v-card
:ripple="false"
flat
class="bg-shades-transparent font-weight-bold d-flex w-100 justify-center text-h5 pa-4 pb-16"
@click="goMain"
>
<div class="d-flex flex-column align-center pt-6">
<v-img :src="logo" width="auto" height="36" class="mb-3" />
<div class="text-subtitle-2 font-weight-medium text-primary">
Autoflow Web Console
</div>
</div>
</v-card>
<v-list nav class="pa-5 pt-0">
<template
v-for="({ title, value, icon, path, depth }, i) in menuUtils.menuItem"
@ -100,7 +85,7 @@ onMounted(() => {
</template>
<template v-else>
<v-list-item
v-if="value !== 'project' || isAdmin"
v-if="value !== 'project'"
rounded
:title="title"
:value="value"

@ -4,6 +4,7 @@ import { storage } from "@/utils/storage.js";
import DrawerComponent from "@/components/common/DrawerComponent.vue";
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { UserManagerService } from "@/components/service/management/userManagerService";
import SidebarHeader from "@/components/common/SidebarHeader.vue";
const route = useRoute();
const router = useRouter();
@ -15,7 +16,8 @@ const projectName = ref(localStorage.getItem("projectName") || "");
// Admin + Admin
// ----------------------
const isAdmin = ref(false); //
const adminMode = ref(false); // ( true)
const adminMode = ref(false); //
const lastNonAdminPath = ref("/home"); //
function computeIsAdmin() {
try {
@ -26,26 +28,25 @@ function computeIsAdmin() {
const roles = raw?.userInfo?.roles ?? raw?.roles ?? [];
const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth;
const inRoles = Array.isArray(roles)
? roles.includes("ROLE_ADMIN")
: roles === "ROLE_ADMIN";
isAdmin.value = inRoles || authCd === "ADMIN";
} catch {
isAdmin.value = false;
}
}
function enterAdmin() {
if (!isAdmin.value) return; //
adminMode.value = true; //
router.push("/project"); //
}
function exitAdmin() {
adminMode.value = false; //
//
// router.push("/home");
//
function toggleAdmin() {
if (!isAdmin.value) return;
if (adminMode.value) {
adminMode.value = false;
router.push(lastNonAdminPath.value || "/home");
} else {
adminMode.value = true;
if (!route.meta?.requiresAdmin) router.push("/project");
}
}
// ----------------------
@ -57,7 +58,7 @@ const menuItems = [
{
title: "Change Password",
click: () => {
/* 비밀번호 모달 열기 등 */
/* open modal */
},
},
{ title: "Logout", icon: "mdi-logout", click: () => logOut() },
@ -67,6 +68,13 @@ const drawer = ref(null);
const pageTitle = computed(() => route.meta.title);
const pagePath = computed(() => route.path);
// active
const isLinkActive = (link) => route.path.includes(link);
const settingsLabel = computed(() =>
adminMode.value ? "Back to Console" : "Settings",
);
function updateUsername() {
const auth = storage.getAuth?.() ?? null;
username.value = auth?.userInfo?.username ?? auth?.username ?? "";
@ -88,7 +96,7 @@ function logOut() {
username.value = "";
projectName.value = "";
sessionStorage.removeItem("initialRedirectDone");
adminMode.value = false; //
adminMode.value = false;
router.push("/login");
});
}
@ -101,8 +109,21 @@ function onStorage(e) {
computeIsAdmin();
}
}
const goMain = () => {
router.push("/home");
};
//
watch(
() => route.fullPath,
() => {
refreshProjectName();
const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home";
},
{ immediate: true },
);
// mount
onMounted(() => {
updateUsername();
computeIsAdmin();
@ -113,54 +134,67 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage);
});
//
watch(
() => route.fullPath,
() => refreshProjectName(),
);
</script>
<template>
<v-app>
<!-- 사이드바: adminMode에 따라 바꿔치기 -->
<!-- 사이드바 -->
<v-navigation-drawer
v-model="drawer"
border="0"
hide-overlay
permanent
v-if="!route.meta.hideSidebar"
:rail="false"
>
<!-- 헤더 -->
<v-card
:ripple="false"
flat
class="bg-shades-transparent d-flex w-100 justify-center text-h5 pa-4 pb-16"
@click="goMain"
>
<SidebarHeader />
</v-card>
<!-- 기본(일반 사용자) 메뉴 -->
<DrawerComponent v-if="!adminMode" />
<!-- 관리자 메뉴 -->
<!-- 관리자 메뉴: 유저 메뉴와 동일한 /여백/활성 스타일 -->
<template v-else>
<v-list nav density="compact" class="pt-6">
<v-list-subheader>Admin</v-list-subheader>
<v-card flat class="mx-auto">
<v-list nav class="pa-5 pt-0">
<v-list-item
rounded
title="Projects"
value="projects"
to="/project"
prepend-icon="mdi-briefcase"
title="Projects"
:active="isLinkActive('/project')"
:active-color="isLinkActive('/project') ? 'primary' : null"
density="compact"
class="pa-2 rounded-lg"
style="padding-inline-start: 10px"
/>
<v-list-item
rounded
title="Users"
value="users"
to="/users"
prepend-icon="mdi-account-multiple"
title="Users"
/>
<v-divider class="my-3" />
<v-list-item
prepend-icon="mdi-arrow-left-bold"
title="Back to Console"
@click="exitAdmin"
:active="isLinkActive('/users')"
:active-color="isLinkActive('/users') ? 'primary' : null"
density="compact"
class="pa-2 rounded-lg"
style="padding-inline-start: 10px"
/>
</v-list>
</v-card>
</template>
</v-navigation-drawer>
<v-app-bar class="bg-shades-transparent" flat>
<v-spacer />
<!-- 설정 버튼: 관리자에게만 보임 / 누르면 관리자 모드 진입 -->
<v-tooltip v-if="isAdmin" location="bottom" text="Settings">
<template #activator="{ props }">
<v-btn
@ -168,13 +202,15 @@ watch(
color="primary"
class="mr-3"
v-bind="props"
@click="enterAdmin"
@click="toggleAdmin"
aria-label="Settings"
>
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- 프로젝트 선택 -->
<v-tooltip location="bottom" text="Project">
<template #activator="{ props }">
<v-btn

@ -0,0 +1,12 @@
<script setup lang="ts">
import logo from "@/assets/iteration (1).png";
</script>
<template>
<div class="d-flex flex-column align-center pt-6">
<v-img :src="logo" width="auto" height="36" class="mb-3" />
<div class="text-subtitle-2 font-weight-medium text-primary">
Autoflow Web Console
</div>
</div>
</template>

@ -0,0 +1,21 @@
export type StepStatus = "Running" | "Success" | "Fail";
export interface WorkflowStep {
projectId: number;
stepName: string;
status?: StepStatus;
pipelineId?: number;
startTime?: string;
endTime?: string;
logPath?: string;
version?: string;
files?: Array<{
refType?: "workflow_step";
originalName: string;
storageName: string;
contentType?: string;
size?: number;
storagePath: string;
}>;
}

@ -1,20 +0,0 @@
import { Workflow } from "@/components/models/management/Autoflow";
import { request } from "@/components/service/index";
export const AutoflowStepService = {
add: (payload: Workflow) => {
return request.post("/api/workflow-steps", payload);
},
getAll: () => {
request.get("/api/workflow-steps", {});
},
delete: (id: Number) => {
return request.delete(`/api/workflow-steps${id}`, {});
},
view: (id: Number) => {
return request.get(`/api/workflow-steps${id}`, {});
},
update: (id: number, payload: Workflow) => {
return request.put(`/api/workflow-steps${id}`, payload);
},
};

@ -1,9 +1,9 @@
import {
Workflow,
WorkflowSearch,
} from "@/components/models/management/Autoflow";
} from "@/components/models/management/Workflow";
import { request } from "@/components/service/index";
export const AutoflowService = {
export const WorkflowService = {
add: (payload: Workflow) => {
return request.post("/api/workflows", payload);
},

@ -0,0 +1,23 @@
import { request } from "@/components/service/index";
import { WorkflowStep } from "@/components/models/management/WorkflowStep";
import { WorkflowSearch } from "@/components/models/management/Workflow";
export const WorkflowStepService = {
add: (payload: WorkflowStep) => {
return request.post("/api/workflow-steps", payload);
},
getAll: (params?: Record<string, any>) => {
return request.get("/api/workflow-steps", { params });
},
delete: (id: number) => {
return request.delete(`/api/workflow-steps/${id}`, {});
},
view: (id: number) => {
return request.get(`/api/workflow-steps/${id}`, {});
},
update: (id: number, payload: WorkflowStep) => {
return request.put(`/api/workflow-steps/${id}`, payload);
},
search: (payload: WorkflowSearch) => {
return request.get("/api/workflow-steps/search", payload);
},
};

@ -1,16 +1,20 @@
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import { commonStore } from "@/stores/commonStore";
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 FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/templates/stepconfig/ViewComponent.vue";
import StapComfigDialog from "@/components/atoms/organisms/StapComfigDialog.vue";
import { AutoflowStepService } from "@/components/service/management/AutoflowStepService";
// const store = commonStore();
import StapComfigDialog from "@/components/atoms/organisms/WorklfowStepBaseDialog.vue";
import { WorkflowStepService } from "@/components/service/management/workflowStepService";
import type { WorkflowStep } from "@/components/models/management/WorkflowStep";
const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
type SearchType = "전체" | "제목" | "작성자";
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Step Name", width: "15%", style: "word-break: keep-all;" },
@ -25,308 +29,279 @@ const tableHeader = [
];
const searchOptions = [
{ searchType: "전체", searchText: "" },
{ searchType: "디바이스 별칭", searchText: "deviceAlias" },
{ searchType: "디바이스 키", searchText: "deviceKey" },
{ searchType: "사용자", searchText: "userId" },
{ searchType: "디바이스 이름", searchText: "deviceName" },
{ searchType: "디바이스 모델", searchText: "deviceModel" },
{ searchType: "디바이스 OS", searchText: "deviceOs" },
{ label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType },
{ label: "작성자", value: "작성자" as SearchType },
];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const workflowList = ["pipeline-a", "pipeline-b", "pipeline-c"];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchType: "전체" as SearchType,
searchText: "",
},
results: [],
totalDataLength: 0,
results: [] as any[],
totalElements: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
modalMode: "" as "create" | "edit" | "",
selectedData: null as any,
isStepVisible: false,
allSelected: false,
selected: [],
isModalVisible: false,
selected: [] as Array<{ deviceKey: number }>,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const toRow = (s: any, no: number) => ({
no,
stepName: s.stepName ?? "-",
type: s.stepType ?? "-",
dataset: s.datasetName ?? "-",
script: s.scriptName ?? "-",
hyperParameters: s.hyperParams ?? "-",
resource: s.resource ?? "-",
status: (s.status ?? "").toLowerCase() === "success" ? "success" : "warning",
workflow: s.workflowName ?? s.pipelineId ?? "-",
deviceKey: s.id,
});
const getData = () => {
// : No 7 1
data.value.results = [
{
no: 7,
stepName: "Data Ingest",
type: "Preprocessing",
dataset: "raw_data",
script: "ingest.py",
hyperParameters: "-",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 7,
},
{
no: 6,
stepName: "Data Preprocess",
type: "Preprocessing",
dataset: "raw_data",
script: "preprocess.py",
hyperParameters: "normalize=True",
resource: "CPU:2, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 6,
},
{
no: 5,
stepName: "Model Training",
type: "Training",
dataset: "processed_data",
script: "train.py",
hyperParameters: "lr=0.01, epochs=10",
resource: "GPU:1, MEM:8Gi",
status: "warning",
workflow: "pipeline-a",
deviceKey: 5,
},
{
no: 4,
stepName: "Model Evaluation",
type: "Evaluation",
dataset: "test_data",
script: "evaluate.py",
hyperParameters: "-",
resource: "CPU:1, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 4,
},
{
no: 3,
stepName: "Model Validation",
type: "Validation",
dataset: "test_data",
script: "validate.py",
hyperParameters: "metrics=['accuracy']",
resource: "CPU:1, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 3,
},
{
no: 2,
stepName: "Package Model",
type: "Packaging",
dataset: "trained_model",
script: "package.py",
hyperParameters: "format='tar.gz'",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 2,
},
{
no: 1,
stepName: "Deploy",
type: "Deployment",
dataset: "package",
script: "deploy.py",
hyperParameters: "env='prod'",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 1,
},
];
data.value.totalDataLength = data.value.results.length;
//
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 fetchList = async () => {
const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) {
console.warn("[WorkflowSteps] 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;
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,
};
WorkflowStepService.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?.workflowName ?? "")
.toLowerCase()
.includes(kw),
);
} else if (mapped === "AUTHOR") {
list = list.filter((w: any) =>
String(w?.regUserId ?? "")
.toLowerCase()
.includes(kw),
);
}
};
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 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 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) =>
toRow(w, firstNo - i),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
})
.catch((err: any) => console.error("워크플로우 조회 에러:", err));
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
/** 검색 실행 (페이지 1로 리셋) */
const doSearch = () => {
data.value.params.pageNum = 1;
fetchList();
};
/** 페이지 이동 */
const changePageNum = (page: number) => {
data.value.params.pageNum = page;
fetchList();
};
/** 페이지 사이즈 변경 */
const changePageSize = (size: number) => {
data.value.params.pageSize = size;
data.value.params.pageNum = 1;
fetchList();
};
const saveStep = async (payload: WorkflowStep) => {
try {
const { data: saved } = await WorkflowStepService.add(payload);
await fetchList();
} catch (e) {
console.error("[STEP SAVE FAIL]", e);
} finally {
data.value.isStepVisible = false;
}
};
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 removeData = (value?: Array<{ deviceKey: number }>) => {
const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return;
const ids = removeList.map((x) => x.deviceKey);
const removeOne = (id: number) =>
WorkflowStepService.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;
}
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
// /
if (ids.length === 1) {
removeOne(ids[0])
.then(() => {
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
});
after();
})
.catch((err) => {
console.error("삭제 실패:", err);
store.setSnackbarMsg({
color: "warning",
text: "삭제 실패",
result: 500,
});
});
} 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(removeOne))
.then(() => {
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
});
})
.catch((err) => {
console.error("일부 삭제 실패:", err);
store.setSnackbarMsg({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
})
.finally(after);
}
};
const handleRemoveData = () => {
if (data.value.selected.length === 0) {
// store.setSnackbarMsg({
// text: " . ",
// result: 500,
// });
return;
}
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
//
removeData(undefined);
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item: any) => ({ deviceKey: item.deviceKey }))
: [];
};
const closeDetail = () => {
openView.value = false;
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const openDetailModal = (selectedItem) => {
const openDetailModal = (selectedItem: any) => {
data.value.selectedData = selectedItem;
openView.value = true;
};
const handleSave = ({
workflow,
stepName,
}: {
workflow: string;
stepName: string;
}) => {
if (data.value.selectedData) {
data.value.selectedData.workflow = workflow;
data.value.selectedData.stepName = stepName;
}
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isStepVisible = true;
};
const openModifyModal = (item: { workflow: string; stepName: string }) => {
const openModifyModal = (item: any) => {
data.value.selectedData = {
workflow: item.workflow,
id: item.deviceKey,
stepName: item.stepName,
status: item.status,
};
openModify.value = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isModalVisible = true;
data.value.modalMode = "edit";
data.value.isStepVisible = true;
};
const closeModal = () => {
data.value.isModalVisible = false;
data.value.isStepVisible = false;
data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
/** 모달 닫히면 목록 새로고침하고 싶으면 아래 watch 사용 */
// watch(() => data.value.isStepVisible, (now, prev) => {
// if (prev && !now) fetchList();
// });
onMounted(() => {
getData();
getCodeList();
fetchList();
});
</script>
@ -344,6 +319,8 @@ onMounted(() => {
</div>
</v-card-item>
</v-card>
<!-- 상단 검색 (워크플로우 화면과 동일 UX) -->
<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">
@ -354,14 +331,16 @@ onMounted(() => {
>
<v-select
v-model="data.params.searchType"
label="검색조건"
label="검색유형"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
item-title="label"
item-value="value"
hide-details
></v-select>
@update:model-value="doSearch"
/>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
@ -371,8 +350,8 @@ onMounted(() => {
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
@keyup.enter="doSearch"
/>
</v-responsive>
<div class="ml-3">
@ -380,14 +359,15 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
@click="doSearch"
>
<v-icon> mdi-magnify</v-icon>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<!-- 개수 / 페이지사이즈 / 생성 버튼 -->
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
@ -396,9 +376,10 @@ onMounted(() => {
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
> {{ 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
@ -410,13 +391,20 @@ onMounted(() => {
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
@update:model-value="changePageSize"
/>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create Workflow Step</v-btn
>
</v-sheet>
</v-sheet>
<!-- 테이블 -->
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
@ -436,6 +424,7 @@ onMounted(() => {
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
@ -445,7 +434,7 @@ onMounted(() => {
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
/>
</th>
<th
v-for="(item, i) in tableHeader"
@ -457,6 +446,7 @@ onMounted(() => {
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="item in data.results"
@ -485,16 +475,12 @@ onMounted(() => {
>
mdi-checkbox-marked-circle
</v-icon>
<v-icon v-else color="warning">
mdi-alert-circle
</v-icon>
<v-icon v-else color="warning">mdi-alert-circle</v-icon>
</td>
<td>{{ item.workflow }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openDetailModal(item)" />
<IconModifyBtn @on-click="openModifyModal(item)" />
<!-- <IconModifyBtn @on-click="openModify = true" /> -->
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
@ -505,6 +491,8 @@ onMounted(() => {
</tbody>
</v-table>
</v-sheet>
<!-- 페이지네이션 -->
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
@ -512,8 +500,8 @@ onMounted(() => {
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
@update:model-value="changePageNum"
/>
</v-card-actions>
</v-col>
</v-card>
@ -521,15 +509,19 @@ onMounted(() => {
</v-card>
</v-container>
</div>
<!-- 상세 보기 -->
<div class="w-100" v-else>
<ViewComponent @close="closeDetail" />
</div>
<v-dialog v-model="openModify" max-width="600px">
<!-- 생성/수정 모달 -->
<v-dialog v-model="data.isStepVisible" max-width="760" persistent>
<StapComfigDialog
v-model="openModify"
:selectedData="data.selectedData"
:workflowList="workflowList"
@save="handleSave"
:editData="data.selectedData"
:mode="data.modalMode"
@saved="saveStep"
@close-modal="closeModal"
/>
</v-dialog>
</template>

@ -1,14 +1,11 @@
<script setup lang="ts">
/* =========================
* Imports
* ========================= */
import { onMounted, ref, watch } from "vue";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone";
import { commonStore } from "@/stores/commonStore";
import { AutoflowService } from "@/components/service/management/AutoflowService";
import { WorkflowService } from "@/components/service/management/workflowService";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.vue";
@ -16,15 +13,11 @@ import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadD
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
// import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
/* =========================
* Constants / Types
* ========================= */
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const store = commonStore();
const openView = ref(false);
type SearchType = "전체" | "제목" | "작성자";
const tableHeader = [
@ -56,9 +49,6 @@ const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
작성자: "AUTHOR",
};
const store = commonStore();
const openView = ref(false);
const data = ref({
params: {
pageNum: 1,
@ -128,7 +118,7 @@ const fetchList = () => {
searchType: mapped,
};
AutoflowService.search(payload)
WorkflowService.search(payload)
.then((res: any) => {
if (res.status !== 200) return;
@ -170,18 +160,18 @@ const fetchList = () => {
return;
}
const totalElements = result?.totalElements;
const totalPages = result?.totalPages;
const serverPage = result?.pageable?.pageNumber;
const serverSize = result?.pageable?.pageSize;
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"
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 - 1),
toRow(w, firstNo - i),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
@ -211,7 +201,7 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
const ids = removeList.map((x) => x.deviceKey);
const remove = (id: number) =>
AutoflowService.delete(id).then((res) => {
WorkflowService.delete(id).then((res) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
@ -268,15 +258,6 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
}
};
/* =========================
* Actions (UI helpers)
* ========================= */
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: [];
};
const handleRemoveData = () => {
if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) {
@ -286,6 +267,12 @@ const handleRemoveData = () => {
removeData();
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: [];
};
const openDetailModal = (selectedItem: any) => {
data.value.selectedData = selectedItem;
openView.value = true;
@ -325,9 +312,6 @@ const closeUploadModal = () => {
data.value.isUploadVisible = false;
};
/* =========================
* Watchers / Lifecycle
* ========================= */
watch(
() => data.value.isCreateVisible,
(now, prev) => {
@ -344,7 +328,6 @@ watch(
onMounted(() => {
fetchList();
// getCodeList(); //
});
</script>

@ -2,7 +2,7 @@
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
import * as monaco from "monaco-editor";
import "monaco-editor/min/vs/editor/editor.main.css";
import { AutoflowService } from "@/components/service/management/AutoflowService";
import { WorkflowService } from "@/components/service/management/workflowService";
type TabKey = "details" | "yaml";
@ -54,7 +54,7 @@ spec:
/** ===== 상세 조회 ===== */
async function fetchDetail(id: number | string) {
try {
const res = await AutoflowService.view(Number(id));
const res = await WorkflowService.view(Number(id));
const d = res.data;
detail.value.workflowName = d.workflowName || "";

@ -8,12 +8,12 @@ import logo2 from "@/assets/workflow.png";
import { UserManagerService } from "@/components/service/management/userManagerService";
const router = useRouter();
const ROLE_ITEMS = ["ROLE_USER", "ROLE_MODERATOR", "ROLE_ADMIN"];
const data = ref({
form: false,
username: "",
email: "",
role: [],
role: ROLE_ITEMS[0],
password: "",
loading: false,
snackbar: false,
@ -35,7 +35,7 @@ const resetLogin = () => {
const resetSignup = () => {
data.value.username = "";
data.value.email = "";
data.value.role = [];
data.value.role = "";
data.value.password = "";
data.value.loading = false;
};
@ -44,7 +44,7 @@ const signUp = () => {
const payload = {
username: data.value.username,
email: data.value.email,
role: data.value.role,
role: data.value.role ? [data.value.role] : [],
password: data.value.password,
};
console.log("회원가입 호출 payload:", payload);
@ -112,8 +112,8 @@ const signUp = () => {
></v-text-field>
<v-select
v-model="data.role"
:items="['ROLE_USER', 'ROLE_MODERATOR', 'ROLE_ADMIN']"
multiple
:items="ROLE_ITEMS"
:rules="[(v) => !!v || '역할을 선택해주세요.']"
variant="outlined"
placeholder="Role"
class="mb-2"

@ -79,14 +79,13 @@ const routes = [
component: () => import("@/pages/DatasetView.vue"),
},
/** ■ 관리자 전용 라우트 */
{
name: "project",
path: "/project",
meta: {
title: "Projects",
requiresAuth: false,
requiresAdmin: true, // ✅ 관리자 전용
requiresAdmin: true,
},
component: () => import("@/pages/ProjectView.vue"),
},
@ -96,7 +95,7 @@ const routes = [
meta: {
title: "Users",
requiresAuth: false,
requiresAdmin: true, // ✅ 관리자 전용
requiresAdmin: true,
},
component: () => import("@/pages/UsersView.vue"),
},

Loading…
Cancel
Save