fix: workflow 컴포넌트 CRUD 수정

main
jschoi 9 months ago
parent c94557442c
commit d2f7ae15c5

@ -13,9 +13,8 @@ import { useAutoflowStore } from "@/stores/autoflowStore";
const { projectId } = storeToRefs(useAutoflowStore()); const { projectId } = storeToRefs(useAutoflowStore());
const props = defineProps<{ const props = defineProps<{
editData?: any; editData: any;
mode?: "create" | "edit"; mode: "create" | "edit";
userOption?: any[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

@ -2,89 +2,83 @@
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { storage } from "@/utils/storage.js"; import { storage } from "@/utils/storage.js";
import DrawerComponent from "@/components/common/DrawerComponent.vue"; import DrawerComponent from "@/components/common/DrawerComponent.vue";
import { ref, watchEffect } from "vue"; import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import Select from "@/views/Select.vue";
import { UserManagerService } from "@/components/service/management/userManagerService"; import { UserManagerService } from "@/components/service/management/userManagerService";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const username = ref("");
const autoflow = useAutoflowStore(); const username = ref("");
const showPasswordModal = ref(false);
const selectedUserData = ref({});
const menu = ref([]);
const projectName = ref(localStorage.getItem("projectName") || ""); const projectName = ref(localStorage.getItem("projectName") || "");
const updateUsername = () => {
// storage : { userInfo: { username, ... }, ... }
const auth = storage.getAuth?.() ?? null;
username.value =
auth?.userInfo?.username ??
auth?.username ?? //
""; //
};
// ----------------------
// Admin + Admin
// ----------------------
const isAdmin = ref(false); //
const adminMode = ref(false); // ( true)
function computeIsAdmin() {
try {
const raw =
typeof storage?.getAuth === "function"
? storage.getAuth()
: JSON.parse(localStorage.getItem("autoflow-auth") || "null");
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");
}
// ----------------------
//
// ----------------------
const menu = ref([]);
const menuItems = [ const menuItems = [
{ { title: "Select Project", click: () => goSelect() },
title: "Select Project",
click: () => {
goSelect();
},
},
{ {
title: "Change Password", title: "Change Password",
click: () => { click: () => {
showPasswordModal.value = true; /* 비밀번호 모달 열기 등 */
},
},
{
title: "Logout",
icon: "mdi-logout",
click: () => {
logOut();
},
},
];
const userMenuItems = [
{
title: "Select Project",
},
{
title: "Change Password",
},
{
title: "Logout",
icon: "mdi-logout",
click: () => {
logOut();
}, },
}, },
{ title: "Logout", icon: "mdi-logout", click: () => logOut() },
]; ];
const drawer = ref(null); const drawer = ref(null);
const pageTitle = computed(() => { const pageTitle = computed(() => route.meta.title);
return route.meta.title; const pagePath = computed(() => route.path);
});
const pagePath = computed(() => { function updateUsername() {
return route.path; const auth = storage.getAuth?.() ?? null;
}); username.value = auth?.userInfo?.username ?? auth?.username ?? "";
}
const refreshProjectName = () => { function refreshProjectName() {
const v = localStorage.getItem("projectName"); const v = localStorage.getItem("projectName");
projectName.value = v ? v : ""; projectName.value = v ? v : "";
}; }
function goSelect() {
const goSelect = () => {
router.push("/select"); router.push("/select");
}; }
function logOut() {
const logOut = () => {
UserManagerService.signOut() UserManagerService.signOut()
.catch(console.error) .catch(console.error)
.finally(() => { .finally(() => {
@ -94,49 +88,41 @@ const logOut = () => {
username.value = ""; username.value = "";
projectName.value = ""; projectName.value = "";
sessionStorage.removeItem("initialRedirectDone"); sessionStorage.removeItem("initialRedirectDone");
adminMode.value = false; //
router.push("/login"); router.push("/login");
}); });
}; }
// storage
function onStorage(e) {
if (!e.key || e.key === "projectName") refreshProjectName();
if (!e.key || e.key === "autoflow-auth" || e.key === "auth") {
updateUsername();
computeIsAdmin();
}
}
// mount
onMounted(() => { onMounted(() => {
updateUsername(); updateUsername();
computeIsAdmin();
refreshProjectName(); refreshProjectName();
menu.value = menuItems; menu.value = menuItems;
window.addEventListener("storage", onStorage);
// projectName
window.addEventListener("storage", (e) => {
if (!e.key || e.key === "projectName") refreshProjectName();
if (!e.key || e.key === "autoflow-auth" || e.key === "auth")
updateUsername();
});
});
onMounted(() => {
updateUsername();
// /
window.addEventListener("storage", (e) => {
if (!e.key || e.key === "auth") updateUsername();
});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("storage", updateUsername); window.removeEventListener("storage", onStorage);
}); });
//
watch( watch(
() => route.fullPath, () => route.fullPath,
() => refreshProjectName(), () => refreshProjectName(),
); );
watchEffect(() => {
// const auth = storage.getAuth().auth;
// if (auth === "ADMIN") {
menu.value = menuItems;
// } else {
// menu.value = userMenuItems;
// }
});
</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"
@ -144,39 +130,74 @@ watchEffect(() => {
permanent permanent
v-if="!route.meta.hideSidebar" v-if="!route.meta.hideSidebar"
> >
<DrawerComponent /> <!-- 기본(일반 사용자) 메뉴 -->
<DrawerComponent v-if="!adminMode" />
<!-- 관리자 메뉴 -->
<template v-else>
<v-list nav density="compact" class="pt-6">
<v-list-subheader>Admin</v-list-subheader>
<v-list-item
to="/project"
prepend-icon="mdi-briefcase"
title="Projects"
/>
<v-list-item
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"
/>
</v-list>
</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-spacer />
<!-- 설정 버튼: 관리자에게만 보임 / 누르면 관리자 모드 진입 -->
<v-tooltip v-if="isAdmin" location="bottom" text="Settings">
<template #activator="{ props }">
<v-btn
icon
color="primary"
class="mr-3"
v-bind="props"
@click="enterAdmin"
>
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip location="bottom" text="Project">
<template #activator="{ props }">
<v-btn
icon
color="primary"
class="mr-3"
@click="goSelect"
v-bind="props"
>
<v-icon>mdi-home</v-icon>
</v-btn>
</template>
</v-tooltip>
<div style="min-width: 180px" class="d-flex flex-column align-end">
<div class="font-weight-black">{{ username || "GUEST" }}</div>
<div class="text-subtitle-2">
{{ projectName || "No Project Selected" }}
</div>
</div>
<v-menu location="bottom end"> <v-menu location="bottom end">
<template v-slot:activator="{ props }"> <template #activator="{ props }">
<v-tooltip location="bottom" text="Settings">
<template #activator="{ props }">
<v-btn icon color="primary" class="mr-3" v-bind="props">
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip location="bottom" text="Projact">
<template #activator="{ props }">
<v-btn
icon
color="primary"
class="mr-3"
@click="goSelect"
v-bind="props"
>
<v-icon>mdi-home</v-icon>
</v-btn>
</template>
</v-tooltip>
<div style="min-width: 180px" class="d-flex flex-column align-end">
<div class="font-weight-black">{{ username || "GUEST" }}</div>
<div class="text-subtitle-2">
{{ projectName || "No Project Selected" }}
</div>
</div>
<v-btn icon color="primary" v-bind="props" class="mr-3"> <v-btn icon color="primary" v-bind="props" class="mr-3">
<v-icon>mdi-arrow-down-drop-circle-outline</v-icon> <v-icon>mdi-arrow-down-drop-circle-outline</v-icon>
</v-btn> </v-btn>

@ -7,3 +7,15 @@ export interface Workflow {
modDt: string; modDt: string;
projectId: number; projectId: number;
} }
export interface WorkflowSearch {
projectId: number; // ✅ 유일한 필수
page?: number;
size?: number;
keyword?: string;
searchType?: "전체" | "제목" | "작성자";
startDate?: string;
endDate?: string;
sortField?: string;
sortDirection?: "ASC" | "DESC";
}

@ -1,4 +1,7 @@
import { Workflow } from "@/components/models/management/Autoflow"; import {
Workflow,
WorkflowSearch,
} from "@/components/models/management/Autoflow";
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
export const AutoflowService = { export const AutoflowService = {
add: (payload: Workflow) => { add: (payload: Workflow) => {
@ -17,4 +20,7 @@ export const AutoflowService = {
update: (id: number, payload: Workflow) => { update: (id: number, payload: Workflow) => {
return request.put(`/api/workflows/${id}`, payload); return request.put(`/api/workflows/${id}`, payload);
}, },
search: (payload: WorkflowSearch) => {
return request.get("/api/workflows/search", payload);
},
}; };

@ -1,89 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; /* =========================
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; * Imports
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue"; * ========================= */
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; 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 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";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
import { AutoflowService } from "@/components/service/management/AutoflowService";
import { commonStore } from "@/stores/commonStore";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import dayjs from "dayjs"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import utc from "dayjs/plugin/utc"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import tz from "dayjs/plugin/timezone"; // import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
const store = commonStore();
const openView = ref(false); /* =========================
const tableHeader = [ * Constants / Types
{ * ========================= */
label: "No", dayjs.extend(utc);
width: "5%", dayjs.extend(tz);
style: "word-break: keep-all;", const KST = "Asia/Seoul";
},
{
label: "Workflow Name",
width: "7%",
style: "word-break: keep-all;",
},
{ type SearchType = "전체" | "제목" | "작성자";
label: "Step Count",
width: "7%", const tableHeader = [
style: "word-break: keep-all;", { label: "No", width: "5%", style: "word-break: keep-all;" },
}, { label: "Workflow Name", width: "7%", style: "word-break: keep-all;" },
{ { label: "Step Count", width: "7%", style: "word-break: keep-all;" },
label: "Config Progress", { label: "Config Progress", width: "7%", style: "word-break: keep-all;" },
width: "7%", { label: "Kubeflow Status", width: "7%", style: "word-break: keep-all;" },
style: "word-break: keep-all;", { label: "Created DateTime", width: "7%", style: "word-break: keep-all;" },
}, { label: "Action", width: "7%", style: "word-break: keep-all;" },
{
label: "Kubeflow Status",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Created DateTime",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Action",
width: "7%",
style: "word-break: keep-all;",
},
]; ];
const searchOptions = [ const searchOptions = [
{ { label: "전체", value: "전체" as SearchType },
searchType: "전체", { label: "제목", value: "제목" as SearchType },
searchText: "", { label: "사용자", value: "작성자" as SearchType },
},
{
searchType: "디바이스 별칭",
searchText: "deviceAlias",
},
{
searchType: "디바이스 키",
searchText: "deviceKey",
},
{
searchType: "사용자",
searchText: "userId",
},
{
searchType: "디바이스 이름",
searchText: "deviceName",
},
{
searchType: "디바이스 모델",
searchText: "deviceModel",
},
{
searchType: "디바이스 OS",
searchText: "deviceOs",
},
]; ];
const pageSizeOptions = [ const pageSizeOptions = [
@ -92,188 +49,170 @@ const pageSizeOptions = [
{ text: "100 페이지", value: 100 }, { text: "100 페이지", value: 100 },
]; ];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
const store = commonStore();
const openView = ref(false);
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" | "upload" | "",
selectedData: null, selectedData: null as any,
allSelected: false, allSelected: false,
selected: [], selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false, isCreateVisible: false,
isUploadVisible: false, isUploadVisible: false,
isModalVisible: false, isModalVisible: false,
isConfirmDialogVisible: false, isConfirmDialogVisible: false,
userOption: [], userOption: [] as any[],
}); });
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const formatDateTime = ( const formatDateTime = (
v?: string | number | Date, v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss", fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : ""); ) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const getCodeList = () => { const toRow = (w: any, no: number) => ({
// UserService.search(data.value.params).then((d) => { no,
// if (d.status === 200) { name: w.workflowName,
// data.value.userOption = d.data.userList; description: w.workflowDescription,
// } version: w.version,
// }); stepCount: w.stepCount,
}; configProgress: w.configProgress,
kubeflowStatus: w.kubeflowStatus,
const toRow = (workflow: any, index: number, offset: number) => ({ registDt: w.regDt,
no: offset + index + 1, deviceKey: w.id,
name: workflow.workflowName,
description: workflow.workflowDescription,
version: workflow.version,
stepCount: workflow.stepCount,
configProgress: workflow.configProgress,
kubeflowStatus: workflow.kubeflowStatus,
registDt: workflow.regDt,
deviceKey: workflow.id,
}); });
const getData = () => { const fetchList = () => {
const params = data.value.params; const projectId = Number(localStorage.getItem("projectId"));
const pageNum = params.pageNum; if (!projectId) {
const pageSize = params.pageSize; console.warn("[Workflows] projectId 없음 — 프로젝트 먼저 선택");
const startIndex = (pageNum - 1) * pageSize; data.value.results = [];
data.value.totalElements = 0;
data.value.pageLength = 0;
return;
}
const { pageNum, searchText, searchType } = 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,
};
AutoflowService.getAll() AutoflowService.search(payload)
.then((res) => { .then((res: any) => {
if (res.status !== 200) { if (res.status !== 200) return;
console.error("워크플로우 조회 실패:", res);
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 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; return;
} }
const result = res.data;
console.log(result);
const rawList = Array.isArray(result) ? result : (result.content ?? []);
const totalCount = Array.isArray(result)
? rawList.length
: (result.totalElements ?? rawList.length);
const currentPageList = Array.isArray(result)
? rawList.slice(startIndex, startIndex + pageSize)
: rawList;
data.value.results = currentPageList.map((w: any, i: number) =>
toRow(w, i, startIndex),
);
data.value.totalDataLength = totalCount;
setPaginationLength(); 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 - 1),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
}) })
.catch((err) => { .catch((err: any) => console.error("워크플로우 조회 에러:", err));
console.error("워크플로우 조회 에러:", err);
});
}; };
const setPaginationLength = () => {
const total = data.value.totalDataLength || 0; const doSearch = () => {
const pageSize = data.value.params.pageSize || 10; data.value.params.pageNum = 1;
data.value.pageLength = fetchList();
total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize);
}; };
const saveData = (formData: any) => { const changePageNum = (page: number) => {
if (data.value.modalMode === "create") { data.value.params.pageNum = page;
AutoflowService.add(formData) fetchList();
.then((res) => { };
if (res.status === 200 || res.status === 201) {
data.value.isCreateVisible = false;
store.setSnackbarMsg({
text: "등록 되었습니다.",
result: 200,
color: "success",
});
changePageNum(1); //
} else {
store.setSnackbarMsg({
text: "등록 실패",
result: 500,
color: "warning",
});
}
})
.catch((err) => {
console.error("등록 에러:", err);
store.setSnackbarMsg({
text: "등록 중 오류가 발생했습니다.",
result: 500,
color: "error",
});
})
.finally(() => {
getData();
});
} else {
//
const id =
Number(formData?.id) ??
Number(formData?.deviceKey) ??
Number(data.value.selectedData?.deviceKey) ??
Number(data.value.selectedData?.id);
if (!id) {
store.setSnackbarMsg({
text: "수정할 ID가 없습니다.",
result: 500,
color: "error",
});
return;
}
AutoflowService.update(id, formData) const changePageSize = (size: number) => {
.then((res) => { data.value.params.pageSize = size;
if (res.status === 200) { data.value.params.pageNum = 1;
data.value.isCreateVisible = false; fetchList();
store.setSnackbarMsg({
text: "수정 되었습니다.",
result: 200,
color: "success",
});
changePageNum(data.value.params.pageNum); //
} else {
store.setSnackbarMsg({
text: "수정 실패",
result: 500,
color: "warning",
});
}
})
.catch((err) => {
console.error("수정 에러:", err);
store.setSnackbarMsg({
text: "수정 중 오류가 발생했습니다.",
result: 500,
color: "error",
});
})
.finally(() => {
getData(); //
});
}
}; };
const removeData = (value) => { const removeData = (value?: Array<{ deviceKey: number }>) => {
const removeList = value ?? data.value.selected; const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return; if (!removeList || removeList.length === 0) return;
const ids = removeList.map((x) => x.deviceKey); const ids = removeList.map((x) => x.deviceKey);
const remove = (id: number) =>
const remove = (id) =>
AutoflowService.delete(id).then((res) => { AutoflowService.delete(id).then((res) => {
if (res.status < 200 || res.status >= 300) { if (res.status < 200 || res.status >= 300) return Promise.reject(res);
return Promise.reject(res);
}
}); });
const after = () => { const after = () => {
@ -283,12 +222,12 @@ const removeData = (value) => {
) { ) {
data.value.params.pageNum -= 1; data.value.params.pageNum -= 1;
} }
getData(); fetchList();
data.value.isConfirmDialogVisible = false; data.value.isConfirmDialogVisible = false;
data.value.selected = []; data.value.selected = [];
data.value.allSelected = false; data.value.allSelected = false;
}; };
console.log(ids.length);
if (ids.length === 1) { if (ids.length === 1) {
remove(ids[0]) remove(ids[0])
@ -329,40 +268,37 @@ const removeData = (value) => {
} }
}; };
/* =========================
* 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) { if (data.value.selected.length === 0) return;
// store.setSnackbarMsg({
// text: " . ",
// result: 500,
// });
return;
}
if (data.value.allSelected || data.value.selected.length !== 1) { if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true; data.value.isConfirmDialogVisible = true;
return; return;
} }
// removeData();
removeData(undefined);
};
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; data.value.selectedData = selectedItem;
openView.value = true; openView.value = true;
}; };
const closeDetail = () => (openView.value = false);
const openModifyModal = (item: any) => { const openModifyModal = (item: any) => {
console.log("[openModifyModal] row =", item);
data.value.selectedData = { data.value.selectedData = {
id: item.deviceKey, id: item.deviceKey,
workflowName: item.name, workflowName: item.name,
workflowDescription: item.description, workflowDescription: item.description,
}; };
data.value.modalMode = "edit"; data.value.modalMode = "edit";
data.value.isCreateVisible = true; data.value.isCreateVisible = true;
}; };
@ -378,40 +314,37 @@ const openUploadModal = () => {
data.value.modalMode = "upload"; data.value.modalMode = "upload";
data.value.isUploadVisible = true; data.value.isUploadVisible = true;
}; };
const closeCreateModal = () => { const closeCreateModal = () => {
data.value.isModalVisible = false; data.value.isModalVisible = false;
data.value.isCreateVisible = false; data.value.isCreateVisible = false;
}; };
const closeUploadModal = () => { const closeUploadModal = () => {
data.value.isModalVisible = false; data.value.isModalVisible = false;
data.value.isUploadVisible = false; data.value.isUploadVisible = false;
}; };
const getSelectedAllData = () => { /* =========================
data.value.selected = data.value.allSelected * Watchers / Lifecycle
? data.value.results.map((item) => { * ========================= */
return {
deviceKey: item.deviceKey,
};
})
: [];
};
watch( watch(
() => data.value.isCreateVisible, () => data.value.isCreateVisible,
(now, prev) => { (now, prev) => {
if (prev && !now) getData(); if (prev && !now) fetchList();
}, },
); );
watch( watch(
() => data.value.isUploadVisible, () => data.value.isUploadVisible,
(now, prev) => { (now, prev) => {
if (prev && !now) getData(); if (prev && !now) fetchList();
}, },
); );
onMounted(() => { onMounted(() => {
getData(); fetchList();
getCodeList(); // getCodeList(); //
}); });
</script> </script>
@ -439,13 +372,14 @@ 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
@ -456,7 +390,7 @@ 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-text-field>
</v-responsive> </v-responsive>
@ -465,7 +399,7 @@ 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>
@ -481,8 +415,8 @@ 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">
@ -495,7 +429,7 @@ 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-select>
</v-responsive> </v-responsive>
</v-sheet> </v-sheet>
@ -606,8 +540,6 @@ onMounted(() => {
:edit-data="data.selectedData" :edit-data="data.selectedData"
:mode="data.modalMode" :mode="data.modalMode"
@close-modal="closeCreateModal" @close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/> />
</v-dialog> </v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent> <v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
@ -615,8 +547,6 @@ onMounted(() => {
:edit-data="data.selectedData" :edit-data="data.selectedData"
:mode="data.modalMode" :mode="data.modalMode"
@close-modal="closeUploadModal" @close-modal="closeUploadModal"
@handle-data="saveData"
:user-option="data.userOption"
/> />
</v-dialog> </v-dialog>
</div> </div>

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

@ -5,141 +5,115 @@ const rootPath = import.meta.env.VITE_ROOT_PATH;
const routes = [ const routes = [
{ {
path: `/`, path: "/",
component: () => import("@/layouts/default.vue"), component: () => import("@/layouts/default.vue"),
redirect: { name: "login" }, redirect: { name: "login" },
children: [ children: [
/** ■ 공용(일반 사용자) 라우트 */
{ {
name: "main", name: "main",
path: `/main`, path: "/main",
meta: { meta: { title: "", requiresAuth: false },
title: "",
requiresAuth: false,
},
component: () => import("@/pages/MainView.vue"), component: () => import("@/pages/MainView.vue"),
}, },
{ {
name: "select", name: "select",
path: `/select`, path: "/select",
meta: { meta: { title: "select", requiresAuth: false, hideSidebar: true },
title: "select",
requiresAuth: false,
hideSidebar: true,
},
component: () => import("@/views/Select.vue"), component: () => import("@/views/Select.vue"),
}, },
{
name: "project",
path: `/project`,
meta: {
title: "Project",
requiresAuth: false,
},
component: () => import("@/pages/ProjectView.vue"),
},
{ {
name: "home", name: "home",
path: `/home`, path: "/home",
meta: { meta: { title: "Home", requiresAuth: false },
title: "Home",
requiresAuth: false,
},
component: () => import("@/pages/HomeView.vue"), component: () => import("@/pages/HomeView.vue"),
}, },
{ {
name: "workflows", name: "workflows",
path: `/workflows`, path: "/workflows",
meta: { meta: { title: "Workflows", requiresAuth: false },
title: "Workflows",
requiresAuth: false,
},
component: () => import("@/pages/WorkflowView.vue"), component: () => import("@/pages/WorkflowView.vue"),
}, },
{ {
name: "workflow-step-config", name: "workflow-step-config",
path: `/workflow-step-config`, path: "/workflow-step-config",
meta: { meta: { title: "Workflow Step Config", requiresAuth: false },
title: "Workflow Step Config",
requiresAuth: false,
},
component: () => import("@/pages/WorkflowStepConfigView.vue"), component: () => import("@/pages/WorkflowStepConfigView.vue"),
}, },
{ {
name: "run", name: "run",
path: `/run/experiment`, path: "/run/experiment",
meta: { meta: { title: "Run", requiresAuth: false },
title: "Run",
requiresAuth: false,
},
redirect: { name: "experiment" }, redirect: { name: "experiment" },
children: [ children: [
{ {
name: "experiment", name: "experiment",
path: `/run/experiment`, path: "/run/experiment",
meta: { meta: { title: "Experiment", requiresAuth: false },
title: "Experiment",
requiresAuth: false,
},
component: () => import("@/pages/ExperimentView.vue"), component: () => import("@/pages/ExperimentView.vue"),
}, },
{ {
name: "Executions", name: "Executions",
path: `/run/executions`, path: "/run/executions",
meta: { meta: { title: "Executions", requiresAuth: false },
title: "Executions",
requiresAuth: false,
},
component: () => import("@/pages/ExecutionsView.vue"), component: () => import("@/pages/ExecutionsView.vue"),
}, },
], ],
}, },
{ {
name: "deployment", name: "deployment",
path: `/deployment`, path: "/deployment",
meta: { meta: { title: "Deployment", requiresAuth: false },
title: "Deployment",
requiresAuth: false,
},
component: () => import("@/pages/DeploymentView.vue"), component: () => import("@/pages/DeploymentView.vue"),
}, },
{ {
name: "training-script", name: "training-script",
path: `/training-script`, path: "/training-script",
meta: { meta: { title: "Training Script", requiresAuth: true },
title: "Training Script",
requiresAuth: true,
},
component: () => import("@/pages/TrainingScriptView.vue"), component: () => import("@/pages/TrainingScriptView.vue"),
}, },
{ {
name: "datasets", name: "datasets",
path: `/datasets`, path: "/datasets",
meta: { title: "Datasets", requiresAuth: true },
component: () => import("@/pages/DatasetView.vue"),
},
/** ■ 관리자 전용 라우트 */
{
name: "project",
path: "/project",
meta: { meta: {
title: "Datasets", title: "Projects",
requiresAuth: true, requiresAuth: false,
requiresAdmin: true, // ✅ 관리자 전용
}, },
component: () => import("@/pages/DatasetView.vue"), component: () => import("@/pages/ProjectView.vue"),
},
{
name: "users",
path: "/users",
meta: {
title: "Users",
requiresAuth: false,
requiresAdmin: true, // ✅ 관리자 전용
},
component: () => import("@/pages/UsersView.vue"),
}, },
], ],
}, },
/** ■ 인증(로그인/회원가입) */
{ {
name: "login", name: "login",
path: `/login`, path: "/login",
meta: { meta: { title: "로그인", requiresAuth: false },
title: "로그인",
requiresAuth: false,
},
component: () => import("@/pages/LoginView.vue"), component: () => import("@/pages/LoginView.vue"),
}, },
{ {
name: "signup", name: "signup",
path: `/signup`, path: "/signup",
meta: { meta: { title: "회원가입", requiresAuth: false },
title: "회원가입",
requiresAuth: false,
},
component: () => import("@/pages/SignupView.vue"), component: () => import("@/pages/SignupView.vue"),
}, },
]; ];
@ -172,6 +146,29 @@ router.beforeEach((to) => {
return { name: "select", replace: true, query: { redirect: to.fullPath } }; return { name: "select", replace: true, query: { redirect: to.fullPath } };
} }
if (to.matched.some((r) => r.meta?.requiresAdmin)) {
try {
const raw =
typeof storage?.getAuth === "function"
? storage.getAuth()
: JSON.parse(localStorage.getItem("autoflow-auth") || "null");
const roles = raw?.userInfo?.roles ?? raw?.roles ?? [];
const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth;
const isAdmin =
(Array.isArray(roles)
? roles.includes("ROLE_ADMIN")
: roles === "ROLE_ADMIN") || authCd === "ADMIN";
if (!isAdmin) {
return { name: "home", replace: true };
}
} catch {
return { name: "home", replace: true };
}
}
return true; return true;
}); });

1
typed-router.d.ts vendored

@ -29,6 +29,7 @@ declare module 'vue-router/auto-routes' {
'/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>, '/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>, '/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>,
'/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>, '/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>,
'/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record<never, never>, Record<never, never>>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>, '/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>,
'/WorkflowView': RouteRecordInfo<'/WorkflowView', '/WorkflowView', Record<never, never>, Record<never, never>>, '/WorkflowView': RouteRecordInfo<'/WorkflowView', '/WorkflowView', Record<never, never>, Record<never, never>>,
} }

Loading…
Cancel
Save