forked from baron/baron-sso
조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인
This commit is contained in:
@@ -12,6 +12,12 @@ export type TenantCSVRow = {
|
||||
emailDomain: string;
|
||||
};
|
||||
|
||||
export type TenantCSVParseOptions = {
|
||||
rootParentSlug?: string;
|
||||
};
|
||||
|
||||
type TenantCSVSourceKey = keyof TenantCSVRow | "mailingList" | "parentOrg";
|
||||
|
||||
export type TenantImportCandidate = {
|
||||
tenantId: string;
|
||||
name: string;
|
||||
@@ -28,6 +34,16 @@ export type TenantImportPreviewRow = {
|
||||
conflicts: TenantImportConflict[];
|
||||
};
|
||||
|
||||
export type TenantImportParentOptionGroupType =
|
||||
| "COMPANY_GROUP"
|
||||
| "COMPANY"
|
||||
| "ORGANIZATION";
|
||||
|
||||
export type TenantImportParentOptionGroup = {
|
||||
type: TenantImportParentOptionGroupType;
|
||||
tenants: TenantSummary[];
|
||||
};
|
||||
|
||||
export type TenantImportConflict =
|
||||
| "external_tenant_id"
|
||||
| "slug_exists"
|
||||
@@ -37,12 +53,15 @@ export type TenantImportResolution =
|
||||
| {
|
||||
mode: "existing";
|
||||
tenantId: string;
|
||||
parentTenantId?: string;
|
||||
parentTenantSlug?: string;
|
||||
}
|
||||
| {
|
||||
mode: "create";
|
||||
tenantId?: string;
|
||||
slug?: string;
|
||||
parentTenantId?: string;
|
||||
parentTenantSlug?: string;
|
||||
}
|
||||
| {
|
||||
mode: "skip";
|
||||
@@ -53,16 +72,18 @@ const importHeaders = [
|
||||
"name",
|
||||
"type",
|
||||
"parent_tenant_id",
|
||||
"parent_tenant_slug",
|
||||
"slug",
|
||||
"memo",
|
||||
"email_domain",
|
||||
];
|
||||
|
||||
const headerAliases: Record<string, keyof TenantCSVRow> = {
|
||||
const headerAliases: Record<string, TenantCSVSourceKey> = {
|
||||
id: "tenantId",
|
||||
tenantid: "tenantId",
|
||||
tenant_id: "tenantId",
|
||||
name: "name",
|
||||
조직명: "name",
|
||||
type: "type",
|
||||
parentid: "parentTenantId",
|
||||
parent_id: "parentTenantId",
|
||||
@@ -70,9 +91,12 @@ const headerAliases: Record<string, keyof TenantCSVRow> = {
|
||||
parent_tenant_id: "parentTenantId",
|
||||
parenttenantslug: "parentTenantSlug",
|
||||
parent_tenant_slug: "parentTenantSlug",
|
||||
상위_조직: "parentOrg",
|
||||
slug: "slug",
|
||||
memo: "memo",
|
||||
description: "memo",
|
||||
설명: "memo",
|
||||
메일링_리스트: "mailingList",
|
||||
"email-domain": "emailDomain",
|
||||
emaildomain: "emailDomain",
|
||||
email_domain: "emailDomain",
|
||||
@@ -80,39 +104,96 @@ const headerAliases: Record<string, keyof TenantCSVRow> = {
|
||||
domains: "emailDomain",
|
||||
};
|
||||
|
||||
export function parseTenantCSV(text: string): TenantCSVRow[] {
|
||||
export function parseTenantCSV(
|
||||
text: string,
|
||||
options: TenantCSVParseOptions = {},
|
||||
): TenantCSVRow[] {
|
||||
const records = parseCSV(text.replace(/^\uFEFF/, ""));
|
||||
if (records.length === 0) return [];
|
||||
|
||||
const header = new Map<keyof TenantCSVRow, number>();
|
||||
const header = new Map<TenantCSVSourceKey, number>();
|
||||
records[0].forEach((column, index) => {
|
||||
const normalized = normalizeHeader(column);
|
||||
const key = headerAliases[normalized];
|
||||
if (key) header.set(key, index);
|
||||
});
|
||||
|
||||
return records.slice(1).flatMap((record, index) => {
|
||||
const isOrgChartCSV = header.has("mailingList") || header.has("parentOrg");
|
||||
const sourceRows = records.slice(1).flatMap((record, index) => {
|
||||
if (record.every((value) => value.trim() === "")) return [];
|
||||
const value = (key: keyof TenantCSVRow) => {
|
||||
const value = (key: TenantCSVSourceKey) => {
|
||||
const columnIndex = header.get(key);
|
||||
if (columnIndex === undefined) return "";
|
||||
return (record[columnIndex] ?? "").trim();
|
||||
};
|
||||
|
||||
return {
|
||||
raw: record,
|
||||
rowNumber: index + 2,
|
||||
tenantId: value("tenantId"),
|
||||
name: value("name"),
|
||||
type: value("type"),
|
||||
slug: value("slug") || slugFromMailingList(value("mailingList")),
|
||||
mailingList: value("mailingList"),
|
||||
parentOrg: value("parentOrg"),
|
||||
value,
|
||||
};
|
||||
});
|
||||
const slugByName = new Map(
|
||||
sourceRows
|
||||
.filter((row) => row.name && row.slug)
|
||||
.map((row) => [row.name, row.slug] as const),
|
||||
);
|
||||
|
||||
return sourceRows.map(({ rowNumber, name, slug, parentOrg, value }) => {
|
||||
const parentTenantSlug =
|
||||
value("parentTenantSlug") ||
|
||||
slugFromParentOrg(parentOrg, slugByName) ||
|
||||
(isOrgChartCSV ? options.rootParentSlug || "" : "");
|
||||
|
||||
return {
|
||||
rowNumber,
|
||||
tenantId: value("tenantId"),
|
||||
name,
|
||||
type: value("type") || (isOrgChartCSV ? "ORGANIZATION" : ""),
|
||||
parentTenantId: value("parentTenantId"),
|
||||
parentTenantSlug: value("parentTenantSlug"),
|
||||
slug: value("slug"),
|
||||
parentTenantSlug,
|
||||
slug,
|
||||
memo: value("memo"),
|
||||
emailDomain: value("emailDomain"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function inferTenantImportRootParentSlug(
|
||||
fileName: string,
|
||||
tenants: TenantSummary[] = [],
|
||||
) {
|
||||
const baseName = fileName.trim().split(/[\\/]/).pop()?.toLowerCase() ?? "";
|
||||
const [prefix = ""] = baseName.split("_");
|
||||
if (!prefix) return "";
|
||||
|
||||
const existingTenant = tenants.find(
|
||||
(tenant) => tenant.slug.toLowerCase() === prefix,
|
||||
);
|
||||
return existingTenant ? prefix : "";
|
||||
}
|
||||
|
||||
export function buildTenantImportParentOptionGroups(
|
||||
tenants: TenantSummary[],
|
||||
): TenantImportParentOptionGroup[] {
|
||||
const orderedTypes: TenantImportParentOptionGroupType[] = [
|
||||
"COMPANY_GROUP",
|
||||
"COMPANY",
|
||||
"ORGANIZATION",
|
||||
];
|
||||
|
||||
return orderedTypes
|
||||
.map((type) => ({
|
||||
type,
|
||||
tenants: tenants.filter((tenant) => tenant.type?.toUpperCase() === type),
|
||||
}))
|
||||
.filter((group) => group.tenants.length > 0);
|
||||
}
|
||||
|
||||
export function buildTenantImportPreview(
|
||||
rows: TenantCSVRow[],
|
||||
tenants: TenantSummary[],
|
||||
@@ -169,27 +250,40 @@ export function serializeTenantImportCSV(
|
||||
typeof resolution === "object" && resolution.mode === "create"
|
||||
? resolution.slug || preview.defaultCreateSlug
|
||||
: preview.row.slug;
|
||||
const hasParentTenantIdOverride =
|
||||
typeof resolution === "object" &&
|
||||
Object.hasOwn(resolution, "parentTenantId");
|
||||
const hasParentTenantSlugOverride =
|
||||
typeof resolution === "object" &&
|
||||
Object.hasOwn(resolution, "parentTenantSlug");
|
||||
const sourceParentTenantSlug = hasParentTenantSlugOverride
|
||||
? resolution.parentTenantSlug || ""
|
||||
: preview.row.parentTenantSlug;
|
||||
const parentTenantId =
|
||||
typeof resolution === "object" && resolution.mode === "create"
|
||||
? (resolution.parentTenantId ??
|
||||
remapParentTenantId(
|
||||
preview.row.parentTenantId,
|
||||
preview.row.parentTenantSlug,
|
||||
targetTenantIds,
|
||||
))
|
||||
typeof resolution === "object"
|
||||
? hasParentTenantIdOverride
|
||||
? resolution.parentTenantId || ""
|
||||
: remapParentTenantId(
|
||||
preview.row.parentTenantId,
|
||||
sourceParentTenantSlug,
|
||||
targetTenantIds,
|
||||
)
|
||||
: preview.row.parentTenantId;
|
||||
const parentTenantSlug = remapParentTenantSlug(
|
||||
sourceParentTenantSlug,
|
||||
targetTenantIds,
|
||||
);
|
||||
const tenantId =
|
||||
typeof resolution === "object" && resolution.mode === "create"
|
||||
? (resolution.tenantId ??
|
||||
targetTenantIds.bySourceId.get(preview.row.tenantId) ??
|
||||
createTenantImportId())
|
||||
: selectedTenantId || preview.row.tenantId;
|
||||
targetTenantIds.byRowNumber.get(preview.row.rowNumber) ??
|
||||
selectedTenantId ??
|
||||
preview.row.tenantId;
|
||||
|
||||
lines.push([
|
||||
tenantId,
|
||||
preview.row.name,
|
||||
preview.row.type,
|
||||
parentTenantId,
|
||||
parentTenantSlug,
|
||||
slug,
|
||||
preview.row.memo,
|
||||
preview.row.emailDomain,
|
||||
@@ -202,8 +296,10 @@ function buildTargetTenantIds(
|
||||
previewRows: TenantImportPreviewRow[],
|
||||
selectedTenantIds: Record<number, string | TenantImportResolution>,
|
||||
) {
|
||||
const byRowNumber = new Map<number, string>();
|
||||
const bySourceId = new Map<string, string>();
|
||||
const bySourceSlug = new Map<string, string>();
|
||||
const bySourceSlugToTargetSlug = new Map<string, string>();
|
||||
|
||||
for (const preview of previewRows) {
|
||||
const resolution = selectedTenantIds[preview.row.rowNumber] ?? "";
|
||||
@@ -217,24 +313,38 @@ function buildTargetTenantIds(
|
||||
: resolution.mode === "existing"
|
||||
? resolution.tenantId
|
||||
: resolution.tenantId || createTenantImportId();
|
||||
const targetSlug =
|
||||
typeof resolution === "object" && resolution.mode === "create"
|
||||
? resolution.slug || preview.defaultCreateSlug
|
||||
: preview.row.slug;
|
||||
|
||||
if (targetTenantId) {
|
||||
byRowNumber.set(preview.row.rowNumber, targetTenantId);
|
||||
}
|
||||
if (preview.row.tenantId) {
|
||||
bySourceId.set(preview.row.tenantId, targetTenantId);
|
||||
}
|
||||
if (preview.row.slug) {
|
||||
bySourceSlug.set(preview.row.slug.toLowerCase(), targetTenantId);
|
||||
bySourceSlugToTargetSlug.set(preview.row.slug.toLowerCase(), targetSlug);
|
||||
}
|
||||
if (targetSlug) {
|
||||
bySourceSlug.set(targetSlug.toLowerCase(), targetTenantId);
|
||||
bySourceSlugToTargetSlug.set(targetSlug.toLowerCase(), targetSlug);
|
||||
}
|
||||
}
|
||||
|
||||
return { bySourceId, bySourceSlug };
|
||||
return { byRowNumber, bySourceId, bySourceSlug, bySourceSlugToTargetSlug };
|
||||
}
|
||||
|
||||
function remapParentTenantId(
|
||||
parentTenantId: string,
|
||||
parentTenantSlug: string,
|
||||
targetTenantIds: {
|
||||
byRowNumber: Map<number, string>;
|
||||
bySourceId: Map<string, string>;
|
||||
bySourceSlug: Map<string, string>;
|
||||
bySourceSlugToTargetSlug: Map<string, string>;
|
||||
},
|
||||
) {
|
||||
if (parentTenantId) {
|
||||
@@ -248,6 +358,20 @@ function remapParentTenantId(
|
||||
return "";
|
||||
}
|
||||
|
||||
function remapParentTenantSlug(
|
||||
parentTenantSlug: string,
|
||||
targetTenantIds: {
|
||||
bySourceSlugToTargetSlug: Map<string, string>;
|
||||
},
|
||||
) {
|
||||
if (!parentTenantSlug) return "";
|
||||
return (
|
||||
targetTenantIds.bySourceSlugToTargetSlug.get(
|
||||
parentTenantSlug.toLowerCase(),
|
||||
) ?? parentTenantSlug
|
||||
);
|
||||
}
|
||||
|
||||
function createTenantImportId() {
|
||||
if (globalThis.crypto?.randomUUID) {
|
||||
return globalThis.crypto.randomUUID();
|
||||
@@ -377,6 +501,33 @@ function normalizeHeader(value: string) {
|
||||
return value.trim().toLowerCase().replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
function slugFromMailingList(value: string) {
|
||||
if (!value) return "";
|
||||
return normalizeTenantSlug(value.split("@")[0] ?? value);
|
||||
}
|
||||
|
||||
function slugFromParentOrg(value: string, slugByName: Map<string, string>) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
const match = trimmed.match(/\(([^)]+)\)/);
|
||||
if (match?.[1]) {
|
||||
return slugFromMailingList(match[1]);
|
||||
}
|
||||
return slugByName.get(trimmed) ?? normalizeTenantSlug(trimmed);
|
||||
}
|
||||
|
||||
function normalizeTenantSlug(value: string) {
|
||||
let slug = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-");
|
||||
slug = slug.replace(/^-+|-+$/g, "");
|
||||
if (slug.length > 25) {
|
||||
slug = slug.slice(0, 25).replace(/-+$/g, "");
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
function normalizeToken(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
|
||||
Reference in New Issue
Block a user