하석형 하석형 05-26
250526 하석형 관리자로그인 2차인증
@edc249b459356a785b731a4f03a07d6b300bf9aa
 
client/resources/api/email.js (added)
+++ client/resources/api/email.js
@@ -0,0 +1,9 @@
+import apiClient from "./index";
+
+export const check2ndAuthProc = email => {
+    return apiClient.post(`/sys/email/check2ndAuthEmailVerifyCode.json`, email);
+}
+
+export const sendAuthEmailProc = email => {
+    return apiClient.post(`/sys/email/sendEmailVerifyCode.json`, email);
+}(파일 끝에 줄바꿈 문자 없음)
client/views/pages/AppRouter.js
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
@@ -183,14 +183,13 @@
 
     // 로그인 모드 확인 (JWT 또는 SESSION)
     const loginMode = store.state.loginMode || 'J'; // 기본값으로 JWT 설정
-    // console.log('loginMode', loginMode)
     // 로그인 상태 확인 (JWT 또는 SESSION)
     const isLogin = loginMode === 'J' ? store.state.authorization : store.state.mbrId;
-    if (!isLogin && to.path !== filters.ctxPath('/login.page')) {
-      sessionStorage.setItem('redirect', to.fullPath); 
-      next({ path: filters.ctxPath("/login.page") });
-      return; 
-    }
+    // if (!isLogin && to.path !== filters.ctxPath('/login.page')) {
+    //   sessionStorage.setItem('redirect', to.fullPath); 
+    //   next({ path: filters.ctxPath("/login.page") });
+    //   return; 
+    // }
 
     // 접근 제어 확인
     const accesCheck = await accessUrl(to.path);
@@ -255,7 +254,7 @@
             return false;
           }
         });
-      }); 
+      });
       // 권한이 있고 접근 가능한 경우
       if (hasAcc) {
         if (to.path.includes('.page')) {
@@ -285,8 +284,10 @@
         // next(from.fullPath ? from.fullPath : '/');
       }
     } else {
-      // sessionStorage.setItem("redirect", to.fullPath);
-      next({ path: filters.ctxPath("/login.page") });
+      if(admPath) {
+        // sessionStorage.setItem("redirect", to.fullPath);
+        next({ path: filters.ctxPath("/cmslogin.page") });
+      }
     }
   });
   return AppRouter;
client/views/pages/login/AdminLogin.vue
--- client/views/pages/login/AdminLogin.vue
+++ client/views/pages/login/AdminLogin.vue
@@ -1,195 +1,264 @@
 <template>
-  <div class="login-page page">
-      <div>
-        <div class="admin-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"
-              >
-                로그인
-              </button>
+    <div class="login-page page" :class="{ 'loading-blur': isLoading }" :style="{ 'cursor': isLoading ? 'wait' : 'default' }">
+        <div>
+            <div class="admin-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>
+                    <div class="input-group" v-if="!isAdminPage">
+                        <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
+                        <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
+                    </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>
-  </div>
+    </div>
 </template>
 
 <script>
 import { useStore } from "vuex";
 import store from "../AppStore";
 import { loginProc } 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,
-      },
-      store: useStore(),
-      isAdminPage: false,
-    };
-  },
-  methods: {
-    checkAdminPage() {
-      if (
-        this.restoreRedirect("redirect") &&
-        this.restoreRedirect("redirect").includes("/adm/")
-      ) {
-        this.isAdminPage = true;
-      } else {
-        this.isAdminPage = false;
-      }
-    },
-    async fnLogin() {
-      try {
-        const res = await loginProc(this.member);
-        if (res.status == 200) {
-          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("/")
-              });
-          }
+    mixins: [queryParams],
+    data: () => {
+        return {
+            isLoading: false,
+            member: {
+                lgnId: null,
+                pswd: null,
+                lgnReqPage: 'A'
+            },
+            store: useStore(),
+            isAdminPage: false,
 
-          
-        }
-      } catch (error) {
-        alert(error.response.data.message);
-      }
+            memberInfo: {
+                email: '', // 이메일
+                code: '', // 인증코드
+            },
+
+            // 인증 절차
+            loginStep1: false, // 1차 인증
+        };
     },
-    moveSearchId() {
-      this.$router.push({
-        path: this.$filters.ctxPath("/resetPswd.page"),
-        query: {
-          tab: "id",
+    methods: {
+        init() {
+            this.member = {
+                lgnId: null,
+                pswd: null,
+                lgnReqPage: 'A'
+            };
+            this.memberInfo = {
+                mbrId: '',
+                email: '',
+                code: '',
+            };
+            this.loginStep1 = false;
         },
-      });
-    },
-    moveResetPswd() { 
-      this.$router.push({
-        path: this.$filters.ctxPath("/resetPswd.page"),
-        query: {
-          tab: "pw",
+        checkAdminPage() {
+            if (
+                this.restoreRedirect("redirect") &&
+                this.restoreRedirect("redirect").includes("/adm/")
+            ) {
+                this.isAdminPage = true;
+            } else {
+                this.isAdminPage = false;
+            }
         },
-      });
+        async fnLogin() {
+            this.isLoading = true;
+            try {
+                const res = await loginProc(this.member);
+                if (res.status == 200) {
+                    this.memberInfo = res.data; // 인증코드 전송을 위한 이메일 정보 저장
+                    this.loginStep1 = true; // 1차 인증 성공
+                }
+            } 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;
+            }
+        },
+        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",
+                },
+            });
+        },
+
+        // 인증코드 입력
+        inputCode(event) {
+            const input = event.target.value.replace(/[^0-9]/g, '');
+            this.code = input;
+        },
+
+        // 인증코드 확인
+        async fnCheck() {
+            const res = await check2ndAuthProc(this.memberInfo);
+            if (res.status == 200) {
+                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("/")
+                    });
+                }
+            } else {
+                const errorData = error.response.data;
+                if (errorData.message != null && errorData.message != "") {
+                    alert(error.response.data.message);
+                } 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;
+            }
+        },
     },
-  },
-  watch: {},
-  computed: {},
-  components: {},
-  created() {
-    this.checkAdminPage();
-  },
-  mounted() {},
+    watch: {},
+    computed: {},
+    components: {},
+    created() {
+        this.init();
+        this.checkAdminPage();
+    },
+    mounted() { },
 };
 </script>
+
+<style scoped>
+.loading-blur {
+    pointer-events: none;
+    opacity: 0.4;
+    filter: grayscale(20%) blur(1px);
+}
+</style>
Add a comment
List