diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index 6142e93c..7e30b90c 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -7,7 +7,9 @@ import { ChevronDown, ChevronLeft, ChevronRight, + Download, FileDown, + FileSpreadsheet, LayoutDashboard, Plus, RefreshCw, @@ -15,6 +17,7 @@ import { Settings2, ShieldCheck, Trash2, + Upload, } from "lucide-react"; import * as React from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -90,7 +93,10 @@ import { } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { isSuperAdminRole } from "../../lib/roles"; -import { UserBulkUploadModal } from "./components/UserBulkUploadModal"; +import { + UserBulkUploadModal, + downloadUserTemplate, +} from "./components/UserBulkUploadModal"; import { normalizeUserStatusValue, type UserStatusValue, @@ -485,27 +491,53 @@ function UserListPage() { {t("ui.common.refresh", "새로고침")} - handleExport(false)} - className="gap-2" - disabled={exportMutation.isPending} - data-testid="user-export-without-ids-btn" - > - - {t("ui.common.export_without_ids", "UUID 제외 내보내기")} - - handleExport(true)} - className="gap-2" - disabled={exportMutation.isPending} - data-testid="user-export-with-ids-btn" - > - - {t("ui.common.export_with_ids", "UUID 포함")} - - query.refetch()} /> + + + + + {t("ui.admin.users.data_mgmt", "데이터 관리")} + + + + + + + {t("ui.admin.users.csv_template", "템플릿 다운로드")} + + + query.refetch()} + /> + + handleExport(false)} + disabled={exportMutation.isPending} + data-testid="user-export-menu-item" + className="cursor-pointer" + > + + {t("ui.common.export_without_ids", "UUID 제외 내보내기")} + + handleExport(true)} + disabled={exportMutation.isPending} + data-testid="user-export-with-ids-menu-item" + className="cursor-pointer" + > + + {t("ui.common.export_with_ids", "UUID 포함 내보내기")} + + + diff --git a/adminfront/src/features/users/components/UserBulkUploadModal.tsx b/adminfront/src/features/users/components/UserBulkUploadModal.tsx index 33ddb68f..6a310b0b 100644 --- a/adminfront/src/features/users/components/UserBulkUploadModal.tsx +++ b/adminfront/src/features/users/components/UserBulkUploadModal.tsx @@ -18,6 +18,7 @@ import { DialogTitle, DialogTrigger, } from "../../../components/ui/dialog"; +import { DropdownMenuItem } from "../../../components/ui/dropdown-menu"; import { ScrollArea } from "../../../components/ui/scroll-area"; import { type BulkUserItem, @@ -121,6 +122,22 @@ function hanmacEmailStatusClass(preview?: HanmacImportEmailPreview) { return "text-muted-foreground"; } +export const downloadUserTemplate = () => { + const headers = + "email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; + const example = + "user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,팀장,프론트엔드,EMP001,second-tenant,센터,책임,,Architecture,EMP002"; + const blob = new Blob([`${headers}\n${example}`], { + type: "text/csv;charset=utf-8;", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "user_bulk_template.csv"; + a.click(); + URL.revokeObjectURL(url); +}; + export function UserBulkUploadModal({ onSuccess, variant = "button", @@ -334,16 +351,14 @@ export function UserBulkUploadModal({ const triggerNode = variant === "dropdown" ? ( - setOpen(true)} + className="cursor-pointer" {...triggerProps} > {t("ui.admin.users.list.bulk_import", "일괄 등록 (CSV)")} - + ) : (