ERP-node/frontend/components/admin/UserPasswordResetModal.tsx

231 lines
7.8 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"use client";
import React, { useState, useCallback } from "react";
2025-11-05 16:36:32 +09:00
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
2025-08-21 09:41:46 +09:00
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Eye, EyeOff } from "lucide-react";
import { userAPI } from "@/lib/api/user";
2025-08-26 13:42:57 +09:00
import { AlertModal, AlertType } from "@/components/common/AlertModal";
2025-08-21 09:41:46 +09:00
interface UserPasswordResetModalProps {
isOpen: boolean;
onClose: () => void;
userId: string | null;
userName: string | null;
onSuccess?: () => void;
}
export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSuccess }: UserPasswordResetModalProps) {
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
2025-08-26 13:42:57 +09:00
// 알림 모달 상태
const [alertState, setAlertState] = useState<{
isOpen: boolean;
type: AlertType;
title: string;
message: string;
}>({
isOpen: false,
type: "info",
title: "",
message: "",
});
// 알림 모달 표시 헬퍼 함수
const showAlert = (type: AlertType, title: string, message: string) => {
setAlertState({
isOpen: true,
type,
title,
message,
});
};
// 알림 모달 닫기
const closeAlert = () => {
setAlertState((prev) => ({ ...prev, isOpen: false }));
// 성공 알림이 닫힐 때 메인 모달도 닫기
if (alertState.type === "success") {
handleClose();
onSuccess?.();
}
};
2025-08-21 09:41:46 +09:00
// 비밀번호 유효성 검사 (영문, 숫자, 특수문자만 허용)
const validatePassword = (password: string) => {
const regex = /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]*$/;
return regex.test(password);
};
// 비밀번호 일치 여부 확인
const isPasswordMatch = newPassword && confirmPassword && newPassword === confirmPassword;
const showMismatchError = confirmPassword && newPassword !== confirmPassword;
// 초기화 핸들러
const handleReset = useCallback(async () => {
if (!userId || !newPassword.trim()) {
2025-08-26 13:42:57 +09:00
showAlert("warning", "입력 필요", "새 비밀번호를 입력해주세요.");
2025-08-21 09:41:46 +09:00
return;
}
if (!validatePassword(newPassword)) {
2025-08-26 13:42:57 +09:00
showAlert("warning", "형식 오류", "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.");
2025-08-21 09:41:46 +09:00
return;
}
if (newPassword !== confirmPassword) {
2025-08-26 13:42:57 +09:00
showAlert("warning", "비밀번호 불일치", "비밀번호가 일치하지 않습니다.");
2025-08-21 09:41:46 +09:00
return;
}
setIsLoading(true);
try {
const response = await userAPI.resetPassword({
userId: userId,
newPassword: newPassword,
});
if (response.success) {
2025-08-26 13:42:57 +09:00
showAlert("success", "초기화 완료", "비밀번호가 성공적으로 초기화되었습니다.");
// 성공 알림은 사용자가 확인 버튼을 눌러서 닫도록 함
2025-08-21 09:41:46 +09:00
} else {
2025-08-26 13:42:57 +09:00
showAlert("error", "초기화 실패", response.message || "비밀번호 초기화에 실패했습니다.");
2025-08-21 09:41:46 +09:00
}
} catch (error) {
console.error("비밀번호 초기화 오류:", error);
2025-08-26 13:42:57 +09:00
showAlert("error", "시스템 오류", "비밀번호 초기화 중 오류가 발생했습니다.");
2025-08-21 09:41:46 +09:00
} finally {
setIsLoading(false);
}
}, [userId, newPassword, confirmPassword, onSuccess]);
// 모달 닫기 핸들러
const handleClose = useCallback(() => {
setNewPassword("");
setConfirmPassword("");
setShowPassword(false);
setShowConfirmPassword(false);
setIsLoading(false);
onClose();
}, [onClose]);
// Enter 키 처리
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !isLoading) {
handleReset();
}
};
if (!userId) return null;
return (
2025-11-05 16:36:32 +09:00
<ResizableDialog open={isOpen} onOpenChange={handleClose}>
<ResizableDialogContent className="sm:max-w-md">
<ResizableDialogHeader>
<ResizableDialogTitle> </ResizableDialogTitle>
</ResizableDialogHeader>
2025-08-21 09:41:46 +09:00
<div className="space-y-4" onKeyDown={handleKeyDown}>
{/* 대상 사용자 정보 */}
<div>
<Label className="text-sm font-medium text-gray-700">
:{" "}
<span className="font-semibold">
{userName} ({userId})
</span>
</Label>
</div>
{/* 새 비밀번호 입력 */}
<div className="space-y-2">
<Label htmlFor="new-password"> </Label>
<div className="relative">
<Input
id="new-password"
type={showPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="새 비밀번호를 입력하세요"
disabled={isLoading}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
disabled={isLoading}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{/* 비밀번호 재확인 */}
<div className="space-y-2">
<Label htmlFor="confirm-password"> </Label>
<div className="relative">
<Input
id="confirm-password"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="비밀번호를 다시 입력하세요"
disabled={isLoading}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
disabled={isLoading}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
{/* 비밀번호 일치 여부 표시 */}
{showMismatchError && <p className="text-sm text-destructive"> .</p>}
2025-08-21 09:41:46 +09:00
{isPasswordMatch && <p className="text-sm text-green-600"> .</p>}
</div>
<div className="text-xs text-gray-500">* , , .</div>
</div>
<div className="mt-6 flex justify-end space-x-2">
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
</Button>
<Button
onClick={handleReset}
disabled={isLoading || !newPassword.trim() || !isPasswordMatch}
className="min-w-[80px]"
>
{isLoading ? "처리중..." : "초기화"}
</Button>
</div>
2025-11-05 16:36:32 +09:00
</ResizableDialogContent>
2025-08-26 13:42:57 +09:00
{/* 알림 모달 */}
<AlertModal
isOpen={alertState.isOpen}
onClose={closeAlert}
type={alertState.type}
title={alertState.title}
message={alertState.message}
/>
2025-11-05 16:36:32 +09:00
</ResizableDialog>
2025-08-21 09:41:46 +09:00
);
}