2025-11-18 06:19:26 +09:00
|
|
|
'use client';
|
|
|
|
|
|
2025-11-18 07:53:09 +09:00
|
|
|
import { useEffect, useState } from "react";
|
2025-11-28 14:49:56 +09:00
|
|
|
import { useRouter } from "next/navigation";
|
2025-11-18 07:53:09 +09:00
|
|
|
import ModalCloseSvg from "../svgs/closexsvg";
|
2025-11-28 14:26:54 +09:00
|
|
|
import apiService from "../lib/apiService";
|
2025-11-18 06:19:26 +09:00
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
2025-11-18 07:53:09 +09:00
|
|
|
onSubmit?: (payload: { email: string; code?: string; newPassword: string }) => void;
|
|
|
|
|
showVerification?: boolean;
|
|
|
|
|
devVerificationState?: 'initial' | 'sent' | 'verified' | 'failed';
|
2025-11-26 21:40:56 +09:00
|
|
|
initialEmail?: string;
|
2025-11-18 06:19:26 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:40:56 +09:00
|
|
|
export default function ChangePasswordModal({ open, onClose, onSubmit, showVerification = false, devVerificationState, initialEmail }: Props) {
|
2025-11-28 14:49:56 +09:00
|
|
|
const router = useRouter();
|
2025-11-26 21:40:56 +09:00
|
|
|
const [email, setEmail] = useState(initialEmail || "");
|
2025-11-18 06:19:26 +09:00
|
|
|
const [code, setCode] = useState("");
|
|
|
|
|
const [newPassword, setNewPassword] = useState("");
|
|
|
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
|
|
|
const [error, setError] = useState<string | null>(null); // 인증번호 오류 등
|
2025-11-18 07:53:09 +09:00
|
|
|
const [requireCode, setRequireCode] = useState<boolean>(showVerification);
|
|
|
|
|
const [isCodeSent, setIsCodeSent] = useState<boolean>(showVerification);
|
|
|
|
|
const canConfirm = code.trim().length > 0;
|
|
|
|
|
const [isVerified, setIsVerified] = useState(false);
|
2025-11-28 14:26:54 +09:00
|
|
|
const [isSending, setIsSending] = useState(false);
|
|
|
|
|
const [isVerifying, setIsVerifying] = useState(false);
|
2025-11-28 14:49:56 +09:00
|
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
2025-11-18 07:53:09 +09:00
|
|
|
const hasError = !!error;
|
|
|
|
|
|
2025-11-26 21:40:56 +09:00
|
|
|
// initialEmail이 변경되면 email state 업데이트
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initialEmail) {
|
|
|
|
|
setEmail(initialEmail);
|
|
|
|
|
}
|
|
|
|
|
}, [initialEmail]);
|
|
|
|
|
|
2025-11-18 07:53:09 +09:00
|
|
|
// 외부에서 전달된 개발모드 상태(devVerificationState)에 따라 UI 동기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!devVerificationState) return;
|
|
|
|
|
switch (devVerificationState) {
|
|
|
|
|
case 'initial':
|
|
|
|
|
setRequireCode(false);
|
|
|
|
|
setIsCodeSent(false);
|
|
|
|
|
setCode("");
|
|
|
|
|
setError(null);
|
|
|
|
|
setIsVerified(false);
|
|
|
|
|
break;
|
|
|
|
|
case 'sent':
|
|
|
|
|
setRequireCode(true);
|
|
|
|
|
setIsCodeSent(true);
|
|
|
|
|
setCode("");
|
|
|
|
|
setError(null);
|
|
|
|
|
setIsVerified(false);
|
|
|
|
|
break;
|
|
|
|
|
case 'verified':
|
|
|
|
|
setRequireCode(true);
|
|
|
|
|
setIsCodeSent(true);
|
|
|
|
|
setCode("123456");
|
|
|
|
|
setError(null);
|
|
|
|
|
setIsVerified(true);
|
|
|
|
|
break;
|
|
|
|
|
case 'failed':
|
|
|
|
|
setRequireCode(true);
|
|
|
|
|
setIsCodeSent(true);
|
|
|
|
|
setCode("");
|
|
|
|
|
setError("올바르지 않은 인증번호입니다. 인증번호를 확인해주세요.");
|
|
|
|
|
setIsVerified(false);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}, [devVerificationState]);
|
2025-11-18 06:19:26 +09:00
|
|
|
|
2025-11-28 14:49:56 +09:00
|
|
|
const handleLoginClick = () => {
|
|
|
|
|
// 토큰 삭제 (로그아웃)
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
|
|
|
}
|
|
|
|
|
// 로그인 페이지로 이동
|
|
|
|
|
router.push('/login');
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
2025-11-18 06:19:26 +09:00
|
|
|
|
2025-11-28 14:49:56 +09:00
|
|
|
const handleSubmit = async () => {
|
2025-11-18 06:19:26 +09:00
|
|
|
setError(null);
|
2025-11-18 07:53:09 +09:00
|
|
|
if (requireCode) {
|
|
|
|
|
if (!code) {
|
|
|
|
|
setError("인증번호를 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-18 06:19:26 +09:00
|
|
|
}
|
|
|
|
|
if (!newPassword || !confirmPassword) {
|
|
|
|
|
setError("새 비밀번호를 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
|
|
|
setError("새 비밀번호가 일치하지 않습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-28 14:49:56 +09:00
|
|
|
if (!email.trim()) {
|
|
|
|
|
setError("이메일을 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (requireCode && !code.trim()) {
|
|
|
|
|
setError("인증번호를 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await apiService.resetPassword(email, code, newPassword, confirmPassword);
|
|
|
|
|
onSubmit?.({ email, code: requireCode ? code : undefined, newPassword });
|
|
|
|
|
setShowSuccessModal(true);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "비밀번호 변경에 실패했습니다.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
// 모든 상태 초기화
|
|
|
|
|
setEmail(initialEmail || "");
|
|
|
|
|
setCode("");
|
|
|
|
|
setNewPassword("");
|
|
|
|
|
setConfirmPassword("");
|
|
|
|
|
setError(null);
|
|
|
|
|
setRequireCode(showVerification);
|
|
|
|
|
setIsCodeSent(showVerification);
|
|
|
|
|
setIsVerified(false);
|
|
|
|
|
setIsSending(false);
|
|
|
|
|
setIsVerifying(false);
|
2025-11-18 06:19:26 +09:00
|
|
|
onClose();
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-28 14:49:56 +09:00
|
|
|
// 완료 팝업은 open prop과 관계없이 표시
|
|
|
|
|
if (showSuccessModal) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[2px]"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
>
|
|
|
|
|
<div className="w-[480px] rounded-[12px] border border-[#dee1e6] bg-white">
|
|
|
|
|
{/* header */}
|
|
|
|
|
<div className="flex items-center justify-end h-[80px] p-6">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label="닫기"
|
|
|
|
|
onClick={handleLoginClick}
|
|
|
|
|
className="inline-flex size-6 items-center justify-center text-neutral-700 hover:opacity-80 cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<ModalCloseSvg />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* body */}
|
|
|
|
|
<div className="flex flex-col gap-4 items-center px-6 pb-0 pt-[60px]">
|
|
|
|
|
<h2 className="text-[24px] font-extrabold leading-[1.45] text-[#333c47] text-center">
|
|
|
|
|
비밀번호 변경이 완료됐습니다.
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-[18px] font-normal leading-[1.5] text-[#6c7682] text-center mb-[148px]">
|
|
|
|
|
새로운 비밀번호로 다시 로그인 해주세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* footer */}
|
|
|
|
|
<div className="flex gap-8 items-end justify-center p-6 h-[72px]">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleLoginClick}
|
|
|
|
|
className="h-12 w-[284px] rounded-[10px] bg-[#1f2b91] px-2 text-[16px] font-semibold leading-[1.5] text-white cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
로그인
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
|
2025-11-18 06:19:26 +09:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[2px]"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
>
|
2025-11-27 00:45:55 +09:00
|
|
|
<div className="w-[480px] rounded-[12px] border border-input-border bg-white shadow-[0_10px_30px_rgba(0,0,0,0.06)]">
|
2025-11-18 06:19:26 +09:00
|
|
|
{/* header */}
|
|
|
|
|
<div className="flex items-center justify-between p-6">
|
2025-11-27 00:45:55 +09:00
|
|
|
<h2 className="text-[20px] font-bold leading-[1.5] text-neutral-700">비밀번호 변경</h2>
|
2025-11-18 06:19:26 +09:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label="닫기"
|
|
|
|
|
onClick={onClose}
|
2025-11-27 00:45:55 +09:00
|
|
|
className="inline-flex size-6 items-center justify-center text-neutral-700 hover:opacity-80 cursor-pointer"
|
2025-11-18 06:19:26 +09:00
|
|
|
>
|
2025-11-18 07:53:09 +09:00
|
|
|
<ModalCloseSvg />
|
2025-11-18 06:19:26 +09:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* body */}
|
|
|
|
|
<div className="flex flex-col gap-[10px] px-6">
|
|
|
|
|
<div className="flex flex-col gap-2">
|
2025-11-27 00:45:55 +09:00
|
|
|
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-text-label">아이디 (이메일)</label>
|
2025-11-18 06:19:26 +09:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={email}
|
|
|
|
|
onChange={(e) => setEmail(e.target.value)}
|
2025-11-18 07:53:09 +09:00
|
|
|
className={[
|
2025-11-27 00:45:55 +09:00
|
|
|
"h-10 flex-1 rounded-[8px] border border-input-border px-3 text-[16px] leading-[1.5] text-neutral-700 placeholder:text-text-placeholder-alt outline-none",
|
2025-11-18 07:53:09 +09:00
|
|
|
hasError ? "bg-white" : isCodeSent ? "bg-neutral-50" : "bg-white",
|
|
|
|
|
].join(" ")}
|
2025-11-18 06:19:26 +09:00
|
|
|
placeholder="이메일"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-11-28 14:26:54 +09:00
|
|
|
onClick={async () => {
|
|
|
|
|
if (!email.trim()) {
|
|
|
|
|
setError("이메일을 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-28 14:27:35 +09:00
|
|
|
|
2025-11-28 14:26:54 +09:00
|
|
|
setIsSending(true);
|
2025-11-18 07:53:09 +09:00
|
|
|
setError(null);
|
2025-11-28 14:27:35 +09:00
|
|
|
|
2025-11-28 14:26:54 +09:00
|
|
|
try {
|
|
|
|
|
await apiService.sendPasswordReset(email);
|
|
|
|
|
setRequireCode(true);
|
|
|
|
|
setIsCodeSent(true);
|
2025-11-28 14:49:56 +09:00
|
|
|
// 재전송 시 기존 인증 상태 초기화
|
|
|
|
|
setCode("");
|
|
|
|
|
setIsVerified(false);
|
2025-11-28 14:26:54 +09:00
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "인증번호 전송에 실패했습니다.");
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSending(false);
|
|
|
|
|
}
|
2025-11-18 07:53:09 +09:00
|
|
|
}}
|
2025-11-28 14:26:54 +09:00
|
|
|
disabled={isSending}
|
|
|
|
|
className={[
|
|
|
|
|
"h-10 w-[136px] rounded-[8px] bg-bg-gray-light px-3 text-[16px] font-semibold leading-[1.5] text-neutral-700",
|
|
|
|
|
isSending ? "cursor-not-allowed opacity-50" : "cursor-pointer"
|
|
|
|
|
].join(" ")}
|
2025-11-18 06:19:26 +09:00
|
|
|
>
|
2025-11-28 14:26:54 +09:00
|
|
|
{isSending ? "전송 중..." : (isCodeSent ? "인증번호 재전송" : "인증번호 전송")}
|
2025-11-18 06:19:26 +09:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-18 07:53:09 +09:00
|
|
|
{requireCode ? (
|
|
|
|
|
<div className="flex flex-col gap-2">
|
2025-11-27 00:45:55 +09:00
|
|
|
<div className="w-[100px] text-[15px] font-semibold leading-[1.5] text-text-label">인증번호</div>
|
2025-11-18 07:53:09 +09:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={code}
|
|
|
|
|
onChange={(e) => setCode(e.target.value)}
|
2025-11-28 14:27:35 +09:00
|
|
|
className="h-10 flex-1 rounded-[8px] border border-input-border bg-white px-3 text-[16px] leading-normal text-neutral-700 placeholder:text-text-placeholder-alt outline-none"
|
2025-11-18 07:53:09 +09:00
|
|
|
placeholder="인증번호를 입력해 주세요."
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-11-28 14:26:54 +09:00
|
|
|
disabled={!canConfirm || isVerifying}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
if (!email.trim()) {
|
|
|
|
|
setError("이메일을 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!code.trim()) {
|
|
|
|
|
setError("인증번호를 입력해 주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-28 14:27:35 +09:00
|
|
|
|
2025-11-28 14:26:54 +09:00
|
|
|
setIsVerifying(true);
|
|
|
|
|
setError(null);
|
2025-11-28 14:27:35 +09:00
|
|
|
|
2025-11-28 14:26:54 +09:00
|
|
|
try {
|
|
|
|
|
await apiService.verifyPasswordResetCode(email, code);
|
|
|
|
|
setIsVerified(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "올바르지 않은 인증번호입니다. 인증번호를 확인해주세요.");
|
|
|
|
|
setIsVerified(false);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsVerifying(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-11-18 07:53:09 +09:00
|
|
|
className={[
|
2025-11-28 14:26:54 +09:00
|
|
|
"h-10 w-[136px] rounded-[8px] px-3 text-[16px] font-semibold leading-[1.5]",
|
|
|
|
|
canConfirm && !isVerifying ? "bg-bg-gray-light text-basic-text cursor-pointer" : "bg-gray-50 text-text-placeholder-alt cursor-default",
|
2025-11-18 07:53:09 +09:00
|
|
|
].join(" ")}
|
|
|
|
|
>
|
2025-11-28 14:26:54 +09:00
|
|
|
{isVerifying ? "확인 중..." : "인증번호 확인"}
|
2025-11-18 07:53:09 +09:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{isCodeSent && !hasError && !isVerified ? (
|
|
|
|
|
<div className="px-1">
|
2025-11-27 00:45:55 +09:00
|
|
|
<p className="text-[13px] font-semibold leading-[1.4] text-primary">
|
2025-11-18 07:53:09 +09:00
|
|
|
인증 확인을 위해 작성한 이메일로 인증번호를 발송했습니다.
|
|
|
|
|
</p>
|
2025-11-27 00:45:55 +09:00
|
|
|
<p className="text-[13px] font-semibold leading-[1.4] text-primary">
|
2025-11-18 07:53:09 +09:00
|
|
|
이메일을 확인해 주세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
{error ? (
|
2025-11-27 00:45:55 +09:00
|
|
|
<p className="px-1 text-[13px] font-semibold leading-[1.4] text-error">
|
2025-11-18 07:53:09 +09:00
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
2025-11-18 06:19:26 +09:00
|
|
|
</div>
|
2025-11-18 07:53:09 +09:00
|
|
|
) : null}
|
2025-11-18 06:19:26 +09:00
|
|
|
<div className="flex flex-col gap-2">
|
2025-11-27 00:45:55 +09:00
|
|
|
<label className="text-[15px] font-semibold leading-[1.5] text-text-label">새 비밀번호</label>
|
2025-11-18 06:19:26 +09:00
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
value={newPassword}
|
|
|
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
2025-11-18 07:53:09 +09:00
|
|
|
disabled={!isVerified}
|
|
|
|
|
className={[
|
2025-11-27 00:45:55 +09:00
|
|
|
"h-10 rounded-[8px] border border-input-border px-3 text-[16px] leading-[1.5] text-neutral-700 placeholder:text-text-placeholder-alt outline-none",
|
2025-11-18 07:53:09 +09:00
|
|
|
isVerified ? "bg-white" : "bg-neutral-50",
|
|
|
|
|
].join(" ")}
|
2025-11-18 06:19:26 +09:00
|
|
|
placeholder="새 비밀번호"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
2025-11-27 00:45:55 +09:00
|
|
|
<label className="text-[15px] font-semibold leading-[1.5] text-text-label">
|
2025-11-18 06:19:26 +09:00
|
|
|
새 비밀번호 확인
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
value={confirmPassword}
|
|
|
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
2025-11-18 07:53:09 +09:00
|
|
|
disabled={!isVerified}
|
|
|
|
|
className={[
|
2025-11-27 00:45:55 +09:00
|
|
|
"h-10 rounded-[8px] border border-input-border px-3 text-[16px] leading-[1.5] text-neutral-700 placeholder:text-text-placeholder-alt outline-none",
|
2025-11-18 07:53:09 +09:00
|
|
|
isVerified ? "bg-white" : "bg-neutral-50",
|
|
|
|
|
].join(" ")}
|
2025-11-18 06:19:26 +09:00
|
|
|
placeholder="새 비밀번호 확인"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* footer */}
|
|
|
|
|
<div className="flex items-center justify-center gap-3 p-6">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2025-11-28 14:49:56 +09:00
|
|
|
onClick={handleCancel}
|
2025-11-27 00:45:55 +09:00
|
|
|
className="h-12 w-[136px] rounded-[10px] bg-bg-gray-light px-4 text-[16px] font-semibold leading-[1.5] text-basic-text cursor-pointer"
|
2025-11-18 06:19:26 +09:00
|
|
|
>
|
|
|
|
|
취소
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleSubmit}
|
2025-11-27 00:45:55 +09:00
|
|
|
className="h-12 w-[136px] rounded-[10px] bg-inactive-button px-4 text-[16px] font-semibold leading-[1.5] text-white cursor-pointer"
|
2025-11-18 06:19:26 +09:00
|
|
|
>
|
|
|
|
|
비밀번호 변경
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|