find.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <template>
  2. <main class="user--main">
  3. <div class="join--container">
  4. <div class="title--wrap">
  5. <h1>{{ activeTab === 'id' ? '아이디 찾기' : '비밀번호 재설정' }}</h1>
  6. </div>
  7. <div class="find--tab--wrap mt--14">
  8. <button type="button" :class="{ active: activeTab === 'id' }" @click="setTab('id')">아이디 찾기</button>
  9. <button type="button" :class="{ active: activeTab === 'pw' }" @click="setTab('pw')">비밀번호 찾기</button>
  10. </div>
  11. <!-- 아이디 찾기 -->
  12. <div v-if="activeTab === 'id'" class="login--wrap mt--20">
  13. <form @submit.prevent="handleFindId">
  14. <p class="mb--15 desc--p">가입 시 등록한 이름과 핸드폰 번호로 아이디를 찾을 수 있습니다.</p>
  15. <div class="input--wrap">
  16. <label for="findId--name">이름</label>
  17. <input id="findId--name" v-model="idForm.name" type="text" placeholder="이름을 입력해 주세요" maxlength="50">
  18. </div>
  19. <div class="input--wrap mt--18">
  20. <label for="findId--phone1">핸드폰 <span class="required">*</span></label>
  21. <div class="input--inner--wrap gap--4">
  22. <input id="findId--phone1" v-model="idForm.phoneFront" type="text" maxlength="3">
  23. <span>-</span>
  24. <input v-model="idForm.phoneMiddle" type="text" maxlength="4" inputmode="numeric">
  25. <span>-</span>
  26. <input v-model="idForm.phoneLast" type="text" maxlength="4" inputmode="numeric">
  27. </div>
  28. </div>
  29. </form>
  30. </div>
  31. <!-- 비밀번호 재설정 (본인확인) -->
  32. <div v-else class="login--wrap mt--20">
  33. <form @submit.prevent="handleVerifyForReset">
  34. <p class="mb--15 desc--p">아이디와 본인인증으로 비밀번호를 재설정할 수 있습니다.</p>
  35. <div class="input--wrap">
  36. <label for="findPw--id">아이디</label>
  37. <input id="findPw--id" v-model="pwForm.username" type="text" placeholder="아이디를 입력해 주세요" maxlength="20">
  38. </div>
  39. <!-- 소셜 가입자 안내 -->
  40. <div v-if="socialNotice" class="mt--20 find--social--box">
  41. <span>💬 소셜 가입 회원 안내</span>
  42. <p>{{ socialNotice }}</p>
  43. </div>
  44. <p class="label--p mt--28">본인인증 (이름ㆍ핸드폰)</p>
  45. <div class="input--wrap mt--18">
  46. <label for="findPw--name">이름</label>
  47. <input id="findPw--name" v-model="pwForm.name" type="text" placeholder="이름을 입력해 주세요" maxlength="50">
  48. </div>
  49. <div class="input--wrap mt--18">
  50. <label for="findPw--phone1">핸드폰 <span class="required">*</span></label>
  51. <div class="input--inner--wrap gap--4">
  52. <input id="findPw--phone1" v-model="pwForm.phoneFront" type="text" maxlength="3">
  53. <span>-</span>
  54. <input v-model="pwForm.phoneMiddle" type="text" maxlength="4" inputmode="numeric">
  55. <span>-</span>
  56. <input v-model="pwForm.phoneLast" type="text" maxlength="4" inputmode="numeric">
  57. </div>
  58. </div>
  59. </form>
  60. </div>
  61. </div>
  62. <div class="float--btn--wrap">
  63. <a
  64. href="#"
  65. :class="{ disabled: submitting }"
  66. @click.prevent="submit"
  67. >{{ submitting ? '처리 중...' : '다음' }}</a>
  68. </div>
  69. <AppAlertModal
  70. v-model="modal.show"
  71. :icon-type="modal.iconType"
  72. :title="modal.title"
  73. :message="modal.message"
  74. />
  75. </main>
  76. </template>
  77. <script setup>
  78. import { ref, reactive, onMounted } from 'vue'
  79. import { useRoute, useRouter } from 'vue-router'
  80. import AppAlertModal from '~/components/AppAlertModal.vue'
  81. const route = useRoute()
  82. const router = useRouter()
  83. const { post } = useApi()
  84. const activeTab = ref('id') // 'id' | 'pw'
  85. // 쿼리스트링 ?tab=pw 로 진입하면 비밀번호 탭 자동 활성
  86. onMounted(() => {
  87. if (route.query.tab === 'pw') activeTab.value = 'pw'
  88. })
  89. const submitting = ref(false)
  90. const socialNotice = ref('')
  91. const idForm = reactive({
  92. name: '',
  93. phoneFront: '010',
  94. phoneMiddle: '',
  95. phoneLast: '',
  96. })
  97. const pwForm = reactive({
  98. username: '',
  99. name: '',
  100. phoneFront: '010',
  101. phoneMiddle: '',
  102. phoneLast: '',
  103. })
  104. const modal = reactive({ show: false, iconType: 'error', title: '', message: '' })
  105. const showAlert = (title, message, iconType = 'error') => {
  106. modal.title = title
  107. modal.message = message
  108. modal.iconType = iconType
  109. modal.show = true
  110. }
  111. const setTab = (t) => {
  112. activeTab.value = t
  113. socialNotice.value = ''
  114. }
  115. const submit = () => {
  116. if (activeTab.value === 'id') handleFindId()
  117. else handleVerifyForReset()
  118. }
  119. // 아이디 찾기
  120. const handleFindId = async () => {
  121. if (submitting.value) return
  122. if (!idForm.name.trim()) return showAlert('입력 확인', '이름을 입력해 주세요.')
  123. if (!idForm.phoneMiddle || !idForm.phoneLast) {
  124. return showAlert('입력 확인', '핸드폰 번호를 입력해 주세요.')
  125. }
  126. submitting.value = true
  127. try {
  128. const phone = `${idForm.phoneFront}-${idForm.phoneMiddle}-${idForm.phoneLast}`
  129. const { data, error } = await post('/users/find-id', {
  130. name: idForm.name.trim(),
  131. phone,
  132. })
  133. if (error || !data?.success) {
  134. return showAlert('조회 실패', error?.message || data?.message || '일치하는 회원 정보가 없습니다.')
  135. }
  136. sessionStorage.setItem('find_id_result', JSON.stringify(data.data))
  137. router.push('/login/findIdComplete')
  138. } catch (e) {
  139. console.error(e)
  140. showAlert('오류', '서버 오류가 발생했습니다.')
  141. } finally {
  142. submitting.value = false
  143. }
  144. }
  145. // 비밀번호 재설정 본인확인
  146. const handleVerifyForReset = async () => {
  147. if (submitting.value) return
  148. socialNotice.value = ''
  149. if (!pwForm.username.trim()) return showAlert('입력 확인', '아이디를 입력해 주세요.')
  150. if (!pwForm.name.trim()) return showAlert('입력 확인', '이름을 입력해 주세요.')
  151. if (!pwForm.phoneMiddle || !pwForm.phoneLast) {
  152. return showAlert('입력 확인', '핸드폰 번호를 입력해 주세요.')
  153. }
  154. submitting.value = true
  155. try {
  156. const phone = `${pwForm.phoneFront}-${pwForm.phoneMiddle}-${pwForm.phoneLast}`
  157. const { data, error } = await post('/users/verify-for-reset', {
  158. username: pwForm.username.trim(),
  159. name: pwForm.name.trim(),
  160. phone,
  161. })
  162. if (error || !data?.success) {
  163. const msg = error?.message || data?.message || '일치하는 회원 정보가 없습니다.'
  164. // 소셜 가입자 안내 (백엔드가 "소셜 가입 회원입니다. (kakao)" 류로 응답)
  165. if (msg.includes('소셜')) {
  166. socialNotice.value = msg + ' 비밀번호 없이 소셜 로그인을 이용해 주세요.'
  167. return
  168. }
  169. return showAlert('인증 실패', msg)
  170. }
  171. sessionStorage.setItem('reset_token', data.data.reset_token)
  172. sessionStorage.setItem('reset_username', pwForm.username.trim())
  173. router.push('/login/findPw')
  174. } catch (e) {
  175. console.error(e)
  176. showAlert('오류', '서버 오류가 발생했습니다.')
  177. } finally {
  178. submitting.value = false
  179. }
  180. }
  181. </script>