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'] ListComponent: typeof import('./src/components/home/ListComponent.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default']
StapComfigDialog: typeof import('./src/components/atoms/organisms/StapComfigDialog.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'] TrainingScriptBaseDoalog: typeof import('./src/components/atoms/organisms/TrainingScriptBaseDoalog.vue')['default']
ViewComponent: typeof import('./src/components/templates/Datasets/ViewComponent.vue')['default'] ViewComponent: typeof import('./src/components/templates/Datasets/ViewComponent.vue')['default']
WorkflowDialog: typeof import('./src/components/atoms/organisms/WorkflowDialog.vue')['default'] WorkflowDialog: typeof import('./src/components/atoms/organisms/WorkflowDialog.vue')['default']
WorkflowsBaseDialog: typeof import('./src/components/atoms/organisms/WorkflowsBaseDialog.vue')['default'] WorkflowsBaseDialog: typeof import('./src/components/atoms/organisms/WorkflowsBaseDialog.vue')['default']
WorkflowsCreateDialog: typeof import('./src/components/atoms/organisms/WorkflowsCreateDialog.vue')['default'] WorkflowsCreateDialog: typeof import('./src/components/atoms/organisms/WorkflowsCreateDialog.vue')['default']
WorkflowsUploadDialog: typeof import('./src/components/atoms/organisms/WorkflowsUploadDialog.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 IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { computed, onBeforeUnmount, onMounted, watch, ref } from "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 { 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 { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore"; import { useAutoflowStore } from "@/stores/autoflowStore";
@ -122,12 +122,12 @@ async function submit() {
return; return;
} }
const { data } = await AutoflowService.update(id, payload); const { data } = await WorkflowService.update(id, payload);
emit("saved", data); emit("saved", data);
emit("close-modal"); emit("close-modal");
} else { } else {
// //
const { data } = await AutoflowService.add(payload); const { data } = await WorkflowService.add(payload);
emit("saved", data); emit("saved", data);
emit("close-modal"); 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 { useRoute, useRouter } from "vue-router";
import { menuUtils } from "@/utils/menuUtils"; import { menuUtils } from "@/utils/menuUtils";
import { storage } from "@/utils/storage"; import { storage } from "@/utils/storage";
import logo from "@/assets/iteration (1).png"; import SidebarHeader from "@/components/common/SidebarHeader.vue";
const route = useRoute(); const route = useRoute();
const router = useRouter(); 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); const isShowAuth = ref(false);
function readRolesFromStorage(): string[] { function readRolesFromStorage(): string[] {
try { try {
// storage.get(...) ,
const raw = const raw =
storage.get?.("autoflow-auth") ?? storage.get?.("autoflow-auth") ??
localStorage.getItem("autoflow-auth") ?? localStorage.getItem("autoflow-auth") ??
@ -20,8 +25,6 @@ function readRolesFromStorage(): string[] {
const auth = typeof raw === "string" ? JSON.parse(raw) : raw; const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
let roles = auth?.userInfo?.roles ?? auth?.roles ?? []; let roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
// "ROLE_USER,ROLE_ADMIN"
if (typeof roles === "string") { if (typeof roles === "string") {
roles = roles.split(",").map((s: string) => s.trim()); roles = roles.split(",").map((s: string) => s.trim());
} }
@ -33,7 +36,6 @@ function readRolesFromStorage(): string[] {
} }
} }
// ADMIN (ROLE_ADMIN ADMIN )
const isAdmin = computed(() => { const isAdmin = computed(() => {
const roles = readRolesFromStorage(); const roles = readRolesFromStorage();
return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN"); return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN");
@ -43,10 +45,6 @@ const isLinkActive = (link) => {
return route.path.includes(link); return route.path.includes(link);
}; };
const goMain = () => {
router.push("/home");
};
onMounted(() => { onMounted(() => {
isShowAuth.value = true; isShowAuth.value = true;
//storage.getAuth().auth === "ADMIN"; //storage.getAuth().auth === "ADMIN";
@ -55,19 +53,6 @@ onMounted(() => {
<template> <template>
<v-card flat class="mx-auto"> <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"> <v-list nav class="pa-5 pt-0">
<template <template
v-for="({ title, value, icon, path, depth }, i) in menuUtils.menuItem" v-for="({ title, value, icon, path, depth }, i) in menuUtils.menuItem"
@ -100,7 +85,7 @@ onMounted(() => {
</template> </template>
<template v-else> <template v-else>
<v-list-item <v-list-item
v-if="value !== 'project' || isAdmin" v-if="value !== 'project'"
rounded rounded
:title="title" :title="title"
:value="value" :value="value"

@ -4,6 +4,7 @@ import { storage } from "@/utils/storage.js";
import DrawerComponent from "@/components/common/DrawerComponent.vue"; import DrawerComponent from "@/components/common/DrawerComponent.vue";
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue"; import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { UserManagerService } from "@/components/service/management/userManagerService"; import { UserManagerService } from "@/components/service/management/userManagerService";
import SidebarHeader from "@/components/common/SidebarHeader.vue";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -15,7 +16,8 @@ const projectName = ref(localStorage.getItem("projectName") || "");
// Admin + Admin // Admin + Admin
// ---------------------- // ----------------------
const isAdmin = ref(false); // const isAdmin = ref(false); //
const adminMode = ref(false); // ( true) const adminMode = ref(false); //
const lastNonAdminPath = ref("/home"); //
function computeIsAdmin() { function computeIsAdmin() {
try { try {
@ -26,26 +28,25 @@ function computeIsAdmin() {
const roles = raw?.userInfo?.roles ?? raw?.roles ?? []; const roles = raw?.userInfo?.roles ?? raw?.roles ?? [];
const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth; const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth;
const inRoles = Array.isArray(roles) const inRoles = Array.isArray(roles)
? roles.includes("ROLE_ADMIN") ? roles.includes("ROLE_ADMIN")
: roles === "ROLE_ADMIN"; : roles === "ROLE_ADMIN";
isAdmin.value = inRoles || authCd === "ADMIN"; isAdmin.value = inRoles || authCd === "ADMIN";
} catch { } catch {
isAdmin.value = false; isAdmin.value = false;
} }
} }
function enterAdmin() { //
if (!isAdmin.value) return; // function toggleAdmin() {
adminMode.value = true; // if (!isAdmin.value) return;
router.push("/project"); // if (adminMode.value) {
} adminMode.value = false;
function exitAdmin() { router.push(lastNonAdminPath.value || "/home");
adminMode.value = false; // } else {
// adminMode.value = true;
// router.push("/home"); if (!route.meta?.requiresAdmin) router.push("/project");
}
} }
// ---------------------- // ----------------------
@ -57,7 +58,7 @@ const menuItems = [
{ {
title: "Change Password", title: "Change Password",
click: () => { click: () => {
/* 비밀번호 모달 열기 등 */ /* open modal */
}, },
}, },
{ title: "Logout", icon: "mdi-logout", click: () => logOut() }, { title: "Logout", icon: "mdi-logout", click: () => logOut() },
@ -67,6 +68,13 @@ const drawer = ref(null);
const pageTitle = computed(() => route.meta.title); const pageTitle = computed(() => route.meta.title);
const pagePath = computed(() => route.path); const pagePath = computed(() => route.path);
// active
const isLinkActive = (link) => route.path.includes(link);
const settingsLabel = computed(() =>
adminMode.value ? "Back to Console" : "Settings",
);
function updateUsername() { function updateUsername() {
const auth = storage.getAuth?.() ?? null; const auth = storage.getAuth?.() ?? null;
username.value = auth?.userInfo?.username ?? auth?.username ?? ""; username.value = auth?.userInfo?.username ?? auth?.username ?? "";
@ -88,7 +96,7 @@ function logOut() {
username.value = ""; username.value = "";
projectName.value = ""; projectName.value = "";
sessionStorage.removeItem("initialRedirectDone"); sessionStorage.removeItem("initialRedirectDone");
adminMode.value = false; // adminMode.value = false;
router.push("/login"); router.push("/login");
}); });
} }
@ -101,8 +109,21 @@ function onStorage(e) {
computeIsAdmin(); 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(() => { onMounted(() => {
updateUsername(); updateUsername();
computeIsAdmin(); computeIsAdmin();
@ -113,54 +134,67 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage); window.removeEventListener("storage", onStorage);
}); });
//
watch(
() => route.fullPath,
() => refreshProjectName(),
);
</script> </script>
<template> <template>
<v-app> <v-app>
<!-- 사이드바: adminMode에 따라 바꿔치기 --> <!-- 사이드바 -->
<v-navigation-drawer <v-navigation-drawer
v-model="drawer" v-model="drawer"
border="0" border="0"
hide-overlay hide-overlay
permanent permanent
v-if="!route.meta.hideSidebar" 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" /> <DrawerComponent v-if="!adminMode" />
<!-- 관리자 메뉴 --> <!-- 관리자 메뉴: 유저 메뉴와 동일한 /여백/활성 스타일 -->
<template v-else> <template v-else>
<v-list nav density="compact" class="pt-6"> <v-card flat class="mx-auto">
<v-list-subheader>Admin</v-list-subheader> <v-list nav class="pa-5 pt-0">
<v-list-item <v-list-item
rounded
title="Projects"
value="projects"
to="/project" to="/project"
prepend-icon="mdi-briefcase" 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 <v-list-item
rounded
title="Users"
value="users"
to="/users" to="/users"
prepend-icon="mdi-account-multiple" prepend-icon="mdi-account-multiple"
title="Users" :active="isLinkActive('/users')"
/> :active-color="isLinkActive('/users') ? 'primary' : null"
<v-divider class="my-3" /> density="compact"
<v-list-item class="pa-2 rounded-lg"
prepend-icon="mdi-arrow-left-bold" style="padding-inline-start: 10px"
title="Back to Console"
@click="exitAdmin"
/> />
</v-list> </v-list>
</v-card>
</template> </template>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar class="bg-shades-transparent" flat> <v-app-bar class="bg-shades-transparent" flat>
<v-spacer /> <v-spacer />
<!-- 설정 버튼: 관리자에게만 보임 / 누르면 관리자 모드 진입 -->
<v-tooltip v-if="isAdmin" location="bottom" text="Settings"> <v-tooltip v-if="isAdmin" location="bottom" text="Settings">
<template #activator="{ props }"> <template #activator="{ props }">
<v-btn <v-btn
@ -168,13 +202,15 @@ watch(
color="primary" color="primary"
class="mr-3" class="mr-3"
v-bind="props" v-bind="props"
@click="enterAdmin" @click="toggleAdmin"
aria-label="Settings"
> >
<v-icon>mdi-cog</v-icon> <v-icon>mdi-cog</v-icon>
</v-btn> </v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<!-- 프로젝트 선택 -->
<v-tooltip location="bottom" text="Project"> <v-tooltip location="bottom" text="Project">
<template #activator="{ props }"> <template #activator="{ props }">
<v-btn <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 { import {
Workflow, Workflow,
WorkflowSearch, WorkflowSearch,
} from "@/components/models/management/Autoflow"; } from "@/components/models/management/Workflow";
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
export const AutoflowService = { export const WorkflowService = {
add: (payload: Workflow) => { add: (payload: Workflow) => {
return request.post("/api/workflows", payload); 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"> <script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import { commonStore } from "@/stores/commonStore";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/templates/stepconfig/ViewComponent.vue"; import ViewComponent from "@/components/templates/stepconfig/ViewComponent.vue";
import StapComfigDialog from "@/components/atoms/organisms/StapComfigDialog.vue"; import StapComfigDialog from "@/components/atoms/organisms/WorklfowStepBaseDialog.vue";
import { AutoflowStepService } from "@/components/service/management/AutoflowStepService";
// const store = commonStore(); import { WorkflowStepService } from "@/components/service/management/workflowStepService";
import type { WorkflowStep } from "@/components/models/management/WorkflowStep";
const store = commonStore();
const openView = ref(false); const openView = ref(false);
const openModify = ref(false); type SearchType = "전체" | "제목" | "작성자";
const tableHeader = [ const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" }, { label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Step Name", width: "15%", style: "word-break: keep-all;" }, { label: "Step Name", width: "15%", style: "word-break: keep-all;" },
@ -25,308 +29,279 @@ const tableHeader = [
]; ];
const searchOptions = [ const searchOptions = [
{ searchType: "전체", searchText: "" }, { label: "전체", value: "전체" as SearchType },
{ searchType: "디바이스 별칭", searchText: "deviceAlias" }, { label: "제목", value: "제목" as SearchType },
{ searchType: "디바이스 키", searchText: "deviceKey" }, { label: "작성자", value: "작성자" as SearchType },
{ searchType: "사용자", searchText: "userId" },
{ searchType: "디바이스 이름", searchText: "deviceName" },
{ searchType: "디바이스 모델", searchText: "deviceModel" },
{ searchType: "디바이스 OS", searchText: "deviceOs" },
]; ];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
const pageSizeOptions = [ const pageSizeOptions = [
{ text: "10 페이지", value: 10 }, { text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 }, { text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 }, { text: "100 페이지", value: 100 },
]; ];
const workflowList = ["pipeline-a", "pipeline-b", "pipeline-c"];
const data = ref({ const data = ref({
params: { params: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
searchType: "", searchType: "전체" as SearchType,
searchText: "", searchText: "",
}, },
results: [], results: [] as any[],
totalDataLength: 0, totalElements: 0,
pageLength: 0, pageLength: 0,
modalMode: "", modalMode: "" as "create" | "edit" | "",
selectedData: null, selectedData: null as any,
isStepVisible: false,
allSelected: false, allSelected: false,
selected: [], selected: [] as Array<{ deviceKey: number }>,
isModalVisible: false,
isConfirmDialogVisible: false, isConfirmDialogVisible: false,
userOption: [],
}); });
const getCodeList = () => { const toRow = (s: any, no: number) => ({
// UserService.search(data.value.params).then((d) => { no,
// if (d.status === 200) { stepName: s.stepName ?? "-",
// data.value.userOption = d.data.userList; 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 = () => { const fetchList = async () => {
// : No 7 1 const projectId = Number(localStorage.getItem("projectId"));
data.value.results = [ if (!projectId) {
{ console.warn("[WorkflowSteps] projectId 없음 — 프로젝트 먼저 선택");
no: 7, data.value.results = [];
stepName: "Data Ingest", data.value.totalElements = 0;
type: "Preprocessing", data.value.pageLength = 0;
dataset: "raw_data", return;
script: "ingest.py", }
hyperParameters: "-",
resource: "CPU:1, MEM:2Gi", const { pageNum, pageSize, searchType, searchText } = data.value.params;
status: "success",
workflow: "pipeline-a", const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
deviceKey: 7, const keyword = (searchText || "").trim();
}, const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
{
no: 6, let reqPage = data.value.params.pageNum;
stepName: "Data Preprocess", let reqSize = data.value.params.pageSize;
type: "Preprocessing", if (needLocalFilter) {
dataset: "raw_data", reqPage = 0;
script: "preprocess.py", reqSize = 1000;
hyperParameters: "normalize=True", }
resource: "CPU:2, MEM:4Gi", const payload = {
status: "success", projectId,
workflow: "pipeline-a", page: reqPage,
deviceKey: 6, size: reqSize,
}, keyword,
{ searchType: mapped,
no: 5, };
stepName: "Model Training",
type: "Training", WorkflowStepService.search(payload)
dataset: "processed_data", .then((res: any) => {
script: "train.py", if (res.status !== 200) return;
hyperParameters: "lr=0.01, epochs=10",
resource: "GPU:1, MEM:8Gi", const result = res.data;
status: "warning", let list = result?.content ?? [];
workflow: "pipeline-a",
deviceKey: 5, if (needLocalFilter) {
}, const kw = keyword.toLowerCase();
{
no: 4, if (mapped === "TITLE") {
stepName: "Model Evaluation", list = list.filter((w: any) =>
type: "Evaluation", String(w?.workflowName ?? "")
dataset: "test_data", .toLowerCase()
script: "evaluate.py", .includes(kw),
hyperParameters: "-", );
resource: "CPU:1, MEM:4Gi", } else if (mapped === "AUTHOR") {
status: "success", list = list.filter((w: any) =>
workflow: "pipeline-a", String(w?.regUserId ?? "")
deviceKey: 4, .toLowerCase()
}, .includes(kw),
{
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 setPaginationLength = () => { const uiSize = data.value.params.pageSize;
if (data.value.totalDataLength % data.value.params.pageSize === 0) { const totalElements = list.length;
data.value.pageLength = const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
data.value.totalDataLength / data.value.params.pageSize; const safePage = Math.min(Math.max(1, pageNum), totalPages);
} else { const start = (safePage - 1) * uiSize;
data.value.pageLength = Math.ceil( const pageSlice = list.slice(start, start + uiSize);
data.value.totalDataLength / data.value.params.pageSize,
// ()
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) => { /** 검색 실행 (페이지 1로 리셋) */
if (data.value.modalMode === "create") { const doSearch = () => {
// DeviceService.add(formData).then((d) => { data.value.params.pageNum = 1;
// if (d.status === 200) { fetchList();
// data.value.isModalVisible = false; };
// store.setSnackbarMsg({
// text: " .", /** 페이지 이동 */
// result: 200, const changePageNum = (page: number) => {
// }); data.value.params.pageNum = page;
// changePageNum(1); fetchList();
// } else { };
// store.setSnackbarMsg({
// text: d, /** 페이지 사이즈 변경 */
// result: 500, const changePageSize = (size: number) => {
// }); data.value.params.pageSize = size;
// } data.value.params.pageNum = 1;
// }); fetchList();
} else { };
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) { const saveStep = async (payload: WorkflowStep) => {
// data.value.isModalVisible = false; try {
// store.setSnackbarMsg({ const { data: saved } = await WorkflowStepService.add(payload);
// text: " .", await fetchList();
// result: 200, } catch (e) {
// }); console.error("[STEP SAVE FAIL]", e);
// changePageNum(); } finally {
// } else { data.value.isStepVisible = false;
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} }
}; };
const removeData = (value) => { const removeData = (value?: Array<{ deviceKey: number }>) => {
let removeList = value ? value : data.value.selected; const removeList = value ?? data.value.selected;
const remove = (code) => { if (!removeList || removeList.length === 0) return;
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) { const ids = removeList.map((x) => x.deviceKey);
// store.setSnackbarMsg({ const removeOne = (id: number) =>
// text: d, WorkflowStepService.delete(id).then((res) => {
// result: 500, 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) { fetchList();
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false; data.value.isConfirmDialogVisible = false;
data.value.selected = []; data.value.selected = [];
data.value.allSelected = false; 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 { } else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally( Promise.all(ids.map(removeOne))
() => { .then(() => {
// store.setSnackbarMsg({ store.setSnackbarMsg({
// text: " .", color: "success",
// result: 200, text: "모두 삭제되었습니다.",
// }); result: 200,
changePageNum(); });
data.value.isConfirmDialogVisible = false; })
data.value.selected = []; .catch((err) => {
data.value.allSelected = false; console.error("일부 삭제 실패:", err);
}, store.setSnackbarMsg({
); color: "warning",
text: "일부 삭제 실패",
result: 500,
});
})
.finally(after);
} }
}; };
const handleRemoveData = () => { const getSelectedAllData = () => {
if (data.value.selected.length === 0) { data.value.selected = data.value.allSelected
// store.setSnackbarMsg({ ? data.value.results.map((item: any) => ({ deviceKey: item.deviceKey }))
// text: " . ", : [];
// result: 500,
// });
return;
}
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
//
removeData(undefined);
}; };
const closeDetail = () => { const closeDetail = () => {
openView.value = false; openView.value = false;
}; };
const changePageNum = (page) => { const openDetailModal = (selectedItem: any) => {
data.value.params.pageNum = page;
getData();
};
const openDetailModal = (selectedItem) => {
data.value.selectedData = selectedItem; data.value.selectedData = selectedItem;
openView.value = true; openView.value = true;
}; };
const handleSave = ({ const openCreateModal = () => {
workflow, data.value.selectedData = null;
stepName, data.value.modalMode = "create";
}: { data.value.isStepVisible = true;
workflow: string;
stepName: string;
}) => {
if (data.value.selectedData) {
data.value.selectedData.workflow = workflow;
data.value.selectedData.stepName = stepName;
}
}; };
const openModifyModal = (item: { workflow: string; stepName: string }) => {
const openModifyModal = (item: any) => {
data.value.selectedData = { data.value.selectedData = {
workflow: item.workflow, id: item.deviceKey,
stepName: item.stepName, stepName: item.stepName,
status: item.status,
}; };
openModify.value = true; data.value.modalMode = "edit";
}; data.value.isStepVisible = true;
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isModalVisible = true;
}; };
const closeModal = () => { const closeModal = () => {
data.value.isModalVisible = false; data.value.isStepVisible = false;
data.value.selectedData = null; data.value.selectedData = null;
}; };
const getSelectedAllData = () => { /** 모달 닫히면 목록 새로고침하고 싶으면 아래 watch 사용 */
data.value.selected = data.value.allSelected // watch(() => data.value.isStepVisible, (now, prev) => {
? data.value.results.map((item) => { // if (prev && !now) fetchList();
return { // });
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => { onMounted(() => {
getData(); fetchList();
getCodeList();
}); });
</script> </script>
@ -344,6 +319,8 @@ onMounted(() => {
</div> </div>
</v-card-item> </v-card-item>
</v-card> </v-card>
<!-- 상단 검색 (워크플로우 화면과 동일 UX) -->
<v-card flat class="bg-shades-transparent w-100"> <v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4"> <v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center"> <div class="d-flex justify-center flex-wrap align-center">
@ -354,14 +331,16 @@ onMounted(() => {
> >
<v-select <v-select
v-model="data.params.searchType" v-model="data.params.searchType"
label="검색조건" label="검색유형"
density="compact" density="compact"
:items="searchOptions" :items="searchOptions"
item-title="searchType" item-title="label"
item-value="searchText" item-value="value"
hide-details hide-details
></v-select> @update:model-value="doSearch"
/>
</v-responsive> </v-responsive>
<v-responsive min-width="540" max-width="540"> <v-responsive min-width="540" max-width="540">
<v-text-field <v-text-field
v-model="data.params.searchText" v-model="data.params.searchText"
@ -371,8 +350,8 @@ onMounted(() => {
required required
class="mt-3 mb-3" class="mt-3 mb-3"
hide-details hide-details
@keyup.enter="changePageNum(1)" @keyup.enter="doSearch"
></v-text-field> />
</v-responsive> </v-responsive>
<div class="ml-3"> <div class="ml-3">
@ -380,14 +359,15 @@ onMounted(() => {
size="large" size="large"
color="primary" color="primary"
:rounded="5" :rounded="5"
@click="changePageNum(1)" @click="doSearch"
> >
<v-icon> mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
</div> </div>
</div> </div>
</v-card> </v-card>
<!-- 개수 / 페이지사이즈 / 생성 버튼 -->
<v-sheet <v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2" class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
> >
@ -396,9 +376,10 @@ onMounted(() => {
class="d-flex align-center mr-3 mb-2 bg-shades-transparent" class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
> >
<v-chip color="primary" <v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }} > {{ data.totalElements.toLocaleString() }}</v-chip
</v-chip> >
</v-sheet> </v-sheet>
<v-sheet class="bg-shades-transparent"> <v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2"> <v-responsive max-width="140" min-width="140" class="mb-2">
<v-select <v-select
@ -410,13 +391,20 @@ onMounted(() => {
variant="outlined" variant="outlined"
color="primary" color="primary"
hide-details hide-details
@update:model-value="changePageNum(1)" @update:model-value="changePageSize"
></v-select> />
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
</v-sheet> </v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create Workflow Step</v-btn
>
</v-sheet>
</v-sheet> </v-sheet>
<!-- 테이블 -->
<v-card class="rounded-lg pa-8"> <v-card class="rounded-lg pa-8">
<v-col cols="12"> <v-col cols="12">
<v-sheet> <v-sheet>
@ -436,6 +424,7 @@ onMounted(() => {
:style="`width:${item.width}`" :style="`width:${item.width}`"
/> />
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th> <th>
@ -445,7 +434,7 @@ onMounted(() => {
:indeterminate="data.allSelected === true" :indeterminate="data.allSelected === true"
hide-details hide-details
@change="getSelectedAllData" @change="getSelectedAllData"
></v-checkbox> />
</th> </th>
<th <th
v-for="(item, i) in tableHeader" v-for="(item, i) in tableHeader"
@ -457,6 +446,7 @@ onMounted(() => {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="text-body-2"> <tbody class="text-body-2">
<tr <tr
v-for="item in data.results" v-for="item in data.results"
@ -485,16 +475,12 @@ onMounted(() => {
> >
mdi-checkbox-marked-circle mdi-checkbox-marked-circle
</v-icon> </v-icon>
<v-icon v-else color="warning"> <v-icon v-else color="warning">mdi-alert-circle</v-icon>
mdi-alert-circle
</v-icon>
</td> </td>
<td>{{ item.workflow }}</td> <td>{{ item.workflow }}</td>
<td style="white-space: nowrap"> <td style="white-space: nowrap">
<IconInfoBtn @on-click="openDetailModal(item)" /> <IconInfoBtn @on-click="openDetailModal(item)" />
<IconModifyBtn @on-click="openModifyModal(item)" /> <IconModifyBtn @on-click="openModifyModal(item)" />
<!-- <IconModifyBtn @on-click="openModify = true" /> -->
<IconDeleteBtn <IconDeleteBtn
@on-click=" @on-click="
removeData([{ deviceKey: item.deviceKey }]) removeData([{ deviceKey: item.deviceKey }])
@ -505,6 +491,8 @@ onMounted(() => {
</tbody> </tbody>
</v-table> </v-table>
</v-sheet> </v-sheet>
<!-- 페이지네이션 -->
<v-card-actions class="text-center mt-8 justify-center"> <v-card-actions class="text-center mt-8 justify-center">
<v-pagination <v-pagination
v-model="data.params.pageNum" v-model="data.params.pageNum"
@ -512,8 +500,8 @@ onMounted(() => {
:total-visible="10" :total-visible="10"
color="primary" color="primary"
rounded="circle" rounded="circle"
@update:model-value="getData" @update:model-value="changePageNum"
></v-pagination> />
</v-card-actions> </v-card-actions>
</v-col> </v-col>
</v-card> </v-card>
@ -521,15 +509,19 @@ onMounted(() => {
</v-card> </v-card>
</v-container> </v-container>
</div> </div>
<!-- 상세 보기 -->
<div class="w-100" v-else> <div class="w-100" v-else>
<ViewComponent @close="closeDetail" /> <ViewComponent @close="closeDetail" />
</div> </div>
<v-dialog v-model="openModify" max-width="600px">
<!-- 생성/수정 모달 -->
<v-dialog v-model="data.isStepVisible" max-width="760" persistent>
<StapComfigDialog <StapComfigDialog
v-model="openModify" :editData="data.selectedData"
:selectedData="data.selectedData" :mode="data.modalMode"
:workflowList="workflowList" @saved="saveStep"
@save="handleSave" @close-modal="closeModal"
/> />
</v-dialog> </v-dialog>
</template> </template>

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

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

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

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

Loading…
Cancel
Save