feat(org-chart): add core cards and fixed-layout integration

This commit is contained in:
shkim4 committed 2026-10-12 17:58:30 +09:00
1 parent d18e3c3380
commit 61c6f8d1f1
161 files changed
+13044 -1

No files matched your search

@@ -0,0 +1,133 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { selectAffiliationSummary, selectMajorOrganizationTotals, selectOrganizationPositionCounts, selectStatusGroups } from './content-selectors.js';
import { calculateSummary } from './summary.js';
import type { OrgChartDataset } from './types.js';
function fixture(): OrgChartDataset {
return {
schemaVersion: '0.1-draft',
organizations: [
{ id: 'root', parentId: null, name: '가상 본부', type: 'root', leaderId: 'leader', displayOrder: 0 },
{ id: 'team', parentId: 'root', name: '가상 팀', type: 'team', leaderId: null, displayOrder: 1 },
{ id: 'empty', parentId: 'root', name: '빈 가상 팀', type: 'team', leaderId: null, displayOrder: 2 },
],
employees: [
{ id: 'leader', organizationId: 'root', affiliationId: 'first', name: '가상 가', position: '대표', jobTitle: '본부장', profileImage: null, workStatus: 'active' },
{ id: 'sent', organizationId: 'team', affiliationId: 'second', name: '가상 나', position: '책임', jobTitle: null, profileImage: null, workStatus: 'dispatched' },
{ id: 'leave', organizationId: 'team', affiliationId: 'first', name: '가상 다', position: '책임', jobTitle: null, profileImage: null, workStatus: 'onLeave' },
],
affiliations: [{ id: 'first', name: '가상 관계사 가' }, { id: 'second', name: '가상 관계사 나' }, { id: 'zero', name: '가상 관계사 다' }],
metadata: { asOfDate: '2026-10-12', dataVersion: 'selector-test' },
};
}
test('position matrix uses direct primary membership, includes leaders and all work statuses', () => {
const data = fixture();
const matrix = selectOrganizationPositionCounts(data);
const summary = calculateSummary(data);
assert.deepEqual([...matrix.get('root')!], [['대표', 1]]);
assert.deepEqual([...matrix.get('team')!], [['책임', 2]]);
assert.equal(matrix.get('empty')!.size, 0);
assert.equal(matrix.get('root')!.get('책임'), undefined);
assert.equal(summary.raw.byOrganization.root!.inclusive, 3);
for (const organization of data.organizations) {
assert.equal([...matrix.get(organization.id)!.values()].reduce((sum, count) => sum + count, 0), summary.raw.byOrganization[organization.id]!.direct);
}
for (const [position, count] of Object.entries(summary.raw.byPosition)) {
assert.equal([...matrix.values()].reduce((sum, row) => sum + (row.get(position) ?? 0), 0), count);
}
assert.equal([...matrix.values()].flatMap(row => [...row.values()]).reduce((sum, count) => sum + count, 0), summary.raw.total);
assert.equal(summary.aggregation.status, 'unavailable');
});
test('duplicate IDs cannot inflate matrix or status groups and original Employee references survive', () => {
const data = fixture();
data.employees.push({ ...data.employees[1]!, organizationId: 'root', position: '대표' });
const summary = calculateSummary(data);
const matrix = selectOrganizationPositionCounts(data);
assert.equal(matrix.get('team')!.get('책임'), 2);
assert.equal(matrix.get('root')!.get('대표'), 1);
const groups = selectStatusGroups(data, summary);
assert.equal(groups[0]!.employees.length, 1);
assert.strictEqual(groups[0]!.employees[0], data.employees[1]);
assert.strictEqual(groups[1]!.employees[0], data.employees[2]);
assert.equal(summary.raw.total, 3);
});
test('opaque prototype-like organization IDs and position strings remain ordinary matrix keys', () => {
const data = fixture();
data.organizations[1]!.id = '__proto__';
data.employees[1]!.organizationId = '__proto__';
data.employees[2]!.organizationId = '__proto__';
data.employees[1]!.position = 'constructor';
data.employees[2]!.position = '__proto__';
const row = selectOrganizationPositionCounts(data).get('__proto__')!;
assert.equal(row.get('constructor'), 1);
assert.equal(row.get('__proto__'), 1);
});
test('affiliation summary preserves declared order, zero entries and original affiliation objects', () => {
const data = fixture();
const summary = calculateSummary(data);
const rows = selectAffiliationSummary(data, summary);
assert.deepEqual(rows.map(row => [row.affiliation.id, row.count]), [['first', 2], ['second', 1], ['zero', 0]]);
assert.strictEqual(rows[2]!.affiliation, data.affiliations[2]);
assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total);
});
test('status flags independently hide groups without changing Raw Count or affiliation totals', () => {
const data = fixture();
const summary = calculateSummary(data);
data.presentation = {
title: '가상 조직도', description: null, hubText: null, organizationOrder: [],
groups: { showEmployees: false, showSummary: false, showAffiliations: false, showDispatched: false, showOnLeave: true },
};
assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['onLeave']);
data.presentation.groups.showDispatched = true;
data.presentation.groups.showOnLeave = false;
assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['dispatched']);
data.presentation.groups.showDispatched = false;
assert.deepEqual(selectStatusGroups(data, summary), []);
assert.deepEqual(calculateSummary(data), summary);
assert.equal(selectAffiliationSummary(data, summary).length, 3);
});
test('zero employees retains all organization/affiliation rows and both empty status groups', () => {
const data = fixture();
data.employees = [];
data.organizations[0]!.leaderId = null;
const summary = calculateSummary(data);
assert.equal(selectOrganizationPositionCounts(data).size, 3);
assert.ok([...selectOrganizationPositionCounts(data).values()].every(row => row.size === 0));
assert.ok(selectAffiliationSummary(data, summary).every(row => row.count === 0));
assert.deepEqual(selectStatusGroups(data, summary).map(group => [group.status, group.count, group.employees.length]), [['dispatched', 0, 0], ['onLeave', 0, 0]]);
});
test('new JSON snapshot updates matrix, affiliations and status groups without a cached count', () => {
const data = fixture();
const before = calculateSummary(data);
data.employees[1]!.organizationId = 'root';
data.employees[1]!.position = '수석';
data.employees[1]!.affiliationId = 'zero';
data.employees[1]!.workStatus = 'active';
const after = calculateSummary(data);
const matrix = selectOrganizationPositionCounts(data);
assert.equal(matrix.get('root')!.get('수석'), 1);
assert.equal(matrix.get('team')!.get('책임'), 1);
assert.deepEqual(selectAffiliationSummary(data, after).map(row => row.count), [2, 0, 1]);
assert.equal(selectStatusGroups(data, after)[0]!.employees.length, 0);
assert.equal(before.raw.byStatus.dispatched, 1);
assert.equal(after.raw.total, before.raw.total);
});
test('major organization totals: root direct members then inclusive root children, summing to the raw total', () => {
const data = fixture();
data.organizations.push({ id: 'cell', parentId: 'team', name: '가상 셀', type: 'cell', leaderId: null, displayOrder: 3 });
data.employees.push({ id: 'member', organizationId: 'cell', affiliationId: 'zero', name: '가상 라', position: '선임', jobTitle: null, profileImage: null, workStatus: 'active' });
const summary = calculateSummary(data);
const rows = selectMajorOrganizationTotals(data, summary);
assert.deepEqual(rows.map(row => [row.organization.id, row.scope, row.count]), [['root', 'direct', 1], ['team', 'inclusive', 3], ['empty', 'inclusive', 0]]);
assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total);
assert.equal(rows.some(row => row.organization.id === 'cell'), false);
});