
2025-03-21 하관우 토큰 재발급 로그인 수정 내정보 수정 App.vue 이미지 클릭시 상위로 이동동
@d719c9732ecc4fb4c43562765e0e8074fb662ba3
--- client/resources/api/index.js
+++ client/resources/api/index.js
... | ... | @@ -1,149 +1,61 @@ |
1 |
-import axios from "axios"; |
|
1 |
+import axios from 'axios'; |
|
2 | 2 |
import store from "../../views/pages/AppStore"; |
3 | 3 |
|
4 | 4 |
const apiClient = axios.create({ |
5 |
- headers: { 'Content-Type': 'application/json; charset=UTF-8' } |
|
5 |
+ headers: { |
|
6 |
+ 'Content-Type': 'application/json; charset=UTF-8', |
|
7 |
+ } |
|
6 | 8 |
}); |
7 |
- |
|
8 |
-// 동시에 여러 요청이 401 에러가 발생했을 때 관리하기 위한 변수 |
|
9 |
-let isRefreshing = false; |
|
10 |
-let failedQueue = []; |
|
11 |
- |
|
12 |
-const processQueue = (error, token = null) => { |
|
13 |
- failedQueue.forEach(prom => { |
|
14 |
- if (error) { |
|
15 |
- prom.reject(error); |
|
16 |
- } else { |
|
17 |
- prom.resolve(token); |
|
18 |
- } |
|
19 |
- }); |
|
20 |
- |
|
21 |
- failedQueue = []; |
|
22 |
-}; |
|
23 | 9 |
|
24 | 10 |
apiClient.interceptors.request.use( |
25 | 11 |
config => { |
26 |
- // Bearer 접두사 추가 (API 요구사항에 따라 필요한 경우) |
|
27 |
- if (store.state.authorization) { |
|
28 |
- config.headers.Authorization = store.state.authorization; |
|
29 |
- } |
|
12 |
+ config.headers.Authorization = store.state.authorization; // 요청 시 AccessToken 추가 |
|
30 | 13 |
return config; |
31 | 14 |
}, |
32 | 15 |
error => { |
33 | 16 |
return Promise.reject(error); |
34 | 17 |
} |
35 |
-); |
|
36 |
- |
|
37 |
-async function refreshAccessToken() { |
|
38 |
- try { |
|
39 |
- // 리프레시 토큰 존재 여부 확인 |
|
40 |
- if (!store.state.refreshToken) { |
|
41 |
- throw new Error('리프레시 토큰이 없습니다.'); |
|
42 |
- } |
|
43 |
- |
|
44 |
- console.log('토큰 재발급 시도:', new Date().toISOString()); |
|
45 |
- |
|
46 |
- // 서버의 구현에 맞게 요청 형식 수정 |
|
47 |
- // 백엔드는 요청 바디가 아닌 쿠키에서 refreshToken을 읽으므로 |
|
48 |
- // 빈 객체로 보내고 withCredentials를 true로 설정 |
|
49 |
- const res = await axios.post('/refresh/tknReissue.json', {}, { |
|
50 |
- withCredentials: true, |
|
51 |
- timeout: 10000 // 10초 타임아웃 설정 |
|
52 |
- }); |
|
53 |
- |
|
54 |
- console.log('토큰 재발급 응답:', res.status); |
|
55 |
- |
|
56 |
- // 백엔드는 응답 헤더에 Authorization으로 새 토큰을 보냄 |
|
57 |
- const newToken = res.headers['authorization']; |
|
58 |
- |
|
59 |
- if (newToken) { |
|
60 |
- // 새 토큰을 스토어에 저장 |
|
61 |
- store.commit('setAuthorization', newToken); |
|
62 |
- return newToken; |
|
63 |
- } else { |
|
64 |
- console.error('응답 헤더에 토큰이 없습니다:', res.headers); |
|
65 |
- throw new Error('토큰이 포함되어 있지 않습니다.'); |
|
66 |
- } |
|
67 |
- } catch (error) { |
|
68 |
- console.error('토큰 재발급 오류:', error); |
|
69 |
- |
|
70 |
- // 네트워크나 서버 오류 구분 |
|
71 |
- if (!navigator.onLine) { |
|
72 |
- alert('네트워크 연결을 확인해주세요.'); |
|
73 |
- } else if (error.response && (error.response.status === 401 || error.response.status === 403)) { |
|
74 |
- alert('세션이 만료되었습니다. 다시 로그인해 주세요.'); |
|
75 |
- store.commit("setStoreReset"); |
|
76 |
- window.location = '/login.page'; |
|
77 |
- } else { |
|
78 |
- alert('토큰 재발급에 실패했습니다. 다시 로그인해 주세요.'); |
|
79 |
- store.commit("setStoreReset"); |
|
80 |
- window.location = '/login.page'; |
|
81 |
- } |
|
82 |
- |
|
83 |
- throw error; |
|
84 |
- } |
|
85 |
-} |
|
18 |
+) |
|
86 | 19 |
|
87 | 20 |
apiClient.interceptors.response.use( |
88 | 21 |
response => { |
89 | 22 |
return response; |
90 | 23 |
}, |
91 | 24 |
async error => { |
92 |
- const originalReq = error.config; |
|
93 |
- |
|
94 |
- // 응답이 없는 경우(네트워크 오류) 처리 |
|
95 |
- if (!error.response) { |
|
96 |
- console.error('네트워크 오류:', error); |
|
97 |
- return Promise.reject(error); |
|
98 |
- } |
|
99 |
- |
|
100 |
- // 403 오류 처리: 접근 권한 없음 |
|
101 |
- if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') { |
|
25 |
+ if (error.response.status == 403 && error.response.data.message == '접근 권한이 없습니다.') { |
|
102 | 26 |
window.history.back(); |
103 |
- return Promise.reject(error); |
|
104 | 27 |
} |
105 |
- |
|
106 |
- // 401 오류 처리: 토큰 만료 |
|
107 |
- if (error.response.status === 401 && !originalReq._retry) { |
|
108 |
- originalReq._retry = true; |
|
109 |
- |
|
110 |
- // 이미 토큰 재발급 중인 경우 대기열에 추가 |
|
111 |
- if (isRefreshing) { |
|
112 |
- return new Promise((resolve, reject) => { |
|
113 |
- failedQueue.push({ resolve, reject }); |
|
114 |
- }).then(token => { |
|
115 |
- originalReq.headers['Authorization'] = token; |
|
116 |
- return apiClient(originalReq); |
|
117 |
- }).catch(err => { |
|
118 |
- return Promise.reject(err); |
|
119 |
- }); |
|
120 |
- } |
|
121 |
- |
|
122 |
- isRefreshing = true; |
|
123 |
- |
|
28 |
+ const originalReq = error.config; |
|
29 |
+ // 토큰의 만료기간이 끝난경우 |
|
30 |
+ if (error.response.status == 401 && error.response.data.message == 'Token expired' && !originalReq._retry) { |
|
31 |
+ originalReq._retry = true; // 재요청 시도(한번만 실행) |
|
124 | 32 |
try { |
125 |
- // 액세스 토큰 재발급 요청 |
|
126 |
- const newToken = await refreshAccessToken(); |
|
127 |
- |
|
128 |
- // 성공적으로 토큰을 재발급받은 경우 |
|
129 |
- originalReq.headers['Authorization'] = newToken; |
|
130 |
- |
|
131 |
- // 대기 중인 요청 처리 |
|
132 |
- processQueue(null, newToken); |
|
133 |
- |
|
134 |
- // 재요청 |
|
135 |
- return apiClient(originalReq); |
|
33 |
+ const res = await axios.post('/refresh/tknReissue.json', {}); |
|
34 |
+ store.commit('setAuthorization', res.headers.authorization); // 새로 발급 받은 AccessToken 저장 |
|
35 |
+ originalReq.headers.Authorization = store.state.authorization; // 새로 발급 받은 AccessToken을 기존 요청에 추가 |
|
36 |
+ /** jwt토큰 디코딩 **/ |
|
37 |
+ const base64String = store.state.authorization.split('.')[1]; |
|
38 |
+ const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/'); |
|
39 |
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { |
|
40 |
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
|
41 |
+ }).join('')); |
|
42 |
+ const mbr = JSON.parse(jsonPayload); |
|
43 |
+ // const mbr = JSON.parse(decodeURIComponent(escape(window.atob(base64String)))); // jwt claim 추출 |
|
44 |
+ store.commit("setUserNm", mbr.userNm); |
|
45 |
+ store.commit('setRoles', mbr.roles); |
|
46 |
+ /** jwt토큰 디코딩 **/ |
|
47 |
+ return apiClient(originalReq); // 원래 요청 재시도 /pathname + search |
|
136 | 48 |
} catch (refreshError) { |
137 |
- // 대기 중인 모든 요청에 오류 전파 |
|
138 |
- processQueue(refreshError, null); |
|
49 |
+ const redirect = window.location.pathname + window.location.search; |
|
50 |
+ sessionStorage.setItem("redirect", redirect); |
|
51 |
+ alert('세션이 종료되었습니다.\n로그인을 새로 해주세요.'); |
|
52 |
+ store.commit("setStoreReset"); |
|
53 |
+ window.location = '/login.page'; |
|
139 | 54 |
return Promise.reject(refreshError); |
140 |
- } finally { |
|
141 |
- isRefreshing = false; |
|
142 | 55 |
} |
143 | 56 |
} |
144 |
- |
|
145 | 57 |
return Promise.reject(error); |
146 | 58 |
} |
147 |
-); |
|
59 |
+) |
|
148 | 60 |
|
149 | 61 |
export default apiClient;(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/logOut.js
... | ... | @@ -1,10 +0,0 @@ |
1 | -import apiClient from "./index"; | |
2 | -import store from '../../views/pages/AppStore'; | |
3 | - | |
4 | -export const logOutProc = () => { | |
5 | - return apiClient.post(`/user/logout.json`, {}, { | |
6 | - headers: { | |
7 | - 'refresh': store.state.refresh | |
8 | - } | |
9 | - }); | |
10 | -}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/login.js
... | ... | @@ -1,5 +0,0 @@ |
1 | -import apiClient from "./index"; | |
2 | - | |
3 | -export const loginProc = mber => { | |
4 | - return apiClient.post(`/user/login.json`, mber); | |
5 | -}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/user.js
... | ... | @@ -0,0 +1,31 @@ |
1 | +import apiClient from "./index"; | |
2 | +import store from '../../views/pages/AppStore'; | |
3 | + | |
4 | +// 로그인 | |
5 | +export const loginProc = mber => { | |
6 | + return apiClient.post(`/user/login.json`, mber); | |
7 | +} | |
8 | + | |
9 | +// 로그아웃 | |
10 | +export const logOutProc = () => { | |
11 | + return apiClient.post(`/user/logout.json`, {}, { | |
12 | + headers: { | |
13 | + 'refresh': store.state.refresh | |
14 | + } | |
15 | + }); | |
16 | +} | |
17 | + | |
18 | +// 아이디로 유저 찾기 | |
19 | +export const findByIdProc = userId => { | |
20 | + return apiClient.get(`/user/${userId}/users.json`); | |
21 | +} | |
22 | + | |
23 | +// 유저 정보 변경 | |
24 | +export const updateUsers = (userId, updateUserDTO) => { | |
25 | + return apiClient.put(`/user/${userId}/users.json`, updateUserDTO); | |
26 | +} | |
27 | + | |
28 | +// 유저 비밀번호 변경 | |
29 | +export const updatePassword = (userId, passwordDTO) => { | |
30 | + return apiClient.put(`/user/${userId}/updatePassword.json`, passwordDTO); | |
31 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/App.vue
+++ client/views/App.vue
... | ... | @@ -1,9 +1,10 @@ |
1 |
+App.vue |
|
1 | 2 |
<template> |
2 | 3 |
<div class="wrapper"> |
3 | 4 |
<Header /> |
4 | 5 |
<div class="container "><router-view /></div> |
5 | 6 |
<Footer /> |
6 |
- <button class="scroll-up"> |
|
7 |
+ <button class="scroll-up" @click="scrollToTop"> |
|
7 | 8 |
<img src="../resources/images/icon/top.png" alt=""> |
8 | 9 |
</button> |
9 | 10 |
</div> |
... | ... | @@ -17,7 +18,15 @@ |
17 | 18 |
components: { |
18 | 19 |
Header: Header, |
19 | 20 |
Footer: Footer, |
20 |
- } |
|
21 |
+ }, |
|
22 |
+ methods: { |
|
23 |
+ scrollToTop() { |
|
24 |
+ window.scrollTo({ |
|
25 |
+ top: 0, |
|
26 |
+ behavior: 'smooth' // 부드러운 스크롤 효과 |
|
27 |
+ }); |
|
28 |
+ }, |
|
29 |
+ }, |
|
21 | 30 |
} |
22 | 31 |
</script> |
23 | 32 |
<style></style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/layout/Header.vue
+++ client/views/layout/Header.vue
... | ... | @@ -8,10 +8,13 @@ |
8 | 8 |
<div class="nav-wrap"> |
9 | 9 |
<nav> |
10 | 10 |
<ul> |
11 |
- <li>기록물</li> |
|
12 |
- <li>언론에서 바라본 구미시</li> |
|
13 |
- <li>회원관리</li> |
|
14 |
- <li>카테고리 관리</li> |
|
11 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_ADMIN'">기록물</li> |
|
12 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_ADMIN'">언론에서 바라본 구미시</li> |
|
13 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_ADMIN'">회원관리</li> |
|
14 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_ADMIN'">카테고리 관리</li> |
|
15 |
+ |
|
16 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_USER'">기록물</li> |
|
17 |
+ <li v-if="$store.state.roles[0]?.authority === 'ROLE_USER'">언론에서 바라본 구미시</li> |
|
15 | 18 |
</ul> |
16 | 19 |
</nav> |
17 | 20 |
</div> |
... | ... | @@ -33,6 +36,7 @@ |
33 | 36 |
</header> |
34 | 37 |
</template> |
35 | 38 |
<script> |
39 |
+import { logOutProc } from "../../resources/api/user" |
|
36 | 40 |
export default { |
37 | 41 |
data() { |
38 | 42 |
return { |
... | ... | @@ -42,9 +46,18 @@ |
42 | 46 |
}, |
43 | 47 |
methods: { |
44 | 48 |
logout() { |
45 |
- this.$store.commit('setStoreReset'); // 로그아웃 뮤테이션 호출 |
|
46 |
- this.isUserNm = null; // 사용자 이름 초기화 |
|
47 |
- this.$router.push({ path: '/Login.page' }); // 로그인 페이지로 리다이렉트 |
|
49 |
+ // 백엔드 로그아웃 API 호출 |
|
50 |
+ logOutProc() |
|
51 |
+ .then(() => { |
|
52 |
+ console.log('로그아웃 성공 - 서버 측 쿠키 삭제 완료'); |
|
53 |
+ this.$store.commit('setStoreReset'); // 로그아웃 성공 후 스토어 초기화 |
|
54 |
+ this.$router.push({ path: '/Login.page' }); // 로그인 페이지로 리다이렉트 |
|
55 |
+ }) |
|
56 |
+ .catch(err => { |
|
57 |
+ console.error('로그아웃 처리 중 오류:', err); |
|
58 |
+ this.$store.commit('setStoreReset'); // 오류가 있어도 스토어는 초기화 |
|
59 |
+ this.$router.push({ path: '/Login.page' }); // 로그인 페이지로 리다이렉트 |
|
60 |
+ }); |
|
48 | 61 |
} |
49 | 62 |
}, |
50 | 63 |
watch: {}, |
--- client/views/pages/AppStore.js
+++ client/views/pages/AppStore.js
... | ... | @@ -1,42 +1,57 @@ |
1 | 1 |
import { createStore } from "vuex"; |
2 | 2 |
import createPersistedState from "vuex-persistedstate"; |
3 |
-import { logOutProc } from "../../resources/api/logOut"; |
|
3 |
+import { logOutProc } from "../../resources/api/user"; |
|
4 | 4 |
|
5 | 5 |
export default createStore({ |
6 | 6 |
plugins: [createPersistedState()], |
7 | 7 |
state: { |
8 | 8 |
authorization: null, |
9 |
- // refresh: null, |
|
10 |
- roles: [], |
|
9 |
+ refresh: null, // 리프레시 토큰을 추가 |
|
10 |
+ userId: null, // 사용자 ID 추가 |
|
11 |
+ loginId: null, // 로그인 ID 추가 |
|
12 |
+ userNm: null, // 사용자 이름 추가 |
|
13 |
+ roles: [], // 사용자 권한 |
|
11 | 14 |
}, |
12 | 15 |
getters: { |
13 |
- getAuthorization: function () {}, |
|
14 |
- // getRefresh: function () {}, |
|
15 |
- getUserNm: function () {}, |
|
16 |
- getRoles: function () {}, |
|
16 |
+ getAuthorization(state) { |
|
17 |
+ return state.authorization; // authorization 반환 |
|
18 |
+ }, |
|
19 |
+ getRefresh(state) { |
|
20 |
+ return state.refresh; // 리프레시 토큰 반환 |
|
21 |
+ }, |
|
22 |
+ getUserNm(state) { |
|
23 |
+ return state.userNm; // 사용자 이름 반환 |
|
24 |
+ }, |
|
25 |
+ getRoles(state) { |
|
26 |
+ return state.roles; // 사용자 권한 반환 |
|
27 |
+ }, |
|
17 | 28 |
}, |
18 | 29 |
mutations: { |
19 | 30 |
setAuthorization(state, newValue) { |
20 |
- state.authorization = newValue; |
|
31 |
+ state.authorization = newValue; // authorization 설정 |
|
21 | 32 |
}, |
22 |
- // setRefresh(state, newValue) { |
|
23 |
- // state.refresh = newValue; |
|
24 |
- // }, |
|
33 |
+ setRefresh(state, newValue) { |
|
34 |
+ state.refresh = newValue; // 리프레시 토큰 설정 |
|
35 |
+ }, |
|
36 |
+ setLoginId(state, newValue) { |
|
37 |
+ state.loginId = newValue; // 로그인 ID 설정 |
|
38 |
+ }, |
|
25 | 39 |
setUserNm(state, newValue) { |
26 |
- state.userNm = newValue; |
|
40 |
+ state.userNm = newValue; // 사용자 이름 설정 |
|
27 | 41 |
}, |
28 | 42 |
setUserId(state, newValue) { |
29 |
- state.userId = newValue; |
|
43 |
+ state.userId = newValue; // 사용자 ID 설정 |
|
30 | 44 |
}, |
31 | 45 |
setRoles(state, newValue) { |
32 |
- state.roles = newValue; |
|
46 |
+ state.roles = newValue; // 사용자 권한 설정 |
|
33 | 47 |
}, |
34 | 48 |
setStoreReset(state) { |
35 | 49 |
state.authorization = null; |
36 |
- // state.refresh = null; |
|
50 |
+ state.refresh = null; // 리프레시 토큰 초기화 |
|
37 | 51 |
state.userNm = null; |
38 | 52 |
state.userId = null; |
39 | 53 |
state.roles = []; |
54 |
+ state.loginId = null; |
|
40 | 55 |
}, |
41 | 56 |
}, |
42 | 57 |
actions: { |
... | ... | @@ -44,20 +59,21 @@ |
44 | 59 |
try { |
45 | 60 |
const res = await logOutProc(); |
46 | 61 |
alert(res.data.message); |
47 |
- if (res.status == 200) { |
|
62 |
+ if (res.status === 200) { |
|
48 | 63 |
commit("setStoreReset"); |
49 | 64 |
} |
50 |
- } catch(error) { |
|
65 |
+ } catch (error) { |
|
66 |
+ console.error("Logout error:", error); // 에러 로그 추가 |
|
51 | 67 |
const errorData = error.response.data; |
52 |
- if (errorData.message != null && errorData.message != "") { |
|
53 |
- alert(error.response.data.message); |
|
68 |
+ if (errorData.message) { |
|
69 |
+ alert(errorData.message); |
|
54 | 70 |
} else { |
55 | 71 |
alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
56 | 72 |
} |
57 | 73 |
} |
58 | 74 |
}, |
59 |
- setStoreReset({commit}) { |
|
75 |
+ setStoreReset({ commit }) { |
|
60 | 76 |
commit("setStoreReset"); |
61 |
- } |
|
77 |
+ }, |
|
62 | 78 |
}, |
63 |
-});(파일 끝에 줄바꿈 문자 없음) |
|
79 |
+}); |
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 |
<div class="breadcrumb-list"> |
6 | 6 |
<ul> |
7 | 7 |
<!-- Bind the image source dynamically for homeicon --> |
8 |
- <li><img :src="homeicon" alt="Home Icon"><p></p></li> |
|
8 |
+ <li><img :src="homeicon" alt="Home Icon"></li> |
|
9 | 9 |
<li><img :src="righticon" alt="Home Icon"></li> |
10 | 10 |
<li>로그인</li> |
11 | 11 |
</ul> |
... | ... | @@ -36,7 +36,6 @@ |
36 | 36 |
|
37 | 37 |
<script> |
38 | 38 |
import { useStore } from "vuex"; |
39 |
-import { loginProc } from "../../../resources/api/login"; |
|
40 | 39 |
import axios from "axios"; |
41 | 40 |
|
42 | 41 |
export default { |
... | ... | @@ -66,35 +65,37 @@ |
66 | 65 |
console.log(response); // 응답 확인 |
67 | 66 |
|
68 | 67 |
if (response.status === 200) { |
69 |
- // 토큰 저장 로직 |
|
70 |
- this.$store.commit("setAuthorization", response.headers.authorization); |
|
71 |
- /** jwt토큰 복호화 **/ |
|
72 |
- const base64String = this.$store.state.authorization.split(".")[1]; |
|
73 |
- const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/"); |
|
74 |
- const jsonPayload = decodeURIComponent( |
|
75 |
- atob(base64) |
|
76 |
- .split("") |
|
77 |
- .map((c) => { |
|
78 |
- return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
|
79 |
- }) |
|
80 |
- .join("") |
|
81 |
- ); |
|
82 |
- const mbr = JSON.parse(jsonPayload); |
|
83 |
- //const mbr = JSON.parse(decodeURIComponent(escape(window.atob(base64String)))); // jwt claim 추출 |
|
84 |
- console.log("리멤버미~", mbr); |
|
85 |
- this.$store.commit("setUserId", mbr.userId); |
|
86 |
- this.$store.commit("setUserNm", mbr.userNm); |
|
87 |
- this.$store.commit("setRoles", mbr.roles); |
|
88 |
- /** jwt토큰 복호화 **/ |
|
68 |
+ const newToken = response.headers.authorization; |
|
69 |
+ this.store.commit("setAuthorization", newToken); |
|
70 |
+ |
|
71 |
+ // JWT 디코딩 |
|
72 |
+ const mbr = this.decodeJwt(newToken); |
|
73 |
+ if (mbr) { |
|
74 |
+ this.store.commit("setUserId", mbr.userId); |
|
75 |
+ this.store.commit("setLoginId", mbr.loginId); |
|
76 |
+ this.store.commit("setUserNm", mbr.userNm); |
|
77 |
+ this.store.commit("setRoles", mbr.roles); |
|
78 |
+ } |
|
89 | 79 |
|
90 | 80 |
// 리다이렉트 처리 |
91 | 81 |
this.$router.push("/"); |
92 | 82 |
} |
93 | 83 |
} catch (error) { |
94 |
- console.error("Login error:", error); // 에러 로그 |
|
84 |
+ console.error("Login error:", error); |
|
95 | 85 |
const message = error.response?.data?.message || "로그인에 실패했습니다."; |
96 | 86 |
alert(message); |
97 | 87 |
} |
88 |
+ }, |
|
89 |
+ decodeJwt(token) { |
|
90 |
+ const base64String = token.split(".")[1]; |
|
91 |
+ const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/"); |
|
92 |
+ const jsonPayload = decodeURIComponent( |
|
93 |
+ atob(base64) |
|
94 |
+ .split("") |
|
95 |
+ .map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) |
|
96 |
+ .join("") |
|
97 |
+ ); |
|
98 |
+ return JSON.parse(jsonPayload); |
|
98 | 99 |
} |
99 | 100 |
}, |
100 | 101 |
watch: {}, |
--- client/views/pages/user/MyInfo.vue
+++ client/views/pages/user/MyInfo.vue
... | ... | @@ -1,69 +1,209 @@ |
1 | 1 |
<template> |
2 | 2 |
<div class="content"> |
3 |
- <div class="sub-title-area mb-110"> |
|
3 |
+ <div class="sub-title-area mb-110"> |
|
4 | 4 |
<h2>내 정보 수정</h2> |
5 | 5 |
<div class="breadcrumb-list"> |
6 | 6 |
<ul> |
7 |
- <li><img :src="homeicon" alt="Home Icon"><p></p></li> |
|
7 |
+ <li><img :src="homeicon" alt="Home Icon"></li> |
|
8 | 8 |
<li><img :src="righticon" alt="Home Icon"></li> |
9 | 9 |
<li>내 정보 수정</li> |
10 | 10 |
</ul> |
11 | 11 |
</div> |
12 |
- </div> |
|
13 |
- <h3>기본정보</h3> |
|
12 |
+ </div> |
|
13 |
+ <h3>기본정보</h3> |
|
14 | 14 |
<form action="" class="info-form mb-50"> |
15 | 15 |
<dl> |
16 |
- <dd > |
|
16 |
+ <dd> |
|
17 | 17 |
<label for="id" class="require">아이디</label> |
18 |
- <input type="text" id="id" value="admin" readonly> |
|
18 |
+ <input type="text" id="id" v-model="$store.state.loginId" readonly> |
|
19 | 19 |
</dd> |
20 | 20 |
<div class="hr"></div> |
21 |
- |
|
22 |
- <dd > |
|
21 |
+ |
|
22 |
+ <dd> |
|
23 | 23 |
<label for="name" class="require">이름</label> |
24 |
- <input type="text" id="name" value="관리자"> |
|
25 |
- |
|
24 |
+ <input type="text" id="name" v-model="userInfo.userNm"> |
|
25 |
+ |
|
26 | 26 |
</dd> |
27 |
- |
|
27 |
+ |
|
28 | 28 |
</dl> |
29 | 29 |
</form> |
30 | 30 |
<h3>비밀번호 변경</h3> |
31 | 31 |
<form action="" class="pwchange-form mb-50"> |
32 | 32 |
<dl> |
33 |
- <dd > |
|
34 |
- <label for="id">아이디</label> |
|
35 |
- <input type="text" id="id" placeholder="아이디를 입력하세요."> |
|
33 |
+ <dd> |
|
34 |
+ <label for="id">현재 비밀번호</label> |
|
35 |
+ <input type="password" id="id" placeholder="비밀번호를 입력하세요." v-model="userPassword.oldPassword"> |
|
36 | 36 |
</dd> |
37 | 37 |
<div class="hr"></div> |
38 |
- <dd > |
|
39 |
- <label for="pw">비밀번호</label> |
|
40 |
- <input type="text" id="pw" placeholder="비밀번호를 입력하세요."> |
|
41 |
- <div class="invalid-feedback"><img :src="erroricon" alt=""><span>영문, 숫자, 특수문자를 최소 한가지씩 조합하고 8자 이상 ~ 20자 이내로 입력해주세요.</span></div> |
|
38 |
+ <dd> |
|
39 |
+ <label for="pw">새 비밀번호</label> |
|
40 |
+ <input type="password" id="pw" placeholder="새 비밀번호를 입력하세요." v-model="userPassword.newPassword" |
|
41 |
+ @input="validatePassword"> |
|
42 |
+ <div class="invalid-feedback" v-if="!isPasswordValid && userPassword.newPassword !== null"> |
|
43 |
+ <img :src="erroricon" alt=""> |
|
44 |
+ <span>영문, 숫자, 특수문자를 최소 한 가지씩 조합하고 8자 이상 ~ 20자 이내로 입력해주세요.</span> |
|
45 |
+ </div> |
|
42 | 46 |
</dd> |
43 | 47 |
<div class="hr"></div> |
44 |
- <dd > |
|
45 |
- <label for="pw">비밀번호</label> |
|
46 |
- <input type="text" id="pw" placeholder="비밀번호를 입력하세요."> |
|
47 |
- |
|
48 |
+ <dd> |
|
49 |
+ <label for="pwCheck">새 비밀번호 확인</label> |
|
50 |
+ <input type="password" id="pwCheck" placeholder="새 비밀번호를 입력하세요." v-model="newPasswordCheck"> |
|
51 |
+ <div class="invalid-feedback" v-if="newPasswordCheck !== null && !passwordsMatch"> |
|
52 |
+ <img :src="erroricon" alt=""><span>비밀번호가 일치하지 않습니다.</span> |
|
53 |
+ </div> |
|
48 | 54 |
</dd> |
49 | 55 |
</dl> |
50 | 56 |
</form> |
51 | 57 |
<div class="btn-group flex-end"> |
52 |
- <button class="signout">회원탈퇴</button> |
|
53 |
- <button class="update">수정</button> |
|
58 |
+ <button class="signout" type="button" @click="fnDeleteUser">회원탈퇴</button> |
|
59 |
+ <button class="update" type="button" @click="fnUpdateUser">수정</button> |
|
54 | 60 |
</div> |
55 | 61 |
</div> |
56 | 62 |
</template> |
57 | 63 |
|
58 | 64 |
<script> |
65 |
+import axios from "axios"; |
|
66 |
+import { updateUsers, logOutProc, updatePassword } from "../../../resources/api/user" |
|
59 | 67 |
export default { |
60 |
- data() { |
|
61 |
- return { |
|
62 |
- // Define the image sources |
|
63 |
- homeicon: 'client/resources/images/icon/home.png', |
|
64 |
- erroricon: 'client/resources/images/icon/error.png', |
|
65 |
- righticon: 'client/resources/images/icon/right.png', |
|
66 |
- }; |
|
67 |
- } |
|
68 |
+ data() { |
|
69 |
+ return { |
|
70 |
+ // Define the image sources |
|
71 |
+ homeicon: 'client/resources/images/icon/home.png', |
|
72 |
+ erroricon: 'client/resources/images/icon/error.png', |
|
73 |
+ righticon: 'client/resources/images/icon/right.png', |
|
74 |
+ |
|
75 |
+ userInfo: { |
|
76 |
+ userNm: this.$store.state.userNm, |
|
77 |
+ userSttus: "1", |
|
78 |
+ useAt: "Y" |
|
79 |
+ }, |
|
80 |
+ |
|
81 |
+ userPassword: { |
|
82 |
+ oldPassword: null, |
|
83 |
+ newPassword: null |
|
84 |
+ }, |
|
85 |
+ newPasswordCheck: null, |
|
86 |
+ isPasswordValid: false // 비밀번호 유효성 체크를 위한 변수 |
|
87 |
+ |
|
88 |
+ }; |
|
89 |
+ }, |
|
90 |
+ methods: { |
|
91 |
+ resetForm() { |
|
92 |
+ // 사용자 정보 초기화 |
|
93 |
+ this.userPassword = { |
|
94 |
+ oldPassword: null, |
|
95 |
+ newPassword: null, |
|
96 |
+ }; |
|
97 |
+ this.newPasswordCheck = null; // 비밀번호 확인 초기화 |
|
98 |
+ }, |
|
99 |
+ validatePassword() { |
|
100 |
+ // 빈 문자열이나 null 체크 |
|
101 |
+ if (!this.userPassword.newPassword) { |
|
102 |
+ this.isPasswordValid = false; // 빈 문자열일 경우 유효성 false |
|
103 |
+ return; |
|
104 |
+ } |
|
105 |
+ |
|
106 |
+ // 정규식: 영문, 숫자, 특수문자를 최소 한 가지씩 조합하고 8자 이상 20자 이내 |
|
107 |
+ const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/; |
|
108 |
+ this.isPasswordValid = passwordRegex.test(this.userPassword.newPassword); |
|
109 |
+ }, |
|
110 |
+ async fnDeleteUser() { |
|
111 |
+ try { |
|
112 |
+ this.userInfo.userSttus = "0"; |
|
113 |
+ this.userInfo.useAt = "N"; |
|
114 |
+ // 사용자 정보를 업데이트하는 API 호출 |
|
115 |
+ const response = await updateUsers(this.$store.state.userId, this.userInfo); |
|
116 |
+ |
|
117 |
+ console.log(response); // 응답 확인 |
|
118 |
+ |
|
119 |
+ if (response.status === 200) { |
|
120 |
+ logOutProc() |
|
121 |
+ .then(() => { |
|
122 |
+ console.log('로그아웃 성공 - 서버 측 쿠키 삭제 완료'); |
|
123 |
+ this.$store.commit('setStoreReset'); // 로그아웃 성공 후 스토어 초기화 |
|
124 |
+ this.$router.push({ path: '/Login.page' }); // 로그인 페이지로 리다이렉트 |
|
125 |
+ }) |
|
126 |
+ .catch(err => { |
|
127 |
+ console.error('로그아웃 처리 중 오류:', err); |
|
128 |
+ this.$store.commit('setStoreReset'); // 오류가 있어도 스토어는 초기화 |
|
129 |
+ this.$router.push({ path: '/Login.page' }); // 로그인 페이지로 리다이렉트 |
|
130 |
+ }); |
|
131 |
+ } |
|
132 |
+ } catch (error) { |
|
133 |
+ console.error("User error:", error); // 에러 로그 |
|
134 |
+ const message = error.response?.data?.message || "회원탈퇴퇴에 실패했습니다."; |
|
135 |
+ alert(message); |
|
136 |
+ } |
|
137 |
+ }, |
|
138 |
+ //사용자 이름 벨류데이션 체크 |
|
139 |
+ isValidationUser() { |
|
140 |
+ return this.userInfo.userNm == null || this.userInfo.userNm == ''; |
|
141 |
+ }, |
|
142 |
+ //사용자 비밀번호 벨류데이션 체크 |
|
143 |
+ isValidationPasswdord() { |
|
144 |
+ return this.userPassword.oldPassword == null || this.userPassword.oldPassword == '' || this.userPassword.newPassword == null || this.userPassword.newPassword == '' || this.newPasswordCheck == null || this.newPasswordCheck == '' |
|
145 |
+ }, |
|
146 |
+ async fnUpdateUser() { |
|
147 |
+ if (this.isValidationUser()) { |
|
148 |
+ if (this.isValidationPasswdord()) { |
|
149 |
+ alert("수정 할 내용이 없습니다."); |
|
150 |
+ } else { |
|
151 |
+ if (confirm("비밀번호 변경을 하시겠습니까?")) { |
|
152 |
+ const response = await updatePassword(this.$store.state.userId, this.userPassword); |
|
153 |
+ if (response.status == 200) { |
|
154 |
+ this.resetForm(); |
|
155 |
+ alert("비밀번호가 변경되었습니다."); |
|
156 |
+ window.location.reload(); // 페이지 새로 고침 |
|
157 |
+ } |
|
158 |
+ } |
|
159 |
+ } |
|
160 |
+ } else { |
|
161 |
+ if (this.$store.state.userNm !== this.userInfo.userNm) { |
|
162 |
+ if (this.isValidationPasswdord()) { |
|
163 |
+ if (confirm("회원정보를 변경을 하시겠습니까?")) { |
|
164 |
+ const response = await updateUsers(this.$store.state.userId, this.userInfo); |
|
165 |
+ if (response.status == 200) { |
|
166 |
+ this.$store.commit("setUserNm", this.userInfo.userNm); |
|
167 |
+ this.resetForm(); |
|
168 |
+ alert("회원정보가 변경되었습니다."); |
|
169 |
+ window.location.reload(); // 페이지 새로 고침 |
|
170 |
+ } |
|
171 |
+ } |
|
172 |
+ } else { |
|
173 |
+ if (confirm("회원과 비밀번호를 변경하시겠습니까?")) { |
|
174 |
+ const res = await updateUsers(this.$store.state.userId, this.userInfo); |
|
175 |
+ const response = await updatePassword(this.$store.state.userId, this.userPassword); |
|
176 |
+ if (res.status == 200 && response.status == 200) { |
|
177 |
+ this.resetForm(); |
|
178 |
+ alert("회원과 비밀번호를 변경되었습니다."); |
|
179 |
+ window.location.reload(); // 페이지 새로 고침 |
|
180 |
+ } |
|
181 |
+ } |
|
182 |
+ } |
|
183 |
+ } else { |
|
184 |
+ if (this.isValidationPasswdord()) { |
|
185 |
+ alert("수정 할 내용이 없습니다."); |
|
186 |
+ } else { |
|
187 |
+ if (confirm("비밀번호 변경을 하시겠습니까?")) { |
|
188 |
+ const response = await updatePassword(this.$store.state.userId, this.userPassword); |
|
189 |
+ if (response.status == 200) { |
|
190 |
+ this.resetForm(); |
|
191 |
+ alert("비밀번호가 변경되었습니다."); |
|
192 |
+ window.location.reload(); // 페이지 새로 고침 |
|
193 |
+ } |
|
194 |
+ } |
|
195 |
+ } |
|
196 |
+ } |
|
197 |
+ } |
|
198 |
+ }, |
|
199 |
+ }, |
|
200 |
+ watch: {}, |
|
201 |
+ computed: { |
|
202 |
+ passwordsMatch() { |
|
203 |
+ return this.userPassword.newPassword === this.newPasswordCheck; |
|
204 |
+ } |
|
205 |
+ }, |
|
206 |
+ components: {}, |
|
207 |
+ mounted() { }, |
|
68 | 208 |
}; |
69 |
-</script> |
|
209 |
+</script>(파일 끝에 줄바꿈 문자 없음) |
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?