비밀번호 변경 기능 구현
This commit is contained in:
parent
9ff797ba89
commit
6f68fa5639
|
|
@ -2592,3 +2592,158 @@ export const deleteCompany = async (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/admin/users/reset-password
|
||||
* 사용자 비밀번호 초기화 API
|
||||
* 기존 Java AdminController.resetUserPassword() 포팅
|
||||
*/
|
||||
export const resetUserPassword = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { userId, newPassword } = req.body;
|
||||
|
||||
logger.info("비밀번호 초기화 요청", { userId, user: req.user });
|
||||
|
||||
// 입력값 검증
|
||||
if (!userId || !userId.trim()) {
|
||||
res.status(400).json({
|
||||
result: false,
|
||||
msg: "사용자 ID가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPassword || !newPassword.trim()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "새 비밀번호가 필요합니다.",
|
||||
msg: "새 비밀번호가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 비밀번호 길이 검증 (최소 4자)
|
||||
if (newPassword.length < 4) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "비밀번호는 최소 4자 이상이어야 합니다.",
|
||||
msg: "비밀번호는 최소 4자 이상이어야 합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new Client({ connectionString: config.databaseUrl });
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// 1. 사용자 존재 여부 확인
|
||||
const userCheckResult = await client.query(
|
||||
"SELECT user_id, user_name FROM user_info WHERE user_id = $1",
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (userCheckResult.rows.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "사용자를 찾을 수 없습니다.",
|
||||
msg: "사용자를 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUser = userCheckResult.rows[0];
|
||||
|
||||
// 2. 비밀번호 암호화 (기존 Java 로직과 동일)
|
||||
let encryptedPassword: string;
|
||||
try {
|
||||
// EncryptUtil과 동일한 암호화 사용
|
||||
const crypto = require("crypto");
|
||||
const keyName = "ILJIAESSECRETKEY";
|
||||
const algorithm = "aes-128-ecb";
|
||||
|
||||
// AES-128-ECB 암호화
|
||||
const cipher = crypto.createCipher(algorithm, keyName);
|
||||
let encrypted = cipher.update(newPassword, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
encryptedPassword = encrypted.toUpperCase();
|
||||
} catch (encryptError) {
|
||||
logger.error("비밀번호 암호화 중 오류 발생", {
|
||||
error: encryptError,
|
||||
userId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "비밀번호 암호화 중 오류가 발생했습니다.",
|
||||
msg: "비밀번호 암호화 중 오류가 발생했습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 비밀번호 업데이트 실행
|
||||
const updateResult = await client.query(
|
||||
"UPDATE user_info SET user_password = $1 WHERE user_id = $2",
|
||||
[encryptedPassword, userId]
|
||||
);
|
||||
|
||||
if (updateResult.rowCount && updateResult.rowCount > 0) {
|
||||
// 4. 이력 저장 (선택적)
|
||||
try {
|
||||
const writer = req.user?.userId || "system";
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO user_info_history
|
||||
(sabun, user_id, user_name, dept_code, dept_name, user_type_name, history_type, writer, regdate, status)
|
||||
VALUES ('', $1, $2, '', '', '', '비밀번호 초기화', $3, NOW(), '')
|
||||
`,
|
||||
[userId, currentUser.user_name || userId, writer]
|
||||
);
|
||||
} catch (historyError) {
|
||||
logger.warn("비밀번호 초기화 이력 저장 실패", {
|
||||
error: historyError,
|
||||
userId,
|
||||
});
|
||||
// 이력 저장 실패해도 비밀번호 초기화는 성공으로 처리
|
||||
}
|
||||
|
||||
logger.info("비밀번호 초기화 성공", {
|
||||
userId,
|
||||
updatedBy: req.user?.userId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: true,
|
||||
message: "비밀번호가 성공적으로 초기화되었습니다.",
|
||||
msg: "비밀번호가 성공적으로 초기화되었습니다.",
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "사용자 정보를 찾을 수 없거나 비밀번호 변경에 실패했습니다.",
|
||||
msg: "사용자 정보를 찾을 수 없거나 비밀번호 변경에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("비밀번호 초기화 중 오류 발생", {
|
||||
error,
|
||||
userId: req.body.userId,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "비밀번호 초기화 중 시스템 오류가 발생했습니다.",
|
||||
msg: "비밀번호 초기화 중 시스템 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
getUserInfo, // 사용자 상세 조회
|
||||
getUserHistory, // 사용자 변경이력 조회
|
||||
changeUserStatus, // 사용자 상태 변경
|
||||
resetUserPassword, // 사용자 비밀번호 초기화
|
||||
getDepartmentList, // 부서 목록 조회
|
||||
checkDuplicateUserId, // 사용자 ID 중복 체크
|
||||
saveUser, // 사용자 등록/수정
|
||||
|
|
@ -45,6 +46,7 @@ router.get("/users/:userId/history", getUserHistory); // 사용자 변경이력
|
|||
router.patch("/users/:userId/status", changeUserStatus); // 사용자 상태 변경
|
||||
router.post("/users", saveUser); // 사용자 등록/수정
|
||||
router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크
|
||||
router.post("/users/reset-password", resetUserPassword); // 사용자 비밀번호 초기화
|
||||
|
||||
// 부서 관리 API
|
||||
router.get("/departments", getDepartmentList); // 부서 목록 조회
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export function UserManagement() {
|
|||
|
||||
// 비밀번호 초기화 성공 핸들러
|
||||
const handlePasswordResetSuccess = () => {
|
||||
refreshData(); // 목록 새로고침
|
||||
// refreshData(); // 비밀번호 변경은 목록에 영향을 주지 않으므로 새로고침 불필요
|
||||
handlePasswordResetClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ 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 { toast } from "react-hot-toast"; // 라이브러리 미설치로 alert 사용
|
||||
import { userAPI } from "@/lib/api/user";
|
||||
import { AlertModal, AlertType } from "@/components/common/AlertModal";
|
||||
|
||||
interface UserPasswordResetModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -24,6 +24,40 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
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!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]*$/;
|
||||
|
|
@ -37,17 +71,17 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
// 초기화 핸들러
|
||||
const handleReset = useCallback(async () => {
|
||||
if (!userId || !newPassword.trim()) {
|
||||
alert("새 비밀번호를 입력해주세요.");
|
||||
showAlert("warning", "입력 필요", "새 비밀번호를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePassword(newPassword)) {
|
||||
alert("비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.");
|
||||
showAlert("warning", "형식 오류", "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert("비밀번호가 일치하지 않습니다.");
|
||||
showAlert("warning", "비밀번호 불일치", "비밀번호가 일치하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -60,15 +94,14 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
});
|
||||
|
||||
if (response.success) {
|
||||
alert("비밀번호가 성공적으로 초기화되었습니다.");
|
||||
handleClose();
|
||||
onSuccess?.();
|
||||
showAlert("success", "초기화 완료", "비밀번호가 성공적으로 초기화되었습니다.");
|
||||
// 성공 알림은 사용자가 확인 버튼을 눌러서 닫도록 함
|
||||
} else {
|
||||
alert(response.message || "비밀번호 초기화에 실패했습니다.");
|
||||
showAlert("error", "초기화 실패", response.message || "비밀번호 초기화에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("비밀번호 초기화 오류:", error);
|
||||
alert("비밀번호 초기화 중 오류가 발생했습니다.");
|
||||
showAlert("error", "시스템 오류", "비밀번호 초기화 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -183,6 +216,15 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
{/* 알림 모달 */}
|
||||
<AlertModal
|
||||
isOpen={alertState.isOpen}
|
||||
onClose={closeAlert}
|
||||
type={alertState.type}
|
||||
title={alertState.title}
|
||||
message={alertState.message}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle, XCircle, AlertTriangle, Info } from "lucide-react";
|
||||
|
||||
export type AlertType = "success" | "error" | "warning" | "info";
|
||||
|
||||
interface AlertModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
type: AlertType;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
const alertConfig = {
|
||||
success: {
|
||||
icon: CheckCircle,
|
||||
iconColor: "text-green-500",
|
||||
titleColor: "text-green-700",
|
||||
},
|
||||
error: {
|
||||
icon: XCircle,
|
||||
iconColor: "text-red-500",
|
||||
titleColor: "text-red-700",
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
iconColor: "text-yellow-500",
|
||||
titleColor: "text-yellow-700",
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
iconColor: "text-blue-500",
|
||||
titleColor: "text-blue-700",
|
||||
},
|
||||
};
|
||||
|
||||
export function AlertModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
confirmText = "확인",
|
||||
onConfirm,
|
||||
}: AlertModalProps) {
|
||||
const config = alertConfig[type];
|
||||
const IconComponent = config.icon;
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<IconComponent className={`h-6 w-6 ${config.iconColor}`} />
|
||||
<DialogTitle className={config.titleColor}>{title}</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-left">{message}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleConfirm} className="w-full">
|
||||
{confirmText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// 편의를 위한 래퍼 함수들
|
||||
export const SuccessModal = (props: Omit<AlertModalProps, "type">) => <AlertModal {...props} type="success" />;
|
||||
|
||||
export const ErrorModal = (props: Omit<AlertModalProps, "type">) => <AlertModal {...props} type="error" />;
|
||||
|
||||
export const WarningModal = (props: Omit<AlertModalProps, "type">) => <AlertModal {...props} type="warning" />;
|
||||
|
||||
export const InfoModal = (props: Omit<AlertModalProps, "type">) => <AlertModal {...props} type="info" />;
|
||||
Loading…
Reference in New Issue