findPw.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <template>
  2. <main class="user--main">
  3. <div class="join--container">
  4. <div class="title--wrap">
  5. <h1>새 비밀번호 설정</h1>
  6. <p class="mt--9">새로 사용할 비밀번호를 입력해주세요.</p>
  7. </div>
  8. <div class="login--wrap mt--20">
  9. <form @submit.prevent="handleReset">
  10. <!-- 새 비밀번호 -->
  11. <div class="input--wrap mt--18 pw--input--wrap">
  12. <label for="reset--pw">새 비밀번호</label>
  13. <input
  14. id="reset--pw"
  15. v-model="password"
  16. :type="showPw ? 'text' : 'password'"
  17. placeholder="비밀번호를 입력해 주세요"
  18. autocomplete="new-password"
  19. maxlength="30"
  20. >
  21. <button
  22. type="button"
  23. class="pw--toggle--btn"
  24. :aria-label="showPw ? '비밀번호 숨기기' : '비밀번호 표시'"
  25. @click="showPw = !showPw"
  26. >
  27. <img v-if="showPw" src="/img/ico--pw.svg" alt="비밀번호 표시" />
  28. <img v-else src="/img/ico--pw--off.svg" alt="비밀번호 숨김" />
  29. </button>
  30. </div>
  31. <p class="input--info" :style="passwordInvalid ? 'color:#e5484d;' : ''">
  32. 영문(소문자)+숫자+특수문자 조합 8자 이상
  33. </p>
  34. <!-- 새 비밀번호 확인 -->
  35. <div class="input--wrap mt--18 pw--input--wrap">
  36. <label for="reset--pw2">새 비밀번호 확인</label>
  37. <input
  38. id="reset--pw2"
  39. v-model="password2"
  40. :type="showPw2 ? 'text' : 'password'"
  41. placeholder="비밀번호를 다시 입력해 주세요"
  42. autocomplete="new-password"
  43. maxlength="30"
  44. >
  45. <button
  46. type="button"
  47. class="pw--toggle--btn"
  48. :aria-label="showPw2 ? '비밀번호 숨기기' : '비밀번호 표시'"
  49. @click="showPw2 = !showPw2"
  50. >
  51. <img v-if="showPw2" src="/img/ico--pw.svg" alt="비밀번호 표시" />
  52. <img v-else src="/img/ico--pw--off.svg" alt="비밀번호 숨김" />
  53. </button>
  54. </div>
  55. <p v-if="password2 && password !== password2" class="input--info" style="color:#e5484d;">
  56. 비밀번호가 일치하지 않습니다.
  57. </p>
  58. <div class="change--pw--box mt--25">
  59. <span>🔒 비밀번호 규칙</span>
  60. <ul>
  61. <li>8자 이상</li>
  62. <li>영문 소문자 + 숫자 + 특수문자 조합</li>
  63. <li>이전 비밀번호와 달라야 함</li>
  64. <li>아이디 포함 불가</li>
  65. </ul>
  66. </div>
  67. <div class="float--btn--wrap">
  68. <a
  69. href="#"
  70. :class="{ disabled: !canSubmit || submitting }"
  71. @click.prevent="handleReset"
  72. >{{ submitting ? '변경 중...' : '비밀번호 변경' }}</a>
  73. </div>
  74. </form>
  75. </div>
  76. </div>
  77. <AppAlertModal
  78. v-model="modal.show"
  79. :icon-type="modal.iconType"
  80. :title="modal.title"
  81. :message="modal.message"
  82. />
  83. </main>
  84. </template>
  85. <script setup>
  86. import { ref, computed, reactive, onMounted } from 'vue'
  87. import { useRouter } from 'vue-router'
  88. import AppAlertModal from '~/components/AppAlertModal.vue'
  89. const router = useRouter()
  90. const { post } = useApi()
  91. const password = ref('')
  92. const password2 = ref('')
  93. const showPw = ref(false)
  94. const showPw2 = ref(false)
  95. const submitting = ref(false)
  96. const resetToken = ref('')
  97. const modal = reactive({ show: false, iconType: 'error', title: '', message: '' })
  98. const showAlert = (title, message, iconType = 'error') => {
  99. modal.title = title
  100. modal.message = message
  101. modal.iconType = iconType
  102. modal.show = true
  103. }
  104. const PASSWORD_RE = /^(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>/?]).{8,}$/
  105. const passwordInvalid = computed(() =>
  106. password.value !== '' && !PASSWORD_RE.test(password.value)
  107. )
  108. const canSubmit = computed(() =>
  109. PASSWORD_RE.test(password.value) && password.value === password2.value
  110. )
  111. onMounted(() => {
  112. resetToken.value = sessionStorage.getItem('reset_token') || ''
  113. if (!resetToken.value) {
  114. // 인증 토큰 없으면 본인확인부터 다시
  115. showAlert('잘못된 접근', '본인 확인부터 진행해 주세요.')
  116. setTimeout(() => router.replace('/login/find'), 1000)
  117. }
  118. })
  119. const handleReset = async () => {
  120. if (!canSubmit.value || submitting.value) return
  121. submitting.value = true
  122. try {
  123. const { data, error } = await post('/users/reset-password', {
  124. reset_token: resetToken.value,
  125. new_password: password.value,
  126. })
  127. if (error || !data?.success) {
  128. return showAlert('변경 실패', error?.message || data?.message || '비밀번호 변경에 실패했습니다.')
  129. }
  130. sessionStorage.removeItem('reset_token')
  131. sessionStorage.removeItem('reset_username')
  132. router.push('/login/findPwComplete')
  133. } catch (e) {
  134. console.error(e)
  135. showAlert('오류', '서버 오류가 발생했습니다.')
  136. } finally {
  137. submitting.value = false
  138. }
  139. }
  140. </script>