index.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <template>
  2. <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--25">
  9. <form @submit.prevent="handleLogin">
  10. <div class="input--wrap">
  11. <label for="login--id">아이디</label>
  12. <input
  13. id="login--id"
  14. v-model="loginId"
  15. type="text"
  16. placeholder="아이디를 입력해 주세요"
  17. autocomplete="username"
  18. >
  19. </div>
  20. <div class="input--wrap mt--18 pw--input--wrap">
  21. <label for="login--pw">비밀번호</label>
  22. <input
  23. id="login--pw"
  24. v-model="loginPw"
  25. :type="showPw ? 'text' : 'password'"
  26. placeholder="비밀번호를 입력해 주세요"
  27. autocomplete="current-password"
  28. >
  29. <button
  30. type="button"
  31. class="pw--toggle--btn"
  32. :aria-label="showPw ? '비밀번호 숨기기' : '비밀번호 표시'"
  33. @click="showPw = !showPw"
  34. >
  35. <img v-if="showPw" src="/img/ico--pw.svg" alt="비밀번호 표시" />
  36. <img v-else src="/img/ico--pw--off.svg" alt="비밀번호 숨김" />
  37. </button>
  38. </div>
  39. <div class="auto--login--wrap mt--18">
  40. <div class="auto--login">
  41. <label>
  42. <input type="checkbox" v-model="autoLogin">
  43. 자동 로그인
  44. </label>
  45. </div>
  46. <NuxtLink to="/find" class="find--pw--btn">아이디ㆍ비밀번호 찾기</NuxtLink>
  47. </div>
  48. <div class="login--btn--wrap mt--30">
  49. <button type="submit" :disabled="loggingIn">
  50. {{ loggingIn ? '로그인 중...' : '로그인' }}
  51. </button>
  52. </div>
  53. </form>
  54. <div class="social--login--wrap">
  55. <div class="social--login--txt mt--28"><span>또는</span></div>
  56. <ul class="social--login--list mt--26">
  57. <li><NuxtLink to="#" class="kakao">카카오톡</NuxtLink></li>
  58. <li><NuxtLink to="#" class="naver">네이버</NuxtLink></li>
  59. <li><NuxtLink to="#" class="apple">애플</NuxtLink></li>
  60. </ul>
  61. </div>
  62. <div class="join--btn--wrap mt--36">
  63. 아직 회원이 아니신가요? <NuxtLink to="/login/agree">회원가입</NuxtLink>
  64. </div>
  65. </div>
  66. </div>
  67. <AppAlertModal
  68. v-model="modal.show"
  69. :icon-type="modal.iconType"
  70. :title="modal.title"
  71. :message="modal.message"
  72. />
  73. </main>
  74. </template>
  75. <script setup>
  76. import { ref, reactive, onMounted } from 'vue'
  77. import { useRouter } from 'vue-router'
  78. import AppAlertModal from '~/components/AppAlertModal.vue'
  79. const router = useRouter()
  80. const { post } = useApi()
  81. const loginId = ref('')
  82. const loginPw = ref('')
  83. const showPw = ref(false)
  84. const autoLogin = ref(false)
  85. const loggingIn = ref(false)
  86. // 공통 알림 모달
  87. const modal = reactive({
  88. show: false,
  89. iconType: 'error',
  90. title: '',
  91. message: '',
  92. })
  93. const showAlert = (title, message, iconType = 'error') => {
  94. modal.title = title
  95. modal.message = message
  96. modal.iconType = iconType
  97. modal.show = true
  98. }
  99. // 로그인 처리
  100. const handleLogin = async () => {
  101. if (loggingIn.value) return
  102. if (!loginId.value.trim() || !loginPw.value) {
  103. showAlert('입력 확인', '아이디와 비밀번호를 모두 입력해 주세요.')
  104. return
  105. }
  106. loggingIn.value = true
  107. try {
  108. const { data, error } = await post('/users/login', {
  109. username: loginId.value.trim(),
  110. password: loginPw.value,
  111. auto_login: autoLogin.value,
  112. })
  113. if (error || !data?.success) {
  114. showAlert(
  115. '로그인에 실패했습니다',
  116. error?.message || data?.message || '아이디 또는 비밀번호가 일치하지 않습니다.\n다시 한번 확인해 주세요.'
  117. )
  118. return
  119. }
  120. // 토큰 + user 정보 저장
  121. const { token, expires_at, user } = data.data
  122. const storage = autoLogin.value ? localStorage : sessionStorage
  123. storage.setItem('user_token', token)
  124. storage.setItem('user_token_expires', expires_at)
  125. storage.setItem('user', JSON.stringify(user))
  126. storage.setItem('auto_login', autoLogin.value ? 'Y' : 'N')
  127. // 메인으로 이동
  128. router.push('/')
  129. } catch (e) {
  130. console.error('[Login] error:', e)
  131. showAlert('오류', '서버 오류가 발생했습니다.')
  132. } finally {
  133. loggingIn.value = false
  134. }
  135. }
  136. // 페이지 진입 시 — 이미 로그인되어 있으면 메인으로
  137. onMounted(() => {
  138. const token = localStorage.getItem('user_token') || sessionStorage.getItem('user_token')
  139. const expires = localStorage.getItem('user_token_expires') || sessionStorage.getItem('user_token_expires')
  140. if (token && expires) {
  141. if (new Date(expires.replace(' ', 'T')) > new Date()) {
  142. // 유효한 토큰 → 메인으로
  143. router.replace('/')
  144. } else {
  145. // 만료된 토큰 정리
  146. localStorage.removeItem('user_token')
  147. localStorage.removeItem('user_token_expires')
  148. localStorage.removeItem('user')
  149. sessionStorage.removeItem('user_token')
  150. sessionStorage.removeItem('user_token_expires')
  151. sessionStorage.removeItem('user')
  152. }
  153. }
  154. })
  155. </script>