하석형 하석형 05-28
250528 하석형 사용자로그인페이지에서 관리자계정 로그인 시 2차인증 로직 추가
@237218bf7ba875f5abd826d624a02a736d990cb0
client/views/pages/AppRouter.js
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
@@ -229,7 +229,7 @@
       }
       store.commit('setPath', path);
       store.commit('setPageAuth', pageAuth);
-      if (path.includes('/main.page')) {
+      if (to.path === filters.ctxPath('/') || path.includes('/main.page')) {
         await cntnStatsSave(null, mbrAuth); // 메인 페이지 접속 시 사용자 접속 통계 증가
       }
       next();
client/views/pages/adm/member/userManagement/UserManagementSelectList.vue
--- client/views/pages/adm/member/userManagement/UserManagementSelectList.vue
+++ client/views/pages/adm/member/userManagement/UserManagementSelectList.vue
@@ -158,7 +158,7 @@
       // 실행
       try {
         const response = await mbrListProc(toRaw(data));
-        this.mblTelnoSplit(response.data.data.list);
+        // this.mblTelnoSplit(response.data.data.list);
         this.list = response.data.data.list;
         this.search = response.data.data.pagination;
         this.makeTbody();
@@ -246,7 +246,7 @@
       // 실행
       try {
         const response = await insertProc(data);
-        alert(response.data["message"]);
+        // alert(response.data["message"]);
         this.fnViewDetail(data);
       } catch (error) {
         const errorData = error.response.data;
client/views/pages/adm/system/LoginPolicy/LoginPolicy.vue
--- client/views/pages/adm/system/LoginPolicy/LoginPolicy.vue
+++ client/views/pages/adm/system/LoginPolicy/LoginPolicy.vue
@@ -47,22 +47,6 @@
               </div>
             </div>
             <div class="layout">
-              <label class="form-title">Context Path 설정</label>
-              <div class="form-group">
-                <div class="check-area">
-                  <div class="form-check">
-                    <input type="text" id="cntxtPth" class="form-control sm" v-model="cntxtPth" ref="cntxtPth" />
-                  </div>
-                  <button class="btn sm main" @click="saveByContextPath">저장</button>
-                </div>
-                <span class="ml10 gray">
-                  <strong>/</strong> 또는 <strong>/경로</strong> 형식으로 입력하세요.
-                </span>
-              </div>
-            </div>
-            <div class="layout">
-            </div>
-            <div class="layout">
               <label class="form-title">이메일 2차 인증 설정</label>
               <div class="form-group">
                 <div class="check-area">
@@ -80,6 +64,22 @@
                 <!--    <p>{{ allowMultipleLogin ? '중복 로그인을 허용하고 있습니다.' : '중복 로그인을 허용하지 않습니다.' }}</p>-->
               </div>
             </div>
+            <div class="layout">
+            </div>
+            <div class="layout">
+              <label class="form-title">Context Path 설정</label>
+              <div class="form-group">
+                <div class="check-area">
+                  <div class="form-check">
+                    <input type="text" id="cntxtPth" class="form-control sm" v-model="cntxtPth" ref="cntxtPth" />
+                  </div>
+                  <button class="btn sm main" @click="saveByContextPath">저장</button>
+                </div>
+                <span class="ml10 gray">
+                  <strong>/</strong> 또는 <strong>/경로</strong> 형식으로 입력하세요.
+                </span>
+              </div>
+            </div>
           </div>
         </div>
       </div>
client/views/pages/login/AdminLogin.vue
--- client/views/pages/login/AdminLogin.vue
+++ client/views/pages/login/AdminLogin.vue
@@ -43,8 +43,9 @@
                     <p>인증코드 입력</p>
                 </div>
                 <div>
-                    <p>{{memberInfo.email}}로 전송된 6자리 인증코드를 입력하세요.</p>
-                    <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="memberInfo.code" maxlength="6" placeholder="인증코드를 입력하세요."/>
+                    <p>{{ memberInfo.email }}로 전송된 6자리 인증코드를 입력하세요.</p>
+                    <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="memberInfo.code"
+                        maxlength="6" placeholder="인증코드를 입력하세요." />
                 </div>
                 <div class="btn-wrap">
                     <button class="btn sm main" @click="fnCheck">인증코드 확인</button>
@@ -119,12 +120,12 @@
                 const res = await loginProc(this.member);
                 if (res.status == 200) {
                     // 2차 인증에 필요한 이메일 정보가 있는지 확인
-                    if(res.data.email) {
+                    if (res.data.email) {
                         this.memberInfo = res.data; // 인증코드 전송을 위한 이메일 정보 저장
-                    // 없을 경우 2차 인증 패스
+                        // 없을 경우 2차 인증 패스
                     } else {
                         this.loginStep2 = true; // 2차 인증 패스
-                        this.loginSuccess(res); // 로그인 성공 처리
+                        this.loginSuccessProc(res); // 로그인 성공 처리
                     }
                     this.loginStep1 = true; // 1차 인증 성공
                 }
@@ -167,64 +168,7 @@
             try {
                 const res = await check2ndAuthProc(this.memberInfo);
                 if (res.status == 200) {
-                    this.loginSuccess(res); // 로그인 성공 처리
-                    // const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분
-                    // if (loginType === 'J') {
-                    //     // JWT 방식
-                    //     store.commit("setAuthorization", res.headers.authorization);
-                    //     store.commit("setLoginMode", "J");
-                    //     localStorage.setItem("loginMode", "J");
-                    //     const base64String = store.state.authorization.split(".")[1];
-                    //     const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
-                    //     const jsonPayload = decodeURIComponent(
-                    //         atob(base64).split("").map((c) => {
-                    //             return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
-                    //         }).join("")
-                    //     );
-                    //     const mbr = JSON.parse(jsonPayload);
-                    //     store.commit("setMbrId", mbr.mbrId);
-                    //     store.commit("setMbrNm", mbr.mbrNm);
-                    //     store.commit("setRoles", 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);
-                    // } else {
-                    //     alert("알 수 없는 로그인 방식입니다.");
-                    //     return;
-                    // }
-                    // const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
-                    // let url = this.restoreRedirect("redirect");
-                    // if (url != null && url != "") {
-                    //     const ctx = store.state.contextPath;
-                    //     if (ctx !== "") {
-                    //         // redirect 값에서 Context Path 추가
-                    //         url = this.$filters.ctxPath(url);
-                    //     } else {
-                    //         // redirect 값에서 기존 Context Path 제거
-                    //         url = url.replace(/^\/[^\/]+/, ""); // 첫 번째 '/' 이후의 경로만 남김
-                    //     }
-                    //     const routeExists = this.$router.getRoutes().some(route => route.path === url);
-
-                    //     if (url == this.$filters.ctxPath("/searchId.page") || url == this.$filters.ctxPath("/resetPswd.page")) {
-                    //         this.$router.push({ path: this.$filters.ctxPath("/main.page") });
-                    //     } else if (routeExists) {
-                    //         this.$router.push({ path: url });
-                    //     } else {
-                    //         this.$router.push({
-                    //             path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
-                    //         });
-                    //     }
-                    // } else {
-                    //     this.$router.push({
-                    //         path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
-                    //     });
-                    // }
+                    await this.loginSuccessProc(res); // 로그인 성공 처리
                 }
             } catch (error) {
                 const errorData = error.response.data;
@@ -238,67 +182,167 @@
         },
 
         // 로그인 성공 시
-        loginSuccess(res) {
+        async loginSuccessProc(res) {
             const loginType = res.headers['login-type']; // 세션/토큰 로그인 구분
             if (loginType === 'J') {
+                this.handleJWTLogin(res);
                 // JWT 방식
-                store.commit("setAuthorization", res.headers.authorization);
-                store.commit("setLoginMode", "J");
-                localStorage.setItem("loginMode", "J");
-                const base64String = store.state.authorization.split(".")[1];
-                const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
-                const jsonPayload = decodeURIComponent(
-                    atob(base64).split("").map((c) => {
-                        return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
-                    }).join("")
-                );
-                const mbr = JSON.parse(jsonPayload);
-                store.commit("setMbrId", mbr.mbrId);
-                store.commit("setMbrNm", mbr.mbrNm);
-                store.commit("setRoles", mbr.roles);
+                // store.commit("setAuthorization", res.headers.authorization);
+                // store.commit("setLoginMode", "J");
+                // localStorage.setItem("loginMode", "J");
+                // const base64String = store.state.authorization.split(".")[1];
+                // const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
+                // const jsonPayload = decodeURIComponent(
+                //     atob(base64).split("").map((c) => {
+                //         return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
+                //     }).join("")
+                // );
+                // const mbr = JSON.parse(jsonPayload);
+                // store.commit("setMbrId", mbr.mbrId);
+                // store.commit("setMbrNm", mbr.mbrNm);
+                // store.commit("setRoles", 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);
+                this.handleSessionLogin(res);
+                // 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);
             } else {
                 alert("알 수 없는 로그인 방식입니다.");
                 return;
             }
 
-            const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
-            let url = this.restoreRedirect("redirect");
-            if (url != null && url != "") {
-                const ctx = store.state.contextPath;
-                if (ctx !== "") {
-                    // redirect 값에서 Context Path 추가
-                    url = this.$filters.ctxPath(url);
-                } else {
-                    // redirect 값에서 기존 Context Path 제거
-                    url = url.replace(/^\/[^\/]+/, ""); // 첫 번째 '/' 이후의 경로만 남김
-                }
-                const routeExists = this.$router.getRoutes().some(route => route.path === url);
+            await this.handleLoginSuccess();
+            // const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
+            // let url = this.restoreRedirect("redirect");
+            // console.log("리다이렉트 URL:", url, isAdmin);
+            // if (url != null && url != "") {
+            //     const ctx = store.state.contextPath;
+            //     console.log("Context Path:", ctx);
+            //     if (ctx !== "") {
+            //         console.log("Context Path가 설정되어 있습니다.");
+            //         // redirect 값에서 Context Path 추가
+            //         url = this.$filters.ctxPath(url);
+            //     } else {
+            //         console.log("Context Path가 설정되어 있지 않습니다.");
+            //         // redirect 값에서 기존 Context Path 제거
+            //         url = url.replace(/^\/[^\/]+/, ""); // 첫 번째 '/' 이후의 경로만 남김
+            //     }
+            //     console.log("Context Path 처리 후 리다이렉트 URL:", url);
+            //     const routeExists = this.$router.getRoutes().some(route => route.path === url);
+            //     console.log("리다이렉트 경로 존재 여부:", routeExists);
 
-                if (url == this.$filters.ctxPath("/searchId.page") || url == this.$filters.ctxPath("/resetPswd.page")) {
-                    this.$router.push({ path: this.$filters.ctxPath("/main.page") });
-                } else if (routeExists) {
-                    this.$router.push({ path: url });
-                } else {
-                    this.$router.push({
-                        path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
-                    });
-                }
+            //     if (url == this.$filters.ctxPath("/searchId.page") || url == this.$filters.ctxPath("/resetPswd.page")) {
+            //         this.$router.push({ path: this.$filters.ctxPath("/main.page") });
+            //     } else if (routeExists) {
+            //         this.$router.push({ path: url });
+            //     } else {
+            //         this.$router.push({
+            //             path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
+            //         });
+            //     }
+            // } else {
+            //     this.$router.push({
+            //         path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
+            //     });
+            // }
+        },
+
+        // JWT 로그인 처리
+        handleJWTLogin(res) {
+            const token = res.headers.authorization;
+            const userInfo = this.parseJWT(token);
+
+            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 });
+        },
+
+        // JWT 토큰 파싱
+        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 {
-                this.$router.push({
-                    path: isAdmin ? this.$filters.ctxPath("/adm/main.page") : this.$filters.ctxPath("/")
-                });
+                localStorage.removeItem("authorization");
             }
         },
 
+        // ========== 로그인 성공 후 처리 ==========
+        async handleLoginSuccess() {
+            const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
+            let redirectUrl = this.restoreRedirect("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");
+        },
+
+        // 리다이렉트 URL이 메인 페이지로 이동해야 하는지 확인
+        shouldRedirectToMain(url) {
+            return url.includes("/searchId.page") ||
+                url.includes("/resetPswd.page") ||
+                url.includes("/login.page");
+        },
+
+        // 유효한 리다이렉트 경로를 반환
+        getValidRedirectPath(redirectUrl, isAdmin) {
+            if (redirectUrl && !redirectUrl.includes("login.page")) {
+                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("/");
+        },
+
         // 인증코드 재전송
         async fnResend() {
             this.isLoading = true;
client/views/pages/login/Login.vue
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
@@ -1,107 +1,97 @@
 <template>
-  <div class="login-page page">
-      <div>
-        <div class="background-img">
-          <p>
-            어서오세요.<br />
-            로그인하세요.
-          </p>
-        </div>
-        <div class="login-wrap">
-            <div class="login">
-              <div
-                :class="{
-                  'login-title': true,
-                  'user-login': !isAdminPage,
-                }"
-              >
-                LOGIN
-              </div>
-              <div class="form-group">
-                <label for="id" class="login-label">아이디</label>
-                <input
-                  type="text"
-                  name=""
-                  id="id"
-                  class="form-control md"
-                  placeholder="아이디를 입력하세요"
-                  v-model="member['lgnId']"
-                />
-              </div>
-              <div class="form-group">
-                <label for="pw" class="login-label">비밀번호</label>
-                <input
-                  type="password"
-                  name=""
-                  id="pw"
-                  class="form-control md"
-                  placeholder="비밀번호를 입력하세요"
-                  v-model="member['pswd']"
-                  @keydown.enter="fnLogin"
-                />
-              </div>
-              <button
-                class="btn md main user-btn"
-                v-if="!isAdminPage"
-                @click="fnLogin"
-                @keydown.enter="fnLogin"
-              >
-                로그인
-              </button>
-              <button
-                class="btn md main"
-                v-else
-                @click="fnLogin"
-                @keydown.enter="fnLogin"
-              >
-                로그인
+  <div class="login-page page" :class="{ 'loading-blur': isLoading }" :style="{ 'cursor': isLoading ? 'wait' : '' }">
+    <div>
+      <div class="background-img">
+        <p>
+          어서오세요.<br />
+          로그인하세요.
+        </p>
+      </div>
+      <div class="login-wrap" v-if="!loginStep1 && !loginStep2">
+        <div class="login">
+          <div :class="{
+            'login-title': true,
+            'user-login': !isAdminPage,
+          }">
+            LOGIN
+          </div>
+          <div class="form-group">
+            <label for="id" class="login-label">아이디</label>
+            <input type="text" name="" id="id" class="form-control md" placeholder="아이디를 입력하세요"
+              v-model="member['lgnId']" />
+          </div>
+          <div class="form-group">
+            <label for="pw" class="login-label">비밀번호</label>
+            <input type="password" name="" id="pw" class="form-control md" placeholder="비밀번호를 입력하세요"
+              v-model="member['pswd']" @keydown.enter="fnLogin" />
+          </div>
+          <button class="btn md main user-btn" v-if="!isAdminPage" @click="fnLogin" @keydown.enter="fnLogin">
+            로그인
+          </button>
+          <button class="btn md main" v-else @click="fnLogin" @keydown.enter="fnLogin">
+            로그인
+          </button>
+
+          <div class="input-group" v-if="!isAdminPage">
+            <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
+            <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
+          </div>
+
+          <!-- OAuth2 로그인 버튼들 -->
+          <div v-if="!isAdminPage">
+            <div>
+              <span>또는</span>
+            </div>
+            <div>
+              <button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading">
+                카카오로 로그인
               </button>
 
-              <div
-                class="input-group"
-                v-if="!isAdminPage"
-              >
-                <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
-                <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
-              </div>
-              
-              <!-- OAuth2 로그인 버튼들 -->
-              <div v-if="!isAdminPage">
-                <div>
-                  <span>또는</span>
-                </div>
-                <div>
-                  <button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading">
-                    카카오로 로그인
-                  </button>
-                  
-                  <button  @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading">
-                    네이버로 로그인
-                  </button>
-                  
-                  <button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading">
-                    구글로 로그인
-                  </button>
-                </div>
-              </div>
+              <button @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading">
+                네이버로 로그인
+              </button>
+
+              <button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading">
+                구글로 로그인
+              </button>
             </div>
+          </div>
+        </div>
+      </div>
+      <div class="login-wrap" v-else-if="loginStep1 && !loginStep2">
+        <div>
+          <p>인증코드 입력</p>
+        </div>
+        <div>
+          <p>{{memberInfo.email}}로 전송된 6자리 인증코드를 입력하세요.</p>
+          <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="memberInfo.code" maxlength="6" placeholder="인증코드를 입력하세요."/>
+        </div>
+        <div class="btn-wrap">
+          <button class="btn sm main" @click="fnCheck">인증코드 확인</button>
+        </div>
+        <div>
+          <p>인증코드를 받지 못하셨나요?</p>
+          <button class="btn sm tertiary" @click="fnResend">인증코드 다시받기</button>
         </div>
       </div>
     </div>
+  </div>
 </template>
 
 <script>
 import { useStore } from "vuex";
 import store from "../AppStore";
 import { loginProc, getUserInfo, oauthLogin } from "../../../resources/api/login";
+import { check2ndAuthProc, sendAuthEmailProc } from "../../../resources/api/email";
 import queryParams from "../../../resources/js/queryParams";
 
 export default {
   mixins: [queryParams],
-  
+
   data() {
     return {
-      member: { lgnId: null, pswd: null },
+      isLoading: false,
+      member: { lgnId: null, pswd: null, lgnReqPage: 'U' },
       store: useStore(),
       isAdminPage: false,
       isOAuthLoading: false,
@@ -110,6 +100,11 @@
       //   { name: 'naver', label: '네이버로 로그인' },
       //   { name: 'google', label: '구글로 로그인' }
       // ]
+
+      memberInfo: { email: '', code: '' },
+      // 인증 절차
+      loginStep1: false, // 1차 인증
+      loginStep2: false, // 2차 인증
     };
   },
 
@@ -144,47 +139,117 @@
     },
 
     hasOAuthParams() {
-      return window.location.search.includes('oauth_success') || 
-             window.location.search.includes('error') ||
-             this.$route.query.oauth_success || 
-             this.$route.query.error;
+      return window.location.search.includes('oauth_success') ||
+        window.location.search.includes('error') ||
+        this.$route.query.oauth_success ||
+        this.$route.query.error;
     },
 
     // ========== 일반 로그인 ==========
     async fnLogin() {
+      this.isLoading = true;
       try {
         const res = await loginProc(this.member);
         if (res.status !== 200) return;
 
-        const loginType = res.headers['login-type'];
-        
-        if (loginType === 'J') {
-          this.handleJWTLogin(res);
-        } else if (loginType === 'S') {
-          this.handleSessionLogin(res);
+        // 2차 인증에 필요한 이메일 정보가 있는지 확인
+        if (res.data.email) {
+          this.memberInfo = res.data; // 인증코드 전송을 위한 이메일 정보 저장
+          // 없을 경우 2차 인증 패스
         } else {
-          alert("알 수 없는 로그인 방식입니다.");
-          return;
+          this.loginStep2 = true; // 2차 인증 패스
+          await this.loginSuccessProc(res); // 로그인 성공 처리
         }
+        this.loginStep1 = true; // 1차 인증 성공
 
-        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) {
         alert(error.response?.data?.message || "로그인에 실패했습니다.");
+      } finally {
+        this.isLoading = false;
       }
     },
 
+    // 인증코드 입력
+    inputCode(event) {
+      const input = event.target.value.replace(/[^0-9]/g, '');
+      this.memberInfo.code = input;
+    },
+
+    // 인증코드 확인
+    async fnCheck() {
+      try {
+          const res = await check2ndAuthProc(this.memberInfo);
+          if (res.status == 200) {
+              await this.loginSuccessProc(res); // 로그인 성공 처리
+          }
+      } catch (error) {
+          const errorData = error.response.data;
+          if (errorData.message != null && errorData.message != "") {
+              alert(error.response.data.message);
+              this.$refs.code.focus();
+          } else {
+              alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
+          }
+      }
+    },
+
+    // 인증코드 재전송
+    async fnResend() {
+      this.isLoading = true;
+      try {
+          const res = await sendAuthEmailProc(this.memberInfo);
+          if (res.status == 200) {
+              alert(res.data.message);
+          }
+      } catch (error) {
+          const errorData = error.response.data;
+          if (errorData.message != null && errorData.message != "") {
+              alert(error.response.data.message);
+          } else {
+              alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
+          }
+      } finally {
+          this.isLoading = false;
+      }
+    },
+
+    // 로그인 성공 시
+    async loginSuccessProc(res) {
+      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();
+    },
     handleJWTLogin(res) {
       const token = res.headers.authorization;
       const userInfo = this.parseJWT(token);
-      
+
       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 });
     },
 
@@ -206,13 +271,13 @@
       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 {
@@ -229,18 +294,18 @@
 
     async handleOAuthCallback() {
       const { error, errorMessage, oauthSuccess, loginMode } = this.parseOAuthParams();
-      
+
       if (error) {
         this.handleOAuthError(error, errorMessage);
         return;
       }
-      
+
       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') {
@@ -248,11 +313,11 @@
         } else {
           throw new Error('알 수 없는 로그인 모드: ' + loginMode);
         }
-        
+
         this.cleanupOAuth();
         await this.$nextTick();
         await this.handleLoginSuccess();
-        
+
       } catch (error) {
         this.handleOAuthError('processing_error', error.message);
       }
@@ -261,7 +326,7 @@
     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,
@@ -275,12 +340,12 @@
       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);
@@ -293,7 +358,7 @@
       store.commit("setAuthorization", null);
       localStorage.removeItem("authorization");
       this.deleteCookie('oauth_access_token');
-      
+
       await this.fetchUserInfoFromServer();
     },
 
@@ -302,15 +367,15 @@
       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 })) : 
+      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 });
     },
 
@@ -318,41 +383,41 @@
     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");
+      return url.includes("/searchId.page") ||
+        url.includes("/resetPswd.page") ||
+        url.includes("/login.page");
     },
 
     getValidRedirectPath(redirectUrl, isAdmin) {
-      if (redirectUrl) {
+      if (redirectUrl && !redirectUrl.includes("login.page")) {
         const routeExists = this.$router.getRoutes().some(route => route.path === redirectUrl);
         if (routeExists) return redirectUrl;
       }
-      
-      return isAdmin ? 
-        this.$filters.ctxPath("/adm/main.page") : 
+
+      return isAdmin ?
+        this.$filters.ctxPath("/adm/main.page") :
         this.$filters.ctxPath("/");
     },
 
@@ -360,10 +425,10 @@
     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") });
     },
 
@@ -372,7 +437,7 @@
       sessionStorage.removeItem('oauth_provider');
       sessionStorage.removeItem('oauth_start_time');
       this.isOAuthLoading = false;
-      
+
       const cleanUrl = window.location.pathname;
       window.history.replaceState({}, document.title, cleanUrl);
     },
@@ -394,8 +459,8 @@
         query: { tab: "id" }
       });
     },
-    
-    moveResetPswd() { 
+
+    moveResetPswd() {
       this.$router.push({
         path: this.$filters.ctxPath("/resetPswd.page"),
         query: { tab: "pw" }
@@ -403,4 +468,12 @@
     }
   }
 };
-</script>
(파일 끝에 줄바꿈 문자 없음)
+</script>
+
+<style scoped>
+.loading-blur {
+    pointer-events: none;
+    opacity: 0.4;
+    filter: grayscale(20%) blur(1px);
+}
+</style>
(파일 끝에 줄바꿈 문자 없음)
Add a comment
List