

250529 김혜민 시스템, 소셜 통합로그인처리
@a8188be3f09f2b96fc7d6a2861cf9f983c54fc2d
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -167,128 +167,145 @@ |
167 | 167 |
routes: newRoutes, |
168 | 168 |
}); |
169 | 169 |
|
170 |
- AppRouter.beforeEach(async (to, from, next) => { |
|
171 |
- const contextPath = store.state.contextPath; // Context Path 정보 |
|
172 |
- const admPath = to.path.includes('/adm'); // 관리자 페이지 여부 (true: 관리자 페이지, false: 사용자 페이지) |
|
173 |
- // const routeExists = AppRouter.getRoutes().some(route => route.path === to.path || (route.name && route.name === to.name)); |
|
174 |
- // if (!routeExists) { |
|
175 |
- // next({ name: 'notfound' }); |
|
176 |
- // return; |
|
177 |
- // } |
|
178 |
- // vue3 권장방식 ('/:pathMatch(.*)*'로 라우터 유무 확인) |
|
179 |
- if(to.name === 'PageNotFound') { |
|
180 |
- next(); |
|
181 |
- return; |
|
182 |
- } |
|
170 |
+ AppRouter.beforeEach(async (to, from, next) => { |
|
171 |
+ const contextPath = store.state.contextPath; // Context Path 정보 |
|
172 |
+ const admPath = to.path.includes('/adm'); // 관리자 페이지 여부 (true: 관리자 페이지, false: 사용자 페이지) |
|
173 |
+ |
|
174 |
+ if(to.name === 'PageNotFound') { |
|
175 |
+ next(); |
|
176 |
+ return; |
|
177 |
+ } |
|
183 | 178 |
|
184 |
- // 로그인 모드 확인 (JWT 또는 SESSION) |
|
185 |
- const loginMode = store.state.loginMode || 'J'; // 기본값으로 JWT 설정 |
|
186 |
- // 로그인 상태 확인 (JWT 또는 SESSION) |
|
187 |
- const isLogin = loginMode === 'J' ? store.state.authorization : store.state.mbrId; |
|
188 |
- // if (!isLogin && to.path !== filters.ctxPath('/login.page')) { |
|
189 |
- // sessionStorage.setItem('redirect', to.fullPath); |
|
190 |
- // next({ path: filters.ctxPath("/login.page") }); |
|
191 |
- // return; |
|
192 |
- // } |
|
179 |
+ // 로그인 모드 확인 개선 |
|
180 |
+ let loginMode = store.state.loginMode || localStorage.getItem('loginMode') || 'J'; // 기본값으로 JWT 설정 |
|
181 |
+ |
|
182 |
+ // 로그인 모드가 여전히 없으면 localStorage에서 다시 한번 확인 |
|
183 |
+ if (!loginMode || loginMode === 'undefined') { |
|
184 |
+ loginMode = 'J'; |
|
185 |
+ } |
|
193 | 186 |
|
194 |
- // 접근 제어 확인 |
|
195 |
- const accesCheck = await accessUrl(to.path); |
|
196 |
- const roleCheck = isValidRole(); |
|
197 |
- if (!accesCheck || !roleCheck) { |
|
198 |
- alert('접근이 불가합니다.\n관리자에게 문의하세요.'); |
|
199 |
- next(filters.ctxPath('/')); |
|
200 |
- } |
|
201 |
- // 경로에 따른 사용자 타입 설정 |
|
202 |
- if (to.path === filters.ctxPath('/')) { |
|
203 |
- store.commit('setUserType', 'portal') |
|
204 |
- } else if (to.path.startsWith(filters.ctxPath('/adm'))) { |
|
205 |
- store.commit('setUserType', 'adm'); |
|
206 |
- } |
|
187 |
+ // 로그인 상태 확인 개선 |
|
188 |
+ let isLogin = false; |
|
189 |
+ if (loginMode === 'J') { |
|
190 |
+ // JWT 모드: authorization 토큰 확인 |
|
191 |
+ const token = store.state.authorization || localStorage.getItem('authorization'); |
|
192 |
+ isLogin = !!token; |
|
193 |
+ } else if (loginMode === 'S') { |
|
194 |
+ // 세션 모드: mbrId 확인 |
|
195 |
+ const mbrId = store.state.mbrId || localStorage.getItem('mbrId'); |
|
196 |
+ isLogin = !!mbrId; |
|
197 |
+ } |
|
207 | 198 |
|
208 |
- if (to.path === filters.ctxPath('/login.page')) { |
|
209 |
- store.commit('setPath', to.path); |
|
210 |
- next(); |
|
211 |
- return; |
|
199 |
+ // OAuth2 콜백 처리 - 로그인 페이지에서만 처리 |
|
200 |
+ if (to.path.includes('/login.page') && (to.query.oauth_success || to.query.error)) { |
|
201 |
+ next(); |
|
202 |
+ return; |
|
203 |
+ } |
|
204 |
+ |
|
205 |
+ // 접근 제어 확인 |
|
206 |
+ const accesCheck = await accessUrl(to.path); |
|
207 |
+ const roleCheck = isValidRole(); |
|
208 |
+ if (!accesCheck || !roleCheck) { |
|
209 |
+ alert('접근이 불가합니다.\n관리자에게 문의하세요.'); |
|
210 |
+ next(filters.ctxPath('/')); |
|
211 |
+ return; |
|
212 |
+ } |
|
213 |
+ |
|
214 |
+ // 경로에 따른 사용자 타입 설정 |
|
215 |
+ if (to.path === filters.ctxPath('/')) { |
|
216 |
+ store.commit('setUserType', 'portal') |
|
217 |
+ } else if (to.path.startsWith(filters.ctxPath('/adm'))) { |
|
218 |
+ store.commit('setUserType', 'adm'); |
|
219 |
+ } |
|
220 |
+ |
|
221 |
+ if (to.path === filters.ctxPath('/login.page')) { |
|
222 |
+ store.commit('setPath', to.path); |
|
223 |
+ next(); |
|
224 |
+ return; |
|
225 |
+ } |
|
226 |
+ |
|
227 |
+ const mbrAuth = store.state.roles.map(auth => auth.authority); // 사용자 권한 정보 |
|
228 |
+ const pageAuth = mergeAuth(mbrAuth, to.meta); |
|
229 |
+ sessionStorage.setItem("redirect", to.fullPath); |
|
230 |
+ |
|
231 |
+ // 메인 페이지 or 로그인 페이지 |
|
232 |
+ if (to.path === filters.ctxPath('/') || to.path.includes('/login.page') || to.path.includes('/cmslogin.page') || to.path.startsWith(filters.ctxPath('/cmmn/')) || to.path.includes('/searchId.page') || to.path.includes('/resetPswd.page')) { |
|
233 |
+ let path = to.path; |
|
234 |
+ // 게시판일 경우 .page로 끝나는 경로가 있으므로 마지막 '/' 이전 경로로 설정 |
|
235 |
+ if (to.path.includes('BBS_MNG')) { |
|
236 |
+ let logicalPath = to.path; |
|
237 |
+ // context path 제거 |
|
238 |
+ if (contextPath !== '/' && logicalPath.startsWith(contextPath)) { |
|
239 |
+ logicalPath = logicalPath.substring(contextPath.length); |
|
240 |
+ } |
|
241 |
+ const lastSlashIndex = logicalPath.lastIndexOf('/'); // 마지막 '/' 인덱스 |
|
242 |
+ path = logicalPath.substring(0, lastSlashIndex); // 마지막 '/' 이전 경로 |
|
212 | 243 |
} |
213 |
- const mbrAuth = store.state.roles.map(auth => auth.authority); // 사용자 권한 정보 |
|
214 |
- const pageAuth = mergeAuth(mbrAuth, to.meta); |
|
215 |
- sessionStorage.setItem("redirect", to.fullPath); |
|
216 |
- |
|
217 |
- // 메인 페이지 or 로그인 페이지 |
|
218 |
- if (to.path === filters.ctxPath('/') || to.path.includes('/login.page') || to.path.includes('/cmslogin.page') || to.path.startsWith(filters.ctxPath('/cmmn/')) || to.path.includes('/searchId.page') || to.path.includes('/resetPswd.page')) { |
|
219 |
- let path = to.path; |
|
220 |
- // 게시판일 경우 .page로 끝나는 경로가 있으므로 마지막 '/' 이전 경로로 설정 |
|
221 |
- if (to.path.includes('BBS_MNG')) { |
|
244 |
+ store.commit('setPath', path); |
|
245 |
+ store.commit('setPageAuth', pageAuth); |
|
246 |
+ if (to.path === filters.ctxPath('/') || path.includes('/main.page')) { |
|
247 |
+ await cntnStatsSave(null, mbrAuth); // 메인 페이지 접속 시 사용자 접속 통계 증가 |
|
248 |
+ } |
|
249 |
+ next(); |
|
250 |
+ } else if (isLogin) { |
|
251 |
+ // 로그인 상태이고, 권한이 허용인 경우 검사 |
|
252 |
+ const hasAcc = to.matched.some(record => { |
|
253 |
+ if (!record.meta.authrt) return false; |
|
254 |
+ return record.meta.authrt.some(auth => { |
|
255 |
+ // 경로별 권한 검사 |
|
256 |
+ if (to.path.includes('/list.page')) { // 목록 권한 검증 |
|
257 |
+ return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
258 |
+ } else if (to.path.includes('/insert.page')) { // 등록 및 수정 권한 검증 |
|
259 |
+ return mbrAuth.includes(auth.authrtCd) && auth.regAuthrt === 'Y'; |
|
260 |
+ } else if (to.path.includes('/view.page')) { // 상세조회 권한 검증 |
|
261 |
+ return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
262 |
+ } else if (to.path.includes('/main.page')) { // 메인 페이지 권한 검증 |
|
263 |
+ return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
264 |
+ } else if (to.path.includes('/search.page')) { |
|
265 |
+ return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
266 |
+ } else { |
|
267 |
+ return false; |
|
268 |
+ } |
|
269 |
+ }); |
|
270 |
+ }); |
|
271 |
+ // 권한이 있고 접근 가능한 경우 |
|
272 |
+ if (hasAcc) { |
|
273 |
+ if (to.path.includes('.page')) { |
|
222 | 274 |
let logicalPath = to.path; |
223 | 275 |
// context path 제거 |
224 | 276 |
if (contextPath !== '/' && logicalPath.startsWith(contextPath)) { |
225 | 277 |
logicalPath = logicalPath.substring(contextPath.length); |
226 | 278 |
} |
227 | 279 |
const lastSlashIndex = logicalPath.lastIndexOf('/'); // 마지막 '/' 인덱스 |
228 |
- path = logicalPath.substring(0, lastSlashIndex); // 마지막 '/' 이전 경로 |
|
280 |
+ const path = logicalPath.substring(0, lastSlashIndex); // 마지막 '/' 이전 경로 |
|
281 |
+ store.commit('setPath', path); |
|
229 | 282 |
} |
230 |
- store.commit('setPath', path); |
|
231 |
- store.commit('setPageAuth', pageAuth); |
|
232 |
- if (to.path === filters.ctxPath('/') || path.includes('/main.page')) { |
|
283 |
+ // 접속 통계 |
|
284 |
+ if (to.path.includes('/main.page')) { |
|
233 | 285 |
await cntnStatsSave(null, mbrAuth); // 메인 페이지 접속 시 사용자 접속 통계 증가 |
234 |
- } |
|
235 |
- next(); |
|
236 |
- } else if (isLogin) { |
|
237 |
- // 로그인 상태이고, 권한이 허용인 경우 검사 |
|
238 |
- // const hasAcc = true; |
|
239 |
- const hasAcc = to.matched.some(record => { |
|
240 |
- if (!record.meta.authrt) return false; |
|
241 |
- return record.meta.authrt.some(auth => { |
|
242 |
- // 경로별 권한 검사 |
|
243 |
- if (to.path.includes('/list.page')) { // 목록 권한 검증 |
|
244 |
- return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
245 |
- } else if (to.path.includes('/insert.page')) { // 등록 및 수정 권한 검증 |
|
246 |
- return mbrAuth.includes(auth.authrtCd) && auth.regAuthrt === 'Y'; |
|
247 |
- } else if (to.path.includes('/view.page')) { // 상세조회 권한 검증 |
|
248 |
- return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
249 |
- } else if (to.path.includes('/main.page')) { // 메인 페이지 권한 검증 |
|
250 |
- return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
251 |
- } else if (to.path.includes('/search.page')) { |
|
252 |
- return mbrAuth.includes(auth.authrtCd) && auth.inqAuthrt === 'Y'; |
|
253 |
- } else { |
|
254 |
- return false; |
|
255 |
- } |
|
256 |
- }); |
|
257 |
- }); |
|
258 |
- // 권한이 있고 접근 가능한 경우 |
|
259 |
- if (hasAcc) { |
|
260 |
- if (to.path.includes('.page')) { |
|
261 |
- let logicalPath = to.path; |
|
262 |
- // context path 제거 |
|
263 |
- if (contextPath !== '/' && logicalPath.startsWith(contextPath)) { |
|
264 |
- logicalPath = logicalPath.substring(contextPath.length); |
|
265 |
- } |
|
266 |
- const lastSlashIndex = logicalPath.lastIndexOf('/'); // 마지막 '/' 인덱스 |
|
267 |
- const path = logicalPath.substring(0, lastSlashIndex); // 마지막 '/' 이전 경로 |
|
268 |
- store.commit('setPath', path); |
|
269 |
- } |
|
270 |
- // 접속 통계 |
|
271 |
- if (to.path.includes('/main.page')) { |
|
272 |
- await cntnStatsSave(null, mbrAuth); // 메인 페이지 접속 시 사용자 접속 통계 증가 |
|
273 |
- } else { |
|
274 |
- if (!to.meta.typeId.includes('BBS_MNG') || to.path.includes('/list.page')) { |
|
275 |
- await cntnStatsSave(to.meta.typeId, mbrAuth); |
|
276 |
- } |
|
277 |
- } |
|
278 |
- store.commit('setPageAuth', pageAuth); |
|
279 |
- next(); |
|
280 |
- // 권한이 없는 경우 이전 페이지 or / 이동 |
|
281 | 286 |
} else { |
282 |
- alert('접근 권한이 없습니다.'); |
|
283 |
- window.history.back(); |
|
284 |
- // next(from.fullPath ? from.fullPath : '/'); |
|
287 |
+ if (!to.meta.typeId.includes('BBS_MNG') || to.path.includes('/list.page')) { |
|
288 |
+ await cntnStatsSave(to.meta.typeId, mbrAuth); |
|
289 |
+ } |
|
285 | 290 |
} |
291 |
+ store.commit('setPageAuth', pageAuth); |
|
292 |
+ next(); |
|
293 |
+ // 권한이 없는 경우 이전 페이지 or / 이동 |
|
286 | 294 |
} else { |
287 |
- if(admPath) { |
|
288 |
- // sessionStorage.setItem("redirect", to.fullPath); |
|
289 |
- next({ path: filters.ctxPath("/cmslogin.page") }); |
|
290 |
- } |
|
295 |
+ alert('접근 권한이 없습니다.'); |
|
296 |
+ window.history.back(); |
|
291 | 297 |
} |
292 |
- }); |
|
298 |
+ } else { |
|
299 |
+ // 관리자 로그인이 필요한 경우 |
|
300 |
+ if(admPath) { |
|
301 |
+ sessionStorage.setItem("redirect", to.fullPath); |
|
302 |
+ next({ path: filters.ctxPath("/cmslogin.page") }); |
|
303 |
+ } else { |
|
304 |
+ // 일반 사용자 페이지에서 로그인이 필요한 경우 |
|
305 |
+ sessionStorage.setItem("redirect", to.fullPath); |
|
306 |
+ next({ path: filters.ctxPath("/login.page") }); |
|
307 |
+ } |
|
308 |
+ } |
|
309 |
+}); |
|
293 | 310 |
return AppRouter; |
294 | 311 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppStore.js
+++ client/views/pages/AppStore.js
... | ... | @@ -90,29 +90,25 @@ |
90 | 90 |
}, |
91 | 91 |
actions: { |
92 | 92 |
async logout({ commit }) { |
93 |
- try { |
|
93 |
+ try { |
|
94 | 94 |
const ctx = this.state.contextPath; // 캐시 초기화 전 contextPath 저장 |
95 | 95 |
const admPath = this.state.path?.includes("/adm") // 캐시 초기화 전 경로 구분 (true: 관리자 페이지, false: 사용자 페이지) |
96 |
- const loginMode = this.state.loginMode; // 로그인 모드 확인 |
|
97 |
- |
|
98 |
- console.log("로그아웃 처리 시작 - 로그인 모드:", loginMode); |
|
96 |
+ const loginMode = this.state.loginMode || localStorage.getItem('loginMode') || 'J'; |
|
99 | 97 |
|
100 | 98 |
// 로그인 모드에 따른 처리 |
101 | 99 |
if (loginMode === 'J') { |
102 | 100 |
// JWT 방식인 경우만 서버 API 호출 |
103 | 101 |
try { |
104 | 102 |
const res = await logOutProc(); |
105 |
- alert(res.data.message); |
|
103 |
+ if (res.data.message) { |
|
104 |
+ alert(res.data.message); |
|
105 |
+ } |
|
106 | 106 |
} catch (error) { |
107 |
- console.log("JWT 로그아웃 API 에러, 클라이언트만 정리:", error); |
|
107 |
+ console.log(error); |
|
108 | 108 |
// API 에러가 발생해도 클라이언트는 정리 |
109 | 109 |
} |
110 |
- } else { |
|
111 |
- // 세션 방식 (OAuth 포함)은 서버 API 호출 없이 클라이언트만 정리 |
|
112 |
- console.log("세션 방식 로그아웃 - 클라이언트만 정리"); |
|
113 | 110 |
} |
114 | 111 |
|
115 |
- // 공통 클라이언트 정리 작업 |
|
116 | 112 |
// 1. 상태 초기화 |
117 | 113 |
commit("setStoreReset"); |
118 | 114 |
|
... | ... | @@ -120,20 +116,29 @@ |
120 | 116 |
localStorage.clear(); |
121 | 117 |
sessionStorage.clear(); |
122 | 118 |
|
123 |
- // 3. 쿠키 삭제 |
|
124 |
- document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
125 |
- document.cookie = "Authorization=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
126 |
- document.cookie = "JSESSIONID=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; |
|
119 |
+ // 3. 모든 가능한 쿠키 삭제 (OAuth 관련 포함) |
|
120 |
+ const cookiesToDelete = [ |
|
121 |
+ 'refresh', |
|
122 |
+ 'Authorization', |
|
123 |
+ 'JSESSIONID', |
|
124 |
+ 'oauth_access_token', // OAuth 토큰 쿠키 |
|
125 |
+ 'oauth_refresh_token', // OAuth 리프레시 토큰 |
|
126 |
+ 'SESSION' // 스프링 기본 세션 쿠키 |
|
127 |
+ ]; |
|
127 | 128 |
|
128 |
- console.log("로그아웃 완료"); |
|
129 |
+ cookiesToDelete.forEach(cookieName => { |
|
130 |
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; |
|
131 |
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`; |
|
132 |
+ }); |
|
129 | 133 |
|
130 |
- // 4. 로그인 페이지로 이동 |
|
131 |
- if(admPath) { |
|
132 |
- window.location = ctx + "/cmslogin.page"; |
|
133 |
- } else { |
|
134 |
- // window.location = ctx + "/login.page"; |
|
135 |
- window.location = ctx + "/"; |
|
136 |
- } |
|
134 |
+ // 4. 로그인 페이지로 이동 (OAuth 관련 파라미터 제거) |
|
135 |
+ const cleanUrl = admPath ? |
|
136 |
+ ctx + "/cmslogin.page" : |
|
137 |
+ ctx + "/login.page"; |
|
138 |
+ |
|
139 |
+ // URL에서 OAuth 관련 파라미터 제거 |
|
140 |
+ window.history.replaceState({}, document.title, cleanUrl); |
|
141 |
+ window.location.href = cleanUrl; |
|
137 | 142 |
|
138 | 143 |
} catch(error) { |
139 | 144 |
console.error("로그아웃 처리 중 오류:", error); |
... | ... | @@ -151,15 +156,15 @@ |
151 | 156 |
if (errorData?.message) { |
152 | 157 |
alert(errorData.message); |
153 | 158 |
} else { |
154 |
- |
|
159 |
+ console.log("로그아웃 처리 중 예상치 못한 오류 발생"); |
|
155 | 160 |
} |
156 | 161 |
|
157 | 162 |
// 로그인 페이지로 이동 |
158 |
- if(admPath) { |
|
159 |
- window.location = ctx + "/cmslogin.page"; |
|
160 |
- } else { |
|
161 |
- window.location = ctx + "/login.page"; |
|
162 |
- } |
|
163 |
+ const cleanUrl = admPath ? |
|
164 |
+ ctx + "/cmslogin.page" : |
|
165 |
+ ctx + "/login.page"; |
|
166 |
+ |
|
167 |
+ window.location.href = cleanUrl; |
|
163 | 168 |
} |
164 | 169 |
}, |
165 | 170 |
setUserType({ commit }, userType) { |
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
... | ... | @@ -7,64 +7,58 @@ |
7 | 7 |
로그인하세요. |
8 | 8 |
</p> |
9 | 9 |
</div> |
10 |
+ |
|
11 |
+ <!-- 일반 로그인 화면 --> |
|
10 | 12 |
<div class="login-wrap" v-if="!loginStep1 && !loginStep2"> |
11 | 13 |
<div class="login"> |
12 |
- <div :class="{ |
|
13 |
- 'login-title': true, |
|
14 |
- 'user-login': !isAdminPage, |
|
15 |
- }"> |
|
14 |
+ <div :class="{ 'login-title': true, 'user-login': !isAdminPage }"> |
|
16 | 15 |
LOGIN |
17 | 16 |
</div> |
17 |
+ |
|
18 |
+ <!-- 아이디/비밀번호 입력 --> |
|
18 | 19 |
<div class="form-group"> |
19 | 20 |
<label for="id" class="login-label">아이디</label> |
20 |
- <input type="text" name="" id="id" class="form-control md" placeholder="아이디를 입력하세요" |
|
21 |
- v-model="member['lgnId']" /> |
|
21 |
+ <input type="text" id="id" class="form-control md" placeholder="아이디를 입력하세요" v-model="member.lgnId" /> |
|
22 | 22 |
</div> |
23 | 23 |
<div class="form-group"> |
24 | 24 |
<label for="pw" class="login-label">비밀번호</label> |
25 |
- <input type="password" name="" id="pw" class="form-control md" placeholder="비밀번호를 입력하세요" |
|
26 |
- v-model="member['pswd']" @keydown.enter="fnLogin" /> |
|
25 |
+ <input type="password" id="pw" class="form-control md" placeholder="비밀번호를 입력하세요" |
|
26 |
+ v-model="member.pswd" @keydown.enter="fnLogin" /> |
|
27 | 27 |
</div> |
28 |
- <button class="btn md main user-btn" v-if="!isAdminPage" @click="fnLogin" @keydown.enter="fnLogin"> |
|
29 |
- 로그인 |
|
30 |
- </button> |
|
31 |
- <button class="btn md main" v-else @click="fnLogin" @keydown.enter="fnLogin"> |
|
28 |
+ |
|
29 |
+ <!-- 로그인 버튼 --> |
|
30 |
+ <button class="btn md main" :class="{ 'user-btn': !isAdminPage }" @click="fnLogin" @keydown.enter="fnLogin"> |
|
32 | 31 |
로그인 |
33 | 32 |
</button> |
34 | 33 |
|
35 |
- <div class="input-group" v-if="!isAdminPage"> |
|
36 |
- <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p> |
|
37 |
- <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p> |
|
38 |
- </div> |
|
39 |
- |
|
40 |
- <!-- OAuth2 로그인 버튼들 --> |
|
34 |
+ <!-- 사용자 페이지 전용 기능 --> |
|
41 | 35 |
<div v-if="!isAdminPage"> |
42 |
- <div> |
|
43 |
- <span>또는</span> |
|
36 |
+ <!-- 아이디/비밀번호 찾기 --> |
|
37 |
+ <div class="input-group"> |
|
38 |
+ <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p> |
|
39 |
+ <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p> |
|
44 | 40 |
</div> |
41 |
+ |
|
42 |
+ <!-- OAuth2 소셜 로그인 --> |
|
45 | 43 |
<div> |
46 |
- <button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading"> |
|
47 |
- 카카오로 로그인 |
|
48 |
- </button> |
|
49 |
- |
|
50 |
- <button @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading"> |
|
51 |
- 네이버로 로그인 |
|
52 |
- </button> |
|
53 |
- |
|
54 |
- <button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading"> |
|
55 |
- 구글로 로그인 |
|
56 |
- </button> |
|
44 |
+ <div><span>또는</span></div> |
|
45 |
+ <div> |
|
46 |
+ <button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading">카카오로 로그인</button> |
|
47 |
+ <button @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading">네이버로 로그인</button> |
|
48 |
+ <button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading">구글로 로그인</button> |
|
49 |
+ </div> |
|
57 | 50 |
</div> |
58 | 51 |
</div> |
59 | 52 |
</div> |
60 | 53 |
</div> |
54 |
+ |
|
55 |
+ <!-- 2차 인증 화면 --> |
|
61 | 56 |
<div class="login-wrap" v-else-if="loginStep1 && !loginStep2"> |
62 | 57 |
<div> |
63 | 58 |
<p>인증코드 입력</p> |
64 |
- </div> |
|
65 |
- <div> |
|
66 | 59 |
<p>{{memberInfo.email}}로 전송된 6자리 인증코드를 입력하세요.</p> |
67 |
- <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="memberInfo.code" maxlength="6" placeholder="인증코드를 입력하세요."/> |
|
60 |
+ <input type="text" class="form-control md" ref="code" @input="inputCode" |
|
61 |
+ v-model="memberInfo.code" maxlength="6" placeholder="인증코드를 입력하세요." /> |
|
68 | 62 |
</div> |
69 | 63 |
<div class="btn-wrap"> |
70 | 64 |
<button class="btn sm main" @click="fnCheck">인증코드 확인</button> |
... | ... | @@ -95,14 +89,7 @@ |
95 | 89 |
store: useStore(), |
96 | 90 |
isAdminPage: false, |
97 | 91 |
isOAuthLoading: false, |
98 |
- // oauthProviders: [ |
|
99 |
- // { name: 'kakao', label: '카카오로 로그인' }, |
|
100 |
- // { name: 'naver', label: '네이버로 로그인' }, |
|
101 |
- // { name: 'google', label: '구글로 로그인' } |
|
102 |
- // ] |
|
103 |
- |
|
104 | 92 |
memberInfo: { email: '', code: '' }, |
105 |
- // 인증 절차 |
|
106 | 93 |
loginStep1: false, // 1차 인증 |
107 | 94 |
loginStep2: false, // 2차 인증 |
108 | 95 |
}; |
... | ... | @@ -132,7 +119,7 @@ |
132 | 119 |
}, |
133 | 120 |
|
134 | 121 |
methods: { |
135 |
- // ========== 초기화 및 유틸리티 ========== |
|
122 |
+ // ========== 초기화 ========== |
|
136 | 123 |
checkAdminPage() { |
137 | 124 |
const redirect = this.restoreRedirect("redirect"); |
138 | 125 |
this.isAdminPage = redirect && redirect.includes("/adm/"); |
... | ... | @@ -152,28 +139,13 @@ |
152 | 139 |
const res = await loginProc(this.member); |
153 | 140 |
if (res.status !== 200) return; |
154 | 141 |
|
155 |
- // 2차 인증에 필요한 이메일 정보가 있는지 확인 |
|
156 | 142 |
if (res.data.email) { |
157 |
- this.memberInfo = res.data; // 인증코드 전송을 위한 이메일 정보 저장 |
|
158 |
- // 없을 경우 2차 인증 패스 |
|
143 |
+ this.memberInfo = res.data; |
|
159 | 144 |
} else { |
160 |
- this.loginStep2 = true; // 2차 인증 패스 |
|
161 |
- await this.loginSuccessProc(res); // 로그인 성공 처리 |
|
145 |
+ this.loginStep2 = true; |
|
146 |
+ await this.loginSuccessProc(res); |
|
162 | 147 |
} |
163 |
- this.loginStep1 = true; // 1차 인증 성공 |
|
164 |
- |
|
165 |
- // const loginType = res.headers['login-type']; |
|
166 |
- |
|
167 |
- // if (loginType === 'J') { |
|
168 |
- // this.handleJWTLogin(res); |
|
169 |
- // } else if (loginType === 'S') { |
|
170 |
- // this.handleSessionLogin(res); |
|
171 |
- // } else { |
|
172 |
- // alert("알 수 없는 로그인 방식입니다."); |
|
173 |
- // return; |
|
174 |
- // } |
|
175 |
- |
|
176 |
- // await this.handleLoginSuccess(); |
|
148 |
+ this.loginStep1 = true; |
|
177 | 149 |
|
178 | 150 |
} catch (error) { |
179 | 151 |
alert(error.response?.data?.message || "로그인에 실패했습니다."); |
... | ... | @@ -182,53 +154,8 @@ |
182 | 154 |
} |
183 | 155 |
}, |
184 | 156 |
|
185 |
- // 인증코드 입력 |
|
186 |
- inputCode(event) { |
|
187 |
- const input = event.target.value.replace(/[^0-9]/g, ''); |
|
188 |
- this.memberInfo.code = input; |
|
189 |
- }, |
|
190 |
- |
|
191 |
- // 인증코드 확인 |
|
192 |
- async fnCheck() { |
|
193 |
- try { |
|
194 |
- const res = await check2ndAuthProc(this.memberInfo); |
|
195 |
- if (res.status == 200) { |
|
196 |
- await this.loginSuccessProc(res); // 로그인 성공 처리 |
|
197 |
- } |
|
198 |
- } catch (error) { |
|
199 |
- const errorData = error.response.data; |
|
200 |
- if (errorData.message != null && errorData.message != "") { |
|
201 |
- alert(error.response.data.message); |
|
202 |
- this.$refs.code.focus(); |
|
203 |
- } else { |
|
204 |
- alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
|
205 |
- } |
|
206 |
- } |
|
207 |
- }, |
|
208 |
- |
|
209 |
- // 인증코드 재전송 |
|
210 |
- async fnResend() { |
|
211 |
- this.isLoading = true; |
|
212 |
- try { |
|
213 |
- const res = await sendAuthEmailProc(this.memberInfo); |
|
214 |
- if (res.status == 200) { |
|
215 |
- alert(res.data.message); |
|
216 |
- } |
|
217 |
- } catch (error) { |
|
218 |
- const errorData = error.response.data; |
|
219 |
- if (errorData.message != null && errorData.message != "") { |
|
220 |
- alert(error.response.data.message); |
|
221 |
- } else { |
|
222 |
- alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
|
223 |
- } |
|
224 |
- } finally { |
|
225 |
- this.isLoading = false; |
|
226 |
- } |
|
227 |
- }, |
|
228 |
- |
|
229 |
- // 로그인 성공 시 |
|
230 | 157 |
async loginSuccessProc(res) { |
231 |
- const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분 |
|
158 |
+ const loginType = res.headers['login-type']; |
|
232 | 159 |
if (loginType === 'J') { |
233 | 160 |
this.handleJWTLogin(res); |
234 | 161 |
} else if (loginType === 'S') { |
... | ... | @@ -239,17 +166,16 @@ |
239 | 166 |
} |
240 | 167 |
await this.handleLoginSuccess(); |
241 | 168 |
}, |
169 |
+ |
|
242 | 170 |
handleJWTLogin(res) { |
243 | 171 |
const token = res.headers.authorization; |
244 | 172 |
const userInfo = this.parseJWT(token); |
245 |
- |
|
246 | 173 |
this.setAuthInfo("J", token, userInfo); |
247 | 174 |
}, |
248 | 175 |
|
249 | 176 |
handleSessionLogin(res) { |
250 | 177 |
const userInfo = res.data; |
251 | 178 |
const roles = userInfo.roles.map(r => ({ authority: r.authrtCd })); |
252 |
- |
|
253 | 179 |
this.setAuthInfo("S", null, { ...userInfo, roles }); |
254 | 180 |
}, |
255 | 181 |
|
... | ... | @@ -264,24 +190,45 @@ |
264 | 190 |
return JSON.parse(jsonPayload); |
265 | 191 |
}, |
266 | 192 |
|
267 |
- setAuthInfo(loginMode, token, userInfo) { |
|
268 |
- // Vuex 상태 저장 |
|
269 |
- store.commit("setLoginMode", loginMode); |
|
270 |
- store.commit("setAuthorization", token); |
|
271 |
- store.commit("setMbrId", userInfo.mbrId); |
|
272 |
- store.commit("setMbrNm", userInfo.mbrNm); |
|
273 |
- store.commit("setRoles", userInfo.roles); |
|
193 |
+ // ========== 2차 인증 ========== |
|
194 |
+ inputCode(event) { |
|
195 |
+ const input = event.target.value.replace(/[^0-9]/g, ''); |
|
196 |
+ this.memberInfo.code = input; |
|
197 |
+ }, |
|
274 | 198 |
|
275 |
- // localStorage 저장 |
|
276 |
- localStorage.setItem("loginMode", loginMode); |
|
277 |
- localStorage.setItem("mbrId", userInfo.mbrId); |
|
278 |
- localStorage.setItem("mbrNm", userInfo.mbrNm); |
|
279 |
- localStorage.setItem("roles", JSON.stringify(userInfo.roles)); |
|
199 |
+ async fnCheck() { |
|
200 |
+ try { |
|
201 |
+ const res = await check2ndAuthProc(this.memberInfo); |
|
202 |
+ if (res.status == 200) { |
|
203 |
+ await this.loginSuccessProc(res); |
|
204 |
+ } |
|
205 |
+ } catch (error) { |
|
206 |
+ const errorData = error.response.data; |
|
207 |
+ if (errorData.message != null && errorData.message != "") { |
|
208 |
+ alert(error.response.data.message); |
|
209 |
+ this.$refs.code.focus(); |
|
210 |
+ } else { |
|
211 |
+ alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
|
212 |
+ } |
|
213 |
+ } |
|
214 |
+ }, |
|
280 | 215 |
|
281 |
- if (token) { |
|
282 |
- localStorage.setItem("authorization", token); |
|
283 |
- } else { |
|
284 |
- localStorage.removeItem("authorization"); |
|
216 |
+ async fnResend() { |
|
217 |
+ this.isLoading = true; |
|
218 |
+ try { |
|
219 |
+ const res = await sendAuthEmailProc(this.memberInfo); |
|
220 |
+ if (res.status == 200) { |
|
221 |
+ alert(res.data.message); |
|
222 |
+ } |
|
223 |
+ } catch (error) { |
|
224 |
+ const errorData = error.response.data; |
|
225 |
+ if (errorData.message != null && errorData.message != "") { |
|
226 |
+ alert(error.response.data.message); |
|
227 |
+ } else { |
|
228 |
+ alert("에러가 발생했습니다.\n관리자에게 문의해주세요."); |
|
229 |
+ } |
|
230 |
+ } finally { |
|
231 |
+ this.isLoading = false; |
|
285 | 232 |
} |
286 | 233 |
}, |
287 | 234 |
|
... | ... | @@ -303,15 +250,16 @@ |
303 | 250 |
if (oauthSuccess !== 'true' && oauthSuccess !== true) return; |
304 | 251 |
|
305 | 252 |
try { |
306 |
- store.commit("setLoginMode", loginMode || "J"); |
|
307 |
- localStorage.setItem("loginMode", loginMode || "J"); |
|
253 |
+ // 기존 시스템 로그인 모드 따라가기 |
|
254 |
+ const finalLoginMode = loginMode || store.state.loginMode || localStorage.getItem('loginMode') || 'J'; |
|
255 |
+ |
|
256 |
+ store.commit("setLoginMode", finalLoginMode); |
|
257 |
+ localStorage.setItem("loginMode", finalLoginMode); |
|
308 | 258 |
|
309 |
- if (loginMode === 'J') { |
|
259 |
+ if (finalLoginMode === 'J') { |
|
310 | 260 |
await this.handleOAuthJWT(); |
311 |
- } else if (loginMode === 'S') { |
|
312 |
- await this.handleOAuthSession(); |
|
313 | 261 |
} else { |
314 |
- throw new Error('알 수 없는 로그인 모드: ' + loginMode); |
|
262 |
+ await this.handleOAuthSession(); |
|
315 | 263 |
} |
316 | 264 |
|
317 | 265 |
this.cleanupOAuth(); |
... | ... | @@ -319,6 +267,7 @@ |
319 | 267 |
await this.handleLoginSuccess(); |
320 | 268 |
|
321 | 269 |
} catch (error) { |
270 |
+ console.error("OAuth2 처리 실패:", error); |
|
322 | 271 |
this.handleOAuthError('processing_error', error.message); |
323 | 272 |
} |
324 | 273 |
}, |
... | ... | @@ -336,70 +285,113 @@ |
336 | 285 |
}, |
337 | 286 |
|
338 | 287 |
async handleOAuthJWT() { |
339 |
- const oauthToken = this.getCookie('oauth_access_token'); |
|
340 |
- if (!oauthToken) { |
|
341 |
- throw new Error('OAuth 토큰을 찾을 수 없습니다.'); |
|
342 |
- } |
|
343 |
- |
|
344 |
- const fullToken = oauthToken.startsWith('Bearer ') ? oauthToken : `Bearer ${oauthToken}`; |
|
345 |
- store.commit("setAuthorization", fullToken); |
|
346 |
- localStorage.setItem("authorization", fullToken); |
|
347 |
- this.deleteCookie('oauth_access_token'); |
|
348 |
- |
|
288 |
+ console.log("JWT 모드 OAuth2 처리 시작"); |
|
289 |
+ |
|
349 | 290 |
try { |
350 |
- const userInfo = this.parseJWT(fullToken.replace('Bearer ', '')); |
|
351 |
- this.setAuthInfo("J", fullToken, userInfo); |
|
352 |
- } catch (jwtError) { |
|
353 |
- await this.fetchUserInfoFromServer(); |
|
291 |
+ const token = localStorage.getItem('authorization') |
|
292 |
+ || this.getCookie('refresh') |
|
293 |
+ || this.getCookie('Authorization'); |
|
294 |
+ |
|
295 |
+ const headers = { 'Content-Type': 'application/json' }; |
|
296 |
+ |
|
297 |
+ if (token) { |
|
298 |
+ headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`; |
|
299 |
+ } |
|
300 |
+ |
|
301 |
+ const response = await fetch('/oauth2/getUserInfo.json', { |
|
302 |
+ method: 'POST', |
|
303 |
+ headers: headers, |
|
304 |
+ credentials: 'include' |
|
305 |
+ }); |
|
306 |
+ |
|
307 |
+ if (response.status === 200) { |
|
308 |
+ const result = await response.json(); |
|
309 |
+ |
|
310 |
+ if (result.success || result.data) { |
|
311 |
+ const userInfo = result.data; |
|
312 |
+ const roles = Array.isArray(userInfo.roles) ? |
|
313 |
+ userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) : |
|
314 |
+ userInfo.roles; |
|
315 |
+ |
|
316 |
+ this.setAuthInfo("J", token, { ...userInfo, roles }); |
|
317 |
+ |
|
318 |
+ } else { |
|
319 |
+ throw new Error('서버에서 실패 응답'); |
|
320 |
+ } |
|
321 |
+ } else { |
|
322 |
+ throw new Error('API 호출 실패: ' + response.status); |
|
323 |
+ } |
|
324 |
+ |
|
325 |
+ } catch (error) { |
|
326 |
+ throw error; |
|
354 | 327 |
} |
355 | 328 |
}, |
356 | 329 |
|
357 | 330 |
async handleOAuthSession() { |
358 |
- store.commit("setAuthorization", null); |
|
359 |
- localStorage.removeItem("authorization"); |
|
360 |
- this.deleteCookie('oauth_access_token'); |
|
331 |
+ try { |
|
332 |
+ const userInfoRes = await getUserInfo(); |
|
333 |
+ if (!userInfoRes || userInfoRes.status !== 200) { |
|
334 |
+ throw new Error('세션 정보를 가져올 수 없습니다.'); |
|
335 |
+ } |
|
361 | 336 |
|
362 |
- await this.fetchUserInfoFromServer(); |
|
337 |
+ const userInfo = userInfoRes.data.data; |
|
338 |
+ const roles = Array.isArray(userInfo.roles) ? |
|
339 |
+ userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) : |
|
340 |
+ userInfo.roles; |
|
341 |
+ |
|
342 |
+ this.setAuthInfo('S', null, { ...userInfo, roles }); |
|
343 |
+ |
|
344 |
+ } catch (error) { |
|
345 |
+ console.error("세션 모드 처리 실패:", error); |
|
346 |
+ throw error; |
|
347 |
+ } |
|
363 | 348 |
}, |
364 | 349 |
|
365 |
- async fetchUserInfoFromServer() { |
|
366 |
- const userInfoRes = await getUserInfo(); |
|
367 |
- if (!userInfoRes || userInfoRes.status !== 200) { |
|
368 |
- throw new Error('사용자 정보를 가져올 수 없습니다.'); |
|
350 |
+ // ========== 공통 처리 ========== |
|
351 |
+ setAuthInfo(loginMode, token, userInfo) { |
|
352 |
+ // Store 설정 |
|
353 |
+ try { |
|
354 |
+ if (typeof store !== 'undefined' && store.commit) { |
|
355 |
+ store.commit("setLoginMode", loginMode); |
|
356 |
+ store.commit("setAuthorization", token); |
|
357 |
+ store.commit("setMbrId", userInfo.mbrId); |
|
358 |
+ store.commit("setMbrNm", userInfo.mbrNm); |
|
359 |
+ store.commit("setRoles", userInfo.roles); |
|
360 |
+ } |
|
361 |
+ } catch (e) { |
|
362 |
+ console.warn("store 설정 실패, localStorage만 사용:", e); |
|
369 | 363 |
} |
370 | 364 |
|
371 |
- const userInfo = userInfoRes.data.data; |
|
372 |
- const roles = Array.isArray(userInfo.roles) ? |
|
373 |
- userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) : |
|
374 |
- userInfo.roles; |
|
365 |
+ // localStorage 저장 |
|
366 |
+ localStorage.setItem("loginMode", loginMode); |
|
367 |
+ localStorage.setItem("mbrId", userInfo.mbrId); |
|
368 |
+ localStorage.setItem("mbrNm", userInfo.mbrNm); |
|
369 |
+ localStorage.setItem("roles", JSON.stringify(userInfo.roles)); |
|
375 | 370 |
|
376 |
- const loginMode = localStorage.getItem("loginMode"); |
|
377 |
- const token = userInfo.token || localStorage.getItem("authorization"); |
|
378 |
- |
|
379 |
- this.setAuthInfo(loginMode, token, { ...userInfo, roles }); |
|
371 |
+ if (token && loginMode === 'J') { |
|
372 |
+ localStorage.setItem("authorization", token); |
|
373 |
+ } else { |
|
374 |
+ localStorage.removeItem("authorization"); |
|
375 |
+ } |
|
380 | 376 |
}, |
381 | 377 |
|
382 |
- // ========== 로그인 성공 후 처리 ========== |
|
383 | 378 |
async handleLoginSuccess() { |
384 | 379 |
const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN"); |
385 | 380 |
let redirectUrl = this.restoreRedirect("redirect") || sessionStorage.getItem('oauth_redirect'); |
386 | 381 |
|
387 |
- // 리다이렉트 URL 정리 |
|
388 | 382 |
if (redirectUrl && this.shouldRedirectToMain(redirectUrl)) { |
389 | 383 |
redirectUrl = this.$filters.ctxPath("/"); |
390 | 384 |
} |
391 | 385 |
|
392 |
- // Context Path 처리 |
|
393 | 386 |
if (redirectUrl && !redirectUrl.startsWith(store.state.contextPath) && store.state.contextPath) { |
394 | 387 |
redirectUrl = this.$filters.ctxPath(redirectUrl); |
395 | 388 |
} |
396 | 389 |
|
397 |
- // 라우터 존재 여부 확인 후 이동 |
|
398 | 390 |
const targetPath = this.getValidRedirectPath(redirectUrl, isAdmin); |
391 |
+ |
|
399 | 392 |
await this.$nextTick(); |
400 | 393 |
this.$router.push({ path: targetPath }); |
401 | 394 |
|
402 |
- // 세션 정리 |
|
403 | 395 |
sessionStorage.removeItem("redirect"); |
404 | 396 |
sessionStorage.removeItem("oauth_redirect"); |
405 | 397 |
}, |
... | ... | @@ -442,15 +434,11 @@ |
442 | 434 |
window.history.replaceState({}, document.title, cleanUrl); |
443 | 435 |
}, |
444 | 436 |
|
445 |
- // ========== 유틸리티 메서드 ========== |
|
437 |
+ // ========== 유틸리티 ========== |
|
446 | 438 |
getCookie(name) { |
447 | 439 |
const value = `; ${document.cookie}`; |
448 | 440 |
const parts = value.split(`; ${name}=`); |
449 | 441 |
return parts.length === 2 ? parts.pop().split(';').shift() : null; |
450 |
- }, |
|
451 |
- |
|
452 |
- deleteCookie(name) { |
|
453 |
- document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; |
|
454 | 442 |
}, |
455 | 443 |
|
456 | 444 |
moveSearchId() { |
... | ... | @@ -472,8 +460,8 @@ |
472 | 460 |
|
473 | 461 |
<style scoped> |
474 | 462 |
.loading-blur { |
475 |
- pointer-events: none; |
|
476 |
- opacity: 0.4; |
|
477 |
- filter: grayscale(20%) blur(1px); |
|
463 |
+ pointer-events: none; |
|
464 |
+ opacity: 0.4; |
|
465 |
+ filter: grayscale(20%) blur(1px); |
|
478 | 466 |
} |
479 | 467 |
</style>(파일 끝에 줄바꿈 문자 없음) |
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?