forked from baron/baron-sso
userfront e2e 전체 테스트
This commit is contained in:
@@ -128,4 +128,23 @@ nullable@test.com,Nullable User,010-1111-1111,user,primary-tenant,개발팀,책
|
||||
});
|
||||
expect(result[1].additionalAppointments).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should preserve sub_email as secondary email metadata without replacing primary email", () => {
|
||||
const csv = `email,name,tenant_slug,employee_id,sub_email
|
||||
primary@samaneng.com,Primary User,rnd-saman,EMP001,secondary@hanmaceng.co.kr`;
|
||||
|
||||
const result = parseUserCSV(csv);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
email: "primary@samaneng.com",
|
||||
tenantSlug: "rnd-saman",
|
||||
metadata: {
|
||||
employee_id: "EMP001",
|
||||
sub_email: "secondary@hanmaceng.co.kr",
|
||||
secondary_emails: ["secondary@hanmaceng.co.kr"],
|
||||
aliasEmails: ["secondary@hanmaceng.co.kr"],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,11 +12,13 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
for (let i = 1; i < records.length; i++) {
|
||||
const values = records[i].map((v) => v.trim());
|
||||
if (values.every((value) => value === "")) continue;
|
||||
const item: Partial<BulkUserItem> & { metadata: Record<string, string> } = {
|
||||
const item: Partial<BulkUserItem> & {
|
||||
metadata: Record<string, unknown>;
|
||||
} = {
|
||||
metadata: {},
|
||||
};
|
||||
const additionalAppointment: BulkUserAppointment & {
|
||||
metadata: Record<string, string>;
|
||||
metadata: Record<string, unknown>;
|
||||
} = {
|
||||
metadata: {},
|
||||
};
|
||||
@@ -94,6 +96,8 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
item.jobTitle = value;
|
||||
} else if (header === "employee_id") {
|
||||
item.metadata.employee_id = value;
|
||||
} else if (header === "secondary_emails") {
|
||||
applySecondaryEmailMetadata(item, value);
|
||||
} else if (header === "tenant_slug1") {
|
||||
additionalAppointment.tenantSlug = value;
|
||||
} else if (header === "department1") {
|
||||
@@ -117,6 +121,7 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
item.metadata.personal_email = value;
|
||||
} else if (header === "subemail") {
|
||||
item.metadata.naverworks_sub_email = value;
|
||||
addWorksmobileAliasEmails(item, splitEmailTokens(value).slice(1));
|
||||
item.email = firstEmailToken(value) || item.email;
|
||||
} else if (header === "nickname") {
|
||||
item.metadata.naverworks_nickname = value;
|
||||
@@ -185,7 +190,7 @@ export function parseUserCSV(text: string): BulkUserItem[] {
|
||||
}
|
||||
|
||||
function cleanAdditionalAppointment(
|
||||
appointment: BulkUserAppointment & { metadata: Record<string, string> },
|
||||
appointment: BulkUserAppointment & { metadata: Record<string, unknown> },
|
||||
) {
|
||||
const metadata =
|
||||
Object.keys(appointment.metadata).length > 0
|
||||
@@ -210,7 +215,31 @@ function cleanAdditionalAppointment(
|
||||
}
|
||||
|
||||
function normalizeHeader(header: string) {
|
||||
return header
|
||||
const raw = header.trim().replace(/^\uFEFF/, "");
|
||||
const lower = raw.toLowerCase();
|
||||
const separatorNormalized = lower.replace(/-+/g, "_").replace(/_+/g, "_");
|
||||
const compactKorean = raw.replace(/\s+/g, "");
|
||||
|
||||
if (
|
||||
[
|
||||
"sub_email",
|
||||
"secondary_email",
|
||||
"secondary_emails",
|
||||
"additional_email",
|
||||
"additional_emails",
|
||||
"alias_email",
|
||||
"alias_emails",
|
||||
"worksmobile_alias_email",
|
||||
"worksmobile_alias_emails",
|
||||
].includes(separatorNormalized) ||
|
||||
["보조이메일", "보조메일", "추가이메일", "추가메일"].includes(
|
||||
compactKorean,
|
||||
)
|
||||
) {
|
||||
return "secondary_emails";
|
||||
}
|
||||
|
||||
return raw
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\uFEFF/, "")
|
||||
@@ -264,12 +293,69 @@ function parseCSVRecords(text: string) {
|
||||
}
|
||||
|
||||
function firstEmailToken(value: string) {
|
||||
return (
|
||||
value
|
||||
.split(/[;,]/)
|
||||
.map((token) => token.trim())
|
||||
.find((token) => token.includes("@")) ?? ""
|
||||
);
|
||||
return splitEmailTokens(value)[0] ?? "";
|
||||
}
|
||||
|
||||
function splitEmailTokens(value: string) {
|
||||
return value
|
||||
.split(/[;,\n\r\t]/)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.includes("@"));
|
||||
}
|
||||
|
||||
function metadataString(value: unknown) {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function metadataEmailList(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return splitEmailTokens(value);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function uniqueEmails(values: string[]) {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addWorksmobileAliasEmails(
|
||||
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
|
||||
emails: string[],
|
||||
) {
|
||||
const aliases = uniqueEmails([
|
||||
...metadataEmailList(item.metadata.aliasEmails),
|
||||
...emails,
|
||||
]);
|
||||
if (aliases.length > 0) {
|
||||
item.metadata.aliasEmails = aliases;
|
||||
item.metadata.worksmobileAliasEmails = aliases;
|
||||
}
|
||||
}
|
||||
|
||||
function applySecondaryEmailMetadata(
|
||||
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
|
||||
value: string,
|
||||
) {
|
||||
const emails = splitEmailTokens(value);
|
||||
item.metadata.sub_email = value;
|
||||
item.metadata.secondary_emails = uniqueEmails([
|
||||
...metadataEmailList(item.metadata.secondary_emails),
|
||||
...emails,
|
||||
]);
|
||||
addWorksmobileAliasEmails(item, emails);
|
||||
}
|
||||
|
||||
function splitOrganizationPath(value: string) {
|
||||
@@ -280,28 +366,32 @@ function splitOrganizationPath(value: string) {
|
||||
}
|
||||
|
||||
function applyNaverWorksFallbacks(
|
||||
item: Partial<BulkUserItem> & { metadata: Record<string, string> },
|
||||
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
|
||||
) {
|
||||
if (!item.name) {
|
||||
const firstName = item.metadata.naverworks_first_name ?? "";
|
||||
const lastName = item.metadata.naverworks_last_name ?? "";
|
||||
const firstName = metadataString(item.metadata.naverworks_first_name);
|
||||
const lastName = metadataString(item.metadata.naverworks_last_name);
|
||||
item.name = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||
if (!item.name && item.metadata.naverworks_nickname) {
|
||||
item.name = item.metadata.naverworks_nickname;
|
||||
const nickname = metadataString(item.metadata.naverworks_nickname);
|
||||
if (!item.name && nickname) {
|
||||
item.name = nickname;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.email) {
|
||||
item.email = item.metadata.personal_email;
|
||||
item.email = metadataString(item.metadata.personal_email);
|
||||
}
|
||||
|
||||
if (!item.phone) {
|
||||
const countryCode = item.metadata.naverworks_mobile_country_code ?? "";
|
||||
const number = item.metadata.naverworks_mobile_numbers ?? "";
|
||||
const countryCode = metadataString(
|
||||
item.metadata.naverworks_mobile_country_code,
|
||||
);
|
||||
const number = metadataString(item.metadata.naverworks_mobile_numbers);
|
||||
item.phone = `${countryCode}${number}`.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
if (!item.grade && item.metadata.naverworks_level) {
|
||||
item.grade = item.metadata.naverworks_level;
|
||||
const level = metadataString(item.metadata.naverworks_level);
|
||||
if (!item.grade && level) {
|
||||
item.grade = level;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,4 +101,60 @@ describe("applyGeneralPlanningOfficePriority", () => {
|
||||
employee_id: "EMP001",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses GPDTDC as the Baron representative while keeping the first affiliation primary for WorksMobile", () => {
|
||||
const user: BulkUserItem = {
|
||||
email: "gpdtdc-dual@test.com",
|
||||
name: "GPDTDC Dual User",
|
||||
tenantSlug: "rnd-saman",
|
||||
department: "삼안기술연구소",
|
||||
grade: "책임",
|
||||
metadata: {
|
||||
employee_id: "SAMAN001",
|
||||
},
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantSlug: "tdc",
|
||||
tenantName: "기술개발센터",
|
||||
grade: "책임연구원",
|
||||
metadata: {
|
||||
employee_id: "B24051",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyGeneralPlanningOfficePriority(user, [
|
||||
tenant("family", "한맥가족사", "hanmac-family"),
|
||||
tenant("gpdtdc", "총괄기획&기술개발센터", "gpdtdc", "family"),
|
||||
tenant("tdc", "기술개발센터", "tdc", "gpdtdc"),
|
||||
tenant("saman", "삼안", "rnd-saman"),
|
||||
]);
|
||||
|
||||
expect(result.tenantSlug).toBe("gpdtdc");
|
||||
expect(result.tenantImport).toMatchObject({
|
||||
slug: "gpdtdc",
|
||||
name: "총괄기획&기술개발센터",
|
||||
});
|
||||
expect(result.metadata.employee_id).toBe("SAMAN001");
|
||||
expect(result.additionalAppointments).toEqual([
|
||||
expect.objectContaining({
|
||||
tenantSlug: "rnd-saman",
|
||||
isPrimary: true,
|
||||
department: "삼안기술연구소",
|
||||
grade: "책임",
|
||||
metadata: {
|
||||
employee_id: "SAMAN001",
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
tenantSlug: "tdc",
|
||||
isPrimary: false,
|
||||
grade: "책임연구원",
|
||||
metadata: {
|
||||
employee_id: "B24051",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import type { BulkUserItem, TenantSummary } from "../../../lib/adminApi";
|
||||
import type {
|
||||
BulkUserAppointment,
|
||||
BulkUserItem,
|
||||
TenantSummary,
|
||||
} from "../../../lib/adminApi";
|
||||
|
||||
export function applyGeneralPlanningOfficePriority(
|
||||
user: BulkUserItem,
|
||||
tenants: TenantSummary[],
|
||||
): BulkUserItem {
|
||||
const gpdtdcRepresentative = applyGPDTDCRepresentativeTenant(user, tenants);
|
||||
if (gpdtdcRepresentative) {
|
||||
return gpdtdcRepresentative;
|
||||
}
|
||||
|
||||
const firstAdditional = user.additionalAppointments?.[0];
|
||||
const secondarySlug = firstAdditional?.tenantSlug;
|
||||
|
||||
@@ -67,6 +76,82 @@ export function applyGeneralPlanningOfficePriority(
|
||||
};
|
||||
}
|
||||
|
||||
function applyGPDTDCRepresentativeTenant(
|
||||
user: BulkUserItem,
|
||||
tenants: TenantSummary[],
|
||||
): BulkUserItem | undefined {
|
||||
const root = findGPDTDCRootTenant(tenants);
|
||||
if (!root) return undefined;
|
||||
|
||||
const primarySlug = user.tenantSlug || "";
|
||||
const hasPrimaryUnderRoot = isUnderTenant(primarySlug, root, tenants);
|
||||
const hasAppointmentUnderRoot = (user.additionalAppointments ?? []).some(
|
||||
(appointment) => isUnderTenant(appointment.tenantSlug || "", root, tenants),
|
||||
);
|
||||
if (!hasPrimaryUnderRoot && !hasAppointmentUnderRoot) return undefined;
|
||||
if (primarySlug === root.slug) return undefined;
|
||||
|
||||
const worksmobileAppointments: BulkUserAppointment[] = [];
|
||||
const seen = new Set<string>();
|
||||
const addAppointment = (
|
||||
appointment: BulkUserAppointment,
|
||||
fallbackKey: string,
|
||||
) => {
|
||||
const key = appointment.tenantSlug || appointment.tenantId || fallbackKey;
|
||||
if (!key || key === root.slug || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
worksmobileAppointments.push(appointment);
|
||||
};
|
||||
|
||||
addAppointment(buildPrimaryAppointment(user), "primary");
|
||||
for (const appointment of user.additionalAppointments ?? []) {
|
||||
addAppointment(
|
||||
{ ...appointment, isPrimary: false },
|
||||
appointment.tenantSlug || "",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
tenantSlug: root.slug,
|
||||
tenantImport: {
|
||||
...(user.tenantImport ?? {}),
|
||||
sourceTenantId: undefined,
|
||||
slug: root.slug,
|
||||
name: root.name,
|
||||
},
|
||||
additionalAppointments:
|
||||
worksmobileAppointments.length > 0 ? worksmobileAppointments : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrimaryAppointment(user: BulkUserItem): BulkUserAppointment {
|
||||
return {
|
||||
...(user.tenantId ? { tenantId: user.tenantId } : {}),
|
||||
...(user.tenantSlug ? { tenantSlug: user.tenantSlug } : {}),
|
||||
isPrimary: true,
|
||||
isOwner: false,
|
||||
...(user.department ? { department: user.department } : {}),
|
||||
...(user.grade ? { grade: user.grade } : {}),
|
||||
...(user.position ? { position: user.position } : {}),
|
||||
...(user.jobTitle ? { jobTitle: user.jobTitle } : {}),
|
||||
metadata: { ...user.metadata },
|
||||
};
|
||||
}
|
||||
|
||||
function findGPDTDCRootTenant(tenants: TenantSummary[]) {
|
||||
return tenants.find((tenant) => {
|
||||
const slug = tenant.slug.trim().toLowerCase();
|
||||
const name = tenant.name.replace(/\s+/g, "").toLowerCase();
|
||||
return (
|
||||
slug === "gpdtdc" ||
|
||||
name === "gpdtdc" ||
|
||||
name.includes("총괄기획&기술개발센터") ||
|
||||
name.includes("총괄기획기술개발센터")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function isUnderGeneralPlanningOffice(
|
||||
tenantSlug: string,
|
||||
tenants: TenantSummary[],
|
||||
@@ -80,6 +165,20 @@ function isUnderGeneralPlanningOffice(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUnderTenant(
|
||||
tenantSlug: string,
|
||||
root: TenantSummary,
|
||||
tenants: TenantSummary[],
|
||||
): boolean {
|
||||
let current = tenants.find((tenant) => tenant.slug === tenantSlug);
|
||||
while (current) {
|
||||
if (current.id === root.id || current.slug === root.slug) return true;
|
||||
if (!current.parentId) break;
|
||||
current = tenants.find((tenant) => tenant.id === current?.parentId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user