하석형 하석형 06-26
250626 하석형 아이디찾기/비밀번호찾기 기능 구현, 회원가입 이메일 인증 구현
@3cddfe6d509f5faf863a41c36427348c31882de5
client/resources/api/email.js
--- client/resources/api/email.js
+++ client/resources/api/email.js
@@ -1,9 +1,16 @@
 import apiClient from "./index";
 
+// 이메일 인증코드 발송
+export const sendAuthEmailProc = email => {
+    return apiClient.post(`/sys/email/sendEmailVerifyCode.json`, email);
+}
+
+// 이메일 인증코드 확인
+export const checkAuthEmailProc = email => {
+    return apiClient.post(`/sys/email/checkEmailVerifyCode.json`, email);
+}
+
+// 2차 인증 이메일 인증코드 확인
 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/resources/api/mbrInfo.js
--- client/resources/api/mbrInfo.js
+++ client/resources/api/mbrInfo.js
@@ -35,7 +35,17 @@
   return apiClient.post(`/mbr/searchIdProc.json`, data);
 };
 
-// 비밀번호 초기화 (공무원용)
-export const resetPswdProc = (data) => {
-  return apiClient.post(`/mbr/resetPswdProc.json`, data);
+// 회원정보 존재 확인
+export const existMbrProc = (data) => {
+  return apiClient.post(`/mbr/existMbrProc.json`, data);
+};
+
+// 새 비밀번호 설정
+export const setNewPswdProc = (data) => {
+  return apiClient.post(`/mbr/setNewPswdProc.json`, data);
+};
+
+// 이메일 중복 검사
+export const emailCheckProc = (data) => {
+  return apiClient.post(`/mbr/findByCheckEmail.json`, data);
 };
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/login/Login.vue
--- client/views/pages/login/Login.vue
+++ client/views/pages/login/Login.vue
@@ -36,8 +36,8 @@
             <!-- 아이디/비밀번호 찾기 -->
             <div class="input-group">
               <p class="pl10 pr10 cursor" @click="moveSignUp">회원가입</p>
-              <p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
-              <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
+              <p class="pl10 pr10 cursor" @click="moveSearchId">아이디 찾기</p>
+              <p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 찾기</p>
             </div>
 
             <!-- OAuth2 소셜 로그인 -->
client/views/pages/login/ResetPswd.vue
--- client/views/pages/login/ResetPswd.vue
+++ client/views/pages/login/ResetPswd.vue
@@ -1,248 +1,494 @@
 <template>
-  <div class="login-page page">
-    <div class="flex-column justify-center align-center content">
-      <div class="login-wrap pd30">
-        <div class="login-title text-ct mb40 user-login">
-          <p v-show="tabView === 'id'">아이디 찾기</p>
-          <p v-show="tabView === 'pw'">비밀번호 초기화</p>
-        </div>
-        <ul class="flex align-center find-tab mb30 pd10">
-          <li
-            v-for="(tab, idx) in tabList"
-            :key="idx"
-            @click="showTab(tab.id)"
-            :class="{
-              'gd-6 pd5 text-ct radius': true,
-              'tab-active': tabView === tab.id,
-            }"
-          >
-            {{ tab.name }}
-          </li>
-        </ul>
-        <div class="tab-content">
-          <div
-            class="flex-column justify-center content"
-            v-show="tabView === 'id'"
-            style="min-height: 275px"
-          >
-            <div>
-              <div class="input-group mb20">
-                <label for="name" class="login-label">이름</label>
-                <input
-                  type="text"
-                  name=""
-                  id="name"
-                  class="full-input login-input"
-                  v-model="mbrVO.mbrNm"
-                  placeholder="이름을 입력하세요"
-                  ref="mbrNm"
-                />
-              </div>
-              <div class="input-group mb20">
-                <label for="eml" class="login-label">이메일</label>
-                <input
-                  type="text"
-                  name=""
-                  id="eml"
-                  class="full-input login-input"
-                  v-model="mbrVO.eml"
-                  placeholder="이메일을 입력하세요"
-                  @keyup.enter="fnSearchId"
-                  ref="eml"
-                />
-              </div>
-              <button
-                class="large-btn green-btn point-font fw-bold"
-                @click="fnSearchId"
-              >
-                조회
-              </button>
+    <div class="content pt50 pb50">
+        <router-link :to="{ path: $filters.ctxPath('/login.page') }">←</router-link>
+        <div class="content w1200">
+            <div class="page-title point-font mb30">
+                <p v-show="tabView === 'id'">아이디 찾기</p>
+                <p v-show="tabView === 'pw'">비밀번호 찾기</p>
             </div>
-          </div>
-          <div
-            class="flex-column justify-center content"
-            v-show="tabView === 'pw'"
-            style="min-height: 275px"
-          >
-            <div>
-              <div class="input-group mb20">
-                <label for="lgnId" class="login-label">아이디</label>
-                <input
-                  type="text"
-                  name=""
-                  id="lgnId"
-                  class="full-input login-input"
-                  v-model="mbrVO.lgnId"
-                  placeholder="아이디를 입력하세요"
-                  ref="lgnId"
-                />
-              </div>
-              <div class="input-group mb20">
-                <label for="mbrNm" class="login-label">이름</label>
-                <input
-                  type="text"
-                  name=""
-                  id="mbrNm"
-                  class="full-input login-input"
-                  v-model="mbrVO.mbrNm"
-                  placeholder="이름을 입력하세요"
-                  ref="mbrNm"
-                />
-              </div>
-              <div class="input-group mb20">
-                <label for="eml" class="login-label">이메일</label>
-                <input
-                  type="text"
-                  name=""
-                  id="eml"
-                  class="full-input login-input"
-                  v-model="mbrVO.eml"
-                  placeholder="이메일을 입력하세요"
-                  @keyup.enter="fnSearchPswd"
-                  ref="eml"
-                />
-              </div>
-              <button
-                class="large-btn green-btn point-font fw-bold"
-                @click="fnSearchPswd"
-              >
-                초기화
-              </button>
+            <ul class="flex align-center find-tab mb30 pd10">
+                <li v-for="(tab, idx) in tabList" :key="idx" @click="showTab(tab.id)" :class="{
+                    'gd-6 pd5 text-ct radius': true,
+                    'tab-active': tabView === tab.id,
+                }" :style="{ fontWeight: tabView === tab.id ? 'bold' : 'normal' }">
+                    {{ tab.name }}
+                </li>
+            </ul>
+            <div class="tab-content">
+                <div class="flex-column justify-center content" v-if="tabView === 'id' && !certYn" style="min-height: 275px">
+                    <ul class="flex align-center find-tab mb30 pd10">
+                        <li v-for="(tab, idx) in idTabList" :key="idx" @click="showSubTab('id', tab.id)" :class="{
+                            'gd-6 pd5 text-ct radius': true,
+                            'tab-active': idTabView === tab.id,
+                        }" :style="{ fontWeight: idTabView === tab.id ? 'bold' : 'normal' }">
+                            {{ tab.name }}
+                        </li>
+                    </ul>
+                    <div class="flex-column justify-center content">
+                        <div>
+                            <div>
+                                <div v-show="idTabView === 'eml'" class="input-group mb20">
+                                    <label for="eml" class="login-label">이메일</label>
+                                    <input type="text" name="" id="eml" class="full-input login-input" v-model="mbrVO.eml"
+                                        placeholder="이메일을 입력하세요" ref="eml" />
+                                </div>
+                                <div v-show="idTabView === 'tel'" class="input-group mb20">
+                                    <label for="name" class="login-label">이름</label>
+                                    <input type="text" name="" id="name" class="full-input login-input" v-model="mbrVO.mbrNm"
+                                        placeholder="이름을 입력하세요" ref="mbrNm" />
+                                </div>
+                                <div v-show="idTabView === 'tel'" class="input-group mb20">
+                                    <label for="mblTelno" class="login-label">휴대폰 번호</label>
+                                    <input type="text" name="" id="mblTelno" class="full-input login-input" v-model="mbrVO.mblTelno"
+                                        @input="inputFormatPhone" maxlength="13" placeholder="휴대폰 번호를 입력하세요" ref="mblTelno" />
+                                </div>
+                            </div>
+                            <div>
+                                <button class="btn sm main" @click="fnSend">
+                                    {{ !sendYn ? "인증코드 발송" : "인증코드 재발송" }}
+                                </button>
+                                <span v-show="sendYn">인증코드가 발송되었습니다.</span>
+                            </div>
+                        </div>
+                        <div v-show="sendYn">
+                            <div>
+                                <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="code"
+                                    maxlength="6" placeholder="인증코드를 입력하세요." />
+                                <!-- <span v-show="certYn">인증코드가 확인되었습니다.</span> -->
+                            </div>
+                            <div class="btn-wrap">
+                                <button class="btn sm main" @click="fnCheck">인증코드 확인</button>
+                            </div>
+                        </div>
+                        <!-- <button v-show="certYn" class="btn sm main" @click="fnSearchPswd">
+                            다음
+                        </button> -->
+                    </div>
+                </div>
+                <div class="flex-column justify-center content" v-if="tabView === 'pw' && !certYn" style="min-height: 275px">
+                    <ul class="flex align-center find-tab mb30 pd10">
+                        <li v-for="(tab, idx) in pwTabList" :key="idx" @click="showSubTab('pw', tab.id)" :class="{
+                            'gd-6 pd5 text-ct radius': true,
+                            'tab-active': pwTabView === tab.id,
+                        }" :style="{ fontWeight: idTabView === tab.id ? 'bold' : 'normal' }">
+                            {{ tab.name }}
+                        </li>
+                    </ul>
+                    <div class="flex-column justify-center content">
+                        <div>
+                            <div>
+                                <div class="input-group mb20">
+                                    <label for="lgnId" class="login-label">아이디</label>
+                                    <input type="text" name="" id="lgnId" class="full-input login-input" v-model="mbrVO.lgnId"
+                                        placeholder="아이디를 입력하세요" ref="lgnId" />
+                                </div>
+                                <div v-show="pwTabView === 'eml'" class="input-group mb20">
+                                    <label for="eml" class="login-label">이메일</label>
+                                    <input type="text" name="" id="eml" class="full-input login-input" v-model="mbrVO.eml"
+                                        placeholder="이메일을 입력하세요" ref="eml" />
+                                </div>
+                                <div v-show="pwTabView === 'tel'" class="input-group mb20">
+                                    <label for="mbrNm" class="login-label">이름</label>
+                                    <input type="text" name="" id="mbrNm" class="full-input login-input" v-model="mbrVO.mbrNm"
+                                        placeholder="이름을 입력하세요" ref="mbrNm" />
+                                </div>
+                                <div v-show="pwTabView === 'tel'" class="input-group mb20">
+                                    <label for="mblTelno" class="login-label">휴대폰 번호</label>
+                                    <input type="text" name="" id="mblTelno" class="full-input login-input" v-model="mbrVO.mblTelno"
+                                        @input="inputFormatPhone" maxlength="13" placeholder="휴대폰 번호를 입력하세요" ref="mblTelno" />
+                                </div>
+                            </div>
+                            <div>
+                                <button class="btn sm main" @click="fnSend">
+                                    {{ !sendYn ? "인증코드 발송" : "인증코드 재발송" }}
+                                </button>
+                                <span v-show="sendYn">인증번호가 발송되었습니다.</span>
+                            </div>
+                        </div>
+                        <div v-show="sendYn">
+                            <div>
+                                <input type="text" class="form-control md" ref="code" @input="inputCode" v-model="code"
+                                    maxlength="6" placeholder="인증코드를 입력하세요." />
+                                <!-- <span v-show="certYn">인증코드가 확인되었습니다.</span> -->
+                            </div>
+                            <div class="btn-wrap">
+                                <button class="btn sm main" @click="fnCheck">인증코드 확인</button>
+                            </div>
+                        </div>
+                        <!-- <button v-show="certYn" class="btn sm main" @click="fnSearchPswd">
+                            다음
+                        </button> -->
+                    </div>
+                </div>
+                <div class="flex-column justify-center content" v-if="certYn" style="min-height: 275px">
+                    <div class="text-ct">
+                        <p v-show="tabView === 'id'">회원님의 아이디는 <strong>{{ mbrVO.lgnId }}</strong> 입니다.</p>
+                        <p v-show="tabView === 'pw'">새 비밀번호를 설정해주세요.</p>
+                        <div v-show="tabView === 'pw'">
+                            <div class="input-group mb20">
+                                <label for="newPswd" class="form-control sm">새 비밀번호</label>
+                                <input type="password" name="" id="newPswd" class="full-input login-input" v-model="pswd.newPswd" minlength="9"
+                                    placeholder="영문, 숫자, 특수문자 조합 9자 이상" ref="newPswd" />
+                                <span :class="{'red-text': pswd.errorPwd}">영문, 숫자, 특수문자를 조합하여 입력해주세요. (9자 이상)</span>
+                            </div>
+                            <div class="input-group mb20">
+                                <label for="newPswdChk" :class="{ 'form-control sm': true, 'error': pswd.pswdChk }">새 비밀번호확인</label>
+                                <input type="password" name="" id="newPswdChk" class="full-input login-input" v-model="pswd.newPswdChk" minlength="9"
+                                    placeholder="비밀번호 확인을 입력하세요." ref="newPswdChk" />
+                            </div>
+                            <button class="btn sm main" @click="fnSetPswd">
+                                완료
+                            </button>
+                        </div>
+                    </div>
+                </div>
             </div>
-          </div>
         </div>
-      </div>
     </div>
-  </div>
 </template>
 
 <script>
 import validateParams from "../../../resources/js/validateParams";
 // API
-import { searchIdProc } from "../../../resources/api/mbrInfo";
-import { resetPswdProc } from "../../../resources/api/mbrInfo";
+import { searchIdProc, existMbrProc, setNewPswdProc } from "../../../resources/api/mbrInfo";
+import { sendAuthEmailProc, checkAuthEmailProc } from "../../../resources/api/email";
 
 export default {
-  mixins: [validateParams],
-  data: () => {
-    return {
-      tabList: [
-        { id: "id", name: "아이디 찾기" },
-        { id: "pw", name: "비밀번호 초기화" },
-      ],
-      mbrVO: {
-        lgnId: null,
-        mbrNm: null,
-        eml: null,
-      },
-      tabView: "id",
-    };
-  },
-  created() {
-    const tab = this.$route.query.tab;
-    if (tab) {
-      this.tabView = tab;
+    mixins: [validateParams],
+    data: () => {
+        return {
+            isLoading: false, // 로딩 여부
+
+            tabList: [
+                { id: "id", name: "아이디 찾기" },
+                { id: "pw", name: "비밀번호 초기화" },
+            ],
+            tabView: "id",
+
+            idTabList: [
+                { id: "eml", name: "이메일로 찾기" },
+                { id: "tel", name: "휴대폰번호로 찾기" },
+            ],
+            idTabView: "eml",
+
+            pwTabList: [
+                { id: "eml", name: "이메일로 찾기" },
+                { id: "tel", name: "휴대폰번호로 찾기" },
+            ],
+            pwTabView: "eml",
+
+            // 회원 정보
+            mbrVO: {
+                lgnId: null,
+                mbrNm: null,
+                pswd: null,
+                eml: null,
+                mblTelno: null,
+            },
+
+            sendYn: false, // 인증코드 발송 여부
+            code: null, // 인증코드
+            certYn: false, // 인증여부
+
+            // 비밀번호 정보
+            pswd: {
+                newPswd: null,
+                newPswdChk: null,
+                errorPwd: false, // 비밀번호 유효성 검사 실패 여부
+                pswdChk: false, // 비밀번호 확인 실패 여부
+            },
+        };
+    },
+    created() {
+        const tab = this.$route.query.tab;
+        if (tab) {
+            this.tabView = tab;
+        }
+    },
+    methods: {
+        // 초기화
+        init() {
+            // 회원 정보 초기화
+            this.mbrVO = {
+                lgnId: null,
+                mbrNm: null,
+                pswd: null,
+                eml: null,
+                mblTelno: null,
+            };
+            this.sendYn = false; // 인증코드 발송 여부 초기화
+            this.code = null; // 인증코드 초기화
+            this.certYn = false; // 인증여부 초기화
+            // 비밀번호 정보 초기화
+            this.pswd = {
+                newPswd: null,
+                newPswdChk: null,
+                errorPwd: false, // 비밀번호 유효성 검사 실패 여부
+                pswdChk: false, // 비밀번호 확인 실패 여부
+            };
+        },
+
+        // 메인 탭 전환
+        showTab(content) {
+            this.init(); // 초기화
+
+            this.idTabView = "eml";
+            this.pwTabView = "eml";
+
+            this.tabView = content;
+        },
+
+        // 서브 탭 전환
+        showSubTab(type, content) {
+            this.init(); // 초기화
+
+            if (type === 'id') {
+                this.idTabView = content;
+            } else if (type === 'pw') {
+                this.pwTabView = content;
+            }
+        },
+        
+        // 아이디찾기 유효성검사
+        validationId() {
+            if (this.idTabView === "eml") {
+                // 이메일
+                if (!this.validateEmlFull(this.mbrVO.eml)) {
+                    return false;
+                }
+            } else if (this.idTabView === "tel") {
+                // 이름
+                if (!this.validateNm(this.mbrVO.mbrNm)) {
+                    return false;
+                }
+                // 휴대폰번호
+                if (!this.validateMblNo(this.mbrVO.mblTelno)) {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        // 비밀번호초기화 유효성검사
+        validationPswd() {
+            // 아이디
+            if (!this.validateId(this.mbrVO.lgnId)) {
+                return false;
+            }
+            if (this.pwTabView === "eml") {
+                // 이메일
+                if (!this.validateEmlFull(this.mbrVO.eml)) {
+                    return false;
+                }
+            } else if (this.pwTabView === "tel") {
+                // 이름
+                if (!this.validateNm(this.mbrVO.mbrNm)) {
+                    return false;
+                }
+                // 휴대폰번호
+                if (!this.validateMblNo(this.mbrVO.mblTelno)) {
+                    return false;
+                }
+            }
+            return true;
+        },
+
+        // 휴대폰 번호 입력 포맷
+        inputFormatPhone(event) {
+            let input = event.target.value.replace(/[^0-9]/g, '');
+
+            if (input.length <= 3) {
+                this.mbrVO.mblTelno = input;
+            } else if (input.length <= 6) {
+                this.mbrVO.mblTelno = input.slice(0, 3) + '-' + input.slice(3);
+            } else if (input.length <= 10) {
+                // 10자리는 3-3-4
+                this.mbrVO.mblTelno = input.slice(0, 3) + '-' + input.slice(3, 6) + '-' + input.slice(6);
+            } else {
+                // 기본은 3-4-4
+                this.mbrVO.mblTelno = input.slice(0, 3) + '-' + input.slice(3, 7) + '-' + input.slice(7, 11);
+            }
+        },
+
+        // 인증코드 발송
+        async fnSend() {
+            // 유효성검사
+            if(this.tabView === "id") {
+                if (!this.validationId()) {
+                    return;
+                }
+            } else if (this.tabView === "pw") {
+                if (!this.validationPswd()) {
+                    return;
+                }
+                if (!await this.fnExistMbr()) {
+                    return;
+                }
+            }
+            try {
+                this.sendYn = true; // 인증코드 발송
+                const res = await sendAuthEmailProc({email: this.mbrVO.eml});
+                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관리자에게 문의해주세요.");
+                }
+                this.sendYn = false; // 발송 실패시 인증코드 발송 여부 초기화
+            }
+        },
+
+        // 인증코드 입력
+        inputCode(event) {
+            const input = event.target.value.replace(/[^0-9]/g, '');
+            this.code = input;
+        },
+
+        // 인증코드 확인
+        async fnCheck() {
+            try {
+                const res = await checkAuthEmailProc({email: this.mbrVO.eml, code: this.code});
+                if (res.status == 200) {
+                    this.fnSearchId();
+                }
+            } 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관리자에게 문의해주세요.");
+                }
+            }
+        },
+
+        // axios: 아이디 찾기
+        async fnSearchId() {
+            // 유효성검사
+            // if (!this.validationId()) {
+            //     return;
+            // }
+            // 데이터 세팅
+            let data = this.mbrVO;
+            // 실행
+            try {
+                const res = await searchIdProc(data);
+                if(res.status == 200) {
+                    this.mbrVO.lgnId = res.data.data;
+                    this.certYn = true;
+                    // let isCheck = confirm("회원님의 아이디는 " + lgnId + "입니다.\n로그인 페이지로 이동하시겠습니까?");
+                    // if (isCheck) {
+                    //     this.$router.push({
+                    //         path: this.$filters.ctxPath("/login.page"),
+                    //     });
+                    // }
+                }
+            } catch (error) {
+                const errorData = error.response?.data;
+                if (errorData?.message != null && errorData?.message != "") {
+                    // alert("입력하신 정보와 일치하는 아이디가 존재하지 않습니다.");
+                    alert(error.response.data.message);
+                } else {
+                    alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
+                }
+            }
+        },
+
+        // axios: 회원정보 존재 확인
+        async fnExistMbr() {
+            // 데이터 세팅
+            let data = this.mbrVO;
+            // 실행
+            let result = false;
+            try {
+                const res = await existMbrProc(data);
+                result = res.data.data; // true or false
+            } catch (error) {
+                const errorData = error.response?.data;
+                if (errorData?.message != null && errorData?.message != "") {
+                    alert("입력하신 정보와 일치하는 회원이 존재하지 않습니다.");
+                } else {
+                    alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
+                }
+            } finally {
+                return result;
+            }
+        },
+
+        // axios: 새 비밀번호 설정
+        async fnSetPswd() {
+            // 유효성검사
+			if (!this.validatePswd(null, this.pswd)) {
+				return false;
+			}
+            // 데이터 세팅
+            this.mbrVO.pswd = this.pswd.newPswd; // 새 비밀번호 설정
+            let data = this.mbrVO;
+            // 실행
+            try {
+                const res = await setNewPswdProc(data);
+                if (res.status == 200) {
+                    alert("비밀번호가 변경되었습니다.\n변경된 비밀번호로 로그인해주세요.");
+                    this.$router.push({
+                        path: this.$filters.ctxPath("/login.page"),
+                    });
+                }
+            } catch (error) {
+                const errorData = error.response?.data;
+                if (errorData?.message != null && errorData?.message != "") {
+                    // alert("입력하신 정보와 일치하는 회원이 존재하지 않습니다.");
+                    alert(error.response.data.message);
+                } else {
+                    alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
+                }
+            }
+        },
+
+        // 비밀번호변경 검증
+		pswdCheck() {
+			if (this.pswd.newPswd != null && this.pswd.newPswd != "") {
+				const validate =
+					/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[$`~!@$!%*#^?&()\-_=+])/;
+				if (
+					!validate.test(this.pswd.newPswd)
+				) {
+					this.pswd.errorPwd = true;
+				} else if (this.pswd.newPswd != null && this.pswd.newPswd != "") {
+					this.pswd.errorPwd = false;
+				}
+			} else {
+				this.pswd.errorPwd = null;
+			}
+		},
+		// 비밀번호변경확인 검증
+		pswdChkCheck() {
+			if (this.pswd.newPswdChk != null && this.pswd.newPswdChk != "") {
+				if (this.pswd.newPswd == this.pswd.newPswdChk) {
+					this.pswd.pswdChk = false;
+				} else {
+					this.pswd.pswdChk = true;
+				}
+			} else {
+				this.pswd.pswdChk = null;
+			}
+		},
+    },
+
+    watch: {
+        // 비밀번호변경 감시
+        "pswd.newPswd"() {
+            this.pswdCheck();
+            this.pswdChkCheck();
+        },
+        // 비밀번호변경확인 감시
+            "pswd.newPswdChk"() {
+            this.pswdChkCheck();
+        },
     }
-  },
-  methods: {
-    // tab conten 보여주기
-    showTab(content) {
-      this.tabView = content;
-    },
-
-    // axios: 아이디 찾기
-    async fnSearchId() {
-      // 유효성검사
-      if (!this.validationId()) {
-        return;
-      }
-      // 데이터 세팅
-      let data = this.mbrVO;
-      // 실행
-      try {
-        const response = await searchIdProc(data);
-        let lgnId = response.data.data;
-        let isCheck = confirm(
-          "아이디 조회에 성공했습니다.\n회원님의 아이디는 " +
-            lgnId +
-            "입니다.\n로그인 페이지로 이동하시겠습니까?"
-        );
-        if (isCheck) {
-          this.$router.push({
-            path: this.$filters.ctxPath("/login.page"),
-          });
-        }
-      } catch (error) {
-        alert(
-          "입력한 정보와 일치하는 회원이 존재하지 않습니다.\n다시 시도하세요."
-        );
-        this.mbrVO = {
-          mbrNm: null,
-          eml: null,
-        };
-      }
-    },
-
-    // axios: 비밀번호 찾기
-    async fnSearchPswd() {
-      // 유효성검사
-      if (!this.validationPswd()) {
-        return;
-      }
-      // 데이터 세팅
-      let data = this.mbrVO;
-      // 실행
-      try {
-        const response = await resetPswdProc(data);
-        if (response.status == 200) {
-          let isCheck = confirm(
-            "비밀번호 초기화에 성공했습니다.\n로그인 페이지로 이동하시겠습니까?"
-          );
-          if (isCheck) {
-            this.$router.push({
-              path: this.$filters.ctxPath("/login.page"),
-            });
-          }
-        }
-      } catch (error) {
-        alert(
-          "입력한 정보와 일치하는 회원이 존재하지 않습니다.\n다시 시도하세요."
-        );
-        this.mbrVO = {
-          mbrNm: null,
-          eml: null,
-        };
-      }
-    },
-    // 아이디찾기 유효성검사
-    validationId() {
-      // 이름
-      if (!this.validateNm(this.mbrVO.mbrNm)) {
-        return false;
-      }
-      // 이메일
-      if (!this.validateEmlFull(this.mbrVO.eml)) {
-        return false;
-      }
-      return true;
-    },
-
-    // 비밀번호초기화 유효성검사
-    validationPswd() {
-      // 아이디
-      if (!this.validateNm(this.mbrVO.lgnId)) {
-        return false;
-      }
-      // 이름
-      if (!this.validateNm(this.mbrVO.mbrNm)) {
-        return false;
-      }
-      // 이메일
-      if (!this.validateEmlFull(this.mbrVO.eml)) {
-        return false;
-      }
-      return true;
-    },
-  },
 };
 </script>
client/views/pages/login/SignUp.vue
--- client/views/pages/login/SignUp.vue
+++ client/views/pages/login/SignUp.vue
@@ -1,5 +1,6 @@
 <template>
 	<div class="content pt50 pb50">
+		<router-link :to="{ path: $filters.ctxPath('/login.page') }">←</router-link>
 		<div class="content w1200">
 			<div class="page-title point-font mb30">
 				<p>회원가입</p>
@@ -65,13 +66,13 @@
 									<div class="check-area">
 										<input type="text" class="form-control sm" ref="emailId" v-model="email.id"
 											oninput="this.value = this.value.replace(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g, '');"
-											placeholder="이메일을 입력하세요." />
+											placeholder="이메일을 입력하세요." :disabled="certYn"/>
 										<div>@</div>
 										<template v-if="email.select == 'self'">
 											<input type="text" class="form-control sm" ref="emailAddress"
-												v-model="email.address" />
+												v-model="email.address" :disabled="certYn"/>
 										</template>
-										<select class="form-select sm" ref="emailSelect" v-model="email.select">
+										<select class="form-select sm" ref="emailSelect" v-model="email.select" :disabled="certYn">
 											<option value="">선택하세요</option>
 											<option value="self">직접입력</option>
 											<option value="naver.com">naver.com</option>
@@ -81,6 +82,22 @@
 											<option value="kakao.com">kakao.com</option>
 										</select>
 									</div>
+									<div v-show="!certYn">
+										<button class="btn sm main" @click="fnSend">
+											{{ !sendYn ? "인증코드 발송" : "인증코드 재발송" }}
+										</button>
+										<span v-show="sendYn">인증코드가 발송되었습니다.</span>
+										<div v-show="sendYn">
+											<div>
+												<input type="text" class="form-control md" ref="code" @input="inputCode" v-model="code"
+													maxlength="6" placeholder="인증코드를 입력하세요." />
+											</div>
+											<div class="btn-wrap">
+												<button v-show="!certYn" class="btn sm main" @click="fnCheck">인증코드 확인</button>
+											</div>
+										</div>
+									</div>
+									<span v-show="certYn">인증코드가 확인되었습니다.</span>
 								</div>
 								<div v-if="showOpt.isSmsAgree" class="layout">
 									<label class="form-title"><span>*</span>문자수신</label>
@@ -152,7 +169,7 @@
 					// 'green-btn': pageRole == 'portal',
 				}" @click="fnInsert">완료
 				</button>
-				<button class="btn sm tertiary" @click="fnCancel">취소</button>
+				<!-- <button class="btn sm tertiary" @click="fnCancel">취소</button> -->
 			</div>
 		</div>
 	</div>
@@ -165,7 +182,8 @@
 // COMPONENETS
 import UserInfoInsert from "../../component/userInfo/UserInfoInsert.vue";
 
-import { mbrDetailProc, mbrInsertProc } from "../../../resources/api/mbrInfo";
+import { mbrDetailProc, mbrInsertProc, emailCheckProc } from "../../../resources/api/mbrInfo";
+import { sendAuthEmailProc, checkAuthEmailProc } from "../../../resources/api/email";
 
 export default {
 	mixins: [validateParams],
@@ -179,6 +197,9 @@
 				mbrStts: 1, // 회원상태
 				smsRcptnAgreYn: "Y", // 문자수신동의
 				emlRcptnAgreYn: "Y", // 이메일수신동의
+				authorList: [
+					{authrtNm: "사용자", authrtCd: "ROLE_USER"},
+				]
 			},
 			showOpt: { ...defaultUserInfoParams }, // 유저정보 표시 여부 객체
 
@@ -193,12 +214,16 @@
 				select: "",
 				address: null,
 			},
+
+			sendYn: false, // 인증코드 발송 여부
+            code: null, // 인증코드
+            certYn: false, // 인증여부
 		};
 	},
 	created() {
 	},
 	methods: {
-		// axios: 사용자 정보 상세 조회
+		// axios: 회원가입
 		async fnInsert() {
 			// 유효성검사
 			if (!this.validation()) {
@@ -211,7 +236,8 @@
 			// 실행
 			try {
 				const response = await mbrInsertProc(data);
-				alert(response.data.message);
+				// alert(response.data.message);
+				alert("회원가입이 완료되었습니다.");
 				this.$router.push({
 					path: this.$filters.ctxPath("/login.page")
 				});
@@ -221,54 +247,6 @@
 					alert(error.response.data.message);
 				} else {
 					alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
-				}
-			}
-		},
-
-		// 표기변경
-		changeFormat() {
-			// 휴대폰번호 포맷
-			const mbl = this.mbrVO.mblTelno?.replace(/[^0-9]/g, '') || '';
-			if (mbl.length === 10) {
-				this.mbrVO.mblTelno = mbl.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
-			} else if (mbl.length === 11) {
-				this.mbrVO.mblTelno = mbl.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
-			}
-
-			// 전화번호 포맷
-			const tel = this.mbrVO.telno?.replace(/[^0-9]/g, '') || '';
-			if (tel.startsWith('02')) {
-				// 서울 지역번호 (2자리)
-				if (tel.length === 9) {
-					this.mbrVO.telno = tel.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3');
-				} else if (tel.length === 10) {
-					this.mbrVO.telno = tel.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3');
-				}
-			} else {
-				// 나머지 지역번호 (3자리)
-				if (tel.length === 10) {
-					this.mbrVO.telno = tel.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
-				} else if (tel.length === 11) {
-					this.mbrVO.telno = tel.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
-				}
-			}
-
-			// 이메일
-			if (this.mbrVO.eml != null) {
-				const email = this.mbrVO.eml.split("@");
-				this.email.id = email[0];
-				switch (email[1]) {
-					case "naver.com":
-					case "gmail.com":
-					case "hanmail.net":
-					case "nate.com":
-					case "kakao.com":
-						this.email.select = email[1];
-						break;
-					default:
-						this.email.select = "self";
-						this.email.address = email[1];
-						break;
 				}
 			}
 		},
@@ -417,6 +395,10 @@
 			if (this.showOpt.isEml) {
 				if (!this.validateEml(this.email)) {
 					return false;
+				} else if (!this.certYn) {
+					alert("이메일 인증을 완료해주세요.");
+					this.$refs.code.focus();
+					return false;
 				}
 			}
 			// 문자수신
@@ -437,23 +419,6 @@
 					return false;
 				}
 			}
-
-			if (this.pageRole == "adm") {
-				if (this.showOpt.isAuthor) {
-					// 회원상태: 차단
-					if (this.showOpt.isStts) {
-						if (!this.validateBlock(this.mbrVO)) {
-							return false;
-						}
-					}
-					// 사용자 권한
-					// if (this.mbrVO.authorList.length < 1) {
-					//   alert("사용자의 권한을 최소 1개 이상 추가하세요.");
-					//   return false;
-					// }
-				}
-			}
-
 			return true;
 		},
 
@@ -466,6 +431,84 @@
 			}
 		},
 
+		// 인증코드 발송
+        async fnSend() {
+            // 이메일 유효성검사
+			if (!this.validateEml(this.email)) {
+				return;
+			}
+			// 데이터 세팅
+			let eml = this.emailSum();
+
+			// 이메일 중복 검사
+			if(await this.fnEmailCheck(eml)) {
+				return;
+			}
+
+            try {
+				this.sendYn = true; // 인증코드 발송
+				const res = await sendAuthEmailProc({email: eml});
+                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관리자에게 문의해주세요.");
+                }
+                this.sendYn = false; // 발송 실패시 인증코드 발송 여부 초기화
+            }
+        },
+
+        // 인증코드 입력
+        inputCode(event) {
+            const input = event.target.value.replace(/[^0-9]/g, '');
+            this.code = input;
+        },
+
+        // 인증코드 확인
+        async fnCheck() {
+			// 데이터 세팅
+			let eml = this.emailSum();
+            try {
+                const res = await checkAuthEmailProc({email: eml, code: this.code});
+                if (res.status == 200) {
+                    this.certYn = true; // 인증 성공
+                }
+            } 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 fnEmailCheck(eml) {
+			let result = true;
+            try {
+                const res = await emailCheckProc({eml: eml});
+                if (res.status == 200) {
+                    result = false; // 중복된 이메일이 없으면 false
+                }
+            } 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관리자에게 문의해주세요.");
+                }
+            } finally {
+				return result; // 중복 검사 결과 반환
+			}
+        },
+
 		// 취소
 		fnCancel() {
 			const isCheck = confirm("회원가입을 취소하시겠습니까?");
Add a comment
List