1
0
forked from baron/baron-sso

조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인

This commit is contained in:
2026-05-11 20:13:54 +09:00
parent d3853fac2a
commit 3063450ee0
59 changed files with 5086 additions and 549 deletions

View File

@@ -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