사용자 변경 이력 조회 구현
This commit is contained in:
parent
3027f2c817
commit
b43a88a045
|
|
@ -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<any[]> = {
|
||||
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<null> = {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 중복 체크
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="text-center font-mono text-sm">{history.no}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.SABUN || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.USER_ID || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.USER_NAME || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.DEPT_NAME || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.sabun || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.userId || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.userName || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.deptName || "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={getStatusBadgeVariant(history.STATUS || "")}>
|
||||
{getStatusText(history.STATUS || "")}
|
||||
<Badge variant={getStatusBadgeVariant(history.status || "")}>
|
||||
{getStatusText(history.status || "")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={getChangeTypeBadgeVariant(history.HISTORY_TYPE || "")}>
|
||||
{history.HISTORY_TYPE || "-"}
|
||||
<Badge variant={getChangeTypeBadgeVariant(history.historyType || "")}>
|
||||
{history.historyType || "-"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.WRITER_NAME || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{history.writerName || "-"}</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{history.REG_DATE_TITLE || formatDate(history.REGDATE || "")}
|
||||
{history.regDateTitle || formatDate(history.regDate || "")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user, index) => (
|
||||
<TableRow key={`${user.user_id}-${index}`} className="hover:bg-muted/50">
|
||||
<TableRow key={`${user.userId}-${index}`} className="hover:bg-muted/50">
|
||||
<TableCell className="font-mono text-sm font-medium">{getRowNumber(index)}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{user.sabun || "-"}</TableCell>
|
||||
<TableCell className="font-medium">{user.companyCode || "-"}</TableCell>
|
||||
|
|
@ -190,7 +190,7 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
|||
<TableCell className="max-w-[200px] truncate" title={user.email}>
|
||||
{user.email || "-"}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(user.regDate)}</TableCell>
|
||||
<TableCell>{formatDate(user.regDate || "")}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -2,28 +2,23 @@
|
|||
|
||||
export interface UserHistory {
|
||||
no?: number; // 순번 (프론트엔드에서 추가)
|
||||
rnum?: number; // 행 번호 (쿼리에서 생성)
|
||||
rm?: number; // 정렬 번호 (쿼리에서 생성)
|
||||
rowNum?: number; // 행 번호 (쿼리에서 생성)
|
||||
|
||||
// USER_INFO_HISTORY 테이블 컬럼들 (원본 구조)
|
||||
SABUN?: string; // 사번
|
||||
USER_ID: string; // 사용자 ID
|
||||
USER_NAME?: string; // 사용자 이름
|
||||
DEPT_CODE?: string; // 부서 코드
|
||||
DEPT_NAME?: string; // 부서명
|
||||
USER_TYPE_NAME?: string; // 사용자 타입명 (회사명)
|
||||
HISTORY_TYPE?: string; // 이력유형
|
||||
WRITER?: string; // 작성자 ID
|
||||
REGDATE?: string; // 등록일시
|
||||
STATUS?: string; // 상태
|
||||
// USER_INFO_HISTORY 테이블 컬럼들 (camelCase)
|
||||
sabun?: string; // 사번
|
||||
userId: string; // 사용자 ID
|
||||
userName?: string; // 사용자 이름
|
||||
deptCode?: string; // 부서 코드
|
||||
deptName?: string; // 부서명
|
||||
userTypeName?: string; // 사용자 타입명 (회사명)
|
||||
historyType?: string; // 이력유형
|
||||
writer?: string; // 작성자 ID
|
||||
regDate?: string; // 등록일시
|
||||
status?: string; // 상태
|
||||
|
||||
// 조인된 컬럼들
|
||||
WRITER_NAME?: string; // 작성자명 (JOIN으로 가져옴)
|
||||
REG_DATE_TITLE?: string; // 작성일 (YYYY-MM-DD 형식)
|
||||
|
||||
// MyBatis ROW_NUMBER() 결과
|
||||
RNUM?: number; // 페이지네이션용 행번호
|
||||
RM?: number; // 정렬용 행번호
|
||||
writerName?: string; // 작성자명 (JOIN으로 가져옴)
|
||||
regDateTitle?: string; // 작성일 (YYYY-MM-DD 형식)
|
||||
}
|
||||
|
||||
// API 응답 타입
|
||||
|
|
|
|||
Loading…
Reference in New Issue