From e9897d416b3e7327bbd4d4aef887eee37751ae82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 27 Jun 2025 01:16:20 +0000 Subject: (대표님) 20250627 오전 10시 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/login/reset-password.tsx | 351 ++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 components/login/reset-password.tsx (limited to 'components/login/reset-password.tsx') diff --git a/components/login/reset-password.tsx b/components/login/reset-password.tsx new file mode 100644 index 00000000..f68018d9 --- /dev/null +++ b/components/login/reset-password.tsx @@ -0,0 +1,351 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormState } from 'react-dom'; +import { useToast } from '@/hooks/use-toast'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Ship, Eye, EyeOff, CheckCircle, XCircle, AlertCircle, Shield } from 'lucide-react'; +import Link from 'next/link'; +import SuccessPage from './SuccessPage'; +import { PasswordPolicy } from '@/lib/users/auth/passwordUtil'; +import { PasswordValidationResult, resetPasswordAction, validatePasswordAction } from '@/lib/users/auth/partners-auth'; + +interface PasswordRequirement { + text: string; + met: boolean; + type: 'length' | 'uppercase' | 'lowercase' | 'number' | 'symbol' | 'pattern'; +} + +interface Props { + token: string; + userId: number; + passwordPolicy: PasswordPolicy; +} + +export default function ResetPasswordForm({ token, userId, passwordPolicy }: Props) { + const router = useRouter(); + const { toast } = useToast(); + + // 상태 관리 + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordValidation, setPasswordValidation] = useState(null); + const [isValidatingPassword, setIsValidatingPassword] = useState(false); + + // 서버 액션 상태 + const [resetState, resetAction] = useFormState(resetPasswordAction, { + success: false, + error: undefined, + message: undefined, + }); + + // 패스워드 검증 (디바운싱 적용) + useEffect(() => { + const validatePassword = async () => { + if (!newPassword) { + setPasswordValidation(null); + return; + } + + setIsValidatingPassword(true); + + try { + // 사용자 ID를 포함한 검증 (히스토리 체크 포함) + const validation = await validatePasswordAction(newPassword, userId); + setPasswordValidation(validation); + } catch (error) { + console.error('Password validation error:', error); + setPasswordValidation(null); + } finally { + setIsValidatingPassword(false); + } + }; + + // 디바운싱: 500ms 후에 검증 실행 + const timeoutId = setTimeout(validatePassword, 500); + return () => clearTimeout(timeoutId); + }, [newPassword, userId]); + + // 서버 액션 결과 처리 + useEffect(() => { + if (resetState.error) { + toast({ + title: '오류', + description: resetState.error, + variant: 'destructive', + }); + } + }, [resetState, toast]); + + // 패스워드 요구사항 생성 + const getPasswordRequirements = (): PasswordRequirement[] => { + if (!passwordValidation) return []; + + const { strength } = passwordValidation; + const requirements: PasswordRequirement[] = [ + { + text: `${passwordPolicy.minLength}자 이상`, + met: strength.length >= passwordPolicy.minLength, + type: 'length' + } + ]; + + if (passwordPolicy.requireUppercase) { + requirements.push({ + text: '대문자 포함', + met: strength.hasUppercase, + type: 'uppercase' + }); + } + + if (passwordPolicy.requireLowercase) { + requirements.push({ + text: '소문자 포함', + met: strength.hasLowercase, + type: 'lowercase' + }); + } + + if (passwordPolicy.requireNumbers) { + requirements.push({ + text: '숫자 포함', + met: strength.hasNumbers, + type: 'number' + }); + } + + if (passwordPolicy.requireSymbols) { + requirements.push({ + text: '특수문자 포함', + met: strength.hasSymbols, + type: 'symbol' + }); + } + + return requirements; + }; + + // 패스워드 강도 색상 + const getStrengthColor = (score: number) => { + switch (score) { + case 1: return 'text-red-600'; + case 2: return 'text-orange-600'; + case 3: return 'text-yellow-600'; + case 4: return 'text-blue-600'; + case 5: return 'text-green-600'; + default: return 'text-gray-600'; + } + }; + + const getStrengthText = (score: number) => { + switch (score) { + case 1: return '매우 약함'; + case 2: return '약함'; + case 3: return '보통'; + case 4: return '강함'; + case 5: return '매우 강함'; + default: return ''; + } + }; + + const passwordRequirements = getPasswordRequirements(); + const allRequirementsMet = passwordValidation?.policyValid && passwordValidation?.historyValid !== false; + const passwordsMatch = newPassword === confirmPassword && confirmPassword.length > 0; + const canSubmit = allRequirementsMet && passwordsMatch && !isValidatingPassword; + + // 성공 화면 + if (resetState.success) { + return ; + } + + return ( + + +
+ + eVCP +
+ 새 비밀번호 설정 + + 계정 보안을 위해 강력한 비밀번호를 설정해주세요. + +
+ + +
+ + + {/* 새 비밀번호 */} +
+ +
+ setNewPassword(e.target.value)} + placeholder="새 비밀번호를 입력하세요" + required + /> + +
+ + {/* 패스워드 강도 표시 */} + {passwordValidation && ( +
+
+ + 강도: + + {getStrengthText(passwordValidation.strength.score)} + + {isValidatingPassword && ( +
+ )} +
+ + {/* 강도 진행바 */} +
+
+
+
+ )} + + {/* 패스워드 요구사항 */} + {passwordRequirements.length > 0 && ( +
+ {passwordRequirements.map((req, index) => ( +
+ {req.met ? ( + + ) : ( + + )} + + {req.text} + +
+ ))} +
+ )} + + {/* 히스토리 검증 결과 */} + {passwordValidation?.historyValid === false && ( +
+
+ + + 최근 {passwordPolicy.historyCount}개 비밀번호와 달라야 합니다 + +
+
+ )} + + {/* 추가 피드백 */} + {passwordValidation?.strength.feedback && passwordValidation.strength.feedback.length > 0 && ( +
+ {passwordValidation.strength.feedback.map((feedback, index) => ( +
+ + {feedback} +
+ ))} +
+ )} + + {/* 정책 오류 */} + {passwordValidation && !passwordValidation.policyValid && passwordValidation.policyErrors.length > 0 && ( +
+ {passwordValidation.policyErrors.map((error, index) => ( +
+ + {error} +
+ ))} +
+ )} +
+ + {/* 비밀번호 확인 */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="비밀번호를 다시 입력하세요" + required + /> + +
+ + {/* 비밀번호 일치 확인 */} + {confirmPassword && ( +
+ {passwordsMatch ? ( + <> + + 비밀번호가 일치합니다 + + ) : ( + <> + + 비밀번호가 일치하지 않습니다 + + )} +
+ )} +
+ + + + +
+ + 로그인 페이지로 돌아가기 + +
+ + + ); +} \ No newline at end of file -- cgit v1.2.3