forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle2, Download, FileText, Loader2, Upload } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Download,
|
||||
FileText,
|
||||
Loader2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
@@ -12,7 +19,11 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import { ScrollArea } from "../../../components/ui/scroll-area";
|
||||
import { bulkCreateUsers, type BulkUserItem, type BulkUserResult } from "../../../lib/adminApi";
|
||||
import {
|
||||
type BulkUserItem,
|
||||
type BulkUserResult,
|
||||
bulkCreateUsers,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
import { parseUserCSV } from "../utils/csvParser";
|
||||
|
||||
@@ -64,9 +75,15 @@ export function UserBulkUploadModal({ onSuccess }: UserBulkUploadModalProps) {
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const headers = "email,name,phone,role,companyCode,department,employee_id";
|
||||
const example = "user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,EMP001";
|
||||
const blob = new Blob([`${headers}
|
||||
${example}`], { type: "text/csv" });
|
||||
const example =
|
||||
"user1@example.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,EMP001";
|
||||
const blob = new Blob(
|
||||
[
|
||||
`${headers}
|
||||
${example}`,
|
||||
],
|
||||
{ type: "text/csv" },
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
@@ -82,11 +99,17 @@ ${example}`], { type: "text/csv" });
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const successCount = results?.filter(r => r.success).length ?? 0;
|
||||
const successCount = results?.filter((r) => r.success).length ?? 0;
|
||||
const failCount = results ? results.length - successCount : 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(val) => { setOpen(val); if (!val) reset(); }}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
if (!val) reset();
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Upload size={16} />
|
||||
@@ -95,16 +118,26 @@ ${example}`], { type: "text/csv" });
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("ui.admin.users.bulk.title", "사용자 일괄 등록")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t("ui.admin.users.bulk.title", "사용자 일괄 등록")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("msg.admin.users.bulk.description", "CSV 파일을 업로드하여 여러 사용자를 한 번에 등록합니다.")}
|
||||
{t(
|
||||
"msg.admin.users.bulk.description",
|
||||
"CSV 파일을 업로드하여 여러 사용자를 한 번에 등록합니다.",
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!results ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="ghost" size="sm" onClick={downloadTemplate} className="gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={downloadTemplate}
|
||||
className="gap-2"
|
||||
>
|
||||
<Download size={14} />
|
||||
{t("ui.admin.users.bulk.download_template", "템플릿 다운로드")}
|
||||
</Button>
|
||||
@@ -115,8 +148,13 @@ ${example}`], { type: "text/csv" });
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button onClick={() => fileInputRef.current?.click()} variant="secondary">
|
||||
{file ? t("ui.common.change_file", "파일 변경") : t("ui.common.select_file", "파일 선택")}
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
variant="secondary"
|
||||
>
|
||||
{file
|
||||
? t("ui.common.change_file", "파일 변경")
|
||||
: t("ui.common.select_file", "파일 선택")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +163,9 @@ ${example}`], { type: "text/csv" });
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<FileText className="text-primary" />
|
||||
<span className="font-medium">{file.name}</span>
|
||||
<span className="text-xs text-muted-foreground">({(file.size / 1024).toFixed(1)} KB)</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({(file.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</div>
|
||||
{parsing ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
@@ -134,7 +174,11 @@ ${example}`], { type: "text/csv" });
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("msg.admin.users.bulk.parsed_count", "{{count}}명의 사용자가 감지되었습니다.", { count: previewData.length })}
|
||||
{t(
|
||||
"msg.admin.users.bulk.parsed_count",
|
||||
"{{count}}명의 사용자가 감지되었습니다.",
|
||||
{ count: previewData.length },
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -160,7 +204,10 @@ ${example}`], { type: "text/csv" });
|
||||
))}
|
||||
{previewData.length > 10 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="p-2 text-center text-muted-foreground italic">
|
||||
<td
|
||||
colSpan={3}
|
||||
className="p-2 text-center text-muted-foreground italic"
|
||||
>
|
||||
... and {previewData.length - 10} more users
|
||||
</td>
|
||||
</tr>
|
||||
@@ -174,28 +221,49 @@ ${example}`], { type: "text/csv" });
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center gap-4 p-4 rounded-lg bg-muted/30 border">
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{successCount}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">{t("ui.common.success", "성공")}</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{successCount}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">
|
||||
{t("ui.common.success", "성공")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-px h-10 bg-border" />
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-2xl font-bold text-destructive">{failCount}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">{t("ui.common.fail", "실패")}</div>
|
||||
<div className="text-2xl font-bold text-destructive">
|
||||
{failCount}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">
|
||||
{t("ui.common.fail", "실패")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[250px] rounded-md border">
|
||||
<div className="p-2 space-y-2">
|
||||
{results.map((r, i) => (
|
||||
<div key={i} className="flex items-start gap-3 p-2 rounded border bg-card text-sm">
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-2 rounded border bg-card text-sm"
|
||||
>
|
||||
{r.success ? (
|
||||
<CheckCircle2 size={16} className="text-green-500 mt-0.5" />
|
||||
<CheckCircle2
|
||||
size={16}
|
||||
className="text-green-500 mt-0.5"
|
||||
/>
|
||||
) : (
|
||||
<AlertCircle size={16} className="text-destructive mt-0.5" />
|
||||
<AlertCircle
|
||||
size={16}
|
||||
className="text-destructive mt-0.5"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{r.email}</div>
|
||||
{!r.success && <div className="text-xs text-destructive">{r.message}</div>}
|
||||
{!r.success && (
|
||||
<div className="text-xs text-destructive">
|
||||
{r.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -206,12 +274,14 @@ ${example}`], { type: "text/csv" });
|
||||
|
||||
<DialogFooter>
|
||||
{!results ? (
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={previewData.length === 0 || mutation.isPending}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{mutation.isPending && <Loader2 size={16} className="mr-2 animate-spin" />}
|
||||
{mutation.isPending && (
|
||||
<Loader2 size={16} className="mr-2 animate-spin" />
|
||||
)}
|
||||
{t("ui.admin.users.bulk.start_upload", "등록 시작")}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user