feat(org-chart): implement expandable layout and multi-company navigation

Integrate P5-1B through P5-4 shared Core layout, collapse, zoom/pan/fit, company roots and keyboard accessibility. Include React/HTML examples, virtual 20/100/300 employee fixtures, focused checks and the additive data contract.
This commit is contained in:
shkim4 committed 2026-10-12 21:59:09 +09:00
1 parent a92913d6bc
commit 064517ec46
30 files changed
+7532 -183

No files matched your search

+10 -6
View File
@@ -13,6 +13,8 @@ export interface EmployeeCardOptions {
cardAsset?: DepartmentCardAsset | RepresentativeCardAsset;
/** Full Composition presentation only; the employee dataset is untouched. */
workdeskPresentation?: boolean;
/** Auto content presentation, independent of Fixed SVG geometry. */
expandedPresentation?: boolean;
/** Compact read-only status groups already convey this label in their header. */
hideStatusLabel?: boolean;
}
@@ -31,15 +33,16 @@ export function createEmployeeCard(
const leader = options.kind !== 'employee';
const representative = options.kind === 'topLeader' || options.cardAsset?.size === 'representative';
const compactStatus = Boolean(options.workdeskPresentation && options.hideStatusLabel && !options.cardAsset);
const styledCard = Boolean(options.cardAsset) || compactStatus;
const presentationName = options.workdeskPresentation || representative;
const styledCard = Boolean(options.cardAsset) || compactStatus || options.expandedPresentation;
const presentationName = options.workdeskPresentation || (representative && !options.expandedPresentation);
const displayName = presentationName ? '홍길동' : employee.name;
const displayedName = presentationName ? '홍 길 동' : displayName;
const displayedName = presentationName ? '홍 길 동' : options.expandedPresentation && /^[가-힣]{3}$/u.test(displayName) ? [...displayName].join(' ') : displayName;
const card = document.createElement('div');
card.className = `hm-orgchart-employee${leader ? ' hm-orgchart-leader' : ''}`;
card.dataset.employeeId = employee.id;
card.dataset.cardKind = options.kind;
card.dataset.workStatus = employee.workStatus;
if (options.expandedPresentation) card.dataset.cardPresentation = 'expanded';
if (options.workdeskPresentation) card.dataset.workdeskPresentation = 'true';
if (compactStatus) card.dataset.cardLayout = 'compact';
if (options.cardAsset) {
@@ -81,14 +84,15 @@ export function createEmployeeCard(
const details = document.createElement('div');
details.className = 'hm-orgchart-employee-details';
const displayedPosition = representative ? '대표이사' : employee.position.replace(/\s+/gu, '');
const displayedPosition = representative && !options.expandedPresentation ? '대표이사' : employee.position.replace(/\s+/gu, '');
const position = text(document, 'span', 'hm-orgchart-person-role', displayedPosition);
const name = text(document, 'strong', 'hm-orgchart-person-name', displayedName);
if (presentationName) {
position.setAttribute('aria-label', representative ? displayedPosition : employee.position);
name.setAttribute('aria-label', displayName);
}
const inlineRoleBadge = styledCard && !representative;
if (options.expandedPresentation) position.setAttribute('aria-label', employee.position);
const inlineRoleBadge = styledCard && !representative && !options.expandedPresentation;
const positionRow = inlineRoleBadge ? document.createElement('div') : null;
if (positionRow) {
positionRow.className = 'hm-orgchart-position-row';
@@ -104,7 +108,7 @@ export function createEmployeeCard(
// Representative cards show only position/name in every consumer. Raw employee
// roles remain available to dialogs/statistics; other cards keep their policy.
const hideExecutiveBadge = options.workdeskPresentation && employee.jobTitle?.trim() === '경영진';
if (!representative && !hideExecutiveBadge && (employee.jobTitle || (leader && !options.cardAsset))) {
if (!representative && !options.expandedPresentation && !hideExecutiveBadge && (employee.jobTitle || (leader && !options.cardAsset))) {
const rawBadge = employee.jobTitle || '조직장';
const badge = text(document, 'span', 'hm-orgchart-job-title', options.workdeskPresentation ? rawBadge.replace(/\s+/gu, '') : rawBadge);
if (options.workdeskPresentation) badge.setAttribute('aria-label', employee.jobTitle || '조직장');