- Integrated multi-language functionality across various user management components, including user list, roles list, and user authorization pages, enhancing accessibility for diverse users. - Updated UI elements to utilize translation keys, ensuring that all text is dynamically translated based on user preferences. - Improved error handling messages to be localized, providing a better user experience in case of issues. These changes significantly enhance the usability and internationalization of the user management features, making the application more inclusive.
350 lines
9.8 KiB
TypeScript
350 lines
9.8 KiB
TypeScript
import { Key, History, Edit } from "lucide-react";
|
|
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";
|
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
|
import { UserStatusConfirmDialog } from "./UserStatusConfirmDialog";
|
|
import { UserHistoryModal } from "./UserHistoryModal";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
interface UserTableProps {
|
|
users: User[];
|
|
isLoading: boolean;
|
|
paginationInfo: PaginationInfo;
|
|
onStatusToggle: (user: User, newStatus: string) => void;
|
|
onPasswordReset: (userId: string, userName: string) => void;
|
|
onEdit: (user: User) => void;
|
|
t?: (key: string, params?: Record<string, string | number>) => string;
|
|
}
|
|
|
|
/**
|
|
* 사용자 목록 테이블 컴포넌트
|
|
*/
|
|
export function UserTable({
|
|
users,
|
|
isLoading,
|
|
paginationInfo,
|
|
onStatusToggle,
|
|
onPasswordReset,
|
|
onEdit,
|
|
t: tProp,
|
|
}: UserTableProps) {
|
|
// 다국어 함수 (prop이 없으면 한국어 기본값 사용)
|
|
const _t = tProp || ((key: string) => {
|
|
const defaults: Record<string, string> = {
|
|
"table.sabun": "사번",
|
|
"table.company": "회사",
|
|
"table.dept": "부서명",
|
|
"table.position": "직책",
|
|
"table.userId": "사용자 ID",
|
|
"table.userName": "사용자명",
|
|
"table.phone": "전화번호",
|
|
"table.email": "이메일",
|
|
"table.regDate": "등록일",
|
|
"table.status": "상태",
|
|
"table.dept.short": "부서",
|
|
"table.contact": "연락처",
|
|
"table.empty": "등록된 사용자가 없습니다.",
|
|
"table.actions": "작업",
|
|
"action.edit.user": "사용자 정보 수정",
|
|
"action.reset.password": "비밀번호 초기화",
|
|
"action.view.history": "변경이력 조회",
|
|
};
|
|
return defaults[key] || key;
|
|
});
|
|
const { user: currentUser } = useAuth();
|
|
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
|
|
|
|
// 확인 모달 상태 관리
|
|
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 "-";
|
|
return dateString.split(" ")[0];
|
|
};
|
|
|
|
// 상태 토글 핸들러 (확인 모달 표시)
|
|
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,
|
|
userId: user.userId,
|
|
userName: user.userName || user.userId,
|
|
});
|
|
};
|
|
|
|
// 변경이력 모달 닫기
|
|
const handleCloseHistoryModal = () => {
|
|
setHistoryModal({
|
|
isOpen: false,
|
|
userId: "",
|
|
userName: "",
|
|
});
|
|
};
|
|
|
|
// 데스크톱 테이블 컬럼 정의
|
|
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: _t("table.sabun"),
|
|
width: "80px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span className="font-mono">{value || "-"}</span>,
|
|
},
|
|
...(isSuperAdmin
|
|
? [
|
|
{
|
|
key: "companyCode" as keyof User,
|
|
label: _t("table.company"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (value: any, user: User) => (
|
|
<span className="font-medium">{(user as any).companyName || value || "-"}</span>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
key: "deptName",
|
|
label: _t("table.dept"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span className="font-medium">{value || "-"}</span>,
|
|
},
|
|
{
|
|
key: "positionName",
|
|
label: _t("table.position"),
|
|
width: "100px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span className="font-medium">{value || "-"}</span>,
|
|
},
|
|
{
|
|
key: "userId",
|
|
label: _t("table.userId"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span className="font-mono">{value}</span>,
|
|
},
|
|
{
|
|
key: "userName",
|
|
label: _t("table.userName"),
|
|
width: "100px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span className="font-medium">{value}</span>,
|
|
},
|
|
{
|
|
key: "tel",
|
|
label: _t("table.phone"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (_value, row) => <span>{row.tel || row.cellPhone || "-"}</span>,
|
|
},
|
|
{
|
|
key: "email",
|
|
label: _t("table.email"),
|
|
width: "200px",
|
|
hideOnMobile: true,
|
|
className: "max-w-[200px] truncate",
|
|
render: (value, row) => (
|
|
<span title={row.email}>{value || "-"}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "regDate",
|
|
label: _t("table.regDate"),
|
|
width: "100px",
|
|
hideOnMobile: true,
|
|
render: (value) => <span>{formatDate(value || "")}</span>,
|
|
},
|
|
{
|
|
key: "status",
|
|
label: _t("table.status"),
|
|
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} 상태 토글`}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
// 모바일 카드 필드 정의
|
|
const cardFields: RDVCardField<User>[] = [
|
|
{
|
|
label: _t("table.sabun"),
|
|
render: (user) => <span className="font-mono font-medium">{user.sabun || "-"}</span>,
|
|
hideEmpty: true,
|
|
},
|
|
...(isSuperAdmin
|
|
? [
|
|
{
|
|
label: _t("table.company"),
|
|
render: (user: User) => (
|
|
<span className="font-medium">{(user as any).companyName || user.companyCode || ""}</span>
|
|
),
|
|
hideEmpty: true,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
label: _t("table.dept.short"),
|
|
render: (user) => <span className="font-medium">{user.deptName || ""}</span>,
|
|
hideEmpty: true,
|
|
},
|
|
{
|
|
label: _t("table.position"),
|
|
render: (user) => <span className="font-medium">{user.positionName || ""}</span>,
|
|
hideEmpty: true,
|
|
},
|
|
{
|
|
label: _t("table.contact"),
|
|
render: (user) => <span>{user.tel || user.cellPhone || ""}</span>,
|
|
hideEmpty: true,
|
|
},
|
|
{
|
|
label: _t("table.email"),
|
|
render: (user) => <span className="break-all">{user.email || ""}</span>,
|
|
hideEmpty: true,
|
|
},
|
|
{
|
|
label: _t("table.regDate"),
|
|
render: (user) => <span>{formatDate(user.regDate || "")}</span>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<ResponsiveDataView<User>
|
|
data={users}
|
|
columns={columns}
|
|
keyExtractor={(u) => u.userId}
|
|
isLoading={isLoading}
|
|
emptyMessage={_t("table.empty")}
|
|
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={_t("table.actions")}
|
|
actionsWidth="200px"
|
|
renderActions={(user) => (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => onEdit(user)}
|
|
className="h-8 w-8"
|
|
title={_t("action.edit.user")}
|
|
>
|
|
<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={_t("action.reset.password")}
|
|
>
|
|
<Key className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleOpenHistoryModal(user)}
|
|
className="h-8 w-8"
|
|
title={_t("action.view.history")}
|
|
>
|
|
<History className="h-4 w-4" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
{/* 상태 변경 확인 모달 */}
|
|
<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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|