Merge branch 'feature/main-js' of http://192.168.10.110/Autoflow/autoflow-web-console into feature/main-js

main
bjkim 9 months ago
commit 7aeaf9bb7e

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

@ -0,0 +1,3 @@
NODE_ENV = "prod"
VITE_APP_API_SERVER_URL = "http://cuuva.com:2480/autoflow-server-mgmt"
VITE_ROOT_PATH = "/autoflow"

@ -0,0 +1,30 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"eslint:recommended",
"plugin:@vue/typescript/recommended",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
],
parserOptions: {
parser: "@typescript-eslint/parser",
},
rules: {
"vue/multi-word-component-names": "off",
"vue/comment-directive": "off",
"vue/no-v-html": "off",
"no-console": process.env.NODE_ENV === "prod" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "prod" ? "warn" : "off",
"vue/no-deprecated-slot-attribute": "off",
"@typescript-eslint/no-explicit-any": "off",
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
],
},
};

@ -0,0 +1,9 @@
FROM nginx:stable-alpine
RUN apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime
COPY default.conf /etc/nginx/conf.d/default.conf
COPY dist/. /usr/share/nginx/html/autoflow
EXPOSE 80
ENTRYPOINT ["nginx", "-g", "daemon off;"]

23
components.d.ts vendored

@ -9,13 +9,30 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AppFooter: typeof import('./src/components/AppFooter.vue')['default']
CompareComponent: typeof import('./src/components/templates/run/executions/CompareComponent.vue')['default']
DatasetsBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetsBaseDoalog.vue')['default']
DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default']
DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default']
ExecutionBaseDialog: typeof import('./src/components/atoms/organisms/ExecutionBaseDialog.vue')['default']
ExperimentCreateDialog: typeof import('./src/components/atoms/organisms/ExperimentCreateDialog.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IconDeleteBtn: typeof import('./src/components/button/IconDeleteBtn.vue')['default']
IconModifyBtn: typeof import('./src/components/button/IconModifyBtn.vue')['default']
IconArrowDown: typeof import('./src/components/atoms/button/IconArrowDown.vue')['default']
IconArrowUp: typeof import('./src/components/atoms/button/IconArrowUp.vue')['default']
IconDeleteBtn: typeof import('./src/components/atoms/button/IconDeleteBtn.vue')['default']
IconDeployment: typeof import('./src/components/atoms/button/IconDeployment.vue')['default']
IconDownloadBtn: typeof import('./src/components/atoms/button/IconDownloadBtn.vue')['default']
IconInfoBtn: typeof import('./src/components/atoms/button/IconInfoBtn.vue')['default']
IconModifyBtn: typeof import('./src/components/atoms/button/IconModifyBtn.vue')['default']
IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.vue')['default']
LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default']
ListComponent: typeof import('./src/components/workflow/ListComponent.vue')['default']
ListComponent: typeof import('./src/components/home/ListComponent.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StapComfigDialog: typeof import('./src/components/atoms/organisms/StapComfigDialog.vue')['default']
TrainingScriptBaseDoalog: typeof import('./src/components/atoms/organisms/TrainingScriptBaseDoalog.vue')['default']
ViewComponent: typeof import('./src/components/templates/Datasets/ViewComponent.vue')['default']
WorkflowDialog: typeof import('./src/components/atoms/organisms/WorkflowDialog.vue')['default']
WorkflowsCreateDialog: typeof import('./src/components/atoms/organisms/WorkflowsCreateDialog.vue')['default']
WorkflowsUploadDialog: typeof import('./src/components/atoms/organisms/WorkflowsUploadDialog.vue')['default']
}
}

@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /autoflow/index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

@ -1,3 +1,9 @@
import vuetify from 'eslint-config-vuetify'
export default vuetify()
export default {
vuetify(),
'vue/attributes-order': 'off',
}
}

@ -1,10 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Vuetify 3</title>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AutoFlow Web Console</title>
</head>
<body>
<div id="app"></div>

@ -6,15 +6,10 @@
"baseUrl": "./",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
"@/*": ["src/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"types": ["vite/client"]
},
"include": ["src/**/*", "env.d.ts"]
}

@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: autoflow
namespace: autoflow
labels:
app: autoflow
spec:
replicas: 1
selector:
matchLabels:
app: autoflow
template:
metadata:
labels:
app: autoflow
spec:
containers:
- image: 192.168.10.120:32100/autoflow:2025.07.005
name: autoflow
ports:
- containerPort: 80
resources:
requests:
cpu: 500m
memory: 100Mi
# limits:
# cpu: 1000m
# memory: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: autoflow
namespace: autoflow
spec:
type: ClusterIP
selector:
app: autoflow
ports:
- protocol: TCP
port: 80
targetPort: 80

@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: autoflow
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 1g
name: autoflow
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /autoflow
pathType: Prefix
backend:
service:
name: autoflow
port:
number: 80

421
package-lock.json generated

@ -10,6 +10,9 @@
"dependencies": {
"@fontsource/roboto": "5.2.5",
"@mdi/font": "7.4.47",
"axios": "^1.11.0",
"monaco-editor": "^0.52.2",
"plotly.js-dist-min": "^3.0.1",
"prettier": "^3.5.3",
"vue": "^3.5.13",
"vuetify": "^3.8.1"
@ -869,12 +872,59 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
@ -1270,6 +1320,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.8.0"
}
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@ -2020,7 +2081,7 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -2148,6 +2209,23 @@
"node": ">=16.14.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2248,6 +2326,14 @@
"devOptional": true,
"license": "MIT/X11"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/builtin-modules": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
@ -2261,6 +2347,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2423,6 +2522,26 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/comment-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
@ -2546,6 +2665,29 @@
"dev": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.169",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.169.tgz",
@ -2573,6 +2715,51 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@ -3511,6 +3698,42 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3525,6 +3748,52 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
@ -3564,6 +3833,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@ -3581,6 +3862,45 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@ -3989,6 +4309,15 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4026,6 +4355,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -4091,6 +4441,12 @@
"pathe": "^2.0.1"
}
},
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -4396,6 +4752,12 @@
"pathe": "^2.0.3"
}
},
"node_modules/plotly.js-dist-min": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-3.0.1.tgz",
"integrity": "sha512-RReOqr6TfoHaTbVAoHR1UbTCOSRDsQ7Hbthd+3XAxOwaKmxCE3oejMhLG7urQSqWC65DAcSKV23kZd8e+7mG7w==",
"license": "MIT"
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@ -4493,6 +4855,12 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5165,6 +5533,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5174,6 +5553,18 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/spdx-correct": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@ -5344,6 +5735,26 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
@ -5472,6 +5883,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/unicorn-magic": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",

@ -4,14 +4,17 @@
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"dev": "vite --mode dev",
"build": "vite build --mode prod",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"@fontsource/roboto": "5.2.5",
"@mdi/font": "7.4.47",
"axios": "^1.11.0",
"monaco-editor": "^0.52.2",
"plotly.js-dist-min": "^3.0.1",
"prettier": "^3.5.3",
"vue": "^3.5.13",
"vuetify": "^3.8.1"

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

@ -0,0 +1,26 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="Down">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-arrow-down"
class="ma-1"
color="brack"
density="comfortable"
elevation="0"
@click="onClick"
size="small"
v-bind="props"
></v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,26 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="Up">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-arrow-up"
class="ma-1"
color="brack"
density="comfortable"
elevation="0"
@click="onClick"
size="small"
v-bind="props"
></v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,26 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="배포">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-server"
class="ma-1"
color="error"
density="comfortable"
elevation="0"
@click="onClick"
size="small"
v-bind="props"
></v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,27 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="다운로드">
<template v-slot:activator="{ props }">
<v-btn
@click="onClick"
class="ma-1"
icon="mdi-download"
color="success"
density="comfortable"
elevation="0"
size="small"
v-bind="props"
>
</v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,27 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="정보">
<template v-slot:activator="{ props }">
<v-btn
@click="onClick"
class="ma-1"
icon="mdi-file-document"
color="info"
density="comfortable"
elevation="0"
size="small"
v-bind="props"
>
</v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,27 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="설정">
<template v-slot:activator="{ props }">
<v-btn
@click="onClick"
class="ma-1"
icon="mdi-cog-play-outline"
color="success"
density="comfortable"
elevation="0"
size="small"
v-bind="props"
>
</v-btn>
</template>
</v-tooltip>
</template>

@ -0,0 +1,110 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal"]);
const visible = ref(true);
const fileInput = ref<HTMLInputElement | null>(null);
const form = ref({
name: "",
description: "",
file: "",
});
//
const dialogTitle = computed(() => {
if (props.mode === "create") return "Create Dataset";
if (props.mode === "edit") return "Edit Dataset";
return "Clone Execution";
});
const onChooseFile = () => {
fileInput.value?.click();
};
const submit = () => {
emit("handle-data", form.value);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
{{ dialogTitle }}
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submit">
<v-row dense class="mb-6">
<v-col cols="6">
<v-subheader class="font-weight-medium white--text mb-2">
Dataset Title
</v-subheader>
<v-text-field
v-model="form.name"
variant="outlined"
dense
hide-details
outlined
style="background: #1e1e1e; color: #fff"
/>
</v-col>
<v-col cols="6">
<v-subheader class="font-weight-medium white--text mb-2">
Dataset Version
</v-subheader>
<v-text-field
variant="outlined"
dense
hide-details
outlined
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Description
</label>
<v-text-field
v-model="form.description"
variant="outlined"
dense
hide-details
required
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Upload File
</label>
<v-file-input
v-model="form.file"
label="Upload File"
@click:append-outer="onChooseFile"
outlined
dense
hide-details
/>
</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,168 @@
<script setup lang="ts">
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
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 { ref, watch } from "vue";
const steps = ref([
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
{
order: 2,
stepName: "Preprocessing",
type: "Preprocess",
status: "Not Configured",
},
{
order: 3,
stepName: "Train Model",
type: "Train",
status: "Not Configured",
},
]);
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal"]);
const visible = ref(true);
const form = ref({
name: "",
description: "",
});
const submit = () => {
emit("handle-data", form.value);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
Deploy Model
</v-card-title>
<v-card-text class="pa-6">
<div class="text-subtitle-1 font-weight-medium mb-4">
Select Model : ImageClassifier
</div>
<VDivider class="my-2" />
<v-form @submit.prevent="submit">
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>OTA
</label>
<v-row dense class="mb-6">
<v-col cols="12">
<v-subheader class="font-weight-medium white--text mb-2">
Select Package
</v-subheader>
<v-select
dense
hide-details
outlined
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
</div>
</v-form>
</v-card-text>
<v-card-text class="pt-6 pb-4 px-6">
<v-subheader class="font-weight-medium mb-2">
Package Preview
</v-subheader>
<v-sheet class="pa-4 mb-6" elevation="1" rounded>
<v-row dense>
<!-- Linux -->
<v-col cols="12" md="6">
<div class="font-weight-medium mb-2">Linux</div>
<v-text-field
label="File Name"
placeholder="4_EdgeInfra_Perception.sh"
dense
outlined
hide-details
/>
<v-text-field
label="File Path"
placeholder="/home/etri/TeslaSystem/EdgeInfraVision/RUN"
dense
outlined
hide-details
/>
</v-col>
<!-- Windows -->
<v-col cols="12" md="6">
<div class="font-weight-medium mb-2">Windows</div>
<v-text-field
label="File Name"
placeholder="4_EdgeInfra_Perception.exe"
dense
outlined
hide-details
/>
<v-text-field
label="File Path"
placeholder="C:/etri/TeslaSystem/EdgeInfraVision/RUN"
dense
outlined
hide-details
/>
</v-col>
</v-row>
</v-sheet>
<!-- Package Preview -->
<!-- Software Name / Version -->
<v-row dense class="mb-4">
<v-col cols="12">
<v-text-field
label="Software Name"
placeholder="Enter software Name"
dense
outlined
hide-details
/>
</v-col>
<v-col cols="12">
<v-text-field
label="Software Version"
placeholder="Enter software Version"
dense
outlined
hide-details
/>
</v-col>
</v-row>
<!-- Executed 여부 -->
<v-row dense class="mb-6">
<v-col cols="12">
<v-radio-group row>
<v-radio label="Executed" value="executed" />
<v-radio label="Not Executed" value="not_executed" />
</v-radio-group>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,159 @@
<script setup lang="ts">
import { ref, watch, computed, defineProps, defineEmits } from "vue";
const props = defineProps<{
modelValue: boolean;
mode: "create" | "edit" | "clone";
selectedData: any;
workflowList: string[];
executionTypes: string[];
}>();
const emit = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "save", payload: any): void;
}>();
// state
const internalWorkflow = ref(props.selectedData?.workflow || "");
const internalExecType = ref(props.selectedData?.execType || "");
const internalName = ref(props.selectedData?.name || "");
const internalDesc = ref(props.selectedData?.description || "");
const internalExperiment = ref(props.selectedData?.experiment || "");
//
watch(
() => props.modelValue,
(open) => {
if (open && props.selectedData) {
internalWorkflow.value = props.selectedData.workflow;
internalExecType.value = props.selectedData.execType;
internalName.value = props.selectedData.name;
internalDesc.value = props.selectedData.description;
internalExperiment.value = props.selectedData.experiment;
}
},
);
//
const dialogTitle = computed(() => {
if (props.mode === "create") return "Create Execution";
if (props.mode === "edit") return "Edit Execution";
return "Clone Execution";
});
function onSave() {
emit("save", {
workflow: internalWorkflow.value,
execType: internalExecType.value,
name: internalName.value,
description: internalDesc.value,
experiment: internalExperiment.value,
});
emit("update:modelValue", false);
}
function onClose() {
emit("update:modelValue", false);
}
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>{{ dialogTitle }}</v-card-title
>
<!-- 본문 -->
<v-card-text class="white--text pa-6">
<!-- Workflow Information 헤더 -->
<v-card-subtitle class="white--text mb-4"
>Workflow Information</v-card-subtitle
>
<v-row dense class="mb-6">
<v-col cols="6">
<v-subheader class="font-weight-medium white--text mb-2">
Select Workflow
</v-subheader>
<v-select
v-model="internalWorkflow"
:items="workflowList"
dense
hide-details
outlined
style="background: #1e1e1e; color: #fff"
/>
</v-col>
<v-col cols="6">
<v-subheader class="font-weight-medium white--text mb-2">
Execution Type
</v-subheader>
<v-select
v-model="internalExecType"
:items="executionTypes"
dense
hide-details
outlined
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Execution Name -->
<v-row dense class="mb-6">
<v-col cols="12">
<v-subheader class="font-weight-medium white--text mb-2">
Execution Type
</v-subheader>
<v-text-field
v-model="internalName"
dense
hide-details
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Description -->
<v-row dense class="mb-6">
<v-col cols="12">
<v-subheader class="font-weight-medium white--text mb-2">
Description
</v-subheader>
<v-text-field
v-model="internalName"
dense
hide-details
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Experiment -->
<v-row dense class="mb-6">
<v-col cols="12">
<v-subheader class="font-weight-medium white--text mb-2">
Experiment
</v-subheader>
<v-text-field
v-model="internalName"
dense
hide-details
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
</v-card-text>
<!-- 액션 -->
<v-card-actions class="justify-end pa-4">
<v-btn color="success" @click="onSave">Start</v-btn>
<v-btn text class="white--text" @click="onClose">Close</v-btn>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref, watch } from "vue";
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal"]);
const visible = ref(true);
const form = ref({
name: "",
description: "",
});
const submit = () => {
emit("handle-data", form.value);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
Create Experiment
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submit">
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Experiment Name</label
>
<v-text-field
v-model="form.name"
variant="outlined"
dense
hide-details
required
/>
</div>
<div>
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Description</label
>
<v-textarea
v-model="form.description"
variant="outlined"
rows="3"
dense
hide-details
/>
</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from "vue";
// Props
const props = defineProps<{
modelValue: boolean;
selectedData: { workflow: string; stepName: string } | null;
workflowList: string[];
}>();
// v-model save Emit
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "save", payload: { workflow: string; stepName: string }): void;
}>();
//
const internalWorkflow = ref(props.selectedData?.workflow || "");
const internalStepName = ref(props.selectedData?.stepName || "");
//
watch(
() => props.modelValue,
(open) => {
if (open && props.selectedData) {
internalWorkflow.value = props.selectedData.workflow;
internalStepName.value = props.selectedData.stepName;
}
},
);
// Save
const onSave = () => {
emit("save", {
workflow: internalWorkflow.value,
stepName: internalStepName.value,
});
emit("update:modelValue", false);
};
// Close
const onClose = () => {
emit("update:modelValue", false);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>Edit Workflow Step Config</v-card-title
>
<!-- 본문 -->
<v-card-text class="pt-6 px-6 pb-4">
<!-- Select Workflow -->
<v-row class="mb-6" dense>
<v-col cols="12" sm="4">
<div class="font-weight-medium white--text">Select Workflow</div>
</v-col>
<v-col cols="12">
<v-select
v-model="internalWorkflow"
:items="workflowList"
dense
hide-details
placeholder="Select Workflow"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Workflow Step Name -->
<v-row class="mb-6" dense>
<v-col cols="12">
<div class="font-weight-medium white--text mb-2">
Workflow Step Name
</div>
<v-text-field
v-model="internalStepName"
dense
hide-details
placeholder="Enter Workflow Step"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
</v-card-text>
<!-- 액션 버튼 -->
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="onSave">Save</v-btn>
<v-btn text class="white--text" @click="onClose">Close</v-btn>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,94 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal"]);
const visible = ref(true);
const fileInput = ref<HTMLInputElement | null>(null);
const form = ref({
name: "",
description: "",
file: "",
});
//
const dialogTitle = computed(() => {
if (props.mode === "create") return "Create Training Script";
if (props.mode === "edit") return "Edit Training Script";
return "Clone Execution";
});
const onChooseFile = () => {
fileInput.value?.click();
};
const submit = () => {
emit("handle-data", form.value);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
{{ dialogTitle }}
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submit">
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Training Script Title
</label>
<v-text-field
v-model="form.name"
variant="outlined"
dense
hide-details
required
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>File
</label>
<v-file-input
v-model="form.file"
label="Upload File"
@click:append-outer="onChooseFile"
outlined
dense
hide-details
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Description
</label>
<v-text-field
v-model="form.description"
variant="outlined"
dense
hide-details
required
/>
</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from "vue";
// Props
const props = defineProps<{
modelValue: boolean;
selectedData: { workflow: string; stepName: string } | null;
workflowList: string[];
}>();
// v-model save Emit
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "save", payload: { workflow: string; stepName: string }): void;
}>();
//
const internalWorkflow = ref(props.selectedData?.workflow || "");
const internalStepName = ref(props.selectedData?.stepName || "");
//
watch(
() => props.modelValue,
(open) => {
if (open && props.selectedData) {
internalWorkflow.value = props.selectedData.workflow;
internalStepName.value = props.selectedData.stepName;
}
},
);
// Save
const onSave = () => {
emit("save", {
workflow: internalWorkflow.value,
stepName: internalStepName.value,
});
emit("update:modelValue", false);
};
// Close
const onClose = () => {
emit("update:modelValue", false);
};
</script>
<template>
<v-card flat>
<!-- 타이틀 -->
<v-toolbar flat color="primary" dark>
<v-toolbar-title>Edit Workflow</v-toolbar-title>
<v-spacer />
<v-btn icon @click="onClose">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<!-- 본문 -->
<v-card-text class="pt-6 px-6 pb-4">
<!-- Select Workflow -->
<v-row class="mb-6" dense>
<v-col cols="12" sm="4">
<div class="font-weight-medium white--text">Select Workflow</div>
</v-col>
<v-col cols="12">
<v-select
v-model="internalWorkflow"
:items="workflowList"
dense
hide-details
placeholder="Select Workflow"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
<!-- Workflow Step Name -->
<v-row class="mb-6" dense>
<v-col cols="12">
<div class="font-weight-medium white--text mb-2">
Workflow Step Name
</div>
<v-text-field
v-model="internalStepName"
dense
hide-details
placeholder="Enter Workflow Step"
style="background: #1e1e1e; color: #fff"
/>
</v-col>
</v-row>
</v-card-text>
<!-- 액션 버튼 -->
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="onSave">Save</v-btn>
<v-btn text class="white--text" @click="onClose">Close</v-btn>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,207 @@
<script setup lang="ts">
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
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 { ref, watch } from "vue";
import { AutoflowService } from "@/components/service/management/AutoflowService";
import { storage } from "@/utils/storage";
import { Workflow } from "@/components/models/management/Autoflow";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const saving = ref(false);
const errorMsg = ref("");
const { projectId } = storeToRefs(useAutoflowStore());
const steps = ref([
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
{
order: 2,
stepName: "Preprocessing",
type: "Preprocess",
status: "Not Configured",
},
{
order: 3,
stepName: "Train Model",
type: "Train",
status: "Not Configured",
},
]);
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal", "saved"]);
const visible = ref(true);
const form = ref({
name: "",
description: "",
});
const nowLocalIso = (): string => {
const t = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
return t.toISOString().slice(0, 23); // , 'Z'
};
const now = nowLocalIso();
const submit = async () => {
errorMsg.value = "";
if (!form.value.name.trim()) {
errorMsg.value = "Workflow Name은 필수입니다.";
return;
}
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
// 2) username (userinfo / userInfo )
const regUserId =
authObj?.userInfo?.username ??
authObj?.userinfo?.username ??
authObj?.username ??
authObj?.userId ??
"";
if (!regUserId) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
const payload: Workflow = {
workflowName: form.value.name.trim(),
workflowDescription: form.value.description?.trim() || "",
uploadYn: "Y",
regUserId,
regDt: now,
modDt: now,
projectId: projectId.value,
};
try {
saving.value = true;
const { data } = await AutoflowService.add(payload);
emit("saved", data);
emit("close-modal");
} catch (e) {
console.error("워크플로우 저장 실패:", e);
errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요.";
} finally {
saving.value = false;
}
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
Create Workflow
</v-card-title>
<v-card-text class="pa-6">
<div class="text-subtitle-1 font-weight-medium mb-4">
Workflow Information
</div>
<v-form @submit.prevent="submit">
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Workflow Name</label
>
<v-text-field
v-model="form.name"
variant="outlined"
dense
hide-details
required
/>
</div>
<div>
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Workflow Description</label
>
<v-textarea
v-model="form.description"
variant="outlined"
rows="3"
dense
hide-details
/>
</div>
</v-form>
</v-card-text>
<v-card-text class="pt-6 pb-4 px-6">
<div class="text-subtitle-1 font-weight-medium mb-4">Workflow Steps</div>
<v-row class="align-center mb-4">
<v-col cols="auto">
<v-btn color="primary" small>
<v-icon left>mdi-plus</v-icon>
Add Step
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn color="success" small>Save</v-btn>
</v-col>
<v-col cols="auto">
<v-btn color="grey" small>Cancel</v-btn>
</v-col>
<v-spacer />
</v-row>
<v-simple-table dense>
<thead>
<tr class="grey lighten-2">
<th class="text-center" style="width: 5%">Order</th>
<th class="text-center">Step Name</th>
<th class="text-center">Component Type</th>
<th class="text-center">Status</th>
<th class="text-center" style="width: 20%">Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="(step, i) in steps"
:key="step.order"
style="border: 1px solid #ccc"
>
<td class="text-center">{{ step.order }}</td>
<td class="text-center">{{ step.stepName }}</td>
<td class="d-flex justify-center align-center">
<v-select
v-model="step.type"
:items="['DataPrep', 'Preprocess', 'Train']"
dense
hide-details
style="max-width: 180px"
/>
</td>
<td class="text-center">{{ step.status }}</td>
<td class="text-center">
<IconArrowUp />
<IconArrowDown />
<IconModifyBtn />
<IconDeleteBtn />
</td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -0,0 +1,89 @@
<script setup lang="ts">
import { ref, watch } from "vue";
const props = defineProps({
editData: Object,
mode: String,
userOption: Array,
});
const emit = defineEmits(["handle-data", "close-modal"]);
const visible = ref(true);
const fileInput = ref<HTMLInputElement | null>(null);
const form = ref({
name: "",
description: "",
file: "",
});
const onChooseFile = () => {
fileInput.value?.click();
};
const submit = () => {
emit("handle-data", form.value);
};
</script>
<template>
<v-card class="rounded-lg overflow-hidden">
<!-- 타이틀 영역 -->
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
Workflow Information
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submit">
<div class="text-subtitle-1 font-weight-medium mb-4">
Workflow Information
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Workflow Name
</label>
<v-text-field
v-model="form.name"
variant="outlined"
dense
hide-details
required
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Workflow Description
</label>
<v-text-field
v-model="form.description"
variant="outlined"
dense
hide-details
required
/>
</div>
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Upload File
</label>
<v-file-input
v-model="form.file"
label="Upload File"
@click:append-outer="onChooseFile"
outlined
dense
hide-details
/>
</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" @click="submit">Save</v-btn>
<v-btn text class="white--text" @click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router";
import { useRouter } from "vue-router";
import { menuUtils } from "@/utils/menuUtils";
@ -11,6 +11,35 @@ const router = useRouter();
const isShowAuth = ref(false);
function readRolesFromStorage(): string[] {
try {
// storage.get(...) ,
const raw =
storage.get?.("autoflow-auth") ??
localStorage.getItem("autoflow-auth") ??
null;
const auth = typeof raw === "string" ? JSON.parse(raw) : raw;
let roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
// "ROLE_USER,ROLE_ADMIN"
if (typeof roles === "string") {
roles = roles.split(",").map((s: string) => s.trim());
}
//
if (!Array.isArray(roles)) return [];
return roles;
} catch {
return [];
}
}
// ADMIN (ROLE_ADMIN ADMIN )
const isAdmin = computed(() => {
const roles = readRolesFromStorage();
return roles.some((r) => r === "ROLE_ADMIN" || r === "ADMIN");
});
const isLinkActive = (link) => {
return route.path.includes(link);
};
@ -21,8 +50,7 @@ const goMain = () => {
onMounted(() => {
isShowAuth.value = true;
//storage.getAuth().auth === "ADMIN";
//storage.getAuth().auth === "ADMIN";
});
</script>
@ -35,9 +63,10 @@ onMounted(() => {
@click="goMain"
>
<div class="d-flex flex-column align-center pt-6">
<v-img :src="logo" width="auto" height="36" class="mb-3"/>
<div class="text-subtitle-2 font-weight-medium text-primary">Autoflow Web Console</div>
<v-img :src="logo" width="auto" height="36" class="mb-3" />
<div class="text-subtitle-2 font-weight-medium text-primary">
Autoflow Web Console
</div>
</div>
</v-card>
<v-list nav class="pa-5 pt-0">
@ -72,6 +101,7 @@ onMounted(() => {
</template>
<template v-else>
<v-list-item
v-if="value !== 'project' || isAdmin"
rounded
:title="title"
:value="value"
@ -137,6 +167,4 @@ onMounted(() => {
</v-card>
</template>
<style scoped lang="sass">
</style>
<style scoped lang="sass"></style>

@ -1,17 +1,39 @@
<script setup>
import {useRoute, useRouter} from "vue-router";
import {storage} from "@/utils/storage.js";
import { useRoute, useRouter } from "vue-router";
import { storage } from "@/utils/storage.js";
import DrawerComponent from "@/components/common/DrawerComponent.vue";
import {watchEffect} from "vue";
import { ref, watchEffect } from "vue";
import Select from "@/views/Select.vue";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const route = useRoute();
const router = useRouter();
const username = ref("");
const autoflow = useAutoflowStore();
const showPasswordModal = ref(false);
const selectedUserData = ref({});
const menu = ref([]);
const projectName = ref(localStorage.getItem("projectName") || "");
const updateUsername = () => {
// storage : { userInfo: { username, ... }, ... }
const auth = storage.getAuth?.() ?? null;
username.value =
auth?.userInfo?.username ??
auth?.username ?? //
""; //
};
const menuItems = [
{
title: "Select Project",
click: () => {
goSelect();
},
},
{
title: "Change Password",
@ -53,34 +75,75 @@ const pagePath = computed(() => {
return route.path;
});
const logOut = () => {
storage.clearAuth();
router.push("/signin");
const refreshProjectName = () => {
const v = localStorage.getItem("projectName");
projectName.value = v ? v : "";
};
const goHome = () => {
router.push("/main");
const goSelect = () => {
router.push("/select");
};
const showPasswordModal = ref(false);
const selectedUserData = ref({});
const logOut = () => {
UserManagerService.signOut()
.catch(console.error)
.finally(() => {
localStorage.removeItem("projectName");
localStorage.removeItem("projectId");
username.value = "";
projectName.value = "";
router.push("/login");
});
};
onMounted(() => {
updateUsername();
refreshProjectName();
menu.value = menuItems;
// projectName
window.addEventListener("storage", (e) => {
if (!e.key || e.key === "projectName") refreshProjectName();
if (!e.key || e.key === "autoflow-auth" || e.key === "auth")
updateUsername();
});
});
onMounted(() => {
updateUsername();
// /
window.addEventListener("storage", (e) => {
if (!e.key || e.key === "auth") updateUsername();
});
});
onBeforeUnmount(() => {
window.removeEventListener("storage", updateUsername);
});
watch(
() => route.fullPath,
() => refreshProjectName(),
);
watchEffect(() => {
// const auth = storage.getAuth().auth;
// if (auth === "ADMIN") {
menu.value = menuItems;
menu.value = menuItems;
// } else {
// menu.value = userMenuItems;
// }
});
</script>
<template>
<v-app>
<v-navigation-drawer v-model="drawer" border="0" hide-overlay permanent>
<DrawerComponent />
</v-navigation-drawer>
<v-navigation-drawer
v-model="drawer"
border="0"
hide-overlay
permanent
v-if="!route.meta.hideSidebar"
>
<DrawerComponent />
</v-navigation-drawer>
<v-app-bar class="bg-shades-transparent" flat>
<v-spacer></v-spacer>
@ -88,23 +151,18 @@ watchEffect(() => {
<template v-slot:activator="{ props }">
<v-tooltip location="bottom" text="settings">
<template #activator="{ props }">
<v-btn
icon
color="primary"
class="mr-3"
v-bind="props"
>
<v-btn icon color="primary" class="mr-3" v-bind="props">
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip location="bottom" text="home">
<v-tooltip location="bottom" text="Projact">
<template #activator="{ props }">
<v-btn
icon
color="primary"
class="mr-3"
@click="goHome"
@click="goSelect"
v-bind="props"
>
<v-icon>mdi-home</v-icon>
@ -112,8 +170,10 @@ watchEffect(() => {
</template>
</v-tooltip>
<div style="min-width: 180px" class="d-flex flex-column align-end">
<div class="font-weight-black">ADMIN_001</div>
<div class="text-subtitle-2">No Project Selected</div>
<div class="font-weight-black">{{ username || "GUEST" }}</div>
<div class="text-subtitle-2">
{{ projectName || "No Project Selected" }}
</div>
</div>
<v-btn icon color="primary" v-bind="props" class="mr-3">
<v-icon>mdi-arrow-down-drop-circle-outline</v-icon>
@ -142,10 +202,7 @@ watchEffect(() => {
<slot></slot>
</v-container>
</v-main>
</v-app>
</template>
<style scoped lang="sass">
</style>
<style scoped lang="sass"></style>

@ -0,0 +1,312 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import Plotly from "plotly.js-dist-min";
const pieChartRef = ref<HTMLElement | null>(null);
onMounted(() => {
if (pieChartRef.value) {
Plotly.newPlot(
pieChartRef.value,
[
{
values: [40, 25, 20, 15],
labels: ["Success", "Pending", "Failed", "Cancelled"],
type: "pie",
marker: {
colors: ["#4caf50", "#ff9800", "#f44336", "#9e9e9e"],
},
textinfo: "label+percent",
textfont: { color: "#fff", size: 14 },
hole: 0.4,
},
],
{
paper_bgcolor: "#1e1e1e",
plot_bgcolor: "#1e1e1e",
showlegend: true,
legend: {
font: { color: "#ffffff", size: 12 },
orientation: "h",
x: 0.5,
xanchor: "center",
y: -0.2,
},
margin: { t: 20, b: 40, l: 0, r: 0 },
},
{ displayModeBar: false },
);
}
});
const recentRuns = [
{ name: "Model A - v1", status: "success", time: "2025-05-12 09:12" },
{ name: "Model B - tuning", status: "success", time: "2025-05-14 08:59" },
{ name: "Model C - test run", status: "failed", time: "2025-05-13 18:13" },
];
const datasetUpdates = [
{ name: "DrivingLog2025", count: 7 },
{ name: "CameraFrames", count: 3 },
{ name: "LidarScans", count: 2 },
{ name: "Traffic_log", count: 2 },
{ name: "Traffic_log2", count: 2 },
];
const dummyWorkflow = [
{ title: "Volcano Test Pipeline", date: "2025-05-13 08:12" },
{ title: "Volcano Test Pipeline", date: "2025-05-13 08:12" },
{ title: "XGBoost Training", date: "2025-05-13 08:12" },
{ title: "Data Preprocess Flow", date: "2025-05-13 08:12" },
];
const tableHeader = [
{ label: "Model Name", width: "10%", style: "word-break: keep-all;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Deployed At", width: "10%", style: "word-break: keep-all;" },
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Download", width: "10%", style: "word-break: keep-all;" },
];
const data = ref({
results: [
{
deviceKey: "1",
name: "LaneDetectionModel",
version: "v1.2.0",
time: "2025-05-13 14:32",
status: "Active",
download: "Finished",
},
{
deviceKey: "2",
name: "TrafficSignClassifier",
version: "v0.9.3",
time: "2025-05-13 09:00",
status: "Pending",
download: "-",
},
{
deviceKey: "3",
name: "PathPlannerModel",
version: "v2.0.1",
time: "2025-05-12 17:44",
status: "Failed",
download: "Failed",
},
],
allSelected: false,
selected: [],
});
const handleRefresh = () => {
alert("Refresh 작업 진행중...");
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map(({ deviceKey }) => ({ deviceKey }))
: [];
};
</script>
<template>
<v-container fluid>
<div class="d-flex justify-space-between align-center mb-6">
<h2 class="text-h6 font-weight-bold">배터리 상태 예측 모델 프로젝트</h2>
<v-btn color="primary" prepend-icon="mdi-refresh" @click="handleRefresh"
>Refresh</v-btn
>
</div>
<v-row>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Workflow Success Rate
</h3>
</div>
<div style="overflow-y: auto; padding: 8px 16px">
<div ref="pieChartRef" style="height: 280px"></div>
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Recently Registered Workflow
</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list density="comfortable" nav>
<v-list-item v-for="(item, idx) in dummyWorkflow" :key="idx">
<template #title>
<div class="d-flex justify-space-between align-center w-100">
<span class="text-body-2 font-weight-medium">{{
item.title
}}</span>
<span class="text-caption text-grey-lighten-1">{{
item.date
}}</span>
</div>
</template>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">Recent Run</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list density="comfortable">
<v-list-item
v-for="(run, idx) in recentRuns"
:key="idx"
class="py-2"
>
<div class="d-flex align-center">
<v-avatar
size="28"
:color="
run.status === 'success'
? 'green lighten-1'
: 'red lighten-1'
"
>
<v-icon size="20" color="white">
{{ run.status === "success" ? "mdi-check" : "mdi-close" }}
</v-icon>
</v-avatar>
<div
class="d-flex flex-column text-right ml-4"
style="flex: 1"
>
<span class="font-weight-medium text-body-2">
{{ run.name }}
</span>
<span class="text-caption text-grey-darken-1">
{{ run.time }}
</span>
</div>
</div>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Dataset Update Activity
</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list dense>
<v-list-item v-for="(data, idx) in datasetUpdates" :key="idx">
<v-list-item-title>{{ data.name }}</v-list-item-title>
<v-progress-linear
:model-value="data.count * 10"
height="8"
color="primary"
class="mt-1"
/>
<v-list-item-subtitle
>{{ data.count }} updates</v-list-item-subtitle
>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
</v-row>
<v-card class="rounded-lg pa-4 mt-4">
<div class="d-flex justify-space-between align-center mt-8 mb-2 px-2">
<div class="d-flex align-center">
<span class="text-subtitle-1 font-weight-bold">Model Deployment</span>
</div>
<v-btn
variant="text"
class="text-caption font-weight-bold"
append-icon="mdi-arrow-right"
style="text-transform: none"
>
Go to Model Deploy
</v-btn>
</div>
<v-col cols="12">
<v-sheet>
<v-table density="comfortable" fixed-header height="625">
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="item.style"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{ deviceKey: item.deviceKey }"
/>
</td>
<td>{{ item.name }}</td>
<td>{{ item.version }}</td>
<td>{{ item.time }}</td>
<td>{{ item.status }}</td>
<td>{{ item.download }}</td>
</tr>
</tbody>
</v-table>
</v-sheet>
</v-col>
</v-card>
</v-container>
</template>
<style scoped>
ul {
list-style: none;
padding-left: 0;
margin-top: 8px;
}
li {
margin-bottom: 8px;
}
</style>

@ -0,0 +1,9 @@
export interface Workflow {
workflowName: string;
workflowDescription?: string;
uploadYn: "Y" | "N";
regUserId: string;
regDt: string;
modDt: string;
projectId: number;
}

@ -0,0 +1,27 @@
export interface UserSearch {
searchType: string;
searchText: string;
pageSize: number;
pageNum: number;
}
export interface User {
userId: string;
userPw: string;
userNm: string;
userAuth: string;
delYn: string;
regDate: string;
regUserId: string;
regUserNm: string;
modDate: string;
modUserId: string;
modUserNm: string;
logingDate: string;
token: string;
}
export interface UserProjectMap {
userId: string;
prjCd: string;
}

@ -0,0 +1,43 @@
export interface ApiProject {
id: number | null;
prjCd: string;
prjNm: string;
prjDesc: string;
prjStartDt: string;
prjEndDt: string;
delYn: string;
regDate: string;
regUserId: string;
regUserNm: string;
modDate: string;
modUserId: string;
modUserNm: string;
}
export interface UiProject {
id: number;
title: string;
creator: string;
date: string;
description: string;
}
export type Permission = "CREATE" | "READ" | "UPDATE" | "DELETE";
export interface ProjectAuthority {
projectId: number;
userId: number | string;
permissions: Permission[];
}
export interface ProjectSearchParams {
page: number;
size: number;
keyword: string;
searchType: string;
startDate: string;
endDate: string;
sortField: string;
sortDirection: string;
projectId: number;
}

@ -0,0 +1,4 @@
export interface Token {
accessToken: string;
refreshToken: string;
}

@ -0,0 +1,130 @@
import axios from "axios";
import { commonStore, loadingStore } from "@/stores/commonStore";
import { storage } from "@/utils/storage";
import router from "@/router";
const loading = loadingStore();
const API_URL = import.meta.env.VITE_APP_API_SERVER_URL;
console.log("API URL:", API_URL);
export const request = {
post: (uri: string, param: any): any => {
return axios.post(`${API_URL}${uri}`, param);
},
get: (uri: string, param: any): any => {
return axios.get(`${API_URL}${uri}`, { params: param });
},
delete: (uri: string, param: any): any => {
return axios.delete(`${API_URL}${uri}`, param);
},
put: (uri: string, param: any): any => {
return axios.put(`${API_URL}${uri}`, param);
},
postFile: (uri: string, param: any, attachment: any, progress: any): any => {
const formData = new FormData();
for (const key in attachment) {
if (Object.prototype.hasOwnProperty.call(attachment, key)) {
const value = attachment[key];
if (Array.isArray(value)) {
for (const file of value) {
formData.append(key, file);
}
} else {
formData.append(key, value);
}
}
}
formData.append("param", JSON.stringify(param));
return axios.post(`${API_URL}${uri}`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: progress,
});
},
postResponseFile: (
uri: string,
param: any,
responseParam: any,
attachment: any,
progress: any,
): any => {
const formData = new FormData();
for (const key in attachment) {
if (Object.prototype.hasOwnProperty.call(attachment, key)) {
const value = attachment[key];
if (Array.isArray(value)) {
for (const file of value) {
formData.append(key, file);
}
}
}
}
formData.append("param", JSON.stringify(param));
formData.append("responseParam", JSON.stringify(responseParam));
return axios.post(`${API_URL}${uri}`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: progress,
});
},
postNoParam: (uri: string): any => {
return axios.post(`${API_URL}${uri}`, null);
},
};
axios.defaults.headers.common["Access-Control-Allow-Origin"] = "*";
axios.interceptors.request.use(
(config) => {
loading.setLoading(true);
const token = storage.getToken();
const refresh = storage.getRefreshToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
if (token) (config.headers as any)["cuuva-jwt"] = token;
if (refresh) (config.headers as any)["cuuva-jwt-refresh"] = refresh;
return config;
},
(error) => {
loading.setLoading(false);
console.log("request error", error);
const store = commonStore();
store.setSnackbarMsg({
text: "에러가 발생하였습니다.",
color: "red",
result: 500,
});
return Promise.reject(error);
},
);
axios.interceptors.response.use(
(response) => {
loading.setLoading(false);
return response;
},
(error) => {
loading.setLoading(false);
const store = commonStore();
store.setSnackbarMsg({
text: "에러가 발생하였습니다.",
color: "red",
result: 500,
});
return Promise.reject(error);
},
);

@ -0,0 +1,15 @@
import { Workflow } from "@/components/models/management/Autoflow";
import { request } from "@/components/service/index";
export const AutoflowService = {
add: (payload: Workflow) => {
return request.post("/api/workflows", payload);
},
getAll: () => request.get("/api/workflows", {}),
delete: (id: Number) => {
return request.delete(`/api/workflows/${id}`, {});
},
view: (id: Number) => {
return request.get(`/api/workflows/${id}`, {});
},
};

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

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

@ -0,0 +1,7 @@
import { request } from "@/components/service/index";
import { Token } from "@/components/models/token/token";
export const TokenService = {
refreshToken: (): Promise<{ data: Token }> =>
request.postNoParam("/api/auth/refreshtoken"),
};

@ -0,0 +1,519 @@
<script setup lang="ts">
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 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();
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 searchOptions = [
{
searchType: "전체",
searchText: "",
},
{
searchType: "디바이스 별칭",
searchText: "deviceAlias",
},
{
searchType: "디바이스 키",
searchText: "deviceKey",
},
{
searchType: "사용자",
searchText: "userId",
},
{
searchType: "디바이스 이름",
searchText: "deviceName",
},
{
searchType: "디바이스 모델",
searchText: "deviceModel",
},
{
searchType: "디바이스 OS",
searchText: "deviceOs",
},
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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) => {
data.value.selectedData = selectedItem;
data.value.modalMode = "setting";
openView.value = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
};
const openModifyModal = () => {
data.value.selectedData = null;
data.value.modalMode = "edit";
data.value.isUploadVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
data.value.isCreateVisible = null;
};
const closeModifyModal = () => {
data.value.isModalVisible = false;
data.value.isUploadVisible = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
<!-- <FormComponent-->
<!-- :edit-data="data.selectedData"-->
<!-- :mode="data.modalMode"-->
<!-- @close-modal="closeModal"-->
<!-- @handle-data="saveData"-->
<!-- :user-option="data.userOption"-->
<!-- />-->
<!-- </v-dialog>-->
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
<!-- <ConfirmDialogComponent-->
<!-- @cancel="data.isConfirmDialogVisible = false"-->
<!-- @delete="removeData(undefined)"-->
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
<!-- />-->
<!-- </v-dialog>-->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Datasets</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create Dataset
</v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>{{ item.title }}</td>
<td>{{ item.fileName }}</td>
<td>{{ item.filePath }}</td>
<td>{{ item.description }}</td>
<td>{{ item.createdData }}</td>
<td>{{ item.modifiedData }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openSettingModal(item)" />
<IconModifyBtn @on-click="openModifyModal()" />
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<v-dialog v-model="data.isCreateVisible" max-width="600" persistent>
<DatasetsBaseDoalog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="600" persistent>
<DatasetsBaseDoalog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeModifyModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent @close="closeDetail" />
</div>
</template>
<style scoped></style>

@ -0,0 +1,257 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref } from "vue";
// const store = commonStore();
const experimentInfo = ref({
datasetTitle: "자율주행차량 배터리 상태 예측 모델 구축",
projectName: "배터리 상태 예측 모델 프로젝트",
version: "2.0",
createdDate: "2025-02-06",
createdId: "ADMIN_001",
modifiedDate: "2025-04-30",
modifiedId: "USER_002",
description: "날씨, 조도, 도로 상태 등의 주행환경 데이터",
fileName: "environment_log.csv",
fileSize: "58KB",
});
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const setPaginationLength = () => {
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
data.value.pageLength =
data.value.totalDataLength / data.value.params.pageSize;
} else {
data.value.pageLength = Math.ceil(
data.value.totalDataLength / data.value.params.pageSize,
);
}
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
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,
// });
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getCodeList();
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-text class="px-6 pb-6 pt-4">
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Dataset Title
</v-col>
<v-col cols="3" class="pa-2">{{
experimentInfo.datasetTitle
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Version </v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.version }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Project Name
</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.projectName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Created Date
</v-col>
<v-col cols="3" class="pa-2">{{
experimentInfo.createdDate
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Created ID </v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Modified Date
</v-col>
<v-col cols="3" class="pa-2">{{
experimentInfo.modifiedDate
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"
>Modified ID
</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.modifiedId }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.description
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">File</v-col>
<v-col cols="9" class="pa-2">{{ experimentInfo.fileName }}</v-col>
</v-row>
</v-card-text>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>

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

@ -0,0 +1,551 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/templates/deployment/ViewComponent.vue";
import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue";
// const store = commonStore();
const openView = ref(false);
const isEditVisible = ref(false);
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Model Name", width: "20%", style: "word-break: keep-all;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Duration", width: "10%", style: "word-break: keep-all;" },
{ label: "Deploy Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Download Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Deployed At", width: "15%", style: "word-break: keep-all;" },
{ label: "Action", width: "10%", style: "word-break: keep-all;" },
];
const searchOptions = [
{ searchType: "전체", searchText: "" },
{ searchType: "디바이스 별칭", searchText: "deviceAlias" },
{ searchType: "디바이스 키", searchText: "deviceKey" },
{ searchType: "사용자", searchText: "userId" },
{ searchType: "디바이스 이름", searchText: "deviceName" },
{ searchType: "디바이스 모델", searchText: "deviceModel" },
{ searchType: "디바이스 OS", searchText: "deviceOs" },
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isEditVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
data.value.results = [
{
no: 23,
modelName: "ImageClassifier",
version: 2,
duration: "-",
deployStatus: "failure",
downloadStatus: "failure",
deployedAt: "2025-04-28",
key: 23,
},
{
no: 22,
modelName: "ImageClassifier",
version: 1,
duration: "21s",
deployStatus: "failure",
downloadStatus: "failure",
deployedAt: "2025-03-21",
key: 22,
},
{
no: 21,
modelName: "LaneDetection",
version: 1,
duration: "12s",
deployStatus: "success",
downloadStatus: "failure",
deployedAt: "2025-03-19",
key: 21,
},
{
no: 20,
modelName: "CustomerChurn",
version: 1,
duration: "-",
deployStatus: "in_progress",
downloadStatus: "-",
deployedAt: "2025-02-08",
key: 20,
},
{
no: 19,
modelName: "AnomalyDetection",
version: 1,
duration: "20s",
deployStatus: "success",
downloadStatus: "success",
deployedAt: "2025-02-06",
key: 19,
},
{
no: 18,
modelName: "TimeSeriesForecast",
version: 2,
duration: "19s",
deployStatus: "success",
downloadStatus: "success",
deployedAt: "2025-01-28",
key: 18,
},
{
no: 17,
modelName: "TextSentiment",
version: 2,
duration: "20s",
deployStatus: "success",
downloadStatus: "success",
deployedAt: "2025-01-28",
key: 17,
},
];
data.value.totalDataLength = data.value.results.length;
const total = data.value.totalDataLength;
const size = data.value.params.pageSize;
data.value.pageLength =
total % size === 0 ? total / size : Math.ceil(total / size);
};
const setPaginationLength = () => {
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
data.value.pageLength =
data.value.totalDataLength / data.value.params.pageSize;
} else {
data.value.pageLength = Math.ceil(
data.value.totalDataLength / data.value.params.pageSize,
);
}
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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 handleRefresh = () => {
alert("Refresh 작업 진행중...");
};
const openInfoModal = () => {
data.value.modalMode = "info";
openView.value = true;
};
const closeDetail = () => {
openView.value = false;
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const openSettingModal = (selectedItem) => {
data.value.selectedData = selectedItem;
data.value.modalMode = "setting";
openView.value = true;
};
const openModifyModal = (selectedItem) => {
data.value.selectedData = selectedItem;
data.value.modalMode = "modify";
data.value.isModalVisible = true;
};
const openDeploymentModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
isEditVisible.value = true;
};
const closeModal = () => {
data.value.isModalVisible = false;
data.value.selectedData = null;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
isEditVisible.value = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
<!-- <FormComponent-->
<!-- :edit-data="data.selectedData"-->
<!-- :mode="data.modalMode"-->
<!-- @close-modal="closeModal"-->
<!-- @handle-data="saveData"-->
<!-- :user-option="data.userOption"-->
<!-- />-->
<!-- </v-dialog>-->
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
<!-- <ConfirmDialogComponent-->
<!-- @cancel="data.isConfirmDialogVisible = false"-->
<!-- @delete="removeData(undefined)"-->
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
<!-- />-->
<!-- </v-dialog>-->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Deployment</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" class="mr-4" @click="handleRefresh"
>Refresh
</v-btn>
<v-btn color="info" @click="openDeploymentModal"
>Deployment</v-btn
>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="item in data.results"
:key="item.no"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
:value="{ deviceKey: item.deviceKey }"
hide-details
style="min-width: 36px"
/>
</td>
<td>{{ item.no }}</td>
<td class="text-start">{{ item.modelName }}</td>
<td>{{ item.version }}</td>
<td>{{ item.duration }}</td>
<td>
<v-icon
v-if="item.deployStatus === 'success'"
color="success"
>
mdi-checkbox-marked-circle
</v-icon>
<v-icon
v-else-if="item.deployStatus === 'in_progress'"
spin
>
mdi-loading
</v-icon>
<v-icon v-else color="error"> mdi-close-circle </v-icon>
</td>
<td>
<v-icon
v-if="item.downloadStatus === 'success'"
color="success"
>
mdi-checkbox-marked-circle
</v-icon>
<v-icon
v-else-if="item.downloadStatus === 'in_progress'"
spin
>
mdi-loading
</v-icon>
<v-icon
v-else-if="item.downloadStatus === 'failure'"
color="error"
>
mdi-close-circle
</v-icon>
<span v-else>-</span>
</td>
<td>{{ item.deployedAt }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openInfoModal(item)" />
<IconDeployment @click="redeploy(item)" />
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
<v-dialog v-model="isEditVisible" max-width="800" persistent>
<DeploymentDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
</v-container>
</div>
<div class="w-100" v-else>
<ViewComponent @close="closeDetail" />
</div>
</template>
<style scoped></style>

@ -0,0 +1,397 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
// const store = commonStore();
const experimentInfo = ref({
modelName: "ImageClassifier",
projectName: "배터리 상태 예측 모델 프로젝트",
experimentName: "Baseline Model Training",
executionName: "run-batch32-lr0.001",
deployDate: "2025-02-06",
createdId: "ADMIN_001",
description: "기본 모델 구조로 학습 성능 측정",
});
const otaInfo = ref({
packageName: "자율주행 타차량 예측",
os: "Linux",
packageFileName: "4_EdgeInfra_Perception.sh",
packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN",
softwareName: "4_EdgeInfra_Perception.sh",
softwareVersion: "v2.0",
execute: "Not Executed",
});
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "run-batch32-lr0.001",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/2",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-10T00:00:00Z",
},
{
name: "run-batch64-lr0.001",
status: "Failed",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-09T00:00:00Z",
},
{
name: "run-batch32-lr0.0005",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-01T00:00:00Z",
},
{
name: "run-batch64-lr0.0005",
status: "Running",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-29T00:00:00Z",
},
{
name: "run-augmented-data",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-31T00:00:00Z",
},
];
data.value.totalDataLength = 5;
setPaginationLength();
// 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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Deploy Model Information </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Model Name </v-col>
<v-col cols="9" class="pa-2">{{ experimentInfo.modelName }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Project Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Project Name</v-col
>
<v-col cols="9" class="pa-2">{{
experimentInfo.projectName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Experiment Name
</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.experimentName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Created Date / ID -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Execution Name</v-col
>
<v-col cols="9" class="pa-2">{{
experimentInfo.executionName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Deploy Date
</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.deployDate }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Created ID</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.description
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Deploy Status
</v-col>
<v-col cols="3" class="pa-2">-</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"
>Download Status
</v-col>
<v-col cols="3" class="pa-2">-</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Deploy OTA Information</span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Package Name</v-col
>
<v-col cols="9">{{ otaInfo.packageName }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">OS</v-col>
<v-col cols="9">{{ otaInfo.os }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Package File Name</v-col
>
<v-col cols="9">{{ otaInfo.packageFileName }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Package File Path</v-col
>
<v-col cols="9">{{ otaInfo.packageFilePath }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Software Name</v-col
>
<v-col cols="3">{{ otaInfo.softwareName }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"
>Software Version</v-col
>
<v-col cols="3">{{ otaInfo.softwareVersion }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Execute</v-col>
<v-col cols="9">{{ otaInfo.execute }}</v-col>
</v-row>
</v-card-text>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>

@ -0,0 +1,526 @@
<script setup lang="ts">
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
import "monaco-editor/min/vs/editor/editor.main.css";
// const store = commonStore();
const activeTab = ref<"details" | "Visualizations">("details");
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "run-batch32-lr0.001",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/2",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-10T00:00:00Z",
},
{
name: "run-batch64-lr0.001",
status: "Failed",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-09T00:00:00Z",
},
{
name: "run-batch32-lr0.0005",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-01T00:00:00Z",
},
{
name: "run-batch64-lr0.0005",
status: "Running",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-29T00:00:00Z",
},
{
name: "run-augmented-data",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-31T00:00:00Z",
},
];
data.value.totalDataLength = 5;
// DeviceService.search(params).then((d) => {
// if (d.status === 200) {
// data.value.results = d.data.deviceList;
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// } else {
// store.setSnackbarMsg({
// text: " ",
// color: "error",
// });
// }
// });
// DeviceService.search().then((d) => {
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// });
};
const activePlot = ref<"parallel" | "scatter" | "box" | "contour">("parallel");
//
const parameters = ["alpha", "l1_ratio", "max_depth", "n_estimators"];
const metrics = ["mae", "rmse", "r2", "accuracy"];
//
const selectedParams = ref<string[]>([]);
const selectedX = ref("");
const selectedY = ref("");
const selectedZ = ref("");
const reverseColor = ref(false);
function clearAll() {
selectedParams.value = [];
selectedX.value = "";
selectedY.value = "";
selectedZ.value = "";
reverseColor.value = false;
}
const runA = {
id: "77578d20d81542f387149c1e3474f633",
name: "defiant-horse-63",
start: "2025-02-13 15:12",
end: "2025-02-13 15:14",
duration: "2.0s",
params: { alpha: 0.2, l1_ratio: 0.2 },
metrics: { mae: 0.564, r2: 0.237, rmse: 0.734 },
tags: {
estimator_class: "sklearn.pipeline.Pipeline",
estimator_name: "Pipeline",
},
artifacts: ["estimator.html", "metrics.json"],
};
const runB = {
id: "9a5da7476f0f4ad28117e0b2e8d58515",
name: "magnificent-eel-8",
start: "2025-02-13 15:12",
end: "2025-02-13 15:13",
duration: "1.9s",
params: { alpha: 0.1, l1_ratio: 0.1 },
metrics: { mae: 0.546, r2: 0.28, rmse: 0.713 },
tags: {
estimator_class: "sklearn.pipeline.Pipeline",
estimator_name: "Pipeline",
},
artifacts: ["estimator.html", "metrics.json"],
};
// const setPaginationLength = () => {
// if (data.value.totalDataLength % data.value.params.pageSize === 0) {
// data.value.pageLength =
// data.value.totalDataLength / data.value.params.pageSize;
// } else {
// data.value.pageLength = Math.ceil(
// data.value.totalDataLength / data.value.params.pageSize,
// );
// }
// };
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<!-- 1) Workflow Information -->
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Compare Executions</div>
</div>
</v-card-item>
</v-card>
<v-tabs
v-model="activeTab"
background-color="grey lighten-4"
style="max-width: 360px"
grow
>
<v-tab value="details">Details</v-tab>
<v-tab value="Visualizations">Visualizations</v-tab>
</v-tabs>
<div v-if="activeTab === 'details'" fluid flat>
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Run Detail </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Workflow Name / Version -->
<v-row align="center" class="py-2">
<v-col cols="2" class="text-h6 font-weight-bold"> Run ID: </v-col>
<v-col cols="5">{{ runA.id }}</v-col>
<v-col cols="5">{{ runB.id }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
Run Name:
</v-col>
<v-col cols="5">{{ runA.name }}</v-col>
<v-col cols="5">{{ runB.name }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
Start Name:
</v-col>
<v-col cols="5">{{ runA.start }}</v-col>
<v-col cols="5">{{ runB.start }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
End Name:
</v-col>
<v-col cols="5">{{ runA.end }}</v-col>
<v-col cols="5">{{ runB.end }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
Duration:
</v-col>
<v-col cols="5">{{ runA.duration }}</v-col>
<v-col cols="5">{{ runB.duration }}</v-col>
</v-row>
<v-divider class="my-2" />
</v-card-text>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Parameters </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Workflow Name / Version -->
<v-row align="center" class="py-2">
<v-col cols="2" class="text-h6 font-weight-bold"> alpha </v-col>
<v-col cols="5">{{ runA.params.alpha }}</v-col>
<v-col cols="5">{{ runB.params.alpha }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
l1_ratio
</v-col>
<v-col cols="5">{{ runA.params.l1_ratio }}</v-col>
<v-col cols="5">{{ runB.params.l1_ratio }}</v-col>
</v-row>
<v-divider class="my-2" />
</v-card-text>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Metrics</span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Workflow Name / Version -->
<v-row align="center" class="py-2">
<v-col cols="2" class="text-h6 font-weight-bold"> mae </v-col>
<v-col cols="5">{{ runA.metrics.mae }}</v-col>
<v-col cols="5">{{ runA.metrics.mae }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold"> r2 </v-col>
<v-col cols="5">{{ runA.metrics.r2 }}</v-col>
<v-col cols="5">{{ runA.metrics.r2 }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold"> rmse </v-col>
<v-col cols="5">{{ runA.metrics.rmse }}</v-col>
<v-col cols="5">{{ runA.metrics.rmse }}</v-col>
</v-row>
<v-divider class="my-2" />
</v-card-text>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Tags </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Workflow Name / Version -->
<v-row align="center" class="py-2">
<v-col cols="2" class="text-h6 font-weight-bold">
estimator_class
</v-col>
<v-col cols="5">{{ runA.tags.estimator_class }}</v-col>
<v-col cols="5">{{ runA.tags.estimator_class }}</v-col>
<v-col cols="2" class="text-h6 font-weight-bold">
estimator_name
</v-col>
<v-col cols="5">{{ runA.tags.estimator_name }}</v-col>
<v-col cols="5">{{ runA.tags.estimator_name }}</v-col>
</v-row>
<v-divider class="my-2" />
</v-card-text>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Artifacts</span>
</v-card-title>
<v-sheet class="d-flex justify-end mt-4">
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
</v-sheet>
</v-card>
</div>
<div v-show="activeTab === 'Visualizations'">
<!-- 2 : 플롯 종류 -->
<v-tabs
v-model="activePlot"
density="compact"
background-color="transparent"
class="sub-tabs mt-4"
grow
show-arrows
slider-color="secondary"
>
<v-tab value="parallel">Parallel Coordinates Plot</v-tab>
<v-tab value="scatter">Scatter Plot</v-tab>
<v-tab value="box">Box Plot</v-tab>
<v-tab value="contour">Contour Plot</v-tab>
</v-tabs>
<!-- 컨텐츠 카드 -->
<v-card flat class="px-6" elevation="2">
<v-row>
<!-- 좌측 컨트롤 -->
<v-col cols="12" md="4" class="pr-4 pt-8">
<v-sheet class="pa-4" elevation="1" style="background: #1e1e1e">
<div v-if="activePlot === 'parallel'">
<v-select
v-model="selectedParams"
:items="parameters"
label="Parameters"
multiple
dense
hide-details
class="mb-4"
/>
<v-select
v-model="selectedY"
:items="metrics"
label="Metrics"
dense
hide-details
class="mb-4"
/>
<v-btn text @click="clearAll">Clear All</v-btn>
</div>
<div v-else-if="activePlot === 'scatter'">
<v-select
v-model="selectedX"
:items="parameters"
label="X-axis"
dense
hide-details
class="mb-4"
/>
<v-select
v-model="selectedY"
:items="metrics"
label="Y-axis"
dense
hide-details
class="mb-4"
/>
<v-btn text @click="clearAll">Clear All</v-btn>
</div>
<div v-else-if="activePlot === 'box'">
<v-select
v-model="selectedX"
:items="parameters"
label="X-axis"
dense
hide-details
class="mb-4"
/>
<v-select
v-model="selectedY"
:items="metrics"
label="Y-axis"
dense
hide-details
class="mb-4"
/>
<v-btn text @click="clearAll">Clear All</v-btn>
</div>
<div v-else-if="activePlot === 'contour'">
<v-select
v-model="selectedX"
:items="metrics"
label="X-axis"
dense
hide-details
class="mb-4"
/>
<v-select
v-model="selectedY"
:items="metrics"
label="Y-axis"
dense
hide-details
class="mb-4"
/>
<v-select
v-model="selectedZ"
:items="[
'training_score',
'validation_score',
'mae',
'rmse',
]"
label="Z-axis"
dense
hide-details
class="mb-4"
/>
<v-switch
v-model="reverseColor"
label="Reverse color"
dense
hide-details
/>
<v-btn text @click="clearAll" class="mt-4">Clear All</v-btn>
</div>
</v-sheet>
</v-col>
<!-- 우측 그래프 placeholder -->
<v-col cols="12" md="8">
<v-sheet class="placeholder-box" height="400px" elevation="1" />
</v-col>
</v-row>
<v-sheet class="d-flex justify-end my-4">
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
</v-sheet>
</v-card>
</div>
</v-card>
</v-container>
</template>
<style scoped>
.editor-container {
width: 100%;
height: 900px;
max-height: 900px;
border: 1px solid #444;
border-radius: 4px;
}
</style>

@ -0,0 +1,607 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import { onMounted, ref, watch } from "vue";
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
import CompareComponent from "@/components/templates/run/executions/CompareComponent.vue";
import ViewComponent from "@/components/templates/run/executions/ViewComponent.vue";
import ExecutionBaseDialog from "@/components/atoms/organisms/ExecutionBaseDialog.vue";
// const store = commonStore();
const openCompare = ref(false);
const openView = ref(false);
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Execution Name", width: "20%", style: "word-break: keep-all;" },
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Duration", width: "10%", style: "word-break: keep-all;" },
{ label: "Experiment", width: "15%", style: "word-break: keep-all;" },
{ label: "Workflow", width: "15%", style: "word-break: keep-all;" },
{ label: "Start Time", width: "15%", style: "word-break: keep-all;" },
{ label: "Registry Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Action", width: "5%", style: "word-break: keep-all;" },
];
const searchOptions = [
{ searchType: "All", searchText: "" },
{ searchType: "Execution Name", searchText: "name" },
{ searchType: "Status", searchText: "status" },
{ searchType: "Duration", searchText: "duration" },
{ searchType: "Experiment", searchText: "experiment" },
{ searchType: "Workflow", searchText: "workflow" },
{ searchType: "Registry Status", searchText: "registryStatus" },
];
const execDialogOpen = ref(false);
const execMode = ref<"create" | "edit" | "clone">("create");
const execSelected = ref<any>(null);
const searchExperimentOptions = [{ searchType: "Experiment", searchText: "" }];
const searchWorkflowOptions = [{ searchType: "Workflow", searchText: "" }];
const workflowList = ref(["pipeline-a", "pipeline-b", "pipeline-c"]);
const executionTypes = ref(["One-off", "Recurring"]);
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
no: 11,
name: "run-batch32-lr0.001",
status: "Succeeded",
duration: "0:00:21",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-20 10:12",
registryStatus: "Registered",
},
{
no: 10,
name: "run-batch64-lr0.001",
status: "Failed",
duration: "0:00:20",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-20 09:10",
registryStatus: "Not Registered",
},
{
no: 9,
name: "run-batch32-lr0.0005",
status: "Succeeded",
duration: "0:00:21",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-19 10:12",
registryStatus: "Registered",
},
{
no: 8,
name: "run-batch64-lr0.0005",
status: "Running",
duration: "0:00:20",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-18 11:50",
registryStatus: "Not Registered",
},
{
no: 7,
name: "run-augmented-data",
status: "Succeeded",
duration: "0:00:20",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-17 09:12",
registryStatus: "Registered",
},
{
no: 6,
name: "run-augmented-data",
status: "Succeeded",
duration: "0:00:20",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-17 09:12",
registryStatus: "Registered",
},
{
no: 5,
name: "run-augmented-data",
status: "Succeeded",
duration: "0:00:20",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-17 09:12",
registryStatus: "Registered",
},
];
data.value.totalDataLength = data.value.results.length;
setPaginationLength();
// 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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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 handleTerminate = () => {
alert("Terminate 작업 진행중...");
};
const handleRetry = () => {
alert("Retry 작업 진행중...");
};
const handleClone = () => {
alert("Clone 작업 진행중...");
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const openCreateExecution = () => {
execMode.value = "create";
execSelected.value = null;
execDialogOpen.value = true;
};
const openComparePage = () => {
openCompare.value = true;
openView.value = false;
};
const openInfoModal = () => {
openView.value = true;
openCompare.value = false;
};
const openModifyModal = (selectedItem) => {
execMode.value = "edit";
execDialogOpen.value = true;
};
const openDownloadModal = () => {
data.value.selectedData = null;
data.value.modalMode = "download";
};
function closeCompare() {
openCompare.value = false;
}
function closeView() {
openView.value = false;
}
const closeModal = () => {
data.value.isModalVisible = false;
data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openCompare && !openView">
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>
<FormComponent
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">
<ConfirmDialogComponent
@cancel="data.isConfirmDialogVisible = false"
@delete="removeData(undefined)"
@init="((data.selected = []), (data.allSelected = false))"
/>
</v-dialog> -->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Executions</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchExperimentOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchWorkflowOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2 mr-3" @click="handleTerminate">
<v-btn color="primary">Terminate </v-btn>
</v-sheet>
<v-sheet class="justify-end mb-2 mr-3" @click="handleRetry">
<v-btn color="primary">Retry </v-btn>
</v-sheet>
<v-sheet class="justify-end mb-2 mr-3" @click="handleClone">
<v-btn color="primary">Clone </v-btn>
</v-sheet>
<v-sheet class="justify-end mb-2 mr-3" @click="openComparePage">
<v-btn color="primary">Compare </v-btn>
</v-sheet>
<v-sheet class="justify-end mb-2" @click="openCreateExecution">
<v-btn color="primary">Execution </v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="item in data.results"
:key="item.no"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
:value="{ deviceKey: item.deviceKey }"
hide-details
style="min-width: 36px"
/>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<td>
<v-icon v-if="item.status === 'Succeeded'" color="green"
>mdi-check-circle</v-icon
>
<v-icon v-else-if="item.status === 'Failed'" color="red"
>mdi-close-circle</v-icon
>
<v-icon v-else color="grey">mdi-loading</v-icon>
</td>
<td>{{ item.duration }}</td>
<td>{{ item.experiment }}</td>
<td>{{ item.workflow }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.registryStatus }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openInfoModal(item)" />
<IconModifyBtn @on-click="openModifyModal(item)" />
<IconDownloadBtn @on-click="openDownloadModal(item)" />
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
<v-dialog v-model="execDialogOpen" max-width="800" persistent>
<ExecutionBaseDialog
:model-value="execDialogOpen"
:mode="execMode"
:selectedData="execSelected"
:workflowList="workflowList"
:executionTypes="executionTypes"
@update:modelValue="execDialogOpen = $event"
/>
</v-dialog>
</v-container>
</div>
<div class="w-100" v-else-if="openCompare">
<CompareComponent @close="closeCompare" />
</div>
<div class="w-100" v-else-if="openView">
<ViewComponent @close="closeView" />
</div>
</template>
<style scoped></style>

@ -0,0 +1,339 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
// const store = commonStore();
const experimentInfo = ref({
executionsName: "run-batch32-lr0.001",
status: "Succeeded",
duration: "0:00:21",
experiment: "Baseline Model Training",
workflow: "baseline_train_pipeline",
startTime: "2025-05-20 10:12",
registryStatus: "Registered",
});
const otaInfo = ref({
packageName: "자율주행 타차량 예측",
os: "Linux",
packageFileName: "4_EdgeInfra_Perception.sh",
packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN",
softwareName: "4_EdgeInfra_Perception.sh",
softwareVersion: "v2.0",
execute: "Not Executed",
});
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "run-batch32-lr0.001",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/2",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-10T00:00:00Z",
},
{
name: "run-batch64-lr0.001",
status: "Failed",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-09T00:00:00Z",
},
{
name: "run-batch32-lr0.0005",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-01T00:00:00Z",
},
{
name: "run-batch64-lr0.0005",
status: "Running",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-29T00:00:00Z",
},
{
name: "run-augmented-data",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-31T00:00:00Z",
},
];
data.value.totalDataLength = 5;
setPaginationLength();
// 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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Deploy Model Information </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Executions Name
</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.executionsName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Project Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Status</v-col>
<v-col cols="9" class="pa-2">
<v-icon v-if="experimentInfo.status === 'Succeeded'" color="green"
>mdi-check-circle</v-icon
>
<v-icon v-else-if="experimentInfo.status === 'Failed'" color="red"
>mdi-close-circle</v-icon
>
<v-icon v-else color="grey">mdi-loading</v-icon></v-col
>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Duration </v-col>
<v-col cols="9" class="pa-2">{{ experimentInfo.duration }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Created Date / ID -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Experiment</v-col>
<v-col cols="9" class="pa-2">{{ experimentInfo.experiment }}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Workflow </v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.workflow }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Start Time</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.startTime }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Registry Status</v-col
>
<v-col cols="9" class="pa-2">{{
experimentInfo.registryStatus
}}</v-col>
</v-row>
<VDivider class="my-2" />
</v-card-text>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>

@ -0,0 +1,489 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/templates/run/experiment/ViewComponent.vue";
import ExperimentCreateDialog from "@/components/atoms/organisms/ExperimentCreateDialog.vue";
// const store = commonStore();
const detailDialog = ref(false);
const openView = ref(false);
const selectedExperiment = ref<{
name: string;
description: string;
createdDate: string;
createdID: string;
} | null>(null);
const tableHeader = [
{
label: "Experiment Name",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Description",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Created Date",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Created ID",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Action",
width: "20%",
style: "word-break: keep-all;",
},
];
const searchOptions = [
{ searchType: "All", searchText: "" },
{ searchType: "Experiment Name", searchText: "name" },
{ searchType: "Description", searchText: "description" },
{ searchType: "Created Date", searchText: "createdDate" },
{ searchType: "Created ID", searchText: "createdID" },
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "Baseline Model Training",
description: "기본 모델 구조로 학습 성능 측정",
createdDate: "2025-04-28",
createdID: "ADMIN_001",
},
{
name: "Batch Size Tuning",
description: "배치 사이즈 변경에 따른 학습 성능",
createdDate: "2025-04-20",
createdID: "ADMIN_001",
},
{
name: "Learning Rate Sweep",
description: "러닝레이트 변경에 따른 손실 ",
createdDate: "2025-04-20",
createdID: "ADMIN_001",
},
{
name: "Optimizer Comparison",
description: "Adam, SGD 등 옵티마이저 종류",
createdDate: "2025-04-20",
createdID: "ADMIN_001",
},
{
name: "Model Architecture A vs B",
description: "서로 다른 모델 구조",
createdDate: "2025-01-28",
createdID: "ADMIN_001",
},
];
data.value.totalDataLength = data.value.results.length;
setPaginationLength();
// 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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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 changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const openDetail = (item: {
name: string;
description: string;
createdDate: string;
createdID: string;
}) => {
selectedExperiment.value = item;
openView.value = true;
};
const closeDetail = () => {
openView.value = false;
selectedExperiment.value = null;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isModalVisible = true;
};
const closeModal = () => {
data.value.isModalVisible = false;
data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Experiment</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="primary" @click="openCreateModal"
>Create Experiment
</v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<!-- <th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th> -->
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<!-- <td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td> -->
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.createdDate }}</td>
<td>{{ item.createdID }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openDetail(item)" />
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<v-dialog v-model="data.isModalVisible" max-width="600" persistent>
<ExperimentCreateDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent :experiment="selectedExperiment" @close="closeDetail" />
</div>
</template>
<style scoped></style>

@ -0,0 +1,405 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
// const store = commonStore();
const tableHeader = [
{
label: "Run Name",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Status",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Duration",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Pipeline",
width: "20%",
style: "word-break: keep-all;",
},
{
label: "Start Time",
width: "20%",
style: "word-break: keep-all;",
},
];
const experimentInfo = ref({
experimentName: "Baseline Model Training",
projectName: "배터리 상태 예측 모델 프로젝트",
createdDate: "2025-02-06",
createdId: "ADMIN_001",
description: "기본 모델 구조로 학습 성능 측정",
});
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
name: "run-batch32-lr0.001",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/2",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-10T00:00:00Z",
},
{
name: "run-batch64-lr0.001",
status: "Failed",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-09T00:00:00Z",
},
{
name: "run-batch32-lr0.0005",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-06-01T00:00:00Z",
},
{
name: "run-batch64-lr0.0005",
status: "Running",
Duration: "0:00:21",
configProgress: "1/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-29T00:00:00Z",
},
{
name: "run-augmented-data",
status: "Succeeded",
Duration: "0:00:21",
configProgress: "0/3",
Pipeline: "baseline_train_pipeline",
registDt: "2025-05-31T00:00:00Z",
},
];
data.value.totalDataLength = 5;
setPaginationLength();
// 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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Experiment Information</span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Experiment Name</v-col
>
<v-col cols="9" class="pa-2">{{
experimentInfo.experimentName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Project Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Project Name</v-col
>
<v-col cols="9" class="pa-2">{{
experimentInfo.projectName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Created Date / ID -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Created Date</v-col
>
<v-col cols="3" class="pa-2">{{
experimentInfo.createdDate
}}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold">Created ID</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.description
}}</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card class="rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Runs</span>
</v-card-title>
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="300"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>{{ item.name }}</td>
<td>{{ item.status }}</td>
<td>{{ item.Duration }}</td>
<td>{{ item.Pipeline }}</td>
<td>{{ item.registDt }}</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>

@ -0,0 +1,536 @@
<script setup lang="ts">
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 ViewComponent from "@/components/templates/stepconfig/ViewComponent.vue";
import StapComfigDialog from "@/components/atoms/organisms/StapComfigDialog.vue";
// const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Step Name", width: "15%", style: "word-break: keep-all;" },
{ label: "Type", width: "10%", style: "word-break: keep-all;" },
{ label: "Dataset", width: "10%", style: "word-break: keep-all;" },
{ label: "Script", width: "10%", style: "word-break: keep-all;" },
{ label: "Hyper Parameters", width: "15%", style: "word-break: keep-all;" },
{ label: "Resource", width: "15%", style: "word-break: keep-all;" },
{ label: "Status", width: "5%", style: "word-break: keep-all;" },
{ label: "Workflow", width: "10%", style: "word-break: keep-all;" },
{ label: "Action", width: "5%", style: "word-break: keep-all;" },
];
const searchOptions = [
{ searchType: "전체", searchText: "" },
{ searchType: "디바이스 별칭", searchText: "deviceAlias" },
{ searchType: "디바이스 키", searchText: "deviceKey" },
{ searchType: "사용자", searchText: "userId" },
{ searchType: "디바이스 이름", searchText: "deviceName" },
{ searchType: "디바이스 모델", searchText: "deviceModel" },
{ searchType: "디바이스 OS", searchText: "deviceOs" },
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const workflowList = ["pipeline-a", "pipeline-b", "pipeline-c"];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
// : No 7 1
data.value.results = [
{
no: 7,
stepName: "Data Ingest",
type: "Preprocessing",
dataset: "raw_data",
script: "ingest.py",
hyperParameters: "-",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 7,
},
{
no: 6,
stepName: "Data Preprocess",
type: "Preprocessing",
dataset: "raw_data",
script: "preprocess.py",
hyperParameters: "normalize=True",
resource: "CPU:2, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 6,
},
{
no: 5,
stepName: "Model Training",
type: "Training",
dataset: "processed_data",
script: "train.py",
hyperParameters: "lr=0.01, epochs=10",
resource: "GPU:1, MEM:8Gi",
status: "warning",
workflow: "pipeline-a",
deviceKey: 5,
},
{
no: 4,
stepName: "Model Evaluation",
type: "Evaluation",
dataset: "test_data",
script: "evaluate.py",
hyperParameters: "-",
resource: "CPU:1, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 4,
},
{
no: 3,
stepName: "Model Validation",
type: "Validation",
dataset: "test_data",
script: "validate.py",
hyperParameters: "metrics=['accuracy']",
resource: "CPU:1, MEM:4Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 3,
},
{
no: 2,
stepName: "Package Model",
type: "Packaging",
dataset: "trained_model",
script: "package.py",
hyperParameters: "format='tar.gz'",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 2,
},
{
no: 1,
stepName: "Deploy",
type: "Deployment",
dataset: "package",
script: "deploy.py",
hyperParameters: "env='prod'",
resource: "CPU:1, MEM:2Gi",
status: "success",
workflow: "pipeline-a",
deviceKey: 1,
},
];
data.value.totalDataLength = data.value.results.length;
//
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 setPaginationLength = () => {
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
data.value.pageLength =
data.value.totalDataLength / data.value.params.pageSize;
} else {
data.value.pageLength = Math.ceil(
data.value.totalDataLength / data.value.params.pageSize,
);
}
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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 openDetailModal = (selectedItem) => {
data.value.selectedData = selectedItem;
openView.value = true;
};
const handleSave = ({
workflow,
stepName,
}: {
workflow: string;
stepName: string;
}) => {
if (data.value.selectedData) {
data.value.selectedData.workflow = workflow;
data.value.selectedData.stepName = stepName;
}
};
const openModifyModal = (item: { workflow: string; stepName: string }) => {
data.value.selectedData = {
workflow: item.workflow,
stepName: item.stepName,
};
openModify.value = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isModalVisible = true;
};
const closeModal = () => {
data.value.isModalVisible = false;
data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Workflows Step Config</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="item in data.results"
:key="item.no"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
:value="{ deviceKey: item.deviceKey }"
hide-details
style="min-width: 36px"
/>
</td>
<td>{{ item.no }}</td>
<td>{{ item.stepName }}</td>
<td>{{ item.type }}</td>
<td>{{ item.dataset }}</td>
<td>{{ item.script }}</td>
<td>{{ item.hyperParameters }}</td>
<td>{{ item.resource }}</td>
<td>
<v-icon
v-if="item.status === 'success'"
color="success"
>
mdi-checkbox-marked-circle
</v-icon>
<v-icon v-else color="warning">
mdi-alert-circle
</v-icon>
</td>
<td>{{ item.workflow }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openDetailModal(item)" />
<IconModifyBtn @on-click="openModifyModal(item)" />
<!-- <IconModifyBtn @on-click="openModify = true" /> -->
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
</div>
<div class="w-100" v-else>
<ViewComponent @close="closeDetail" />
</div>
<v-dialog v-model="openModify" max-width="600px">
<StapComfigDialog
v-model="openModify"
:selectedData="data.selectedData"
:workflowList="workflowList"
@save="handleSave"
/>
</v-dialog>
</template>
<style scoped></style>

@ -0,0 +1,135 @@
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<!-- 상단 타이틀 -->
<v-card flat class="bg-shades-transparent w-100 mb-6">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<!-- Workflow Step Information -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Workflow Step Information</span>
</v-card-title>
<v-card-text class="py-4">
<v-row align="center" class="mb-2">
<v-col cols="3" class="font-weight-bold">Workflow Step Name</v-col>
<v-col cols="3">Train Model</v-col>
<v-col cols="3" class="font-weight-bold">Workflow Name</v-col>
<v-col cols="3">sentiment-analysis</v-col>
</v-row>
<v-divider />
<v-row align="center" class="mt-2">
<v-col cols="3" class="font-weight-bold">Created Date</v-col>
<v-col cols="3">2025-02-06</v-col>
<v-col cols="3" class="font-weight-bold">Created ID</v-col>
<v-col cols="3">ADMIN_001</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Dataset -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Dataset</span>
</v-card-title>
<v-card-text class="py-4">
<v-row align="center">
<v-col cols="3" class="font-weight-bold">Dataset Name</v-col>
<v-col cols="3">야간 주행용 레이더 데이터셋 구성</v-col>
<v-col cols="3" class="font-weight-bold">Version</v-col>
<v-col cols="3">v2.0</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Training Script -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Training Script</span>
</v-card-title>
<v-card-text class="py-4">
<v-row align="center" class="mb-2">
<v-col cols="3" class="font-weight-bold">Script File</v-col>
<v-col cols="9">baseline_train.py</v-col>
</v-row>
<v-divider />
<v-row align="center" class="mt-2">
<v-col cols="3" class="font-weight-bold">Script File Path</v-col>
<v-col cols="9" style="word-break: break-all">
/mnt/nfs/model_code/baseline/baseline_train.py
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Hyperparameters -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Hyperparameters</span>
</v-card-title>
<v-card-text class="py-4">
<v-row align="center" class="mb-2">
<v-col cols="3" class="font-weight-bold">Batch Size</v-col>
<v-col cols="3">64</v-col>
<v-col cols="3" class="font-weight-bold">Learning Rate</v-col>
<v-col cols="3">0.001</v-col>
</v-row>
<v-divider />
<v-row align="center" class="mt-2">
<v-col cols="3" class="font-weight-bold">Optimizer</v-col>
<v-col cols="3">Adam</v-col>
<v-col cols="3" class="font-weight-bold">Epochs</v-col>
<v-col cols="3">20</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Resource & Scheduling -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Resource & Scheduling</span>
</v-card-title>
<v-card-text class="py-4">
<v-row align="center" class="mb-2">
<v-col cols="3" class="font-weight-bold">CPU Cores</v-col>
<v-col cols="3">4</v-col>
<v-col cols="3" class="font-weight-bold">Memory</v-col>
<v-col cols="3">16Gi</v-col>
</v-row>
<v-divider />
<v-row align="center" class="mt-2">
<v-col cols="3" class="font-weight-bold">GPU</v-col>
<v-col cols="9">1 (NVIDIA A100)</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Docker image -->
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Docker image</span>
</v-card-title>
<v-card-text class="py-4">
<v-sheet class="pa-2" elevation="2">
kubeflownotebookswg/jupyter-pytorch-cuda-full:v1.9.2
</v-sheet>
</v-card-text>
<v-sheet class="d-flex justify-end mt-4">
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
</v-sheet>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { defineEmits } from "vue";
const emit = defineEmits<{
(e: "close"): void;
}>();
</script>
<style scoped></style>

@ -0,0 +1,519 @@
<script setup lang="ts">
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 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();
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 searchOptions = [
{
searchType: "전체",
searchText: "",
},
{
searchType: "디바이스 별칭",
searchText: "deviceAlias",
},
{
searchType: "디바이스 키",
searchText: "deviceKey",
},
{
searchType: "사용자",
searchText: "userId",
},
{
searchType: "디바이스 이름",
searchText: "deviceName",
},
{
searchType: "디바이스 모델",
searchText: "deviceModel",
},
{
searchType: "디바이스 OS",
searchText: "deviceOs",
},
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [
{
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 saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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) => {
data.value.selectedData = selectedItem;
data.value.modalMode = "setting";
openView.value = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
};
const openModifyModal = () => {
data.value.selectedData = null;
data.value.modalMode = "edit";
data.value.isUploadVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
data.value.isCreateVisible = null;
};
const closeModifyModal = () => {
data.value.isModalVisible = false;
data.value.isUploadVisible = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
<!-- <FormComponent-->
<!-- :edit-data="data.selectedData"-->
<!-- :mode="data.modalMode"-->
<!-- @close-modal="closeModal"-->
<!-- @handle-data="saveData"-->
<!-- :user-option="data.userOption"-->
<!-- />-->
<!-- </v-dialog>-->
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
<!-- <ConfirmDialogComponent-->
<!-- @cancel="data.isConfirmDialogVisible = false"-->
<!-- @delete="removeData(undefined)"-->
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
<!-- />-->
<!-- </v-dialog>-->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Training Script</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create Script
</v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>{{ item.title }}</td>
<td>{{ item.fileName }}</td>
<td>{{ item.filePath }}</td>
<td>{{ item.description }}</td>
<td>{{ item.createdData }}</td>
<td>{{ item.modifiedData }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openSettingModal(item)" />
<IconModifyBtn @on-click="openModifyModal()" />
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<v-dialog v-model="data.isCreateVisible" max-width="600" persistent>
<TrainingScriptBaseDoalog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="600" persistent>
<TrainingScriptBaseDoalog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeModifyModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent @close="closeDetail" />
</div>
</template>
<style scoped></style>

@ -0,0 +1,291 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import * as monaco from "monaco-editor";
import "monaco-editor/min/vs/editor/editor.main.css";
// const store = commonStore();
const editorRef = ref<HTMLDivElement | null>(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
const experimentInfo = ref({
modelName: "ImageClassifier",
projectName: "배터리 상태 예측 모델 프로젝트",
experimentName: "Baseline Model Training",
executionName: "run-batch32-lr0.001",
deployDate: "2025-02-06",
createdId: "ADMIN_001",
description: "기본 모델 구조로 학습 성능 측정",
});
const yamlContent = `import argparse
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import os
class SimpleNet(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
`;
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const setPaginationLength = () => {
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
data.value.pageLength =
data.value.totalDataLength / data.value.params.pageSize;
} else {
data.value.pageLength = Math.ceil(
data.value.totalDataLength / data.value.params.pageSize,
);
}
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
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,
// });
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const changePageNum = (page) => {
data.value.params.pageNum = page;
};
const emit = defineEmits<{
(e: "close"): void;
}>();
onMounted(() => {
getCodeList();
if (editorRef.value) {
editorInstance = monaco.editor.create(editorRef.value, {
value: yamlContent,
language: "yaml",
theme: "vs-dark",
readOnly: true,
automaticLayout: true,
minimap: { enabled: false },
lineNumbers: "on",
});
}
});
onBeforeUnmount(() => {
if (editorInstance) {
editorInstance.dispose();
editorInstance = null;
}
});
</script>
<template>
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">View Details</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Training Script Information </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<!-- Experiment Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Training Script Title
</v-col>
<v-col cols="9" class="pa-2">{{ experimentInfo.modelName }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Project Name -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">File Name </v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.projectName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">File Path </v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.experimentName
}}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Created Date / ID -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold"
>Created Date
</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.deployDate }}</v-col>
<v-col cols="3" class="text-h6 font-weight-bold"
>Modified Date
</v-col>
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
</v-row>
<VDivider class="my-2" />
<!-- Description -->
<v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
<v-col cols="9" class="pa-2">{{
experimentInfo.description
}}</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Training Script Preview </span>
</v-card-title>
<v-card-text class="px-6 pb-6 pt-4">
<div ref="editorRef" class="editor-container"></div
></v-card-text>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
</v-sheet>
</v-card>
</v-card>
</v-container>
</template>
<style scoped>
.editor-container {
width: 100%;
height: 400px; /* 원하시는 높이로 설정하세요 */
}
.v-card-text {
width: 100% !important;
border-collapse: collapse;
/* 전체 테이블 1px 테두리 */
}
.v-card-text th {
font-size: 20px;
min-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.v-card-text td {
font-size: 16px;
min-width: 600px;
padding: 12px 16px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.v-card-text tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.02);
}
</style>

@ -0,0 +1,578 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsCreateDialog from "@/components/atoms/organisms/WorkflowsCreateDialog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
import { AutoflowService } from "@/components/service/management/AutoflowService";
import { commonStore } from "@/stores/commonStore";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
const tableHeader = [
{
label: "No",
width: "5%",
style: "word-break: keep-all;",
},
{
label: "Workflow Name",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Step Count",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Config Progress",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Kubeflow Status",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Created DateTime",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Action",
width: "7%",
style: "word-break: keep-all;",
},
];
const searchOptions = [
{
searchType: "전체",
searchText: "",
},
{
searchType: "디바이스 별칭",
searchText: "deviceAlias",
},
{
searchType: "디바이스 키",
searchText: "deviceKey",
},
{
searchType: "사용자",
searchText: "userId",
},
{
searchType: "디바이스 이름",
searchText: "deviceName",
},
{
searchType: "디바이스 모델",
searchText: "deviceModel",
},
{
searchType: "디바이스 OS",
searchText: "deviceOs",
},
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const toRow = (workflow: any, index: number, offset: number) => ({
no: offset + index + 1,
name: workflow.workflowName,
version: workflow.version,
stepCount: workflow.stepCount,
configProgress: workflow.configProgress,
kubeflowStatus: workflow.kubeflowStatus,
registDt: workflow.regDt,
deviceKey: workflow.id,
});
const getData = () => {
const params = data.value.params;
const pageNum = params.pageNum;
const pageSize = params.pageSize;
const startIndex = (pageNum - 1) * pageSize;
AutoflowService.getAll()
.then((res) => {
if (res.status !== 200) {
console.error("워크플로우 조회 실패:", res);
return;
}
const result = res.data;
console.log(result);
const rawList = Array.isArray(result) ? result : (result.content ?? []);
const totalCount = Array.isArray(result)
? rawList.length
: (result.totalElements ?? rawList.length);
const currentPageList = Array.isArray(result)
? rawList.slice(startIndex, startIndex + pageSize)
: rawList;
data.value.results = currentPageList.map((w: any, i: number) =>
toRow(w, i, startIndex),
);
data.value.totalDataLength = totalCount;
setPaginationLength();
})
.catch((err) => {
console.error("워크플로우 조회 에러:", err);
});
};
const setPaginationLength = () => {
const total = data.value.totalDataLength || 0;
const pageSize = data.value.params.pageSize || 10;
data.value.pageLength =
total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize);
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return;
const ids = removeList.map((x) => x.deviceKey);
const remove = (id) =>
AutoflowService.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;
}
getData();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
if (ids.length === 1) {
remove(ids[0])
.then(() => {
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
});
after();
})
.catch((err) => {
store.setSnackbarMsg({
color: "warning",
text: "삭제 실패",
result: 500,
});
console.error(err);
});
} else {
Promise.all(ids.map(remove))
.then(() => {
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
});
})
.catch((err) => {
store.setSnackbarMsg({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
console.error(err);
})
.finally(after);
}
};
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 openDetailModal = (selectedItem) => {
data.value.selectedData = selectedItem;
openView.value = true;
};
const openModifyModal = (item: { workflow: string; stepName: string }) => {
data.value.selectedData = {
workflow: item.workflow,
stepName: item.stepName,
};
openModify.value = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
};
const openUploadModal = () => {
data.value.selectedData = null;
data.value.modalMode = "upload";
data.value.isUploadVisible = true;
};
const closeCreateModal = () => {
data.value.isModalVisible = false;
data.value.isCreateVisible = false;
};
const closeUploadModal = () => {
data.value.isModalVisible = false;
data.value.isUploadVisible = false;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) getData();
},
);
watch(
() => data.value.isUploadVisible,
(now, prev) => {
if (prev && !now) getData();
},
);
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<div class="w-100" v-if="!openView">
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Workflows</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" class="mr-4" @click="openUploadModal"
>Upload Workflow
</v-btn>
<v-btn color="info" @click="openCreateModal"
>Create Workflow
</v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<td>{{ item.stepCount }}</td>
<td>{{ item.configProgress }}</td>
<td>{{ item.kubeflowStatus }}</td>
<td>{{ item.registDt }}</td>
<td style="white-space: nowrap">
<IconInfoBtn @on-click="openDetailModal(item)" />
<IconSettingBtn />
<IconModifyBtn @on-click="openModifyModal(item)" />
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="changePageNum"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
<WorkflowsCreateDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
<WorkflowsUploadDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeUploadModal"
@handle-data="saveData"
:user-option="data.userOption"
/>
</v-dialog>
<v-dialog v-model="openModify" max-width="600px">
<StapComfigDialog
v-model="openModify"
:selectedData="data.selectedData"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent
v-if="data.selectedData"
:id="data.selectedData.deviceKey"
@close="closeDetail"
/>
</div>
</template>
<style scoped></style>

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

@ -1,532 +0,0 @@
<script setup lang="ts">
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
import { onMounted, ref, watch } from "vue";
// const store = commonStore();
const tableHeader = [
{
label: "No",
width: "5%",
style: "word-break: keep-all;",
},
{
label: "Workflow Name",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Version",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Step Count",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Config Progress",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Kubeflow Status",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Created DateTime",
width: "7%",
style: "word-break: keep-all;",
},
{
label: "Action",
width: "7%",
style: "word-break: keep-all;",
},
];
const searchOptions = [
{
searchType: "전체",
searchText: "",
},
{
searchType: "디바이스 별칭",
searchText: "deviceAlias",
},
{
searchType: "디바이스 키",
searchText: "deviceKey",
},
{
searchType: "사용자",
searchText: "userId",
},
{
searchType: "디바이스 이름",
searchText: "deviceName",
},
{
searchType: "디바이스 모델",
searchText: "deviceModel",
},
{
searchType: "디바이스 OS",
searchText: "deviceOs",
},
{
searchType: "디바이스 버전",
searchText: "deviceVersion",
},
];
const pageSizeOptions = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "",
searchText: "",
},
results: [],
totalDataLength: 0,
pageLength: 0,
modalMode: "",
selectedData: null,
allSelected: false,
selected: [],
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [],
});
const getCodeList = () => {
// UserService.search(data.value.params).then((d) => {
// if (d.status === 200) {
// data.value.userOption = d.data.userList;
// }
// });
};
const getData = () => {
const params = { ...data.value.params };
if (params.searchType === "" || params.searchText === "") {
delete params.searchType;
delete params.searchText;
}
data.value.results = [{
"no":5,
"name": "sentiment-analysis",
"version": "v2.0",
"stepCount": 2,
"configProgress": "0/2",
"kubeflowStatus": "Not Uploaded",
"registDt": "2025-06-10T00:00:00Z",
},
{
"no":4,
"name": "image-classfier",
"version": "v2.0",
"stepCount": 3,
"configProgress": "1/3",
"kubeflowStatus": "Not Uploaded",
"registDt": "2025-06-09T00:00:00Z",
},
{
"no":3,
"name": "user-clustering",
"version": "v1.0",
"stepCount": 3,
"configProgress": "0/3",
"kubeflowStatus": "Not Uploaded",
"registDt": "2025-06-01T00:00:00Z",
},
{
"no":2,
"name": "time-series-train",
"version": "v1.0",
"stepCount": 2,
"configProgress": "1/3",
"kubeflowStatus": "Not Uploaded",
"registDt": "2025-05-29T00:00:00Z",
},
{
"no":1,
"name": "customer-churn-pred",
"version": "v1.0",
"stepCount": 3,
"configProgress": "0/3",
"kubeflowStatus": "Not Uploaded",
"registDt": "2025-05-31T00:00:00Z",
}];
data.value.totalDataLength =5;
// DeviceService.search(params).then((d) => {
// if (d.status === 200) {
// data.value.results = d.data.deviceList;
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// } else {
// store.setSnackbarMsg({
// text: " ",
// color: "error",
// });
// }
// });
// DeviceService.search().then((d) => {
// data.value.totalDataLength = d.data.totalCount;
// setTimeout(() => {
// setPaginationLength();
// }, 200);
// });
};
const setPaginationLength = () => {
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
data.value.pageLength =
data.value.totalDataLength / data.value.params.pageSize;
} else {
data.value.pageLength = Math.ceil(
data.value.totalDataLength / data.value.params.pageSize,
);
}
};
const saveData = (formData) => {
if (data.value.modalMode === "create") {
// DeviceService.add(formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum(1);
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
} else {
// DeviceService.update(formData.deviceKey, formData).then((d) => {
// if (d.status === 200) {
// data.value.isModalVisible = false;
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
// changePageNum();
// } else {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
}
};
const removeData = (value) => {
let removeList = value ? value : data.value.selected;
const remove = (code) => {
// return DeviceService.delete(code).then((d) => {
// if (d.status !== 200) {
// store.setSnackbarMsg({
// text: d,
// result: 500,
// });
// }
// });
};
if (removeList.length === 1) {
remove(removeList[0].deviceKey).then(() => {
// store.setSnackbarMsg({
// text: ".",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
});
} else {
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
() => {
// store.setSnackbarMsg({
// text: " .",
// result: 200,
// });
changePageNum();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
},
);
}
};
const 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 changePageNum = (page) => {
data.value.params.pageNum = page;
getData();
};
const openModifyModal = (selectedItem) => {
data.value.selectedData = selectedItem;
data.value.modalMode = "modify";
data.value.isModalVisible = true;
};
const openCreateModal = () => {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isModalVisible = true;
};
const closeModal = () => {
data.value.isModalVisible = false;
data.value.selectedData = null;
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => {
return {
deviceKey: item.deviceKey,
};
})
: [];
};
onMounted(() => {
getData();
getCodeList();
});
</script>
<template>
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
<!-- <FormComponent-->
<!-- :edit-data="data.selectedData"-->
<!-- :mode="data.modalMode"-->
<!-- @close-modal="closeModal"-->
<!-- @handle-data="saveData"-->
<!-- :user-option="data.userOption"-->
<!-- />-->
<!-- </v-dialog>-->
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
<!-- <ConfirmDialogComponent-->
<!-- @cancel="data.isConfirmDialogVisible = false"-->
<!-- @delete="removeData(undefined)"-->
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
<!-- />-->
<!-- </v-dialog>-->
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
<v-card
flat
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
>
<v-card flat class="bg-shades-transparent w-100">
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
<div class="d-flex flex-row justify-start align-center">
<div class="text-primary">Workflows</div>
</div>
</v-card-item>
</v-card>
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
item-title="searchType"
item-value="searchText"
hide-details
></v-select>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="changePageNum(1)"
></v-text-field>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="changePageNum(1)"
>
<v-icon> mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalDataLength.toLocaleString() }}
</v-chip>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageNum(1)"
></v-select>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="error" class="mr-4" @click="handleRemoveData"
>선택 삭제
</v-btn>
<v-btn color="success" @click="openCreateModal"></v-btn>
</v-sheet>
</v-sheet>
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<td>{{ item.version }}</td>
<td>{{ item.stepCount }}</td>
<td>{{ item.configProgress }}</td>
<td>{{ item.kubeflowStatus }}</td>
<td>{{ item.registDt }}</td>
<td style="white-space: nowrap">
<IconModifyBtn @on-click="openModifyModal(item)" />
<IconDeleteBtn
@on-click="removeData([{ deviceKey: item.deviceKey }])"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="getData"
></v-pagination>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
</template>
<style scoped></style>

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

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

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

@ -1,10 +0,0 @@
<script setup>
</script>
<template>
</template>
<style scoped lang="sass">
</style>

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

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

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

@ -5,13 +5,15 @@ import { storage } from "@/utils/storage";
import logo from "@/assets/wordmark.png";
import logo2 from "@/assets/workflow.png";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { TokenService } from "@/components/service/token/tokenService";
const API_URL = import.meta.env.VITE_APP_API_SERVER_URL;
const router = useRouter();
const data = ref({
form: false,
userId: "",
userPw: "",
username: "",
password: "",
loading: false,
snackbar: false,
snackbarText: "",
@ -24,25 +26,39 @@ const resetForm = () => {
data.value.loading = false;
};
const resetLogin = () => {
data.value.userId = "";
data.value.userPw = "";
data.value.loading = false;
};
const resetSignup = () => {
data.value.username = "";
data.value.email = "";
data.value.role = [];
data.value.password = "";
data.value.loading = false;
};
const signIn = () => {
// if (!data.value.form) return;
// data.value.loading = true;
// setTimeout(() => (data.value.loading = false), 2000);
const payload = {
username: data.value.username,
password: data.value.password,
};
// const params = {
// userId: data.value.userId,
// password: data.value.userPw,
// };
//
// UserManagerService.signIn(params).then((d) => {
// if (d.data.success === true) {
// storage.setAuth(d.data.data);
router.push("/workflows");
// } else {
// resetForm();
// data.value.snackbar = true;
// }
// });
UserManagerService.signIn(payload)
// .then((res) => TokenService.refreshToken().then(() => res))
.then((res) => {
storage.setAuth(res.data);
router.push("/select");
})
.catch((err) => {
if (err.response?.status === 401) {
data.value.snackbarText = "아이디 또는 비밀번호가 올바르지 않습니다.";
} else {
data.value.snackbarText = "서버 에러가 발생했습니다.";
}
data.value.snackbarColor = "red";
data.value.snackbar = true;
});
};
</script>
@ -54,7 +70,7 @@ const signIn = () => {
<v-card flat class="bg-transparent d-flex align-center flex-column">
<v-card
class="mx-auto pa-10 mb-4 login-box rounded-lg d-flex flex-column"
style="min-width: 450px !important;"
style="min-width: 450px !important"
density="comfortable"
>
<div class="mb-4 w-100 d-flex justify-center">
@ -64,7 +80,6 @@ const signIn = () => {
>
<v-icon class="text-primary">mdi-shield-key-outline</v-icon>
</div>
</div>
<div class="text-h5 pb-2 text-center font-weight-bold text-primary">
Autoflow Web Console
@ -72,7 +87,7 @@ const signIn = () => {
<v-form v-model="data.form" @submit.prevent="signIn" class="mt-3">
<v-text-field
v-model="data.userId"
v-model="data.username"
:readonly="data.loading"
class="mb-2"
variant="outlined"
@ -81,7 +96,7 @@ const signIn = () => {
></v-text-field>
<v-text-field
v-model="data.userPw"
v-model="data.password"
:readonly="data.loading"
type="password"
variant="outlined"
@ -102,7 +117,19 @@ const signIn = () => {
login
</v-btn>
</v-form>
<div class="mt-4 text-center">
<span>계정이 없으십니까?</span>
<router-link
to="/signup"
class="ml-2 font-weight-medium"
style="color: #90caf9; text-decoration: none"
>
SignUp
</router-link>
</div>
</v-card>
<!-- 회원가입 -->
</v-card>
<v-sheet
@ -110,7 +137,7 @@ const signIn = () => {
style="bottom: 0; left: 0"
>
<v-sheet class="bg-shades-transparent d-flex align-end"
>Copyright © 2025 Autoflow Web Console
>Copyright © 2025 Autoflow Web Console
</v-sheet>
</v-sheet>
<v-sheet
@ -144,13 +171,15 @@ const signIn = () => {
}
.background-image {
background-image: linear-gradient(
90deg,
rgba(19, 18, 18, 0.5),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.5)
),url("@/assets/4117551.jpg"); /* 배경 이미지 경로 */
background-image:
linear-gradient(
90deg,
rgba(19, 18, 18, 0.5),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.5)
),
url("@/assets/4117551.jpg"); /* 배경 이미지 경로 */
background-size: cover; /* 이미지가 화면을 꽉 채우도록 설정 */
background-position: center; /* 이미지 중앙 정렬 */
background-repeat: no-repeat; /* 이미지 반복 방지 */

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

@ -0,0 +1,210 @@
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import { storage } from "@/utils/storage";
import logo from "@/assets/wordmark.png";
import logo2 from "@/assets/workflow.png";
import { UserManagerService } from "@/components/service/management/userManagerService";
const router = useRouter();
const data = ref({
form: false,
username: "",
email: "",
role: [],
password: "",
loading: false,
snackbar: false,
snackbarText: "",
snackbarColor: "",
});
const resetForm = () => {
data.value.userId = "";
data.value.userPw = "";
data.value.loading = false;
};
const resetLogin = () => {
data.value.userId = "";
data.value.userPw = "";
data.value.loading = false;
};
const resetSignup = () => {
data.value.username = "";
data.value.email = "";
data.value.role = [];
data.value.password = "";
data.value.loading = false;
};
const signUp = () => {
const payload = {
username: data.value.username,
email: data.value.email,
role: data.value.role,
password: data.value.password,
};
console.log("회원가입 호출 payload:", payload);
UserManagerService.signUp(payload)
.then((res) => {
if (res.data.success) {
router.push("/login");
} else {
data.value.snackbarText =
res.data.message || "회원가입에 실패했습니다.";
data.value.snackbarColor = "red";
data.value.snackbar = true;
}
})
.catch((err) => {
console.error(err);
data.value.snackbarText =
"비밀번호는 6자 이상, 40자 이하로 입력해야 합니다.";
data.value.snackbarColor = "red";
data.value.snackbar = true;
});
};
</script>
<template>
<v-container :fluid="true" class="pa-0">
<v-sheet
class="background-image w-100 h-screen d-flex align-center justify-center flex-column"
>
<v-card flat class="bg-transparent d-flex align-center flex-column">
<v-card
class="mx-auto pa-10 mb-4 login-box rounded-lg d-flex flex-column"
style="min-width: 450px !important"
density="comfortable"
>
<div class="mb-4 w-100 d-flex justify-center">
<div
class="bg-transparent rounded-circle pa-2"
style="border: 1.8px solid #398bec"
>
<v-icon class="text-primary">mdi-shield-key-outline</v-icon>
</div>
</div>
<div class="text-h5 pb-2 text-center font-weight-bold text-primary">
Autoflow Web Console
</div>
<v-form v-model="data.form" @submit.prevent="signUp" class="mt-3">
<v-text-field
v-model="data.username"
:readonly="data.loading"
class="mb-2"
variant="outlined"
color="secondary"
placeholder="아이디를 입력해주세요."
></v-text-field>
<v-text-field
v-model="data.email"
:readonly="data.loading"
class="mb-2"
variant="outlined"
color="secondary"
placeholder="이메일을 입력해주세요."
></v-text-field>
<v-select
v-model="data.role"
:items="['ROLE_USER', 'ROLE_MODERATOR', 'ROLE_ADMIN']"
multiple
variant="outlined"
placeholder="Role"
class="mb-2"
/>
<v-text-field
v-model="data.password"
:readonly="data.loading"
type="password"
variant="outlined"
color="secondary"
class="mb-2"
placeholder="비밀번호를 입력해주세요."
></v-text-field>
<v-btn
:disabled="!data.form"
:loading="data.loading"
block
color="primary"
size="large"
type="submit"
variant="flat"
>
SignUp
</v-btn>
</v-form>
<div class="mt-4 text-center">
<span>계정이 있으십니까?</span>
<router-link
to="/login"
class="ml-2 font-weight-medium"
style="color: #90caf9; text-decoration: none"
>
Login
</router-link>
</div>
</v-card>
</v-card>
<v-sheet
class="position-absolute w-100 bg-transparent pa-4"
style="bottom: 0; left: 0"
>
<v-sheet class="bg-shades-transparent d-flex align-end"
>Copyright © 2025 Autoflow Web Console
</v-sheet>
</v-sheet>
<v-sheet
class="position-absolute w-100 bg-transparent pa-4"
style="bottom: 0; right: 0"
>
<v-sheet class="bg-shades-transparent d-flex justify-end">
<img :src="logo" style="width: 5%" />
</v-sheet>
</v-sheet>
</v-sheet>
</v-container>
<v-snackbar
v-model="data.snackbar"
timeout="2000"
location="button"
:color="data.snackbarColor"
>
{{ data.snackbarText }}
<template #actions>
<v-btn variant="text" @click="data.snackbar = false"> 닫기 </v-btn>
</template>
</v-snackbar>
</template>
<style scoped>
.login-box {
background-color: rgba(18, 18, 18, 0.4); /* 흰색 배경에 30% 불투명도 */
backdrop-filter: blur(10px); /* 배경 블러 효과 */
}
.background-image {
background-image:
linear-gradient(
90deg,
rgba(19, 18, 18, 0.5),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.3),
rgba(19, 18, 18, 0.5)
),
url("@/assets/4117551.jpg"); /* 배경 이미지 경로 */
background-size: cover; /* 이미지가 화면을 꽉 채우도록 설정 */
background-position: center; /* 이미지 중앙 정렬 */
background-repeat: no-repeat; /* 이미지 반복 방지 */
min-height: 100vh; /* 화면 전체 높이 설정 */
}
</style>

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

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

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

@ -1,14 +1,13 @@
import { createRouter, createWebHistory } from "vue-router";
import { storage } from "@/utils/storage";
const rootPath = import.meta.env.VITE_ROOT_PATH;
const routes = [
{
path: `/`,
component: () => import('@/layouts/default.vue'),
redirect: { name: "signin" },
component: () => import("@/layouts/default.vue"),
redirect: { name: "login" },
children: [
{
name: "main",
@ -17,7 +16,26 @@ const routes = [
title: "",
requiresAuth: false,
},
component: () => import('@/pages/MainView.vue'),
component: () => import("@/pages/MainView.vue"),
},
{
name: "select",
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",
@ -28,6 +46,7 @@ const routes = [
},
component: () => import("@/pages/HomeView.vue"),
},
{
name: "workflows",
path: `/workflows`,
@ -48,7 +67,7 @@ const routes = [
},
{
name: "run",
path: `/run`,
path: `/run/experiment`,
meta: {
title: "Run",
requiresAuth: false,
@ -65,16 +84,17 @@ const routes = [
component: () => import("@/pages/ExperimentView.vue"),
},
{
name: "Excutions",
path: `/run/excutions`,
name: "Executions",
path: `/run/executions`,
meta: {
title: "Excutions",
title: "Executions",
requiresAuth: false,
},
component: () => import("@/pages/ExcutionsView.vue"),
component: () => import("@/pages/ExecutionsView.vue"),
},
],
},
{
name: "deployment",
path: `/deployment`,
@ -105,13 +125,22 @@ const routes = [
],
},
{
name: "signin",
path: `/signin`,
name: "login",
path: `/login`,
meta: {
title: "로그인",
requiresAuth: false,
},
component: () => import("@/pages/LoginView.vue"),
},
{
name: "signup",
path: `/signup`,
meta: {
title: "로그인",
requiresAuth: false,
},
component: () => import('@/pages/LoginView.vue'),
component: () => import("@/pages/SignupView.vue"),
},
];

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

@ -0,0 +1,32 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { load } from "webfontloader";
export const commonStore = defineStore("common", () => {
const snackbarMsg = ref({ text: "", color: "", result: 200 });
const setSnackbarMsg = (data: {
result: number;
color: string;
text: string;
}) => {
snackbarMsg.value = data;
};
return {
snackbarMsg,
setSnackbarMsg,
};
});
export const loadingStore = defineStore("loading", () => {
const loading = ref(false);
const setLoading = (value: boolean) => {
loading.value = value;
};
return {
loading,
setLoading,
};
});

@ -0,0 +1,96 @@
export const CommonFunctions = {
stringTruncate: (str: string, len: number) => {
return str.length > len ? str.slice(0, len) + "..." : str;
},
getFileExtension: (filename: string) => {
const index = filename.lastIndexOf(".");
return index !== -1 ? filename.substring(index + 1).toLowerCase() : null;
},
getDateFormatYmd: (date: string) => {
if (date != null && date != "") {
const inputDate = new Date(date);
const year = inputDate.getFullYear();
const month = (inputDate.getMonth() + 1).toString().padStart(2, "0");
const day = inputDate.getDate().toString().padStart(2, "0");
const formattedDate = `${year}.${month}.${day}`;
return formattedDate;
} else {
return "";
}
},
getDateFormatHis: (date: string) => {
if (date != null && date != "") {
const timeMatch = date.match(/\b\d{2}:\d{2}:\d{2}\b/);
if (timeMatch) {
return timeMatch[0];
} else {
return "";
}
} else {
return "";
}
},
getQuantumDateFormat: (dateString: string) => {
if (dateString != null && dateString != "" && dateString != "-") {
const date = new Date(dateString);
// 날짜와 시간을 각각 포맷팅
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}.${month}.${day} ${hours}:${minutes}:${seconds}`;
} else {
return "-";
}
dateString;
},
getQuantumDateFormatToCustom: (dateString: string) => {
if (dateString != null && dateString != "" && dateString != "-") {
const date = new Date(dateString);
// toLocaleString을 사용해 형식 지정
return date.toLocaleString("en-US", {
month: "short", // Oct
day: "2-digit", // 07
year: "numeric", // 2024
hour: "2-digit", // 10
minute: "2-digit", // 37
hour12: true, // AM/PM 형식
});
} else {
return "-";
}
},
getTimeDifferenceInSeconds: (dateString1: string, dateString2: string) => {
const date1 = new Date(dateString1);
const date2 = new Date(dateString2);
// 두 날짜의 밀리초 차이 계산
const differenceInMs = Math.abs(date1.getTime() - date2.getTime());
// 밀리초 차이를 초 단위로 변환
const differenceInSeconds = Math.floor(differenceInMs / 1000);
return differenceInSeconds;
},
getJobChartData: (data: string[]) => {
const binaryMapping: Record<string, string> = {
"0x0": "00",
"0x1": "01",
"0x2": "10",
"0x3": "11",
};
const frequency: Record<string, number> = {};
data.forEach((d) => {
const binary = binaryMapping[d] || "00";
frequency[binary] = (frequency[binary] || 0) + 1;
});
return frequency;
},
};

@ -6,6 +6,12 @@ export const menuUtils = {
value: "home",
icon: "mdi-monitor-multiple",
},
{
title: "Project",
path: "/project",
value: "project",
icon: "mdi-folder-cog-outline",
},
{
title: "Workflows",
path: "/workflows",
@ -25,7 +31,7 @@ export const menuUtils = {
icon: "mdi-format-list-bulleted-square",
depth: [
{ title: "Experiment", path: "/run/experiment" },
{ title: "Excutions", path: "/run/excutions" },
{ title: "Executions", path: "/run/executions" },
],
},
{
@ -40,13 +46,13 @@ export const menuUtils = {
title: "Training Script",
path: "/training-script",
value: "training-script",
icon: "mdi-account",
icon: "mdi-file-code-outline",
},
{
title: "Datasets",
path: "/datasets",
value: "datasets",
icon: "mdi-account",
icon: "mdi-database-outline",
},
],
};

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

@ -0,0 +1,420 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { useAutoflowStore } from "@/stores/autoflowStore";
import type {
UiProject,
ApiProject,
Permission,
} from "@/components/models/project/Project";
import { ProjectService } from "@/components/service/project/projectService";
import { UserManagerService } from "@/components/service/management/userManagerService";
import { storage } from "@/utils/storage.js";
/** ===== 상수 & 기본 권한 ===== */
const DEFAULT_PERMISSIONS: Permission[] = [
"CREATE",
"READ",
"UPDATE",
"DELETE",
];
/** ===== 라우터 & 스토어 ===== */
const router = useRouter();
const autoflowStore = useAutoflowStore();
/** ===== 상태 ===== */
const dialog = ref(false);
const contextMenu = ref(false);
const menuX = ref(0);
const menuY = ref(0);
const selectedIndex = ref<number | null>(null);
const projects = ref<UiProject[]>([]);
type UserOption = { id: number | string; username: string };
const userOptions = ref<UserOption[]>([]);
const modalMode = ref<"create" | "edit">("create");
const editingProjectId = ref<number | null>(null);
const form = ref({
prjCd: "",
prjNm: "",
prjDesc: "",
selectedUsers: [] as string[],
});
/** ===== 서버 응답 타입 ===== */
const roles = ref<string[]>([]);
const refreshRoles = () => {
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
const r = auth?.userInfo?.roles ?? auth?.roles ?? [];
roles.value = Array.isArray(r) ? r : [];
};
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
/** ===== 서버 응답 타입 ===== */
interface ProjectSearchResponseItem {
id: number;
prjNm: string;
prjDesc: string;
prjStartDt?: string;
regUserId?: string; // "" ( username)
}
interface UserResponseItem {
id: number | string;
username: string;
}
/** ===== 유틸 ===== */
const buildApiProjectPayload = (): ApiProject => {
const today = new Date().toISOString().slice(0, 10);
const nowIso = new Date().toISOString();
const namesCsv = form.value.selectedUsers.join(",");
return {
id: modalMode.value === "edit" ? editingProjectId.value! : null,
prjCd: form.value.prjCd,
prjNm: form.value.prjNm,
prjDesc: form.value.prjDesc,
prjStartDt: today,
prjEndDt: today,
delYn: "N",
regDate: nowIso,
regUserId: namesCsv,
regUserNm: namesCsv,
modDate: nowIso,
modUserId: namesCsv,
modUserNm: namesCsv,
};
};
const resetForm = () => {
form.value.prjCd = `PRJ${Date.now()}`;
form.value.prjNm = "";
form.value.prjDesc = "";
form.value.selectedUsers = [];
};
const loadProjects = async () => {
try {
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,
}));
} catch (e) {
console.error("프로젝트 조회 실패:", e);
}
};
const loadUsers = async () => {
try {
const { data } = await UserManagerService.getAll();
const raw = data as UserResponseItem[];
userOptions.value = raw.map((u) => ({ id: u.id, username: u.username }));
} catch (e) {
console.error("사용자 조회 실패:", e);
}
};
const openContextMenu = (event: MouseEvent, index: number) => {
event.preventDefault();
selectedIndex.value = index;
menuX.value = event.pageX;
menuY.value = event.pageY;
contextMenu.value = true;
};
const closeDialog = () => {
dialog.value = false;
contextMenu.value = false;
selectedIndex.value = null;
};
const selectProject = (index: number) => {
const selected = projects.value[index];
autoflowStore.setProjectId(selected.id);
autoflowStore.setProjectName(selected.title);
router.push("/home");
};
/** ===== 프로젝트 저장 & 권한 부여 ===== */
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
permissions: DEFAULT_PERMISSIONS,
}),
),
);
};
const saveProject = async () => {
if (!isAdmin.value) {
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
return;
}
const payload = buildApiProjectPayload();
try {
let projectId: number;
if (modalMode.value === "create") {
const createRes = await ProjectService.add(payload);
projectId = createRes.data.id;
} else {
await ProjectService.update(editingProjectId.value!, payload);
projectId = editingProjectId.value!;
}
await grantDefaultPermissions(projectId, form.value.selectedUsers);
await loadProjects();
closeDialog();
} catch (error: any) {
console.error(`${modalMode.value} 실패:`, error?.response?.data || error);
alert(error?.response?.data?.message || "저장 실패");
}
};
const deleteProject = async () => {
try {
if (selectedIndex.value === null) return;
const target = projects.value[selectedIndex.value];
await ProjectService.delete(target.id);
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 deleteProject = async (): Promise<void> => {
// try {
// if (selectedIndex.value === null) return;
// const target = projects.value[selectedIndex.value];
// const projectId = target.id;
// // 1) username
// const usernames = (target.creator || "")
// .split(",")
// .map((s) => s.trim())
// .filter(Boolean);
// // 2) username -> userId
// if (usernames.length) {
// const ids = userOptions.value
// .filter((u) => usernames.includes(u.username))
// .map((u) => u.id);
// // 3) /
// await Promise.all(
// ids.map((uid) => ProjectService.deleteProjectAuthority(projectId, uid)),
// );
// }
// // 4)
// await ProjectService.delete(projectId);
// await loadProjects();
// } catch (e: any) {
// console.error(" :", e?.response?.status, e?.response?.data || e);
// alert(" : " + (e?.response?.data?.message || e.message || ""));
// } finally {
// contextMenu.value = false;
// }
// };
const onAddProject = () => {
if (!isAdmin.value) {
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
return;
}
modalMode.value = "create";
editingProjectId.value = null;
resetForm();
dialog.value = true;
};
const modifyProject = () => {
contextMenu.value = false;
if (selectedIndex.value === null) return;
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;
form.value.selectedUsers =
selected.creator
?.split(",")
.map((s) => s.trim())
.filter(Boolean) ?? [];
dialog.value = true;
};
/** ===== 라이프사이클 ===== */
const onStorage = (e: StorageEvent) => {
if (!e.key || /auth|vpp-Auth/i.test(e.key)) refreshRoles();
};
onMounted(async () => {
refreshRoles();
await Promise.all([loadProjects(), loadUsers()]);
window.addEventListener("storage", onStorage);
});
onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage);
});
</script>
<template>
<v-container class="mt-12" style="max-width: 1600px">
<v-row class="mb-6" align="center" justify="space-between">
<v-col cols="auto">
<h2 class="font-weight-bold text-h5">Project Selection</h2>
</v-col>
<v-col cols="auto">
<!-- ADMIN만 노출 -->
<v-btn
v-show="isAdmin"
color="secondary"
variant="flat"
class="text-white font-weight-bold"
@click="onAddProject"
>
<v-icon left icon="mdi-plus" size="20" />
Create Project
</v-btn>
</v-col>
</v-row>
<v-row dense>
<v-col
v-for="(project, index) in projects"
:key="index"
cols="12"
sm="6"
md="6"
lg="6"
class="d-flex"
>
<v-card
class="pa-4 flex-grow-1 d-flex flex-column"
color="primary"
variant="elevated"
elevation="6"
rounded="lg"
@click="selectProject(index)"
@contextmenu.prevent="(e) => openContextMenu(e, index)"
>
<v-card-title class="d-flex align-center">
<v-icon color="#6EC1E4" icon="mdi-file" start size="18" />
<h4>{{ project.title }}</h4>
</v-card-title>
<v-card-subtitle
class="text-white text-caption d-flex justify-space-between"
>
<span>Select Users: {{ project.creator }}</span>
<span>등록일: {{ project.date }}</span>
</v-card-subtitle>
<v-card-text
class="text-white mt-3 text-body-2 flex-grow-1"
style="white-space: normal"
>
{{ project.description }}
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-menu
v-model="contextMenu"
absolute
:style="{ top: menuY + 'px', left: menuX + 'px' }"
max-width="180"
width="130"
>
<v-list>
<v-list-item @click="modifyProject">
<v-list-item-icon
><v-icon>mdi-square-edit-outline</v-icon></v-list-item-icon
>
<v-list-item-title>Modify</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteProject">
<v-list-item-icon><v-icon>mdi-delete</v-icon></v-list-item-icon>
<v-list-item-title>Delete</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="dialog" max-width="500">
<v-card>
<v-card-title class="headline">
{{ modalMode === "create" ? "Create Project" : "Modify Project" }}
</v-card-title>
<v-card-text>
<v-form>
<v-text-field label="Project Name" v-model="form.prjNm" required />
<v-textarea
label="Description"
v-model="form.prjDesc"
rows="3"
required
/>
<v-select
label="Select Users"
v-model="form.selectedUsers"
:items="userOptions"
item-title="username"
item-value="username"
multiple
chips
closable-chips
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" @click="saveProject">
{{ modalMode === "create" ? "Create" : "Save" }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<style scoped></style>

4
typed-router.d.ts vendored

@ -21,11 +21,13 @@ declare module 'vue-router/auto-routes' {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/DatasetView': RouteRecordInfo<'/DatasetView', '/DatasetView', Record<never, never>, Record<never, never>>,
'/DeploymentView': RouteRecordInfo<'/DeploymentView', '/DeploymentView', Record<never, never>, Record<never, never>>,
'/ExcutionsView': RouteRecordInfo<'/ExcutionsView', '/ExcutionsView', Record<never, never>, Record<never, never>>,
'/ExecutionsView': RouteRecordInfo<'/ExecutionsView', '/ExecutionsView', Record<never, never>, Record<never, never>>,
'/ExperimentView': RouteRecordInfo<'/ExperimentView', '/ExperimentView', Record<never, never>, Record<never, never>>,
'/HomeView': RouteRecordInfo<'/HomeView', '/HomeView', Record<never, never>, Record<never, never>>,
'/LoginView': RouteRecordInfo<'/LoginView', '/LoginView', Record<never, never>, Record<never, never>>,
'/MainView': RouteRecordInfo<'/MainView', '/MainView', Record<never, never>, Record<never, never>>,
'/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>,
'/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>,
'/WorkflowView': RouteRecordInfo<'/WorkflowView', '/WorkflowView', Record<never, never>, Record<never, never>>,

@ -1,19 +1,22 @@
// Plugins
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import Fonts from 'unplugin-fonts/vite'
import Layouts from 'vite-plugin-vue-layouts-next'
import Vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'
import { VueRouterAutoImports } from 'unplugin-vue-router'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import Fonts from "unplugin-fonts/vite";
import Layouts from "vite-plugin-vue-layouts-next";
import Vue from "@vitejs/plugin-vue";
import VueRouter from "unplugin-vue-router/vite";
import { VueRouterAutoImports } from "unplugin-vue-router";
import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/
export default defineConfig({
// 배포할때는 주석 풀기
// base: process.env.VITE_ROOT_PATH,
// base: "/autoflow/",
plugins: [
VueRouter(),
Layouts(),
@ -24,24 +27,26 @@ export default defineConfig({
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
configFile: "src/styles/settings.scss",
},
}),
Components(),
Fonts({
google: {
families: [{
name: 'Roboto',
styles: 'wght@100;300;400;500;700;900',
}],
families: [
{
name: "Roboto",
styles: "wght@100;300;400;500;700;900",
},
],
},
}),
AutoImport({
imports: [
'vue',
"vue",
VueRouterAutoImports,
{
pinia: ['defineStore', 'storeToRefs'],
pinia: ["defineStore", "storeToRefs"],
},
],
eslintrc: {
@ -52,27 +57,19 @@ export default defineConfig({
],
optimizeDeps: {
exclude: [
'vuetify',
'vue-router',
'unplugin-vue-router/runtime',
'unplugin-vue-router/data-loaders',
'unplugin-vue-router/data-loaders/basic',
"vuetify",
"vue-router",
"unplugin-vue-router/runtime",
"unplugin-vue-router/data-loaders",
"unplugin-vue-router/data-loaders/basic",
],
},
define: { 'process.env': {} },
define: { "process.env": {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('src', import.meta.url)),
"@": fileURLToPath(new URL("src", import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"],
},
server: {
port: 3000,
@ -80,11 +77,8 @@ export default defineConfig({
css: {
preprocessorOptions: {
sass: {
api: 'modern-compiler',
},
scss: {
api: 'modern-compiler',
api: "modern-compiler",
},
},
},
})
});

Loading…
Cancel
Save