forked from baron/baron-sso
조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인
This commit is contained in:
@@ -72,7 +72,9 @@ import { isSeedTenant } from "../utils/protectedTenants";
|
||||
import {
|
||||
type TenantImportPreviewRow,
|
||||
type TenantImportResolution,
|
||||
buildTenantImportParentOptionGroups,
|
||||
buildTenantImportPreview,
|
||||
inferTenantImportRootParentSlug,
|
||||
parseTenantCSV,
|
||||
serializeTenantImportCSV,
|
||||
} from "../utils/tenantCsvImport";
|
||||
@@ -97,6 +99,119 @@ const getTenantIcon = (type?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const noImportParentRef = "__none__";
|
||||
|
||||
function tenantParentRef(tenantId: string) {
|
||||
return `tenant:${tenantId}`;
|
||||
}
|
||||
|
||||
function previewParentRef(rowNumber: number) {
|
||||
return `row:${rowNumber}`;
|
||||
}
|
||||
|
||||
function slugParentRef(slug: string) {
|
||||
return `slug:${slug}`;
|
||||
}
|
||||
|
||||
function getImportParentGroupLabel(type: string) {
|
||||
switch (type) {
|
||||
case "COMPANY_GROUP":
|
||||
return t(
|
||||
"ui.admin.tenants.import_preview.parent_company_groups",
|
||||
"기존 Company Group",
|
||||
);
|
||||
case "COMPANY":
|
||||
return t(
|
||||
"ui.admin.tenants.import_preview.parent_companies",
|
||||
"기존 Company",
|
||||
);
|
||||
case "ORGANIZATION":
|
||||
return t(
|
||||
"ui.admin.tenants.import_preview.parent_organizations",
|
||||
"기존 Organization",
|
||||
);
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultImportParentRef(
|
||||
preview: TenantImportPreviewRow,
|
||||
previewRows: TenantImportPreviewRow[],
|
||||
tenants: TenantSummary[],
|
||||
) {
|
||||
if (preview.row.parentTenantId) {
|
||||
return tenantParentRef(preview.row.parentTenantId);
|
||||
}
|
||||
if (!preview.row.parentTenantSlug) {
|
||||
return noImportParentRef;
|
||||
}
|
||||
|
||||
const normalizedSlug = preview.row.parentTenantSlug.toLowerCase();
|
||||
const existingTenant = tenants.find(
|
||||
(tenant) => tenant.slug.toLowerCase() === normalizedSlug,
|
||||
);
|
||||
if (existingTenant) {
|
||||
return tenantParentRef(existingTenant.id);
|
||||
}
|
||||
|
||||
const parentPreview = previewRows.find(
|
||||
(candidate) =>
|
||||
candidate.row.rowNumber !== preview.row.rowNumber &&
|
||||
candidate.row.slug.toLowerCase() === normalizedSlug,
|
||||
);
|
||||
if (parentPreview) {
|
||||
return previewParentRef(parentPreview.row.rowNumber);
|
||||
}
|
||||
|
||||
return slugParentRef(preview.row.parentTenantSlug);
|
||||
}
|
||||
|
||||
function selectedImportSlug(
|
||||
preview: TenantImportPreviewRow,
|
||||
selectedCreateSlugs: Record<number, string>,
|
||||
) {
|
||||
return (
|
||||
selectedCreateSlugs[preview.row.rowNumber] || preview.defaultCreateSlug
|
||||
);
|
||||
}
|
||||
|
||||
function resolveImportParentSelection(
|
||||
parentRef: string,
|
||||
previewRows: TenantImportPreviewRow[],
|
||||
selectedMatches: Record<number, string>,
|
||||
selectedCreateSlugs: Record<number, string>,
|
||||
) {
|
||||
if (!parentRef || parentRef === noImportParentRef) {
|
||||
return { parentTenantId: "", parentTenantSlug: "" };
|
||||
}
|
||||
if (parentRef.startsWith("tenant:")) {
|
||||
return {
|
||||
parentTenantId: parentRef.slice("tenant:".length),
|
||||
parentTenantSlug: "",
|
||||
};
|
||||
}
|
||||
if (parentRef.startsWith("slug:")) {
|
||||
return { parentTenantSlug: parentRef.slice("slug:".length) };
|
||||
}
|
||||
if (parentRef.startsWith("row:")) {
|
||||
const rowNumber = Number(parentRef.slice("row:".length));
|
||||
const selected = selectedMatches[rowNumber] ?? "__create__";
|
||||
if (selected && selected !== "__create__") {
|
||||
return { parentTenantId: selected, parentTenantSlug: "" };
|
||||
}
|
||||
const parentPreview = previewRows.find(
|
||||
(preview) => preview.row.rowNumber === rowNumber,
|
||||
);
|
||||
return {
|
||||
parentTenantSlug: parentPreview
|
||||
? selectedImportSlug(parentPreview, selectedCreateSlugs)
|
||||
: "",
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function TenantListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = React.useState<"list" | "hierarchy">("list");
|
||||
@@ -114,6 +229,9 @@ function TenantListPage() {
|
||||
const [selectedCreateSlugs, setSelectedCreateSlugs] = React.useState<
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [selectedParentRefs, setSelectedParentRefs] = React.useState<
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
@@ -189,6 +307,7 @@ function TenantListPage() {
|
||||
setPreviewOpen(false);
|
||||
setPreviewRows([]);
|
||||
setSelectedMatches({});
|
||||
setSelectedParentRefs({});
|
||||
query.refetch();
|
||||
},
|
||||
onError: (error: AxiosError<{ error?: string }>) => {
|
||||
@@ -234,6 +353,8 @@ function TenantListPage() {
|
||||
: null;
|
||||
|
||||
const allTenants = query.data?.items ?? [];
|
||||
const importParentOptionGroups =
|
||||
buildTenantImportParentOptionGroups(allTenants);
|
||||
const tenants = React.useMemo(() => {
|
||||
// 1. Calculate recursive counts
|
||||
// buildTenantFullTree returns subTree which represents roots, but it also mutates the mapped nodes internally.
|
||||
@@ -373,7 +494,9 @@ function TenantListPage() {
|
||||
if (!file) return;
|
||||
setImportMessage("");
|
||||
const text = await file.text();
|
||||
const rows = parseTenantCSV(text);
|
||||
const rows = parseTenantCSV(text, {
|
||||
rootParentSlug: inferTenantImportRootParentSlug(file.name, allTenants),
|
||||
});
|
||||
if (rows.length === 0) {
|
||||
setImportMessage(
|
||||
t("msg.admin.tenants.import_empty", "가져올 테넌트 행이 없습니다."),
|
||||
@@ -395,6 +518,14 @@ function TenantListPage() {
|
||||
preview.map((row) => [row.row.rowNumber, row.defaultCreateSlug]),
|
||||
),
|
||||
);
|
||||
setSelectedParentRefs(
|
||||
Object.fromEntries(
|
||||
preview.map((row) => [
|
||||
row.row.rowNumber,
|
||||
resolveDefaultImportParentRef(row, preview, allTenants),
|
||||
]),
|
||||
),
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
@@ -406,7 +537,21 @@ function TenantListPage() {
|
||||
if (selected && selected !== "__create__") {
|
||||
return [
|
||||
preview.row.rowNumber,
|
||||
{ mode: "existing", tenantId: selected },
|
||||
{
|
||||
mode: "existing",
|
||||
tenantId: selected,
|
||||
...resolveImportParentSelection(
|
||||
selectedParentRefs[preview.row.rowNumber] ??
|
||||
resolveDefaultImportParentRef(
|
||||
preview,
|
||||
previewRows,
|
||||
allTenants,
|
||||
),
|
||||
previewRows,
|
||||
selectedMatches,
|
||||
selectedCreateSlugs,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -417,6 +562,17 @@ function TenantListPage() {
|
||||
slug:
|
||||
selectedCreateSlugs[preview.row.rowNumber] ||
|
||||
preview.defaultCreateSlug,
|
||||
...resolveImportParentSelection(
|
||||
selectedParentRefs[preview.row.rowNumber] ??
|
||||
resolveDefaultImportParentRef(
|
||||
preview,
|
||||
previewRows,
|
||||
allTenants,
|
||||
),
|
||||
previewRows,
|
||||
selectedMatches,
|
||||
selectedCreateSlugs,
|
||||
),
|
||||
},
|
||||
];
|
||||
}),
|
||||
@@ -860,6 +1016,9 @@ function TenantListPage() {
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.table.slug", "SLUG")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.import_preview.parent", "상위")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.import_preview.match", "매칭")}
|
||||
</TableHead>
|
||||
@@ -909,6 +1068,94 @@ function TenantListPage() {
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<select
|
||||
className="h-9 w-full min-w-[220px] rounded-md border border-input bg-background px-3 text-sm"
|
||||
value={
|
||||
selectedParentRefs[preview.row.rowNumber] ??
|
||||
resolveDefaultImportParentRef(
|
||||
preview,
|
||||
previewRows,
|
||||
allTenants,
|
||||
)
|
||||
}
|
||||
data-testid={`tenant-import-parent-select-${preview.row.rowNumber}`}
|
||||
onChange={(event) =>
|
||||
setSelectedParentRefs((prev) => ({
|
||||
...prev,
|
||||
[preview.row.rowNumber]: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value={noImportParentRef}>
|
||||
{t("ui.common.none", "없음")}
|
||||
</option>
|
||||
{importParentOptionGroups.map((group) => (
|
||||
<optgroup
|
||||
key={group.type}
|
||||
label={getImportParentGroupLabel(group.type)}
|
||||
>
|
||||
{group.tenants.map((tenant) => (
|
||||
<option
|
||||
key={tenant.id}
|
||||
value={tenantParentRef(tenant.id)}
|
||||
>
|
||||
{tenant.name} ({tenant.slug}) - {tenant.type}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
<optgroup
|
||||
label={t(
|
||||
"ui.admin.tenants.import_preview.csv_parents",
|
||||
"가져오기 CSV",
|
||||
)}
|
||||
>
|
||||
{previewRows
|
||||
.filter(
|
||||
(candidate) =>
|
||||
candidate.row.rowNumber !==
|
||||
preview.row.rowNumber,
|
||||
)
|
||||
.map((candidate) => (
|
||||
<option
|
||||
key={candidate.row.rowNumber}
|
||||
value={previewParentRef(
|
||||
candidate.row.rowNumber,
|
||||
)}
|
||||
>
|
||||
{candidate.row.name} (
|
||||
{selectedImportSlug(
|
||||
candidate,
|
||||
selectedCreateSlugs,
|
||||
)}
|
||||
)
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{(
|
||||
selectedParentRefs[preview.row.rowNumber] ??
|
||||
resolveDefaultImportParentRef(
|
||||
preview,
|
||||
previewRows,
|
||||
allTenants,
|
||||
)
|
||||
).startsWith("slug:") && (
|
||||
<option
|
||||
value={
|
||||
selectedParentRefs[preview.row.rowNumber] ??
|
||||
resolveDefaultImportParentRef(
|
||||
preview,
|
||||
previewRows,
|
||||
allTenants,
|
||||
)
|
||||
}
|
||||
>
|
||||
{preview.row.parentTenantSlug}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user