-
{
-
+
@@ -54,16 +196,29 @@ const submit = () => {
v-model="form.description"
variant="outlined"
rows="3"
+ :disabled="saving"
dense
hide-details
/>
+
+
{{ errorMsg }}
- Save
- Save
+ Close
diff --git a/src/components/atoms/organisms/StapComfigDialog.vue b/src/components/atoms/organisms/StapComfigDialog.vue
deleted file mode 100644
index 0f9e8ef..0000000
--- a/src/components/atoms/organisms/StapComfigDialog.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
- Edit Workflow Step Config
-
-
-
-
-
-
- Select Workflow
-
-
-
-
-
-
-
-
-
-
- Workflow Step Name
-
-
-
-
-
-
-
-
- Save
- Close
-
-
-
diff --git a/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue b/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue
index 842af9a..132a1da 100644
--- a/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue
+++ b/src/components/atoms/organisms/TrainingScriptBaseDoalog.vue
@@ -1,40 +1,131 @@
-
{
+ >Training Script Title
+
- Description
+
+
- File
+
+
+ {{ errorMsg }}
- Save
- Close
+ {{ isEdit ? "Update" : "Save" }}
+
+
+ Close
+
diff --git a/src/components/atoms/organisms/WorkflowsBaseDialog.vue b/src/components/atoms/organisms/WorkflowsBaseDialog.vue
index 5fd2423..2900b80 100644
--- a/src/components/atoms/organisms/WorkflowsBaseDialog.vue
+++ b/src/components/atoms/organisms/WorkflowsBaseDialog.vue
@@ -4,18 +4,22 @@ import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { computed, onBeforeUnmount, onMounted, watch, ref } from "vue";
-import { AutoflowService } from "@/components/service/management/AutoflowService";
+import { WorkflowService } from "@/components/service/management/workflowService";
import { storage } from "@/utils/storage";
-import type { Workflow } from "@/components/models/management/Autoflow";
+import type { Workflow } from "@/components/models/management/Workflow";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
+import { kubeflowService } from "@/components/service/management/kubeflowService";
+import {
+ toKubeflowForm,
+ type KubeflowUploadDto,
+} from "@/components/models/management/Kubeflow";
const { projectId } = storeToRefs(useAutoflowStore());
const props = defineProps<{
- editData?: any;
- mode?: "create" | "edit";
- userOption?: any[];
+ editData: any;
+ mode: "create" | "edit";
}>();
const emit = defineEmits<{
@@ -28,6 +32,77 @@ const isEdit = computed(() => props.mode === "edit");
const saving = ref(false);
const errorMsg = ref("");
+// ====== KFP 이름 제한 & 한글 제한 유틸 ======
+const KFP_NAME_REGEX = /^[a-z0-9]([-a-z0-9\.]*[a-z0-9])?$/; // 소문자/숫자/.-, 시작/끝 영숫자
+
+const sanitizeKfpName = (s: string) => {
+ let x = (s ?? "").toLowerCase();
+ x = x.replace(/[\s_]+/g, "-"); // 공백/언더스코어 -> 하이픈
+ x = x.replace(/[^a-z0-9.-]/g, ""); // 허용 외 문자 제거(한글 포함)
+ x = x.replace(/-+/g, "-"); // 하이픈 중복 축소
+ x = x.replace(/^[^a-z0-9]+/, ""); // 앞쪽 비허용 제거
+ x = x.replace(/[^a-z0-9]+$/, ""); // 뒤쪽 비허용 제거
+ return x;
+};
+
+// 힌트/에러 메시지
+const nameHint = ref(
+ "허용 문자: 소문자 a–z, 숫자 0–9, '-', '.' (시작/끝은 영숫자). 한글/공백/대문자/언더스코어 불가",
+);
+const nameInvalid = computed(
+ () => !!form.value.name && !KFP_NAME_REGEX.test(form.value.name),
+);
+const nameErrorMsg = computed(() =>
+ nameInvalid.value
+ ? "형식이 올바르지 않습니다. (소문자/숫자, '-', '.', 시작/끝은 영숫자)"
+ : "",
+);
+
+// 입력 시 자동 정제
+function onNameInput(v: string) {
+ const cleaned = sanitizeKfpName(v || "");
+ if (cleaned !== v) {
+ nameHint.value = "허용되지 않는 문자는 자동으로 제거됩니다.";
+ } else {
+ nameHint.value =
+ "허용 문자: 소문자 a–z, 숫자 0–9, '-', '.' (시작/끝은 영숫자)";
+ }
+ form.value.name = cleaned;
+}
+
+function extractApiErrorMessage(err: any): string {
+ const status = err?.response?.status;
+ const data = err?.response?.data;
+ const raw =
+ (typeof data === "string"
+ ? data
+ : data?.message || data?.error || data?.detail) ||
+ err?.message ||
+ "";
+
+ const text = String(raw);
+
+ if (
+ status === 409 ||
+ /already\s*exists|duplicate|이미 존재|중복/i.test(text)
+ ) {
+ return "같은 이름의 파이프라인이 이미 존재합니다. 다른 이름으로 등록해주세요.";
+ }
+ if (status === 400 && /name|display[_ ]?name|invalid/i.test(text)) {
+ return "이름(name)이 유효하지 않습니다. 공백/특수문자 여부를 확인해주세요.";
+ }
+ if (status === 401 || status === 403) {
+ return "권한이 없거나 로그인 정보가 만료되었습니다. 다시 로그인 후 시도하세요.";
+ }
+ if (status === 413 || /file too large|payload too large|size/i.test(text)) {
+ return "업로드 파일 용량이 너무 큽니다.";
+ }
+ if (status === 500 && /InvalidUrl|Bad authority|host/i.test(text)) {
+ return "서버 설정 오류로 업로드에 실패했습니다. (관리자에게 KFP URL 설정 점검을 요청하세요)";
+ }
+ return text || `요청에 실패했습니다. (HTTP ${status ?? "Error"})`;
+}
+
const steps = ref([
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
{
@@ -48,15 +123,16 @@ const steps = ref([
const form = ref({
name: "",
description: "",
+ file: null as File | null,
});
/** props.editData -> form 바인딩 */
function hydrateFormFromEdit(data: any) {
if (!data) return;
+ // 표시값은 원본을 보여주되, 저장 시 최종 검증/정제
form.value.name = data.workflowName ?? data.name ?? "";
form.value.description = data.workflowDescription ?? data.description ?? "";
}
-
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
@@ -66,7 +142,9 @@ watch(
if (isEdit.value) hydrateFormFromEdit(v);
},
);
-
+function onDescInput(v: string) {
+ form.value.description = v ?? "";
+}
/** 시간 포맷 */
const nowLocalIso = (): string => {
const t = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
@@ -75,10 +153,15 @@ const nowLocalIso = (): string => {
async function submit() {
errorMsg.value = "";
- const name = form.value.name.trim();
- if (!name) {
- errorMsg.value = "Workflow Name은 필수입니다.";
+ // 제출 직전에 한 번 더 정제 & 검증
+ form.value.name = sanitizeKfpName(form.value.name);
+ form.value.description = (form.value.description ?? "").trim();
+
+ const name = form.value.name.trim();
+ if (!name || !KFP_NAME_REGEX.test(name)) {
+ errorMsg.value =
+ "Workflow Name 형식이 올바르지 않습니다. (소문자/숫자, '-', '.', 시작/끝은 영숫자)";
return;
}
@@ -86,7 +169,6 @@ async function submit() {
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
-
const regUserId =
authObj?.userInfo?.username ??
authObj?.userinfo?.username ??
@@ -98,48 +180,86 @@ async function submit() {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
+ if (!projectId.value) {
+ errorMsg.value = "프로젝트가 선택되지 않았습니다.";
+ return;
+ }
const now = nowLocalIso();
- const payload: Workflow = {
- workflowName: name,
- workflowDescription: form.value.description?.trim() || "",
- uploadYn: "Y",
- regUserId,
- regDt: now,
- modDt: now,
- projectId: projectId.value,
- };
try {
saving.value = true;
if (isEdit.value) {
- // 수정: id 추출(행에서 넘겨준 deviceKey 또는 id 지원)
+ // ===== 수정 =====
const rawId = props.editData?.id ?? props.editData?.deviceKey;
const id = Number(rawId);
-
if (!id) {
errorMsg.value = "수정할 ID가 없습니다.";
return;
}
- const { data } = await AutoflowService.update(id, payload);
+ // ① 기존 값 조회
+ const viewRes = await WorkflowService.view(id);
+ const current = (viewRes?.data ?? viewRes) || {};
+
+ // ② name/description만 변경, 그 외는 기존 값 유지해서 null 덮어쓰기 방지
+ const updatePayload = cleanUndefined({
+ id,
+ name, // 변경
+ description: form.value.description?.trim() || "", // 변경
+
+ // ===== 기존 유지 필드 =====
+ displayName: current.displayName,
+ namespace: current.namespace,
+ pipelineId: current.pipelineId,
+ kubeflowStatus: current.kubeflowStatus,
+ version: current.version,
+
+ regUserId: current.regUserId ?? regUserId,
+ projectId: current.projectId ?? projectId.value,
+ regDt: current.regDt,
+ modDt: now,
+ });
+
+ const { data } = await WorkflowService.update(id, updatePayload);
emit("saved", data);
emit("close-modal");
} else {
- // 생성
- const { data } = await AutoflowService.add(payload);
+ // ===== 생성 =====
+ if (!form.value.file) {
+ errorMsg.value = "업로드할 파일을 선택하세요.";
+ return;
+ }
+ const dto: KubeflowUploadDto = {
+ name,
+ display_name: name,
+ description: form.value.description?.trim() || "",
+ namespace: "default",
+ regUserId,
+ projectId: projectId.value!,
+ uploadfile: form.value.file,
+ };
+ const fd = toKubeflowForm(dto);
+ const { data } = await kubeflowService.upload(fd);
emit("saved", data);
emit("close-modal");
}
- } catch (e) {
+ } catch (e: any) {
console.error("워크플로우 저장 실패:", e);
- errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요.";
+ errorMsg.value = extractApiErrorMessage(e);
} finally {
saving.value = false;
}
}
+/** undefined 필드는 제거해서 불필요한 키 전송 방지 */
+function cleanUndefined
>(obj: T): T {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([, v]) => v !== undefined),
+ ) as T;
+}
+
/** ESC로 닫기 */
function onEsc(e: KeyboardEvent) {
if (e.key === "Escape") emit("close-modal");
@@ -164,31 +284,56 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
+
-
+
-
-
+
+
+
+
+
+
+
+
+
@@ -196,82 +341,18 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
-
- Workflow Steps
-
-
- mdi-plus Add Step
- {{
- isEdit ? "Update" : "Save"
- }}
- Cancel
-
-
-
-
-
-
- | Order |
- Step Name |
- Component Type |
- Status |
- Action |
-
-
-
-
- | {{ step.order }} |
- {{ step.stepName }} |
-
-
- |
- {{ step.status }} |
-
-
-
-
-
- |
-
-
-
-
-
- {{
- isEdit ? "Update" : "Save"
- }}
+
+ {{ isEdit ? "Update" : "Save" }}
+
Close
+ Close
+
diff --git a/src/components/atoms/organisms/WorkflowsRunDialog.vue b/src/components/atoms/organisms/WorkflowsRunDialog.vue
new file mode 100644
index 0000000..801b5b5
--- /dev/null
+++ b/src/components/atoms/organisms/WorkflowsRunDialog.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+ Run Pipeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorMsg }}
+
+
+
+
+
+ RUN
+
+ CLOSE
+
+
+
diff --git a/src/components/atoms/organisms/WorklfowStepBaseDialog.vue b/src/components/atoms/organisms/WorklfowStepBaseDialog.vue
new file mode 100644
index 0000000..89dc1f7
--- /dev/null
+++ b/src/components/atoms/organisms/WorklfowStepBaseDialog.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+ {{ isEdit ? "Edit Workflow Step" : "Create Workflow Step" }}
+
+
+
+
+ Workflow Step Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorMsg }}
+
+
+
+
+ {{
+ isEdit ? "Update" : "Save"
+ }}
+ Close
+
+
+
diff --git a/src/components/common/DrawerComponent.vue b/src/components/common/DrawerComponent.vue
index 4d5ed09..c026f39 100644
--- a/src/components/common/DrawerComponent.vue
+++ b/src/components/common/DrawerComponent.vue
@@ -3,16 +3,21 @@ import { ref, onMounted, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { menuUtils } from "@/utils/menuUtils";
import { storage } from "@/utils/storage";
-import logo from "@/assets/iteration (1).png";
+import SidebarHeader from "@/components/common/SidebarHeader.vue";
const route = useRoute();
const router = useRouter();
+const isAdminRoute = computed(() =>
+ route.matched.some((r) => r.meta?.requiresAdmin),
+);
+const menuItems = computed(() =>
+ isAdminRoute.value ? menuUtils.adminMenuItem : menuUtils.menuItem,
+);
const isShowAuth = ref(false);
function readRolesFromStorage(): string[] {
try {
- // storage.get(...) 이 문자열일 수도, 객체일 수도 있어서 분기
const raw =
storage.get?.("autoflow-auth") ??
localStorage.getItem("autoflow-auth") ??
@@ -20,8 +25,6 @@ function readRolesFromStorage(): string[] {
const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
let roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
-
- // "ROLE_USER,ROLE_ADMIN" 처럼 문자열로 오는 경우
if (typeof roles === "string") {
roles = roles.split(",").map((s: string) => s.trim());
}
@@ -33,7 +36,6 @@ function readRolesFromStorage(): string[] {
}
}
-// ADMIN 인지 계산 (ROLE_ADMIN 또는 ADMIN 둘 다 허용)
const isAdmin = computed(() => {
const roles = readRolesFromStorage();
return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN");
@@ -43,10 +45,6 @@ const isLinkActive = (link) => {
return route.path.includes(link);
};
-const goMain = () => {
- router.push("/home");
-};
-
onMounted(() => {
isShowAuth.value = true;
//storage.getAuth().auth === "ADMIN";
@@ -55,19 +53,6 @@ onMounted(() => {
-
-
-
-
- Autoflow Web Console
-
-
-
{
{
- // storage 구조: { userInfo: { username, ... }, ... } 라고 가정
- const auth = storage.getAuth?.() ?? null;
- username.value =
- auth?.userInfo?.username ??
- auth?.username ?? // 혹시 평문으로 저장한 경우
- ""; // 없으면 빈값
-};
+function computeIsAdmin() {
+ try {
+ const raw =
+ typeof storage?.getAuth === "function"
+ ? storage.getAuth()
+ : JSON.parse(localStorage.getItem("autoflow-auth") || "null");
-const menuItems = [
- {
- title: "Select Project",
- click: () => {
- goSelect();
- },
- },
- {
- title: "Change Password",
- click: () => {
- showPasswordModal.value = true;
- },
- },
- {
- title: "Logout",
- icon: "mdi-logout",
- click: () => {
- logOut();
- },
- },
-];
+ 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;
+ }
+}
-const userMenuItems = [
- {
- title: "Select Project",
- },
+// 설정 버튼 토글
+function toggleAdmin() {
+ if (!isAdmin.value) return;
+ if (adminMode.value) {
+ adminMode.value = false;
+ router.push(lastNonAdminPath.value || "/home");
+ } else {
+ adminMode.value = true;
+ if (!route.meta?.requiresAdmin) router.push("/project");
+ }
+}
+
+// ----------------------
+// 상단 드롭다운 메뉴
+// ----------------------
+const menu = ref([]);
+const menuItems = [
+ { title: "Select Project", click: () => goSelect() },
{
title: "Change Password",
- },
- {
- title: "Logout",
- icon: "mdi-logout",
click: () => {
- logOut();
+ /* open modal */
},
},
+ { 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;
-});
+// ✅ 유저 메뉴와 동일한 active 계산
+const isLinkActive = (link) => route.path.includes(link);
+
+const settingsLabel = computed(() =>
+ adminMode.value ? "Back to Console" : "Settings",
+);
-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,89 +96,144 @@ 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();
+ }
+}
+const goMain = () => {
+ router.push("/home");
};
+// 마지막 일반 경로 추적
+watch(
+ () => route.fullPath,
+ () => {
+ refreshProjectName();
+ const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
+ if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home";
+ },
+ { immediate: true },
+);
+
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);
-});
-watch(
- () => route.fullPath,
- () => refreshProjectName(),
-);
-watchEffect(() => {
- // const auth = storage.getAuth().auth;
- // if (auth === "ADMIN") {
- menu.value = menuItems;
- // } else {
- // menu.value = userMenuItems;
- // }
+ window.removeEventListener("storage", onStorage);
});
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ mdi-cog
+
+
+
+
+
+
+
+
+ mdi-home
+
+
+
+
+
+
{{ username || "GUEST" }}
+
+ {{ projectName || "No Project Selected" }}
+
+
+
-
-
-
-
- mdi-cog
-
-
-
-
-
-
- mdi-home
-
-
-
-
-
{{ username || "GUEST" }}
-
- {{ projectName || "No Project Selected" }}
-
-
+
mdi-arrow-down-drop-circle-outline
diff --git a/src/components/common/SidebarHeader.vue b/src/components/common/SidebarHeader.vue
new file mode 100644
index 0000000..23c5522
--- /dev/null
+++ b/src/components/common/SidebarHeader.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Autoflow Web Console
+
+
+
diff --git a/src/components/models/management/Attachments.ts b/src/components/models/management/Attachments.ts
new file mode 100644
index 0000000..8c76a59
--- /dev/null
+++ b/src/components/models/management/Attachments.ts
@@ -0,0 +1,24 @@
+export type AttachmentUpload = {
+ refId?: number | null;
+ refType: string;
+ title?: string;
+ description?: string;
+ version?: number;
+ regUserId: string;
+ projectId: number;
+ file: File | Blob;
+ path?: string;
+};
+
+export type AttachmentSearch = {
+ projectId: number;
+ page?: number;
+ size?: number;
+ keyword?: string;
+ searchType?: "전체" | "제목" | "작성자";
+ startDate?: string;
+ endDate?: string;
+ sortField?: string;
+ sortDirection?: "ASC" | "DESC";
+ refType?: "WORKFLOW_STEP" | "DATASET" | "TRAINING_SCRIPT";
+};
diff --git a/src/components/models/management/Autoflow.ts b/src/components/models/management/Autoflow.ts
deleted file mode 100644
index c6de4c2..0000000
--- a/src/components/models/management/Autoflow.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export interface Workflow {
- workflowName: string;
- workflowDescription?: string;
- uploadYn: "Y" | "N";
- regUserId: string;
- regDt: string;
- modDt: string;
- projectId: number;
-}
diff --git a/src/components/models/management/Experiments.ts b/src/components/models/management/Experiments.ts
new file mode 100644
index 0000000..34b6b89
--- /dev/null
+++ b/src/components/models/management/Experiments.ts
@@ -0,0 +1,29 @@
+export interface ExperimentCreateDto {
+ kubeFlowId?: string;
+ mlFlowId?: string;
+ name: string;
+ displayName: string;
+ description?: string;
+ artifactLocation?: string;
+ lifecycleStage?: string;
+ storageState?: string;
+ kubeflowCreatedAt?: string;
+ mlflowCreatedAt?: string;
+ lastUpdateTime?: string;
+ lastRunCreatedAt?: string;
+ regUserId: string;
+ projectId: number;
+}
+
+export type ExperimentSearch = {
+ projectId: number;
+ page?: number;
+ size?: number;
+ keyword?: string;
+ searchType?: "전체" | "제목" | "작성자";
+ startDate?: string;
+ endDate?: string;
+ sortField?: string;
+ sortDirection?: "ASC" | "DESC";
+ refType?: "WORKFLOW_STEP" | "DATASET" | "TRAINING_SCRIPT";
+};
diff --git a/src/components/models/management/Kubeflow.ts b/src/components/models/management/Kubeflow.ts
new file mode 100644
index 0000000..9d3be98
--- /dev/null
+++ b/src/components/models/management/Kubeflow.ts
@@ -0,0 +1,23 @@
+export type KubeflowUploadDto = {
+ name: string;
+ display_name?: string;
+ description?: string;
+ namespace?: string;
+ regUserId: string;
+ projectId: number | string;
+ uploadfile: File | Blob;
+};
+
+export type kubeflow = FormData;
+
+export function toKubeflowForm(dto: KubeflowUploadDto): FormData {
+ const fd = new FormData();
+ fd.append("name", dto.name);
+ fd.append("display_name", dto.display_name || dto.name);
+ fd.append("description", dto.description || "");
+ fd.append("namespace", dto.namespace || "default");
+ fd.append("regUserId", String(dto.regUserId));
+ fd.append("projectId", String(dto.projectId));
+ fd.append("uploadfile", dto.uploadfile);
+ return fd;
+}
diff --git a/src/components/models/management/Workflow.ts b/src/components/models/management/Workflow.ts
new file mode 100644
index 0000000..84c4af0
--- /dev/null
+++ b/src/components/models/management/Workflow.ts
@@ -0,0 +1,21 @@
+export interface Workflow {
+ workflowName: string;
+ workflowDescription?: string;
+ uploadYn: "Y" | "N";
+ regUserId: string;
+ regDt: string;
+ 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";
+}
diff --git a/src/components/models/management/WorkflowStep.ts b/src/components/models/management/WorkflowStep.ts
new file mode 100644
index 0000000..bc3df01
--- /dev/null
+++ b/src/components/models/management/WorkflowStep.ts
@@ -0,0 +1,21 @@
+export type StepStatus = "Running" | "Success" | "Fail";
+
+export interface WorkflowStep {
+ projectId: number;
+ stepName: string;
+ status?: StepStatus;
+ pipelineId?: number;
+ startTime?: string;
+ endTime?: string;
+ logPath?: string;
+ version?: string;
+
+ files?: Array<{
+ refType?: "workflow_step";
+ originalName: string;
+ storageName: string;
+ contentType?: string;
+ size?: number;
+ storagePath: string;
+ }>;
+}
diff --git a/src/components/models/project/Project.ts b/src/components/models/project/Project.ts
index b03dc92..d1ce50e 100644
--- a/src/components/models/project/Project.ts
+++ b/src/components/models/project/Project.ts
@@ -30,7 +30,7 @@ export interface ProjectAuthority {
permissions: Permission[];
}
-export interface ProjectSearchParams {
+export interface ProjectSearch {
page: number;
size: number;
keyword: string;
diff --git a/src/components/service/index.ts b/src/components/service/index.ts
index aed462f..91ae7b7 100644
--- a/src/components/service/index.ts
+++ b/src/components/service/index.ts
@@ -20,6 +20,12 @@ export const request = {
put: (uri: string, param: any): any => {
return axios.put(`${API_URL}${uri}`, param);
},
+ getFile: (uri: string, param: any): any => {
+ return axios.get(`${API_URL}${uri}`, {
+ params: param,
+ responseType: "blob",
+ });
+ },
postFile: (uri: string, param: any, attachment: any, progress: any): any => {
const formData = new FormData();
diff --git a/src/components/service/management/AutoflowStepService.ts b/src/components/service/management/AutoflowStepService.ts
deleted file mode 100644
index e2c4826..0000000
--- a/src/components/service/management/AutoflowStepService.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Workflow } from "@/components/models/management/Autoflow";
-import { request } from "@/components/service/index";
-export const AutoflowStepService = {
- add: (payload: Workflow) => {
- return request.post("/api/workflow-steps", payload);
- },
- getAll: () => {
- request.get("/api/workflow-steps", {});
- },
-
- delete: (id: Number) => {
- return request.delete(`/api/workflow-steps${id}`, {});
- },
- view: (id: Number) => {
- return request.get(`/api/workflow-steps${id}`, {});
- },
- update: (id: number, payload: Workflow) => {
- return request.put(`/api/workflow-steps${id}`, payload);
- },
-};
diff --git a/src/components/service/management/attachmentsService.ts b/src/components/service/management/attachmentsService.ts
new file mode 100644
index 0000000..f553589
--- /dev/null
+++ b/src/components/service/management/attachmentsService.ts
@@ -0,0 +1,36 @@
+import {
+ AttachmentSearch,
+ AttachmentUpload,
+} from "@/components/models/management/Attachments";
+
+import { request } from "@/components/service/index";
+export const AttachmentsService = {
+ upload: (payload: AttachmentUpload) => {
+ return request.post("/api/attachments/upload", payload);
+ },
+ delete: (id: Number) => {
+ return request.delete(`/api/attachments/${id}`, {});
+ },
+ view: (id: number) => {
+ return request.get(`/api/attachments/${id}`, {});
+ },
+ update: (id: number, payload: AttachmentUpload) => {
+ return request.put(`/api/attachments/${id}/update`, payload);
+ },
+
+ readTextByPath: (objectName: string) => {
+ return request.get(
+ `/api/attachments/readYamlText?objectName=${objectName}`,
+ {},
+ );
+ },
+ downloadFile: (objectName: string) => {
+ return request.getFile(
+ `/api/attachments/download?objectName=${objectName}`,
+ {},
+ );
+ },
+ search: (payload: AttachmentSearch) => {
+ return request.get("/api/attachments/search", payload);
+ },
+};
diff --git a/src/components/service/management/experimentService.ts b/src/components/service/management/experimentService.ts
new file mode 100644
index 0000000..3042203
--- /dev/null
+++ b/src/components/service/management/experimentService.ts
@@ -0,0 +1,22 @@
+import {
+ ExperimentCreateDto,
+ ExperimentSearch,
+} from "@/components/models/management/Experiments";
+import { request } from "@/components/service/index";
+export const ExperimentService = {
+ add: (payload: ExperimentCreateDto) => {
+ return request.post("/api/experiments", payload);
+ },
+ delete: (id: Number) => {
+ return request.delete(`/api/experiments/${id}`, {});
+ },
+ view: (id: number) => {
+ return request.get(`/api/experiments/${id}`, {});
+ },
+ // update: (id: number, payload: AttachmentUpload) => {
+ // return request.put(`/api/experiments/${id}`, payload);
+ // },
+ search: (payload: ExperimentSearch) => {
+ return request.get("/api/experiments/search", payload);
+ },
+};
diff --git a/src/components/service/management/kubeflowService.ts b/src/components/service/management/kubeflowService.ts
new file mode 100644
index 0000000..9fd07c5
--- /dev/null
+++ b/src/components/service/management/kubeflowService.ts
@@ -0,0 +1,10 @@
+import { kubeflow } from "@/components/models/management/Kubeflow";
+import { request } from "@/components/service/index";
+export const kubeflowService = {
+ upload: (payload: kubeflow) => {
+ return request.post("/pipelines/upload", payload);
+ },
+ run: (payload: kubeflow) => {
+ return request.post("/pipelines/runs", payload);
+ },
+};
diff --git a/src/components/service/management/userManagerService.ts b/src/components/service/management/userManagerService.ts
index bdbec82..b701047 100644
--- a/src/components/service/management/userManagerService.ts
+++ b/src/components/service/management/userManagerService.ts
@@ -26,4 +26,12 @@ export const UserManagerService = {
getUser: (userId: number) => {
return request.get(`/api/auth/users/${userId}`, {});
},
+ // 사용자 수정
+ update: (id: number, payload: User) => {
+ return request.put(`/api/auth/users/${id}`, payload);
+ },
+ // 사용자 삭제
+ delete: (id: Number) => {
+ return request.delete(`/api/auth/users/${id}`, {});
+ },
};
diff --git a/src/components/service/management/AutoflowService.ts b/src/components/service/management/workflowService.ts
similarity index 68%
rename from src/components/service/management/AutoflowService.ts
rename to src/components/service/management/workflowService.ts
index 89ed4c8..b0cf7c8 100644
--- a/src/components/service/management/AutoflowService.ts
+++ b/src/components/service/management/workflowService.ts
@@ -1,13 +1,15 @@
-import { Workflow } from "@/components/models/management/Autoflow";
+import {
+ Workflow,
+ WorkflowSearch,
+} from "@/components/models/management/Workflow";
import { request } from "@/components/service/index";
-export const AutoflowService = {
+export const WorkflowService = {
add: (payload: Workflow) => {
return request.post("/api/workflows", payload);
},
getAll: () => {
return request.get("/api/workflows", {});
},
-
delete: (id: Number) => {
return request.delete(`/api/workflows/${id}`, {});
},
@@ -17,4 +19,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);
+ },
};
diff --git a/src/components/service/management/workflowStepService.ts b/src/components/service/management/workflowStepService.ts
new file mode 100644
index 0000000..b9dec9c
--- /dev/null
+++ b/src/components/service/management/workflowStepService.ts
@@ -0,0 +1,23 @@
+import { request } from "@/components/service/index";
+import { WorkflowStep } from "@/components/models/management/WorkflowStep";
+import { WorkflowSearch } from "@/components/models/management/Workflow";
+export const WorkflowStepService = {
+ add: (payload: WorkflowStep) => {
+ return request.post("/api/workflow-steps", payload);
+ },
+ getAll: (params?: Record) => {
+ return request.get("/api/workflow-steps", { params });
+ },
+ delete: (id: number) => {
+ return request.delete(`/api/workflow-steps/${id}`, {});
+ },
+ view: (id: number) => {
+ return request.get(`/api/workflow-steps/${id}`, {});
+ },
+ update: (id: number, payload: WorkflowStep) => {
+ return request.put(`/api/workflow-steps/${id}`, payload);
+ },
+ search: (payload: WorkflowSearch) => {
+ return request.get("/api/workflow-steps/search", payload);
+ },
+};
diff --git a/src/components/service/project/projectService.ts b/src/components/service/project/projectService.ts
index 735736f..38a350e 100644
--- a/src/components/service/project/projectService.ts
+++ b/src/components/service/project/projectService.ts
@@ -2,7 +2,7 @@ import { request } from "@/components/service/index";
import {
ApiProject,
ProjectAuthority,
- ProjectSearchParams,
+ ProjectSearch,
} from "@/components/models/project/Project";
export const ProjectService = {
@@ -27,9 +27,9 @@ export const ProjectService = {
return request.post("/api/projects", payload);
},
// 검색 및 페이지네이션 프로젝트 목록 조회
- searchProjects: (params: ProjectSearchParams) =>
- request.get("/api/projects/search", params),
-
+ searchProjects: (params: ProjectSearch) => {
+ return request.get("/api/projects/search", params);
+ },
// ----------------------------------------------------------------------
// 프로젝트 권한
@@ -43,4 +43,7 @@ export const ProjectService = {
{},
);
},
+ userProjectAuthority: (id: number) => {
+ return request.get(`/api/projects/users/${id}/projects`, {});
+ },
};
diff --git a/src/components/templates/Datasets/ListComponent.vue b/src/components/templates/Datasets/ListComponent.vue
index 2beb334..005b78b 100644
--- a/src/components/templates/Datasets/ListComponent.vue
+++ b/src/components/templates/Datasets/ListComponent.vue
@@ -2,340 +2,334 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
-// import FormComponent from "@/components/device/FormComponent.vue";
-import { onMounted, ref, watch } from "vue";
+import { onMounted, ref } from "vue";
+import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue";
-import DatasetsBaseDoalog from "@/components/atoms/organisms/DatasetsBaseDoalog.vue";
-import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
-// const store = commonStore();
+import DatasetBaseDoalog from "@/components/atoms/organisms/DatasetBaseDoalog.vue";
+import { AttachmentsService } from "@/components/service/management/attachmentsService";
+import { commonStore } from "@/stores/commonStore";
+const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
-const tableHeader = [
- {
- label: "Title",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "File Name",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "File Path",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Description",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Created Data",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Modified Data",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Action",
- width: "7%",
- style: "word-break: keep-all;",
- },
-];
+
+const username = ref("");
+
+// ===== 검색/페이지네이션(워크플로우 화면과 동일 패턴) =====
+type SearchType = "전체" | "제목" | "작성자";
const searchOptions = [
- {
- searchType: "전체",
- searchText: "",
- },
- {
- searchType: "디바이스 별칭",
- searchText: "deviceAlias",
- },
- {
- searchType: "디바이스 키",
- searchText: "deviceKey",
- },
- {
- searchType: "사용자",
- searchText: "userId",
- },
- {
- searchType: "디바이스 이름",
- searchText: "deviceName",
- },
- {
- searchType: "디바이스 모델",
- searchText: "deviceModel",
- },
- {
- searchType: "디바이스 OS",
- searchText: "deviceOs",
- },
+ { label: "전체", value: "전체" as SearchType },
+ { label: "제목", value: "제목" as SearchType },
+ { label: "작성자", value: "작성자" as SearchType },
];
+const SEARCH_TYPE_MAP: Record = {
+ "": "ALL",
+ 전체: "ALL",
+ 제목: "TITLE",
+ 작성자: "AUTHOR",
+};
+
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
+// 테이블 헤더
+const tableHeader = [
+ { label: "Title", width: "7%", style: "word-break: keep-all;" },
+ { label: "File Name", width: "7%", style: "word-break: keep-all;" },
+ { label: "File Path", width: "7%", style: "word-break: keep-all;" },
+ { label: "Description", width: "7%", style: "word-break: keep-all;" },
+ { label: "Created Data", width: "7%", style: "word-break: keep-all;" },
+ { label: "Modified Data", width: "7%", style: "word-break: keep-all;" },
+ { label: "Action", width: "7%", style: "word-break: keep-all;" },
+];
+
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" | "setting" | "",
+ selectedData: null as any,
allSelected: false,
- selected: [],
+ selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
- userOption: [],
+ userOption: [] as any[],
});
-const getCodeList = () => {
- // UserService.search(data.value.params).then((d) => {
- // if (d.status === 200) {
- // data.value.userOption = d.data.userList;
- // }
- // });
+// 유저명
+function readUsernameFromStorage(): string {
+ try {
+ const raw =
+ storage?.get?.("autoflow-auth") ??
+ storage?.getAuth?.() ??
+ localStorage.getItem("autoflow-auth") ??
+ null;
+ const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
+ const u1 = auth?.userInfo?.username;
+ const u2 = auth?.username;
+ const u3 = auth?.userInfo?.userName;
+ const u4 = auth?.userInfo?.email?.split("@")?.[0];
+ return (u1 || u2 || u3 || u4 || "").toString();
+ } catch {
+ return "";
+ }
+}
+
+// 프로젝트 ID
+const getProjectId = (): number => {
+ const v = Number(localStorage.getItem("projectId"));
+ return Number.isFinite(v) ? v : 0;
};
-const getData = () => {
- const params = { ...data.value.params };
- if (params.searchType === "" || params.searchText === "") {
- delete params.searchType;
- delete params.searchText;
+// 행 변환기(표 스키마에 맞춤)
+const toRow = (a: any) => ({
+ deviceKey: a.id,
+ id: a.id,
+ title: a.title ?? "",
+ fileName: a.originalName ?? "",
+ filePath: a.storagePath ?? "",
+ description: a.description ?? "",
+ createdData: String(a.regDt ?? "")
+ .replace("T", " ")
+ .slice(0, 19),
+ modifiedData: "-",
+});
+
+const fetchList = async () => {
+ const projectId = getProjectId();
+ if (!projectId) {
+ console.warn("[TrainingScript] projectId 없음 — 프로젝트 먼저 선택");
+ data.value.results = [];
+ data.value.totalElements = 0;
+ data.value.pageLength = 0;
+ return;
}
- data.value.results = [
- {
- title: "배터리 상태 예측 모델 프로젝트",
- fileName: "train.py",
- filePath: "/kubeflow-users/battery/train.py",
- description: "배터리 상태 예측 스크립트",
- createdData: "2025-04-28 12:01:00",
- modifiedData: "2025-04-28 12:01:00",
- },
- {
- title: "상태 추적 모델",
- fileName: "detection.py",
- filePath: "/kubeflow-users/status/detection.py",
- description: "상태 추적 스크립트",
- createdData: "2025-04-20 12:01:00",
- modifiedData: "2025-04-28 12:01:00",
- },
- ];
- 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 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 { pageNum, pageSize, searchType, searchText } = data.value.params;
+
+ const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
+ const keyword = (searchText || "").trim();
+ const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
+
+ let reqPage = data.value.params.pageNum;
+ let reqSize = data.value.params.pageSize;
+ if (needLocalFilter) {
+ reqPage = 0;
+ reqSize = 1000;
}
-};
-const 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 payload = {
+ projectId,
+ page: reqPage,
+ size: reqSize,
+ keyword,
+ searchType: mapped,
+ sortField: "id",
+ sortDirection: "DESC",
+ refType: "DATASET",
+ };
+
+ try {
+ const res = await AttachmentsService.search(payload as any);
+ const result = res?.data ?? res;
+ let list = result?.content ?? [];
+
+ if (needLocalFilter) {
+ const kw = keyword.toLowerCase();
+ if (mapped === "TITLE") {
+ list = list.filter((x: any) =>
+ String(x?.title ?? "")
+ .toLowerCase()
+ .includes(kw),
+ );
+ } else if (mapped === "AUTHOR") {
+ list = list.filter((x: any) =>
+ String(x?.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);
+
+ data.value.results = pageSlice.map(toRow);
+ data.value.totalElements = totalElements;
+ data.value.pageLength = totalPages;
+ return;
+ }
+
+ data.value.results = (list as any[]).map(toRow);
+ data.value.totalElements = result?.totalElements ?? list.length;
+ data.value.pageLength = result?.totalPages ?? 1;
+ } catch (err) {
+ console.error("[TrainingScript] 조회 에러:", err);
+ data.value.results = [];
+ data.value.totalElements = 0;
+ data.value.pageLength = 1;
}
};
-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,
- // });
- // }
- // });
- };
+/** 검색 실행 (페이지 1로 리셋) */
+const doSearch = () => {
+ data.value.params.pageNum = 1;
+ fetchList();
+};
+
+/** 페이지 이동 */
+const changePageNum = (page: number) => {
+ data.value.params.pageNum = page;
+ fetchList();
+};
+
+/** 페이지 사이즈 변경 */
+const changePageSize = (size: number) => {
+ data.value.params.pageSize = size;
+ data.value.params.pageNum = 1;
+ fetchList();
+};
+
+// 삭제/수정 버튼 등(기존 로직 유지)
+const removeData = (value?: Array<{ deviceKey: number }>) => {
+ const removeList = value ?? data.value.selected;
+ if (!removeList || removeList.length === 0) return;
- 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;
+ const ids = removeList.map((x) => x.deviceKey);
+ const removeOne = (id: number) =>
+ AttachmentsService.delete(id).then((res) => {
+ if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
+
+ const after = () => {
+ if (
+ ids.length >= data.value.results.length &&
+ data.value.params.pageNum > 1
+ ) {
+ data.value.params.pageNum -= 1;
+ }
+
+ fetchList();
+ data.value.isConfirmDialogVisible = false;
+ data.value.selected = [];
+ data.value.allSelected = false;
+ };
+
+ // 단건/다건 처리
+ if (ids.length === 1) {
+ removeOne(ids[0])
+ .then(() => {
+ store.setSnackbarMsg({
+ color: "success",
+ text: "삭제되었습니다.",
+ result: 200,
+ });
+ after();
+ })
+ .catch((err) => {
+ console.error("삭제 실패:", err);
+ store.setSnackbarMsg({
+ color: "warning",
+ text: "삭제 실패",
+ result: 500,
+ });
+ });
} else {
- Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
- () => {
- // store.setSnackbarMsg({
- // text: "모두 삭제되었습니다.",
- // result: 200,
- // });
- changePageNum();
- data.value.isConfirmDialogVisible = false;
- data.value.selected = [];
- data.value.allSelected = false;
- },
- );
+ Promise.all(ids.map(removeOne))
+ .then(() => {
+ store.setSnackbarMsg({
+ color: "success",
+ text: "모두 삭제되었습니다.",
+ result: 200,
+ });
+ })
+ .catch((err) => {
+ console.error("일부 삭제 실패:", err);
+ store.setSnackbarMsg({
+ color: "warning",
+ text: "일부 삭제 실패",
+ result: 500,
+ });
+ })
+ .finally(after);
}
};
-const handleRemoveData = () => {
- if (data.value.selected.length === 0) {
- // store.setSnackbarMsg({
- // text: "삭제 할 데이터를 선택해주세요. ",
- // result: 500,
- // });
- return;
- }
- if (data.value.allSelected || data.value.selected.length !== 1) {
- data.value.isConfirmDialogVisible = true;
- return;
- }
- //리스트로 삭제 할때
- removeData(undefined);
-};
const closeDetail = () => {
openView.value = false;
};
-const changePageNum = (page) => {
- data.value.params.pageNum = page;
- getData();
-};
-const openSettingModal = (selectedItem) => {
+
+const openDetailModal = (selectedItem: any) => {
data.value.selectedData = selectedItem;
- data.value.modalMode = "setting";
openView.value = true;
};
const openCreateModal = () => {
- data.value.selectedData = null;
data.value.modalMode = "create";
+ data.value.selectedData = {
+ username: username.value,
+ projectId: getProjectId(),
+ };
data.value.isCreateVisible = true;
};
-const openModifyModal = () => {
- data.value.selectedData = null;
+const openModifyModal = (item: any) => {
data.value.modalMode = "edit";
- data.value.isUploadVisible = true;
+ data.value.selectedData = {
+ id: item.deviceKey,
+ title: item.title,
+ description: item.description,
+ };
+ data.value.isCreateVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
- data.value.isCreateVisible = null;
+ data.value.isCreateVisible = false;
+ data.value.selectedData = null;
};
+
const closeModifyModal = () => {
data.value.isModalVisible = false;
- data.value.isUploadVisible = null;
+ data.value.isUploadVisible = false;
+ data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
- ? data.value.results.map((item) => {
- return {
- deviceKey: item.deviceKey,
- };
- })
+ ? data.value.results.map((item: any) => ({ deviceKey: item.deviceKey }))
: [];
};
-
onMounted(() => {
- getData();
- getCodeList();
+ username.value = readUsernameFromStorage();
+ fetchList();
});
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -343,7 +337,9 @@ onMounted(() => {
+
+
{
label="검색조건"
density="compact"
:items="searchOptions"
- item-title="searchType"
- item-value="searchText"
+ item-title="label"
+ item-value="value"
hide-details
- >
+ />
+
{
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
- >
+ @keyup.enter="doSearch"
+ />
@@ -379,14 +376,15 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
- mdi-magnify
+ mdi-magnify
+
@@ -394,10 +392,12 @@ onMounted(() => {
+
총 {{ data.totalDataLength.toLocaleString() }}개
-
+ >총 {{ data.totalElements.toLocaleString() }}개
+
{
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
- >
+ @update:model-value="changePageSize"
+ />
+
- Create Dataset
-
+ Add Dataset
+
@@ -428,8 +428,6 @@ onMounted(() => {
density="comfortable"
fixed-header
height="625"
- col-md-12
- col-12
overflow-x-auto
>
@@ -439,18 +437,20 @@ onMounted(() => {
:style="`width:${item.width}`"
/>
+
|
{{ item.label }}
|
+
{
| {{ item.createdData }} |
{{ item.modifiedData }} |
-
-
+
+
{
|
+
{
:total-visible="10"
color="primary"
rounded="circle"
- @update:model-value="getData"
- >
+ @update:model-value="changePageNum"
+ />
+
+
-
-
-
-
-
+
diff --git a/src/components/templates/Datasets/ViewComponent.vue b/src/components/templates/Datasets/ViewComponent.vue
index 36ee04a..87cb1ce 100644
--- a/src/components/templates/Datasets/ViewComponent.vue
+++ b/src/components/templates/Datasets/ViewComponent.vue
@@ -1,147 +1,164 @@
@@ -191,19 +208,6 @@ onMounted(() => {
Created ID
{{ experimentInfo.createdId }}
-
-
- Modified Date
-
- {{
- experimentInfo.modifiedDate
- }}
- Modified ID
-
- {{ experimentInfo.modifiedId }}
-
@@ -217,7 +221,20 @@ onMounted(() => {
File
- {{ experimentInfo.fileName }}
+
+ {{ experimentInfo.fileName }}
+
+ mdi-download
+
+
diff --git a/src/components/home/ListComponent.vue b/src/components/templates/home/ListComponent.vue
similarity index 69%
rename from src/components/home/ListComponent.vue
rename to src/components/templates/home/ListComponent.vue
index 800484a..a9ff167 100644
--- a/src/components/home/ListComponent.vue
+++ b/src/components/templates/home/ListComponent.vue
@@ -1,44 +1,58 @@
@@ -138,15 +214,23 @@ const getSelectedAllData = () => {
-
+
{{
item.title
}}
- {{
- item.date
- }}
+
+ {{ formatToYmdHm(item.timestamp) }}
+
+
+
+
+
+
+
+
+ 최근 등록/수정된 워크플로우가 없습니다.
diff --git a/src/components/templates/Project/ListComponent.vue b/src/components/templates/projects/ListComponent.vue
similarity index 67%
rename from src/components/templates/Project/ListComponent.vue
rename to src/components/templates/projects/ListComponent.vue
index a80b41d..580be1c 100644
--- a/src/components/templates/Project/ListComponent.vue
+++ b/src/components/templates/projects/ListComponent.vue
@@ -2,20 +2,29 @@
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 } from "@/components/models/project/Project";
+import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
+import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
-import type {
- Permission,
- ApiProject,
-} from "@/components/models/project/Project";
-
-// ──────────────────────────────────────────────────────────────
-// 권한/공통
-// ──────────────────────────────────────────────────────────────
const store = commonStore();
+type SearchType = "전체" | "제목" | "작성자";
+
+const searchOptions = [
+ { label: "전체", value: "전체" as SearchType },
+ { label: "제목", value: "제목" as SearchType },
+ { label: "작성자", value: "작성자" as SearchType },
+];
+
+const SEARCH_TYPE_MAP: Record = {
+ "": "ALL",
+ 전체: "ALL",
+ 제목: "TITLE",
+ 작성자: "AUTHOR",
+};
+
const DEFAULT_PERMISSIONS: Permission[] = [
"CREATE",
"READ",
@@ -31,9 +40,6 @@ const refreshRoles = () => {
};
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;" },
@@ -43,13 +49,6 @@ const tableHeader = [
{ 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 },
@@ -60,13 +59,23 @@ type Row = {
no: number;
name: string;
desc: string;
- users: string[];
+ users: string[]; // 목록 표시용(= mod_user_nm 우선)
registDt: string;
deviceKey: number;
};
+// 원본 reg 값 보관(수정 시 그대로 유지)
+const projectRegById = ref>(
+ {},
+);
+
const data = ref({
- params: { pageNum: 1, pageSize: 10, searchType: "", searchText: "" },
+ params: {
+ pageNum: 1,
+ pageSize: 10,
+ searchType: "전체" as SearchType,
+ searchText: "",
+ },
results: [] as Row[],
totalDataLength: 0,
pageLength: 0,
@@ -87,60 +96,157 @@ const splitCsv = (v?: string) =>
.map((s) => s.trim())
.filter(Boolean);
-// ──────────────────────────────────────────────────────────────
-/** 사용자 목록 (v-select items) */
-// ──────────────────────────────────────────────────────────────
type UserOption = { id: number | string; username: string };
const userOptions = ref([]);
-
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 {
+// 서버 → UI 한 줄 변환 (mod_user_nm 우선)
+function toRow(p: any, no: number): Row {
+ let displayNm = "";
+ if (typeof p.modUserNm === "string" && p.modUserNm.length > 0)
+ displayNm = p.modUserNm;
+ else if (typeof p.regUserNm === "string") displayNm = p.regUserNm;
+
+ // 원본 reg 보관
+ projectRegById.value[p.id] = { regId: p.regUserId, regNm: p.regUserNm };
+
return {
- no: offset + index + 1,
- name: p.prjNm ?? "-",
- desc: p.prjDesc ?? "-",
- users: splitCsv(p.regUserId ?? p.regUserNm),
- registDt: fmtDate(p.regDate ?? p.prjStartDt),
+ no,
+ name: p.prjNm,
+ desc: p.prjDesc,
+ users: splitCsv(displayNm),
+ registDt: fmtDate(p.prjStartDt), // 그대로 사용
deviceKey: p.id,
};
}
+/** ===== 목록 조회 (검색/페이지네이션 포함) ===== */
async function getData() {
- const { pageNum, pageSize } = data.value.params;
- const startIndex = (pageNum - 1) * pageSize;
+ const { pageNum, pageSize, searchType, searchText } = data.value.params;
- const res = await ProjectService.search();
- const raw = Array.isArray(res.data) ? res.data : (res.data?.content ?? []);
+ const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
+ const keyword = (searchText || "").trim();
- data.value.totalDataLength = Array.isArray(res.data)
- ? raw.length
- : (res.data?.totalElements ?? raw.length);
+ const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
- 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),
- );
+ let reqPage = data.value.params.pageNum;
+ let reqSize = data.value.params.pageSize;
+ if (needLocalFilter) {
+ reqPage = 0;
+ reqSize = 1000;
+ }
- 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 payload = {
+ page: reqPage,
+ size: reqSize,
+ keyword,
+ searchType: mapped,
+ sortField: "id",
+ sortDirection: "DESC",
+ };
+
+ try {
+ const res = await ProjectService.searchProjects(payload as any);
+ const result = res?.data ?? res;
+ let list: any[] = result?.content ?? (Array.isArray(result) ? result : []);
+
+ // 로컬 필터
+ if (needLocalFilter) {
+ const kw = keyword.toLowerCase();
+ if (mapped === "TITLE") {
+ list = list.filter((x: any) =>
+ String(x?.prjNm ?? "")
+ .toLowerCase()
+ .includes(kw),
+ );
+ } else if (mapped === "AUTHOR") {
+ list = list.filter((x: any) => {
+ let authorStr = "";
+ if (typeof x.modUserNm === "string" && x.modUserNm.length > 0)
+ authorStr = x.modUserNm;
+ else if (typeof x.regUserNm === "string") authorStr = x.regUserNm;
+ return authorStr.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;
+
+ // ★ 새로 그릴 때 reg 원본 맵도 갱신
+ projectRegById.value = {};
+ data.value.results = pageSlice.map((p: any, i: number) =>
+ toRow(p, Math.max(1, firstNo - i)),
+ );
+ data.value.totalDataLength = totalElements;
+ data.value.pageLength = totalPages;
+ return;
+ }
+
+ const totalElements =
+ typeof result?.totalElements === "number"
+ ? result.totalElements
+ : (list.length ?? 0);
+
+ const serverPage = result?.pageable?.pageNumber;
+ const serverSize = result?.pageable?.pageSize;
+ const offset =
+ typeof result?.pageable?.offset === "number"
+ ? result.pageable.offset
+ : typeof serverPage === "number" && typeof serverSize === "number"
+ ? serverPage * serverSize
+ : (pageNum - 1) * pageSize;
+
+ const firstNo = totalElements - offset;
+
+ // ★ 새로 그릴 때 reg 원본 맵도 갱신
+ projectRegById.value = {};
+ data.value.results = list.map((p: any, i: number) =>
+ toRow(p, Math.max(1, firstNo - i)),
+ );
+
+ data.value.totalDataLength = totalElements;
+ data.value.pageLength =
+ typeof result?.totalPages === "number"
+ ? result.totalPages
+ : Math.max(1, Math.ceil(totalElements / pageSize));
+ } catch (e) {
+ console.error("[Project] search error:", e);
+ data.value.results = [];
+ data.value.totalDataLength = 0;
+ data.value.pageLength = 1;
+ }
}
-// ──────────────────────────────────────────────────────────────
-/** 폼 & 권한 부여 & 저장 흐름 (카드형과 동일) */
-// ──────────────────────────────────────────────────────────────
+/** 트리거 */
+function doSearch() {
+ data.value.params.pageNum = 1;
+ getData();
+}
+function changePageSize(size: number) {
+ data.value.params.pageSize = size;
+ data.value.params.pageNum = 1;
+ getData();
+}
+function changePageNum(page: number) {
+ data.value.params.pageNum = page;
+ getData();
+}
+watch(
+ () => data.value.params.searchType,
+ () => doSearch(),
+);
+
+/** ===== 생성/수정 다이얼로그 ===== */
const form = ref({
prjCd: "",
prjNm: "",
@@ -156,12 +262,52 @@ const resetForm = () => {
form.value.selectedUsers = [];
};
-const buildApiPayload = (): ApiProject => {
+// 생성 payload (mod* 안 보냄)
+type NewProjectPayload = {
+ id: null;
+ prjCd: string;
+ prjNm: string;
+ prjDesc: string;
+ prjStartDt: string;
+ prjEndDt: string;
+ delYn: string;
+ regDate: string;
+ regUserId?: string;
+ regUserNm?: string;
+};
+// 수정 payload (reg* 유지, mod* 반영)
+type UpdateProjectPayload = {
+ id: number;
+ prjCd: string;
+ prjNm: string;
+ prjDesc: string;
+ prjStartDt: string;
+ prjEndDt: string;
+ delYn: string;
+ regDate: string;
+ regUserId?: string;
+ regUserNm?: string;
+ modDate: string;
+ modUserId?: string;
+ modUserNm?: string;
+};
+
+function buildCreatePayload(): NewProjectPayload {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
- const namesCsv = form.value.selectedUsers.join(",");
+
+ const names = form.value.selectedUsers;
+ const namesCsv = names.join(",");
+ const idsCsv = names
+ .map((name) => userOptions.value.find((u) => u.username === name)?.id)
+ .filter(
+ (v): v is number | string => v !== undefined && v !== null && v !== "",
+ )
+ .map(String)
+ .join(",");
+
return {
- id: data.value.modalMode === "edit" ? editingProjectId.value! : null,
+ id: null,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
@@ -169,13 +315,44 @@ const buildApiPayload = (): ApiProject => {
prjEndDt: today,
delYn: "N",
regDate: nowIso,
- regUserId: namesCsv,
+ regUserId: idsCsv,
regUserNm: namesCsv,
+ };
+}
+
+function buildUpdatePayload(): UpdateProjectPayload {
+ const today = new Date().toISOString().slice(0, 10);
+ const nowIso = new Date().toISOString();
+
+ const names = form.value.selectedUsers;
+ const namesCsv = names.join(",");
+ const idsCsv = names
+ .map((name) => userOptions.value.find((u) => u.username === name)?.id)
+ .filter(
+ (v): v is number | string => v !== undefined && v !== null && v !== "",
+ )
+ .map(String)
+ .join(",");
+
+ const id = editingProjectId.value!;
+ const kept = projectRegById.value[id] || {};
+
+ return {
+ id,
+ prjCd: form.value.prjCd,
+ prjNm: form.value.prjNm,
+ prjDesc: form.value.prjDesc,
+ prjStartDt: today,
+ prjEndDt: today,
+ delYn: "N",
+ regDate: nowIso,
+ regUserId: kept.regId,
+ regUserNm: kept.regNm,
modDate: nowIso,
- modUserId: namesCsv,
+ modUserId: idsCsv,
modUserNm: namesCsv,
};
-};
+}
async function grantDefaultPermissions(projectId: number, usernames: string[]) {
if (!usernames?.length) return;
@@ -198,13 +375,14 @@ async function grantDefaultPermissions(projectId: number, usernames: string[]) {
async function saveProject() {
try {
- const payload = buildApiPayload();
let projectId: number;
if (data.value.modalMode === "create") {
+ const payload = buildCreatePayload();
const res = await ProjectService.add(payload);
projectId = res.data.id;
} else {
+ const payload = buildUpdatePayload();
await ProjectService.update(editingProjectId.value!, payload);
projectId = editingProjectId.value!;
}
@@ -217,15 +395,41 @@ async function saveProject() {
}
}
-// ──────────────────────────────────────────────────────────────
-// 삭제
-// ──────────────────────────────────────────────────────────────
+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] : [];
+
+ // 선택 목록에 없는 사용자도 보이도록 보정
+ 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 getSelectedAllData() {
+ data.value.selected = data.value.allSelected
+ ? data.value.results.map((r) => ({ deviceKey: r.deviceKey }))
+ : [];
+}
+
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);
@@ -283,55 +487,6 @@ async function deleteRows(targetList?: Array<{ deviceKey: number }>) {
}
}
-// ──────────────────────────────────────────────────────────────
-// 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) => {
@@ -339,9 +494,6 @@ watch(
},
);
-// ──────────────────────────────────────────────────────────────
-// 초기 로딩
-// ──────────────────────────────────────────────────────────────
onMounted(async () => {
refreshRoles();
await Promise.all([loadUsers(), getData()]);
@@ -359,7 +511,7 @@ onMounted(async () => {
@@ -373,13 +525,14 @@ onMounted(async () => {
min-width="180"
class="mr-3 mt-3 mb-3"
>
+
@@ -393,7 +546,7 @@ onMounted(async () => {
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
+ @keyup.enter="doSearch"
/>
@@ -402,7 +555,7 @@ onMounted(async () => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
mdi-magnify
@@ -410,6 +563,7 @@ onMounted(async () => {
+
@@ -421,6 +575,7 @@ onMounted(async () => {
>총 {{ data.totalDataLength.toLocaleString() }}개
+
{
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
+ @update:model-value="changePageSize"
/>
@@ -502,12 +657,10 @@ onMounted(async () => {
{{ item.no }} |
{{ item.name }} |
-
{{ item.desc }}
|
-
{
- Cancel
{{ data.modalMode === "create" ? "Create" : "Save" }}
+ Cancel
diff --git a/src/components/templates/run/experiment/ListComponent.vue b/src/components/templates/run/experiment/ListComponent.vue
index bc15a6f..e7d8d20 100644
--- a/src/components/templates/run/experiment/ListComponent.vue
+++ b/src/components/templates/run/experiment/ListComponent.vue
@@ -1,296 +1,299 @@
@@ -308,7 +311,9 @@ onMounted(() => {
+
+
{
label="검색조건"
density="compact"
:items="searchOptions"
- item-title="searchType"
- item-value="searchText"
+ item-title="label"
+ item-value="value"
hide-details
- >
+ />
+
{
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
- >
+ @keyup.enter="doSearch"
+ />
@@ -344,14 +350,15 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
- mdi-magnify
+ mdi-magnify
+
@@ -360,8 +367,8 @@ onMounted(() => {
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
총 {{ data.totalDataLength.toLocaleString() }}개
-
+ >총 {{ data.totalElements.toLocaleString() }}개
@@ -374,18 +381,20 @@ onMounted(() => {
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
- >
+ @update:model-value="changePageSize"
+ />
+
Create Experiment
-
+ >Create Experiment
+
@@ -393,8 +402,6 @@ onMounted(() => {
density="comfortable"
fixed-header
height="625"
- col-md-12
- col-12
overflow-x-auto
>
@@ -406,20 +413,11 @@ onMounted(() => {
-
|
{{ item.label }}
|
@@ -431,21 +429,12 @@ onMounted(() => {
:key="i"
class="text-center"
>
-
{{ item.name }} |
{{ item.description }} |
{{ item.createdDate }} |
{{ item.createdID }} |
-
+
{
+
{
:total-visible="10"
color="primary"
rounded="circle"
- @update:model-value="getData"
- >
+ @update:model-value="changePageNum"
+ />
-
+
+
+
{}"
/>
+
-
+
diff --git a/src/components/templates/run/experiment/ViewComponent.vue b/src/components/templates/run/experiment/ViewComponent.vue
index bc4ff1b..9d8418a 100644
--- a/src/components/templates/run/experiment/ViewComponent.vue
+++ b/src/components/templates/run/experiment/ViewComponent.vue
@@ -1,246 +1,78 @@
@@ -257,7 +89,10 @@ onMounted(() => {
-
+
Experiment Information
@@ -287,14 +122,23 @@ onMounted(() => {
+ Created ID
+ {{ experimentInfo.createdId }}
Created Date
{{
experimentInfo.createdDate
}}
- Created ID
- {{ experimentInfo.createdId }}
+
+
+
+
+
+ Kubeflow ID
+ {{ experimentInfo.kubeFlowId }}
+ MLflow ID
+ {{ experimentInfo.mlFlowId }}
@@ -306,100 +150,20 @@ onMounted(() => {
}}
-
-
-
-
- Runs
-
-
-
-
-
-
-
-
-
- |
- {{ item.label }}
- |
-
-
-
-
- | {{ item.name }} |
- {{ item.status }} |
- {{ item.Duration }} |
- {{ item.Pipeline }} |
- {{ item.registDt }} |
-
-
-
-
-
-
-
-
-
- Back to List
-
-
+
+
+
+
+
+
+ Back to List
+
-
-
diff --git a/src/components/templates/stepconfig/ListComponent.vue b/src/components/templates/stepconfig/ListComponent.vue
index 9c1d899..974d295 100644
--- a/src/components/templates/stepconfig/ListComponent.vue
+++ b/src/components/templates/stepconfig/ListComponent.vue
@@ -1,16 +1,20 @@
@@ -344,6 +321,8 @@ onMounted(() => {
+
+
@@ -357,11 +336,13 @@ onMounted(() => {
label="검색조건"
density="compact"
:items="searchOptions"
- item-title="searchType"
- item-value="searchText"
+ item-title="label"
+ item-value="value"
hide-details
- >
+ @update:model-value="doSearch"
+ />
+
{
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
- >
+ @keyup.enter="doSearch"
+ />
@@ -380,14 +361,15 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
- mdi-magnify
+ mdi-magnify
+
@@ -396,9 +378,10 @@ onMounted(() => {
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
총 {{ data.totalDataLength.toLocaleString() }}개
-
+ >총 {{ data.totalElements.toLocaleString() }}개
+
{
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
- >
+ @update:model-value="changePageSize"
+ />
+
+
+ Create Workflow Step
+
+
@@ -436,6 +426,7 @@ onMounted(() => {
:style="`width:${item.width}`"
/>
+
|
@@ -445,7 +436,7 @@ onMounted(() => {
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
- >
+ />
|
{
|
+
{
>
mdi-checkbox-marked-circle
-
- mdi-alert-circle
-
+ mdi-alert-circle
| {{ item.workflow }} |
-
-
{
|
+
+
{
:total-visible="10"
color="primary"
rounded="circle"
- @update:model-value="getData"
- >
+ @update:model-value="changePageNum"
+ />
@@ -521,15 +511,23 @@ onMounted(() => {
+
+
-
+
-
+
+
+
diff --git a/src/components/templates/stepconfig/ViewComponent.vue b/src/components/templates/stepconfig/ViewComponent.vue
index 325c40d..5662e5a 100644
--- a/src/components/templates/stepconfig/ViewComponent.vue
+++ b/src/components/templates/stepconfig/ViewComponent.vue
@@ -1,3 +1,98 @@
+
+
@@ -17,22 +112,27 @@
Workflow Step Name
- Train Model
+ {{ info.stepName }}
+
Workflow Name
- sentiment-analysis
+ {{ info.workflowName }}
Created Date
- 2025-02-06
+ {{ info.createdDate }}
+
Created ID
- ADMIN_001
+ {{ info.createdId }}
+
+ Back to List
+
-
+
-
+
-
+
-
+
-
+
-
-
diff --git a/src/components/templates/trainingscript/ListComponent.vue b/src/components/templates/trainingscript/ListComponent.vue
index 5b85386..5992118 100644
--- a/src/components/templates/trainingscript/ListComponent.vue
+++ b/src/components/templates/trainingscript/ListComponent.vue
@@ -2,340 +2,334 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
-// import FormComponent from "@/components/device/FormComponent.vue";
-import { onMounted, ref, watch } from "vue";
+import { onMounted, ref } from "vue";
+import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue";
import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue";
-import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
-// const store = commonStore();
+import { AttachmentsService } from "@/components/service/management/attachmentsService";
+import { commonStore } from "@/stores/commonStore";
+const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
-const tableHeader = [
- {
- label: "Title",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "File Name",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "File Path",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Description",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Created Data",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Modified Data",
- width: "7%",
- style: "word-break: keep-all;",
- },
- {
- label: "Action",
- width: "7%",
- style: "word-break: keep-all;",
- },
-];
+
+const username = ref("");
+
+type SearchType = "전체" | "제목" | "작성자";
const searchOptions = [
- {
- searchType: "전체",
- searchText: "",
- },
- {
- searchType: "디바이스 별칭",
- searchText: "deviceAlias",
- },
- {
- searchType: "디바이스 키",
- searchText: "deviceKey",
- },
- {
- searchType: "사용자",
- searchText: "userId",
- },
- {
- searchType: "디바이스 이름",
- searchText: "deviceName",
- },
- {
- searchType: "디바이스 모델",
- searchText: "deviceModel",
- },
- {
- searchType: "디바이스 OS",
- searchText: "deviceOs",
- },
+ { label: "전체", value: "전체" as SearchType },
+ { label: "제목", value: "제목" as SearchType },
+ { label: "작성자", value: "작성자" as SearchType },
];
+const SEARCH_TYPE_MAP: Record = {
+ "": "ALL",
+ 전체: "ALL",
+ 제목: "TITLE",
+ 작성자: "AUTHOR",
+};
+
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
+// 테이블 헤더
+const tableHeader = [
+ { label: "Title", width: "7%", style: "word-break: keep-all;" },
+ { label: "File Name", width: "7%", style: "word-break: keep-all;" },
+ { label: "File Path", width: "7%", style: "word-break: keep-all;" },
+ { label: "Description", width: "7%", style: "word-break: keep-all;" },
+ { label: "Created Data", width: "7%", style: "word-break: keep-all;" },
+ { label: "Modified Data", width: "7%", style: "word-break: keep-all;" },
+ { label: "Action", width: "7%", style: "word-break: keep-all;" },
+];
+
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" | "setting" | "",
+ selectedData: null as any,
allSelected: false,
- selected: [],
+ selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
- userOption: [],
+ userOption: [] as any[],
});
-const getCodeList = () => {
- // UserService.search(data.value.params).then((d) => {
- // if (d.status === 200) {
- // data.value.userOption = d.data.userList;
- // }
- // });
-};
+// 유저명
+function readUsernameFromStorage(): string {
+ try {
+ const raw =
+ storage?.get?.("autoflow-auth") ??
+ storage?.getAuth?.() ??
+ localStorage.getItem("autoflow-auth") ??
+ null;
+ const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
+ const u1 = auth?.userInfo?.username;
-const getData = () => {
- const params = { ...data.value.params };
- if (params.searchType === "" || params.searchText === "") {
- delete params.searchType;
- delete params.searchText;
+ return u1.toString();
+ } catch {
+ return "";
}
- data.value.results = [
- {
- title: "배터리 상태 예측 모델 프로젝트",
- fileName: "train.py",
- filePath: "/kubeflow-users/battery/train.py",
- description: "배터리 상태 예측 스크립트",
- createdData: "2025-04-28 12:01:00",
- modifiedData: "2025-04-28 12:01:00",
- },
- {
- title: "상태 추적 모델",
- fileName: "detection.py",
- filePath: "/kubeflow-users/status/detection.py",
- description: "상태 추적 스크립트",
- createdData: "2025-04-20 12:01:00",
- modifiedData: "2025-04-28 12:01:00",
- },
- ];
- 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);
- // });
+}
+
+// 프로젝트 ID
+const getProjectId = (): number => {
+ const v = Number(localStorage.getItem("projectId"));
+ return Number.isFinite(v) ? v : 0;
};
-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 fmtDate = (v?: string) =>
+ v ? String(v).replace("T", " ").slice(0, 19) : "";
+
+// 행 변환기(표 스키마에 맞춤)
+const toRow = (a: any) => ({
+ deviceKey: a.id,
+ id: a.id,
+ title: a.title ?? "",
+ fileName: a.originalName ?? "",
+ filePath: a.storagePath ?? "",
+ description: a.description ?? "",
+ createdData: fmtDate(a.regDt),
+
+ modifiedData: "-",
+});
+
+const fetchList = async () => {
+ const projectId = Number(localStorage.getItem("projectId"));
+ if (!projectId) {
+ console.warn("[TrainingScript] projectId 없음 — 프로젝트 먼저 선택");
+ data.value.results = [];
+ data.value.totalElements = 0;
+ data.value.pageLength = 0;
+ return;
}
-};
-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 { pageNum, pageSize, searchType, searchText } = data.value.params;
+
+ const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
+ const keyword = (searchText || "").trim();
+ const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
+
+ let reqPage = data.value.params.pageNum;
+ let reqSize = data.value.params.pageSize;
+ if (needLocalFilter) {
+ reqPage = 0;
+ reqSize = 1000;
}
-};
-const removeData = (value) => {
- let removeList = value ? value : data.value.selected;
- const remove = (code) => {
- // return DeviceService.delete(code).then((d) => {
- // if (d.status !== 200) {
- // store.setSnackbarMsg({
- // text: d,
- // result: 500,
- // });
- // }
- // });
+ const payload = {
+ projectId,
+ page: reqPage,
+ size: reqSize,
+ keyword,
+ searchType: mapped,
+ sortField: "id",
+ sortDirection: "DESC",
+ refType: "TRAINING_SCRIPT",
};
- 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;
- },
- );
+ try {
+ const res = await AttachmentsService.search(payload as any);
+ const result = res?.data ?? res;
+ let list = result?.content ?? [];
+
+ if (needLocalFilter) {
+ const kw = keyword.toLowerCase();
+ if (mapped === "TITLE") {
+ list = list.filter((x: any) =>
+ String(x?.title ?? "")
+ .toLowerCase()
+ .includes(kw),
+ );
+ } else if (mapped === "AUTHOR") {
+ list = list.filter((x: any) =>
+ String(x?.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);
+
+ data.value.results = pageSlice.map(toRow);
+ data.value.totalElements = totalElements;
+ data.value.pageLength = totalPages;
+ return;
+ }
+
+ // 서버 페이징 그대로 적용
+ data.value.results = (list as any[]).map(toRow);
+ data.value.totalElements = result?.totalElements ?? list.length;
+ data.value.pageLength = result?.totalPages ?? 1;
+ } catch (err) {
+ console.error("[TrainingScript] 조회 에러:", err);
+ data.value.results = [];
+ data.value.totalElements = 0;
+ data.value.pageLength = 1;
}
};
-const handleRemoveData = () => {
- if (data.value.selected.length === 0) {
- // store.setSnackbarMsg({
- // text: "삭제 할 데이터를 선택해주세요. ",
- // result: 500,
- // });
- return;
- }
- if (data.value.allSelected || data.value.selected.length !== 1) {
- data.value.isConfirmDialogVisible = true;
- return;
+/** 검색 실행 (페이지 1로 리셋) */
+const doSearch = () => {
+ data.value.params.pageNum = 1;
+ fetchList();
+};
+
+/** 페이지 이동 */
+const changePageNum = (page: number) => {
+ data.value.params.pageNum = page;
+ fetchList();
+};
+
+/** 페이지 사이즈 변경 */
+const changePageSize = (size: number) => {
+ data.value.params.pageSize = size;
+ data.value.params.pageNum = 1;
+ fetchList();
+};
+
+// 삭제/수정 버튼 등(기존 로직 유지)
+const removeData = (value?: Array<{ deviceKey: number }>) => {
+ const removeList = value ?? data.value.selected;
+ if (!removeList || removeList.length === 0) return;
+
+ const ids = removeList.map((x) => x.deviceKey);
+ const removeOne = (id: number) =>
+ AttachmentsService.delete(id).then((res) => {
+ if (res.status < 200 || res.status >= 300) return Promise.reject(res);
+ });
+
+ const after = () => {
+ if (
+ ids.length >= data.value.results.length &&
+ data.value.params.pageNum > 1
+ ) {
+ data.value.params.pageNum -= 1;
+ }
+
+ fetchList();
+ data.value.isConfirmDialogVisible = false;
+ data.value.selected = [];
+ data.value.allSelected = false;
+ };
+
+ // 단건/다건 처리
+ if (ids.length === 1) {
+ removeOne(ids[0])
+ .then(() => {
+ store.setSnackbarMsg({
+ color: "success",
+ text: "삭제되었습니다.",
+ result: 200,
+ });
+ after();
+ })
+ .catch((err) => {
+ console.error("삭제 실패:", err);
+ store.setSnackbarMsg({
+ color: "warning",
+ text: "삭제 실패",
+ result: 500,
+ });
+ });
+ } else {
+ Promise.all(ids.map(removeOne))
+ .then(() => {
+ store.setSnackbarMsg({
+ color: "success",
+ text: "모두 삭제되었습니다.",
+ result: 200,
+ });
+ })
+ .catch((err) => {
+ console.error("일부 삭제 실패:", err);
+ store.setSnackbarMsg({
+ color: "warning",
+ text: "일부 삭제 실패",
+ result: 500,
+ });
+ })
+ .finally(after);
}
- //리스트로 삭제 할때
- removeData(undefined);
};
+
const closeDetail = () => {
openView.value = false;
};
-const changePageNum = (page) => {
- data.value.params.pageNum = page;
- getData();
-};
-const openSettingModal = (selectedItem) => {
+
+const openDetailModal = (selectedItem: any) => {
data.value.selectedData = selectedItem;
- data.value.modalMode = "setting";
openView.value = true;
};
const openCreateModal = () => {
- data.value.selectedData = null;
data.value.modalMode = "create";
+ data.value.selectedData = {
+ username: username.value,
+ projectId: getProjectId(),
+ };
data.value.isCreateVisible = true;
};
-const openModifyModal = () => {
- data.value.selectedData = null;
+const openModifyModal = (item: any) => {
data.value.modalMode = "edit";
- data.value.isUploadVisible = true;
+ data.value.selectedData = {
+ id: item.deviceKey,
+ title: item.title,
+ description: item.description,
+ };
+ data.value.isCreateVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
- data.value.isCreateVisible = null;
+ data.value.isCreateVisible = false;
+ data.value.selectedData = null;
};
+
const closeModifyModal = () => {
data.value.isModalVisible = false;
- data.value.isUploadVisible = null;
+ data.value.isUploadVisible = false;
+ data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
- ? data.value.results.map((item) => {
- return {
- deviceKey: item.deviceKey,
- };
- })
+ ? data.value.results.map((item: any) => ({ deviceKey: item.deviceKey }))
: [];
};
-
onMounted(() => {
- getData();
- getCodeList();
+ username.value = readUsernameFromStorage();
+ fetchList();
});
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -343,7 +337,9 @@ onMounted(() => {
+
+
{
label="검색조건"
density="compact"
:items="searchOptions"
- item-title="searchType"
- item-value="searchText"
+ item-title="label"
+ item-value="value"
hide-details
- >
+ />
+
{
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
- >
+ @keyup.enter="doSearch"
+ />
@@ -379,14 +376,15 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
- mdi-magnify
+ mdi-magnify
+
@@ -394,10 +392,12 @@ onMounted(() => {
+
총 {{ data.totalDataLength.toLocaleString() }}개
-
+ >총 {{ data.totalElements.toLocaleString() }}개
+
{
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
- >
+ @update:model-value="changePageSize"
+ />
+
- Create Script
-
+ Create Script
+
@@ -428,8 +428,6 @@ onMounted(() => {
density="comfortable"
fixed-header
height="625"
- col-md-12
- col-12
overflow-x-auto
>
@@ -439,18 +437,20 @@ onMounted(() => {
:style="`width:${item.width}`"
/>
+
|
{{ item.label }}
|
+
{
| {{ item.createdData }} |
{{ item.modifiedData }} |
-
-
+
+
{
|
+
{
:total-visible="10"
color="primary"
rounded="circle"
- @update:model-value="getData"
- >
+ @update:model-value="changePageNum"
+ />
+
+
-
-
-
-
+
diff --git a/src/components/templates/trainingscript/ViewComponent.vue b/src/components/templates/trainingscript/ViewComponent.vue
index bd008ef..bc82581 100644
--- a/src/components/templates/trainingscript/ViewComponent.vue
+++ b/src/components/templates/trainingscript/ViewComponent.vue
@@ -1,259 +1,175 @@
-
-
-
-
-
-
-
-
-
- Training Script Information
-
-
-
-
-
- Training Script Title
-
- {{ experimentInfo.modelName }}
-
-
-
-
-
- File Name
- {{
- experimentInfo.projectName
- }}
-
-
-
- File Path
- {{
- experimentInfo.experimentName
- }}
-
-
-
+
+
+
+
+
-
- Created Date
-
- {{ experimentInfo.deployDate }}
- Modified Date
-
- {{ experimentInfo.createdId }}
-
-
+
+
+ Training Script Information
+
+
+
+
+ Training Script Title
+ {{ info.title }}
+
+
+
+ File Name
+ {{ info.fileName }}
+
+
+
+ File Path
+ {{
+ info.filePath
+ }}
+
+
+
+ Created Date
+ {{ info.createdDate }}
+ Modified Date
+ {{ info.modifiedDate }}
+
+
+
+ Created ID
+ {{ info.createdId }}
+
+
+
+ Description
+ {{ info.description }}
+
+
+
-
-
- Description
- {{
- experimentInfo.description
- }}
-
-
-
-
-
- Training Script Preview
-
-
-
-
- Back to List
-
-
+
+
+
+ Training Script Preview
+
+
+
+
+
+ Back to List
+
@@ -261,31 +177,6 @@ onBeforeUnmount(() => {
diff --git a/src/components/templates/users/ListComponent.vue b/src/components/templates/users/ListComponent.vue
new file mode 100644
index 0000000..880ec39
--- /dev/null
+++ b/src/components/templates/users/ListComponent.vue
@@ -0,0 +1,669 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-magnify
+
+
+
+
+
+
+
+
+
+ 총 {{ data.totalDataLength.toLocaleString() }}개
+
+
+
+
+
+
+
+
+
+
+ Create User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+ {{ item.label }}
+ |
+
+
+
+
+
+ |
+
+ |
+
+ {{ item.no }} |
+ {{ item.name }} |
+
+
+ {{ item.desc || "-" }}
+ |
+
+
+
+
+ {{ u }}
+
+
+ -
+ |
+
+
+
+
+ {{ p }}
+
+
+ -
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ data.modalMode === "create" ? "Create User" : "Modify User" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ data.modalMode === "create" ? "Create" : "Save" }}
+
+ Cancel
+
+
+
+
+
+
+
diff --git a/src/components/templates/workflow/ListComponent.vue b/src/components/templates/workflow/ListComponent.vue
index f62a47c..ff1fda8 100644
--- a/src/components/templates/workflow/ListComponent.vue
+++ b/src/components/templates/workflow/ListComponent.vue
@@ -1,89 +1,42 @@
@@ -442,10 +366,11 @@ onMounted(() => {
label="검색조건"
density="compact"
:items="searchOptions"
- item-title="searchType"
- item-value="searchText"
+ item-title="label"
+ item-value="value"
hide-details
- >
+ @update:model-value="doSearch"
+ />
{
required
class="mt-3 mb-3"
hide-details
- @keyup.enter="changePageNum(1)"
+ @keyup.enter="doSearch"
>
@@ -465,7 +390,7 @@ onMounted(() => {
size="large"
color="primary"
:rounded="5"
- @click="changePageNum(1)"
+ @click="doSearch"
>
mdi-magnify
@@ -481,8 +406,8 @@ onMounted(() => {
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
총 {{ data.totalDataLength.toLocaleString() }}개
-
+ >총 {{ data.totalElements.toLocaleString() }}개
@@ -495,7 +420,7 @@ onMounted(() => {
variant="outlined"
color="primary"
hide-details
- @update:model-value="changePageNum(1)"
+ @update:model-value="changePageSize"
>
@@ -567,14 +492,14 @@ onMounted(() => {
|
{{ item.no }} |
{{ item.name }} |
- {{ item.stepCount }} |
- {{ item.configProgress }} |
+ {{ item.description }} |
+ {{ item.version }} |
{{ item.kubeflowStatus }} |
{{ formatDateTime(item.registDt) }} |
+
-
-
+
{
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
- @handle-data="saveData"
- :user-option="data.userOption"
/>
@@ -615,8 +538,14 @@ onMounted(() => {
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeUploadModal"
- @handle-data="saveData"
- :user-option="data.userOption"
+ />
+
+
+
diff --git a/src/components/templates/workflow/ViewComponent.vue b/src/components/templates/workflow/ViewComponent.vue
index 901a8e2..dfe770f 100644
--- a/src/components/templates/workflow/ViewComponent.vue
+++ b/src/components/templates/workflow/ViewComponent.vue
@@ -2,7 +2,7 @@
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
import * as monaco from "monaco-editor";
import "monaco-editor/min/vs/editor/editor.main.css";
-import { AutoflowService } from "@/components/service/management/AutoflowService";
+import { WorkflowService } from "@/components/service/management/workflowService";
type TabKey = "details" | "yaml";
@@ -10,32 +10,22 @@ const props = defineProps<{ id: number | string }>();
const emit = defineEmits<{ (e: "close"): void }>();
const activeTab = ref("details");
+
+// ----- Monaco Editor -----
const editorRef = ref(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
+// 화면에 뿌릴 상세 데이터(요청 항목만)
const detail = ref({
- workflowName: "",
+ name: "",
version: "",
- workflowDescription: "",
- createdDate: "",
- createdId: "",
+ description: "",
+ kubeflowStatus: "",
+ namespace: "",
+ pipelineId: "",
+ regDt: "",
});
-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
@@ -51,30 +41,32 @@ spec:
args: ["echo hello"]
`;
-/** ===== 상세 조회 ===== */
+// 간단한 날짜 포맷터 (ISO/T 포함 모두 대응)
+function formatDateTime(raw?: string): string {
+ if (!raw) return "-";
+ const s = String(raw).replace("T", " ");
+ const m = s.match(/^(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})/);
+ return m ? m[1] : s.slice(0, 19);
+}
+
+// ===== 상세 조회 =====
async function fetchDetail(id: number | string) {
try {
- const res = await AutoflowService.view(Number(id));
- const d = res.data;
+ const res = await WorkflowService.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 = [];
- }
+ // 백엔드 필드명(스네이크/카멜) 혼재 대비 매핑
+ detail.value = {
+ name: d.name ?? d.workflowName ?? "",
+ version: String(d.version ?? ""),
+ description: d.description ?? d.workflowDescription ?? "",
+ kubeflowStatus: d.kubeflow_status ?? d.kubeflowStatus ?? "",
+ namespace: d.namespace ?? "",
+ pipelineId: d.pipeline_id ?? d.pipelineId ?? "",
+ regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate),
+ };
- // YAML 표시 (서버 필드 이름에 맞춰 하나라도 있으면 사용)
+ // YAML (있으면 보여주고, 없으면 기본 예시)
const yamlFromServer =
d.workflowYaml ||
d.yaml ||
@@ -86,11 +78,11 @@ async function fetchDetail(id: number | string) {
editorInstance.setValue(yamlFromServer || defaultYaml);
}
} catch (e) {
- console.error("[Child] view API failed:", e);
+ console.error("[Workflow Detail] view API failed:", e);
}
}
-/** ===== 마운트 & 변경 감지 ===== */
+// ===== 마운트 & 변경 감지 =====
onMounted(() => {
if (editorRef.value) {
editorInstance = monaco.editor.create(editorRef.value, {
@@ -105,7 +97,6 @@ onMounted(() => {
}
});
-// props.id가 바뀌면 재조회
watch(
() => props.id,
(val) => {
@@ -122,6 +113,22 @@ onBeforeUnmount(() => {
editorInstance = null;
}
});
+
+// ===== (나중 사용) Step 테이블 정의 =====
+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 }>
+>([]);
@@ -156,37 +163,67 @@ onBeforeUnmount(() => {
Workflow Information
+
Workflow Name
- {{ detail.workflowName }}
+ {{ detail.name || "-" }}
Version
- {{ detail.version }}
+ {{ detail.version || "-" }}
+
+
Workflow Description
- {{ detail.workflowDescription }}
+ {{ detail.description || "-" }}
+
+
Created DateKubeflow Status
- {{ detail.createdDate }}
+ {{ detail.kubeflowStatus || "-" }}
+ Namespace
+ {{ detail.namespace || "-" }}
+
+
+
+
+
Created IDPipeline ID
- {{ detail.createdId }}
+ {{
+ detail.pipelineId || "-"
+ }}
+
+
+
+
+
+ Created Date
+ {{ detail.regDt || "-" }}
+
+
+
+ Back to List
+
+
-
+
@@ -253,10 +282,14 @@ onBeforeUnmount(() => {
min-height: 500px;
padding-bottom: 84px;
}
-
.back-to-list {
position: absolute;
right: 24px;
bottom: 24px;
}
+.text-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/src/pages/HomeView.vue b/src/pages/HomeView.vue
index 352f6c7..2da9f2f 100644
--- a/src/pages/HomeView.vue
+++ b/src/pages/HomeView.vue
@@ -1,5 +1,5 @@
diff --git a/src/pages/ProjectView.vue b/src/pages/ProjectView.vue
index 67cc292..4492698 100644
--- a/src/pages/ProjectView.vue
+++ b/src/pages/ProjectView.vue
@@ -1,5 +1,5 @@
diff --git a/src/pages/SignupView.vue b/src/pages/SignupView.vue
index 7d254f4..fc54f7c 100644
--- a/src/pages/SignupView.vue
+++ b/src/pages/SignupView.vue
@@ -8,12 +8,12 @@ import logo2 from "@/assets/workflow.png";
import { UserManagerService } from "@/components/service/management/userManagerService";
const router = useRouter();
-
+const ROLE_ITEMS = ["ROLE_USER", "ROLE_MODERATOR", "ROLE_ADMIN"];
const data = ref({
form: false,
username: "",
email: "",
- role: [],
+ role: ROLE_ITEMS[0],
password: "",
loading: false,
snackbar: false,
@@ -35,7 +35,7 @@ const resetLogin = () => {
const resetSignup = () => {
data.value.username = "";
data.value.email = "";
- data.value.role = [];
+ data.value.role = "";
data.value.password = "";
data.value.loading = false;
};
@@ -44,7 +44,7 @@ const signUp = () => {
const payload = {
username: data.value.username,
email: data.value.email,
- role: data.value.role,
+ role: data.value.role ? [data.value.role] : [],
password: data.value.password,
};
console.log("회원가입 호출 payload:", payload);
@@ -112,8 +112,8 @@ const signUp = () => {
>
+import ListComponent from "@/components/templates/users/ListComponent.vue";
+
+
+
+
+
+
+
diff --git a/src/router/index.js b/src/router/index.js
index ec2a5e3..82f1c49 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -5,141 +5,114 @@ 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 +145,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;
});
diff --git a/src/utils/menuUtils.js b/src/utils/menuUtils.js
index 90efece..e7421f2 100644
--- a/src/utils/menuUtils.js
+++ b/src/utils/menuUtils.js
@@ -6,24 +6,18 @@ export const menuUtils = {
value: "home",
icon: "mdi-monitor-multiple",
},
- {
- title: "Project",
- path: "/project",
- value: "project",
- icon: "mdi-folder-cog-outline",
- },
{
title: "Workflows",
path: "/workflows",
value: "workflows",
icon: "mdi-code-braces",
},
- {
- title: "Workflow Step Config",
- path: "/workflow-step-config",
- value: "workflow-step-config",
- icon: "mdi-hammer-wrench",
- },
+ // {
+ // title: "Workflow Step Config",
+ // path: "/workflow-step-config",
+ // value: "workflow-step-config",
+ // icon: "mdi-hammer-wrench",
+ // },
{
title: "Run",
path: "/run",
diff --git a/src/views/Select.vue b/src/views/Select.vue
index ed6ce2b..4e7052c 100644
--- a/src/views/Select.vue
+++ b/src/views/Select.vue
@@ -5,7 +5,6 @@ import { useAutoflowStore } from "@/stores/autoflowStore";
import type {
UiProject,
- ApiProject,
Permission,
} from "@/components/models/project/Project";
import { ProjectService } from "@/components/service/project/projectService";
@@ -31,6 +30,11 @@ const menuX = ref(0);
const menuY = ref(0);
const selectedIndex = ref(null);
+// id별 원본 등록자(reg) 보관 (값 그대로, 가공/문자열화 X)
+const projectRegById = ref>(
+ {},
+);
+
const projects = ref([]);
type UserOption = { id: number | string; username: string };
const userOptions = ref([]);
@@ -43,7 +47,8 @@ const form = ref({
prjDesc: "",
selectedUsers: [] as string[],
});
-/** ===== 서버 응답 타입 ===== */
+
+/** ===== 롤 ===== */
const roles = ref([]);
const refreshRoles = () => {
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
@@ -52,27 +57,93 @@ const refreshRoles = () => {
};
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
+/** ===== 페이지네이션 상태 ===== */
+const pager = ref({ pageNum: 1, pageSize: 8, total: 0, pageLength: 1 });
+
+const pagedProjects = computed(() => {
+ const total = projects.value.length;
+ pager.value.total = total;
+ pager.value.pageLength = Math.max(1, Math.ceil(total / pager.value.pageSize));
+ if (pager.value.pageNum > pager.value.pageLength)
+ pager.value.pageNum = pager.value.pageLength;
+ if (pager.value.pageNum < 1) pager.value.pageNum = 1;
+ const start = (pager.value.pageNum - 1) * pager.value.pageSize;
+ return projects.value.slice(start, start + pager.value.pageSize);
+});
+
+const changePageNum = (page: number) => {
+ pager.value.pageNum = page;
+};
+const changePageSize = (size: number) => {
+ pager.value.pageSize = size;
+ pager.value.pageNum = 1;
+};
+
/** ===== 서버 응답 타입 ===== */
interface ProjectSearchResponseItem {
id: number;
prjNm: string;
prjDesc: string;
prjStartDt?: string;
- regUserId?: string; // 화면의 "생성자"에 그대로 표시(콤마 구분 username들)
+ regUserId?: string;
+ regUserNm?: string;
+ modUserId?: string;
+ modUserNm?: string;
}
interface UserResponseItem {
id: number | string;
username: string;
}
+/** ===== 페이로드 타입(서비스 시그니처를 못 바꾼다는 가정에서 지역 타입 분리) ===== */
+// 생성 시 mod* 안 보냄
+type NewProjectPayload = {
+ id: null;
+ prjCd: string;
+ prjNm: string;
+ prjDesc: string;
+ prjStartDt: string;
+ prjEndDt: string;
+ delYn: string;
+ regDate: string;
+ regUserId?: string;
+ regUserNm?: string;
+};
+// 수정 시 reg*는 기존 그대로 유지, mod*만 채움
+type UpdateProjectPayload = {
+ id: number;
+ prjCd: string;
+ prjNm: string;
+ prjDesc: string;
+ prjStartDt: string;
+ prjEndDt: string;
+ delYn: string;
+ regDate: string;
+ regUserId?: string;
+ regUserNm?: string;
+ modDate: string;
+ modUserId?: string;
+ modUserNm?: string;
+};
+
/** ===== 유틸 ===== */
-const buildApiProjectPayload = (): ApiProject => {
+function buildCreatePayload(): NewProjectPayload {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
- const namesCsv = form.value.selectedUsers.join(",");
+ const names = form.value.selectedUsers;
+ const namesCsv = names.join(",");
+ const idsCsv = names
+ .map((name) => userOptions.value.find((u) => u.username === name)?.id)
+ .filter(
+ (v): v is number | string => v !== undefined && v !== null && v !== "",
+ )
+ .map(String)
+ .join(","); // "6,5"
+
+ // mod* 없음 (생성)
return {
- id: modalMode.value === "edit" ? editingProjectId.value! : null,
+ id: null,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
@@ -80,13 +151,50 @@ const buildApiProjectPayload = (): ApiProject => {
prjEndDt: today,
delYn: "N",
regDate: nowIso,
- regUserId: namesCsv,
+ regUserId: idsCsv,
regUserNm: namesCsv,
+ };
+}
+
+function buildUpdatePayload(): UpdateProjectPayload {
+ const today = new Date().toISOString().slice(0, 10);
+ const nowIso = new Date().toISOString();
+
+ const names = form.value.selectedUsers;
+ const namesCsv = names.join(",");
+ const idsCsv = names
+ .map((name) => userOptions.value.find((u) => u.username === name)?.id)
+ .filter(
+ (v): v is number | string => v !== undefined && v !== null && v !== "",
+ )
+ .map(String)
+ .join(",");
+
+ const id = editingProjectId.value!;
+ const kept = projectRegById.value[id] || {};
+
+ // reg*는 DB 원본 유지, mod*만 현재 선택으로 반영
+ return {
+ id,
+ prjCd: form.value.prjCd,
+ prjNm: form.value.prjNm,
+ prjDesc: form.value.prjDesc,
+ prjStartDt: today,
+ prjEndDt: today,
+ delYn: "N",
+ regDate: nowIso,
+ regUserId: kept.regId,
+ regUserNm: kept.regNm,
modDate: nowIso,
- modUserId: namesCsv,
+ modUserId: idsCsv,
modUserNm: namesCsv,
};
-};
+}
+
+const fillerCount = computed(() =>
+ Math.max(0, pager.value.pageSize - pagedProjects.value.length),
+);
+const fillers = computed(() => Array.from({ length: fillerCount.value }));
const resetForm = () => {
form.value.prjCd = `PRJ${Date.now()}`;
@@ -99,13 +207,33 @@ const loadProjects = async () => {
try {
const { data } = await ProjectService.search();
const rawList = data as ProjectSearchResponseItem[];
- projects.value = rawList.map((p) => ({
- id: p.id,
- title: p.prjNm,
- creator: p.regUserId ?? "",
- date: p.prjStartDt ?? "",
- description: p.prjDesc,
- }));
+
+ const sorted = [...rawList].sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
+
+ projectRegById.value = {};
+ projects.value = sorted.map((p) => {
+ // 원본 reg 값은 따로 보관(수정 시 reg* 유지용)
+ projectRegById.value[p.id] = { regId: p.regUserId, regNm: p.regUserNm };
+
+ // ★ 화면/모달에 보여줄 선택값은 mod_user_nm이 있으면 그걸로, 없으면 reg_user_nm
+ const displayNm =
+ p.modUserNm && p.modUserNm.length > 0 ? p.modUserNm : p.regUserNm || "";
+
+ return {
+ id: p.id,
+ title: p.prjNm,
+ creator: displayNm, // ← 이 값이 모달 v-select v-model에 들어감
+ date: p.prjStartDt, // fallback 없이 그대로
+ description: p.prjDesc,
+ };
+ });
+
+ if (
+ pager.value.pageNum >
+ Math.max(1, Math.ceil(projects.value.length / pager.value.pageSize))
+ ) {
+ pager.value.pageNum = 1;
+ }
} catch (e) {
console.error("프로젝트 조회 실패:", e);
}
@@ -136,30 +264,27 @@ const closeDialog = () => {
};
const selectProject = (index: number) => {
- const selected = projects.value[index];
+ const selected = pagedProjects.value[index];
autoflowStore.setProjectId(selected.id);
autoflowStore.setProjectName(selected.title);
router.push("/home");
};
-/** ===== 프로젝트 저장 & 권한 부여 ===== */
const grantDefaultPermissions = async (
projectId: number,
usernames: string[],
) => {
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로 보장
+ userId: uid,
permissions: DEFAULT_PERMISSIONS,
}),
),
@@ -171,20 +296,17 @@ const saveProject = async () => {
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
return;
}
-
- const payload = buildApiProjectPayload();
-
try {
let projectId: number;
-
if (modalMode.value === "create") {
- const createRes = await ProjectService.add(payload);
+ const createPayload = buildCreatePayload(); // mod* 없음
+ const createRes = await ProjectService.add(createPayload); // ← 오타 수정
projectId = createRes.data.id;
} else {
- await ProjectService.update(editingProjectId.value!, payload);
+ const updatePayload = buildUpdatePayload(); // reg* 유지, mod* 반영
+ await ProjectService.update(editingProjectId.value!, updatePayload); // ← non-null 보장
projectId = editingProjectId.value!;
}
-
await grantDefaultPermissions(projectId, form.value.selectedUsers);
await loadProjects();
closeDialog();
@@ -197,8 +319,7 @@ const saveProject = async () => {
const deleteProject = async () => {
try {
if (selectedIndex.value === null) return;
- const target = projects.value[selectedIndex.value];
-
+ const target = pagedProjects.value[selectedIndex.value];
await ProjectService.delete(target.id);
await loadProjects();
} catch (e: any) {
@@ -209,43 +330,6 @@ const deleteProject = async () => {
}
};
-// 프로젝트 권한있을 때
-// const deleteProject = async (): Promise => {
-// 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 = () => {
if (!isAdmin.value) {
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
@@ -260,19 +344,19 @@ const onAddProject = () => {
const modifyProject = () => {
contextMenu.value = false;
if (selectedIndex.value === null) return;
+ const selected = pagedProjects.value[selectedIndex.value];
- const selected = projects.value[selectedIndex.value];
modalMode.value = "edit";
editingProjectId.value = selected.id;
-
form.value.prjCd = selected.title;
form.value.prjNm = selected.title;
form.value.prjDesc = selected.description;
+
+ // creator는 reg_user_nm 그대로 들어오므로 그대로 분해. 값 없으면 []
form.value.selectedUsers =
- selected.creator
- ?.split(",")
- .map((s) => s.trim())
- .filter(Boolean) ?? [];
+ typeof selected.creator === "string" && selected.creator.length
+ ? selected.creator.split(",").map((s) => s.trim())
+ : [];
dialog.value = true;
};
@@ -301,7 +385,6 @@ onBeforeUnmount(() => {
-
{
+
+
+
{
class="d-flex"
>
{
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
- Cancel
{{ modalMode === "create" ? "Create" : "Save" }}
+ Cancel
-
+
diff --git a/typed-router.d.ts b/typed-router.d.ts
index 8c4a1a7..e75a5c5 100644
--- a/typed-router.d.ts
+++ b/typed-router.d.ts
@@ -29,6 +29,7 @@ declare module 'vue-router/auto-routes' {
'/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record, Record>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record, Record>,
'/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record, Record>,
+ '/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record, Record>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record, Record>,
'/WorkflowView': RouteRecordInfo<'/WorkflowView', '/WorkflowView', Record, Record>,
}
| |