2025-10-27 16:40:59 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Shield, ShieldCheck, User as UserIcon, Users, Building2 } from "lucide-react";
|
2026-03-09 22:07:11 +09:00
|
|
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
2025-10-27 16:40:59 +09:00
|
|
|
|
2026-04-01 15:57:12 +09:00
|
|
|
// 컴포넌트 내부 기본 텍스트 (t prop이 없을 때 사용)
|
|
|
|
|
const USER_AUTH_TABLE_DEFAULTS: Record<string, string> = {
|
|
|
|
|
"role.super_admin": "최고 관리자",
|
|
|
|
|
"role.company_admin": "회사 관리자",
|
|
|
|
|
"role.user": "일반 사용자",
|
|
|
|
|
"role.guest": "게스트",
|
|
|
|
|
"role.partner": "협력업체",
|
|
|
|
|
"role.unassigned": "미지정",
|
|
|
|
|
"table.userId": "사용자 ID",
|
|
|
|
|
"table.userName": "사용자명",
|
|
|
|
|
"table.company": "회사",
|
|
|
|
|
"table.dept": "부서",
|
|
|
|
|
"table.currentAuth": "현재 권한",
|
|
|
|
|
"table.empty": "등록된 사용자가 없습니다.",
|
|
|
|
|
"table.actions": "액션",
|
|
|
|
|
"action.change.auth": "권한 변경",
|
|
|
|
|
"pagination.prev": "이전",
|
|
|
|
|
"pagination.next": "다음",
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-27 16:40:59 +09:00
|
|
|
interface UserAuthTableProps {
|
|
|
|
|
users: any[];
|
|
|
|
|
isLoading: boolean;
|
2026-04-01 15:49:49 +09:00
|
|
|
isSuperAdmin?: boolean;
|
2025-10-27 16:40:59 +09:00
|
|
|
paginationInfo: {
|
|
|
|
|
currentPage: number;
|
|
|
|
|
pageSize: number;
|
|
|
|
|
totalItems: number;
|
|
|
|
|
totalPages: number;
|
|
|
|
|
};
|
|
|
|
|
onEditAuth: (user: any) => void;
|
|
|
|
|
onPageChange: (page: number) => void;
|
2026-04-01 15:57:12 +09:00
|
|
|
t?: (key: string, params?: Record<string, string | number>) => string;
|
2025-10-27 16:40:59 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자 권한 테이블 컴포넌트
|
|
|
|
|
*
|
|
|
|
|
* 사용자 목록과 권한 정보를 표시하고 권한 변경 기능 제공
|
|
|
|
|
*/
|
2026-04-01 15:57:12 +09:00
|
|
|
export function UserAuthTable({ users, isLoading, isSuperAdmin, paginationInfo, onEditAuth, onPageChange, t }: UserAuthTableProps) {
|
|
|
|
|
// 다국어 래퍼 (t prop이 없으면 기본 텍스트 사용)
|
|
|
|
|
const _t = t || ((key: string) => USER_AUTH_TABLE_DEFAULTS[key] || key);
|
|
|
|
|
|
2025-10-27 16:40:59 +09:00
|
|
|
// 권한 레벨 표시
|
|
|
|
|
const getUserTypeInfo = (userType: string) => {
|
|
|
|
|
switch (userType) {
|
|
|
|
|
case "SUPER_ADMIN":
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("role.super_admin"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <ShieldCheck className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-primary/20 text-primary border-primary/30",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
case "COMPANY_ADMIN":
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("role.company_admin"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <Building2 className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-primary/20 text-primary border-primary/30",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
case "USER":
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("role.user"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <UserIcon className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-muted/50 text-muted-foreground border-border",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
case "GUEST":
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("role.guest"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <Users className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-success/20 text-success border-success/30",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
case "PARTNER":
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("role.partner"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <Shield className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-warning/20 text-warning border-warning/30",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
default:
|
|
|
|
|
return {
|
2026-04-01 15:57:12 +09:00
|
|
|
label: userType || _t("role.unassigned"),
|
2025-10-27 16:40:59 +09:00
|
|
|
icon: <UserIcon className="h-3 w-3" />,
|
2025-10-30 15:39:39 +09:00
|
|
|
className: "bg-muted/50 text-muted-foreground border-border",
|
2025-10-27 16:40:59 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 행 번호 계산
|
|
|
|
|
const getRowNumber = (index: number) => {
|
|
|
|
|
return (paginationInfo.currentPage - 1) * paginationInfo.pageSize + index + 1;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-09 22:07:11 +09:00
|
|
|
// 데스크톱 테이블 컬럼 정의
|
|
|
|
|
const columns: RDVColumn<any>[] = [
|
|
|
|
|
{
|
|
|
|
|
key: "no",
|
|
|
|
|
label: "No",
|
|
|
|
|
width: "80px",
|
|
|
|
|
className: "text-center",
|
|
|
|
|
render: (_value, _row, index) => <span>{getRowNumber(index)}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "userId",
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.userId"),
|
2026-03-09 22:07:11 +09:00
|
|
|
render: (value) => <span className="font-mono">{value}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "userName",
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.userName"),
|
2026-03-09 22:07:11 +09:00
|
|
|
},
|
2026-04-01 15:49:49 +09:00
|
|
|
...(isSuperAdmin
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
key: "companyName",
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.company"),
|
2026-04-01 15:49:49 +09:00
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (_value: any, row: any) => <span>{row.companyName || row.companyCode}</span>,
|
|
|
|
|
} as RDVColumn<any>,
|
|
|
|
|
]
|
|
|
|
|
: []),
|
2026-03-09 22:07:11 +09:00
|
|
|
{
|
|
|
|
|
key: "deptName",
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.dept"),
|
2026-03-09 22:07:11 +09:00
|
|
|
hideOnMobile: true,
|
|
|
|
|
render: (value) => <span>{value || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "userType",
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.currentAuth"),
|
2026-03-09 22:07:11 +09:00
|
|
|
className: "text-center",
|
|
|
|
|
render: (_value, row) => {
|
|
|
|
|
const typeInfo = getUserTypeInfo(row.userType);
|
|
|
|
|
return (
|
|
|
|
|
<Badge variant="outline" className={`gap-1 ${typeInfo.className}`}>
|
|
|
|
|
{typeInfo.icon}
|
|
|
|
|
{typeInfo.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-10-27 16:40:59 +09:00
|
|
|
|
2026-03-09 22:07:11 +09:00
|
|
|
// 모바일 카드 필드 정의
|
|
|
|
|
const cardFields: RDVCardField<any>[] = [
|
2026-04-01 15:49:49 +09:00
|
|
|
...(isSuperAdmin
|
|
|
|
|
? [
|
|
|
|
|
{
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.company"),
|
2026-04-01 15:49:49 +09:00
|
|
|
render: (user: any) => <span>{user.companyName || user.companyCode}</span>,
|
|
|
|
|
} as RDVCardField<any>,
|
|
|
|
|
]
|
|
|
|
|
: []),
|
2026-03-09 22:07:11 +09:00
|
|
|
{
|
2026-04-01 15:57:12 +09:00
|
|
|
label: _t("table.dept"),
|
2026-03-09 22:07:11 +09:00
|
|
|
render: (user) => <span>{user.deptName || "-"}</span>,
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-10-27 16:40:59 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
2026-03-09 22:07:11 +09:00
|
|
|
<ResponsiveDataView<any>
|
|
|
|
|
data={users}
|
|
|
|
|
columns={columns}
|
|
|
|
|
keyExtractor={(u) => u.userId}
|
|
|
|
|
isLoading={isLoading}
|
2026-04-01 15:57:12 +09:00
|
|
|
emptyMessage={_t("table.empty")}
|
2026-03-09 22:07:11 +09:00
|
|
|
skeletonCount={10}
|
|
|
|
|
cardTitle={(u) => u.userName}
|
|
|
|
|
cardSubtitle={(u) => <span className="font-mono">{u.userId}</span>}
|
|
|
|
|
cardHeaderRight={(u) => {
|
|
|
|
|
const typeInfo = getUserTypeInfo(u.userType);
|
2025-10-27 16:40:59 +09:00
|
|
|
return (
|
2026-03-09 22:07:11 +09:00
|
|
|
<Badge variant="outline" className={`gap-1 ${typeInfo.className}`}>
|
|
|
|
|
{typeInfo.icon}
|
|
|
|
|
{typeInfo.label}
|
|
|
|
|
</Badge>
|
2025-10-27 16:40:59 +09:00
|
|
|
);
|
2026-03-09 22:07:11 +09:00
|
|
|
}}
|
|
|
|
|
cardFields={cardFields}
|
2026-04-01 15:57:12 +09:00
|
|
|
actionsLabel={_t("table.actions")}
|
2026-03-09 22:07:11 +09:00
|
|
|
actionsWidth="120px"
|
|
|
|
|
renderActions={(user) => (
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => onEditAuth(user)} className="h-8 gap-1 text-sm">
|
|
|
|
|
<Shield className="h-3 w-3" />
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("action.change.auth")}
|
2026-03-09 22:07:11 +09:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-10-27 16:40:59 +09:00
|
|
|
|
|
|
|
|
{/* 페이지네이션 */}
|
|
|
|
|
{paginationInfo.totalPages > 1 && (
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => onPageChange(paginationInfo.currentPage - 1)}
|
|
|
|
|
disabled={paginationInfo.currentPage === 1}
|
|
|
|
|
>
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("pagination.prev")}
|
2025-10-27 16:40:59 +09:00
|
|
|
</Button>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{paginationInfo.currentPage} / {paginationInfo.totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => onPageChange(paginationInfo.currentPage + 1)}
|
|
|
|
|
disabled={paginationInfo.currentPage === paginationInfo.totalPages}
|
|
|
|
|
>
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("pagination.next")}
|
2025-10-27 16:40:59 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|