index.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. <template>
  2. <div class="admin--login">
  3. <div class="login--container">
  4. <div class="login--layout">
  5. <div class="login--layout-inner">
  6. <div class="login--brand">
  7. <div class="login--brand--logo">
  8. 🏴‍☠️
  9. </div>
  10. <div class="login--brand--txt">
  11. <h1 class="login--brand-title">파이럿존</h1>
  12. <p class="login--brand-tagline">ADMIN SYSTEM</p>
  13. </div>
  14. </div>
  15. <div class="login--deco--line">
  16. <span class="circle1"></span>
  17. <span class="circle2"></span>
  18. <span class="circle3">🧭</span>
  19. </div>
  20. <div class="login--intro">
  21. <h2 class="login--intro-title">낚시를 모험으로</h2>
  22. <p class="login--intro-desc">
  23. Fishing as Adventure
  24. </p>
  25. <ul class="login--intro-list">
  26. 파이럿존 관리자 시스템 v1.0 <br />챌린지 · 퀘스트 · 회원 · 진출권을 관리합니다.
  27. </ul>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="login--panel">
  32. <div class="login--box">
  33. <div class="login--logo">
  34. <span>환영합니다 👋</span>
  35. <h1>어드민 로그인</h1>
  36. <p class="subtitle">관리자 권한이 있는 계정으로 로그인하세요.</p>
  37. </div>
  38. <form @submit.prevent="handleLogin" class="login--form">
  39. <div v-if="errorMessage" class="login--error">
  40. {{ errorMessage }}
  41. </div>
  42. <div class="form--group">
  43. <input
  44. v-model="username"
  45. type="text"
  46. placeholder="아이디"
  47. class="form--input"
  48. required
  49. :disabled="isLoading"
  50. />
  51. </div>
  52. <div class="form--group">
  53. <input
  54. v-model="password"
  55. type="password"
  56. placeholder="비밀번호"
  57. class="form--input"
  58. required
  59. :disabled="isLoading"
  60. />
  61. </div>
  62. <div class="form--options">
  63. <label class="checkbox--label">
  64. <input type="checkbox" v-model="rememberMe" :disabled="isLoading" />
  65. <span>로그인 상태 유지</span>
  66. </label>
  67. <!-- <a href="#" class="forgot--password">비밀번호 찾기</a> -->
  68. </div>
  69. <button type="submit" class="login--button" :disabled="isLoading">
  70. {{ isLoading ? "로그인 중..." : "로그인 →" }}
  71. </button>
  72. </form>
  73. </div>
  74. </div>
  75. </div>
  76. <!-- 비밀번호 변경 권장 모달 -->
  77. <PasswordChangeRecommendModal
  78. v-if="showPasswordChangeModal"
  79. :admin-id="loggedInAdminId"
  80. @close="closePasswordChangeModal"
  81. @changed="handlePasswordChanged"
  82. />
  83. <!-- 계정 잠금 모달 -->
  84. <AccountLockedModal v-if="showAccountLockedModal" @close="closeAccountLockedModal" />
  85. </div>
  86. </template>
  87. <script setup>
  88. import { ref } from "vue";
  89. import PasswordChangeRecommendModal from "~/components/admin/PasswordChangeRecommendModal.vue";
  90. import AccountLockedModal from "~/components/admin/AccountLockedModal.vue";
  91. definePageMeta({
  92. layout: false,
  93. middleware: ["auth"],
  94. });
  95. const { post } = useApi();
  96. const username = ref("");
  97. const password = ref("");
  98. const rememberMe = ref(false);
  99. const isLoading = ref(false);
  100. const errorMessage = ref("");
  101. const showPasswordChangeModal = ref(false);
  102. const showAccountLockedModal = ref(false);
  103. const loggedInAdminId = ref(null);
  104. const handleLogin = async () => {
  105. if (!username.value || !password.value) {
  106. errorMessage.value = "아이디와 비밀번호를 입력해주세요.";
  107. return;
  108. }
  109. isLoading.value = true;
  110. errorMessage.value = "";
  111. try {
  112. console.log("[Login] 로그인 시도:", username.value);
  113. const { data, error } = await post("/auth/login", {
  114. username: username.value,
  115. password: password.value,
  116. remember: rememberMe.value,
  117. });
  118. console.log("[Login] API 응답:", { data, error });
  119. if (error) {
  120. console.error("[Login] 에러 발생:", error);
  121. const errorMsg = error.message || "로그인에 실패했습니다.";
  122. // 403 에러이고 계정 잠금 메시지인 경우 모달 표시
  123. if (
  124. error.statusCode === 403 ||
  125. errorMsg.includes("계정이 잠겼습니다") ||
  126. errorMsg.includes("슈퍼 관리자에게 문의")
  127. ) {
  128. showAccountLockedModal.value = true;
  129. errorMessage.value = "";
  130. } else {
  131. errorMessage.value = errorMsg;
  132. }
  133. return;
  134. }
  135. // API 응답: { success: true, data: { token, admin, password_change_needed }, message }
  136. if (data?.success && data?.data?.token) {
  137. console.log(
  138. "[Login] 로그인 성공, 토큰 저장:",
  139. data.data.token.substring(0, 20) + "..."
  140. );
  141. localStorage.setItem("admin_token", data.data.token);
  142. if (data.data.admin) {
  143. localStorage.setItem("admin_user", JSON.stringify(data.data.admin));
  144. loggedInAdminId.value = data.data.admin.id;
  145. }
  146. console.log(
  147. "[Login] localStorage 확인:",
  148. localStorage.getItem("admin_token") ? "저장됨" : "저장 실패"
  149. );
  150. // 비밀번호 변경이 필요한 경우 모달 표시
  151. if (data.data.password_change_needed) {
  152. console.log("[Login] 비밀번호 변경 필요, 모달 표시");
  153. showPasswordChangeModal.value = true;
  154. } else {
  155. // 비밀번호 변경이 필요 없으면 바로 dashboard로 이동
  156. console.log("[Login] dashboard로 이동 시도...");
  157. await navigateTo("/site-manager/dashboard");
  158. console.log("[Login] navigateTo 완료");
  159. }
  160. } else {
  161. console.error("[Login] 잘못된 응답 형식:", data);
  162. errorMessage.value = data?.message || "로그인 정보가 올바르지 않습니다.";
  163. }
  164. } catch (error) {
  165. errorMessage.value = "서버 오류가 발생했습니다.";
  166. console.error("[Login] Exception:", error);
  167. } finally {
  168. isLoading.value = false;
  169. }
  170. };
  171. const closePasswordChangeModal = async () => {
  172. showPasswordChangeModal.value = false;
  173. // 모달을 닫으면 dashboard로 이동
  174. await navigateTo("/site-manager/dashboard");
  175. };
  176. const handlePasswordChanged = async (message) => {
  177. showPasswordChangeModal.value = false;
  178. // 비밀번호 변경 성공 후 dashboard로 이동
  179. if (message) {
  180. console.log("[Login] 비밀번호 변경 성공:", message);
  181. }
  182. await navigateTo("/site-manager/dashboard");
  183. };
  184. const closeAccountLockedModal = () => {
  185. showAccountLockedModal.value = false;
  186. // 계정 잠금 모달 닫으면 에러 메시지 지우고 로그인 폼 초기화
  187. errorMessage.value = "";
  188. password.value = "";
  189. };
  190. </script>