231 lines
7.8 KiB
TypeScript
231 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback } from "react";
|
|
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
|
|
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";
|
|
import { AlertModal, AlertType } from "@/components/common/AlertModal";
|
|
|
|
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);
|
|
|
|
// 알림 모달 상태
|
|
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?.();
|
|
}
|
|
};
|
|
|
|
// 비밀번호 유효성 검사 (영문, 숫자, 특수문자만 허용)
|
|
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()) {
|
|
showAlert("warning", "입력 필요", "새 비밀번호를 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (!validatePassword(newPassword)) {
|
|
showAlert("warning", "형식 오류", "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.");
|
|
return;
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
showAlert("warning", "비밀번호 불일치", "비밀번호가 일치하지 않습니다.");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await userAPI.resetPassword({
|
|
userId: userId,
|
|
newPassword: newPassword,
|
|
});
|
|
|
|
if (response.success) {
|
|
showAlert("success", "초기화 완료", "비밀번호가 성공적으로 초기화되었습니다.");
|
|
// 성공 알림은 사용자가 확인 버튼을 눌러서 닫도록 함
|
|
} else {
|
|
showAlert("error", "초기화 실패", response.message || "비밀번호 초기화에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("비밀번호 초기화 오류:", error);
|
|
showAlert("error", "시스템 오류", "비밀번호 초기화 중 오류가 발생했습니다.");
|
|
} 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 (
|
|
<ResizableDialog open={isOpen} onOpenChange={handleClose}>
|
|
<ResizableDialogContent className="sm:max-w-md">
|
|
<ResizableDialogHeader>
|
|
<ResizableDialogTitle>비밀번호 초기화</ResizableDialogTitle>
|
|
</ResizableDialogHeader>
|
|
|
|
<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>}
|
|
{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>
|
|
</ResizableDialogContent>
|
|
|
|
{/* 알림 모달 */}
|
|
<AlertModal
|
|
isOpen={alertState.isOpen}
|
|
onClose={closeAlert}
|
|
type={alertState.type}
|
|
title={alertState.title}
|
|
message={alertState.message}
|
|
/>
|
|
</ResizableDialog>
|
|
);
|
|
}
|