사용자 변경 이력 조회 구현
This commit is contained in:
parent
3027f2c817
commit
b43a88a045
|
|
@ -1832,6 +1832,158 @@ export const checkDuplicateUserId = async (
|
||||||
* 사용자 등록/수정 API
|
* 사용자 등록/수정 API
|
||||||
* 기존 Java AdminController의 saveUserInfo 기능 포팅
|
* 기존 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) => {
|
export const saveUser = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const userData = req.body;
|
const userData = req.body;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
deleteMenusBatch, // 메뉴 일괄 삭제
|
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||||
getUserList,
|
getUserList,
|
||||||
getUserInfo, // 사용자 상세 조회
|
getUserInfo, // 사용자 상세 조회
|
||||||
|
getUserHistory, // 사용자 변경이력 조회
|
||||||
getDepartmentList, // 부서 목록 조회
|
getDepartmentList, // 부서 목록 조회
|
||||||
checkDuplicateUserId, // 사용자 ID 중복 체크
|
checkDuplicateUserId, // 사용자 ID 중복 체크
|
||||||
saveUser, // 사용자 등록/수정
|
saveUser, // 사용자 등록/수정
|
||||||
|
|
@ -39,6 +40,7 @@ router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
|
||||||
// 사용자 관리 API
|
// 사용자 관리 API
|
||||||
router.get("/users", getUserList);
|
router.get("/users", getUserList);
|
||||||
router.get("/users/:userId", getUserInfo); // 사용자 상세 조회
|
router.get("/users/:userId", getUserInfo); // 사용자 상세 조회
|
||||||
|
router.get("/users/:userId/history", getUserHistory); // 사용자 변경이력 조회
|
||||||
router.post("/users", saveUser); // 사용자 등록/수정
|
router.post("/users", saveUser); // 사용자 등록/수정
|
||||||
router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크
|
router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,20 @@ export interface AuthStatusInfo {
|
||||||
export interface AuthenticatedRequest extends Request {
|
export interface AuthenticatedRequest extends Request {
|
||||||
user?: PersonBean;
|
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 [maxPageSize, setMaxPageSize] = useState(1);
|
||||||
|
|
||||||
// 페이지네이션 정보 계산
|
// 페이지네이션 정보 계산
|
||||||
|
const totalPages = Math.ceil(totalItems / pageSize);
|
||||||
const paginationInfo: PaginationInfo = {
|
const paginationInfo: PaginationInfo = {
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages: maxPageSize,
|
totalPages: totalPages || 1,
|
||||||
totalItems,
|
totalItems,
|
||||||
itemsPerPage: pageSize,
|
itemsPerPage: pageSize,
|
||||||
startItem: totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0,
|
startItem: totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0,
|
||||||
|
|
@ -62,14 +63,16 @@ export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHist
|
||||||
console.log("📊 백엔드 응답:", response);
|
console.log("📊 백엔드 응답:", response);
|
||||||
|
|
||||||
if (response && response.success && Array.isArray(response.data)) {
|
if (response && response.success && Array.isArray(response.data)) {
|
||||||
// 원본 JSP처럼 No 컬럼을 RM 값으로 설정
|
const responseTotal = response.total || 0;
|
||||||
const mappedHistoryList = response.data.map((item) => ({
|
|
||||||
|
// No 컬럼을 rowNum 값으로 설정 (페이징 고려)
|
||||||
|
const mappedHistoryList = response.data.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
no: item.RM || 0, // 원본 JSP에서는 RM을 No로 사용
|
no: item.rowNum || responseTotal - (pageToLoad - 1) * pageSize - index, // rowNum 우선, 없으면 계산
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setHistoryList(mappedHistoryList);
|
setHistoryList(mappedHistoryList);
|
||||||
setTotalItems(response.total || 0);
|
setTotalItems(responseTotal);
|
||||||
setMaxPageSize(response.maxPageSize || 1);
|
setMaxPageSize(response.maxPageSize || 1);
|
||||||
} else if (response && response.success && (!response.data || response.data.length === 0)) {
|
} 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) => (
|
historyList.map((history, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className="text-center font-mono text-sm">{history.no}</TableCell>
|
<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.sabun || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-sm">{history.USER_ID || "-"}</TableCell>
|
<TableCell className="text-center text-sm">{history.userId || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-sm">{history.USER_NAME || "-"}</TableCell>
|
<TableCell className="text-center text-sm">{history.userName || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-sm">{history.DEPT_NAME || "-"}</TableCell>
|
<TableCell className="text-center text-sm">{history.deptName || "-"}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge variant={getStatusBadgeVariant(history.STATUS || "")}>
|
<Badge variant={getStatusBadgeVariant(history.status || "")}>
|
||||||
{getStatusText(history.STATUS || "")}
|
{getStatusText(history.status || "")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge variant={getChangeTypeBadgeVariant(history.HISTORY_TYPE || "")}>
|
<Badge variant={getChangeTypeBadgeVariant(history.historyType || "")}>
|
||||||
{history.HISTORY_TYPE || "-"}
|
{history.historyType || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</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">
|
<TableCell className="text-center text-sm">
|
||||||
{history.REG_DATE_TITLE || formatDate(history.REGDATE || "")}
|
{history.regDateTitle || formatDate(history.regDate || "")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,8 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
||||||
const handleOpenHistoryModal = (user: User) => {
|
const handleOpenHistoryModal = (user: User) => {
|
||||||
setHistoryModal({
|
setHistoryModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
userId: user.user_id,
|
userId: user.userId,
|
||||||
userName: user.user_name || user.user_id,
|
userName: user.userName || user.userId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,7 +178,7 @@ export function UserTable({ users, isLoading, paginationInfo, onStatusToggle, on
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{users.map((user, index) => (
|
{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 font-medium">{getRowNumber(index)}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{user.sabun || "-"}</TableCell>
|
<TableCell className="font-mono text-sm">{user.sabun || "-"}</TableCell>
|
||||||
<TableCell className="font-medium">{user.companyCode || "-"}</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}>
|
<TableCell className="max-w-[200px] truncate" title={user.email}>
|
||||||
{user.email || "-"}
|
{user.email || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{formatDate(user.regDate)}</TableCell>
|
<TableCell>{formatDate(user.regDate || "")}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,23 @@
|
||||||
|
|
||||||
export interface UserHistory {
|
export interface UserHistory {
|
||||||
no?: number; // 순번 (프론트엔드에서 추가)
|
no?: number; // 순번 (프론트엔드에서 추가)
|
||||||
rnum?: number; // 행 번호 (쿼리에서 생성)
|
rowNum?: number; // 행 번호 (쿼리에서 생성)
|
||||||
rm?: number; // 정렬 번호 (쿼리에서 생성)
|
|
||||||
|
|
||||||
// USER_INFO_HISTORY 테이블 컬럼들 (원본 구조)
|
// USER_INFO_HISTORY 테이블 컬럼들 (camelCase)
|
||||||
SABUN?: string; // 사번
|
sabun?: string; // 사번
|
||||||
USER_ID: string; // 사용자 ID
|
userId: string; // 사용자 ID
|
||||||
USER_NAME?: string; // 사용자 이름
|
userName?: string; // 사용자 이름
|
||||||
DEPT_CODE?: string; // 부서 코드
|
deptCode?: string; // 부서 코드
|
||||||
DEPT_NAME?: string; // 부서명
|
deptName?: string; // 부서명
|
||||||
USER_TYPE_NAME?: string; // 사용자 타입명 (회사명)
|
userTypeName?: string; // 사용자 타입명 (회사명)
|
||||||
HISTORY_TYPE?: string; // 이력유형
|
historyType?: string; // 이력유형
|
||||||
WRITER?: string; // 작성자 ID
|
writer?: string; // 작성자 ID
|
||||||
REGDATE?: string; // 등록일시
|
regDate?: string; // 등록일시
|
||||||
STATUS?: string; // 상태
|
status?: string; // 상태
|
||||||
|
|
||||||
// 조인된 컬럼들
|
// 조인된 컬럼들
|
||||||
WRITER_NAME?: string; // 작성자명 (JOIN으로 가져옴)
|
writerName?: string; // 작성자명 (JOIN으로 가져옴)
|
||||||
REG_DATE_TITLE?: string; // 작성일 (YYYY-MM-DD 형식)
|
regDateTitle?: string; // 작성일 (YYYY-MM-DD 형식)
|
||||||
|
|
||||||
// MyBatis ROW_NUMBER() 결과
|
|
||||||
RNUM?: number; // 페이지네이션용 행번호
|
|
||||||
RM?: number; // 정렬용 행번호
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 응답 타입
|
// API 응답 타입
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue