export type OrgChartTenantSelection = { id: string; name: string; }; export type OrgChartUserSelection = { id: string; name: string; email: string; rootTenantName?: string; leafTenantName?: string; }; export type TenantFilterTarget = { id?: string; tenantId?: string; slug?: string; tenantSlug?: string; type?: string; parentId?: string | null; name?: string; tenantName?: string; visibility?: string; config?: Record; }; export type HanmacFamilyUserTarget = { companyCode?: string; tenantSlug?: string; tenant?: TenantFilterTarget; joinedTenants?: TenantFilterTarget[]; metadata?: Record; }; export type UserMembershipTenantTabId = | "hanmac-family" | "commercial" | "public-org" | "edu" | "personal"; export type UserMembershipTenantTab = { id: UserMembershipTenantTabId; label: string; rootSlug: string; seedTenantId?: string; }; type OrgChartPickerMessage = { type?: unknown; payload?: { selections?: Array<{ type?: unknown; id?: unknown; name?: unknown; email?: unknown; rootTenantName?: unknown; leafTenantName?: unknown; tenantName?: unknown; }>; }; }; type OrgChartTenantPickerOptions = { includeInternal?: boolean; tenantId?: string; }; type OrgChartUserMultiPickerOptions = { tenantId?: string; }; type OrgChartLoginOptions = { includeInternal?: boolean; returnTo?: string; }; const DEFAULT_ORGFRONT_BASE_URL = "http://localhost:5175"; export const USER_MEMBERSHIP_TENANT_TABS: UserMembershipTenantTab[] = [ { id: "hanmac-family", label: "한맥가족", rootSlug: "hanmac-family", seedTenantId: "038326b6-954a-48a7-a85f-efd83f62b82a", }, { id: "commercial", label: "일반회사", rootSlug: "commercial", }, { id: "public-org", label: "공공기관", rootSlug: "public-org", }, { id: "edu", label: "교육기관", rootSlug: "edu", }, { id: "personal", label: "개인", rootSlug: "personal", seedTenantId: "9607eb7b-04d2-42ab-80fe-780fe21c7e8f", }, ]; export const GPDTDC_GRADE_OPTIONS = [ "연구원", "선임", "책임", "수석", "부사장", "사장", ] as const; export const HANMAC_FAMILY_GRADE_OPTIONS = [ "사원", "대리", "과장", "차장", "부장", "이사", "상무이사", "전무이사", "부사장", "사장", "회장", ] as const; function isSystemTenant(tenant: TenantFilterTarget) { const slug = tenant.slug?.trim().toLowerCase(); const type = tenant.type?.trim().toUpperCase(); return ( !tenant.id?.trim() || !tenant.slug?.trim() || type === "SYSTEM" || slug === "system" || slug === "global" ); } function resolveTenantTarget( target: TenantFilterTarget | undefined, tenants: T[], ) { if (!target) return undefined; const tenantID = target.id ?? target.tenantId ?? ""; const tenantSlug = target.slug ?? target.tenantSlug ?? ""; return ( tenants.find((tenant) => tenantID && tenant.id === tenantID) ?? tenants.find( (tenant) => tenantSlug && tenant.slug?.trim().toLowerCase() === tenantSlug.trim().toLowerCase(), ) ?? target ); } function resolveMembershipRoot( tab: UserMembershipTenantTab, tenants: T[], ) { const rootSlug = tab.rootSlug.toLowerCase(); return ( tenants.find( (tenant) => tab.seedTenantId && tenant.id === tab.seedTenantId, ) ?? tenants.find((tenant) => tenant.slug?.trim().toLowerCase() === rootSlug) ); } export function classifyTenantByMembershipRoot( target: TenantFilterTarget | undefined, tenants: T[], ) { const tenant = resolveTenantTarget(target, tenants); if (!tenant?.id) return undefined; const tenantById = new Map( tenants .filter((item) => item.id?.trim()) .map((item) => [item.id as string, item]), ); return USER_MEMBERSHIP_TENANT_TABS.find((tab) => { const root = resolveMembershipRoot(tab, tenants); if (!root?.id) return false; const resolvedTenant = tenantById.get(tenant.id ?? "") ?? tenant; return isInTenantSubtree(resolvedTenant, root.id, tenantById); }); } export function filterTenantsByMembershipRoot( tenants: T[], tabId: UserMembershipTenantTabId, ) { const tab = USER_MEMBERSHIP_TENANT_TABS.find((item) => item.id === tabId); if (!tab) return []; const root = resolveMembershipRoot(tab, tenants); if (!root?.id) return []; const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); return tenants.filter( (tenant) => !isSystemTenant(tenant) && isPublicRepresentativeTenant(tenant) && isInTenantSubtree(tenant, root.id as string, tenantById), ); } export function resolveUserMembershipTenantTab( user: HanmacFamilyUserTarget, tenants: T[], ) { const metadataAppointments = Array.isArray( user.metadata?.additionalAppointments, ) ? user.metadata.additionalAppointments .map((appointment) => appointment as TenantFilterTarget) .filter( (appointment) => typeof appointment.tenantId === "string" || typeof appointment.id === "string" || typeof appointment.tenantSlug === "string" || typeof appointment.slug === "string", ) .map((appointment) => ({ id: appointment.id ?? appointment.tenantId, slug: appointment.slug ?? appointment.tenantSlug, parentId: appointment.parentId, type: appointment.type, name: appointment.name ?? appointment.tenantName, })) : []; const tenantBySlug = new Map( tenants .filter((tenant) => tenant.slug?.trim()) .map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]), ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); const candidates = [ user.tenant, ...(user.joinedTenants ?? []), ...metadataAppointments, ...metadataAppointments.map((appointment) => tenantById.get(appointment.id ?? ""), ), tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""), ]; return ( USER_MEMBERSHIP_TENANT_TABS.find((tab) => candidates.some( (candidate) => classifyTenantByMembershipRoot(candidate, tenants)?.id === tab.id, ), ) ?? USER_MEMBERSHIP_TENANT_TABS[0] ); } function isGPDTDCTenant( target: TenantFilterTarget | undefined, tenants: T[], ) { const tenant = resolveTenantTarget(target, tenants); if (!tenant) return false; const tenantById = new Map( tenants .filter((item) => item.id?.trim()) .map((item) => [item.id as string, item]), ); let current: TenantFilterTarget | undefined = tenant; const visited = new Set(); while (current) { const slug = current.slug?.trim().toLowerCase(); if (slug === "gpdtdc") { return true; } const parentId = current.parentId ?? ""; if (!parentId || visited.has(parentId)) { return false; } visited.add(parentId); current = tenantById.get(parentId); } return false; } export function getTenantGradeOptions( target: TenantFilterTarget | undefined, tenants: T[], ) { return isGPDTDCTenant(target, tenants) ? [...GPDTDC_GRADE_OPTIONS] : [...HANMAC_FAMILY_GRADE_OPTIONS]; } function isPublicRepresentativeTenant(tenant: TenantFilterTarget) { const visibility = String( tenant.visibility ?? tenant.config?.visibility ?? "public", ) .trim() .toLowerCase(); return visibility !== "internal" && visibility !== "private"; } function isInTenantSubtree( tenant: T, rootTenantId: string, tenantById: Map, ) { if (!rootTenantId) { return false; } if (tenant.id === rootTenantId) { return true; } const visitedTenantIds = new Set(); let parentId = tenant.parentId ?? ""; while (parentId) { if (parentId === rootTenantId) { return true; } if (visitedTenantIds.has(parentId)) { return false; } visitedTenantIds.add(parentId); parentId = tenantById.get(parentId)?.parentId ?? ""; } return false; } function resolveHanmacFamilyTenantId( tenants: T[], hanmacFamilyTenantId?: string, ) { const envTenantId = hanmacFamilyTenantId?.trim(); if (envTenantId) return envTenantId; return ( tenants.find((tenant) => tenant.slug?.toLowerCase() === "hanmac-family") ?.id ?? "" ); } export function isHanmacFamilyTenant( tenant: T | undefined, tenants: T[], hanmacFamilyTenantId?: string, ) { if (!tenant?.id) return false; const rootTenantId = resolveHanmacFamilyTenantId( tenants, hanmacFamilyTenantId, ); if (!rootTenantId) return false; const tenantById = new Map( tenants .filter((item) => item.id?.trim()) .map((item) => [item.id as string, item]), ); const target = tenantById.get(tenant.id) ?? tenant; return isInTenantSubtree(target, rootTenantId, tenantById); } export function isHanmacFamilyUser( user: HanmacFamilyUserTarget, tenants: T[], hanmacFamilyTenantId?: string, ) { const metadataAppointments = Array.isArray( user.metadata?.additionalAppointments, ) ? user.metadata.additionalAppointments .map((appointment) => appointment as TenantFilterTarget) .filter( (appointment) => typeof appointment.tenantId === "string" || typeof appointment.id === "string" || typeof appointment.tenantSlug === "string" || typeof appointment.slug === "string", ) .map((appointment) => ({ id: appointment.id ?? appointment.tenantId, slug: appointment.slug ?? appointment.tenantSlug, parentId: appointment.parentId, type: appointment.type, name: appointment.name ?? appointment.tenantName, })) : []; const tenantBySlug = new Map( tenants .filter((tenant) => tenant.slug?.trim()) .map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]), ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); const tenantCandidates = [ user.tenant, ...(user.joinedTenants ?? []), ...metadataAppointments, ...metadataAppointments.map((appointment) => tenantById.get(appointment.id ?? ""), ), tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""), ]; return tenantCandidates.some((tenant) => isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId), ); } export function filterNonHanmacFamilyTenants( tenants: T[], hanmacFamilyTenantId?: string, ) { const rootTenantId = resolveHanmacFamilyTenantId( tenants, hanmacFamilyTenantId, ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); return tenants.filter( (tenant) => !isSystemTenant(tenant) && isPublicRepresentativeTenant(tenant) && !isInTenantSubtree(tenant, rootTenantId, tenantById), ); } export function buildOrgChartTenantPickerUrl( baseUrl?: string, options: OrgChartTenantPickerOptions = {}, ) { const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); const params = new URLSearchParams({ mode: "single", select: "tenant", width: "400", height: "600", }); const tenantId = options.tenantId?.trim(); if (tenantId) { params.set("tenantId", tenantId); } if (options.includeInternal) { params.set("includeInternal", "true"); } return `${normalizedBase}/embed/picker?${params.toString()}`; } export function buildAuthenticatedOrgChartTenantPickerUrl( baseUrl?: string, options: OrgChartTenantPickerOptions = {}, ) { const pickerUrl = buildOrgChartTenantPickerUrl("", { includeInternal: true, ...options, }); return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl }); } export function buildOrgChartUserMultiPickerUrl( baseUrl?: string, options: OrgChartUserMultiPickerOptions = {}, ) { const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); const params = new URLSearchParams({ mode: "multiple", select: "user", width: "720", height: "640", }); params.set("includeInternal", "true"); params.set("includeDescendants", "true"); params.set("showDescendantToggle", "true"); if (options.tenantId?.trim()) { params.set("tenantId", options.tenantId.trim()); } else { params.set("rootTenantId", "all"); } return `${normalizedBase}/embed/picker?${params.toString()}`; } export function buildAuthenticatedOrgChartUserMultiPickerUrl( baseUrl?: string, options: OrgChartUserMultiPickerOptions = {}, ) { const pickerUrl = buildOrgChartUserMultiPickerUrl("", options); return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl }); } export function buildAuthenticatedOrgChartUrl( baseUrl?: string, options: OrgChartLoginOptions = { includeInternal: false }, ) { const normalizedBase = baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_ORGFRONT_BASE_URL; let returnTo = options.returnTo?.trim() || "/chart"; if (options.includeInternal && returnTo.startsWith("/chart")) { const [path, query = ""] = returnTo.split("?", 2); const params = new URLSearchParams(query); params.set("includeInternal", "true"); returnTo = `${path}?${params.toString()}`; } const params = new URLSearchParams({ auto: "1", returnTo, }); return `${normalizedBase}/login?${params.toString()}`; } export function parseOrgChartTenantSelection( message: unknown, ): OrgChartTenantSelection | null { const data = message as OrgChartPickerMessage; if (data?.type !== "orgfront:picker:confirm") { return null; } const selection = data.payload?.selections?.[0]; if ( selection?.type !== "tenant" || typeof selection.id !== "string" || typeof selection.name !== "string" || selection.id.trim() === "" ) { return null; } return { id: selection.id, name: selection.name, }; } export function parseOrgChartUserSelections( message: unknown, ): OrgChartUserSelection[] { const data = message as OrgChartPickerMessage; if (data?.type !== "orgfront:picker:confirm") { return []; } return (data.payload?.selections ?? []) .filter( ( selection, ): selection is { type: "user"; id: string; name: string; email?: string; rootTenantName?: string; leafTenantName?: string; tenantName?: string; } => selection?.type === "user" && typeof selection.id === "string" && typeof selection.name === "string" && selection.id.trim() !== "", ) .map((selection) => ({ id: selection.id, name: selection.name, email: typeof selection.email === "string" ? selection.email : "", rootTenantName: typeof selection.rootTenantName === "string" ? selection.rootTenantName : undefined, leafTenantName: typeof selection.leafTenantName === "string" ? selection.leafTenantName : typeof selection.tenantName === "string" ? selection.tenantName : undefined, })); }