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 (
+
+ );
+}
+
+// 편의를 위한 래퍼 함수들
+export const SuccessModal = (props: Omit) => ;
+
+export const ErrorModal = (props: Omit) => ;
+
+export const WarningModal = (props: Omit) => ;
+
+export const InfoModal = (props: Omit) => ;