From b43a88a045746b7bb92ff10f51a6b9da57f692b4 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 25 Aug 2025 18:30:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=9D=B4=EB=A0=A5=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/adminController.ts | 152 ++++++++++++++++++ backend-node/src/routes/adminRoutes.ts | 2 + backend-node/src/types/auth.ts | 17 ++ .../components/admin/UserHistoryModal.tsx | 33 ++-- frontend/components/admin/UserTable.tsx | 8 +- frontend/types/userHistory.ts | 33 ++-- 6 files changed, 207 insertions(+), 38 deletions(-) diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index bac13d71..a91e8a31 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1832,6 +1832,158 @@ export const checkDuplicateUserId = async ( * 사용자 등록/수정 API * 기존 Java AdminController의 saveUserInfo 기능 포팅 */ +/** + * GET /api/admin/users/:userId/history + * 사용자 변경이력 조회 API + * 기존 Java AdminController.getUserHistory() 포팅 + */ +export const getUserHistory = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { userId } = req.params; + const { page = 1, countPerPage = 10 } = req.query; + + logger.info(`사용자 변경이력 조회 요청 - userId: ${userId}`, { + page, + countPerPage, + user: req.user, + }); + + if (!userId) { + res.status(400).json({ + success: false, + message: "사용자 ID가 필요합니다.", + error: { + code: "USER_ID_REQUIRED", + details: "userId parameter is required", + }, + }); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + // 페이징 계산 + const currentPage = Number(page); + const pageSize = Number(countPerPage); + const pageStart = (currentPage - 1) * pageSize + 1; + const pageEnd = currentPage * pageSize; + + // 전체 건수 조회 쿼리 (기존 backend와 동일한 로직) + const countQuery = ` + SELECT + CEIL(TOTAL_CNT::float / $1)::integer AS MAX_PAGE_SIZE, + TOTAL_CNT + FROM ( + SELECT + COUNT(1) AS TOTAL_CNT + FROM user_info_history + WHERE user_id = $2 + ) A + `; + + const countResult = await client.query(countQuery, [pageSize, userId]); + const countData = countResult.rows[0] || { + total_cnt: 0, + max_page_size: 1, + }; + + // 변경이력 목록 조회 쿼리 (기존 backend와 동일한 로직) + const historyQuery = ` + SELECT + A.* + FROM ( + SELECT + A.*, + ROW_NUMBER() OVER (ORDER BY RM DESC) AS RNUM + FROM ( + SELECT + T.*, + ROW_NUMBER() OVER (ORDER BY regdate) AS RM, + (SELECT user_name FROM user_info UI WHERE T.writer = UI.user_id) AS writer_name, + TO_CHAR(T.regdate, 'YYYY-MM-DD HH24:MI:SS') AS reg_date_title + FROM + user_info_history T + WHERE user_id = $1 + ) A + WHERE 1=1 + ) A + WHERE 1=1 + AND RNUM::integer <= $2 + AND RNUM::integer >= $3 + ORDER BY RM DESC + `; + + const historyResult = await client.query(historyQuery, [ + userId, + pageEnd, + pageStart, + ]); + + // 응답 데이터 가공 + const historyList = historyResult.rows.map((row) => ({ + sabun: row.sabun || "", + userId: row.user_id || "", + userName: row.user_name || "", + deptCode: row.dept_code || "", + deptName: row.dept_name || "", + userTypeName: row.user_type_name || "", + historyType: row.history_type || "", + writer: row.writer || "", + writerName: row.writer_name || "", + regDate: row.regdate, + regDateTitle: row.reg_date_title || "", + status: row.status || "", + rowNum: row.rnum, + })); + + logger.info( + `사용자 변경이력 조회 완료 - userId: ${userId}, 조회건수: ${historyList.length}, 전체: ${countData.total_cnt}` + ); + + const response: ApiResponse = { + success: true, + data: historyList, + total: Number(countData.total_cnt), + + pagination: { + page: currentPage, + limit: pageSize, + total: Number(countData.total_cnt), + totalPages: Number(countData.max_page_size), + }, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("사용자 변경이력 조회 중 오류 발생", error); + + const response: ApiResponse = { + success: false, + message: "사용자 변경이력 조회 중 오류가 발생했습니다.", + error: { + code: "USER_HISTORY_FETCH_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +}; + export const saveUser = async (req: AuthenticatedRequest, res: Response) => { try { const userData = req.body; diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index 1d40c7b9..c3e79cd6 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -9,6 +9,7 @@ import { deleteMenusBatch, // 메뉴 일괄 삭제 getUserList, getUserInfo, // 사용자 상세 조회 + getUserHistory, // 사용자 변경이력 조회 getDepartmentList, // 부서 목록 조회 checkDuplicateUserId, // 사용자 ID 중복 체크 saveUser, // 사용자 등록/수정 @@ -39,6 +40,7 @@ router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제 // 사용자 관리 API router.get("/users", getUserList); router.get("/users/:userId", getUserInfo); // 사용자 상세 조회 +router.get("/users/:userId/history", getUserHistory); // 사용자 변경이력 조회 router.post("/users", saveUser); // 사용자 등록/수정 router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크 diff --git a/backend-node/src/types/auth.ts b/backend-node/src/types/auth.ts index 18ba619f..785157f9 100644 --- a/backend-node/src/types/auth.ts +++ b/backend-node/src/types/auth.ts @@ -92,3 +92,20 @@ export interface AuthStatusInfo { export interface AuthenticatedRequest extends Request { user?: PersonBean; } + +// 사용자 변경이력 타입 +export interface UserHistory { + sabun: string; + userId: string; + userName: string; + deptCode: string; + deptName: string; + userTypeName: string; + historyType: string; + writer: string; + writerName: string; + regDate: Date; + regDateTitle: string; + status: string; + rowNum: number; +} diff --git a/frontend/components/admin/UserHistoryModal.tsx b/frontend/components/admin/UserHistoryModal.tsx index fb351799..dd6614b0 100644 --- a/frontend/components/admin/UserHistoryModal.tsx +++ b/frontend/components/admin/UserHistoryModal.tsx @@ -33,9 +33,10 @@ export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHist const [maxPageSize, setMaxPageSize] = useState(1); // 페이지네이션 정보 계산 + const totalPages = Math.ceil(totalItems / pageSize); const paginationInfo: PaginationInfo = { currentPage, - totalPages: maxPageSize, + totalPages: totalPages || 1, totalItems, itemsPerPage: pageSize, startItem: totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0, @@ -62,14 +63,16 @@ export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHist console.log("📊 백엔드 응답:", response); if (response && response.success && Array.isArray(response.data)) { - // 원본 JSP처럼 No 컬럼을 RM 값으로 설정 - const mappedHistoryList = response.data.map((item) => ({ + const responseTotal = response.total || 0; + + // No 컬럼을 rowNum 값으로 설정 (페이징 고려) + const mappedHistoryList = response.data.map((item, index) => ({ ...item, - no: item.RM || 0, // 원본 JSP에서는 RM을 No로 사용 + no: item.rowNum || responseTotal - (pageToLoad - 1) * pageSize - index, // rowNum 우선, 없으면 계산 })); setHistoryList(mappedHistoryList); - setTotalItems(response.total || 0); + setTotalItems(responseTotal); setMaxPageSize(response.maxPageSize || 1); } else if (response && response.success && (!response.data || response.data.length === 0)) { // 데이터가 비어있는 경우 @@ -202,23 +205,23 @@ export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHist historyList.map((history, index) => ( {history.no} - {history.SABUN || "-"} - {history.USER_ID || "-"} - {history.USER_NAME || "-"} - {history.DEPT_NAME || "-"} + {history.sabun || "-"} + {history.userId || "-"} + {history.userName || "-"} + {history.deptName || "-"} - - {getStatusText(history.STATUS || "")} + + {getStatusText(history.status || "")} - - {history.HISTORY_TYPE || "-"} + + {history.historyType || "-"} - {history.WRITER_NAME || "-"} + {history.writerName || "-"} - {history.REG_DATE_TITLE || formatDate(history.REGDATE || "")} + {history.regDateTitle || formatDate(history.regDate || "")} )) diff --git a/frontend/components/admin/UserTable.tsx b/frontend/components/admin/UserTable.tsx index bc5714e9..a4b55731 100644 --- a/frontend/components/admin/UserTable.tsx +++ b/frontend/components/admin/UserTable.tsx @@ -81,8 +81,8 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on const handleOpenHistoryModal = (user: User) => { setHistoryModal({ isOpen: true, - userId: user.user_id, - userName: user.user_name || user.user_id, + userId: user.userId, + userName: user.userName || user.userId, }); }; @@ -178,7 +178,7 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on {users.map((user, index) => ( - + {getRowNumber(index)} {user.sabun || "-"} {user.companyCode || "-"} @@ -190,7 +190,7 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on {user.email || "-"} - {formatDate(user.regDate)} + {formatDate(user.regDate || "")}