
+++ client/resources/api/auth.js
... | ... | @@ -0,0 +1,17 @@ |
1 | +import apiClient from "./index"; | |
2 | + | |
3 | +// 회원가입 기능 | |
4 | +export const resistProc = (data) => { | |
5 | + return apiClient.post(`/auth/register.json`, data); | |
6 | +}; | |
7 | + | |
8 | +// 로그인 기능 | |
9 | +export const loginProc = (data) => { | |
10 | + return apiClient.post(`/auth/login.json`, data); | |
11 | +}; | |
12 | + | |
13 | +// 로그아웃 기능 | |
14 | +export const logoutProc = () => { | |
15 | + return apiClient.post(`/auth/logout.json`); | |
16 | +}; | |
17 | + |
+++ client/resources/api/index.js
... | ... | @@ -0,0 +1,80 @@ |
1 | +import axios from 'axios'; | |
2 | +import store from "../../views/pages/AppStore"; | |
3 | + | |
4 | +const apiClient = axios.create({ | |
5 | + baseURL: '/api', | |
6 | + withCredentials: true, | |
7 | + headers: { | |
8 | + 'Content-Type': 'application/json; charset=UTF-8', | |
9 | + }, | |
10 | +}); | |
11 | + | |
12 | +// 요청 인터셉터: 저장된 액세스 토큰을 헤더에 추가 | |
13 | +apiClient.interceptors.request.use( | |
14 | + config => { | |
15 | + // store.state.authorization에 액세스 토큰이 저장되어 있다고 가정 | |
16 | + if (store.state.authorization) { | |
17 | + config.headers.Authorization = store.state.authorization; | |
18 | + } | |
19 | + | |
20 | + return config; | |
21 | + }, | |
22 | + error => Promise.reject(error) | |
23 | +); | |
24 | + | |
25 | +// 응답 인터셉터: 401 에러(토큰 만료) 발생 시 리프레시 토큰을 이용해 새 액세스 토큰 재발급 | |
26 | +apiClient.interceptors.response.use( | |
27 | + response => response, | |
28 | + async error => { | |
29 | + const originalReq = error.config; | |
30 | + | |
31 | + if (error.response.status === 403) { | |
32 | + return Promise.reject(error); | |
33 | + } | |
34 | + | |
35 | + if (error.response.status === 401 && | |
36 | + error.response.data.message === 'Token expired' && | |
37 | + !originalReq._retry) { | |
38 | + | |
39 | + originalReq._retry = true; | |
40 | + | |
41 | + try { | |
42 | + // ✅ 쿠키 기반 리프레시 요청: 따로 refreshToken을 보낼 필요 없음 | |
43 | + const refreshResponse = await axios.post( | |
44 | + '/api/auth/refresh/tokenReissue.json', | |
45 | + {}, | |
46 | + { withCredentials: true } // 쿠키 전송 허용 | |
47 | + ); | |
48 | + | |
49 | + const newAccessToken = refreshResponse.data.accessToken; | |
50 | + store.commit('setAuthorization', newAccessToken); | |
51 | + | |
52 | + // JWT 디코딩하여 사용자 정보 저장 | |
53 | + const base64String = newAccessToken.split('.')[1]; | |
54 | + const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); | |
55 | + const jsonPayload = decodeURIComponent( | |
56 | + atob(base64).split('').map(c => | |
57 | + '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) | |
58 | + ).join('') | |
59 | + ); | |
60 | + const mem = JSON.parse(jsonPayload); | |
61 | + store.commit("setMemId", mem.memberId); | |
62 | + store.commit('setMemNm', mem.memberName); | |
63 | + store.commit('setMemLoginId', mem.memberLoginId); | |
64 | + | |
65 | + // 원래 요청 헤더에 새 토큰 설정 후 재시도 | |
66 | + originalReq.headers.Authorization = newAccessToken; | |
67 | + return apiClient(originalReq); | |
68 | + } catch (refreshError) { | |
69 | + sessionStorage.setItem("redirect", window.location.pathname + window.location.search); | |
70 | + store.commit("setStoreReset"); | |
71 | + window.location.href = "/login.page"; | |
72 | + return Promise.reject(refreshError); | |
73 | + } | |
74 | + } | |
75 | + | |
76 | + return Promise.reject(error); | |
77 | + } | |
78 | +); | |
79 | + | |
80 | +export default apiClient; |
--- client/views/index.js
+++ client/views/index.js
... | ... | @@ -7,9 +7,10 @@ |
7 | 7 |
|
8 | 8 |
import AppRouter from './pages/AppRouter.js'; |
9 | 9 |
import App from './pages/App.vue'; |
10 |
+import AppStore from './pages/AppStore.js' |
|
10 | 11 |
import "../resources/scss/main.scss"; |
11 | 12 |
|
12 | 13 |
|
13 |
-const vue = createApp(App).use(AppRouter).mount('#root'); |
|
14 |
+const vue = createApp(App).use(AppRouter).use(AppStore).mount('#root'); |
|
14 | 15 |
|
15 | 16 |
|
--- client/views/layout/Header.vue
+++ client/views/layout/Header.vue
... | ... | @@ -5,16 +5,19 @@ |
5 | 5 |
<span style="font-weight: bold;">{{ selectedLabel }}</span> |
6 | 6 |
</div> |
7 | 7 |
<div class="user-info gap10"> |
8 |
- <div class="user-name"><img src="../../resources/img/content/ico_user.svg" alt="" style="vertical-align: middle;"> 관리자</div> |
|
9 |
- <button class="user-logout"><img src="../../resources/img/component/common/ico_logout_w_16.svg" alt="" style="color: #ffffff;"> 로그아웃</button> |
|
8 |
+ <div class="user-name"><img src="../../resources/img/content/ico_user.svg" alt="" |
|
9 |
+ style="vertical-align: middle;"> 관리자</div> |
|
10 |
+ <button class="user-logout" @click="fnlogOut()"><img src="../../resources/img/component/common/ico_logout_w_16.svg" alt="" |
|
11 |
+ style="color: #ffffff;" > 로그아웃</button> |
|
10 | 12 |
</div> |
11 | 13 |
</header> |
12 | 14 |
</template> |
13 | 15 |
|
14 | 16 |
<script> |
17 |
+import { mapActions, mapGetters } from "vuex"; |
|
15 | 18 |
|
16 | 19 |
export default { |
17 |
- props: { |
|
20 |
+ props: { |
|
18 | 21 |
selectedLabel: String, |
19 | 22 |
selectedIcon: [String, Object] |
20 | 23 |
}, |
... | ... | @@ -23,15 +26,20 @@ |
23 | 26 |
}; |
24 | 27 |
}, |
25 | 28 |
methods: { |
26 |
- |
|
29 |
+ ...mapActions(["logout"]), |
|
30 |
+ async fnlogOut() { |
|
31 |
+ await this.logout(); // Vuex의 logout 액션 실행 |
|
32 |
+ this.$router.push({ path: "/login.page" }); // 로그인 페이지로 이동 |
|
33 |
+ }, |
|
34 |
+ |
|
27 | 35 |
}, |
28 | 36 |
watch: {}, |
29 | 37 |
computed: {}, |
30 | 38 |
components: {}, |
31 |
- created() {}, |
|
39 |
+ created() { }, |
|
32 | 40 |
mounted() { |
33 | 41 |
}, |
34 |
- beforeUnmount() {}, |
|
42 |
+ beforeUnmount() { }, |
|
35 | 43 |
}; |
36 | 44 |
|
37 | 45 |
</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/AppStore.js
... | ... | @@ -0,0 +1,69 @@ |
1 | +import { createStore } from "vuex"; | |
2 | +import createPersistedState from "vuex-persistedstate"; | |
3 | +import { logoutProc } from "../../resources/api/auth"; | |
4 | + | |
5 | +export default createStore({ | |
6 | + plugins: [createPersistedState()], | |
7 | + state: { | |
8 | + authorization: null, | |
9 | + path: null, | |
10 | + pageAuth: null, | |
11 | + memId: null, | |
12 | + memNm: null, | |
13 | + memLoginId: null, | |
14 | + }, | |
15 | + getters: { | |
16 | + getAuthorization: (state) => state.authorization, | |
17 | + getMemId: (state) => state.memId, | |
18 | + getMemNm: (state) => state.memNm, | |
19 | + getMemLoginId: (state) => state.memLoginId, | |
20 | + }, | |
21 | + mutations: { | |
22 | + setAuthorization(state, newValue) { | |
23 | + state.authorization = newValue; | |
24 | + }, | |
25 | + setMenu(state, newValue) { | |
26 | + state.menu = newValue; | |
27 | + }, | |
28 | + setPath(state, newValue) { | |
29 | + state.path = newValue; | |
30 | + }, | |
31 | + setPageAuth(state, newValue) { | |
32 | + state.pageAuth = newValue; | |
33 | + }, | |
34 | + setMemId(state, newValue) { | |
35 | + state.memId = newValue; | |
36 | + }, | |
37 | + setMemNm(state, newValue) { | |
38 | + state.memNm = newValue; | |
39 | + }, | |
40 | + setMemLoginId(state, newValue) { | |
41 | + state.memLoginId = newValue; | |
42 | + }, | |
43 | + setStoreReset(state) { | |
44 | + state.authorization = null; | |
45 | + state.memId = null; | |
46 | + state.memNm = null; | |
47 | + state.memLoginId = null; | |
48 | + state.pageAuth = null; | |
49 | + }, | |
50 | + | |
51 | + | |
52 | + }, | |
53 | + actions: { | |
54 | + async logout({ commit }) { | |
55 | + | |
56 | + const res = await logoutProc(); | |
57 | + if (res.status === 200) { | |
58 | + commit("setStoreReset"); | |
59 | + } | |
60 | + }, | |
61 | + | |
62 | + setPath({ commit }, path) { | |
63 | + commit("setPath", path); | |
64 | + }, | |
65 | + setPageAuth({ commit }, pageAuth) { | |
66 | + commit("setPageAuth", pageAuth); | |
67 | + }, | |
68 | + }, | |
69 | +}); |
--- client/views/pages/common/Join.vue
+++ client/views/pages/common/Join.vue
... | ... | @@ -15,18 +15,18 @@ |
15 | 15 |
<th style="background-color: #eff1fa;">아이디</th> |
16 | 16 |
<td> |
17 | 17 |
<div class="input-group"> |
18 |
- <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요"> |
|
18 |
+ <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요" v-model="loginId"> |
|
19 | 19 |
<button class="btn sm black">중복확인</button> |
20 | 20 |
</div> |
21 | 21 |
</td> |
22 | 22 |
</tr> |
23 | 23 |
<tr> |
24 | 24 |
<th style="background-color: #eff1fa;">비밀번호</th> |
25 |
- <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요"></td> |
|
25 |
+ <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요" v-model="password"></td> |
|
26 | 26 |
</tr> |
27 | 27 |
<tr> |
28 | 28 |
<th style="background-color: #eff1fa;">비밀번호 확인</th> |
29 |
- <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요"></td> |
|
29 |
+ <td><input type="password" class="form-control sm" placeholder="3-16자 영문/숫자 조합으로 입력해주세요" v-model="passwordCheck"></td> |
|
30 | 30 |
</tr> |
31 | 31 |
</tbody> |
32 | 32 |
</table> |
... | ... | @@ -40,13 +40,13 @@ |
40 | 40 |
<tbody> |
41 | 41 |
<tr> |
42 | 42 |
<th style="background-color: #eff1fa;">이름</th> |
43 |
- <td><input type="text" class="form-control sm" placeholder="한글 15자, 영문 30자까지 가능합니다."></td> |
|
43 |
+ <td><input type="text" class="form-control sm" placeholder="한글 15자, 영문 30자까지 가능합니다." v-model="memberName"></td> |
|
44 | 44 |
</tr> |
45 | 45 |
<tr> |
46 | 46 |
<th style="background-color: #eff1fa;">이메일</th> |
47 | 47 |
<td> |
48 | 48 |
<div class="input-group"> |
49 |
- <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요"> |
|
49 |
+ <input type="text" class="form-control sm" placeholder="3-15자 영문/숫자 조합으로 입력해주세요" v-model="email"> |
|
50 | 50 |
<button class="btn sm black">중복확인</button> |
51 | 51 |
</div> |
52 | 52 |
</td> |
... | ... | @@ -65,11 +65,11 @@ |
65 | 65 |
<th style="background-color: #eff1fa;border-radius: 1rem 0 0 1rem; ">전화번호</th> |
66 | 66 |
<td> |
67 | 67 |
<div class="input-group mb10"> |
68 |
- <input type="text" class="form-control sm" placeholder="-제외 휴대전화 번호를 입력해주세요"> |
|
68 |
+ <input type="text" class="form-control sm" placeholder="-제외 휴대전화 번호를 입력해주세요" v-model="phoneNumber"> |
|
69 | 69 |
<button class="btn sm black">인증번호 발송</button> |
70 | 70 |
</div> |
71 | 71 |
<div class="input-group"> |
72 |
- <input type="text" class="form-control sm" placeholder="인증번호 6자리 숫자를 입력해주세요" style="background-color: #ebebeb;"> |
|
72 |
+ <input type="text" class="form-control sm" placeholder="인증번호 6자리 숫자를 입력해주세요" style="background-color: #ebebeb;" v-model="vertifiCode"> |
|
73 | 73 |
<button class="btn sm black" style="min-width: 119px;">확인</button> |
74 | 74 |
</div> |
75 | 75 |
</td> |
... | ... | @@ -91,6 +91,13 @@ |
91 | 91 |
export default { |
92 | 92 |
data() { |
93 | 93 |
return { |
94 |
+ loginId: null, |
|
95 |
+ password: null, |
|
96 |
+ passwordCheck: null, |
|
97 |
+ memberName: null, |
|
98 |
+ email: null, |
|
99 |
+ phoneNumber: null, |
|
100 |
+ vertifiCode: null, |
|
94 | 101 |
}; |
95 | 102 |
}, |
96 | 103 |
methods: {}, |
--- client/views/pages/common/Login.vue
+++ client/views/pages/common/Login.vue
... | ... | @@ -7,42 +7,87 @@ |
7 | 7 |
<div class="login-form"> |
8 | 8 |
<div> |
9 | 9 |
<div class="mb10"> |
10 |
- <input type="text" id="username" class="form-control md" v-model="username" required placeholder="아이디를 입력하세요"> |
|
10 |
+ <input type="text" id="loginId" class="form-control md" v-model="loginId" required |
|
11 |
+ placeholder="아이디를 입력하세요"> |
|
11 | 12 |
</div> |
12 | 13 |
<div class="mb20"> |
13 |
- <input type="password" id="password" class="form-control md" v-model="password" required placeholder="비밀번호를 입력하세요"> |
|
14 |
+ <input type="password" id="password" class="form-control md" v-model="password" required |
|
15 |
+ placeholder="비밀번호를 입력하세요"> |
|
14 | 16 |
</div> |
15 | 17 |
</div> |
16 | 18 |
<div class="error-message mb20" v-if="!isValid"> |
17 |
- <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디 또는 비밀번호가 잘못되었습니다.</p> |
|
18 |
- <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디와 비밀번호를 정확히 입력해주세요.</p> |
|
19 |
+ <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디 또는 비밀번호가 |
|
20 |
+ 잘못되었습니다.</p> |
|
21 |
+ <p><img src="../../../resources/img/component/common/ico_invalid_error_20.svg" alt=""> 아이디와 비밀번호를 |
|
22 |
+ 정확히 입력해주세요.</p> |
|
19 | 23 |
</div> |
20 |
- <button class="btn lg primary login-btn mb30"><span class="icon-label lbtn">로그인</span></button> |
|
24 |
+ <button class="btn lg primary login-btn mb30" @click="loginProc"><span |
|
25 |
+ class="icon-label lbtn">로그인</span></button> |
|
21 | 26 |
</div> |
22 | 27 |
<div class="layout center justify-center links"> |
23 | 28 |
<router-link :to="{ path: '/find.page', query: { type: 'id' } }">아이디 찾기</router-link> |
24 | 29 |
<router-link :to="{ path: '/find.page', query: { type: 'pw' } }">비밀번호 찾기</router-link> |
25 |
- <router-link to="/join.page">회원가입 찾기</router-link> |
|
30 |
+ <router-link to="/join.page">회원가입 찾기</router-link> |
|
26 | 31 |
</div> |
27 | 32 |
</div> |
28 | 33 |
</div> |
29 | 34 |
</template> |
30 | 35 |
|
31 | 36 |
<script> |
32 |
- |
|
37 |
+import { loginProc } from '../../../resources/api/auth'; |
|
38 |
+import AppStore from '../AppStore'; |
|
33 | 39 |
export default { |
34 | 40 |
data() { |
35 | 41 |
return { |
36 | 42 |
isValid: true, |
43 |
+ loginId: null, |
|
44 |
+ password: null, |
|
37 | 45 |
}; |
38 | 46 |
}, |
39 |
- methods: {}, |
|
47 |
+ methods: { |
|
48 |
+ async loginProc() { |
|
49 |
+ const vm = this; |
|
50 |
+ // 사용자 아이디와 비밀번호 입력값 검증 |
|
51 |
+ if (!vm.loginId || vm.loginId.trim() === '') { |
|
52 |
+ return; |
|
53 |
+ } |
|
54 |
+ if (!vm.password || vm.password.trim() === '') { |
|
55 |
+ return; |
|
56 |
+ } |
|
57 |
+ |
|
58 |
+ const loginData = { |
|
59 |
+ loginId : vm.loginId, |
|
60 |
+ password : vm.password |
|
61 |
+ } |
|
62 |
+ |
|
63 |
+ try { |
|
64 |
+ const res = await loginProc(loginData); |
|
65 |
+ if (res.status === 200) { |
|
66 |
+ const accessToken = res.data.accessToken; |
|
67 |
+ AppStore.commit('setAuthorization', accessToken); |
|
68 |
+ |
|
69 |
+ const base64String = AppStore.state.authorization.split('.')[1]; |
|
70 |
+ const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
|
71 |
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { |
|
72 |
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
|
73 |
+ }).join('')); |
|
74 |
+ const mem = JSON.parse(jsonPayload); |
|
75 |
+ AppStore.commit("setMemId", mem.memberId); |
|
76 |
+ AppStore.commit('setMemNm', mem.memberName); |
|
77 |
+ AppStore.commit('setMemLoginId', mem.loginId); |
|
78 |
+ |
|
79 |
+ this.$router.push({ path: "/" }); |
|
80 |
+ } |
|
81 |
+ } catch (error) { |
|
82 |
+ } |
|
83 |
+ }, |
|
84 |
+ }, |
|
40 | 85 |
watch: {}, |
41 | 86 |
computed: {}, |
42 | 87 |
components: {}, |
43 |
- created() {}, |
|
44 |
- mounted() {}, |
|
45 |
- beforeUnmount() {}, |
|
88 |
+ created() { }, |
|
89 |
+ mounted() { }, |
|
90 |
+ beforeUnmount() { }, |
|
46 | 91 |
}; |
47 | 92 |
|
48 | 93 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- package-lock.json
+++ package-lock.json
... | ... | @@ -1,5 +1,5 @@ |
1 | 1 |
{ |
2 |
- "name": "node_vue_web_server_framework_v1.0-master", |
|
2 |
+ "name": "3D_AI_System", |
|
3 | 3 |
"lockfileVersion": 3, |
4 | 4 |
"requires": true, |
5 | 5 |
"packages": { |
... | ... | @@ -7,6 +7,7 @@ |
7 | 7 |
"dependencies": { |
8 | 8 |
"@babel/cli": "7.19.3", |
9 | 9 |
"@babel/core": "7.19.3", |
10 |
+ "axios": "^1.10.0", |
|
10 | 11 |
"babel-loader": "8.2.5", |
11 | 12 |
"css-loader": "6.7.1", |
12 | 13 |
"express": "4.18.1", |
... | ... | @@ -21,6 +22,8 @@ |
21 | 22 |
"vue-router": "4.1.5", |
22 | 23 |
"vue-style-loader": "4.1.3", |
23 | 24 |
"vue3-sfc-loader": "^0.8.4", |
25 |
+ "vuex": "^4.1.0", |
|
26 |
+ "vuex-persistedstate": "^4.1.0", |
|
24 | 27 |
"webpack": "5.74.0", |
25 | 28 |
"webpack-cli": "4.10.0" |
26 | 29 |
}, |
... | ... | @@ -1009,6 +1012,11 @@ |
1009 | 1012 |
"node": "*" |
1010 | 1013 |
} |
1011 | 1014 |
}, |
1015 |
+ "node_modules/asynckit": { |
|
1016 |
+ "version": "0.4.0", |
|
1017 |
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", |
|
1018 |
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" |
|
1019 |
+ }, |
|
1012 | 1020 |
"node_modules/autoprefixer": { |
1013 | 1021 |
"version": "10.4.21", |
1014 | 1022 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", |
... | ... | @@ -1045,6 +1053,16 @@ |
1045 | 1053 |
}, |
1046 | 1054 |
"peerDependencies": { |
1047 | 1055 |
"postcss": "^8.1.0" |
1056 |
+ } |
|
1057 |
+ }, |
|
1058 |
+ "node_modules/axios": { |
|
1059 |
+ "version": "1.10.0", |
|
1060 |
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", |
|
1061 |
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", |
|
1062 |
+ "dependencies": { |
|
1063 |
+ "follow-redirects": "^1.15.6", |
|
1064 |
+ "form-data": "^4.0.0", |
|
1065 |
+ "proxy-from-env": "^1.1.0" |
|
1048 | 1066 |
} |
1049 | 1067 |
}, |
1050 | 1068 |
"node_modules/babel-loader": { |
... | ... | @@ -1521,6 +1539,17 @@ |
1521 | 1539 |
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", |
1522 | 1540 |
"license": "MIT" |
1523 | 1541 |
}, |
1542 |
+ "node_modules/combined-stream": { |
|
1543 |
+ "version": "1.0.8", |
|
1544 |
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", |
|
1545 |
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", |
|
1546 |
+ "dependencies": { |
|
1547 |
+ "delayed-stream": "~1.0.0" |
|
1548 |
+ }, |
|
1549 |
+ "engines": { |
|
1550 |
+ "node": ">= 0.8" |
|
1551 |
+ } |
|
1552 |
+ }, |
|
1524 | 1553 |
"node_modules/commander": { |
1525 | 1554 |
"version": "4.1.1", |
1526 | 1555 |
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", |
... | ... | @@ -1722,6 +1751,22 @@ |
1722 | 1751 |
"node": ">=0.10.0" |
1723 | 1752 |
} |
1724 | 1753 |
}, |
1754 |
+ "node_modules/deepmerge": { |
|
1755 |
+ "version": "4.3.1", |
|
1756 |
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", |
|
1757 |
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", |
|
1758 |
+ "engines": { |
|
1759 |
+ "node": ">=0.10.0" |
|
1760 |
+ } |
|
1761 |
+ }, |
|
1762 |
+ "node_modules/delayed-stream": { |
|
1763 |
+ "version": "1.0.0", |
|
1764 |
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", |
|
1765 |
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", |
|
1766 |
+ "engines": { |
|
1767 |
+ "node": ">=0.4.0" |
|
1768 |
+ } |
|
1769 |
+ }, |
|
1725 | 1770 |
"node_modules/delegates": { |
1726 | 1771 |
"version": "1.0.0", |
1727 | 1772 |
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", |
... | ... | @@ -1919,6 +1964,20 @@ |
1919 | 1964 |
"license": "MIT", |
1920 | 1965 |
"dependencies": { |
1921 | 1966 |
"es-errors": "^1.3.0" |
1967 |
+ }, |
|
1968 |
+ "engines": { |
|
1969 |
+ "node": ">= 0.4" |
|
1970 |
+ } |
|
1971 |
+ }, |
|
1972 |
+ "node_modules/es-set-tostringtag": { |
|
1973 |
+ "version": "2.1.0", |
|
1974 |
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", |
|
1975 |
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", |
|
1976 |
+ "dependencies": { |
|
1977 |
+ "es-errors": "^1.3.0", |
|
1978 |
+ "get-intrinsic": "^1.2.6", |
|
1979 |
+ "has-tostringtag": "^1.0.2", |
|
1980 |
+ "hasown": "^2.0.2" |
|
1922 | 1981 |
}, |
1923 | 1982 |
"engines": { |
1924 | 1983 |
"node": ">= 0.4" |
... | ... | @@ -2267,6 +2326,40 @@ |
2267 | 2326 |
"flat": "cli.js" |
2268 | 2327 |
} |
2269 | 2328 |
}, |
2329 |
+ "node_modules/follow-redirects": { |
|
2330 |
+ "version": "1.15.9", |
|
2331 |
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", |
|
2332 |
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", |
|
2333 |
+ "funding": [ |
|
2334 |
+ { |
|
2335 |
+ "type": "individual", |
|
2336 |
+ "url": "https://github.com/sponsors/RubenVerborgh" |
|
2337 |
+ } |
|
2338 |
+ ], |
|
2339 |
+ "engines": { |
|
2340 |
+ "node": ">=4.0" |
|
2341 |
+ }, |
|
2342 |
+ "peerDependenciesMeta": { |
|
2343 |
+ "debug": { |
|
2344 |
+ "optional": true |
|
2345 |
+ } |
|
2346 |
+ } |
|
2347 |
+ }, |
|
2348 |
+ "node_modules/form-data": { |
|
2349 |
+ "version": "4.0.3", |
|
2350 |
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", |
|
2351 |
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", |
|
2352 |
+ "dependencies": { |
|
2353 |
+ "asynckit": "^0.4.0", |
|
2354 |
+ "combined-stream": "^1.0.8", |
|
2355 |
+ "es-set-tostringtag": "^2.1.0", |
|
2356 |
+ "hasown": "^2.0.2", |
|
2357 |
+ "mime-types": "^2.1.12" |
|
2358 |
+ }, |
|
2359 |
+ "engines": { |
|
2360 |
+ "node": ">= 6" |
|
2361 |
+ } |
|
2362 |
+ }, |
|
2270 | 2363 |
"node_modules/forwarded": { |
2271 | 2364 |
"version": "0.2.0", |
2272 | 2365 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", |
... | ... | @@ -2594,6 +2687,20 @@ |
2594 | 2687 |
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", |
2595 | 2688 |
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", |
2596 | 2689 |
"license": "MIT", |
2690 |
+ "engines": { |
|
2691 |
+ "node": ">= 0.4" |
|
2692 |
+ }, |
|
2693 |
+ "funding": { |
|
2694 |
+ "url": "https://github.com/sponsors/ljharb" |
|
2695 |
+ } |
|
2696 |
+ }, |
|
2697 |
+ "node_modules/has-tostringtag": { |
|
2698 |
+ "version": "1.0.2", |
|
2699 |
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", |
|
2700 |
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", |
|
2701 |
+ "dependencies": { |
|
2702 |
+ "has-symbols": "^1.0.3" |
|
2703 |
+ }, |
|
2597 | 2704 |
"engines": { |
2598 | 2705 |
"node": ">= 0.4" |
2599 | 2706 |
}, |
... | ... | @@ -4419,6 +4526,11 @@ |
4419 | 4526 |
"node": ">= 0.10" |
4420 | 4527 |
} |
4421 | 4528 |
}, |
4529 |
+ "node_modules/proxy-from-env": { |
|
4530 |
+ "version": "1.1.0", |
|
4531 |
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", |
|
4532 |
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" |
|
4533 |
+ }, |
|
4422 | 4534 |
"node_modules/punycode": { |
4423 | 4535 |
"version": "2.3.1", |
4424 | 4536 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", |
... | ... | @@ -4943,6 +5055,12 @@ |
4943 | 5055 |
"engines": { |
4944 | 5056 |
"node": ">=8" |
4945 | 5057 |
} |
5058 |
+ }, |
|
5059 |
+ "node_modules/shvl": { |
|
5060 |
+ "version": "2.0.3", |
|
5061 |
+ "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz", |
|
5062 |
+ "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==", |
|
5063 |
+ "deprecated": "older versions vulnerable to prototype pollution" |
|
4946 | 5064 |
}, |
4947 | 5065 |
"node_modules/side-channel": { |
4948 | 5066 |
"version": "1.1.0", |
... | ... | @@ -5789,6 +5907,30 @@ |
5789 | 5907 |
"integrity": "sha512-eziaIrk/N9f9OCpyFEkR6vMsZUHcF5mQslXjffwcb5Iq6EuU74QrlpBeJqA04MvAGT7f5O8la2v9k3NtQnJb3Q==", |
5790 | 5908 |
"license": "MIT" |
5791 | 5909 |
}, |
5910 |
+ "node_modules/vuex": { |
|
5911 |
+ "version": "4.1.0", |
|
5912 |
+ "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", |
|
5913 |
+ "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", |
|
5914 |
+ "dependencies": { |
|
5915 |
+ "@vue/devtools-api": "^6.0.0-beta.11" |
|
5916 |
+ }, |
|
5917 |
+ "peerDependencies": { |
|
5918 |
+ "vue": "^3.2.0" |
|
5919 |
+ } |
|
5920 |
+ }, |
|
5921 |
+ "node_modules/vuex-persistedstate": { |
|
5922 |
+ "version": "4.1.0", |
|
5923 |
+ "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz", |
|
5924 |
+ "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==", |
|
5925 |
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", |
|
5926 |
+ "dependencies": { |
|
5927 |
+ "deepmerge": "^4.2.2", |
|
5928 |
+ "shvl": "^2.0.3" |
|
5929 |
+ }, |
|
5930 |
+ "peerDependencies": { |
|
5931 |
+ "vuex": "^3.0 || ^4.0.0-rc" |
|
5932 |
+ } |
|
5933 |
+ }, |
|
5792 | 5934 |
"node_modules/watchpack": { |
5793 | 5935 |
"version": "2.4.2", |
5794 | 5936 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", |
--- package.json
+++ package.json
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 |
"dependencies": { |
3 | 3 |
"@babel/cli": "7.19.3", |
4 | 4 |
"@babel/core": "7.19.3", |
5 |
+ "axios": "^1.10.0", |
|
5 | 6 |
"babel-loader": "8.2.5", |
6 | 7 |
"css-loader": "6.7.1", |
7 | 8 |
"express": "4.18.1", |
... | ... | @@ -16,6 +17,8 @@ |
16 | 17 |
"vue-router": "4.1.5", |
17 | 18 |
"vue-style-loader": "4.1.3", |
18 | 19 |
"vue3-sfc-loader": "^0.8.4", |
20 |
+ "vuex": "^4.1.0", |
|
21 |
+ "vuex-persistedstate": "^4.1.0", |
|
19 | 22 |
"webpack": "5.74.0", |
20 | 23 |
"webpack-cli": "4.10.0" |
21 | 24 |
}, |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?