export type OrgContextMember = { id?: string; email: string; name: string; phone?: string; department?: string; grade?: string; position?: string; jobTitle?: string; isOwner?: boolean; isLeader?: boolean; isPrimary?: boolean; }; export type OrgContextTenant = { id: string; type: string; name: string; slug: string; parentId?: string | null; status: string; description: string; domains: string[]; memberCount: number; visibility: string; orgUnitType?: string; createdAt: string; updatedAt: string; members: OrgContextMember[]; }; export type OrgContextTreeNode = OrgContextTenant & { children: OrgContextTreeNode[]; }; export type OrgContextResponse = { schemaVersion: "baron.org-context.v1"; issuedAt: string; scope: { tenantId: string; tenantSlug: string; }; tree: OrgContextTreeNode; tenants: OrgContextTenant[]; }; export type OrgChartNode = OrgContextTenant & { children: OrgChartNode[]; depth: number; path: string[]; }; export type OrgChartMember = OrgContextMember & { tenantIds: string[]; }; export type OrgChartModel = { root: OrgChartNode; nodes: OrgChartNode[]; tenantsById: Map; tenantsBySlug: Map; membersByEmail: Map; response: OrgContextResponse; }; export type OrgContextClientOptions = { baseUrl: string; credentials?: { keyId: string; keySecret: string; }; fetch?: typeof fetch; headers?: Record; }; export type FetchOrgContextOptions = { tenantSlug?: string; includeUsers?: boolean; includeUserIds?: boolean; }; export type OrgPickerSelection = { id: string; name: string; type: "tenant" | "user"; }; export type OrgPickerVariant = "default" | "orgfront"; export type OrgPickerOptions = { mode?: "single" | "multiple"; selectable?: "tenant" | "user" | "both"; includeDescendants?: boolean; injectStyles?: boolean; showDescendantToggle?: boolean; variant?: OrgPickerVariant; onCancel?: () => void; onChange?: (selection: OrgPickerSelection[]) => void; onConfirm?: (selection: OrgPickerSelection[]) => void; }; export type OrgPickerController = { cancel: () => void; confirm: () => void; destroy: () => void; getSelection: () => OrgPickerSelection[]; }; const API_PATH = "/api/v1/integrations/org-context"; const DEFAULT_STYLE_ID = "baron-org-context-chart-default-style"; const DEFAULT_STYLE = ` .baron-org-chart,.baron-org-picker{box-sizing:border-box;color:#0f172a;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.45} .baron-org-chart *,.baron-org-picker *{box-sizing:border-box} .baron-org-chart__tree{display:flex;min-width:100%;gap:24px;overflow:auto;padding:16px} .baron-org-chart__node{min-width:220px;border:1px solid #d8dee9;border-radius:8px;background:#fff;box-shadow:0 10px 30px rgba(15,23,42,.08)} .baron-org-chart__title{margin:0;padding:10px 12px;border-bottom:1px solid #e5e7eb;background:#f8fafc;font-size:14px;font-weight:700} .baron-org-chart__meta{margin:0;padding:8px 12px;color:#64748b;font-size:12px} .baron-org-chart__members{margin:0;padding:0 12px 12px 28px;color:#334155;font-size:12px} .baron-org-chart__children{display:flex;gap:16px;margin:12px;padding:12px 0 0 16px;border-left:1px solid #d8dee9} .baron-org-picker{width:100%;max-width:520px;border:1px solid #d8dee9;border-radius:8px;background:#fff;box-shadow:0 12px 34px rgba(15,23,42,.1);overflow:hidden} .baron-org-picker__toolbar{display:flex;flex-direction:column;gap:10px;padding:12px;border-bottom:1px solid #e5e7eb;background:#f8fafc} .baron-org-picker__search-wrap{position:relative} .baron-org-picker__search-icon{pointer-events:none;position:absolute;left:12px;top:50%;width:16px;height:16px;transform:translateY(-50%);color:#64748b} .baron-org-picker__search-icon::before{content:"";position:absolute;left:2px;top:2px;width:8px;height:8px;border:2px solid currentColor;border-radius:999px} .baron-org-picker__search-icon::after{content:"";position:absolute;left:10px;top:11px;width:6px;height:2px;background:currentColor;border-radius:999px;transform:rotate(45deg);transform-origin:left center} .baron-org-picker__search{width:100%;height:38px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;padding:0 10px;color:#0f172a;font:inherit;outline:none} .baron-org-picker__search:focus{border-color:#24449c;box-shadow:0 0 0 3px rgba(36,68,156,.18)} .baron-org-picker__controls{display:flex;align-items:center;justify-content:space-between;gap:12px;color:#475569;font-size:12px} .baron-org-picker__descendants{display:inline-flex;align-items:center;gap:6px;white-space:nowrap} .baron-org-picker__summary{color:#64748b} .baron-org-picker__clear{border:0;background:transparent;color:#24449c;cursor:pointer;font:inherit;font-weight:700;padding:2px 0} .baron-org-picker__clear:hover{text-decoration:underline} .baron-org-picker__list,.baron-org-picker__children{list-style:none;margin:0;padding:0} .baron-org-picker__list{max-height:420px;overflow:auto;padding:8px} .baron-org-picker__item{margin:0} .baron-org-picker__children{margin-left:8px;border-left:1px solid #e5e7eb} .baron-org-picker__row{display:flex;min-height:32px;align-items:center;gap:8px;border-radius:6px;padding:4px 8px;color:#0f172a;cursor:pointer} .baron-org-picker__row:hover{background:#f1f5f9} .baron-org-picker__row--selected{background:#e8eefc;color:#18327a} .baron-org-picker__row--member{color:#334155;font-size:13px} .baron-org-picker__label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .baron-org-picker__label-primary,.baron-org-picker__label-secondary{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .baron-org-picker__label-primary{font-weight:600;line-height:20px} .baron-org-picker__label-secondary{color:#64748b;font-size:12px;line-height:20px} .baron-org-picker input[type="checkbox"],.baron-org-picker input[type="radio"]{accent-color:#24449c} .baron-org-picker__empty{padding:28px 12px;color:#64748b;text-align:center} .baron-org-picker__toggle,.baron-org-picker__toggle-placeholder{display:grid;width:24px;height:24px;flex:0 0 24px;place-items:center;border:0;border-radius:4px;background:transparent;color:#64748b;font:inherit;line-height:1} .baron-org-picker__toggle{cursor:pointer} .baron-org-picker__toggle:hover{background:#e2e8f0} .baron-org-picker__chevron{position:relative;display:block;width:16px;height:16px;color:currentColor} .baron-org-picker__chevron::before{content:"";position:absolute;width:6px;height:6px;border-right:2px solid currentColor;border-bottom:2px solid currentColor} .baron-org-picker__chevron--open::before{left:4px;top:3px;transform:rotate(45deg)} .baron-org-picker__chevron--closed::before{left:3px;top:4px;transform:rotate(-45deg)} .baron-org-picker__select{min-width:0;flex:1;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;outline:none;padding:0 4px} .baron-org-picker__select:focus-visible{box-shadow:0 0 0 2px #3a98e5} .baron-org-picker__footer{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid #e5e7eb;background:#fff;padding:8px 12px} .baron-org-picker__actions{display:flex;align-items:center;gap:8px} .baron-org-picker__button{display:inline-flex;height:36px;align-items:center;justify-content:center;gap:8px;white-space:nowrap;border-radius:6px;border:1px solid #e5e7eb;background:#fff;color:#0f172a;padding:0 12px;font:inherit;font-size:14px;font-weight:600;cursor:pointer} .baron-org-picker__button:hover{background:#f4b840;color:#0f172a} .baron-org-picker__button--primary{border-color:#3a98e5;background:#3a98e5;color:#fff;box-shadow:0 1px 2px rgba(15,23,42,.12)} .baron-org-picker__button--primary:hover{background:#2588d8;color:#fff} .baron-org-picker__button:disabled{cursor:not-allowed;opacity:.5} .baron-org-picker--orgfront{--boc-background:hsl(var(--background,0 0% 98%));--boc-foreground:hsl(var(--foreground,223 25% 12%));--boc-primary:hsl(var(--primary,209 79% 52%));--boc-secondary:hsl(var(--secondary,220 17% 94%));--boc-muted-foreground:hsl(var(--muted-foreground,223 15% 45%));--boc-accent:hsl(var(--accent,40 96% 62%));--boc-accent-foreground:hsl(var(--accent-foreground,223 25% 12%));--boc-border:hsl(var(--border,220 17% 90%));--boc-input:hsl(var(--input,220 17% 90%));--boc-ring:hsl(var(--ring,209 79% 52%));display:flex;height:100%;min-height:320px;max-width:none;flex-direction:column;border:0;border-radius:0;background:var(--boc-background);box-shadow:none;color:var(--boc-foreground);overflow:hidden} .baron-org-picker--orgfront .baron-org-picker__toolbar{display:block;border-bottom:1px solid var(--boc-border);background:var(--boc-background);padding:8px} .baron-org-picker--orgfront .baron-org-picker__toolbar-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:8px} .baron-org-picker--orgfront .baron-org-picker__search{height:36px;border-color:var(--boc-input);border-radius:6px;background:var(--boc-background);padding:0 12px 0 36px;font-size:14px} .baron-org-picker--orgfront .baron-org-picker__search:focus{border-color:var(--boc-input);box-shadow:0 0 0 2px var(--boc-ring)} .baron-org-picker--orgfront .baron-org-picker__controls{height:36px;font-size:14px} .baron-org-picker--orgfront .baron-org-picker__descendants{height:36px;font-size:14px} .baron-org-picker--orgfront .baron-org-picker__list{min-height:0;flex:1;max-height:none;overflow:auto;padding:12px} .baron-org-picker--orgfront .baron-org-picker__children{margin-left:16px;border-left:0} .baron-org-picker--orgfront .baron-org-picker__row{min-height:28px;gap:6px;border-radius:4px;padding:2px 6px 2px 4px;transition:background-color .15s,color .15s,box-shadow .15s} .baron-org-picker--orgfront .baron-org-picker__row:hover{background:color-mix(in srgb,var(--boc-secondary) 50%,transparent)} .baron-org-picker--orgfront .baron-org-picker__row--selected{background:color-mix(in srgb,var(--boc-primary) 15%,transparent);box-shadow:0 0 0 2px color-mix(in srgb,var(--boc-primary) 60%,transparent);color:var(--boc-foreground)} .baron-org-picker--orgfront .baron-org-picker__row--member{font-size:14px;color:var(--boc-foreground)} .baron-org-picker--orgfront .baron-org-picker__footer{border-top-color:var(--boc-border);background:var(--boc-background)} .baron-org-picker--orgfront .baron-org-picker__summary{font-size:14px;color:var(--boc-muted-foreground)} .baron-org-picker--orgfront .baron-org-picker__button{border-color:var(--boc-input);background:var(--boc-background);color:var(--boc-foreground)} .baron-org-picker--orgfront .baron-org-picker__button:hover{background:var(--boc-accent);color:var(--boc-accent-foreground)} .baron-org-picker--orgfront .baron-org-picker__button--primary{border-color:var(--boc-primary);background:var(--boc-primary);color:#fff} .baron-org-picker--orgfront .baron-org-picker__empty{margin:12px;min-height:160px;border:1px dashed var(--boc-border);border-radius:6px;background:var(--boc-background);display:grid;place-items:center} `; export function createOrgContextClient(options: OrgContextClientOptions) { const fetcher = options.fetch ?? globalThis.fetch; if (!fetcher) { throw new Error("A fetch implementation is required."); } return { async fetchOrgContext( query: FetchOrgContextOptions = {}, ): Promise { const url = new URL(API_PATH, normalizeBaseUrl(options.baseUrl)); appendQuery(url, "tenantSlug", query.tenantSlug); appendQuery(url, "includeUsers", query.includeUsers); appendQuery(url, "includeUserIds", query.includeUserIds); const headers: Record = { Accept: "application/json", ...options.headers, }; if (options.credentials) { headers["X-Baron-Key-ID"] = options.credentials.keyId; headers["X-Baron-Key-Secret"] = options.credentials.keySecret; } const response = await fetcher(url.toString(), { headers }); if (!response.ok) { throw new Error(`Org context request failed with ${response.status}`); } const payload = (await response.json()) as OrgContextResponse; if (payload.schemaVersion !== "baron.org-context.v1") { throw new Error( `Unsupported org context schema: ${payload.schemaVersion}`, ); } return payload; }, }; } export function buildOrgChartModel( response: OrgContextResponse, ): OrgChartModel { if (response.schemaVersion !== "baron.org-context.v1") { throw new Error( `Unsupported org context schema: ${response.schemaVersion}`, ); } const nodes: OrgChartNode[] = []; const tenantsById = new Map(); const tenantsBySlug = new Map(); const membersByEmail = new Map(); const visit = ( node: OrgContextTreeNode, depth: number, ancestorPath: string[], ): OrgChartNode => { const path = [...ancestorPath, node.id]; const chartNode: OrgChartNode = { ...node, children: [], depth, path, }; nodes.push(chartNode); tenantsById.set(chartNode.id, chartNode); tenantsBySlug.set(chartNode.slug, chartNode); for (const member of chartNode.members) { const key = member.email.toLowerCase(); const existing = membersByEmail.get(key); if (existing) { existing.tenantIds.push(chartNode.id); } else { membersByEmail.set(key, { ...member, tenantIds: [chartNode.id], }); } } chartNode.children = node.children.map((child) => visit(child, depth + 1, path), ); return chartNode; }; return { root: visit(response.tree, 0, []), nodes, tenantsById, tenantsBySlug, membersByEmail, response, }; } export function renderOrgChart( container: HTMLElement, model: OrgChartModel, ): { destroy: () => void } { ensureDefaultStyles(); container.replaceChildren(); container.classList.add("baron-org-chart"); const root = document.createElement("div"); root.className = "baron-org-chart__tree"; root.append(renderChartNode(model.root)); container.append(root); return { destroy() { container.replaceChildren(); container.classList.remove("baron-org-chart"); }, }; } export function renderOrgPicker( container: HTMLElement, model: OrgChartModel, options: OrgPickerOptions = {}, ): OrgPickerController { if (options.injectStyles !== false) { ensureDefaultStyles(); } const mode = options.mode ?? "single"; const selectable = options.selectable ?? "tenant"; const variant = options.variant ?? "default"; const isOrgfront = variant === "orgfront"; let includeDescendants = options.includeDescendants ?? (isOrgfront && mode === "multiple"); let searchQuery = ""; const selected = new Map(); const expanded = new Set(model.nodes.map((node) => node.id)); const showDescendantToggle = options.showDescendantToggle ?? true; const currentSelection = () => Array.from(selected.values()); const emitChange = () => { const selection = currentSelection(); options.onChange?.(selection); container.dispatchEvent( new CustomEvent("baron-org-picker-change", { bubbles: true, detail: { selection }, }), ); }; const emitConfirm = () => { const selection = currentSelection(); options.onConfirm?.(selection); container.dispatchEvent( new CustomEvent("baron-org-picker-confirm", { bubbles: true, detail: { selection }, }), ); }; const emitCancel = () => { options.onCancel?.(); container.dispatchEvent( new CustomEvent("baron-org-picker-cancel", { bubbles: true, }), ); }; const toggleSelection = ( selection: OrgPickerSelection, checked: boolean, descendants: OrgPickerSelection[], ) => { if (mode === "single") { selected.clear(); if (checked) { selected.set(selectionKey(selection), selection); } emitChange(); rerender(); return; } const targets = includeDescendants && selection.type === "tenant" ? [selection, ...descendants] : [selection]; for (const target of targets) { if (checked) { selected.set(selectionKey(target), target); } else { selected.delete(selectionKey(target)); } } emitChange(); rerender(); }; const renderPickerNode = (node: OrgChartNode): HTMLElement => { const item = document.createElement("li"); item.className = "baron-org-picker__item"; const hasChildren = node.children.length > 0; const tenantSelection: OrgPickerSelection = { id: node.id, name: node.name, type: "tenant", }; const row = document.createElement(isOrgfront ? "div" : "label"); row.className = "baron-org-picker__row"; if (selected.has(selectionKey(tenantSelection))) { row.classList.add("baron-org-picker__row--selected"); } row.style.paddingLeft = `${node.depth * 16}px`; if (isOrgfront) { row.append(createExpandToggle(node, hasChildren)); appendOrgfrontSelectionControl(row, node, tenantSelection); } else { if (selectable === "tenant" || selectable === "both") { row.append( createPickerInput({ mode, selection: tenantSelection, selected, onToggle: (checked) => toggleSelection( tenantSelection, checked, collectDescendantSelections(node, selectable), ), }), ); } row.append(createLabelText(node.name, node.type)); } item.append(row); if (selectable === "user" || selectable === "both") { for (const member of node.members) { item.append( renderMemberPickerRow( member, node, mode, selected, (value) => toggleSelection(value, !selected.has(selectionKey(value)), []), isOrgfront, ), ); } } if (hasChildren && (!isOrgfront || expanded.has(node.id))) { const children = document.createElement("ul"); children.className = "baron-org-picker__children"; for (const child of node.children) { children.append(renderPickerNode(child)); } item.append(children); } return item; }; const appendOrgfrontSelectionControl = ( row: HTMLElement, node: OrgChartNode, tenantSelection: OrgPickerSelection, ) => { const canSelect = selectable === "tenant" || selectable === "both"; if (!canSelect) { row.append(createOrgfrontLabelText(node.name)); return; } if (mode === "multiple") { row.append( createPickerInput({ mode, selection: tenantSelection, selected, onToggle: (checked) => toggleSelection( tenantSelection, checked, collectDescendantSelections(node, selectable), ), }), ); row.append(createOrgfrontLabelText(node.name)); return; } const button = document.createElement("button"); button.className = "baron-org-picker__select"; button.dataset.baronOrgPickerValue = selectionKey(tenantSelection); button.type = "button"; button.ariaPressed = String(selected.has(selectionKey(tenantSelection))); button.addEventListener("click", () => toggleSelection( tenantSelection, true, collectDescendantSelections(node, selectable), ), ); button.append(createOrgfrontLabelText(node.name)); row.append(button); }; const createExpandToggle = (node: OrgChartNode, hasChildren: boolean) => { if (!hasChildren) { const placeholder = document.createElement("span"); placeholder.className = "baron-org-picker__toggle-placeholder"; placeholder.ariaHidden = "true"; return placeholder; } const toggle = document.createElement("button"); toggle.className = "baron-org-picker__toggle"; toggle.dataset.baronOrgPickerToggle = selectionKey({ id: node.id, name: node.name, type: "tenant", }); toggle.type = "button"; const chevron = document.createElement("span"); chevron.className = expanded.has(node.id) ? "baron-org-picker__chevron baron-org-picker__chevron--open" : "baron-org-picker__chevron baron-org-picker__chevron--closed"; chevron.dataset.baronOrgPickerChevron = "true"; chevron.ariaHidden = "true"; toggle.append(chevron); toggle.ariaLabel = `${node.name} ${expanded.has(node.id) ? "접기" : "펼치기"}`; toggle.addEventListener("click", () => { if (expanded.has(node.id)) { expanded.delete(node.id); } else { expanded.add(node.id); } rerender(); }); return toggle; }; const rerender = () => { container.replaceChildren(); container.classList.add("baron-org-picker"); container.classList.toggle("baron-org-picker--orgfront", isOrgfront); container.append(renderPickerToolbar()); const visibleRoot = filterOrgChartNode(model.root, searchQuery, selectable); if (!visibleRoot) { const empty = document.createElement("div"); empty.className = "baron-org-picker__empty"; empty.textContent = isOrgfront ? "검색 결과가 없습니다." : "No matching organization or member."; container.append(empty); if (isOrgfront) { container.append(renderPickerFooter()); } return; } const list = document.createElement("ul"); list.className = "baron-org-picker__list"; list.append(renderPickerNode(visibleRoot)); container.append(list); if (isOrgfront) { container.append(renderPickerFooter()); } }; const renderPickerToolbar = () => { const toolbar = document.createElement("div"); toolbar.className = "baron-org-picker__toolbar"; const toolbarContent = isOrgfront ? document.createElement("div") : toolbar; if (isOrgfront) { toolbarContent.className = "baron-org-picker__toolbar-grid"; toolbar.append(toolbarContent); } const searchWrap = document.createElement("div"); searchWrap.className = "baron-org-picker__search-wrap"; if (isOrgfront) { const searchIcon = document.createElement("span"); searchIcon.className = "baron-org-picker__search-icon"; searchIcon.dataset.baronOrgPickerSearchIcon = "true"; searchIcon.ariaHidden = "true"; searchWrap.append(searchIcon); } const search = document.createElement("input"); search.className = "baron-org-picker__search"; search.dataset.baronOrgPickerSearch = "true"; search.placeholder = isOrgfront ? "ID, 이름, 이메일, 메타데이터" : "Search organization or member"; search.type = "search"; search.value = searchQuery; search.addEventListener("input", () => { searchQuery = search.value; rerender(); const nextSearch = container.querySelector( "[data-baron-org-picker-search]", ); nextSearch?.focus(); nextSearch?.setSelectionRange(searchQuery.length, searchQuery.length); }); searchWrap.append(search); toolbarContent.append(searchWrap); const controls = document.createElement("div"); controls.className = "baron-org-picker__controls"; if (mode === "multiple" && selectable !== "user" && showDescendantToggle) { const descendantsLabel = document.createElement("label"); descendantsLabel.className = "baron-org-picker__descendants"; const descendants = document.createElement("input"); descendants.dataset.baronOrgPickerDescendants = "true"; descendants.type = "checkbox"; descendants.checked = includeDescendants; const updateDescendantSelection = () => { includeDescendants = descendants.checked; rerender(); }; descendants.addEventListener("change", updateDescendantSelection); descendants.addEventListener("click", updateDescendantSelection); descendantsLabel.append( descendants, isOrgfront ? "하위 선택" : "Include descendants", ); controls.append(descendantsLabel); } else if (!isOrgfront) { controls.append(document.createElement("span")); } if (!isOrgfront) { const summary = document.createElement("span"); summary.className = "baron-org-picker__summary"; summary.dataset.baronOrgPickerSummary = "true"; summary.textContent = `${selected.size} selected`; controls.append(summary); if (selected.size > 0) { const clear = document.createElement("button"); clear.className = "baron-org-picker__clear"; clear.type = "button"; clear.textContent = "Clear"; clear.addEventListener("click", () => { selected.clear(); emitChange(); rerender(); }); controls.append(clear); } } toolbarContent.append(controls); return toolbar; }; const renderPickerFooter = () => { const footer = document.createElement("footer"); footer.className = "baron-org-picker__footer"; footer.dataset.baronOrgPickerFooter = "true"; const summary = document.createElement("div"); summary.className = "baron-org-picker__summary"; summary.dataset.baronOrgPickerSummary = "true"; summary.textContent = selected.size > 0 ? `${selected.size}개 항목 선택됨` : "선택된 항목이 없습니다."; footer.append(summary); const actions = document.createElement("div"); actions.className = "baron-org-picker__actions"; const cancel = document.createElement("button"); cancel.className = "baron-org-picker__button"; cancel.dataset.baronOrgPickerCancel = "true"; cancel.type = "button"; cancel.textContent = "취소"; cancel.addEventListener("click", emitCancel); actions.append(cancel); const confirm = document.createElement("button"); confirm.className = "baron-org-picker__button baron-org-picker__button--primary"; confirm.dataset.baronOrgPickerConfirm = "true"; confirm.disabled = selected.size === 0; confirm.type = "button"; confirm.textContent = "선택 완료"; confirm.addEventListener("click", emitConfirm); actions.append(confirm); footer.append(actions); return footer; }; rerender(); return { cancel: emitCancel, confirm: emitConfirm, destroy() { container.replaceChildren(); container.classList.remove( "baron-org-picker", "baron-org-picker--orgfront", ); selected.clear(); }, getSelection() { return currentSelection(); }, }; } function appendQuery( url: URL, key: string, value: string | boolean | undefined, ) { if (value !== undefined) { url.searchParams.set(key, String(value)); } } function normalizeBaseUrl(baseUrl: string) { return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; } function ensureDefaultStyles() { if (typeof document === "undefined") return; if (document.getElementById(DEFAULT_STYLE_ID)) return; const style = document.createElement("style"); style.id = DEFAULT_STYLE_ID; style.dataset.baronOrgContextChartStyle = "default"; style.textContent = DEFAULT_STYLE; document.head.append(style); } function renderChartNode(node: OrgChartNode): HTMLElement { const item = document.createElement("section"); item.className = "baron-org-chart__node"; item.dataset.baronOrgNode = node.id; const title = document.createElement("h3"); title.className = "baron-org-chart__title"; title.textContent = node.name; item.append(title); const meta = document.createElement("p"); meta.className = "baron-org-chart__meta"; meta.textContent = [node.type, node.orgUnitType, node.visibility] .filter(Boolean) .join(" · "); item.append(meta); if (node.members.length > 0) { const memberList = document.createElement("ul"); memberList.className = "baron-org-chart__members"; for (const member of node.members) { const memberItem = document.createElement("li"); memberItem.textContent = formatMember(member); memberList.append(memberItem); } item.append(memberList); } if (node.children.length > 0) { const children = document.createElement("div"); children.className = "baron-org-chart__children"; for (const child of node.children) { children.append(renderChartNode(child)); } item.append(children); } return item; } function renderMemberPickerRow( member: OrgContextMember, node: OrgChartNode, mode: "single" | "multiple", selected: Map, onSelect: (selection: OrgPickerSelection) => void, isOrgfront = false, ) { const selection: OrgPickerSelection = { id: member.id || `${node.id}:${member.email}`, name: member.name, type: "user", }; const row = document.createElement(isOrgfront ? "div" : "label"); row.className = "baron-org-picker__row baron-org-picker__row--member"; if (selected.has(selectionKey(selection))) { row.classList.add("baron-org-picker__row--selected"); } row.style.paddingLeft = `${node.depth * 16 + 24}px`; if (isOrgfront && mode === "single") { row.append(createOrgfrontMemberSpacer()); const button = document.createElement("button"); button.className = "baron-org-picker__select"; button.dataset.baronOrgPickerValue = selectionKey(selection); button.type = "button"; button.ariaPressed = String(selected.has(selectionKey(selection))); button.addEventListener("click", () => onSelect(selection)); button.append(createOrgfrontLabelText(member.name, member.email)); row.append(button); return row; } if (isOrgfront) { row.append(createOrgfrontMemberSpacer()); } row.append( createPickerInput({ mode, selection, selected, onToggle: () => onSelect(selection), }), ); row.append( isOrgfront ? createOrgfrontLabelText(member.name, member.email) : createLabelText(member.name, member.email), ); return row; } function createPickerInput({ mode, selection, selected, onToggle, }: { mode: "single" | "multiple"; selection: OrgPickerSelection; selected: Map; onToggle: (checked: boolean) => void; }) { const input = document.createElement("input"); input.type = mode === "single" ? "radio" : "checkbox"; input.name = "baron-org-picker"; input.value = selectionKey(selection); input.checked = selected.has(selectionKey(selection)); const handleToggle = () => { onToggle(mode === "single" ? true : !selected.has(selectionKey(selection))); }; input.addEventListener("click", handleToggle); return input; } function createLabelText(primary: string, secondary?: string) { const text = document.createElement("span"); text.className = "baron-org-picker__label"; text.textContent = secondary ? `${primary} (${secondary})` : primary; return text; } function createOrgfrontMemberSpacer() { const spacer = document.createElement("span"); spacer.className = "baron-org-picker__toggle-placeholder"; spacer.ariaHidden = "true"; return spacer; } function createOrgfrontLabelText(primary: string, secondary?: string) { const text = document.createElement("span"); text.className = "baron-org-picker__label"; const primaryText = document.createElement("span"); primaryText.className = "baron-org-picker__label-primary"; primaryText.textContent = primary; text.append(primaryText); if (secondary) { const secondaryText = document.createElement("span"); secondaryText.className = "baron-org-picker__label-secondary"; secondaryText.textContent = secondary; text.append(secondaryText); } return text; } function filterOrgChartNode( node: OrgChartNode, rawQuery: string, selectable: "tenant" | "user" | "both", ): OrgChartNode | null { const query = rawQuery.trim().toLowerCase(); if (!query) return node; const childMatches = node.children .map((child) => filterOrgChartNode(child, rawQuery, selectable)) .filter((child): child is OrgChartNode => Boolean(child)); const tenantMatch = orgTenantMatchesSearch(node, query, selectable); const matchingMembers = orgMemberMatchesSearch(node, query, selectable); if ( !tenantMatch && matchingMembers.length === 0 && childMatches.length === 0 ) { return null; } return { ...node, members: tenantMatch ? node.members : matchingMembers, children: childMatches, }; } function orgTenantMatchesSearch( node: OrgChartNode, query: string, selectable: "tenant" | "user" | "both", ) { const tenantValues = [ node.id, node.name, node.slug, node.type, node.orgUnitType ?? "", node.visibility, ...node.domains, ]; if ( selectable !== "user" && tenantValues.some((value) => value.toLowerCase().includes(query)) ) { return true; } if (selectable === "tenant") { return false; } return false; } function orgMemberMatchesSearch( node: OrgChartNode, query: string, selectable: "tenant" | "user" | "both", ) { if (selectable === "tenant") { return []; } return node.members.filter((member) => [ member.id ?? "", member.email, member.name, member.department ?? "", member.grade ?? "", member.position ?? "", member.jobTitle ?? "", ].some((value) => value.toLowerCase().includes(query)), ); } function collectDescendantSelections( node: OrgChartNode, selectable: "tenant" | "user" | "both", ): OrgPickerSelection[] { const selections: OrgPickerSelection[] = []; const visit = (child: OrgChartNode) => { if (selectable === "tenant" || selectable === "both") { selections.push({ id: child.id, name: child.name, type: "tenant" }); } if (selectable === "user" || selectable === "both") { for (const member of child.members) { selections.push({ id: member.id || `${child.id}:${member.email}`, name: member.name, type: "user", }); } } for (const grandchild of child.children) { visit(grandchild); } }; for (const child of node.children) { visit(child); } return selections; } function selectionKey(selection: OrgPickerSelection) { return `${selection.type}:${selection.id}`; } function formatMember(member: OrgContextMember) { return [ member.name, member.position, member.jobTitle, member.isLeader || member.isOwner ? "조직장" : "", ] .filter(Boolean) .join(" · "); }