-
{
-
+
@@ -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/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 a84d065..2900b80 100644
--- a/src/components/atoms/organisms/WorkflowsBaseDialog.vue
+++ b/src/components/atoms/organisms/WorkflowsBaseDialog.vue
@@ -34,7 +34,6 @@ const errorMsg = ref("");
// ====== KFP 이름 제한 & 한글 제한 유틸 ======
const KFP_NAME_REGEX = /^[a-z0-9]([-a-z0-9\.]*[a-z0-9])?$/; // 소문자/숫자/.-, 시작/끝 영숫자
-const KOREAN_RX = /[ㄱ-ㅎㅏ-ㅣ가-힣]/g;
const sanitizeKfpName = (s: string) => {
let x = (s ?? "").toLowerCase();
@@ -45,13 +44,11 @@ const sanitizeKfpName = (s: string) => {
x = x.replace(/[^a-z0-9]+$/, ""); // 뒤쪽 비허용 제거
return x;
};
-const stripKorean = (s: string) => (s ?? "").replace(KOREAN_RX, "");
// 힌트/에러 메시지
const nameHint = ref(
"허용 문자: 소문자 a–z, 숫자 0–9, '-', '.' (시작/끝은 영숫자). 한글/공백/대문자/언더스코어 불가",
);
-const descHint = ref("한글은 사용할 수 없습니다.");
const nameInvalid = computed(
() => !!form.value.name && !KFP_NAME_REGEX.test(form.value.name),
);
@@ -72,10 +69,6 @@ function onNameInput(v: string) {
}
form.value.name = cleaned;
}
-function onDescInput(v: string) {
- const cleaned = stripKorean(v || "");
- form.value.description = cleaned;
-}
function extractApiErrorMessage(err: any): string {
const status = err?.response?.status;
@@ -149,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);
@@ -161,7 +156,7 @@ async function submit() {
// 제출 직전에 한 번 더 정제 & 검증
form.value.name = sanitizeKfpName(form.value.name);
- form.value.description = stripKorean(form.value.description);
+ form.value.description = (form.value.description ?? "").trim();
const name = form.value.name.trim();
if (!name || !KFP_NAME_REGEX.test(name)) {
@@ -322,7 +317,6 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
dense
hide-details="auto"
persistent-hint
- :hint="descHint"
@update:model-value="onDescInput"
/>
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/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/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/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/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/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/templates/home/ListComponent.vue b/src/components/templates/home/ListComponent.vue
index 4ef56ea..a9ff167 100644
--- a/src/components/templates/home/ListComponent.vue
+++ b/src/components/templates/home/ListComponent.vue
@@ -1,47 +1,58 @@
diff --git a/src/components/templates/projects/ListComponent.vue b/src/components/templates/projects/ListComponent.vue
index a80b41d..580be1c 100644
--- a/src/components/templates/projects/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 efa1371..974d295 100644
--- a/src/components/templates/stepconfig/ListComponent.vue
+++ b/src/components/templates/stepconfig/ListComponent.vue
@@ -333,7 +333,7 @@ onMounted(() => {
>
("");
+
+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 5881fb3..ff1fda8 100644
--- a/src/components/templates/workflow/ListComponent.vue
+++ b/src/components/templates/workflow/ListComponent.vue
@@ -363,7 +363,7 @@ onMounted(() => {
>
| |