diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 03b58c26..1858aedb 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -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: "비밀번호 초기화 중 시스템 오류가 발생했습니다.", + }); + } +}; diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index 460e81d9..afdfb284 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -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); // 부서 목록 조회 diff --git a/frontend/components/admin/UserManagement.tsx b/frontend/components/admin/UserManagement.tsx index 46f867f5..fab8a20f 100644 --- a/frontend/components/admin/UserManagement.tsx +++ b/frontend/components/admin/UserManagement.tsx @@ -84,7 +84,7 @@ export function UserManagement() { // 비밀번호 초기화 성공 핸들러 const handlePasswordResetSuccess = () => { - refreshData(); // 목록 새로고침 + // refreshData(); // 비밀번호 변경은 목록에 영향을 주지 않으므로 새로고침 불필요 handlePasswordResetClose(); }; diff --git a/frontend/components/admin/UserPasswordResetModal.tsx b/frontend/components/admin/UserPasswordResetModal.tsx index cda4dbbb..46928bd3 100644 --- a/frontend/components/admin/UserPasswordResetModal.tsx +++ b/frontend/components/admin/UserPasswordResetModal.tsx @@ -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 + + {/* 알림 모달 */} + ); } diff --git a/frontend/components/common/AlertModal.tsx b/frontend/components/common/AlertModal.tsx new file mode 100644 index 00000000..e1fb19f4 --- /dev/null +++ b/frontend/components/common/AlertModal.tsx @@ -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 ( + !open && onClose()}> + + +
+ + {title} +
+ {message} +
+ + + + +
+
+ ); +} + +// 편의를 위한 래퍼 함수들 +export const SuccessModal = (props: Omit) => ; + +export const ErrorModal = (props: Omit) => ; + +export const WarningModal = (props: Omit) => ; + +export const InfoModal = (props: Omit) => ;