summaryrefslogtreecommitdiff
path: root/components/login/reset-password.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
commite9897d416b3e7327bbd4d4aef887eee37751ae82 (patch)
treebd20ce6eadf9b21755bd7425492d2d31c7700a0e /components/login/reset-password.tsx
parent3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff)
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'components/login/reset-password.tsx')
-rw-r--r--components/login/reset-password.tsx351
1 files changed, 351 insertions, 0 deletions
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<PasswordValidationResult | null>(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 <SuccessPage message={resetState.message} />;
+ }
+
+ return (
+ <Card className="w-full max-w-md">
+ <CardHeader className="text-center">
+ <div className="mx-auto flex items-center justify-center space-x-2 mb-4">
+ <Ship className="w-6 h-6 text-blue-600" />
+ <span className="text-xl font-bold">eVCP</span>
+ </div>
+ <CardTitle className="text-2xl">새 비밀번호 설정</CardTitle>
+ <CardDescription>
+ 계정 보안을 위해 강력한 비밀번호를 설정해주세요.
+ </CardDescription>
+ </CardHeader>
+
+ <CardContent>
+ <form action={resetAction} className="space-y-6">
+ <input type="hidden" name="token" value={token} />
+
+ {/* 새 비밀번호 */}
+ <div className="space-y-2">
+ <label htmlFor="newPassword" className="text-sm font-medium text-gray-700">
+ 새 비밀번호
+ </label>
+ <div className="relative">
+ <Input
+ id="newPassword"
+ name="newPassword"
+ type={showPassword ? "text" : "password"}
+ value={newPassword}
+ onChange={(e) => setNewPassword(e.target.value)}
+ placeholder="새 비밀번호를 입력하세요"
+ required
+ />
+ <button
+ type="button"
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
+ onClick={() => setShowPassword(!showPassword)}
+ >
+ {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
+ </button>
+ </div>
+
+ {/* 패스워드 강도 표시 */}
+ {passwordValidation && (
+ <div className="mt-2 space-y-2">
+ <div className="flex items-center space-x-2">
+ <Shield className="h-4 w-4 text-gray-500" />
+ <span className="text-xs text-gray-600">강도:</span>
+ <span className={`text-xs font-medium ${getStrengthColor(passwordValidation.strength.score)}`}>
+ {getStrengthText(passwordValidation.strength.score)}
+ </span>
+ {isValidatingPassword && (
+ <div className="ml-2 animate-spin rounded-full h-3 w-3 border-b border-blue-600"></div>
+ )}
+ </div>
+
+ {/* 강도 진행바 */}
+ <div className="w-full bg-gray-200 rounded-full h-2">
+ <div
+ className={`h-2 rounded-full transition-all duration-300 ${
+ passwordValidation.strength.score === 1 ? 'bg-red-500' :
+ passwordValidation.strength.score === 2 ? 'bg-orange-500' :
+ passwordValidation.strength.score === 3 ? 'bg-yellow-500' :
+ passwordValidation.strength.score === 4 ? 'bg-blue-500' :
+ 'bg-green-500'
+ }`}
+ style={{ width: `${(passwordValidation.strength.score / 5) * 100}%` }}
+ />
+ </div>
+ </div>
+ )}
+
+ {/* 패스워드 요구사항 */}
+ {passwordRequirements.length > 0 && (
+ <div className="mt-2 space-y-1">
+ {passwordRequirements.map((req, index) => (
+ <div key={index} className="flex items-center space-x-2 text-xs">
+ {req.met ? (
+ <CheckCircle className="h-3 w-3 text-green-500" />
+ ) : (
+ <XCircle className="h-3 w-3 text-red-500" />
+ )}
+ <span className={req.met ? 'text-green-700' : 'text-red-700'}>
+ {req.text}
+ </span>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* 히스토리 검증 결과 */}
+ {passwordValidation?.historyValid === false && (
+ <div className="mt-2">
+ <div className="flex items-center space-x-2 text-xs">
+ <XCircle className="h-3 w-3 text-red-500" />
+ <span className="text-red-700">
+ 최근 {passwordPolicy.historyCount}개 비밀번호와 달라야 합니다
+ </span>
+ </div>
+ </div>
+ )}
+
+ {/* 추가 피드백 */}
+ {passwordValidation?.strength.feedback && passwordValidation.strength.feedback.length > 0 && (
+ <div className="mt-2 space-y-1">
+ {passwordValidation.strength.feedback.map((feedback, index) => (
+ <div key={index} className="flex items-center space-x-2 text-xs">
+ <AlertCircle className="h-3 w-3 text-orange-500" />
+ <span className="text-orange-700">{feedback}</span>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* 정책 오류 */}
+ {passwordValidation && !passwordValidation.policyValid && passwordValidation.policyErrors.length > 0 && (
+ <div className="mt-2 space-y-1">
+ {passwordValidation.policyErrors.map((error, index) => (
+ <div key={index} className="flex items-center space-x-2 text-xs">
+ <XCircle className="h-3 w-3 text-red-500" />
+ <span className="text-red-700">{error}</span>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+
+ {/* 비밀번호 확인 */}
+ <div className="space-y-2">
+ <label htmlFor="confirmPassword" className="text-sm font-medium text-gray-700">
+ 비밀번호 확인
+ </label>
+ <div className="relative">
+ <Input
+ id="confirmPassword"
+ name="confirmPassword"
+ type={showConfirmPassword ? "text" : "password"}
+ value={confirmPassword}
+ onChange={(e) => setConfirmPassword(e.target.value)}
+ placeholder="비밀번호를 다시 입력하세요"
+ required
+ />
+ <button
+ type="button"
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
+ >
+ {showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
+ </button>
+ </div>
+
+ {/* 비밀번호 일치 확인 */}
+ {confirmPassword && (
+ <div className="flex items-center space-x-2 text-xs">
+ {passwordsMatch ? (
+ <>
+ <CheckCircle className="h-3 w-3 text-green-500" />
+ <span className="text-green-700">비밀번호가 일치합니다</span>
+ </>
+ ) : (
+ <>
+ <XCircle className="h-3 w-3 text-red-500" />
+ <span className="text-red-700">비밀번호가 일치하지 않습니다</span>
+ </>
+ )}
+ </div>
+ )}
+ </div>
+
+ <Button
+ type="submit"
+ className="w-full"
+ disabled={!canSubmit}
+ >
+ {isValidatingPassword ? '검증 중...' : '비밀번호 변경하기'}
+ </Button>
+ </form>
+
+ <div className="mt-6 text-center">
+ <Link href="/partners" className="text-sm text-blue-600 hover:text-blue-500">
+ 로그인 페이지로 돌아가기
+ </Link>
+ </div>
+ </CardContent>
+ </Card>
+ );
+} \ No newline at end of file