2025-08-21 09:41:46 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { AlertCircle, User } from "lucide-react";
|
|
|
|
|
import { UserHistory, UserHistoryResponse } from "@/types/userHistory";
|
|
|
|
|
import { userAPI } from "@/lib/api/user";
|
|
|
|
|
import { Pagination, PaginationInfo } from "@/components/common/Pagination";
|
|
|
|
|
import { USER_HISTORY_TABLE_COLUMNS, STATUS_BADGE_VARIANTS, CHANGE_TYPE_BADGE_VARIANTS } from "@/constants/userHistory";
|
|
|
|
|
|
|
|
|
|
interface UserHistoryModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 변경이력 모달 컴포넌트 (원본 JSP 로직 기반)
|
|
|
|
|
*/
|
|
|
|
|
export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHistoryModalProps) {
|
|
|
|
|
const [historyList, setHistoryList] = useState<UserHistory[]>([]);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// 페이지네이션 상태 (원본 JSP와 동일)
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [pageSize] = useState(10); // 원본에서 하드코딩된 값
|
|
|
|
|
const [totalItems, setTotalItems] = useState(0);
|
|
|
|
|
const [maxPageSize, setMaxPageSize] = useState(1);
|
|
|
|
|
|
|
|
|
|
// 페이지네이션 정보 계산
|
2025-08-25 18:30:07 +09:00
|
|
|
const totalPages = Math.ceil(totalItems / pageSize);
|
2025-08-21 09:41:46 +09:00
|
|
|
const paginationInfo: PaginationInfo = {
|
|
|
|
|
currentPage,
|
2025-08-25 18:30:07 +09:00
|
|
|
totalPages: totalPages || 1,
|
2025-08-21 09:41:46 +09:00
|
|
|
totalItems,
|
|
|
|
|
itemsPerPage: pageSize,
|
|
|
|
|
startItem: totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0,
|
|
|
|
|
endItem: Math.min(currentPage * pageSize, totalItems),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 변경이력 데이터 로드 (원본 JSP 로직 기반)
|
|
|
|
|
const loadUserHistory = useCallback(
|
|
|
|
|
async (pageToLoad: number) => {
|
|
|
|
|
if (!userId) return;
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const params = {
|
|
|
|
|
page: pageToLoad,
|
|
|
|
|
countPerPage: pageSize,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log("📊 사용자 변경이력 로드:", { userId, params });
|
|
|
|
|
const response: UserHistoryResponse = await userAPI.getHistory(userId, params);
|
|
|
|
|
|
|
|
|
|
console.log("📊 백엔드 응답:", response);
|
|
|
|
|
|
|
|
|
|
if (response && response.success && Array.isArray(response.data)) {
|
2025-08-25 18:30:07 +09:00
|
|
|
const responseTotal = response.total || 0;
|
|
|
|
|
|
|
|
|
|
// No 컬럼을 rowNum 값으로 설정 (페이징 고려)
|
|
|
|
|
const mappedHistoryList = response.data.map((item, index) => ({
|
2025-08-21 09:41:46 +09:00
|
|
|
...item,
|
2025-08-25 18:30:07 +09:00
|
|
|
no: item.rowNum || responseTotal - (pageToLoad - 1) * pageSize - index, // rowNum 우선, 없으면 계산
|
2025-08-21 09:41:46 +09:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setHistoryList(mappedHistoryList);
|
2025-08-25 18:30:07 +09:00
|
|
|
setTotalItems(responseTotal);
|
2025-08-21 09:41:46 +09:00
|
|
|
setMaxPageSize(response.maxPageSize || 1);
|
|
|
|
|
} else if (response && response.success && (!response.data || response.data.length === 0)) {
|
|
|
|
|
// 데이터가 비어있는 경우
|
|
|
|
|
console.log("📋 변경이력이 없습니다.");
|
|
|
|
|
setHistoryList([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
setMaxPageSize(1);
|
|
|
|
|
} else {
|
|
|
|
|
setHistoryList([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
setMaxPageSize(1);
|
|
|
|
|
setError(response?.message || "변경이력 조회에 실패했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("사용자 변경이력 로드 오류:", err);
|
|
|
|
|
setError(err instanceof Error ? err.message : "변경이력 조회 중 오류가 발생했습니다.");
|
|
|
|
|
setHistoryList([]);
|
|
|
|
|
setTotalItems(0);
|
|
|
|
|
setMaxPageSize(1);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[userId, pageSize],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 모달이 열릴 때마다 데이터 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen && userId) {
|
|
|
|
|
setCurrentPage(1); // 페이지 초기화
|
|
|
|
|
loadUserHistory(1); // 첫 페이지 로드
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, userId, loadUserHistory]);
|
|
|
|
|
|
|
|
|
|
// 페이지 변경 핸들러 (원본 JSP의 fnc_goPage와 유사)
|
|
|
|
|
const handlePageChange = (page: number) => {
|
|
|
|
|
setCurrentPage(page);
|
|
|
|
|
loadUserHistory(page);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태에 따른 배지 색상 (원본 JSP 로직 기반)
|
|
|
|
|
const getStatusBadgeVariant = (status: string) => {
|
|
|
|
|
if (status === "active") return "default";
|
|
|
|
|
if (status === "inactive") return "secondary";
|
|
|
|
|
return STATUS_BADGE_VARIANTS[status as keyof typeof STATUS_BADGE_VARIANTS] || "outline";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 변경 유형에 따른 배지 색상
|
|
|
|
|
const getChangeTypeBadgeVariant = (changeType: string) => {
|
|
|
|
|
return CHANGE_TYPE_BADGE_VARIANTS[changeType as keyof typeof CHANGE_TYPE_BADGE_VARIANTS] || "outline";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태 텍스트 변환 (원본 JSP 로직: ${info.STATUS eq 'active' ? '활성화':'비활성화'})
|
|
|
|
|
const getStatusText = (status: string) => {
|
|
|
|
|
return status === "active" ? "활성화" : "비활성화";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 날짜 포맷팅
|
|
|
|
|
const formatDate = (dateString: string) => {
|
|
|
|
|
if (!dateString) return "-";
|
|
|
|
|
try {
|
|
|
|
|
return new Date(dateString).toLocaleString("ko-KR", {
|
|
|
|
|
year: "numeric",
|
|
|
|
|
month: "2-digit",
|
|
|
|
|
day: "2-digit",
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
return dateString;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
|
|
|
<DialogContent className="flex max-h-[90vh] max-w-6xl flex-col">
|
|
|
|
|
<DialogHeader className="flex-shrink-0">
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
<User className="h-5 w-5" />
|
|
|
|
|
사용자 관리 이력
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<div className="text-muted-foreground text-sm">
|
|
|
|
|
{userName} ({userId})의 변경이력을 조회합니다.
|
|
|
|
|
</div>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
|
|
|
{/* 로딩 상태 */}
|
|
|
|
|
{isLoading && (
|
|
|
|
|
<div className="flex flex-1 items-center justify-center">
|
|
|
|
|
<div className="text-muted-foreground">로딩 중...</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 에러 상태 */}
|
|
|
|
|
{error && !isLoading && (
|
|
|
|
|
<div className="flex flex-1 items-center justify-center">
|
|
|
|
|
<div className="text-destructive flex items-center gap-2">
|
|
|
|
|
<AlertCircle className="h-5 w-5" />
|
|
|
|
|
<span>{error}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 변경이력 테이블 (원본 JSP 구조 기반) */}
|
|
|
|
|
{!isLoading && !error && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex-1 overflow-auto rounded-md border">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader className="bg-background sticky top-0">
|
|
|
|
|
<TableRow>
|
|
|
|
|
{USER_HISTORY_TABLE_COLUMNS.map((column) => (
|
|
|
|
|
<TableHead key={column.key} className="text-center" style={{ width: column.width }}>
|
|
|
|
|
{column.label}
|
|
|
|
|
</TableHead>
|
|
|
|
|
))}
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{historyList.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell
|
|
|
|
|
colSpan={USER_HISTORY_TABLE_COLUMNS.length}
|
|
|
|
|
className="text-muted-foreground py-8 text-center"
|
|
|
|
|
>
|
|
|
|
|
조회된 정보가 없습니다.
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
) : (
|
|
|
|
|
historyList.map((history, index) => (
|
|
|
|
|
<TableRow key={index}>
|
|
|
|
|
<TableCell className="text-center font-mono text-sm">{history.no}</TableCell>
|
2025-08-25 18:30:07 +09:00
|
|
|
<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>
|
2025-08-21 09:41:46 +09:00
|
|
|
<TableCell className="text-center">
|
2025-08-25 18:30:07 +09:00
|
|
|
<Badge variant={getStatusBadgeVariant(history.status || "")}>
|
|
|
|
|
{getStatusText(history.status || "")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-center">
|
2025-08-25 18:30:07 +09:00
|
|
|
<Badge variant={getChangeTypeBadgeVariant(history.historyType || "")}>
|
|
|
|
|
{history.historyType || "-"}
|
2025-08-21 09:41:46 +09:00
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
2025-08-25 18:30:07 +09:00
|
|
|
<TableCell className="text-center text-sm">{history.writerName || "-"}</TableCell>
|
2025-08-21 09:41:46 +09:00
|
|
|
<TableCell className="text-center text-sm">
|
2025-08-25 18:30:07 +09:00
|
|
|
{history.regDateTitle || formatDate(history.regDate || "")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-27 11:20:25 +09:00
|
|
|
{/* 페이지네이션 (항상 표시) */}
|
|
|
|
|
<div className="flex-shrink-0 pt-4">
|
|
|
|
|
<Pagination
|
|
|
|
|
paginationInfo={paginationInfo}
|
|
|
|
|
onPageChange={handlePageChange}
|
|
|
|
|
onPageSizeChange={() => {}} // 페이지 크기는 고정 (원본과 동일)
|
|
|
|
|
/>
|
|
|
|
|
<div className="text-muted-foreground mt-2 text-center text-sm">총 {totalItems}건</div>
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 하단 버튼 */}
|
|
|
|
|
<div className="flex flex-shrink-0 justify-end border-t pt-4">
|
|
|
|
|
<Button variant="outline" onClick={onClose}>
|
|
|
|
|
닫기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|