1
0
forked from baron/baron-sso

동기화 기초구조 마련

This commit is contained in:
2026-05-12 12:25:31 +09:00
parent 3063450ee0
commit 5e649c279f
33 changed files with 3364 additions and 408 deletions

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
getHanmacFamilyTenantOrderRank,
orderHanmacFamilyChildren,
orderHanmacFamilyTenants,
} from "./hanmacFamilyOrder";
function tenant(name: string, slug: string) {
return { name, slug };
}
describe("hanmac family organization order", () => {
it("orders the top hanmac-family siblings by policy", () => {
const ordered = orderHanmacFamilyTenants([
tenant("바론그룹", "baron-group"),
tenant("한맥기술", "hanmac"),
tenant("삼안", "saman"),
tenant("총괄기획&기술개발센터", "gpdtdc"),
]);
expect(ordered.map((item) => item.name)).toEqual([
"총괄기획&기술개발센터",
"삼안",
"한맥기술",
"바론그룹",
]);
});
it("keeps hanmac-family as the root before ordered descendants", () => {
const family = tenant("한맥가족", "hanmac-family");
const children = orderHanmacFamilyChildren(family, [
tenant("바론그룹", "baron-group"),
tenant("총괄기획&기술개발센터", "gpdtdc"),
tenant("삼안", "saman"),
tenant("한맥기술", "hanmac"),
]);
expect([family, ...children].map((item) => item.name)).toEqual([
"한맥가족",
"총괄기획&기술개발센터",
"삼안",
"한맥기술",
"바론그룹",
]);
});
it("does not rank generic technical centers as GPDTDC", () => {
expect(
getHanmacFamilyTenantOrderRank(
tenant("기술개발센터", "rnd-center"),
),
).toBe(Number.MAX_SAFE_INTEGER);
});
});

View File

@@ -0,0 +1,65 @@
export type HanmacFamilyOrderTenant = {
name: string;
slug: string;
};
export const HANMAC_FAMILY_ROOT_SLUG = "hanmac-family";
export const HANMAC_FAMILY_TENANT_ORDER = [
"gpdtdc",
"saman",
"hanmac",
"baron-group",
] as const;
function normalizedTenantText(tenant: HanmacFamilyOrderTenant) {
return `${tenant.slug} ${tenant.name}`.trim().toLowerCase();
}
export function isHanmacFamilyRootTenant(tenant: HanmacFamilyOrderTenant) {
return (
tenant.slug.toLowerCase() === HANMAC_FAMILY_ROOT_SLUG ||
tenant.name.includes("한맥가족")
);
}
export function getHanmacFamilyTenantOrderRank(
tenant: HanmacFamilyOrderTenant,
) {
const text = normalizedTenantText(tenant);
if (text.includes("gpdtdc") || text.includes("총괄기획")) return 0;
if (text.includes("saman") || text.includes("삼안")) return 1;
if (
(text.includes("hanmac") || text.includes("한맥기술")) &&
!isHanmacFamilyRootTenant(tenant)
) {
return 2;
}
if (text.includes("baron-group") || text.includes("바론그룹")) return 3;
return Number.MAX_SAFE_INTEGER;
}
export function compareHanmacFamilyTenants<T extends HanmacFamilyOrderTenant>(
a: T,
b: T,
) {
const rankDiff =
getHanmacFamilyTenantOrderRank(a) - getHanmacFamilyTenantOrderRank(b);
if (rankDiff !== 0) return rankDiff;
return a.name.localeCompare(b.name);
}
export function orderHanmacFamilyTenants<T extends HanmacFamilyOrderTenant>(
tenants: readonly T[],
) {
return [...tenants].sort(compareHanmacFamilyTenants);
}
export function orderHanmacFamilyChildren<T extends HanmacFamilyOrderTenant>(
parent: HanmacFamilyOrderTenant,
children: readonly T[],
) {
return isHanmacFamilyRootTenant(parent)
? orderHanmacFamilyTenants(children)
: [...children];
}

View File

@@ -51,6 +51,40 @@ describe("buildOrgPickerTree", () => {
]);
});
it("orders hanmac-family children by the shared organization policy", () => {
const tenants = [
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
tenant(
"baron-group-id",
"COMPANY_GROUP",
"바론그룹",
"baron-group",
"hanmac-family-id",
),
tenant("hanmac-id", "COMPANY", "한맥기술", "hanmac", "hanmac-family-id"),
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
tenant(
"gpdtdc-id",
"ORGANIZATION",
"총괄기획&기술개발센터",
"gpdtdc",
"hanmac-family-id",
),
];
const tree = buildOrgPickerTree({
tenants,
users: [] satisfies UserSummary[],
});
expect(tree.roots[0]?.children.map((node) => node.name)).toEqual([
"총괄기획&기술개발센터",
"삼안",
"한맥기술",
"바론그룹",
]);
});
it("scopes descendant filtering by tenant slug", () => {
const tenants = [
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),

View File

@@ -1,5 +1,6 @@
import type { TenantSummary, UserSummary } from "../../lib/adminApi";
import { type TenantNode, buildTenantFullTree } from "../../lib/tenantTree";
import { orderHanmacFamilyChildren } from "./hanmacFamilyOrder";
import type { OrgPickerTreeNode } from "./pickerTypes";
import { filterTenantsByVisibility } from "./tenantVisibility";
import { getOrgChartUserDisplayName } from "./userDisplay";
@@ -50,9 +51,10 @@ function tenantToPickerNode(
tenant: TenantNode,
usersBySlug: Map<string, UserSummary[]>,
): OrgPickerTreeNode {
const tenantChildren = tenant.children.map((child) =>
tenantToPickerNode(child, usersBySlug),
);
const tenantChildren = orderHanmacFamilyChildren(
tenant,
tenant.children,
).map((child) => tenantToPickerNode(child, usersBySlug));
const userChildren = (usersBySlug.get(tenant.slug.toLowerCase()) || []).map(
(user) => ({
type: "user" as const,
@@ -150,9 +152,10 @@ export function buildOrgPickerTree({
if (!groupNode) return { roots: [], companies: [], companyGroupId: "" };
const companies = groupNode.children.filter(
(node) => node.type === "COMPANY",
);
const companies = orderHanmacFamilyChildren(
groupNode,
groupNode.children,
).filter((node) => node.type === "COMPANY");
const scopedRoot = tenantId
? findTenantNode([groupNode], tenantId)
: groupNode;

View File

@@ -10,6 +10,10 @@ import {
fetchUsers,
} from "../../../lib/adminApi";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
import {
orderHanmacFamilyChildren,
orderHanmacFamilyTenants,
} from "../hanmacFamilyOrder";
import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility";
import { getOrgChartUserDisplayName, getUserOrgProfile } from "../userDisplay";
@@ -565,7 +569,10 @@ function buildOrgNode(
? 0
: inheritedCompanyColorDepth + 1;
const members = usersMap.get(slug) || [];
const children = tenantNode.children.map((child) =>
const children = orderHanmacFamilyChildren(
tenantNode,
tenantNode.children,
).map((child) =>
buildOrgNode(
child,
usersMap,
@@ -1018,33 +1025,14 @@ function collectOrgSelectionDescendants(
]);
}
function getOrgSelectionPolicyRank(node: TenantNode) {
const text = `${node.slug} ${node.name}`.toLowerCase();
if (text.includes("gpdtdc") || text.includes("총괄기획")) return 1;
if (text.includes("saman") || text.includes("삼안")) return 2;
if (
(text.includes("hanmac") || text.includes("한맥기술")) &&
!text.includes("hanmac-family")
) {
return 3;
}
if (text.includes("baron") || text.includes("바론")) return 4;
return 100;
}
export function buildOrgSelectionOptions(
familyRoot: TenantNode | null,
): OrgSelectionOption[] {
return (familyRoot?.children ?? [])
.filter((node) =>
return orderHanmacFamilyTenants(
(familyRoot?.children ?? []).filter((node) =>
["COMPANY_GROUP", "COMPANY", "ORGANIZATION"].includes(node.type),
)
.sort((a, b) => {
const rankDiff =
getOrgSelectionPolicyRank(a) - getOrgSelectionPolicyRank(b);
if (rankDiff !== 0) return rankDiff;
return a.name.localeCompare(b.name);
})
),
)
.map((node) => ({
descendants: collectOrgSelectionDescendants(node, 2),
id: node.id,