1
0
forked from baron/baron-sso

네이버 계정 정합성 맞춤

This commit is contained in:
2026-06-15 19:54:09 +09:00
parent 8e9d015443
commit 4d468cd39f
97 changed files with 5837 additions and 2031 deletions

View File

@@ -618,6 +618,120 @@ describe("org chart layout", () => {
]);
});
it("maps appointment memberships by tenant UUID when the stored slug is stale", () => {
const root = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
const division = {
...tenantNode("division", "ORGANIZATION", "총괄기획실", "gpd"),
parentId: "gpdtdc",
};
const migratedLeaf = {
...tenantNode(
"migrated-leaf",
"ORGANIZATION",
"통합시스템",
"intigrated-system",
),
parentId: "division",
};
const rootNode = {
...root,
children: [{ ...division, children: [migratedLeaf] }],
};
const usersMap = buildUsersMap(
[
{
...member("migrated-user"),
companyCode: undefined,
tenantSlug: "gpdtdc",
metadata: {
additionalAppointments: [
{
tenantId: "migrated-leaf",
tenantName: "기술기획",
tenantSlug: "tech-planning",
},
],
},
joinedTenants: undefined,
},
],
[rootNode],
{ activeOnly: true },
);
expect(usersMap.get("gpdtdc")).toBeUndefined();
expect(usersMap.get("tech-planning")).toBeUndefined();
expect(usersMap.get("intigrated-system")?.map((user) => user.id)).toEqual([
"migrated-user",
]);
});
it("maps primary and joined memberships by tenant UUID when stored slugs are stale", () => {
const root = tenantNode("root-company", "COMPANY", "Root", "root-company");
const primaryLeaf = {
...tenantNode("primary-leaf", "ORGANIZATION", "Primary", "primary-new"),
parentId: "root-company",
};
const joinedLeaf = {
...tenantNode("joined-leaf", "ORGANIZATION", "Joined", "joined-new"),
parentId: "root-company",
};
const rootNode = {
...root,
children: [primaryLeaf, joinedLeaf],
};
const usersMap = buildUsersMap(
[
{
...member("primary-user"),
companyCode: undefined,
tenantSlug: undefined,
tenant: {
id: "primary-leaf",
type: "ORGANIZATION",
name: "Primary",
slug: "primary-old",
description: "",
status: "active",
createdAt: "2026-05-11T00:00:00.000Z",
updatedAt: "2026-05-11T00:00:00.000Z",
},
joinedTenants: undefined,
},
{
...member("joined-user"),
companyCode: undefined,
tenantSlug: undefined,
joinedTenants: [
{
id: "joined-leaf",
type: "ORGANIZATION",
name: "Joined",
slug: "joined-old",
description: "",
status: "active",
createdAt: "2026-05-11T00:00:00.000Z",
updatedAt: "2026-05-11T00:00:00.000Z",
},
],
},
],
[rootNode],
{ activeOnly: true },
);
expect(usersMap.get("primary-new")?.map((user) => user.id)).toEqual([
"primary-user",
]);
expect(usersMap.get("joined-new")?.map((user) => user.id)).toEqual([
"joined-user",
]);
expect(usersMap.get("primary-old")).toBeUndefined();
expect(usersMap.get("joined-old")).toBeUndefined();
});
it("does not fall back to a visible parent for hidden leaf memberships", () => {
const gpdtdc = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
const internalLeaf = {

View File

@@ -1270,14 +1270,27 @@ function getUserOrgAppointmentRefs(user: UserSummary): UserOrgAppointmentRef[] {
}
function addTenantSlugCandidate(
slugs: Set<string>,
tenantIds: Set<string>,
tenantIndexes: TenantIndexes,
slug: string,
) {
const normalizedSlug = normalizeOrgSlug(slug);
if (!normalizedSlug) return;
if (!tenantIndexes.bySlug.has(normalizedSlug)) return;
slugs.add(normalizedSlug);
const tenant = tenantIndexes.bySlug.get(normalizedSlug);
if (!tenant) return;
tenantIds.add(tenant.id);
}
function addTenantIdCandidate(
tenantIds: Set<string>,
tenantIndexes: TenantIndexes,
id: unknown,
) {
if (typeof id !== "string") return;
const normalizedId = id.trim();
if (!normalizedId) return;
if (!tenantIndexes.byId.has(normalizedId)) return;
tenantIds.add(normalizedId);
}
function isDescendantTenant(
@@ -1298,19 +1311,19 @@ function isDescendantTenant(
return false;
}
function getLeafMembershipSlugs(
slugs: Set<string>,
function getLeafMembershipIds(
tenantIds: Set<string>,
tenantIndexes: TenantIndexes,
) {
const memberships = Array.from(slugs);
const memberships = Array.from(tenantIds);
return memberships.filter((slug) => {
const tenant = tenantIndexes.bySlug.get(slug);
return memberships.filter((id) => {
const tenant = tenantIndexes.byId.get(id);
if (!tenant) return true;
return !memberships.some((otherSlug) => {
if (otherSlug === slug) return false;
const otherTenant = tenantIndexes.bySlug.get(otherSlug);
return !memberships.some((otherId) => {
if (otherId === id) return false;
const otherTenant = tenantIndexes.byId.get(otherId);
if (!otherTenant) return false;
return isDescendantTenant(otherTenant, tenant, tenantIndexes.byId);
});
@@ -1332,7 +1345,7 @@ export function buildUsersMap(
if (options.activeOnly && user.status !== "active") continue;
if (!isVisibleOrgChartUser(user)) continue;
const slugs = new Set<string>();
const tenantIds = new Set<string>();
const primarySlug = normalizeOrgSlug(user.tenantSlug);
const legacyCompanySlug = normalizeOrgSlug(user.companyCode);
if (
@@ -1344,7 +1357,7 @@ export function buildUsersMap(
name: primarySlug,
})
) {
addTenantSlugCandidate(slugs, membershipTenantIndexes, primarySlug);
addTenantSlugCandidate(tenantIds, membershipTenantIndexes, primarySlug);
}
if (
legacyCompanySlug &&
@@ -1355,24 +1368,51 @@ export function buildUsersMap(
name: legacyCompanySlug,
})
) {
addTenantSlugCandidate(slugs, membershipTenantIndexes, legacyCompanySlug);
addTenantSlugCandidate(
tenantIds,
membershipTenantIndexes,
legacyCompanySlug,
);
}
if (user.tenant?.slug && !isSystemGlobalTenant(user.tenant)) {
addTenantSlugCandidate(slugs, membershipTenantIndexes, user.tenant.slug);
addTenantIdCandidate(tenantIds, membershipTenantIndexes, user.tenant.id);
addTenantSlugCandidate(
tenantIds,
membershipTenantIndexes,
user.tenant.slug,
);
}
for (const joinedTenant of user.joinedTenants || []) {
if (joinedTenant.slug && !isSystemGlobalTenant(joinedTenant)) {
addTenantIdCandidate(
tenantIds,
membershipTenantIndexes,
joinedTenant.id,
);
addTenantSlugCandidate(
slugs,
tenantIds,
membershipTenantIndexes,
joinedTenant.slug,
);
}
}
for (const appointment of getUserOrgAppointmentRefs(user)) {
const hasTenantIdCandidate =
appointment.tenantId &&
membershipTenantIndexes.byId.has(appointment.tenantId);
if (hasTenantIdCandidate) {
addTenantIdCandidate(
tenantIds,
membershipTenantIndexes,
appointment.tenantId,
);
continue;
}
if (appointment.tenantSlug) {
addTenantSlugCandidate(
slugs,
tenantIds,
membershipTenantIndexes,
appointment.tenantSlug,
);
@@ -1383,12 +1423,14 @@ export function buildUsersMap(
? membershipTenantIndexes.byId.get(appointment.tenantId)
: undefined;
if (tenantById) {
addTenantSlugCandidate(slugs, membershipTenantIndexes, tenantById.slug);
addTenantIdCandidate(tenantIds, membershipTenantIndexes, tenantById.id);
}
}
for (const slug of getLeafMembershipSlugs(slugs, membershipTenantIndexes)) {
if (!visibleTenantIndexes.bySlug.has(slug)) continue;
for (const id of getLeafMembershipIds(tenantIds, membershipTenantIndexes)) {
const visibleTenant = visibleTenantIndexes.byId.get(id);
if (!visibleTenant) continue;
const slug = visibleTenant.slug.toLowerCase();
const list = map.get(slug) || [];
if (!list.some((existing) => existing.id === user.id)) list.push(user);
map.set(slug, list);
@@ -1444,6 +1486,9 @@ export function TenantOrgChartPage() {
queryKey: ["orgchart-snapshot", { cache: "redis" }],
queryFn: () => fetchOrgChartSnapshot(),
enabled: !shareToken,
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: true,
});
const { rootNodes, usersMap, sharedWith } = React.useMemo(() => {

View File

@@ -194,6 +194,24 @@ describe("OrgPickerEmbedPage orgchart data source", () => {
});
});
it("forces authenticated picker snapshots to refetch instead of trusting the in-memory query cache", async () => {
adminApiMocks.fetchOrgChartSnapshot.mockResolvedValue(snapshot);
const rendered = renderPicker("/embed/picker?select=both");
await waitForExpect(() => {
expect(adminApiMocks.fetchOrgChartSnapshot).toHaveBeenCalledTimes(1);
});
const query = rendered.queryClient.getQueryCache().find({
queryKey: ["org-picker-orgchart", "authenticated", undefined],
});
expect(query?.options.staleTime).toBe(0);
expect(query?.options.refetchOnMount).toBe("always");
expect(query?.options.refetchOnWindowFocus).toBe(true);
});
it("uses the public orgchart snapshot when a share token is present", async () => {
adminApiMocks.fetchOrgChartSnapshot.mockResolvedValue(snapshot);
adminApiMocks.fetchPublicOrgChart.mockResolvedValue(snapshot);

View File

@@ -384,6 +384,9 @@ export function OrgPickerEmbedPage() {
],
queryFn: () =>
shareToken ? fetchPublicOrgChart(shareToken) : fetchOrgChartSnapshot(),
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: true,
});
React.useEffect(() => {