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 props = defineProps<{
editData?: any;
mode?: "create" | "edit";
userOption?: any[];
editData: any;
mode: "create" | "edit";
}>();
const emit = defineEmits<{

@ -2,89 +2,83 @@
import { useRoute, useRouter } from "vue-router";
import { storage } from "@/utils/storage.js";
import DrawerComponent from "@/components/common/DrawerComponent.vue";
import { ref, watchEffect } from "vue";
import Select from "@/views/Select.vue";
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const route = useRoute();
const router = useRouter();
const username = ref("");
const autoflow = useAutoflowStore();
const showPasswordModal = ref(false);
const selectedUserData = ref({});
const menu = ref([]);
const username = ref("");
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 = [
{
title: "Select Project",
click: () => {
goSelect();
},
},
{ title: "Select Project", click: () => goSelect() },
{
title: "Change Password",
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 pageTitle = computed(() => {
return route.meta.title;
});
const pageTitle = computed(() => route.meta.title);
const pagePath = computed(() => route.path);
const pagePath = computed(() => {
return route.path;
});
const refreshProjectName = () => {
function updateUsername() {
const auth = storage.getAuth?.() ?? null;
username.value = auth?.userInfo?.username ?? auth?.username ?? "";
}
function refreshProjectName() {
const v = localStorage.getItem("projectName");
projectName.value = v ? v : "";
};
const goSelect = () => {
}
function goSelect() {
router.push("/select");
};
const logOut = () => {
}
function logOut() {
UserManagerService.signOut()
.catch(console.error)
.finally(() => {
@ -94,49 +88,41 @@ const logOut = () => {
username.value = "";
projectName.value = "";
sessionStorage.removeItem("initialRedirectDone");
adminMode.value = false; //
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(() => {
updateUsername();
computeIsAdmin();
refreshProjectName();
menu.value = menuItems;
// 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();
});
window.addEventListener("storage", onStorage);
});
onBeforeUnmount(() => {
window.removeEventListener("storage", updateUsername);
window.removeEventListener("storage", onStorage);
});
//
watch(
() => route.fullPath,
() => refreshProjectName(),
);
watchEffect(() => {
// const auth = storage.getAuth().auth;
// if (auth === "ADMIN") {
menu.value = menuItems;
// } else {
// menu.value = userMenuItems;
// }
});
</script>
<template>
<v-app>
<!-- 사이드바: adminMode에 따라 바꿔치기 -->
<v-navigation-drawer
v-model="drawer"
border="0"
@ -144,39 +130,74 @@ watchEffect(() => {
permanent
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-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">
<template v-slot: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>
<template #activator="{ props }">
<v-btn icon color="primary" v-bind="props" class="mr-3">
<v-icon>mdi-arrow-down-drop-circle-outline</v-icon>
</v-btn>

@ -7,3 +7,15 @@ export interface Workflow {
modDt: string;
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";
export const AutoflowService = {
add: (payload: Workflow) => {
@ -17,4 +20,7 @@ export const AutoflowService = {
update: (id: number, payload: Workflow) => {
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">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
/* =========================
* 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 ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.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 dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone";
const store = commonStore();
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
// import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
const openView = ref(false);
const tableHeader = [
{
label: "No",
width: "5%",
style: "word-break: keep-all;",
},
{
label: "Workflow Name",
width: "7%",
style: "word-break: keep-all;",
},
/* =========================
* Constants / Types
* ========================= */
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
{
label: "Step Count",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Config Progress",
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;",
},
type SearchType = "전체" | "제목" | "작성자";
const tableHeader = [
{ 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", 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 = [
{
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 pageSizeOptions = [
@ -92,188 +49,170 @@ const pageSizeOptions = [
{ 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({
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" | "upload" | "",
selectedData: null as any,
allSelected: false,
selected: [],
selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
userOption: [] as any[],
});
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const formatDateTime = (
v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const toRow = (workflow: any, index: number, offset: number) => ({
no: offset + index + 1,
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 toRow = (w: any, no: number) => ({
no,
name: w.workflowName,
description: w.workflowDescription,
version: w.version,
stepCount: w.stepCount,
configProgress: w.configProgress,
kubeflowStatus: w.kubeflowStatus,
registDt: w.regDt,
deviceKey: w.id,
});
const getData = () => {
const params = data.value.params;
const pageNum = params.pageNum;
const pageSize = params.pageSize;
const startIndex = (pageNum - 1) * pageSize;
const fetchList = () => {
const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) {
console.warn("[Workflows] projectId 없음 — 프로젝트 먼저 선택");
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()
.then((res) => {
if (res.status !== 200) {
console.error("워크플로우 조회 실패:", res);
AutoflowService.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 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 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) => {
console.error("워크플로우 조회 에러:", err);
});
.catch((err: any) => console.error("워크플로우 조회 에러:", err));
};
const setPaginationLength = () => {
const total = data.value.totalDataLength || 0;
const pageSize = data.value.params.pageSize || 10;
data.value.pageLength =
total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize);
const doSearch = () => {
data.value.params.pageNum = 1;
fetchList();
};
const saveData = (formData: any) => {
if (data.value.modalMode === "create") {
AutoflowService.add(formData)
.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;
}
const changePageNum = (page: number) => {
data.value.params.pageNum = page;
fetchList();
};
AutoflowService.update(id, formData)
.then((res) => {
if (res.status === 200) {
data.value.isCreateVisible = false;
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 changePageSize = (size: number) => {
data.value.params.pageSize = size;
data.value.params.pageNum = 1;
fetchList();
};
const removeData = (value) => {
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 remove = (id) =>
const remove = (id: number) =>
AutoflowService.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);
});
const after = () => {
@ -283,12 +222,12 @@ const removeData = (value) => {
) {
data.value.params.pageNum -= 1;
}
getData();
fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
console.log(ids.length);
if (ids.length === 1) {
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 = () => {
if (data.value.selected.length === 0) {
// store.setSnackbarMsg({
// text: " . ",
// result: 500,
// });
return;
}
if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
//
removeData(undefined);
};
const closeDetail = () => {
openView.value = false;
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
removeData();
};
const openDetailModal = (selectedItem) => {
const openDetailModal = (selectedItem: any) => {
data.value.selectedData = selectedItem;
openView.value = true;
};
const closeDetail = () => (openView.value = false);
const openModifyModal = (item: any) => {
console.log("[openModifyModal] row =", item);
data.value.selectedData = {
id: item.deviceKey,
workflowName: item.name,
workflowDescription: item.description,
};
data.value.modalMode = "edit";
data.value.isCreateVisible = true;
};
@ -378,40 +314,37 @@ const openUploadModal = () => {
data.value.modalMode = "upload";
data.value.isUploadVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
data.value.isCreateVisible = false;
};
const closeUploadModal = () => {
data.value.isModalVisible = false;
data.value.isUploadVisible = false;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
/* =========================
* Watchers / Lifecycle
* ========================= */
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) getData();
if (prev && !now) fetchList();
},
);
watch(
() => data.value.isUploadVisible,
(now, prev) => {
if (prev && !now) getData();
if (prev && !now) fetchList();
},
);
onMounted(() => {
getData();
getCodeList();
fetchList();
// getCodeList(); //
});
</script>
@ -439,13 +372,14 @@ 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
@ -456,7 +390,7 @@ onMounted(() => {
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
@keyup.enter="doSearch"
></v-text-field>
</v-responsive>
@ -465,7 +399,7 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
@click="doSearch"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
@ -481,8 +415,8 @@ 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">
@ -495,7 +429,7 @@ onMounted(() => {
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
@update:model-value="changePageSize"
></v-select>
</v-responsive>
</v-sheet>
@ -606,8 +540,6 @@ onMounted(() => {
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
@ -615,8 +547,6 @@ onMounted(() => {
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeUploadModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
</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 = [
{
path: `/`,
path: "/",
component: () => import("@/layouts/default.vue"),
redirect: { name: "login" },
children: [
/** ■ 공용(일반 사용자) 라우트 */
{
name: "main",
path: `/main`,
meta: {
title: "",
requiresAuth: false,
},
path: "/main",
meta: { title: "", requiresAuth: false },
component: () => import("@/pages/MainView.vue"),
},
{
name: "select",
path: `/select`,
meta: {
title: "select",
requiresAuth: false,
hideSidebar: true,
},
path: "/select",
meta: { title: "select", requiresAuth: false, hideSidebar: true },
component: () => import("@/views/Select.vue"),
},
{
name: "project",
path: `/project`,
meta: {
title: "Project",
requiresAuth: false,
},
component: () => import("@/pages/ProjectView.vue"),
},
{
name: "home",
path: `/home`,
meta: {
title: "Home",
requiresAuth: false,
},
path: "/home",
meta: { title: "Home", requiresAuth: false },
component: () => import("@/pages/HomeView.vue"),
},
{
name: "workflows",
path: `/workflows`,
meta: {
title: "Workflows",
requiresAuth: false,
},
path: "/workflows",
meta: { title: "Workflows", requiresAuth: false },
component: () => import("@/pages/WorkflowView.vue"),
},
{
name: "workflow-step-config",
path: `/workflow-step-config`,
meta: {
title: "Workflow Step Config",
requiresAuth: false,
},
path: "/workflow-step-config",
meta: { title: "Workflow Step Config", requiresAuth: false },
component: () => import("@/pages/WorkflowStepConfigView.vue"),
},
{
name: "run",
path: `/run/experiment`,
meta: {
title: "Run",
requiresAuth: false,
},
path: "/run/experiment",
meta: { title: "Run", requiresAuth: false },
redirect: { name: "experiment" },
children: [
{
name: "experiment",
path: `/run/experiment`,
meta: {
title: "Experiment",
requiresAuth: false,
},
path: "/run/experiment",
meta: { title: "Experiment", requiresAuth: false },
component: () => import("@/pages/ExperimentView.vue"),
},
{
name: "Executions",
path: `/run/executions`,
meta: {
title: "Executions",
requiresAuth: false,
},
path: "/run/executions",
meta: { title: "Executions", requiresAuth: false },
component: () => import("@/pages/ExecutionsView.vue"),
},
],
},
{
name: "deployment",
path: `/deployment`,
meta: {
title: "Deployment",
requiresAuth: false,
},
path: "/deployment",
meta: { title: "Deployment", requiresAuth: false },
component: () => import("@/pages/DeploymentView.vue"),
},
{
name: "training-script",
path: `/training-script`,
meta: {
title: "Training Script",
requiresAuth: true,
},
path: "/training-script",
meta: { title: "Training Script", requiresAuth: true },
component: () => import("@/pages/TrainingScriptView.vue"),
},
{
name: "datasets",
path: `/datasets`,
path: "/datasets",
meta: { title: "Datasets", requiresAuth: true },
component: () => import("@/pages/DatasetView.vue"),
},
/** ■ 관리자 전용 라우트 */
{
name: "project",
path: "/project",
meta: {
title: "Datasets",
requiresAuth: true,
title: "Projects",
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",
path: `/login`,
meta: {
title: "로그인",
requiresAuth: false,
},
path: "/login",
meta: { title: "로그인", requiresAuth: false },
component: () => import("@/pages/LoginView.vue"),
},
{
name: "signup",
path: `/signup`,
meta: {
title: "회원가입",
requiresAuth: false,
},
path: "/signup",
meta: { title: "회원가입", requiresAuth: false },
component: () => import("@/pages/SignupView.vue"),
},
];
@ -172,6 +146,29 @@ router.beforeEach((to) => {
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;
});

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>>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', 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>>,
'/WorkflowView': RouteRecordInfo<'/WorkflowView', '/WorkflowView', Record<never, never>, Record<never, never>>,
}

Loading…
Cancel
Save