feat : 프로젝트 권한 api 추가, workflow api 추가

main
jschoi 9 months ago
parent f4ea2f30b7
commit fe5c03229b

@ -1,3 +1,3 @@
NODE_ENV = "dev" NODE_ENV = "dev"
VITE_APP_API_SERVER_URL = "http://10.10.11.144:8080" VITE_APP_API_SERVER_URL = "http://localhost:80"
VITE_ROOT_PATH = "" VITE_ROOT_PATH = ""

22
components.d.ts vendored

@ -9,28 +9,28 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppFooter: typeof import('./src/components/AppFooter.vue')['default'] AppFooter: typeof import('./src/components/AppFooter.vue')['default']
CompareComponent: typeof import('./src/components/run/executions/CompareComponent.vue')['default'] CompareComponent: typeof import('./src/components/templates/run/executions/CompareComponent.vue')['default']
DatasetsBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetsBaseDoalog.vue')['default'] DatasetsBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetsBaseDoalog.vue')['default']
DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default'] DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default']
DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default'] DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default']
ExecutionBaseDialog: typeof import('./src/components/atoms/organisms/ExecutionBaseDialog.vue')['default'] ExecutionBaseDialog: typeof import('./src/components/atoms/organisms/ExecutionBaseDialog.vue')['default']
ExperimentCreateDialog: typeof import('./src/components/atoms/organisms/ExperimentCreateDialog.vue')['default'] ExperimentCreateDialog: typeof import('./src/components/atoms/organisms/ExperimentCreateDialog.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IconArrowDown: typeof import('./src/components/button/IconArrowDown.vue')['default'] IconArrowDown: typeof import('./src/components/atoms/button/IconArrowDown.vue')['default']
IconArrowUp: typeof import('./src/components/button/IconArrowUp.vue')['default'] IconArrowUp: typeof import('./src/components/atoms/button/IconArrowUp.vue')['default']
IconDeleteBtn: typeof import('./src/components/button/IconDeleteBtn.vue')['default'] IconDeleteBtn: typeof import('./src/components/atoms/button/IconDeleteBtn.vue')['default']
IconDeployment: typeof import('./src/components/button/IconDeployment.vue')['default'] IconDeployment: typeof import('./src/components/atoms/button/IconDeployment.vue')['default']
IconDownloadBtn: typeof import('./src/components/button/IconDownloadBtn.vue')['default'] IconDownloadBtn: typeof import('./src/components/atoms/button/IconDownloadBtn.vue')['default']
IconInfoBtn: typeof import('./src/components/button/IconInfoBtn.vue')['default'] IconInfoBtn: typeof import('./src/components/atoms/button/IconInfoBtn.vue')['default']
IconModifyBtn: typeof import('./src/components/button/IconModifyBtn.vue')['default'] IconModifyBtn: typeof import('./src/components/atoms/button/IconModifyBtn.vue')['default']
IconSettingBtn: typeof import('./src/components/button/IconSettingBtn.vue')['default'] IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.vue')['default']
LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default'] LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default']
ListComponent: typeof import('./src/components/Datasets/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']
StapComfigDialog: typeof import('./src/components/atoms/organisms/StapComfigDialog.vue')['default'] StapComfigDialog: typeof import('./src/components/atoms/organisms/StapComfigDialog.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/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']
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']

@ -5,5 +5,5 @@
</template> </template>
<script setup> <script setup>
// //
</script> </script>

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import IconArrowDown from "@/components/button/IconArrowDown.vue"; import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
import IconArrowUp from "@/components/button/IconArrowUp.vue"; import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
const steps = ref([ const steps = ref([

@ -1,14 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import IconArrowDown from "@/components/button/IconArrowDown.vue"; import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
import IconArrowUp from "@/components/button/IconArrowUp.vue"; import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { AutoflowService } from "@/components/service/management/AutoflowService"; import { AutoflowService } from "@/components/service/management/AutoflowService";
import { storage } from "@/utils/storage"; import { storage } from "@/utils/storage";
import { Workflow } from "@/components/models/management/Autoflow"; import { Workflow } from "@/components/models/management/Autoflow";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const saving = ref(false); const saving = ref(false);
const errorMsg = ref(""); const errorMsg = ref("");
const { projectId } = storeToRefs(useAutoflowStore());
const steps = ref([ const steps = ref([
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" }, { order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
{ {
@ -76,6 +79,7 @@ const submit = async () => {
regUserId, regUserId,
regDt: now, regDt: now,
modDt: now, modDt: now,
projectId: projectId.value,
}; };
try { try {

@ -1,5 +1,5 @@
<script setup> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { menuUtils } from "@/utils/menuUtils"; import { menuUtils } from "@/utils/menuUtils";
@ -11,6 +11,35 @@ const router = useRouter();
const isShowAuth = ref(false); const isShowAuth = ref(false);
function readRolesFromStorage(): string[] {
try {
// storage.get(...) ,
const raw =
storage.get?.("autoflow-auth") ??
localStorage.getItem("autoflow-auth") ??
null;
const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
let roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
// "ROLE_USER,ROLE_ADMIN"
if (typeof roles === "string") {
roles = roles.split(",").map((s: string) => s.trim());
}
//
if (!Array.isArray(roles)) return [];
return roles;
} catch {
return [];
}
}
// ADMIN (ROLE_ADMIN ADMIN )
const isAdmin = computed(() => {
const roles = readRolesFromStorage();
return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN");
});
const isLinkActive = (link) => { const isLinkActive = (link) => {
return route.path.includes(link); return route.path.includes(link);
}; };
@ -72,6 +101,7 @@ onMounted(() => {
</template> </template>
<template v-else> <template v-else>
<v-list-item <v-list-item
v-if="value !== 'project' || isAdmin"
rounded rounded
:title="title" :title="title"
:value="value" :value="value"

@ -2,22 +2,31 @@
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 { watchEffect } from "vue"; import { ref, watchEffect } from "vue";
import Select from "@/views/Select.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 { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore"; import { useAutoflowStore } from "@/stores/autoflowStore";
const route = useRoute();
const router = useRouter();
const username = ref("");
const autoflow = useAutoflowStore(); const autoflow = useAutoflowStore();
const { ProjectName } = storeToRefs(autoflow);
const showPasswordModal = ref(false); const showPasswordModal = ref(false);
const selectedUserData = ref({}); const selectedUserData = ref({});
const route = useRoute();
const router = useRouter();
const menu = ref([]); const menu = ref([]);
const projectName = ref(localStorage.getItem("projectName") || "");
const updateUsername = () => {
// storage : { userInfo: { username, ... }, ... }
const auth = storage.getAuth?.() ?? null;
username.value =
auth?.userInfo?.username ??
auth?.username ?? //
""; //
};
const menuItems = [ const menuItems = [
{ {
@ -65,26 +74,55 @@ const pageTitle = computed(() => {
const pagePath = computed(() => { const pagePath = computed(() => {
return route.path; return route.path;
}); });
const refreshProjectName = () => {
const v = localStorage.getItem("projectName");
projectName.value = v ? v : "";
};
const goSelect = () => { const goSelect = () => {
storage.clearAuth();
router.push("/select"); router.push("/select");
}; };
const logOut = () => { const logOut = () => {
UserManagerService.signOut() UserManagerService.signOut()
.catch((err) => { .catch(console.error)
console.error("logout API failed:", err);
})
.finally(() => { .finally(() => {
autoflow.setProjectName(""); localStorage.removeItem("projectName");
storage.clearAuth(); localStorage.removeItem("projectId");
username.value = "";
projectName.value = "";
router.push("/login"); router.push("/login");
}); });
}; };
const goHome = () => {
router.push("/main");
};
onMounted(() => {
updateUsername();
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();
});
});
onBeforeUnmount(() => {
window.removeEventListener("storage", updateUsername);
});
watch(
() => route.fullPath,
() => refreshProjectName(),
);
watchEffect(() => { watchEffect(() => {
// const auth = storage.getAuth().auth; // const auth = storage.getAuth().auth;
// if (auth === "ADMIN") { // if (auth === "ADMIN") {
@ -132,9 +170,9 @@ watchEffect(() => {
</template> </template>
</v-tooltip> </v-tooltip>
<div style="min-width: 180px" class="d-flex flex-column align-end"> <div style="min-width: 180px" class="d-flex flex-column align-end">
<div class="font-weight-black">ADMIN_001</div> <div class="font-weight-black">{{ username || "GUEST" }}</div>
<div class="text-subtitle-2"> <div class="text-subtitle-2">
{{ ProjectName || "No Project Selected" }} {{ projectName || "No Project Selected" }}
</div> </div>
</div> </div>
<v-btn icon color="primary" v-bind="props" class="mr-3"> <v-btn icon color="primary" v-bind="props" class="mr-3">

@ -5,4 +5,5 @@ export interface Workflow {
regUserId: string; regUserId: string;
regDt: string; regDt: string;
modDt: string; modDt: string;
projectId: number;
} }

@ -21,3 +21,23 @@ export interface UiProject {
date: string; date: string;
description: string; description: string;
} }
export type Permission = "CREATE" | "READ" | "UPDATE" | "DELETE";
export interface ProjectAuthority {
projectId: number;
userId: number | string;
permissions: Permission[];
}
export interface ProjectSearchParams {
page: number;
size: number;
keyword: string;
searchType: string;
startDate: string;
endDate: string;
sortField: string;
sortDirection: string;
projectId: number;
}

@ -78,16 +78,19 @@ export const request = {
}, },
}; };
// axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = "*"; axios.defaults.headers.common["Access-Control-Allow-Origin"] = "*";
axios.interceptors.request.use( axios.interceptors.request.use(
(config) => { (config) => {
loading.setLoading(true); loading.setLoading(true);
const token = storage.getToken(); const token = storage.getToken();
const refresh = storage.getRefreshToken();
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
if (token) (config.headers as any)["cuuva-jwt"] = token;
if (refresh) (config.headers as any)["cuuva-jwt-refresh"] = refresh;
return config; return config;
}, },

@ -9,4 +9,7 @@ export const AutoflowService = {
delete: (id: Number) => { delete: (id: Number) => {
return request.delete(`/api/workflows/${id}`, {}); return request.delete(`/api/workflows/${id}`, {});
}, },
view: (id: Number) => {
return request.get(`/api/workflows/${id}`, {});
},
}; };

@ -2,26 +2,28 @@ import { request } from "@/components/service/index";
import { UserSearch, User } from "@/components/models/management/User"; import { UserSearch, User } from "@/components/models/management/User";
export const UserManagerService = { export const UserManagerService = {
// 로그인
signIn: (payload: User) => { signIn: (payload: User) => {
return request.post("/api/auth/signin", payload); return request.post("/api/auth/signin", payload);
}, },
// 회원가입
signUp: (payload: User) => { signUp: (payload: User) => {
return request.post("/api/auth/signup", payload); return request.post("/api/auth/signup", payload);
}, },
signOut: () => request.post("/api/auth/signout", {}), // 로그아웃
signOut: () => {
getAll: () => request.get("/api/auth/users", {}), return request.post("/api/auth/signout", {});
select: (param: User) => {
return request.post("/management/user/select", param);
}, },
add: (param: User) => { // 토큰 갱신
return request.post("/management/user/add", param); refreshtoken: () => {
return request.post("/api/auth/refreshtoken", {});
}, },
update: (param: User) => { // 전체 사용자 조회
return request.post("/management/user/update", param); getAll: () => {
return request.get("/api/auth/users", {});
}, },
delete: (param: User) => { // 사용자 조회
return request.delete("/management/user/delete", param); getUser: (userId: number) => {
return request.get(`/api/auth/users/${userId}`, {});
}, },
}; };

@ -1,13 +1,46 @@
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
import { ApiProject } from "@/components/models/project/Project"; import {
ApiProject,
ProjectAuthority,
ProjectSearchParams,
} from "@/components/models/project/Project";
export const ProjectService = { export const ProjectService = {
search: () => request.get("/api/projects", {}), // ID로 프로젝트 조회
add: (payload: ApiProject) => { fetchProjectById: (id: number) => {
return request.post("/api/projects", payload); return request.get(`/api/projects/${id}`, {});
}, },
// 프로젝트 수정
update: (id: number, payload: ApiProject) => { update: (id: number, payload: ApiProject) => {
return request.put(`/api/projects/${id}`, payload); return request.put(`/api/projects/${id}`, payload);
}, },
delete: (id: number) => request.delete(`/api/projects/${id}`, {}), // 프로젝트 삭제
delete: (id: number) => {
return request.delete(`/api/projects/${id}`, {});
},
// 전체 프로젝트 목록 조회
search: () => {
return request.get("/api/projects", {});
},
// 프로젝트 생성
add: (payload: ApiProject) => {
return request.post("/api/projects", payload);
},
// 검색 및 페이지네이션 프로젝트 목록 조회
searchProjects: (params: ProjectSearchParams) =>
request.get("/api/projects/search", params),
// ----------------------------------------------------------------------
// 프로젝트 권한
projectAuthority: (projectId: number, payload: ProjectAuthority) => {
return request.post(`/api/projects/${projectId}/users`, payload);
},
deleteProjectAuthority: (projectId: number, userId: number) => {
return request.delete(
`/api/projects/${projectId}/users/${userId}/permissions`,
{},
);
},
}; };

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/Datasets/ViewComponent.vue"; import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue";
import DatasetsBaseDoalog from "@/components/atoms/organisms/DatasetsBaseDoalog.vue"; import DatasetsBaseDoalog from "@/components/atoms/organisms/DatasetsBaseDoalog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
// const store = commonStore(); // const store = commonStore();

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
// const store = commonStore(); // const store = commonStore();

@ -0,0 +1,599 @@
<script setup lang="ts">
import { ref, onMounted, watch, computed } from "vue";
import { commonStore } from "@/stores/commonStore";
import { storage } from "@/utils/storage.js";
import { ProjectService } from "@/components/service/project/projectService";
import { UserManagerService } from "@/components/service/management/userManagerService";
import type {
Permission,
ApiProject,
} from "@/components/models/project/Project";
//
// /
//
const store = commonStore();
const DEFAULT_PERMISSIONS: Permission[] = [
"CREATE",
"READ",
"UPDATE",
"DELETE",
];
const roles = ref<string[]>([]);
const refreshRoles = () => {
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
const r = auth?.userInfo?.roles ?? auth?.roles ?? [];
roles.value = Array.isArray(r) ? r : [];
};
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
//
// / (UI )
//
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Project Name", width: "20%", style: "word-break: keep-all;" },
{ label: "Description", width: "10%", style: "word-break: keep-all;" },
{ label: "Select Users", width: "12%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "18%", style: "word-break: keep-all;" },
{ label: "Action", width: "13%", style: "word-break: keep-all;" },
];
const searchOptions = [
{ searchType: "전체", searchText: "" },
{ searchType: "프로젝트명", searchText: "prjNm" },
{ searchType: "설명", searchText: "prjDesc" },
{ searchType: "생성자", searchText: "regUserId" },
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
type Row = {
no: number;
name: string;
desc: string;
users: string[];
registDt: string;
deviceKey: number;
};
const data = ref({
params: { pageNum: 1, pageSize: 10, searchType: "", searchText: "" },
results: [] as Row[],
totalDataLength: 0,
pageLength: 0,
modalMode: "" as "create" | "edit" | "",
selectedData: null as Row | null,
allSelected: false,
selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
});
const fmtDate = (v?: string) => (v ? v.replace("T", " ").slice(0, 19) : "-");
const splitCsv = (v?: string) =>
String(v ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
//
/** 사용자 목록 (v-select items) */
//
type UserOption = { id: number | string; username: string };
const userOptions = ref<UserOption[]>([]);
async function loadUsers() {
const { data } = await UserManagerService.getAll();
const raw = data as Array<{ id: number | string; username: string }>;
userOptions.value = raw.map((u) => ({ id: u.id, username: u.username }));
}
//
/** 목록 로드: 카드형 로직과 동일한 응답을 테이블 Row로 매핑 */
//
function toRow(p: any, index: number, offset: number): Row {
return {
no: offset + index + 1,
name: p.prjNm ?? "-",
desc: p.prjDesc ?? "-",
users: splitCsv(p.regUserId ?? p.regUserNm),
registDt: fmtDate(p.regDate ?? p.prjStartDt),
deviceKey: p.id,
};
}
async function getData() {
const { pageNum, pageSize } = data.value.params;
const startIndex = (pageNum - 1) * pageSize;
const res = await ProjectService.search();
const raw = Array.isArray(res.data) ? res.data : (res.data?.content ?? []);
data.value.totalDataLength = Array.isArray(res.data)
? raw.length
: (res.data?.totalElements ?? raw.length);
const slice = Array.isArray(res.data)
? raw.slice(startIndex, startIndex + pageSize)
: raw;
data.value.results = slice.map((p: any, i: number) =>
toRow(p, i, startIndex),
);
const total = data.value.totalDataLength || 0;
data.value.pageLength =
total % data.value.params.pageSize === 0
? total / data.value.params.pageSize
: Math.ceil(total / data.value.params.pageSize);
}
//
/** 폼 & 권한 부여 & 저장 흐름 (카드형과 동일) */
//
const form = ref({
prjCd: "",
prjNm: "",
prjDesc: "",
selectedUsers: [] as string[],
});
const editingProjectId = ref<number | null>(null);
const resetForm = () => {
form.value.prjCd = `PRJ${Date.now()}`;
form.value.prjNm = "";
form.value.prjDesc = "";
form.value.selectedUsers = [];
};
const buildApiPayload = (): ApiProject => {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
const namesCsv = form.value.selectedUsers.join(",");
return {
id: data.value.modalMode === "edit" ? editingProjectId.value! : null,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
prjStartDt: today,
prjEndDt: today,
delYn: "N",
regDate: nowIso,
regUserId: namesCsv,
regUserNm: namesCsv,
modDate: nowIso,
modUserId: namesCsv,
modUserNm: namesCsv,
};
};
async function grantDefaultPermissions(projectId: number, usernames: string[]) {
if (!usernames?.length) return;
const set = new Set(usernames);
const numericIds = userOptions.value
.filter((u) => set.has(u.username))
.map((u) => Number(u.id))
.filter((n) => Number.isFinite(n));
await Promise.all(
numericIds.map((uid) =>
ProjectService.projectAuthority(projectId, {
projectId,
userId: uid,
permissions: DEFAULT_PERMISSIONS,
}),
),
);
}
async function saveProject() {
try {
const payload = buildApiPayload();
let projectId: number;
if (data.value.modalMode === "create") {
const res = await ProjectService.add(payload);
projectId = res.data.id;
} else {
await ProjectService.update(editingProjectId.value!, payload);
projectId = editingProjectId.value!;
}
await grantDefaultPermissions(projectId, form.value.selectedUsers);
await getData();
data.value.isCreateVisible = false;
} catch (e) {
console.error(`${data.value.modalMode} 실패:`, e);
}
}
//
//
//
async function deleteRows(targetList?: Array<{ deviceKey: number }>) {
const removeList = targetList ?? data.value.selected;
if (!removeList?.length) return;
const ids = removeList.map((x) => x.deviceKey);
const remove = (id: number) =>
ProjectService.delete(id).then((res) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
const after = async () => {
if (
ids.length >= data.value.results.length &&
data.value.params.pageNum > 1
) {
data.value.params.pageNum -= 1;
}
await getData();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
if (ids.length === 1) {
try {
await remove(ids[0]);
store.setSnackbarMsg?.({
color: "success",
text: "삭제되었습니다.",
result: 200,
});
} catch (err) {
store.setSnackbarMsg?.({
color: "warning",
text: "삭제 실패",
result: 500,
});
console.error(err);
} finally {
after();
}
} else {
Promise.all(ids.map(remove))
.then(() =>
store.setSnackbarMsg?.({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
}),
)
.catch((err) => {
store.setSnackbarMsg?.({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
console.error(err);
})
.finally(after);
}
}
//
// UI ( / )
//
function getSelectedAllData() {
data.value.selected = data.value.allSelected
? data.value.results.map((r) => ({ deviceKey: r.deviceKey }))
: [];
}
function changePageNum(page: number) {
data.value.params.pageNum = page;
getData();
}
function openCreateModal() {
data.value.modalMode = "create";
editingProjectId.value = null;
resetForm();
data.value.isCreateVisible = true;
}
function openEditModal(row: Row) {
data.value.modalMode = "edit";
editingProjectId.value = row.deviceKey;
form.value.prjCd = row.name;
form.value.prjNm = row.name;
form.value.prjDesc = row.desc === "-" ? "" : row.desc;
form.value.selectedUsers = Array.isArray(row.users) ? [...row.users] : [];
// v-select
const known = new Set(userOptions.value.map((u) => u.username));
const missing = form.value.selectedUsers
.filter((u) => !known.has(u))
.map((u) => ({ id: u, username: u }));
if (missing.length) userOptions.value = [...userOptions.value, ...missing];
data.value.isCreateVisible = true;
}
function openDetailModal(row: Row) {
data.value.selectedData = row;
}
function closeDetail() {
data.value.selectedData = null;
}
//
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) getData();
},
);
//
//
//
onMounted(async () => {
refreshRoles();
await Promise.all([loadUsers(), getData()]);
});
</script>
<template>
<div class="w-100">
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<!-- 헤더 -->
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Project</div>
</div>
</v-card-item>
</v-card>
<!-- 검색/페이지 -->
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
/>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
/>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}</v-chip
>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
/>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create Project</v-btn
>
</v-sheet>
</v-sheet>
<!-- 테이블 -->
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
/>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{ deviceKey: item.deviceKey }"
/>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<!-- Description -->
<td>
<div class="truncate-2">{{ item.desc }}</div>
</td>
<!-- Select Users -->
<td>
<template v-if="item.users?.length">
<v-chip
v-for="u in item.users"
:key="u"
size="small"
class="ma-1"
color="blue-lighten-2"
text-color="white"
>
{{ u }}
</v-chip>
</template>
<span v-else>-</span>
</td>
<td>{{ item.registDt }}</td>
<td style="white-space: nowrap">
<IconModifyBtn @on-click="openEditModal(item)" />
<IconDeleteBtn
@on-click="
deleteRows([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="changePageNum"
/>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<!-- 생성/수정 다이얼로그 -->
<v-dialog v-model="data.isCreateVisible" max-width="560" persistent>
<v-card>
<v-card-title class="headline">
{{
data.modalMode === "create" ? "Create Project" : "Modify Project"
}}
</v-card-title>
<v-card-text>
<v-form>
<v-text-field label="Project Name" v-model="form.prjNm" required />
<v-textarea
label="Description"
v-model="form.prjDesc"
rows="3"
required
/>
<v-select
label="Select Users"
v-model="form.selectedUsers"
:items="userOptions"
item-title="username"
item-value="username"
multiple
chips
closable-chips
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="data.isCreateVisible = false">Cancel</v-btn>
<v-btn color="primary" @click="saveProject">
{{ data.modalMode === "create" ? "Create" : "Save" }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<style scoped></style>

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconSettingBtn from "@/components/button/IconSettingBtn.vue"; import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/deployment/ViewComponent.vue"; import ViewComponent from "@/components/templates/deployment/ViewComponent.vue";
import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue"; import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue";
// const store = commonStore(); // const store = commonStore();

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import IconDownloadBtn from "@/components/button/IconDownloadBtn.vue"; import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
import CompareComponent from "@/components/run/executions/CompareComponent.vue"; import CompareComponent from "@/components/templates/run/executions/CompareComponent.vue";
import ViewComponent from "@/components/run/executions/ViewComponent.vue"; import ViewComponent from "@/components/templates/run/executions/ViewComponent.vue";
import ExecutionBaseDialog from "@/components/atoms/organisms/ExecutionBaseDialog.vue"; import ExecutionBaseDialog from "@/components/atoms/organisms/ExecutionBaseDialog.vue";
// const store = commonStore(); // const store = commonStore();

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconInfoBtn from "@/components/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/run/experiment/ViewComponent.vue"; import ViewComponent from "@/components/templates/run/experiment/ViewComponent.vue";
import ExperimentCreateDialog from "@/components/atoms/organisms/ExperimentCreateDialog.vue"; import ExperimentCreateDialog from "@/components/atoms/organisms/ExperimentCreateDialog.vue";
// const store = commonStore(); // const store = commonStore();

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/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/StapComfigDialog.vue";
// const store = commonStore(); // const store = commonStore();
@ -323,20 +323,6 @@ const getSelectedAllData = () => {
: []; : [];
}; };
// Save
// const saveStep = () => {
// if (!data.value.selectedData) return;
// //
// data.value.selectedData.workflow = selectedWorkflow.value;
// data.value.selectedData.stepName = stepName.value;
// // TODO: API or saveData(data.value.selectedData)
// // saveData(data.value.selectedData)
// openModify.value = false;
// };
onMounted(() => { onMounted(() => {
getData(); getData();
getCodeList(); getCodeList();
@ -345,22 +331,6 @@ onMounted(() => {
<template> <template>
<div class="w-100" v-if="!openView"> <div class="w-100" v-if="!openView">
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
<!-- <FormComponent-->
<!-- :edit-data="data.selectedData"-->
<!-- :mode="data.modalMode"-->
<!-- @close-modal="closeModal"-->
<!-- @handle-data="saveData"-->
<!-- :user-option="data.userOption"-->
<!-- />-->
<!-- </v-dialog>-->
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
<!-- <ConfirmDialogComponent-->
<!-- @cancel="data.isConfirmDialogVisible = false"-->
<!-- @delete="removeData(undefined)"-->
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
<!-- />-->
<!-- </v-dialog>-->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center"> <v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card <v-card
flat flat

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/trainingscript/ViewComponent.vue"; import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue";
import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue"; import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
// const store = commonStore(); // const store = commonStore();

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onBeforeUnmount, onMounted, ref, watch } from "vue"; import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";

@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconSettingBtn from "@/components/button/IconSettingBtn.vue"; import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue"; // import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/workflow/ViewComponent.vue"; import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsCreateDialog from "@/components/atoms/organisms/WorkflowsCreateDialog.vue"; import WorkflowsCreateDialog from "@/components/atoms/organisms/WorkflowsCreateDialog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
import { AutoflowService } from "../service/management/AutoflowService"; import { AutoflowService } from "@/components/service/management/AutoflowService";
import { commonStore } from "@/stores/commonStore"; import { commonStore } from "@/stores/commonStore";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
const store = commonStore(); const store = commonStore();
const openView = ref(false); const openView = ref(false);
@ -24,11 +25,7 @@ const tableHeader = [
width: "7%", width: "7%",
style: "word-break: keep-all;", style: "word-break: keep-all;",
}, },
{
label: "Version",
width: "7%",
style: "word-break: keep-all;",
},
{ {
label: "Step Count", label: "Step Count",
width: "7%", width: "7%",
@ -122,15 +119,15 @@ const getCodeList = () => {
// }); // });
}; };
const toRow = (w: any, i: number, offset: number) => ({ const toRow = (workflow: any, index: number, offset: number) => ({
no: offset + i + 1, no: offset + index + 1,
name: w.workflowName, name: workflow.workflowName,
version: w.version ?? "v1.0", version: workflow.version,
stepCount: w.stepCount ?? w.steps?.length ?? 0, stepCount: workflow.stepCount,
configProgress: `${w.configDone ?? 0}/${w.configTotal ?? w.steps?.length ?? 0}`, configProgress: workflow.configProgress,
kubeflowStatus: w.uploadYn === "Y" ? "Uploaded" : "Not Uploaded", kubeflowStatus: workflow.kubeflowStatus,
registDt: w.regDt ?? w.regDate ?? "-", registDt: workflow.regDt,
deviceKey: w.id ?? w.workflowId ?? offset + i, deviceKey: workflow.id,
}); });
const getData = () => { const getData = () => {
@ -298,9 +295,8 @@ const changePageNum = (page) => {
data.value.params.pageNum = page; data.value.params.pageNum = page;
getData(); getData();
}; };
const openSettingModal = (selectedItem) => { const openDetailModal = (selectedItem) => {
data.value.selectedData = selectedItem; data.value.selectedData = selectedItem;
data.value.modalMode = "setting";
openView.value = true; openView.value = true;
}; };
const openModifyModal = (item: { workflow: string; stepName: string }) => { const openModifyModal = (item: { workflow: string; stepName: string }) => {
@ -511,13 +507,13 @@ onMounted(() => {
</td> </td>
<td>{{ item.no }}</td> <td>{{ item.no }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.version }}</td>
<td>{{ item.stepCount }}</td> <td>{{ item.stepCount }}</td>
<td>{{ item.configProgress }}</td> <td>{{ item.configProgress }}</td>
<td>{{ item.kubeflowStatus }}</td> <td>{{ item.kubeflowStatus }}</td>
<td>{{ item.registDt }}</td> <td>{{ item.registDt }}</td>
<td style="white-space: nowrap"> <td style="white-space: nowrap">
<IconSettingBtn @on-click="openSettingModal(item)" /> <IconInfoBtn @on-click="openDetailModal(item)" />
<IconSettingBtn />
<IconModifyBtn @on-click="openModifyModal(item)" /> <IconModifyBtn @on-click="openModifyModal(item)" />
<IconDeleteBtn <IconDeleteBtn
@on-click=" @on-click="
@ -571,7 +567,11 @@ onMounted(() => {
</div> </div>
<div class="w-100" v-else> <div class="w-100" v-else>
<ViewComponent @close="closeDetail" /> <ViewComponent
v-if="data.selectedData"
:id="data.selectedData.deviceKey"
@close="closeDetail"
/>
</div> </div>
</template> </template>

@ -0,0 +1,262 @@
<script setup lang="ts">
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
import * as monaco from "monaco-editor";
import "monaco-editor/min/vs/editor/editor.main.css";
import { AutoflowService } from "@/components/service/management/AutoflowService";
type TabKey = "details" | "yaml";
const props = defineProps<{ id: number | string }>();
const emit = defineEmits<{ (e: "close"): void }>();
const activeTab = ref<TabKey>("details");
const editorRef = ref<HTMLDivElement | null>(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
const detail = ref({
workflowName: "",
version: "",
workflowDescription: "",
createdDate: "",
createdId: "",
});
const stepHeaders = [
{ title: "Order", key: "order", width: "10%", align: "center" },
{ title: "Step Name", key: "name", width: "40%", align: "center" },
{
title: "Component Type",
key: "componentType",
width: "30%",
align: "center",
},
{ title: "Status", key: "status", width: "20%", align: "center" },
];
const steps = ref<
Array<{ order: number; name: string; componentType: string; status: string }>
>([]);
const defaultYaml = `# YAML not provided by server
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: dummy-
spec:
entrypoint: main
templates:
- name: main
container:
image: alpine:latest
command: [sh, -c]
args: ["echo hello"]
`;
/** ===== 상세 조회 ===== */
async function fetchDetail(id: number | string) {
try {
const res = await AutoflowService.view(Number(id));
const d = res.data;
detail.value.workflowName = d.workflowName || "";
detail.value.version = String(d.version || 1);
detail.value.workflowDescription = d.workflowDescription || "";
detail.value.createdDate = d.regDt || d.regDate || "-";
detail.value.createdId = d.regUserId || "-";
if (Array.isArray(d.steps)) {
steps.value = d.steps.map((s: any, idx: number) => ({
order: idx + 1,
name: s.stepName || s.name || `Step ${idx + 1}`,
componentType: s.componentType || s.type || "-",
status: s.status || "Not Configured",
}));
} else {
steps.value = [];
}
// YAML ( )
const yamlFromServer =
d.workflowYaml ||
d.yaml ||
d.pipelineYaml ||
d.specYaml ||
d.yamlStr ||
"";
if (editorInstance) {
editorInstance.setValue(yamlFromServer || defaultYaml);
}
} catch (e) {
console.error("[Child] view API failed:", e);
}
}
/** ===== 마운트 & 변경 감지 ===== */
onMounted(() => {
if (editorRef.value) {
editorInstance = monaco.editor.create(editorRef.value, {
value: defaultYaml,
language: "yaml",
theme: "vs-dark",
readOnly: true,
automaticLayout: true,
minimap: { enabled: false },
lineNumbers: "on",
});
}
});
// props.id
watch(
() => props.id,
(val) => {
if (val !== null && val !== undefined && val !== "") {
fetchDetail(val);
}
},
{ immediate: true },
);
onBeforeUnmount(() => {
if (editorInstance) {
editorInstance.dispose();
editorInstance = null;
}
});
</script>
<template>
<v-container class="h-100 w-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<!-- 헤더 -->
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<!-- -->
<v-tabs
v-model="activeTab"
background-color="grey lighten-4"
style="max-width: 360px"
grow
>
<v-tab value="details">Details</v-tab>
<v-tab value="yaml">YAML</v-tab>
</v-tabs>
<!-- Details -->
<template v-if="activeTab === 'details'">
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8 step-card">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Workflow Information</span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Workflow Name</v-col
>
<v-col cols="3">{{ detail.workflowName }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Version</v-col>
<v-col cols="3">{{ detail.version }}</v-col>
</v-row>
<v-divider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Workflow Description</v-col
>
<v-col cols="9">{{ detail.workflowDescription }}</v-col>
</v-row>
<v-divider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Created Date</v-col
>
<v-col cols="3">{{ detail.createdDate }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"
>Created ID</v-col
>
<v-col cols="3">{{ detail.createdId }}</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Steps -->
<v-card
flat
class="bordered-box mb-6 w-100 rounded-lg pa-8"
style="min-height: 500px"
>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Step Overview</span>
</v-card-title>
<v-data-table
:headers="stepHeaders"
:items="steps"
dense
class="text-center"
hide-default-footer
:items-per-page="5"
header-color="primary"
disable-sort
>
<template #item.order="{ index }">{{ index + 1 }}</template>
<template #item.status="{ item }">
<v-chip
:color="
{ Configured: 'success', 'Not Configured': 'warning' }[
item.status
] || 'default'
"
small
dark
>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
<v-sheet class="d-flex justify-end mt-4">
<v-btn class="back-to-list" color="primary" @click="emit('close')"
>Back to List</v-btn
>
</v-sheet>
</v-card>
</template>
<!-- YAML -->
<div
v-show="activeTab === 'yaml'"
ref="editorRef"
class="editor-container"
/>
</v-card>
</v-container>
</template>
<style scoped>
.editor-container {
width: 100%;
height: 900px;
max-height: 900px;
border: 1px solid #444;
border-radius: 4px;
}
.step-card {
position: relative;
min-height: 500px;
padding-bottom: 84px;
}
.back-to-list {
position: absolute;
right: 24px;
bottom: 24px;
}
</style>

@ -1,497 +0,0 @@
<script setup lang="ts">
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
import * as monaco from "monaco-editor";
import "monaco-editor/min/vs/editor/editor.main.css";
// const store = commonStore();
const activeTab = ref<"details" | "yaml">("details");
const editorRef = ref<HTMLDivElement | null>(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
const tableHeader = [
{
label: "Run Name",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Status",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Duration",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Pipeline",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Start Time",
width: "20%",
style: "word-break: keep-all;",
},
];
const experimentInfo = ref({
workflowName: "sentiment-analysis",
version: "v2.0",
workflowDescription: "감정 분석 모델 학습 워크플로우",
createdDate: "2025-02-06",
createdId: "ADMIN_001",
});
const stepHeaders = [
{ title: "Order", key: "order", width: "10%", align: "center" },
{ title: "Step Name", key: "name", width: "40%", align: "center" },
{
title: "Component Type",
key: "componentType",
width: "30%",
align: "center",
},
{ title: "Status", key: "status", width: "20%", align: "center" },
];
const steps = ref([
{ name: "Data loading", componentType: "DataPrep", status: "Configured" },
{
name: "Preprocessing",
componentType: "Preprocess",
status: "Not Configured",
},
{
name: "Preprocessing",
componentType: "Preprocess",
status: "Not Configured",
},
{
name: "Preprocessing",
componentType: "Preprocess",
status: "Not Configured",
},
{ name: "Train Model", componentType: "Train", status: "Not Configured" },
]);
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "run-batch32-lr0.001",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/2",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-10T00:00:00Z",
},
{
name: "run-batch64-lr0.001",
status: "Failed",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-09T00:00:00Z",
},
{
name: "run-batch32-lr0.0005",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-01T00:00:00Z",
},
{
name: "run-batch64-lr0.0005",
status: "Running",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-29T00:00:00Z",
},
{
name: "run-augmented-data",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-31T00:00:00Z",
},
];
data.value.totalDataLength = 5;
// DeviceService.search(params).then((d) => {
// if (d.status === 200) {
// data.value.results = d.data.deviceList;
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// } else {
// store.setSnackbarMsg({
// text: " ",
// color: "error",
// });
// }
// });
// DeviceService.search().then((d) => {
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// });
};
const yamlContent = `apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: dummy-workflow-
namespace: default
labels:
workflows.argoproj.io/archive-strategy: "true"
spec:
entrypoint: main
arguments:
parameters:
- name: message
value: "Hello, Monaco YAML Highlight!"
templates:
# 1) Main steps orchestration
- name: main
steps:
- - name: print
template: whalesay
arguments:
parameters:
- name: message
value: "{{workflow.parameters.message}}"
- - name: wait
template: sleep
arguments:
parameters:
- name: duration
value: "5"
- - name: finalize
template: next
# 2) Whalesay container
- name: whalesay
inputs:
parameters:
- name: message
container:
image: docker/whalesay:latest
command: [cowsay]
args: ["{{inputs.parameters.message}}"]
resources:
limits:
memory: "64Mi"
cpu: "100m"
# 3) Simple sleep step
- name: sleep
inputs:
parameters:
- name: duration
container:
image: alpine:latest
command: [sh, -c]
args:
- |
echo "Sleeping for {{inputs.parameters.duration}} seconds..."
sleep {{inputs.parameters.duration}}
# 4) Final step
- name: next
container:
image: alpine:latest
command: [sh, -c]
args:
- echo "Workflow complete at $(date)!"
# Optional: retention policy
ttlStrategy:
secondsAfterCompletion: 3600
`;
onMounted(() => {
if (editorRef.value) {
editorInstance = monaco.editor.create(editorRef.value, {
value: yamlContent,
language: "yaml",
theme: "vs-dark",
readOnly: true,
automaticLayout: true,
minimap: { enabled: false },
lineNumbers: "on",
});
}
});
onBeforeUnmount(() => {
if (editorInstance) {
editorInstance.dispose();
editorInstance = null;
}
});
// const setPaginationLength = () => {
// if (data.value.totalDataLength % data.value.params.pageSize === 0) {
// data.value.pageLength =
// data.value.totalDataLength / data.value.params.pageSize;
// } else {
// data.value.pageLength = Math.ceil(
// data.value.totalDataLength / data.value.params.pageSize,
// );
// }
// };
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<v-container class="h-100 w-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<!-- 1) Workflow Information -->
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-tabs
v-model="activeTab"
background-color="grey lighten-4"
style="max-width: 360px"
grow
>
<v-tab value="details">Details</v-tab>
<v-tab value="yaml">YAML</v-tab>
</v-tabs>
<template v-if="activeTab === 'details'" fluid flat>
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Workflow Information</span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Workflow Name / Version -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">
Workflow Name
</v-col>
<v-col cols="3">{{ experimentInfo.workflowName }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"> Version </v-col>
<v-col cols="3">{{ experimentInfo.version }}</v-col>
</v-row>
<v-divider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">
Workflow Description
</v-col>
<v-col cols="9">{{ experimentInfo.workflowDescription }}</v-col>
</v-row>
<v-divider class="my-2" />
<!-- Created Date / ID -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">
Created Date
</v-col>
<v-col cols="3">{{ experimentInfo.createdDate }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">
Created ID
</v-col>
<v-col cols="3">{{ experimentInfo.createdId }}</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- 2) Step Overview -->
<v-card
flat
class="bordered-box mb-6 w-100 rounded-lg pa-8"
style="min-height: 500px"
>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Step Overview</span>
</v-card-title>
<v-data-table
:headers="stepHeaders"
:items="steps"
dense
class="text-center"
hide-default-footer
:items-per-page="5"
header-color="primary"
disable-sort
>
<!-- 순번을 직접 렌더링 -->
<template #item.order="{ index }">
{{ index + 1 }}
</template>
<!-- 상태에 따라 색을 다르게 -->
<template #item.status="{ item }">
<v-chip
:color="
{
Configured: 'success',
'Not Configured': 'warning',
}[item.status]
"
small
dark
>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
<v-sheet class="d-flex justify-end mt-4">
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
</v-sheet>
</v-card>
</template>
<div
v-show="activeTab === 'yaml'"
ref="editorRef"
class="editor-container"
></div>
</v-card>
</v-container>
</template>
<style scoped>
.editor-container {
width: 100%;
height: 900px;
max-height: 900px;
border: 1px solid #444;
border-radius: 4px;
}
</style>

@ -5,5 +5,5 @@
</template> </template>
<script setup> <script setup>
import LayoutComponent from '@/components/common/LayoutComponent.vue'; import LayoutComponent from "@/components/common/LayoutComponent.vue";
</script> </script>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/Datasets/ListComponent.vue"; import ListComponent from "@/components/templates/Datasets/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/deployment/ListComponent.vue"; import ListComponent from "@/components/templates/deployment/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/run/executions/ListComponent.vue"; import ListComponent from "@/components/templates/run/executions/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/run/experiment/ListComponent.vue"; import ListComponent from "@/components/templates/run/experiment/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,4 @@
<script setup> <script setup>
import axios from "axios";
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { storage } from "@/utils/storage"; import { storage } from "@/utils/storage";

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

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/trainingscript/ListComponent.vue"; import ListComponent from "@/components/templates/trainingscript/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/stepconfig/ListComponent.vue"; import ListComponent from "@/components/templates/stepconfig/ListComponent.vue";
</script> </script>
<template> <template>

@ -1,5 +1,5 @@
<script setup> <script setup>
import ListComponent from "@/components/workflow/ListComponent.vue"; import ListComponent from "@/components/templates/workflow/ListComponent.vue";
</script> </script>
<template> <template>

@ -18,15 +18,6 @@ const routes = [
}, },
component: () => import("@/pages/MainView.vue"), component: () => import("@/pages/MainView.vue"),
}, },
{
name: "home",
path: `/home`,
meta: {
title: "Home",
requiresAuth: false,
},
component: () => import("@/pages/HomeView.vue"),
},
{ {
name: "select", name: "select",
path: `/select`, path: `/select`,
@ -37,6 +28,25 @@ const routes = [
}, },
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",
path: `/home`,
meta: {
title: "Home",
requiresAuth: false,
},
component: () => import("@/pages/HomeView.vue"),
},
{ {
name: "workflows", name: "workflows",
path: `/workflows`, path: `/workflows`,

@ -2,14 +2,38 @@ import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
export const useAutoflowStore = defineStore("autoflowStore", () => { export const useAutoflowStore = defineStore("autoflowStore", () => {
const ProjectName = ref<String>(""); // 초기값 복원
const setProjectName = (v) => { const storedId = localStorage.getItem("projectId");
ProjectName.value = v; const projectId = ref<number | null>(storedId ? Number(storedId) : null);
const projectName = ref<string>(localStorage.getItem("projectName") || "");
const setProjectId = (id: number) => {
projectId.value = id;
localStorage.setItem("projectId", String(id));
}; };
return { const clearProjectId = () => {
ProjectName, projectId.value = null;
localStorage.removeItem("projectId");
};
const setProjectName = (name: string) => {
projectName.value = name;
localStorage.setItem("projectName", name);
};
const clearProjectName = () => {
projectName.value = "";
localStorage.removeItem("projectName");
};
return {
projectId,
projectName,
setProjectId,
clearProjectId,
setProjectName, setProjectName,
clearProjectName,
}; };
}); });

@ -6,6 +6,12 @@ export const menuUtils = {
value: "home", value: "home",
icon: "mdi-monitor-multiple", icon: "mdi-monitor-multiple",
}, },
{
title: "Project",
path: "/project",
value: "project",
icon: "mdi-folder-cog-outline",
},
{ {
title: "Workflows", title: "Workflows",
path: "/workflows", path: "/workflows",
@ -40,13 +46,13 @@ export const menuUtils = {
title: "Training Script", title: "Training Script",
path: "/training-script", path: "/training-script",
value: "training-script", value: "training-script",
icon: "mdi-account", icon: "mdi-file-code-outline",
}, },
{ {
title: "Datasets", title: "Datasets",
path: "/datasets", path: "/datasets",
value: "datasets", value: "datasets",
icon: "mdi-account", icon: "mdi-database-outline",
}, },
], ],
}; };

@ -8,8 +8,40 @@ export const storage = {
getToken: () => { getToken: () => {
const authString = localStorage.getItem("autoflow-auth"); const authString = localStorage.getItem("autoflow-auth");
if (authString !== null) { if (authString !== null) {
try {
const auth = JSON.parse(authString); const auth = JSON.parse(authString);
return auth.token;
if (auth.jwtCookie) {
const match = auth.jwtCookie.match(/cuuva-jwt=([^;]+)/);
if (match && match[1]) {
return match[1];
}
}
} catch (e) {
console.error("[storage] getToken parse error:", e);
}
}
return "";
},
// 리프레시 토큰만 잘라서 반환
getRefreshToken: () => {
const authString = localStorage.getItem("autoflow-auth");
if (authString !== null) {
try {
const auth = JSON.parse(authString);
if (auth.jwtRefreshCookie) {
const match = auth.jwtRefreshCookie.match(
/cuuva-jwt-refresh=([^;]+)/,
);
if (match && match[1]) {
return match[1];
}
}
} catch (e) {
console.error("[storage] getRefreshToken parse error:", e);
}
} }
return ""; return "";
}, },
@ -21,7 +53,7 @@ export const storage = {
} }
return ""; return "";
}, },
getId: () =>{ getId: () => {
const authString = localStorage.getItem("autoflow-auth"); const authString = localStorage.getItem("autoflow-auth");
if (authString !== null) { if (authString !== null) {
const auth = JSON.parse(authString); const auth = JSON.parse(authString);

@ -1,148 +1,295 @@
<script setup lang="ts"> <script setup lang="ts">
import { ApiProject, UiProject } from "@/components/models/project/Project"; import { onMounted, onBeforeUnmount, ref, computed } from "vue";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { ProjectService } from "@/components/service/project/projectService";
import { useAutoflowStore } from "@/stores/autoflowStore";
import { storeToRefs } from "pinia";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useAutoflowStore } from "@/stores/autoflowStore";
import type {
UiProject,
ApiProject,
Permission,
} from "@/components/models/project/Project";
import { ProjectService } from "@/components/service/project/projectService";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { storage } from "@/utils/storage.js";
/** ===== 상수 & 기본 권한 ===== */
const DEFAULT_PERMISSIONS: Permission[] = [
"CREATE",
"READ",
"UPDATE",
"DELETE",
];
/** ===== 라우터 & 스토어 ===== */
const router = useRouter();
const autoflowStore = useAutoflowStore();
/** ===== 상태 ===== */
const dialog = ref(false); const dialog = ref(false);
const contextMenu = ref(false); const contextMenu = ref(false);
const menuX = ref(0); const menuX = ref(0);
const menuY = ref(0); const menuY = ref(0);
const selectedIndex = ref<number | null>(null); const selectedIndex = ref<number | null>(null);
const projects = ref<UiProject[]>([]); const projects = ref<UiProject[]>([]);
const userOptions = ref<string[]>([]); type UserOption = { id: number | string; username: string };
const userOptions = ref<UserOption[]>([]);
const modalMode = ref<"create" | "edit">("create"); const modalMode = ref<"create" | "edit">("create");
const editingProjectId = ref<number | null>(null); const editingProjectId = ref<number | null>(null);
const autoflowStore = useAutoflowStore();
const router = useRouter();
const form = ref({ const form = ref({
prjCd: "", prjCd: "",
prjNm: "", prjNm: "",
prjDesc: "", prjDesc: "",
selectedUsers: [] as string[], selectedUsers: [] as string[],
}); });
/** ===== 서버 응답 타입 ===== */
const roles = ref<string[]>([]);
const refreshRoles = () => {
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
const r = auth?.userInfo?.roles ?? auth?.roles ?? [];
roles.value = Array.isArray(r) ? r : [];
};
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
const loadProjects = async (): Promise<void> => { /** ===== 서버 응답 타입 ===== */
interface ProjectSearchResponseItem {
id: number;
prjNm: string;
prjDesc: string;
prjStartDt?: string;
regUserId?: string; // "" ( username)
}
interface UserResponseItem {
id: number | string;
username: string;
}
/** ===== 유틸 ===== */
const buildApiProjectPayload = (): ApiProject => {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
const namesCsv = form.value.selectedUsers.join(",");
return {
id: modalMode.value === "edit" ? editingProjectId.value! : null,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
prjStartDt: today,
prjEndDt: today,
delYn: "N",
regDate: nowIso,
regUserId: namesCsv,
regUserNm: namesCsv,
modDate: nowIso,
modUserId: namesCsv,
modUserNm: namesCsv,
};
};
const resetForm = () => {
form.value.prjCd = `PRJ${Date.now()}`;
form.value.prjNm = "";
form.value.prjDesc = "";
form.value.selectedUsers = [];
};
const loadProjects = async () => {
try { try {
const res = await ProjectService.search(); const { data } = await ProjectService.search();
projects.value = res.data.map((p) => ({ const rawList = data as ProjectSearchResponseItem[];
id: p.id!, projects.value = rawList.map((p) => ({
id: p.id,
title: p.prjNm, title: p.prjNm,
creator: p.regUserId, creator: p.regUserId ?? "",
date: p.prjStartDt, date: p.prjStartDt ?? "",
description: p.prjDesc, description: p.prjDesc,
})); }));
} catch (error) { } catch (e) {
console.error("프로젝트 조회 실패:", error); console.error("프로젝트 조회 실패:", e);
} }
}; };
const loadUsers = async () => { const loadUsers = async () => {
try { try {
const res = await UserManagerService.getAll(); const { data } = await UserManagerService.getAll();
userOptions.value = res.data.map((u: any) => u.username); const raw = data as UserResponseItem[];
} catch (err) { userOptions.value = raw.map((u) => ({ id: u.id, username: u.username }));
console.error("사용자 조회 실패:", err); } catch (e) {
console.error("사용자 조회 실패:", e);
} }
}; };
const selectProject = (idx: number): void => { const openContextMenu = (event: MouseEvent, index: number) => {
const p = projects.value[idx]; event.preventDefault();
autoflowStore.setProjectName(p.title); selectedIndex.value = index;
menuX.value = event.pageX;
menuY.value = event.pageY;
contextMenu.value = true;
};
const closeDialog = () => {
dialog.value = false;
contextMenu.value = false;
selectedIndex.value = null;
};
const selectProject = (index: number) => {
const selected = projects.value[index];
autoflowStore.setProjectId(selected.id);
autoflowStore.setProjectName(selected.title);
router.push("/home"); router.push("/home");
}; };
const openContextMenu = (e: MouseEvent, idx: number): void => { /** ===== 프로젝트 저장 & 권한 부여 ===== */
e.preventDefault(); const grantDefaultPermissions = async (
selectedIndex.value = idx; projectId: number,
menuX.value = e.pageX; usernames: string[],
menuY.value = e.pageY; ) => {
contextMenu.value = true; if (!usernames?.length) return;
const nameSet = new Set(usernames);
const numericIds = userOptions.value
.filter((u) => nameSet.has(u.username))
.map((u) => Number(u.id))
.filter((n) => Number.isFinite(n));
await Promise.all(
numericIds.map((uid) =>
ProjectService.projectAuthority(projectId, {
projectId,
userId: uid, // number
permissions: DEFAULT_PERMISSIONS,
}),
),
);
}; };
const saveProject = async () => { const saveProject = async () => {
const payload: ApiProject = { if (!isAdmin.value) {
id: modalMode.value === "edit" ? editingProjectId.value! : null, alert("권한이 없습니다. (ROLE_ADMIN 전용)");
prjCd: form.value.prjCd, return;
prjNm: form.value.prjNm, }
prjDesc: form.value.prjDesc,
prjStartDt: new Date().toISOString().slice(0, 10), const payload = buildApiProjectPayload();
prjEndDt: new Date().toISOString().slice(0, 10),
delYn: "N",
regDate: new Date().toISOString(),
regUserId: form.value.selectedUsers.join(","),
regUserNm: form.value.selectedUsers.join(","),
modDate: new Date().toISOString(),
modUserId: form.value.selectedUsers.join(","),
modUserNm: form.value.selectedUsers.join(","),
};
try { try {
let projectId: number;
if (modalMode.value === "create") { if (modalMode.value === "create") {
await ProjectService.add(payload); const createRes = await ProjectService.add(payload);
projectId = createRes.data.id;
} else { } else {
await ProjectService.update(editingProjectId.value!, payload); await ProjectService.update(editingProjectId.value!, payload);
projectId = editingProjectId.value!;
} }
await grantDefaultPermissions(projectId, form.value.selectedUsers);
await loadProjects(); await loadProjects();
closeDialog(); closeDialog();
} catch (err) { } catch (error: any) {
console.error(`${modalMode.value} 실패:`, err); console.error(`${modalMode.value} 실패:`, error?.response?.data || error);
alert(error?.response?.data?.message || "저장 실패");
} }
}; };
const deleteProject = async (): Promise<void> => { const deleteProject = async () => {
contextMenu.value = false;
const idx = selectedIndex.value;
if (idx === null) return;
const p = projects.value[idx];
try { try {
await ProjectService.delete(p.id); if (selectedIndex.value === null) return;
const target = projects.value[selectedIndex.value];
await ProjectService.delete(target.id);
await loadProjects(); await loadProjects();
} catch (error) { } catch (e: any) {
console.error("삭제 실패:", error); console.error("삭제 실패:", e?.response?.status, e?.response?.data || e);
alert("삭제 실패: " + (e?.response?.data?.message || e.message || ""));
} finally {
contextMenu.value = false;
} }
}; };
//
// const deleteProject = async (): Promise<void> => {
// try {
// if (selectedIndex.value === null) return;
// const target = projects.value[selectedIndex.value];
// const projectId = target.id;
// // 1) username
// const usernames = (target.creator || "")
// .split(",")
// .map((s) => s.trim())
// .filter(Boolean);
// // 2) username -> userId
// if (usernames.length) {
// const ids = userOptions.value
// .filter((u) => usernames.includes(u.username))
// .map((u) => u.id);
// // 3) /
// await Promise.all(
// ids.map((uid) => ProjectService.deleteProjectAuthority(projectId, uid)),
// );
// }
// // 4)
// await ProjectService.delete(projectId);
// await loadProjects();
// } catch (e: any) {
// console.error(" :", e?.response?.status, e?.response?.data || e);
// alert(" : " + (e?.response?.data?.message || e.message || ""));
// } finally {
// contextMenu.value = false;
// }
// };
const onAddProject = () => { const onAddProject = () => {
if (!isAdmin.value) {
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
return;
}
modalMode.value = "create"; modalMode.value = "create";
editingProjectId.value = null; editingProjectId.value = null;
form.value.prjCd = `PRJ${Date.now()}`; resetForm();
form.value.prjNm = "";
form.value.prjDesc = "";
form.value.selectedUsers = [];
dialog.value = true; dialog.value = true;
}; };
const modifyProject = () => { const modifyProject = () => {
contextMenu.value = false; contextMenu.value = false;
const idx = selectedIndex.value; if (selectedIndex.value === null) return;
if (idx === null) return;
const p = projects.value[idx]; const selected = projects.value[selectedIndex.value];
modalMode.value = "edit"; modalMode.value = "edit";
editingProjectId.value = p.id; editingProjectId.value = selected.id;
form.value.prjCd = p.title;
form.value.prjNm = p.title; form.value.prjCd = selected.title;
form.value.prjDesc = p.description; form.value.prjNm = selected.title;
form.value.selectedUsers = p.creator.split(","); form.value.prjDesc = selected.description;
form.value.selectedUsers =
selected.creator
?.split(",")
.map((s) => s.trim())
.filter(Boolean) ?? [];
dialog.value = true; dialog.value = true;
}; };
const cancel = () => {
dialog.value = false;
};
const closeDialog = () => { /** ===== 라이프사이클 ===== */
dialog.value = false; const onStorage = (e: StorageEvent) => {
contextMenu.value = false; if (!e.key || /auth|vpp-Auth/i.test(e.key)) refreshRoles();
selectedIndex.value = null;
}; };
onMounted(() => { onMounted(async () => {
loadProjects(); refreshRoles();
loadUsers(); await Promise.all([loadProjects(), loadUsers()]);
window.addEventListener("storage", onStorage);
});
onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage);
}); });
</script> </script>
@ -152,8 +299,11 @@ onMounted(() => {
<v-col cols="auto"> <v-col cols="auto">
<h2 class="font-weight-bold text-h5">Project Selection</h2> <h2 class="font-weight-bold text-h5">Project Selection</h2>
</v-col> </v-col>
<v-col cols="auto"> <v-col cols="auto">
<!-- ADMIN만 노출 -->
<v-btn <v-btn
v-show="isAdmin"
color="secondary" color="secondary"
variant="flat" variant="flat"
class="text-white font-weight-bold" class="text-white font-weight-bold"
@ -192,7 +342,7 @@ onMounted(() => {
<v-card-subtitle <v-card-subtitle
class="text-white text-caption d-flex justify-space-between" class="text-white text-caption d-flex justify-space-between"
> >
<span>생성자: {{ project.creator }}</span> <span>Select Users: {{ project.creator }}</span>
<span>등록일: {{ project.date }}</span> <span>등록일: {{ project.date }}</span>
</v-card-subtitle> </v-card-subtitle>
@ -205,6 +355,7 @@ onMounted(() => {
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<v-menu <v-menu
v-model="contextMenu" v-model="contextMenu"
absolute absolute
@ -225,9 +376,9 @@ onMounted(() => {
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<v-dialog v-model="dialog" max-width="500"> <v-dialog v-model="dialog" max-width="500">
<v-card> <v-card>
<!-- 모드에 따라 타이틀 변경 -->
<v-card-title class="headline"> <v-card-title class="headline">
{{ modalMode === "create" ? "Create Project" : "Modify Project" }} {{ modalMode === "create" ? "Create Project" : "Modify Project" }}
</v-card-title> </v-card-title>
@ -245,6 +396,8 @@ onMounted(() => {
label="Select Users" label="Select Users"
v-model="form.selectedUsers" v-model="form.selectedUsers"
:items="userOptions" :items="userOptions"
item-title="username"
item-value="username"
multiple multiple
chips chips
closable-chips closable-chips
@ -255,7 +408,6 @@ onMounted(() => {
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn text @click="closeDialog">Cancel</v-btn> <v-btn text @click="closeDialog">Cancel</v-btn>
<!-- 클릭 핸들러도 공용 saveProject -->
<v-btn color="primary" @click="saveProject"> <v-btn color="primary" @click="saveProject">
{{ modalMode === "create" ? "Create" : "Save" }} {{ modalMode === "create" ? "Create" : "Save" }}
</v-btn> </v-btn>

1
typed-router.d.ts vendored

@ -26,6 +26,7 @@ declare module 'vue-router/auto-routes' {
'/HomeView': RouteRecordInfo<'/HomeView', '/HomeView', Record<never, never>, Record<never, never>>, '/HomeView': RouteRecordInfo<'/HomeView', '/HomeView', Record<never, never>, Record<never, never>>,
'/LoginView': RouteRecordInfo<'/LoginView', '/LoginView', Record<never, never>, Record<never, never>>, '/LoginView': RouteRecordInfo<'/LoginView', '/LoginView', Record<never, never>, Record<never, never>>,
'/MainView': RouteRecordInfo<'/MainView', '/MainView', Record<never, never>, Record<never, never>>, '/MainView': RouteRecordInfo<'/MainView', '/MainView', 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>>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>, '/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>,

@ -15,7 +15,8 @@ import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
// 배포할때는 주석 풀기 // 배포할때는 주석 풀기
// base: "/autoflow/", // base: process.env.VITE_ROOT_PATH,
base: "/autoflow/",
plugins: [ plugins: [
VueRouter(), VueRouter(),
Layouts(), Layouts(),
@ -78,9 +79,6 @@ export default defineConfig({
sass: { sass: {
api: "modern-compiler", api: "modern-compiler",
}, },
scss: {
api: "modern-compiler",
},
}, },
}, },
}); });

Loading…
Cancel
Save