hmkim 05-28
250528 김혜민 oauth2 로그인 수정
@c4a337881995f0cedd243fb56c022e9354cf8cda
client/resources/api/login.js
--- client/resources/api/login.js
+++ client/resources/api/login.js
@@ -1,20 +1,19 @@
 import apiClient from "./index";
 
+// 일반 로그인
 export const loginProc = mber => {
     return apiClient.post(`/mbr/loginProc.json`, mber);
 }
 
+// 사용자 정보 조회 - OAuth2에서도 사용
+export const getUserInfo = () => {
+    return apiClient.post(`/oauth2/getUserInfo.json`);
+}
+
+// OAuth2 로그인 - 서버로 리다이렉트
 export const oauthLogin = (provider) => {
-  const { API_SERVER_HOST } = require("../../../Global");
-  const oauthUrl = `//${API_SERVER_HOST}/oauth2/login?provider=${provider}`;
-  window.location.href = oauthUrl;
-};
-
-export const getOAuthUrl = (provider) => {
-  const { API_SERVER_HOST } = require("../../../Global");
-  return `//${API_SERVER_HOST}/oauth2/login?provider=${provider}`;
-};
-
-export const getOAuthUserInfo = () => {
-  return apiClient.get(`/oauth2/user-info`);
+    // 현재 도메인 유지하면서 OAuth2 엔드포인트로 이동
+    const { API_SERVER_HOST } = require("../../../Global");
+    const oauthUrl = `//${API_SERVER_HOST}/oauth2/login?provider=${provider}`;
+    window.location.href = oauthUrl;
 };
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/login/Login.vue
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
@@ -44,7 +44,7 @@
                 class="btn md main user-btn"
                 v-if="!isAdminPage"
                 @click="fnLogin"
-                   @keydown.enter="fnLogin"
+                @keydown.enter="fnLogin"
               >
                 로그인
               </button>
@@ -64,267 +64,343 @@
                 <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
                 <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
               </div>
-              <!-- <div class="oauth-login-section" v-if="!isAdminPage">
-                <div class="oauth-divider">
+              
+              <!-- OAuth2 로그인 버튼들 -->
+              <div v-if="!isAdminPage">
+                <div>
                   <span>또는</span>
                 </div>
-                
-                <div class="oauth-buttons">
-                  <button class="oauth-btn kakao-btn" @click="fnOAuthLogin('kakao')">
-                    <img src="/images/kakao-icon.png" alt="카카오" class="oauth-icon" />
+                <div>
+                  <button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading">
                     카카오로 로그인
                   </button>
                   
-                  <button class="oauth-btn naver-btn" @click="fnOAuthLogin('naver')">
-                    <img src="/images/naver-icon.png" alt="네이버" class="oauth-icon" />
+                  <button  @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading">
                     네이버로 로그인
                   </button>
                   
-                  <button class="oauth-btn google-btn" @click="fnOAuthLogin('google')">
-                    <img src="/images/google-icon.png" alt="구글" class="oauth-icon" />
+                  <button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading">
                     구글로 로그인
                   </button>
                 </div>
-            </div> -->
+              </div>
+            </div>
         </div>
       </div>
     </div>
-  </div>
 </template>
 
 <script>
 import { useStore } from "vuex";
 import store from "../AppStore";
-import { loginProc, oauthLogin, getOAuthUserInfo } from "../../../resources/api/login";
+import { loginProc, getUserInfo, oauthLogin } from "../../../resources/api/login";
 import queryParams from "../../../resources/js/queryParams";
 
 export default {
   mixins: [queryParams],
-  data: () => {
+  
+  data() {
     return {
-      member: {
-        lgnId: null,
-        pswd: null,
-      },
+      member: { lgnId: null, pswd: null },
       store: useStore(),
       isAdminPage: false,
+      isOAuthLoading: false,
+      // oauthProviders: [
+      //   { name: 'kakao', label: '카카오로 로그인' },
+      //   { name: 'naver', label: '네이버로 로그인' },
+      //   { name: 'google', label: '구글로 로그인' }
+      // ]
     };
   },
-  methods: {
-    checkAdminPage() {
-      if (
-        this.restoreRedirect("redirect") &&
-        this.restoreRedirect("redirect").includes("/adm/")
-      ) {
-        this.isAdminPage = true;
-      } else {
-        this.isAdminPage = false;
-      }
-    },
-     
-   // OAuth2 로그인 처리 (API 모듈 사용)
-    fnOAuthLogin(provider) {
-    const redirectUrl = this.restoreRedirect("redirect") || this.$route.fullPath;
-    sessionStorage.setItem('oauth_redirect', redirectUrl);
-    
-    oauthLogin(provider);
+
+  async created() {
+    this.checkAdminPage();
   },
+
+  async mounted() {
+    await this.$nextTick();
+    if (this.hasOAuthParams()) {
+      await this.handleOAuthCallback();
+    }
+  },
+
+  beforeUnmount() {
+    this.isOAuthLoading = false;
+  },
+
+  watch: {
+    '$route'(to) {
+      if (to.query.oauth_success || to.query.error) {
+        this.handleOAuthCallback();
+      }
+    }
+  },
+
+  methods: {
+    // ========== 초기화 및 유틸리티 ==========
+    checkAdminPage() {
+      const redirect = this.restoreRedirect("redirect");
+      this.isAdminPage = redirect && redirect.includes("/adm/");
+    },
+
+    hasOAuthParams() {
+      return window.location.search.includes('oauth_success') || 
+             window.location.search.includes('error') ||
+             this.$route.query.oauth_success || 
+             this.$route.query.error;
+    },
+
+    // ========== 일반 로그인 ==========
     async fnLogin() {
       try {
         const res = await loginProc(this.member);
-        if (res.status == 200) {
-          const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분
-          
-          if (loginType === 'J') {
-            // JWT 방식
-            const token = res.headers.authorization;
-            store.commit("setAuthorization", token);
-            store.commit("setLoginMode", "J");
-            
-            // localStorage에도 저장
-            localStorage.setItem("authorization", token);
-            localStorage.setItem("loginMode", "J");
-            
-            const base64String = token.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 mbr = JSON.parse(jsonPayload);
-            
-            store.commit("setMbrId", mbr.mbrId);
-            store.commit("setMbrNm", mbr.mbrNm);
-            store.commit("setRoles", mbr.roles);
-            
-            // localStorage에 사용자 정보도 저장
-            localStorage.setItem("mbrId", mbr.mbrId);
-            localStorage.setItem("mbrNm", mbr.mbrNm);
-            localStorage.setItem("roles", JSON.stringify(mbr.roles));
-            
-          } else if (loginType === 'S') {
-            store.commit("setLoginMode", "S");
-            localStorage.setItem("loginMode", "S");
-            const mbr = res.data;
-            store.commit("setAuthorization", null);
-            store.commit("setMbrId", mbr.mbrId);
-            store.commit("setMbrNm", mbr.mbrNm);
-            const roles = mbr.roles.map(r => ({ authority: r.authrtCd }));
-            store.commit("setRoles", roles);
-            
-            // localStorage에 세션 정보도 저장
-            localStorage.setItem("mbrId", mbr.mbrId);
-            localStorage.setItem("mbrNm", mbr.mbrNm);
-            localStorage.setItem("roles", JSON.stringify(roles));
-            
-          } else {
-            alert("알 수 없는 로그인 방식입니다.");
-            return;
-          }
+        if (res.status !== 200) return;
 
-          // 페이지 이동 처리
-          await this.handleLoginSuccess();
+        const loginType = res.headers['login-type'];
+        
+        if (loginType === 'J') {
+          this.handleJWTLogin(res);
+        } else if (loginType === 'S') {
+          this.handleSessionLogin(res);
+        } else {
+          alert("알 수 없는 로그인 방식입니다.");
+          return;
         }
+
+        await this.handleLoginSuccess();
+        
       } catch (error) {
-        console.error("로그인 에러:", error);
         alert(error.response?.data?.message || "로그인에 실패했습니다.");
       }
     },
 
-    // 로그인 성공 후 페이지 이동 처리
-    async handleLoginSuccess() {
-      const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
-      let redirectUrl = this.restoreRedirect("redirect");
+    handleJWTLogin(res) {
+      const token = res.headers.authorization;
+      const userInfo = this.parseJWT(token);
       
-      if (redirectUrl && redirectUrl !== "") {
-        // 특정 페이지들은 메인으로 리다이렉트
-        if (redirectUrl.includes("/searchId.page") || redirectUrl.includes("/resetPswd.page") || redirectUrl.includes("/login.page")) {
-          redirectUrl = this.$filters.ctxPath("/");
-        }
-        
-        // Context Path 처리
-        if (!redirectUrl.startsWith(store.state.contextPath) && store.state.contextPath) {
-          redirectUrl = this.$filters.ctxPath(redirectUrl);
-        }
-        
-        
-        // 라우터에 해당 경로가 존재하는지 확인
-        const routeExists = this.$router.getRoutes().some(route => route.path === redirectUrl);
-        
-        if (routeExists) {
-          await this.$nextTick();
-          this.$router.push({ path: redirectUrl });
-        } else {
-          const defaultPath = isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/");
-          this.$router.push({ path: defaultPath });
-        }
+      this.setAuthInfo("J", token, userInfo);
+    },
+
+    handleSessionLogin(res) {
+      const userInfo = res.data;
+      const roles = userInfo.roles.map(r => ({ authority: r.authrtCd }));
+      
+      this.setAuthInfo("S", null, { ...userInfo, roles });
+    },
+
+    parseJWT(token) {
+      const base64String = token.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("")
+      );
+      return JSON.parse(jsonPayload);
+    },
+
+    setAuthInfo(loginMode, token, userInfo) {
+      // Vuex 상태 저장
+      store.commit("setLoginMode", loginMode);
+      store.commit("setAuthorization", token);
+      store.commit("setMbrId", userInfo.mbrId);
+      store.commit("setMbrNm", userInfo.mbrNm);
+      store.commit("setRoles", userInfo.roles);
+      
+      // localStorage 저장
+      localStorage.setItem("loginMode", loginMode);
+      localStorage.setItem("mbrId", userInfo.mbrId);
+      localStorage.setItem("mbrNm", userInfo.mbrNm);
+      localStorage.setItem("roles", JSON.stringify(userInfo.roles));
+      
+      if (token) {
+        localStorage.setItem("authorization", token);
       } else {
-        const defaultPath = isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/");
-        await this.$nextTick();
-        this.$router.push({ path: defaultPath });
+        localStorage.removeItem("authorization");
+      }
+    },
+
+    // ========== OAuth2 로그인 ==========
+    fnOAuthLogin(provider) {
+      const redirectUrl = this.restoreRedirect("redirect") || this.$route.fullPath;
+      sessionStorage.setItem('oauth_redirect', redirectUrl);
+      oauthLogin(provider);
+    },
+
+    async handleOAuthCallback() {
+      const { error, errorMessage, oauthSuccess, loginMode } = this.parseOAuthParams();
+      
+      if (error) {
+        this.handleOAuthError(error, errorMessage);
+        return;
       }
       
-      // redirect 세션 정리
+      if (oauthSuccess !== 'true' && oauthSuccess !== true) return;
+      
+      try {
+        store.commit("setLoginMode", loginMode || "J");
+        localStorage.setItem("loginMode", loginMode || "J");
+        
+        if (loginMode === 'J') {
+          await this.handleOAuthJWT();
+        } else if (loginMode === 'S') {
+          await this.handleOAuthSession();
+        } else {
+          throw new Error('알 수 없는 로그인 모드: ' + loginMode);
+        }
+        
+        this.cleanupOAuth();
+        await this.$nextTick();
+        await this.handleLoginSuccess();
+        
+      } catch (error) {
+        this.handleOAuthError('processing_error', error.message);
+      }
+    },
+
+    parseOAuthParams() {
+      const urlParams = new URLSearchParams(window.location.search);
+      const routeQuery = this.$route.query;
+      
+      return {
+        error: urlParams.get('error') || routeQuery.error,
+        errorMessage: urlParams.get('message') || routeQuery.message,
+        oauthSuccess: urlParams.get('oauth_success') || routeQuery.oauth_success,
+        loginMode: urlParams.get('loginMode') || routeQuery.loginMode
+      };
+    },
+
+    async handleOAuthJWT() {
+      const oauthToken = this.getCookie('oauth_access_token');
+      if (!oauthToken) {
+        throw new Error('OAuth 토큰을 찾을 수 없습니다.');
+      }
+      
+      const fullToken = oauthToken.startsWith('Bearer ') ? oauthToken : `Bearer ${oauthToken}`;
+      store.commit("setAuthorization", fullToken);
+      localStorage.setItem("authorization", fullToken);
+      this.deleteCookie('oauth_access_token');
+      
+      try {
+        const userInfo = this.parseJWT(fullToken.replace('Bearer ', ''));
+        this.setAuthInfo("J", fullToken, userInfo);
+      } catch (jwtError) {
+        await this.fetchUserInfoFromServer();
+      }
+    },
+
+    async handleOAuthSession() {
+      store.commit("setAuthorization", null);
+      localStorage.removeItem("authorization");
+      this.deleteCookie('oauth_access_token');
+      
+      await this.fetchUserInfoFromServer();
+    },
+
+    async fetchUserInfoFromServer() {
+      const userInfoRes = await getUserInfo();
+      if (!userInfoRes || userInfoRes.status !== 200) {
+        throw new Error('사용자 정보를 가져올 수 없습니다.');
+      }
+      
+      const userInfo = userInfoRes.data.data;
+      const roles = Array.isArray(userInfo.roles) ? 
+        userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) : 
+        userInfo.roles;
+      
+      const loginMode = localStorage.getItem("loginMode");
+      const token = userInfo.token || localStorage.getItem("authorization");
+      
+      this.setAuthInfo(loginMode, token, { ...userInfo, roles });
+    },
+
+    // ========== 로그인 성공 후 처리 ==========
+    async handleLoginSuccess() {
+      const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
+      let redirectUrl = this.restoreRedirect("redirect") || sessionStorage.getItem('oauth_redirect');
+      
+      // 리다이렉트 URL 정리
+      if (redirectUrl && this.shouldRedirectToMain(redirectUrl)) {
+        redirectUrl = this.$filters.ctxPath("/");
+      }
+      
+      // Context Path 처리
+      if (redirectUrl && !redirectUrl.startsWith(store.state.contextPath) && store.state.contextPath) {
+        redirectUrl = this.$filters.ctxPath(redirectUrl);
+      }
+      
+      // 라우터 존재 여부 확인 후 이동
+      const targetPath = this.getValidRedirectPath(redirectUrl, isAdmin);
+      await this.$nextTick();
+      this.$router.push({ path: targetPath });
+      
+      // 세션 정리
       sessionStorage.removeItem("redirect");
+      sessionStorage.removeItem("oauth_redirect");
+    },
+
+    shouldRedirectToMain(url) {
+      return url.includes("/searchId.page") || 
+             url.includes("/resetPswd.page") || 
+             url.includes("/login.page");
+    },
+
+    getValidRedirectPath(redirectUrl, isAdmin) {
+      if (redirectUrl) {
+        const routeExists = this.$router.getRoutes().some(route => route.path === redirectUrl);
+        if (routeExists) return redirectUrl;
+      }
+      
+      return isAdmin ? 
+        this.$filters.ctxPath("/adm/main.page") : 
+        this.$filters.ctxPath("/");
+    },
+
+    // ========== 에러 처리 및 정리 ==========
+    handleOAuthError(error, errorMessage) {
+      this.isOAuthLoading = false;
+      this.cleanupOAuth();
+      
+      const message = decodeURIComponent(errorMessage || error || 'OAuth 로그인에 실패했습니다.');
+      alert(`소셜 로그인 실패: ${message}`);
+      
+      this.$router.push({ path: this.$filters.ctxPath("/login.page") });
+    },
+
+    cleanupOAuth() {
+      sessionStorage.removeItem('oauth_redirect');
+      sessionStorage.removeItem('oauth_provider');
+      sessionStorage.removeItem('oauth_start_time');
+      this.isOAuthLoading = false;
+      
+      const cleanUrl = window.location.pathname;
+      window.history.replaceState({}, document.title, cleanUrl);
+    },
+
+    // ========== 유틸리티 메서드 ==========
+    getCookie(name) {
+      const value = `; ${document.cookie}`;
+      const parts = value.split(`; ${name}=`);
+      return parts.length === 2 ? parts.pop().split(';').shift() : null;
+    },
+
+    deleteCookie(name) {
+      document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
     },
 
     moveSearchId() {
       this.$router.push({
         path: this.$filters.ctxPath("/resetPswd.page"),
-        query: {
-          tab: "id",
-        },
-      });
-    },
-    moveResetPswd() { 
-      this.$router.push({
-        path: this.$filters.ctxPath("/resetPswd.page"),
-        query: {
-          tab: "pw",
-        },
+        query: { tab: "id" }
       });
     },
     
- // OAuth2 로그인 성공 후 처리
-async handleOAuthCallback() {
-  const urlParams = new URLSearchParams(window.location.search);
-  const error = urlParams.get('error');
-  
-  if (error) {
-    alert('소셜 로그인에 실패했습니다: ' + decodeURIComponent(urlParams.get('message') || error));
-    return;
-  }
-  
-  // URL에 OAuth 관련 파라미터가 있는 경우
-  if (this.$route.query.oauth_success) {
-    try {
-      console.log("OAuth2 로그인 성공! 간단한 세션 처리를 시작합니다.");
-      
-      // OAuth는 일반적으로 세션 방식이므로 세션으로 설정
-      store.commit("setLoginMode", "S");
-      localStorage.setItem("loginMode", "S");
-      
-      // 기본 사용자 정보 설정 - 서버에서 세션에 저장된 정보 사용
-      // 실제 사용자 정보는 다른 API나 페이지 로드 시 가져올 수 있음
-      const defaultMbrId = 'oauth_user_' + Date.now(); // 임시 ID
-      const defaultMbrNm = 'OAuth 사용자';
-      const defaultRoles = [{ authority: 'ROLE_USER' }];
-      
-      store.commit("setAuthorization", null);
-      store.commit("setMbrId", defaultMbrId);
-      store.commit("setMbrNm", defaultMbrNm);
-      store.commit("setRoles", defaultRoles);
-      
-      // localStorage에 정보 저장
-      localStorage.setItem("mbrId", defaultMbrId);
-      localStorage.setItem("mbrNm", defaultMbrNm);
-      localStorage.setItem("roles", JSON.stringify(defaultRoles));
-      
-      console.log("OAuth 세션 로그인 완료 - Store 상태:", {
-        authorization: store.state.authorization,
-        mbrId: store.state.mbrId,
-        mbrNm: store.state.mbrNm,
-        roles: store.state.roles,
-        loginMode: store.state.loginMode
+    moveResetPswd() { 
+      this.$router.push({
+        path: this.$filters.ctxPath("/resetPswd.page"),
+        query: { tab: "pw" }
       });
-
-      // 페이지 이동 처리
-      await this.$nextTick();
-      
-      // 리다이렉트 URL 처리
-      const redirectUrl = sessionStorage.getItem('oauth_redirect');
-      sessionStorage.removeItem('oauth_redirect');
-      
-      if (redirectUrl && redirectUrl !== '/login' && !redirectUrl.includes('/login.page')) {
-        console.log("OAuth 리다이렉트:", redirectUrl);
-        this.$router.replace({ path: redirectUrl });
-      } else {
-        // 메인 페이지로 이동
-        const mainPath = this.$filters.ctxPath("/");
-        console.log("OAuth 메인 페이지로 이동:", mainPath);
-        this.$router.replace({ path: mainPath });
-      }
-      
-    } catch (error) {
-      console.error('OAuth 콜백 처리 실패:', error);
-      alert('OAuth 로그인 처리 중 오류가 발생했습니다: ' + (error.message || '알 수 없는 오류'));
-      
-      // 에러 발생 시 로그인 페이지로 이동
-      store.commit("setStoreReset");
-      this.$router.push({ path: this.$filters.ctxPath("/login.page") });
     }
   }
-}
-  },
-  watch: {},
-  computed: {},
-  components: {},
-  created() {
-    this.checkAdminPage();
-    // OAuth2 콜백 처리
-    this.handleOAuthCallback();
-  },
-  mounted() {},
 };
 </script>
(파일 끝에 줄바꿈 문자 없음)
Add a comment
List