page.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. 'use client';
  2. import './style.scss';
  3. import Link from 'next/link';
  4. import { useRouter } from 'next/navigation';
  5. import { useState, useEffect, useRef } from 'react';
  6. import { VerificationType } from '@/constants/common';
  7. import { VerifyEmailRequest, ResendEmailRequest } from '@/types/request/auth';
  8. import { fetchApi } from '@/lib/utils/client';
  9. import Loading from '@/app/component/Loading';
  10. export default function Approval()
  11. {
  12. const router = useRouter();
  13. const [loading, setLoading] = useState<boolean>(false);
  14. const [error, setError] = useState<string>('');
  15. const [verifyCode, setVerifyCode] = useState<string>('');
  16. const verifyCodeRef = useRef<HTMLInputElement>(null);
  17. const type: string|null = sessionStorage.getItem('type');
  18. const email: string = (sessionStorage.getItem('email') || '');
  19. const expiration: string|null = sessionStorage.getItem('expiration');
  20. const callbackURL: string = (sessionStorage.getItem('callbackURL') || '/');
  21. // 인증번호 유지 시간 조회
  22. const getResendApprovalSecond = (): number => {
  23. let remainResendApprovalSecond: string | null = sessionStorage.getItem('remainResendApprovalSecond');
  24. if (!remainResendApprovalSecond) {
  25. remainResendApprovalSecond = '120'; // 2분 후 만료
  26. sessionStorage.setItem('remainResendApprovalSecond', remainResendApprovalSecond);
  27. }
  28. return Number(remainResendApprovalSecond);
  29. };
  30. const [resendApprovalSecond, setResendApprovalSecond] = useState(
  31. getResendApprovalSecond()
  32. );
  33. // 다시 보내기 여부
  34. const [canResend, setCanResend] = useState(false);
  35. useEffect(() => {
  36. if (error) {
  37. alert(error);
  38. setError('');
  39. }
  40. }, [error]);
  41. // 페이지 벗어날 때 세션 정리
  42. useEffect(() => {
  43. if (!email || email == '') {
  44. alert('잘못된 접근입니다.');
  45. location.replace('/');
  46. return;
  47. }
  48. // 인증 시간 만료 확인
  49. if (type == null || type == "") {
  50. alert('다시 시도해주세요.');
  51. router.push('/');
  52. return;
  53. }
  54. if (!expiration || Date.now() > Number(expiration)) {
  55. alert('인증 시간이 만료되었습니다. 다시 시도해주세요.');
  56. router.push(callbackURL);
  57. return;
  58. }
  59. const handleUnload = (e: BeforeUnloadEvent) => {
  60. const isReload = (e.type === 'beforeunload' || (performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming)?.type === 'reload' || performance.navigation.type === 1);
  61. if (isReload) {
  62. e.preventDefault();
  63. return;
  64. }
  65. sessionStorage.clear();
  66. }
  67. window.addEventListener('beforeunload', handleUnload);
  68. return () => window.removeEventListener('beforeunload', handleUnload);
  69. }, [expiration, router]);
  70. // 다시 보내기 타이머 설정
  71. useEffect(() => {
  72. if (resendApprovalSecond <= 0) {
  73. setCanResend(true);
  74. sessionStorage.setItem('expiration', '0');
  75. sessionStorage.removeItem('remainResendApprovalSecond');
  76. return;
  77. }
  78. const timer = setInterval(() => {
  79. setResendApprovalSecond((prev) => {
  80. if (prev <= 1) {
  81. clearInterval(timer);
  82. setCanResend(true);
  83. return 0;
  84. }
  85. return prev - 1;
  86. });
  87. }, 1000);
  88. sessionStorage.setItem('remainResendApprovalSecond', resendApprovalSecond.toString());
  89. return () => clearInterval(timer);
  90. }, [resendApprovalSecond]);
  91. // 인증번호 검증
  92. const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  93. e.preventDefault();
  94. setLoading(true);
  95. try {
  96. if (!verifyCode) {
  97. verifyCodeRef.current?.focus();
  98. throw new Error('인증번호를 입력하세요.');
  99. }
  100. await new Promise(resolve => setTimeout(resolve, 500));
  101. const res = await fetchApi('/api/auth/verify-email', {
  102. method: 'POST',
  103. body: { Email: email, Code: verifyCode, Type: Number(type) } as VerifyEmailRequest
  104. });
  105. sessionStorage.setItem('email', email);
  106. switch (type) {
  107. // 회원가입 완료
  108. case VerificationType.Registration.toString():
  109. router.push('/welcome');
  110. break;
  111. // 비밀번호 변경 페이지 이동
  112. case VerificationType.ForgotPassword.toString():
  113. router.push('/reset-password');
  114. break;
  115. }
  116. } catch (err) {
  117. if (err instanceof Error) {
  118. setError(err.message);
  119. }
  120. } finally {
  121. setLoading(false);
  122. }
  123. };
  124. // 인증번호 다시 보내기
  125. const handleResend = async () => {
  126. try {
  127. await fetchApi('/api/auth/resend-email', {
  128. method: 'POST',
  129. body: { Email: email, Type: Number(type) } as ResendEmailRequest
  130. });
  131. setCanResend(false);
  132. setResendApprovalSecond(getResendApprovalSecond());
  133. setVerifyCode("");
  134. } catch (err) {
  135. if (err instanceof Error) {
  136. setError(err.message);
  137. }
  138. }
  139. };
  140. return (
  141. <>
  142. {loading && <Loading />}
  143. <div id="approvalForm" className="row-start-2 flex flex-row flex-wrap gap-2">
  144. <fieldset className="grow">
  145. <legend>인증번호 확인</legend>
  146. <form id="fApproval" method="post" acceptCharset="utf-8" autoComplete="off" className="grid p-4" onSubmit={handleSubmit}>
  147. <p>보안을 위해 인증번호를 발송했습니다. <br />수신된 인증번호를 입력해주세요.</p>
  148. <div>
  149. <br />
  150. </div>
  151. <label htmlFor="email">수신 이메일</label>
  152. <input type="email" name="email" id="email" value={email} disabled />
  153. <div className="flex flex-row flex-1">
  154. <label htmlFor="verifyCode" className="flex-auto">인증번호</label>
  155. <label className="text-right">
  156. {canResend ? (
  157. <button type="button" className="text-blue-600 no-underline hover:underline" onClick={handleResend}>
  158. 다시 받기
  159. </button>
  160. ) : (
  161. <><span className="text-green-700">유효시간:(<em>{resendApprovalSecond}</em>초)</span></>
  162. )}
  163. </label>
  164. </div>
  165. <input type="number" name="verify_code" id="verifyCode" ref={verifyCodeRef} minLength={6} maxLength={10} onChange={e => setVerifyCode(e.target.value)} autoComplete="off" />
  166. <div className="grid grid-cols-2 gap-2">
  167. <button type="submit" className="btn btn-submit" disabled={loading}>
  168. {loading ? "확인 중..." : "확인"}
  169. </button>
  170. <button type="button" className="btn btn-default" onClick={() => router.push(callbackURL)}>취소</button>
  171. </div>
  172. <hr />
  173. <dl>
  174. <dt>도움이 필요하신가요?</dt>
  175. <dd>인증번호를 받을 수 없거나 오류가 발생한 경우 <Link href="/support/qna">운영자에게 문의</Link>하세요.</dd>
  176. </dl>
  177. </form>
  178. </fieldset>
  179. </div>
  180. </>
  181. );
  182. }