2025-10-27 16:40:59 +09:00
|
|
|
import { Key, History, Edit } from "lucide-react";
|
2025-08-21 09:41:46 +09:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { User } from "@/types/user";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
import { PaginationInfo } from "@/components/common/Pagination";
|
2026-03-09 22:07:11 +09:00
|
|
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
2025-08-21 09:41:46 +09:00
|
|
|
import { UserStatusConfirmDialog } from "./UserStatusConfirmDialog";
|
|
|
|
|
import { UserHistoryModal } from "./UserHistoryModal";
|
|
|
|
|
|
|
|
|
|
interface UserTableProps {
|
|
|
|
|
users: User[];
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
paginationInfo: PaginationInfo;
|
|
|
|
|
onStatusToggle: (user: User, newStatus: string) => void;
|
|
|
|
|
onPasswordReset: (userId: string, userName: string) => void;
|
2025-10-27 16:40:59 +09:00
|
|
|
onEdit: (user: User) => void;
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 목록 테이블 컴포넌트
|
|
|
|
|
*/
|
2025-10-27 16:40:59 +09:00
|
|
|
export function UserTable({
|
|
|
|
|
users,
|
|
|
|
|
isLoading,
|
|
|
|
|
paginationInfo,
|
|
|
|
|
onStatusToggle,
|
|
|
|
|
onPasswordReset,
|
|
|
|
|
onEdit,
|
|
|
|
|
}: UserTableProps) {
|
2025-08-21 09:41:46 +09:00
|
|
|
// 확인 모달 상태 관리
|
|
|
|
|
const [confirmDialog, setConfirmDialog] = useState<{
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
user: User | null;
|
|
|
|
|
newStatus: string;
|
|
|
|
|
}>({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
user: null,
|
|
|
|
|
newStatus: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 히스토리 모달 상태 관리
|
|
|
|
|
const [historyModal, setHistoryModal] = useState<{
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
}>({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
userId: "",
|
|
|
|
|
userName: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// NO 컬럼 계산 함수 (페이지네이션 고려)
|
|
|
|
|
const getRowNumber = (index: number) => {
|
|
|
|
|
return paginationInfo.startItem + index;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 날짜 포맷팅 함수
|
|
|
|
|
const formatDate = (dateString: string) => {
|
|
|
|
|
if (!dateString) return "-";
|
2026-03-09 22:07:11 +09:00
|
|
|
return dateString.split(" ")[0];
|
2025-08-21 09:41:46 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태 토글 핸들러 (확인 모달 표시)
|
|
|
|
|
const handleStatusToggle = (user: User, checked: boolean) => {
|
|
|
|
|
const newStatus = checked ? "active" : "inactive";
|
|
|
|
|
setConfirmDialog({
|
|
|
|
|
isOpen: true,
|
|
|
|
|
user,
|
|
|
|
|
newStatus,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태 변경 확인
|
|
|
|
|
const handleConfirmStatusChange = () => {
|
|
|
|
|
if (confirmDialog.user) {
|
|
|
|
|
onStatusToggle(confirmDialog.user, confirmDialog.newStatus);
|
|
|
|
|
}
|
|
|
|
|
setConfirmDialog({ isOpen: false, user: null, newStatus: "" });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태 변경 취소
|
|
|
|
|
const handleCancelStatusChange = () => {
|
|
|
|
|
setConfirmDialog({ isOpen: false, user: null, newStatus: "" });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 변경이력 모달 열기
|
|
|
|
|
const handleOpenHistoryModal = (user: User) => {
|
|
|
|
|
setHistoryModal({
|
|
|
|
|
isOpen: true,
|
2025-08-25 18:30:07 +09:00
|
|
|
userId: user.userId,
|
|
|
|
|
userName: user.userName || user.userId,
|
2025-08-21 09:41:46 +09:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 변경이력 모달 닫기
|
|
|
|
|
const handleCloseHistoryModal = () => {
|
|
|
|
|
setHistoryModal({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
userId: "",
|
|
|
|
|
userName: "",
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-09 22:07:11 +09:00
|
|
|
// 데스크톱 테이블 컬럼 정의
|
|
|
|
|
const columns: RDVColumn<User>[] = [
|
|
|
|
|
{
|
|
|
|
|
key: "no",
|
|
|
|
|
label: "No",
|
|
|
|
|
width: "60px",
|
|
|
|
|
render: (_value, _row, index) => (
|
|
|
|
|
<span className="font-mono font-medium">{getRowNumber(index)}</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "sabun",
|
|
|
|
|
label: "사번",
|
|
|
|
|
width: "80px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-mono">{value || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "companyCode",
|
|
|
|
|
label: "회사",
|
|
|
|
|
width: "120px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-medium">{value || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "deptName",
|
|
|
|
|
label: "부서명",
|
|
|
|
|
width: "120px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-medium">{value || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "positionName",
|
|
|
|
|
label: "직책",
|
|
|
|
|
width: "100px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-medium">{value || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "userId",
|
|
|
|
|
label: "사용자 ID",
|
|
|
|
|
width: "120px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-mono">{value}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "userName",
|
|
|
|
|
label: "사용자명",
|
|
|
|
|
width: "100px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span className="font-medium">{value}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "tel",
|
|
|
|
|
label: "전화번호",
|
|
|
|
|
width: "120px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (_value, row) => <span>{row.tel || row.cellPhone || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "email",
|
|
|
|
|
label: "이메일",
|
|
|
|
|
width: "200px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
className: "max-w-[200px] truncate",
|
|
|
|
|
render: (value, row) => (
|
|
|
|
|
<span title={row.email}>{value || "-"}</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "regDate",
|
|
|
|
|
label: "등록일",
|
|
|
|
|
width: "100px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span>{formatDate(value || "")}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "status",
|
|
|
|
|
label: "상태",
|
|
|
|
|
width: "120px",
|
|
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (_value, row) => (
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<Switch
|
|
|
|
|
checked={row.status === "active"}
|
|
|
|
|
onCheckedChange={(checked) => handleStatusToggle(row, checked)}
|
|
|
|
|
aria-label={`${row.userName} 상태 토글`}
|
|
|
|
|
/>
|
2025-10-22 14:52:13 +09:00
|
|
|
</div>
|
2026-03-09 22:07:11 +09:00
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-10-22 14:52:13 +09:00
|
|
|
|
2026-03-09 22:07:11 +09:00
|
|
|
// 모바일 카드 필드 정의
|
|
|
|
|
const cardFields: RDVCardField<User>[] = [
|
|
|
|
|
{
|
|
|
|
|
label: "사번",
|
|
|
|
|
render: (user) => <span className="font-mono font-medium">{user.sabun || "-"}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "회사",
|
|
|
|
|
render: (user) => <span className="font-medium">{user.companyCode || ""}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "부서",
|
|
|
|
|
render: (user) => <span className="font-medium">{user.deptName || ""}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "직책",
|
|
|
|
|
render: (user) => <span className="font-medium">{user.positionName || ""}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "연락처",
|
|
|
|
|
render: (user) => <span>{user.tel || user.cellPhone || ""}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "이메일",
|
|
|
|
|
render: (user) => <span className="break-all">{user.email || ""}</span>,
|
|
|
|
|
hideEmpty: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "등록일",
|
|
|
|
|
render: (user) => <span>{formatDate(user.regDate || "")}</span>,
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-10-22 14:52:13 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-03-09 22:07:11 +09:00
|
|
|
<ResponsiveDataView<User>
|
|
|
|
|
data={users}
|
|
|
|
|
columns={columns}
|
|
|
|
|
keyExtractor={(u) => u.userId}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
emptyMessage="등록된 사용자가 없습니다."
|
|
|
|
|
skeletonCount={10}
|
|
|
|
|
cardTitle={(u) => u.userName || ""}
|
|
|
|
|
cardSubtitle={(u) => <span className="font-mono">{u.userId}</span>}
|
|
|
|
|
cardHeaderRight={(u) => (
|
|
|
|
|
<Switch
|
|
|
|
|
checked={u.status === "active"}
|
|
|
|
|
onCheckedChange={(checked) => handleStatusToggle(u, checked)}
|
|
|
|
|
aria-label={`${u.userName} 상태 토글`}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
cardFields={cardFields}
|
|
|
|
|
actionsLabel="작업"
|
|
|
|
|
actionsWidth="200px"
|
|
|
|
|
renderActions={(user) => (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => onEdit(user)}
|
|
|
|
|
className="h-8 w-8"
|
|
|
|
|
title="사용자 정보 수정"
|
|
|
|
|
>
|
|
|
|
|
<Edit className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => onPasswordReset(user.userId, user.userName || user.userId)}
|
|
|
|
|
className="h-8 w-8"
|
|
|
|
|
title="비밀번호 초기화"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => handleOpenHistoryModal(user)}
|
|
|
|
|
className="h-8 w-8"
|
|
|
|
|
title="변경이력 조회"
|
|
|
|
|
>
|
|
|
|
|
<History className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
{/* 상태 변경 확인 모달 */}
|
|
|
|
|
<UserStatusConfirmDialog
|
|
|
|
|
user={confirmDialog.user}
|
|
|
|
|
newStatus={confirmDialog.newStatus}
|
|
|
|
|
isOpen={confirmDialog.isOpen}
|
|
|
|
|
onConfirm={handleConfirmStatusChange}
|
|
|
|
|
onCancel={handleCancelStatusChange}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 사용자 변경이력 모달 */}
|
|
|
|
|
<UserHistoryModal
|
|
|
|
|
isOpen={historyModal.isOpen}
|
|
|
|
|
onClose={handleCloseHistoryModal}
|
|
|
|
|
userId={historyModal.userId}
|
|
|
|
|
userName={historyModal.userName}
|
|
|
|
|
/>
|
2025-10-22 14:52:13 +09:00
|
|
|
</>
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
}
|