diff --git a/applicationFE/.env b/applicationFE/.env
new file mode 100644
index 0000000..3fd20df
--- /dev/null
+++ b/applicationFE/.env
@@ -0,0 +1 @@
+# VITE_API_URL="http://localhost:18083"
\ No newline at end of file
diff --git a/applicationFE/.eslintrc.cjs b/applicationFE/.eslintrc.cjs
new file mode 100644
index 0000000..7e4f590
--- /dev/null
+++ b/applicationFE/.eslintrc.cjs
@@ -0,0 +1,19 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+ root: true,
+ 'extends': [
+ 'plugin:vue/vue3-essential',
+ 'eslint:recommended',
+ '@vue/eslint-config-typescript',
+ '@vue/eslint-config-prettier/skip-formatting'
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest'
+ },
+ rules: {
+ // should always be multi-word 적용 X
+ 'vue/multi-word-component-names' : 0
+ },
+}
diff --git a/applicationFE/.gitignore b/applicationFE/.gitignore
new file mode 100644
index 0000000..cef63ec
--- /dev/null
+++ b/applicationFE/.gitignore
@@ -0,0 +1,31 @@
+yarn.lock
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
diff --git a/applicationFE/.prettierrc.json b/applicationFE/.prettierrc.json
new file mode 100644
index 0000000..66e2335
--- /dev/null
+++ b/applicationFE/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/prettierrc",
+ "semi": false,
+ "tabWidth": 2,
+ "singleQuote": true,
+ "printWidth": 100,
+ "trailingComma": "none"
+}
\ No newline at end of file
diff --git a/applicationFE/README.md b/applicationFE/README.md
new file mode 100644
index 0000000..342b5c1
--- /dev/null
+++ b/applicationFE/README.md
@@ -0,0 +1,39 @@
+# workflow-manager-ui
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+yarn install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+yarn dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+yarn build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+yarn lint
+```
diff --git a/applicationFE/env.d.ts b/applicationFE/env.d.ts
new file mode 100644
index 0000000..409cda1
--- /dev/null
+++ b/applicationFE/env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL: string
+ // 다른 환경 변수들에 대한 타입 정의...
+ }
+
+ interface ImportMeta {
+ readonly env: ImportMetaEnv
+ }
\ No newline at end of file
diff --git a/applicationFE/index.html b/applicationFE/index.html
new file mode 100644
index 0000000..c6f3b8c
--- /dev/null
+++ b/applicationFE/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Workflow
+
+
+
+
+
+
diff --git a/applicationFE/nginx/nginx.config b/applicationFE/nginx/nginx.config
new file mode 100644
index 0000000..e43ec02
--- /dev/null
+++ b/applicationFE/nginx/nginx.config
@@ -0,0 +1,15 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
\ No newline at end of file
diff --git a/applicationFE/package.json b/applicationFE/package.json
new file mode 100644
index 0000000..620217d
--- /dev/null
+++ b/applicationFE/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "application-manager-ui",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "run-p type-check \"build-only {@}\" --",
+ "preview": "vite preview",
+ "build-only": "vite build",
+ "type-check": "vue-tsc --build --force",
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+ "format": "prettier --write src/"
+ },
+ "dependencies": {
+ "@tabler/core": "^1.0.0-beta20",
+ "ace-builds": "^1.35.2",
+ "axios": "^1.7.2",
+ "bootstrap": "^5.3.0",
+ "lodash": "^4.17.21",
+ "pinia": "^2.1.7",
+ "tabulator-tables": "^6.2.1",
+ "vue": "^3.4.29",
+ "vue-draggable-next": "^2.2.1",
+ "vue-router": "^4.3.3",
+ "vue-toastification": "^2.0.0-rc.5",
+ "vue3-ace-editor": "^2.2.4"
+ },
+ "devDependencies": {
+ "@rushstack/eslint-patch": "^1.8.0",
+ "@tsconfig/node20": "^20.1.4",
+ "@types/node": "^20.14.5",
+ "@types/tabulator-tables": "^6.2.2",
+ "@vitejs/plugin-vue": "^5.0.5",
+ "@vue/eslint-config-prettier": "^9.0.0",
+ "@vue/eslint-config-typescript": "^13.0.0",
+ "@vue/tsconfig": "^0.5.1",
+ "eslint": "^8.57.0",
+ "eslint-plugin-vue": "^9.23.0",
+ "npm-run-all2": "^6.2.0",
+ "prettier": "^3.2.5",
+ "typescript": "~5.4.0",
+ "vite": "^5.3.1",
+ "vite-plugin-vue-devtools": "^7.3.1",
+ "vue-tsc": "^2.0.21"
+ }
+}
diff --git a/applicationFE/src/App.vue b/applicationFE/src/App.vue
new file mode 100644
index 0000000..df4dfcf
--- /dev/null
+++ b/applicationFE/src/App.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/applicationFE/src/api/eventListener.ts b/applicationFE/src/api/eventListener.ts
new file mode 100644
index 0000000..791e6b4
--- /dev/null
+++ b/applicationFE/src/api/eventListener.ts
@@ -0,0 +1,45 @@
+import request from "../common/request";
+import type { EventListener } from "../views/type/type";
+
+
+// Event Listener 목록
+export const getEventListenerList = () => {
+ return request.get('/eventlistener/list')
+}
+
+// Event Listener 상세
+export function getEventListenerDetailInfo(eventlistenerIdx:number) {
+ return request.get("/eventlistener/" + eventlistenerIdx);
+}
+
+// 중복확인
+export function duplicateCheck(param: {eventListenerName:string, eventListenerUrl: string}) {
+ return request.get(`/eventlistener/duplicate?eventlistenerName=${param.eventListenerName}&eventListenerUrl=${param.eventListenerUrl}`)
+}
+
+// Event Listener 등록
+export function registEventListener(param: EventListener) {
+ return request.post(`/eventlistener`, param)
+}
+
+// Event Listener 수정
+export function updateEventListener(param: EventListener) {
+ return request.patch(`/eventlistener/${param.eventListenerIdx}`, param)
+}
+
+// Event Listener 삭제
+export function deleteEventListener(eventlistenerIdx: number) {
+ return request.delete(`/eventlistener/${eventlistenerIdx}`)
+}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/applicationFE/src/api/oss.ts b/applicationFE/src/api/oss.ts
new file mode 100644
index 0000000..172121d
--- /dev/null
+++ b/applicationFE/src/api/oss.ts
@@ -0,0 +1,57 @@
+import request from "../common/request";
+import type { Oss } from "../views/type/type";
+
+
+// OSS Type 목록
+export const getOssTypeList = () => {
+ return request.get('/ossType/list')
+}
+
+// OSS Type 필터링 목록
+// 이미 생성된 OSS는 또 다시 생성할 수 없다
+export const getOssTypeFilteredList = () => {
+ return request.get('/ossType/filter/list')
+}
+
+// OSS 목록
+export const getOssAllList = () => {
+ return request.get('/oss/list')
+}
+
+// OSS 목록
+export const getOssList = (ossTypeName:string) => {
+ return request.get(`/oss/list/${ossTypeName}`)
+}
+
+// 중복확인
+export function duplicateCheck(param: { ossName:string, ossUrl:string, ossUsername:string}) {
+ return request.get(`/oss/duplicate?ossName=${param.ossName}&ossUrl=${param.ossUrl}&ossUsername=${param.ossUsername}`)
+}
+
+
+// 연결 확인
+export function ossConnectionChecked(param: { ossUrl: string, ossUsername: string, ossPassword: string, ossTypeIdx: number }) {
+ return request.post(`/oss/connection-check`, param)
+}
+
+
+// OSS 상세
+export function getOssDetailInfo(ossIdx:number | string | string[]) {
+ return request.get("/oss/" + ossIdx);
+}
+
+
+// OSS 등록
+export function registOss(param:Oss) {
+ return request.post(`/oss`, param)
+}
+
+// OSS 수정
+export function updateOss(param: Oss) {
+ return request.patch(`/oss/${param.ossIdx}`, param)
+}
+
+// OSS 삭제
+export function deleteOss(ossIdx: number) {
+ return request.delete(`/oss/${ossIdx}`)
+}
\ No newline at end of file
diff --git a/applicationFE/src/api/workflow.ts b/applicationFE/src/api/workflow.ts
new file mode 100644
index 0000000..3a05221
--- /dev/null
+++ b/applicationFE/src/api/workflow.ts
@@ -0,0 +1,63 @@
+import request from "../common/request";
+import type { Workflow } from "../views/type/type"
+
+// 워크플로우 목록
+// export const getWorkflowList = () => {
+// return request.get('/workflow/list')
+// }
+export const getWorkflowList = (eventlistenerYn:String) => {
+ return request.get(`/eventlistener/workflowList/${eventlistenerYn}`)
+}
+
+// 중복확인
+export function duplicateCheck(workflowName:string) {
+ return request.get(`/workflow/name/duplicate?workflowName=${workflowName}`)
+}
+
+// default 스크립트
+export function getTemplateStage(workflowName:string) {
+ return request.get(`/workflow/template/${workflowName}`)
+}
+
+// 파이프라인 목록
+export function getWorkflowPipelineList() {
+ return request.get(`/workflow/workflowStageList`)
+}
+
+// 파이프라인 구분 목록
+export function getPipelineCdList() {
+ return request.get(`/workflowStageType/list`);
+}
+
+// 워크플로우 상세
+// export function getWorkflowDetailInfo(workflowIdx:number | string | string[]) {
+// return request.get("/workflow/" + workflowIdx + "/N");
+// }
+export function getWorkflowDetailInfo(workflowIdx:number | string | string[], eventlistenerYn:String) {
+ return request.get(`/eventlistener/workflowDetail/${workflowIdx}/${eventlistenerYn}`);
+}
+
+// 저장
+export function registWorkflow(workflow: Workflow | any) {
+ return request.post(`/workflow`, workflow);
+}
+
+// 수정
+export function updateWorkflow(workflow: Workflow | any) {
+ return request.patch(`/workflow/${workflow.workflowInfo.workflowIdx}`, workflow);
+}
+
+
+// 삭제
+export function deleteWorkflow(workflowIdx: number) {
+ return request.delete(`/workflow/${workflowIdx}`);
+}
+
+// 배포 실행
+export function runWorkflow(params: Workflow) {
+ return request.post(`/workflow/run`, params);
+}
+
+export function existEventListener(workflowIdx: number) {
+ return request.get(`/workflow/existEventListener/${workflowIdx}`);
+}
\ No newline at end of file
diff --git a/applicationFE/src/api/workflowStage.ts b/applicationFE/src/api/workflowStage.ts
new file mode 100644
index 0000000..c0e8cd5
--- /dev/null
+++ b/applicationFE/src/api/workflowStage.ts
@@ -0,0 +1,56 @@
+import request from "../common/request";
+import type { WorkflowStage } from "../views/type/type";
+
+
+// Workflow Stage Type 목록
+export const getWorkflowStageTypeList = () => {
+ return request.get('/workflowStageType/list')
+}
+
+// Workflow Stage 목록
+export const getWorkflowStageList = () => {
+ return request.get('/workflowStage/list')
+}
+
+// Workflow Stage 상세
+export function getWorkflowStageDetailInfo(workflowStageIdx:number) {
+ return request.get("/workflowStage/" + workflowStageIdx);
+}
+
+// 중복확인
+export function duplicateCheck(param: { workflowStageName:string, workflowStageTypeName:string}) {
+ return request.get(`/workflowStage/duplicate?workflowStageName=${param.workflowStageName}&workflowStageTypeName=${param.workflowStageTypeName}`)
+}
+
+// Workflow Stage 등록
+export function registWorkflowStage(param: WorkflowStage) {
+ return request.post(`/workflowStage`, param)
+}
+
+// Workflow Stage 수정
+export function updateWorkflowStage(param: WorkflowStage) {
+ return request.patch(`/workflowStage/${param.workflowStageIdx}`, param)
+}
+
+// Workflow Stage 삭제
+export function deleteWorkflowStage(workflowStageIdx: number) {
+ return request.delete(`/workflowStage/${workflowStageIdx}`)
+}
+
+// Workflow Stage DefaultScript 조회
+export function getWorkflowStageDefaultScript(workflowStageTypeName: string) {
+ return request.get(`/workflowStage/default/script/${workflowStageTypeName}`)
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/applicationFE/src/common/request.ts b/applicationFE/src/common/request.ts
new file mode 100644
index 0000000..68a30a8
--- /dev/null
+++ b/applicationFE/src/common/request.ts
@@ -0,0 +1,49 @@
+import axios from "axios";
+import { useToast } from "vue-toastification";
+
+const splitUrl = window.location.host.split(':');
+const baseUrl = window.location.protocol + '//' + splitUrl[0] + ':18083'
+const toast = useToast();
+const service = axios.create({
+ // baseURL: process.env.VUE_APP_API_URL,
+ baseURL: baseUrl,
+ timeout: 300000
+});
+
+
+// request interceptor
+service.interceptors.request.use(
+ config => {
+ // console.log("##[", "api", "]##", "request", config.url, config);
+ return config;
+ },
+ error => {
+ console.log("error ---------- ", error);
+ return Promise.reject(error);
+ }
+);
+
+// response interceptor
+service.interceptors.response.use(
+ response => {
+ const res = response.data;
+
+ if (res.code === 200) {
+ return res;
+ } else {
+ toast.error(res.detail)
+ return Promise.reject(new Error(res.message || "Error"));
+ }
+ },
+ error => {
+ console.log("ApiService.Response -> fail", error);
+
+ if (axios.isCancel(error)) {
+ return Promise.reject(error);
+ }
+ // toast.error(error.message)
+ return Promise.reject(error);
+ }
+);
+
+export default service;
diff --git a/applicationFE/src/components/Table/TableHeader.vue b/applicationFE/src/components/Table/TableHeader.vue
new file mode 100644
index 0000000..7358f09
--- /dev/null
+++ b/applicationFE/src/components/Table/TableHeader.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/components/Table/Tabulator.vue b/applicationFE/src/components/Table/Tabulator.vue
new file mode 100644
index 0000000..a4840b0
--- /dev/null
+++ b/applicationFE/src/components/Table/Tabulator.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/img/icon/Plus.svg b/applicationFE/src/img/icon/Plus.svg
new file mode 100644
index 0000000..270fa62
--- /dev/null
+++ b/applicationFE/src/img/icon/Plus.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/main.ts b/applicationFE/src/main.ts
new file mode 100644
index 0000000..e5efa72
--- /dev/null
+++ b/applicationFE/src/main.ts
@@ -0,0 +1,30 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import '@tabler/core/dist/css/tabler.min.css'
+import 'tabulator-tables/dist/css/tabulator_bootstrap4.min.css'
+import axios from 'axios'
+import './permission'
+
+const app = createApp(App)
+
+// Axios
+axios.defaults.baseURL = import.meta.env.VITE_API_URL
+app.config.globalProperties.axios = axios
+
+app.use(createPinia())
+
+import router from './router'
+app.use(router)
+
+import Toast from "vue-toastification";
+import "vue-toastification/dist/index.css";
+app.use(Toast, {});
+
+
+import 'bootstrap/dist/css/bootstrap.min.css'
+import 'bootstrap/dist/js/bootstrap.bundle.min.js'
+
+app.mount('#app')
+
diff --git a/applicationFE/src/permission.ts b/applicationFE/src/permission.ts
new file mode 100644
index 0000000..e1ea05b
--- /dev/null
+++ b/applicationFE/src/permission.ts
@@ -0,0 +1,8 @@
+import router from "./router/index";
+
+router.beforeEach(async (to, from, next) => {
+ console.log('## to ### : ', to)
+ console.log('## from ### : ', from)
+
+ next();
+});
\ No newline at end of file
diff --git a/applicationFE/src/router/index.ts b/applicationFE/src/router/index.ts
new file mode 100644
index 0000000..8812cc9
--- /dev/null
+++ b/applicationFE/src/router/index.ts
@@ -0,0 +1,19 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ {
+ path: '/web',
+ name: 'rootOssList',
+ component: () => import('@/views/oss/OssList.vue' as any)
+ },
+ {
+ path: '/web/oss/list',
+ name: 'ossList',
+ component: () => import('@/views/oss/OssList.vue' as any)
+ },
+ ]
+})
+
+export default router
diff --git a/applicationFE/src/stores/counter.ts b/applicationFE/src/stores/counter.ts
new file mode 100644
index 0000000..b6757ba
--- /dev/null
+++ b/applicationFE/src/stores/counter.ts
@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+ const count = ref(0)
+ const doubleCount = computed(() => count.value * 2)
+ function increment() {
+ count.value++
+ }
+
+ return { count, doubleCount, increment }
+})
diff --git a/applicationFE/src/utils/common.js b/applicationFE/src/utils/common.js
new file mode 100644
index 0000000..e465424
--- /dev/null
+++ b/applicationFE/src/utils/common.js
@@ -0,0 +1,240 @@
+
+/* 사용자 정보 이름과 아이디 함께 노출시 정의한 포맷으로 반환*/
+export function getUserNameAndIdStr(name, id) {
+ name = name || ''
+ id = id || ''
+ return `${name}(@${id})`
+}
+
+export function validateGroupName(name) {
+ const reg = /^[A-z0-9_-]{1,50}$/
+ return reg.test(name)
+}
+
+export function validateProjectName(name) {
+ const reg = /^[A-z0-9_-]{1,50}$/
+ return reg.test(name)
+}
+
+export function validateOpenShiftAppName(name) {
+ //alphanumeric (a-z, 0-9) 최대 16 character. 첫 글자는 a-z 만 허용. ‘-’ 는 허용되는데 처음과 끝은 안됨
+ const reg = /^[A-Za-z]{1}[A-z0-9_-]{1,16}[A-Za-z0-9]$/
+ return reg.test(name)
+}
+
+
+
+/**
+ * 바이트 문자 입력가능 문자수 체크
+ *
+ * @param id : tag id
+ * @param title : tag title
+ * @param maxLength : 최대 입력가능 수 (byte)
+ * @returns {Boolean}
+ */
+export function maxLengthCheck(title, maxLength){
+ maxLength = maxLength || 255;
+ if(Number(byteCheck(title)) > Number(maxLength)) {
+ return false;
+ } else {
+ return true;
+ }
+}
+
+/**
+* 바이트수 반환
+*
+* @param el : tag jquery object
+* @returns {Number}
+*/
+export function byteCheck(txt){
+ let codeByte = 0;
+ for (let idx = 0; idx < txt.length; idx++) {
+ let oneChar = escape(txt.charAt(idx));
+ if ( oneChar.length == 1 ) {
+ codeByte ++;
+ } else if (oneChar.indexOf("%u") != -1) {
+ codeByte += 2;
+ } else if (oneChar.indexOf("%") != -1) {
+ codeByte ++;
+ }
+ }
+ return codeByte;
+}
+
+
+export function getSize(fileSize, fixed) {
+ var value = {}
+ // GB 단위 이상일때 GB 단위로 환산
+ if (fileSize >= 1024 * 1024 * 1024) {
+ fileSize = fileSize / (1024 * 1024 * 1024)
+ fileSize = (fixed === undefined) ? fileSize : fileSize.toFixed(fixed)
+ value.size = fileSize
+ value.unit = 'gb'
+ }
+
+ // MB 단위 이상일때 MB 단위로 환산
+ if (fileSize >= 1024 * 1024) {
+ fileSize = fileSize / (1024 * 1024)
+ fileSize = (fixed === undefined) ? fileSize : fileSize.toFixed(fixed)
+ value.size = fileSize
+ value.unit = 'mb'
+ } else if (fileSize >= 1024) { // KB 단위 이상일때 KB 단위로 환산
+ fileSize = fileSize / 1024
+ fileSize = (fixed === undefined) ? fileSize : fileSize.toFixed(fixed)
+ value.size = fileSize
+ value.unit = 'kb'
+ } else { // KB 단위보다 작을때 byte 단위로 환산
+ fileSize = (fixed === undefined) ? fileSize : fileSize.toFixed(fixed)
+ value.size = fileSize
+ value.unit = 'byte'
+ }
+ return value
+}
+
+export function isOverOneDay(curDate) {
+ var today, resultDate
+ today = new Date()
+ resultDate = new Date(curDate)
+
+ // Time (minutes * seconds * millisecond)
+ if ((today - resultDate) / (60 * 60 * 1000) <= 24) {
+ return true
+ }
+ return false
+}
+
+export function toPascalCase(str) {
+ var arr = str.split(/\s|_/)
+ for (var i = 0, l = arr.length; i < l; i++) {
+ arr[i] = arr[i].substr(0, 1).toUpperCase() +
+ (arr[i].length > 1 ? arr[i].substr(1).toLowerCase() : '')
+ }
+ return arr.join(' ')
+}
+
+export function copyClipboard(str) {
+ var tempElem = document.createElement('textarea')
+ tempElem.value = str
+ document.body.appendChild(tempElem)
+
+ tempElem.select()
+ document.execCommand('copy')
+ document.body.removeChild(tempElem)
+}
+
+
+/*
+ - ary 요소의 key, value 값을 오브젝트로 만들기.
+ - call: deploy params에서 호출
+*/
+export function arrayToObject(ary) {
+ let object = {};
+ if (ary == null)
+ return {};
+
+ ary.forEach((item) => {
+ object[item.key] = item.value;
+ })
+
+ return object;
+}
+
+/*
+ - ary 요소의 object.value값을 배열의 요소로 만들기.
+ - call: deploy params에서 호출
+*/
+export function valueObjectToArray(ary) {
+ let array = [];
+ if (ary == null)
+ return [];
+
+
+ ary.forEach((item) => {
+ array.push(item.value);
+ })
+
+ return array;
+}
+
+
+/*
+object 정보를 key, value 오브젝트 배열 요소로 추가
+ {
+ key1:value1,
+ key2:value2
+ }
+
+ =>
+ [{key1:value1},{key2:value2}]
+*/
+export function objectToArray(obj) {
+ let toArray = [];
+ if (obj == null)
+ return [];
+
+ Object.keys(obj).forEach((key) => {
+ toArray.push({
+ key: key,
+ value:obj[key]
+ })
+ })
+
+ return toArray;
+}
+
+/*
+["value1", "value2"] => [{value:"value1"}, {value:"value2"}]
+*/
+export function arrayToValueObjectArray(targetArray) {
+ let toArray = [];
+
+ if (targetArray == null)
+ return [{value:""}];
+
+ targetArray.forEach((value) => {
+ toArray.push({
+ value:value
+ })
+ })
+
+ return toArray;
+}
+
+
+
+export function isEmptyObject(obj) {
+ if (obj == null)
+ return true;
+
+
+ if (Object.keys(obj).length == 0) {
+ return true;
+ }
+
+ return false;
+}
+
+
+/*
+.을 기준으로 앞에는 name
+뒤에는 domin 값으로 분리하는 기능
+*/
+export function splitACRServerInfo(str) {
+ let index = str.indexOf(".");
+ let info = {
+ name:"",
+ domain:""
+ }
+ if (index == -1) {
+ info.name = str;
+ } else {
+ info.name = str.substr(0, index);
+ info.domain = str.substr(index+1, str.length);
+ }
+
+ return info;
+}
+export function joinACRServerInfo(name, domain) {
+ return name +"."+domain;
+}
\ No newline at end of file
diff --git a/applicationFE/src/utils/input-validate.js b/applicationFE/src/utils/input-validate.js
new file mode 100644
index 0000000..ee20828
--- /dev/null
+++ b/applicationFE/src/utils/input-validate.js
@@ -0,0 +1,250 @@
+// import { checkIP } from './validate'
+
+
+
+
+// const _validateAlphanumericUnderbarHyphen = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const reg = /^[A-z0-9_-]{1,50}$/
+
+// if (!reg.test(value)) {
+// callback(new Error(t("validation.alphanumericUnderbarHyphen")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateAlphanumericUnderbarHyphenSpace = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const reg = /^[A-z0-9_-\s]{1,50}$/
+
+// if (!reg.test(value)) {
+// callback(new Error(t("validation.alphanumericUnderbarHyphen")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateAlphaNumeric = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const reg = /^[0-9a-zA-Z]+$/
+// if (!reg.test(value)) {
+// callback(new Error(t("validation.alphanumeric")));
+// } else {
+// callback()
+// }
+// }
+export const _validateAlphaNumericHyphen= (rule, value, callback) => {
+ const reg = /^[0-9a-zA-Z-]+$/
+ if (!reg.test(value)) {
+ callback(new Error("규칙에 맞게 입력해주세요.(영문자, 숫자, (-)만 가능)"));
+ } else {
+ callback()
+ }
+}
+
+// const _validateDomain = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const reg = /^[^((http(s?))\:\/\/)]([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/
+// if (!reg.test(value)) {
+// callback(new Error(t("validation.domain")));
+// } else {
+// callback()
+// }
+
+// }
+
+// const _validateOpenShiftAppName = (rule, value, callback) => {
+// const { t } = i18n.global;
+// //alphanumeric (a-z, 0-9) 최대 16 character. 첫 글자는 a-z 만 허용. ‘-’ 는 허용되는데 처음과 끝은 안됨
+// const reg = /^[A-Za-z]{1}[A-z0-9_-]{1,16}[A-Za-z0-9]$/
+// if (!reg.test(value) || value.length > 16) {
+// callback(new Error(t("validation.openShiftAppNameMsg")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateAppName = (rule, value, callback) => {
+// const { t } = i18n.global;
+// //alphanumeric (a-z, 0-9) 최대 16 character. 첫 글자는 a-z 만 허용. ‘-’ 는 허용되는데 처음과 끝은 안됨
+// const reg = /^[a-z]{1}[a-z0-9_-]{1,16}$/
+// if (!reg.test(value) || value.length > 16) {
+// callback(new Error(t("validation.appNameMsg")));
+// } else {
+// callback()
+// }
+// }
+
+
+// const _validateName = (rule, value, callback) => {
+// const { t } = i18n.global;
+// if (value==null || !value.toString().length) {
+// callback(new Error(t("validation.msgEmtpyInput")));
+// } else {
+// callback()
+
+// }
+
+// }
+
+// const _valdateLimitLength=(rule, value, callback)=>{
+// const { t } = i18n.global;
+// if (value.length > rule.length) {
+// callback(new Error(t("validation.limitLength")));
+// } else {
+// callback()
+// }
+// }
+
+export const _validateLength = (rule, value, callback) => {
+ if (value.length < rule.length) {
+ if (rule.length == 2) {
+ callback(new Error("2자 이상 입력해주세요."));
+ return
+ }
+ if (rule.length == 3) {
+ callback(new Error("3자 이상 입력해주세요."));
+ return
+ }
+ if (rule.length == 4) {
+ callback(new Error("4자 이상 입력해주세요."));
+ return
+ }
+ callback(new Error("내용을 입력해주세요."));
+ } else {
+ callback()
+ }
+}
+
+// const _validateIp = (rule, value, callback) => {
+// const { t } = i18n.global;
+// if (checkIP(value) == false) {
+// callback(new Error(t("validation.msgIP")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateEmail = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+
+// if (reg.test(value) == false) {
+// callback(new Error(t("validation.msgEmail")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateUserPassword = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const passwordPatten = /^(?=.*?[A-Z])(?=.*?[0-9]).{8,16}$/
+// if (passwordPatten.test(value) == false) {
+// let msgType = rule.msgType || "full";
+// if(msgType=="full")
+// callback(new Error(t("validation.msgPw")));
+// else
+// callback(new Error(t("validation.msgSimplePw")));
+
+// } else {
+// callback()
+// }
+// }
+
+// const _validatePackageName = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const packageNamePatten = /^([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*$/
+// if (packageNamePatten.test(value) == false) {
+// callback(new Error(t("validation.msgPackageName")));
+
+// } else {
+// callback()
+// }
+// }
+
+
+// const _validateUserLengthPassword = (rule, value, callback) => {
+// const { t } = i18n.global;
+// let temp = value.toString();
+// if (temp.length>=8 && temp.length<=16) {
+// callback()
+// } else {
+// callback(new Error(t("validation.msgPw2")));
+// }
+// }
+
+// const _validateSelect = (rule, value, callback) => {
+// const { t } = i18n.global;
+// if (value=="" || value==undefined) {
+// callback(new Error(t("validation.msgSelect")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateSelect2 = (rule, value, callback) => {
+// const { t } = i18n.global;
+// if (parseInt(value)==-1) {
+// callback(new Error(t("validation.msgSelect")));
+// } else {
+// callback()
+// }
+// }
+
+
+
+// const _validateURL = (rule, value, callback) => {
+// const { t } = i18n.global;
+// const urlPatten = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+// if (urlPatten.test(value) == false) {
+// callback(new Error(t("validation.msgURL")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateURL_http = (rule, value, callback) => {
+// console.log('http == ', value)
+// const { t } = i18n.global;
+// const urlPatten = /^(http):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+// if (urlPatten.test(value) == false) {
+// callback(new Error(t("validation.msgURL")));
+// } else {
+// callback()
+// }
+// }
+
+// const _validateURL_https = (rule, value, callback) => {
+// console.log('https == ', value)
+// const { t } = i18n.global;
+// const urlPatten = /^(https):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+// if (urlPatten.test(value) == false) {
+// callback(new Error(t("validation.msgURL")));
+// } else {
+// callback()
+// }
+// }
+
+// export {
+// _validateName,
+// _validateLength,
+// _validateIp,
+// _validateEmail,
+// _validateUserPassword,
+// _validateUserLengthPassword,
+// _validatePackageName,
+// _validateSelect,
+// _validateSelect2,
+// _validateURL,
+// _validateURL_http,
+// _validateURL_https,
+// _valdateLimitLength,
+// _validateAlphanumericUnderbarHyphen,
+// _validateAlphanumericUnderbarHyphenSpace,
+// _validateAlphaNumeric,
+// _validateAlphaNumericHyphen,
+// _validateDomain,
+// _validateOpenShiftAppName,
+// _validateAppName
+// }
diff --git a/applicationFE/src/utils/scroll-to.js b/applicationFE/src/utils/scroll-to.js
new file mode 100644
index 0000000..c5d8e04
--- /dev/null
+++ b/applicationFE/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/applicationFE/src/utils/validate.js b/applicationFE/src/utils/validate.js
new file mode 100644
index 0000000..0f3d0b0
--- /dev/null
+++ b/applicationFE/src/utils/validate.js
@@ -0,0 +1,92 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+ const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+ const reg = /^[a-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+ const reg = /^[A-Z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+ if (typeof str === 'string' || str instanceof String) {
+ return true
+ }
+ return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
+
+export function checkIP(strIP) {
+ var expUrl = /^(1|2)?\d?\d([.](1|2)?\d?\d){3}$/
+ return expUrl.test(strIP)
+}
diff --git a/applicationFE/src/views/oss/OssList.vue b/applicationFE/src/views/oss/OssList.vue
new file mode 100644
index 0000000..98d01ff
--- /dev/null
+++ b/applicationFE/src/views/oss/OssList.vue
@@ -0,0 +1,158 @@
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/views/oss/components/deleteOss.vue b/applicationFE/src/views/oss/components/deleteOss.vue
new file mode 100644
index 0000000..0796e71
--- /dev/null
+++ b/applicationFE/src/views/oss/components/deleteOss.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+ OSS 삭제
+
+
+ {{ props.ossName }}을(를) 정말 삭제하시겠습니까?
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/views/oss/components/ossForm.vue b/applicationFE/src/views/oss/components/ossForm.vue
new file mode 100644
index 0000000..ca709c6
--- /dev/null
+++ b/applicationFE/src/views/oss/components/ossForm.vue
@@ -0,0 +1,335 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/applicationFE/src/views/type/type.ts b/applicationFE/src/views/type/type.ts
new file mode 100644
index 0000000..350316e
--- /dev/null
+++ b/applicationFE/src/views/type/type.ts
@@ -0,0 +1,14 @@
+export interface Oss {
+ ossIdx: number
+ ossTypeIdx: number
+ ossName: string
+ ossDesc: string
+ ossUsername: string
+ ossPassword: string
+ ossUrl: string
+}
+export interface OssType {
+ ossTypeIdx: number
+ ossTypeName: string
+ ossTypeDesc: string
+}
\ No newline at end of file
diff --git a/applicationFE/tsconfig.app.json b/applicationFE/tsconfig.app.json
new file mode 100644
index 0000000..e14c754
--- /dev/null
+++ b/applicationFE/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"],
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/applicationFE/tsconfig.json b/applicationFE/tsconfig.json
new file mode 100644
index 0000000..66b5e57
--- /dev/null
+++ b/applicationFE/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/applicationFE/tsconfig.node.json b/applicationFE/tsconfig.node.json
new file mode 100644
index 0000000..f094063
--- /dev/null
+++ b/applicationFE/tsconfig.node.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@tsconfig/node20/tsconfig.json",
+ "include": [
+ "vite.config.*",
+ "vitest.config.*",
+ "cypress.config.*",
+ "nightwatch.conf.*",
+ "playwright.config.*"
+ ],
+ "compilerOptions": {
+ "composite": true,
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ }
+}
diff --git a/applicationFE/vite-env.d.ts b/applicationFE/vite-env.d.ts
new file mode 100644
index 0000000..409cda1
--- /dev/null
+++ b/applicationFE/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL: string
+ // 다른 환경 변수들에 대한 타입 정의...
+ }
+
+ interface ImportMeta {
+ readonly env: ImportMetaEnv
+ }
\ No newline at end of file
diff --git a/applicationFE/vite.config.ts b/applicationFE/vite.config.ts
new file mode 100644
index 0000000..dd2da0f
--- /dev/null
+++ b/applicationFE/vite.config.ts
@@ -0,0 +1,26 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueDevTools from 'vite-plugin-vue-devtools'
+
+export default defineConfig({
+
+ plugins: [
+ vue(),
+ vueDevTools(),
+ ],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url))
+ }
+ },
+ // server: {
+ // proxy: {
+ // '/*': {
+ // target: import.meta.env.VITE_API_URL,
+ // changeOrigin: true,
+ // },
+ // },
+ // }
+})
diff --git a/src/main/java/kr/co/mcmp/config/WebConfig.java b/src/main/java/kr/co/mcmp/config/WebConfig.java
new file mode 100644
index 0000000..ec50511
--- /dev/null
+++ b/src/main/java/kr/co/mcmp/config/WebConfig.java
@@ -0,0 +1,19 @@
+package kr.co.mcmp.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addViewControllers(ViewControllerRegistry registry) {
+ registry.addViewController("/web/{spring:[a-zA-Z\\-]+}")
+ .setViewName("forward:/index.html");
+ registry.addViewController("/web/**/{spring:[a-zA-Z\\-]+}")
+ .setViewName("forward:/index.html");
+ registry.addViewController("/web/{spring:[a-zA-Z\\-]+}/**{spring:?!(\\.js|\\.css)$}")
+ .setViewName("forward:/index.html");
+ }
+}
diff --git a/src/main/resources/static/tabler/software-catalog.html b/src/main/resources/static/tabler/software-catalog.html
index d16b3e6..744e943 100644
--- a/src/main/resources/static/tabler/software-catalog.html
+++ b/src/main/resources/static/tabler/software-catalog.html
@@ -21,7 +21,7 @@
}