박민혁 박민혁 07-11
250711 박민혁 로그인 로그아웃 기능 추가
@523cb2bdbd38b7f850fe30bab65ce2a151ecdffc
 
client/resources/api/auth.js (added)
+++ client/resources/api/auth.js
@@ -0,0 +1,17 @@
+import apiClient from "./index";
+
+// 회원가입 기능
+export const resistProc = (data) => {
+    return apiClient.post(`/auth/register.json`, data);
+};
+
+// 로그인 기능
+export const loginProc = (data) => {
+    return apiClient.post(`/auth/login.json`, data);
+};
+
+// 로그아웃 기능
+export const logoutProc = () => {
+    return apiClient.post(`/auth/logout.json`);
+};
+
 
client/resources/api/index.js (added)
+++ client/resources/api/index.js
@@ -0,0 +1,80 @@
+import axios from 'axios';
+import store from "../../views/pages/AppStore";
+
+const apiClient = axios.create({
+    baseURL: '/api',
+    withCredentials: true,
+    headers: {
+        'Content-Type': 'application/json; charset=UTF-8',
+    },
+});
+
+// 요청 인터셉터: 저장된 액세스 토큰을 헤더에 추가
+apiClient.interceptors.request.use(
+    config => {
+        // store.state.authorization에 액세스 토큰이 저장되어 있다고 가정
+        if (store.state.authorization) {
+            config.headers.Authorization = store.state.authorization;
+        }
+
+        return config;
+    },
+    error => Promise.reject(error)
+);
+
+// 응답 인터셉터: 401 에러(토큰 만료) 발생 시 리프레시 토큰을 이용해 새 액세스 토큰 재발급
+apiClient.interceptors.response.use(
+  response => response,
+  async error => {
+    const originalReq = error.config;
+
+    if (error.response.status === 403) {
+      return Promise.reject(error);
+    }
+
+    if (error.response.status === 401 &&
+        error.response.data.message === 'Token expired' &&
+        !originalReq._retry) {
+
+      originalReq._retry = true;
+
+      try {
+        // ✅ 쿠키 기반 리프레시 요청: 따로 refreshToken을 보낼 필요 없음
+        const refreshResponse = await axios.post(
+          '/api/auth/refresh/tokenReissue.json',
+          {},
+          { withCredentials: true } // 쿠키 전송 허용
+        );
+
+        const newAccessToken = refreshResponse.data.accessToken;
+        store.commit('setAuthorization', newAccessToken);
+
+        // JWT 디코딩하여 사용자 정보 저장
+        const base64String = newAccessToken.split('.')[1];
+        const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/');
+        const jsonPayload = decodeURIComponent(
+          atob(base64).split('').map(c =>
+            '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
+          ).join('')
+        );
+        const mem = JSON.parse(jsonPayload);
+        store.commit("setMemId", mem.memberId);
+        store.commit('setMemNm', mem.memberName);
+        store.commit('setMemLoginId', mem.memberLoginId);
+
+        // 원래 요청 헤더에 새 토큰 설정 후 재시도
+        originalReq.headers.Authorization = newAccessToken;
+        return apiClient(originalReq);
+      } catch (refreshError) {
+        sessionStorage.setItem("redirect", window.location.pathname + window.location.search);
+        store.commit("setStoreReset");
+        window.location.href = "/login.page";
+        return Promise.reject(refreshError);
+      }
+    }
+
+    return Promise.reject(error);
+  }
+);
+
+export default apiClient;
client/views/index.js
--- client/views/index.js
+++ client/views/index.js
@@ -7,9 +7,10 @@
 
 import AppRouter from './pages/AppRouter.js';
 import App from './pages/App.vue';
+import AppStore from './pages/AppStore.js'
 import "../resources/scss/main.scss";
 
 
-const vue = createApp(App).use(AppRouter).mount('#root');
+const vue = createApp(App).use(AppRouter).use(AppStore).mount('#root');
 
 
client/views/layout/Header.vue
--- client/views/layout/Header.vue
+++ client/views/layout/Header.vue
@@ -5,16 +5,19 @@
             <span style="font-weight: bold;">{{ selectedLabel }}</span>
         </div>
         <div class="user-info gap10">
-            <div class="user-name"><img src="../../resources/img/content/ico_user.svg" alt="" style="vertical-align: middle;"> 관리자</div>
-            <button class="user-logout"><img src="../../resources/img/component/common/ico_logout_w_16.svg" alt="" style="color: #ffffff;"> 로그아웃</button>
+            <div class="user-name"><img src="../../resources/img/content/ico_user.svg" alt=""
+                    style="vertical-align: middle;"> 관리자</div>
+            <button class="user-logout" @click="fnlogOut()"><img src="../../resources/img/component/common/ico_logout_w_16.svg" alt=""
+                    style="color: #ffffff;" > 로그아웃</button>
         </div>
     </header>
 </template>
 
 <script>
+import { mapActions, mapGetters } from "vuex";
 
 export default {
-     props: {
+    props: {
         selectedLabel: String,
         selectedIcon: [String, Object]
     },
@@ -23,15 +26,20 @@
         };
     },
     methods: {
-       
+        ...mapActions(["logout"]),
+        async fnlogOut() {
+            await this.logout();  // Vuex의 logout 액션 실행
+            this.$router.push({ path: "/login.page" });  // 로그인 페이지로 이동
+        },
+
     },
     watch: {},
     computed: {},
     components: {},
-    created() {},
+    created() { },
     mounted() {
     },
-    beforeUnmount() {},
+    beforeUnmount() { },
 };
 
 </script>
(파일 끝에 줄바꿈 문자 없음)
 
client/views/pages/AppStore.js (added)
+++ client/views/pages/AppStore.js
@@ -0,0 +1,69 @@
+import { createStore } from "vuex";
+import createPersistedState from "vuex-persistedstate";
+import { logoutProc } from "../../resources/api/auth";
+
+export default createStore({
+  plugins: [createPersistedState()],
+  state: {
+    authorization: null,
+    path: null,
+    pageAuth: null,
+    memId: null,
+    memNm: null,
+    memLoginId: null,
+  },
+  getters: {
+    getAuthorization: (state) => state.authorization,
+    getMemId: (state) => state.memId,
+    getMemNm: (state) => state.memNm,
+    getMemLoginId: (state) => state.memLoginId,
+  },
+  mutations: {
+    setAuthorization(state, newValue) {
+      state.authorization = newValue;
+    },
+    setMenu(state, newValue) {
+      state.menu = newValue;
+    },
+    setPath(state, newValue) {
+      state.path = newValue;
+    },
+    setPageAuth(state, newValue) {
+      state.pageAuth = newValue;
+    },
+    setMemId(state, newValue) {
+      state.memId = newValue;
+    },
+    setMemNm(state, newValue) {
+      state.memNm = newValue;
+    },
+    setMemLoginId(state, newValue) {
+      state.memLoginId = newValue;
+    },
+    setStoreReset(state) {
+      state.authorization = null;
+      state.memId = null;
+      state.memNm = null;
+      state.memLoginId = null;
+      state.pageAuth = null;
+    },
+
+
+  },
+  actions: {
+    async logout({ commit }) {
+
+      const res = await logoutProc();
+      if (res.status === 200) {
+        commit("setStoreReset");
+      }
+    },
+
+    setPath({ commit }, path) {
+      commit("setPath", path);
+    },
+    setPageAuth({ commit }, pageAuth) {
+      commit("setPageAuth", pageAuth);
+    },
+  },
+});
client/views/pages/common/Join.vue
--- client/views/pages/common/Join.vue
+++ client/views/pages/common/Join.vue
@@ -15,18 +15,18 @@
                         <th style="background-color: #eff1fa;">아이디</th>
                         <td>
                             <div class="input-group">
-                                <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요">
+                                <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요" v-model="loginId">
                                 <button class="btn sm black">중복확인</button>
                             </div>
                         </td>
                     </tr>
                     <tr>
                         <th  style="background-color: #eff1fa;">비밀번호</th>
-                        <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요"></td>
+                        <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요" v-model="password"></td>
                     </tr>
                     <tr>
                         <th style="background-color: #eff1fa;">비밀번호 확인</th>
-                        <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요"></td>
+                        <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요" v-model="passwordCheck"></td>
                     </tr>
                 </tbody>
             </table>
@@ -40,13 +40,13 @@
                 <tbody>
                     <tr>
                         <th style="background-color: #eff1fa;">이름</th>
-                        <td><input type="text" class="form-control sm" placeholder="한글 15자, 영문 30자까지 가능합니다."></td>
+                        <td><input type="text" class="form-control sm" placeholder="한글 15자, 영문 30자까지 가능합니다." v-model="memberName"></td>
                     </tr>
                     <tr>
                         <th style="background-color: #eff1fa;">이메일</th>
                         <td>
                             <div class="input-group">
-                                <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요">
+                                <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요" v-model="email">
                                 <button class="btn sm black">중복확인</button>
                             </div>
                         </td>
@@ -65,11 +65,11 @@
                         <th style="background-color: #eff1fa;border-radius: 1rem 0 0 1rem; ">전화번호</th>
                         <td>
                             <div class="input-group mb10">
-                                <input type="text" class="form-control sm" placeholder="-제외 휴대전화 번호를 입력해주세요">
+                                <input type="text" class="form-control sm" placeholder="-제외 휴대전화 번호를 입력해주세요" v-model="phoneNumber">
                                 <button class="btn sm black">인증번호 발송</button>
                             </div>
                             <div class="input-group">
-                                <input type="text" class="form-control sm" placeholder="인증번호 6자리 숫자를 입력해주세요" style="background-color: #ebebeb;">
+                                <input type="text" class="form-control sm" placeholder="인증번호 6자리 숫자를 입력해주세요" style="background-color: #ebebeb;" v-model="vertifiCode">
                                 <button class="btn sm black" style="min-width: 119px;">확인</button>
                             </div>
                         </td>
@@ -91,6 +91,13 @@
 export default {
     data() {
         return {
+            loginId: null,
+            password: null,
+            passwordCheck: null,
+            memberName: null,
+            email: null,
+            phoneNumber: null,
+            vertifiCode: null,
         };
     },
     methods: {},
client/views/pages/common/Login.vue
--- client/views/pages/common/Login.vue
+++ client/views/pages/common/Login.vue
@@ -7,42 +7,87 @@
             <div class="login-form">
                 <div>
                     <div class="mb10">
-                        <input type="text" id="username" class="form-control md" v-model="username" required placeholder="아이디를 입력하세요"> 
+                        <input type="text" id="loginId" class="form-control md" v-model="loginId" required
+                            placeholder="아이디를 입력하세요">
                     </div>
                     <div class="mb20">
-                        <input type="password" id="password" class="form-control md" v-model="password" required placeholder="비밀번호를 입력하세요">
+                        <input type="password" id="password" class="form-control md" v-model="password" required
+                            placeholder="비밀번호를 입력하세요">
                     </div>
                 </div>
                 <div class="error-message mb20" v-if="!isValid">
-                    <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디 또는 비밀번호가 잘못되었습니다.</p>
-                    <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디와 비밀번호를 정확히 입력해주세요.</p>
+                    <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디 또는 비밀번호가
+                        잘못되었습니다.</p>
+                    <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디와 비밀번호를
+                        정확히 입력해주세요.</p>
                 </div>
-                <button class="btn lg primary login-btn mb30"><span class="icon-label lbtn">로그인</span></button>
+                <button class="btn lg primary login-btn mb30" @click="loginProc"><span
+                        class="icon-label lbtn">로그인</span></button>
             </div>
             <div class="layout center justify-center links">
                 <router-link :to="{ path: '/find.page', query: { type: 'id' } }">아이디 찾기</router-link>
                 <router-link :to="{ path: '/find.page', query: { type: 'pw' } }">비밀번호 찾기</router-link>
-                <router-link to="/join.page">회원가입 찾기</router-link> 
+                <router-link to="/join.page">회원가입 찾기</router-link>
             </div>
         </div>
     </div>
 </template>
 
 <script>
-
+import { loginProc } from '../../../resources/api/auth';
+import AppStore from '../AppStore';
 export default {
     data() {
         return {
             isValid: true,
+            loginId: null,
+            password: null,
         };
     },
-    methods: {},
+    methods: {
+        async loginProc() {
+            const vm = this;
+            // 사용자 아이디와 비밀번호 입력값 검증
+            if (!vm.loginId || vm.loginId.trim() === '') {
+                return;
+            }
+            if (!vm.password || vm.password.trim() === '') {
+                return;
+            }
+
+            const loginData = {
+                loginId : vm.loginId,
+                password : vm.password
+            }
+
+            try {
+                const res = await loginProc(loginData);
+                if (res.status === 200) {
+                    const accessToken = res.data.accessToken;
+                    AppStore.commit('setAuthorization', accessToken);
+
+                    const base64String = AppStore.state.authorization.split('.')[1];
+                    const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/');
+                    const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => {
+                        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+                    }).join(''));
+                    const mem = JSON.parse(jsonPayload);
+                    AppStore.commit("setMemId", mem.memberId);
+                    AppStore.commit('setMemNm', mem.memberName);
+                    AppStore.commit('setMemLoginId', mem.loginId);
+
+                    this.$router.push({ path: "/" });
+                }
+            } catch (error) {
+            }
+        },
+    },
     watch: {},
     computed: {},
     components: {},
-    created() {},
-    mounted() {},
-    beforeUnmount() {},
+    created() { },
+    mounted() { },
+    beforeUnmount() { },
 };
 
 </script>
(파일 끝에 줄바꿈 문자 없음)
package-lock.json
--- package-lock.json
+++ package-lock.json
@@ -1,5 +1,5 @@
 {
-  "name": "node_vue_web_server_framework_v1.0-master",
+  "name": "3D_AI_System",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
@@ -7,6 +7,7 @@
       "dependencies": {
         "@babel/cli": "7.19.3",
         "@babel/core": "7.19.3",
+        "axios": "^1.10.0",
         "babel-loader": "8.2.5",
         "css-loader": "6.7.1",
         "express": "4.18.1",
@@ -21,6 +22,8 @@
         "vue-router": "4.1.5",
         "vue-style-loader": "4.1.3",
         "vue3-sfc-loader": "^0.8.4",
+        "vuex": "^4.1.0",
+        "vuex-persistedstate": "^4.1.0",
         "webpack": "5.74.0",
         "webpack-cli": "4.10.0"
       },
@@ -1009,6 +1012,11 @@
         "node": "*"
       }
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.21",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -1045,6 +1053,16 @@
       },
       "peerDependencies": {
         "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
+      "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
       }
     },
     "node_modules/babel-loader": {
@@ -1521,6 +1539,17 @@
       "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
       "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==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/commander": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1722,6 +1751,22 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/delegates": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1919,6 +1964,20 @@
       "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==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2267,6 +2326,40 @@
         "flat": "cli.js"
       }
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
+      "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+      "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/forwarded": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2594,6 +2687,20 @@
       "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==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
       "engines": {
         "node": ">= 0.4"
       },
@@ -4419,6 +4526,11 @@
         "node": ">= 0.10"
       }
     },
+    "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=="
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4943,6 +5055,12 @@
       "engines": {
         "node": ">=8"
       }
+    },
+    "node_modules/shvl": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz",
+      "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==",
+      "deprecated": "older versions vulnerable to prototype pollution"
     },
     "node_modules/side-channel": {
       "version": "1.1.0",
@@ -5789,6 +5907,30 @@
       "integrity": "sha512-eziaIrk/N9f9OCpyFEkR6vMsZUHcF5mQslXjffwcb5Iq6EuU74QrlpBeJqA04MvAGT7f5O8la2v9k3NtQnJb3Q==",
       "license": "MIT"
     },
+    "node_modules/vuex": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
+      "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.0.0-beta.11"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/vuex-persistedstate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz",
+      "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+      "dependencies": {
+        "deepmerge": "^4.2.2",
+        "shvl": "^2.0.3"
+      },
+      "peerDependencies": {
+        "vuex": "^3.0 || ^4.0.0-rc"
+      }
+    },
     "node_modules/watchpack": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
package.json
--- package.json
+++ package.json
@@ -2,6 +2,7 @@
   "dependencies": {
     "@babel/cli": "7.19.3",
     "@babel/core": "7.19.3",
+    "axios": "^1.10.0",
     "babel-loader": "8.2.5",
     "css-loader": "6.7.1",
     "express": "4.18.1",
@@ -16,6 +17,8 @@
     "vue-router": "4.1.5",
     "vue-style-loader": "4.1.3",
     "vue3-sfc-loader": "^0.8.4",
+    "vuex": "^4.1.0",
+    "vuex-persistedstate": "^4.1.0",
     "webpack": "5.74.0",
     "webpack-cli": "4.10.0"
   },
Add a comment
List