1
0
forked from baron/baron-sso

fix(orgfront): place GPDTDC users on leaf appointments

This commit is contained in:
2026-05-29 08:38:05 +09:00
parent da01f63c54
commit 731ae9251e
3 changed files with 168 additions and 6 deletions

View File

@@ -448,4 +448,53 @@ describe("org chart layout", () => {
"engineering-user",
]);
});
it("maps GPDTDC representative users to their visible leaf appointment", () => {
const gpdtdc = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
const tdc = {
...tenantNode("tdc", "ORGANIZATION", "기술개발센터", "tdc"),
parentId: "gpdtdc",
};
const leaf = {
...tenantNode("tdc-leaf", "USER_GROUP", "기술개발센터 1팀", "tdc-leaf"),
parentId: "tdc",
};
const rootNode = {
...gpdtdc,
children: [{ ...tdc, children: [leaf] }],
};
const usersMap = buildUsersMap(
[
{
...member("gpdtdc-user"),
companyCode: undefined,
tenantSlug: "gpdtdc",
metadata: {
additionalAppointments: [
{
tenantSlug: "internal-planning",
isPrimary: true,
},
{
tenantSlug: "tdc-leaf",
isPrimary: false,
grade: "책임",
position: "팀장",
},
],
},
joinedTenants: undefined,
},
],
[rootNode],
{ activeOnly: true },
);
expect(usersMap.get("gpdtdc")).toBeUndefined();
expect(usersMap.get("internal-planning")).toBeUndefined();
expect(usersMap.get("tdc-leaf")?.map((user) => user.id)).toEqual([
"gpdtdc-user",
]);
});
});

View File

@@ -1121,6 +1121,11 @@ type TenantIndexes = {
bySlug: Map<string, TenantNode>;
};
type UserOrgAppointmentRef = {
tenantId?: string;
tenantSlug?: string;
};
function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
const byId = new Map<string, TenantNode>();
const bySlug = new Map<string, TenantNode>();
@@ -1134,6 +1139,36 @@ function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
return { byId, bySlug };
}
function normalizeOrgSlug(value: unknown) {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function getUserOrgAppointmentRefs(user: UserSummary): UserOrgAppointmentRef[] {
const rawAppointments = user.metadata?.additionalAppointments;
if (!Array.isArray(rawAppointments)) return [];
return rawAppointments
.filter(
(item): item is Record<string, unknown> =>
typeof item === "object" && item !== null,
)
.map((item) => ({
tenantId: typeof item.tenantId === "string" ? item.tenantId.trim() : "",
tenantSlug: normalizeOrgSlug(item.tenantSlug),
}));
}
function addTenantSlugCandidate(
slugs: Set<string>,
tenantIndexes: TenantIndexes,
slug: string,
) {
const normalizedSlug = normalizeOrgSlug(slug);
if (!normalizedSlug) return;
if (!tenantIndexes.bySlug.has(normalizedSlug)) return;
slugs.add(normalizedSlug);
}
function isDescendantTenant(
candidate: TenantNode,
ancestor: TenantNode,
@@ -1184,8 +1219,8 @@ export function buildUsersMap(
if (!isVisibleOrgChartUser(user)) continue;
const slugs = new Set<string>();
const primarySlug = user.tenantSlug?.toLowerCase() || "";
const legacyCompanySlug = user.companyCode?.toLowerCase() || "";
const primarySlug = normalizeOrgSlug(user.tenantSlug);
const legacyCompanySlug = normalizeOrgSlug(user.companyCode);
if (
primarySlug &&
!isSystemGlobalTenant({
@@ -1195,7 +1230,7 @@ export function buildUsersMap(
name: primarySlug,
})
) {
slugs.add(primarySlug);
addTenantSlugCandidate(slugs, tenantIndexes, primarySlug);
}
if (
legacyCompanySlug &&
@@ -1206,14 +1241,27 @@ export function buildUsersMap(
name: legacyCompanySlug,
})
) {
slugs.add(legacyCompanySlug);
addTenantSlugCandidate(slugs, tenantIndexes, legacyCompanySlug);
}
if (user.tenant?.slug && !isSystemGlobalTenant(user.tenant)) {
slugs.add(user.tenant.slug.toLowerCase());
addTenantSlugCandidate(slugs, tenantIndexes, user.tenant.slug);
}
for (const joinedTenant of user.joinedTenants || []) {
if (joinedTenant.slug && !isSystemGlobalTenant(joinedTenant)) {
slugs.add(joinedTenant.slug.toLowerCase());
addTenantSlugCandidate(slugs, tenantIndexes, joinedTenant.slug);
}
}
for (const appointment of getUserOrgAppointmentRefs(user)) {
if (appointment.tenantSlug) {
addTenantSlugCandidate(slugs, tenantIndexes, appointment.tenantSlug);
continue;
}
const tenantById = appointment.tenantId
? tenantIndexes.byId.get(appointment.tenantId)
: undefined;
if (tenantById) {
addTenantSlugCandidate(slugs, tenantIndexes, tenantById.slug);
}
}

View File

@@ -385,6 +385,71 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
await expect(svg.getByText(/^1$/)).toHaveCount(4);
});
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
page,
}) => {
await page.route("**/api/v1/public/orgchart**", async (route) => {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({
sharedWith: "Playwright",
tenants: [
tenant("group", "HMAC Group", "hmac"),
tenant("gpdtdc", "GPDTDC", "gpdtdc", "group", "COMPANY"),
tenant("tdc", "기술개발센터", "tdc", "gpdtdc", "ORGANIZATION"),
tenant("tdc-leaf", "기술개발센터 1팀", "tdc-leaf", "tdc"),
tenant(
"internal-planning",
"내부 구성 조직",
"internal-planning",
"group",
"ORGANIZATION",
{ visibility: "internal" },
),
],
users: [
{
...user("u-gpdtdc-leaf", "GPDTDC Leaf User", "gpdtdc"),
tenantSlug: "gpdtdc",
companyCode: undefined,
metadata: {
additionalAppointments: [
{
tenantSlug: "internal-planning",
isPrimary: true,
},
{
tenantSlug: "tdc-leaf",
isPrimary: false,
grade: "책임",
position: "팀장",
},
],
},
},
],
}),
});
});
await page.goto("/chart?token=gpdtdc-leaf");
await expect(page.getByText("총 1명")).toBeVisible();
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
await expect(svg).toBeVisible();
await expect(svg.getByText("내부 구성 조직")).toHaveCount(0);
await expect(
page
.locator('[data-testid="orgchart-node-gpdtdc"]')
.getByText(/GPDTDC Leaf User/),
).toHaveCount(0);
await expect(
page
.locator('[data-testid="orgchart-node-tdc-leaf"]')
.getByText("GPDTDC Leaf User 책임(팀장)"),
).toBeVisible();
});
test("org chart counts multi-leaf tenant users once in ancestor totals", async ({
page,
}) => {