fix: align org chart hierarchy and typography weights

This commit is contained in:
shkim4 committed 2026-10-14 13:33:59 +09:00
1 parent 72d427fcd2
commit d42466f5eb
33 files changed
+358 -85

No files matched your search

@@ -23,10 +23,12 @@ for (const [size, count, companyCount, depth] of [['small', 20, 3, 2], ['medium'
if (count > 20) division.leaderId = addEmployee(division, '본부장');
const office = org(`office-${branch}`, division.id, `가상 ${branch + 1}실`, 'office', 0);
if (depth >= 3) {
const cell = org(`cell-${branch}`, office.id, `가상 ${branch + 1}셀`, 'cell', 0);
// Keep record order/IDs and weighted employee placement stable. Large
// examples use office -> team -> cell; no team may descend from a cell.
org(`cell-${branch}`, depth >= 4 ? `${companyId}-team-${branch}` : office.id, `가상 ${branch + 1}셀`, 'cell', 0);
if (depth >= 4) {
org(`team-${branch}`, cell.id, `가상 ${branch + 1}팀`, 'team', 0);
if (branch === 0) org('team-sibling', cell.id, '가상 협력팀', 'team', 1);
org(`team-${branch}`, office.id, `가상 ${branch + 1}팀`, 'team', 0);
if (branch === 0) org('team-sibling', office.id, '가상 협력팀', 'team', 1);
}
}
if (count > 20 && branch === 0) org('office-sibling', division.id, '가상 계획실', 'office', 1);
+35 -35
View File
@@ -31,7 +31,7 @@
{
"id": "hanmac-cell-0",
"companyId": "hanmac",
"parentId": "hanmac-office-0",
"parentId": "hanmac-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -40,7 +40,7 @@
{
"id": "hanmac-team-0",
"companyId": "hanmac",
"parentId": "hanmac-cell-0",
"parentId": "hanmac-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -49,7 +49,7 @@
{
"id": "hanmac-team-sibling",
"companyId": "hanmac",
"parentId": "hanmac-cell-0",
"parentId": "hanmac-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -85,7 +85,7 @@
{
"id": "hanmac-cell-1",
"companyId": "hanmac",
"parentId": "hanmac-office-1",
"parentId": "hanmac-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -94,7 +94,7 @@
{
"id": "hanmac-team-1",
"companyId": "hanmac",
"parentId": "hanmac-cell-1",
"parentId": "hanmac-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -130,7 +130,7 @@
{
"id": "saman-cell-0",
"companyId": "saman",
"parentId": "saman-office-0",
"parentId": "saman-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -139,7 +139,7 @@
{
"id": "saman-team-0",
"companyId": "saman",
"parentId": "saman-cell-0",
"parentId": "saman-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -148,7 +148,7 @@
{
"id": "saman-team-sibling",
"companyId": "saman",
"parentId": "saman-cell-0",
"parentId": "saman-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -184,7 +184,7 @@
{
"id": "saman-cell-1",
"companyId": "saman",
"parentId": "saman-office-1",
"parentId": "saman-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -193,7 +193,7 @@
{
"id": "saman-team-1",
"companyId": "saman",
"parentId": "saman-cell-1",
"parentId": "saman-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -229,7 +229,7 @@
{
"id": "jangheon-industry-cell-0",
"companyId": "jangheon-industry",
"parentId": "jangheon-industry-office-0",
"parentId": "jangheon-industry-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -238,7 +238,7 @@
{
"id": "jangheon-industry-team-0",
"companyId": "jangheon-industry",
"parentId": "jangheon-industry-cell-0",
"parentId": "jangheon-industry-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -247,7 +247,7 @@
{
"id": "jangheon-industry-team-sibling",
"companyId": "jangheon-industry",
"parentId": "jangheon-industry-cell-0",
"parentId": "jangheon-industry-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -283,7 +283,7 @@
{
"id": "jangheon-industry-cell-1",
"companyId": "jangheon-industry",
"parentId": "jangheon-industry-office-1",
"parentId": "jangheon-industry-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -292,7 +292,7 @@
{
"id": "jangheon-industry-team-1",
"companyId": "jangheon-industry",
"parentId": "jangheon-industry-cell-1",
"parentId": "jangheon-industry-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -328,7 +328,7 @@
{
"id": "jangheon-cell-0",
"companyId": "jangheon",
"parentId": "jangheon-office-0",
"parentId": "jangheon-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -337,7 +337,7 @@
{
"id": "jangheon-team-0",
"companyId": "jangheon",
"parentId": "jangheon-cell-0",
"parentId": "jangheon-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -346,7 +346,7 @@
{
"id": "jangheon-team-sibling",
"companyId": "jangheon",
"parentId": "jangheon-cell-0",
"parentId": "jangheon-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -382,7 +382,7 @@
{
"id": "jangheon-cell-1",
"companyId": "jangheon",
"parentId": "jangheon-office-1",
"parentId": "jangheon-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -391,7 +391,7 @@
{
"id": "jangheon-team-1",
"companyId": "jangheon",
"parentId": "jangheon-cell-1",
"parentId": "jangheon-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -427,7 +427,7 @@
{
"id": "ptc-cell-0",
"companyId": "ptc",
"parentId": "ptc-office-0",
"parentId": "ptc-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -436,7 +436,7 @@
{
"id": "ptc-team-0",
"companyId": "ptc",
"parentId": "ptc-cell-0",
"parentId": "ptc-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -445,7 +445,7 @@
{
"id": "ptc-team-sibling",
"companyId": "ptc",
"parentId": "ptc-cell-0",
"parentId": "ptc-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -481,7 +481,7 @@
{
"id": "ptc-cell-1",
"companyId": "ptc",
"parentId": "ptc-office-1",
"parentId": "ptc-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -490,7 +490,7 @@
{
"id": "ptc-team-1",
"companyId": "ptc",
"parentId": "ptc-cell-1",
"parentId": "ptc-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -526,7 +526,7 @@
{
"id": "halla-cell-0",
"companyId": "halla",
"parentId": "halla-office-0",
"parentId": "halla-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -535,7 +535,7 @@
{
"id": "halla-team-0",
"companyId": "halla",
"parentId": "halla-cell-0",
"parentId": "halla-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -544,7 +544,7 @@
{
"id": "halla-team-sibling",
"companyId": "halla",
"parentId": "halla-cell-0",
"parentId": "halla-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -580,7 +580,7 @@
{
"id": "halla-cell-1",
"companyId": "halla",
"parentId": "halla-office-1",
"parentId": "halla-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -589,7 +589,7 @@
{
"id": "halla-team-1",
"companyId": "halla",
"parentId": "halla-cell-1",
"parentId": "halla-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
@@ -625,7 +625,7 @@
{
"id": "baron-cell-0",
"companyId": "baron",
"parentId": "baron-office-0",
"parentId": "baron-team-0",
"name": "가상 1셀",
"type": "cell",
"leaderId": null,
@@ -634,7 +634,7 @@
{
"id": "baron-team-0",
"companyId": "baron",
"parentId": "baron-cell-0",
"parentId": "baron-office-0",
"name": "가상 1팀",
"type": "team",
"leaderId": null,
@@ -643,7 +643,7 @@
{
"id": "baron-team-sibling",
"companyId": "baron",
"parentId": "baron-cell-0",
"parentId": "baron-office-0",
"name": "가상 협력팀",
"type": "team",
"leaderId": null,
@@ -679,7 +679,7 @@
{
"id": "baron-cell-1",
"companyId": "baron",
"parentId": "baron-office-1",
"parentId": "baron-team-1",
"name": "가상 2셀",
"type": "cell",
"leaderId": null,
@@ -688,7 +688,7 @@
{
"id": "baron-team-1",
"companyId": "baron",
"parentId": "baron-cell-1",
"parentId": "baron-office-1",
"name": "가상 2팀",
"type": "team",
"leaderId": null,
+1 -1
View File
@@ -96,7 +96,7 @@ function verifyTypography(value, label) {
check(cards.length > 0 && cards.every(card => card.role.color === (size === 'representative' ? 'rgb(34, 67, 135)' : 'rgb(121, 121, 121)')
&& card.role.fontSize === (small ? '15px' : '20px') && card.role.fontWeight === '400' && card.role.lineHeight === (small ? '18px' : '22px')
&& card.role.letterSpacing === (small ? '-0.6px' : '-0.8px')), `${label}/${size}: position exact computed typography`);
check(cards.every(card => card.person.color === 'rgb(7, 29, 58)' && card.person.fontSize === (small ? '20px' : '24px') && card.person.fontWeight === '600'
check(cards.every(card => card.person.color === 'rgb(7, 29, 58)' && card.person.fontSize === (small ? '20px' : '24px') && card.person.fontWeight === '700'
&& card.person.lineHeight === (small ? '24px' : '30px') && card.person.letterSpacing === (size === 'leader' ? '-0.96px' : '-0.8px')), `${label}/${size}: name exact computed typography`);
check(cards.every(card => card.cssGap === '0px' && card.textGap === 0 && card.role.margin === '0px' && card.role.padding === '0px'
&& card.person.margin === '0px' && card.person.padding === '0px'), `${label}/${size}: zero gap/margins/padding between two text rows`);
@@ -95,7 +95,7 @@ async function capture(story, scenario, data) {
const person = (data ?? fixture).employees.find(item => item.id === card.id);
check(card.badge?.text === person.jobTitle || (!person.jobTitle && !card.badge), `${prefix}: jobTitle-driven badge`, { jobTitle: person.jobTitle, badge: card.badge?.text ?? null });
if (!person.jobTitle) check(card.attributesHeight === 0 && card.attributesPosition === 'absolute', `${prefix}: no empty badge space`, card.attributesHeight);
if (card.badge && card.size !== 'representative') check(card.badge.fontSize === (card.size === 'leader' ? '9px' : '8.5px') && card.badge.fontWeight === (card.size === 'leader' ? '700' : '600'), `${prefix}: badge typography`, card.badge);
if (card.badge && card.size !== 'representative') check(card.badge.fontSize === (card.size === 'leader' ? '9px' : '8.5px') && card.badge.fontWeight === (card.size === 'leader' ? '700' : '500'), `${prefix}: badge typography`, card.badge);
if (card.size !== 'representative') {
check(card.row?.display === 'flex' && card.row.alignItems === 'center' && card.row.gap === '4px' && Math.abs(card.row.nameGap) < 0.1, `${prefix}: position/badge row and zero name gap`, card.row);
if (card.badge) check(Math.abs(card.row.badgeGap - 4) < 0.1 && card.row.badgeMargin === '0px', `${prefix}: actual position/badge gap and margin`, card.row);
+2 -2
View File
@@ -109,9 +109,9 @@ function verify(m, route, v) {
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [146,74] : c.size === 'leader' ? [146,68] : c.size === 'employee' ? [108,52] : [114,54]; return near(c.box.width,w) && near(c.box.height,h); }), `${name}: approved card sizes and existing P3 status sizes preserved`);
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [36,62] : c.size === 'leader' ? [32,56] : c.size === 'employee' ? [24,44] : [28,28]; return c.photo === 'placeholder' && c.image?.placeholder === 'true' && c.image.naturalWidth > 0 && near(c.avatar.box.width,w) && near(c.avatar.box.height,h) && c.avatar.borderRadius !== '50%'; }) && new Set(m.cards.map(c => c.image?.src)).size === 1, `${name}: common decoded square icon in every noncircular slot`);
check(m.headers.filter(h => h.depth === 1).every(h => h.fontSize === '20px' && h.fontWeight === '700' && h.lineHeight === '36px' && near(h.box.height,36)), `${name}: 1Depth 20/700/36 and header36`, m.headers.filter(h => h.depth === 1));
check(m.headers.filter(h => h.depth >= 2).every(h => { const small = h.id.startsWith('talent-growth-') || h.id.startsWith('business-planning-'); return h.fontSize === (small ? '18px':'20px') && h.fontWeight === (small?'600':'700') && h.lineHeight === (small?'16.9px':'19.7px') && near(h.box.width,114) && near(h.box.height,30.4); }), `${name}: 2Depth source typography/surface`, m.headers.filter(h => h.depth >= 2));
check(m.headers.filter(h => h.depth >= 2).every(h => { const small = h.id.startsWith('talent-growth-') || h.id.startsWith('business-planning-'); return h.fontSize === (small ? '18px':'20px') && h.fontWeight === '700' && h.lineHeight === (small?'16.9px':'19.7px') && near(h.box.width,114) && near(h.box.height,30.4); }), `${name}: 2Depth source typography/surface`, m.headers.filter(h => h.depth >= 2));
check(m.headers.every(h => h.gap === null || near(h.gap,8)), `${name}: all title-to-first-card gaps 8px`, m.headers.map(h => [h.id,h.gap]));
check(m.hub.font.fontSize === '21px' && m.hub.font.fontWeight === '750' && m.hub.font.lineHeight === '24px' && m.hub.paddingLeft === '22px' && m.hub.paddingRight === '24px' && m.hub.gap === '8px' && near(m.hub.box.height,42) && near(m.hub.icon.width,20) && near(m.hub.icon.height,20) && near(m.hub.icon.y+10,m.hub.label.y+m.hub.label.height/2) && near(m.hub.box.x+m.hub.box.width/2,m.hub.axis.x+m.hub.axis.width/2) && m.hub.paths === 3, `${name}: hub font/padding/icon/axis alignment`, m.hub);
check(m.hub.font.fontSize === '21px' && m.hub.font.fontWeight === '700' && m.hub.font.lineHeight === '24px' && m.hub.paddingLeft === '22px' && m.hub.paddingRight === '24px' && m.hub.gap === '8px' && near(m.hub.box.height,42) && near(m.hub.icon.width,20) && near(m.hub.icon.height,20) && near(m.hub.icon.y+10,m.hub.label.y+m.hub.label.height/2) && near(m.hub.box.x+m.hub.box.width/2,m.hub.axis.x+m.hub.axis.width/2) && m.hub.paths === 3, `${name}: hub font/padding/icon/axis alignment`, m.hub);
check(m.cells.every(c => c.whiteSpace === 'nowrap' && c.textOverflow !== 'ellipsis' && c.ranges.length === 1 && c.ranges[0].x >= c.box.x + parseFloat(c.paddingLeft) - .5 && c.ranges[0].x+c.ranges[0].width <= c.box.x+c.box.width-parseFloat(c.paddingRight)+.5), `${name}: every table label fits on one line without clipping`, m.cells.filter(c => c.ranges.length !== 1 || c.ranges[0].width > c.box.width-parseFloat(c.paddingLeft)-parseFloat(c.paddingRight)+.5));
check(near(m.table.box.width,330,1) && m.table.total === '41' && m.footer.includes('41'), `${name}: table width/count and footer retained`, { table:m.table.box,total:m.table.total });
check(m.document.scrollWidth === m.document.clientWidth && m.document.scrollHeight === m.document.clientHeight && !m.outside.length, `${name}: no page scroll; every card inside stage`, m.document);
+3 -3
View File
@@ -191,7 +191,7 @@ function verify(value, label, viewport) {
check(assetCards.every(card => near(card.rowGap, 0) && card.badgeInRow && (card.badgeGap === null || near(card.badgeGap, 4))), `${label}: role and badge share one row, gap4; position/name gap0`, assetCards.map(({ id, rowGap, badgeGap, badgeInRow }) => ({ id, rowGap, badgeGap, badgeInRow })));
check(value.zeroSpacingSerialization.authored === '0px' && assetCards.filter(card => card.badge).every(card => {
const small = card.size === 'employee', style = card.badgeStyle;
return style.borderTopWidth === '1px' && style.borderRadius === '4px' && style.letterSpacing === value.zeroSpacingSerialization.computed && style.fontSize === (small ? '8.5px' : '9px') && style.fontWeight === (small ? '600' : '700') && style.lineHeight === (small ? '8.5px' : '9px') && style.paddingLeft === (small ? '2px' : '3px') && style.paddingRight === (small ? '2px' : '3px');
return style.borderTopWidth === '1px' && style.borderRadius === '4px' && style.letterSpacing === value.zeroSpacingSerialization.computed && style.fontSize === (small ? '8.5px' : '9px') && style.fontWeight === (small ? '500' : '700') && style.lineHeight === (small ? '8.5px' : '9px') && style.paddingLeft === (small ? '2px' : '3px') && style.paddingRight === (small ? '2px' : '3px');
}), `${label}: badge source border/radius/font restored`, { zeroSpacingSerialization: value.zeroSpacingSerialization, badges: assetCards.filter(card => card.badge).map(({ id, badgeStyle }) => ({ id, badgeStyle })) });
const tagColors = { 'business-planning': 'rgb(37, 78, 126)', 'talent-growth': 'rgb(14, 103, 200)', 'integrated-system': 'rgb(110, 71, 160)', 'design-planning': 'rgb(6, 154, 149)', 'solution-sales': 'rgb(81, 139, 53)' };
check(assetCards.filter(card => card.badge).every(card => card.badgeStyle.color === (tagColors[card.asset] ?? 'rgb(14, 84, 146)')), `${label}: organization-specific badge source colors`, assetCards.filter(card => card.badge).map(({ id, asset, badgeStyle }) => ({ id, asset, color: badgeStyle.color })));
@@ -208,9 +208,9 @@ function verify(value, label, viewport) {
check(value.headers.filter(header => header.depth >= 2).every(header => {
let id = header.id; while (depthOf(id) > 1) id = organizationById.get(id).parentId;
const expected = headerGradients[id], hr = id === 'talent-growth';
return expected?.every(color => header.style.backgroundImage.includes(color)) && header.style.fontSize === (hr ? '18px' : '20px') && header.style.fontWeight === (hr ? '600' : '700') && header.style.lineHeight === (hr ? '16.9px' : '19.7px');
return expected?.every(color => header.style.backgroundImage.includes(color)) && header.style.fontSize === (hr ? '18px' : '20px') && header.style.fontWeight === '700' && header.style.lineHeight === (hr ? '16.9px' : '19.7px');
}), `${label}: deeper headers inherit original parent color and source typography`, value.headers.filter(header => header.depth >= 2).map(({ id, style }) => ({ id, style })));
check(value.hub.paths === 3 && near(value.hub.iconBox?.width, 20) && near(value.hub.iconBox?.height, 20) && near(value.hub.gap, 8) && value.hub.style.fontSize === '21px' && value.hub.style.fontWeight === '750' && value.hub.style.lineHeight === '24px', `${label}: original hub icon and source text metrics`, value.hub);
check(value.hub.paths === 3 && near(value.hub.iconBox?.width, 20) && near(value.hub.iconBox?.height, 20) && near(value.hub.gap, 8) && value.hub.style.fontSize === '21px' && value.hub.style.fontWeight === '700' && value.hub.style.lineHeight === '24px', `${label}: original hub icon and source text metrics`, value.hub);
check(value.hub.iconBox && value.hub.labelBox && [value.hub.iconBox, value.hub.labelBox].every(box => box.x >= value.hub.box.x - 0.5 && box.y >= value.hub.box.y - 0.5 && box.x + box.width <= value.hub.box.x + value.hub.box.width + 0.5 && box.y + box.height <= value.hub.box.y + value.hub.box.height + 0.5), `${label}: hub icon and label remain inside source pill`, value.hub);
check(value.status.every(group => group.heading === (group.status === 'dispatched' ? '파견' : '휴직')) && value.cards.filter(card => card.statusGroup).every(card => card.statusLabels === 0), `${label}: compact status headers retained and below-name labels removed`, value.status);
check(value.table?.title === '직위별 인원 현황 (팀별)' && JSON.stringify(value.table?.header) === JSON.stringify(['팀원', ...positions, '소계']) && JSON.stringify(value.table?.rows) === JSON.stringify(expectedRows) && JSON.stringify(value.table?.total) === JSON.stringify(expectedTotal) && fixture.employees.length === 41, `${label}: position matrix cross-counts original 41 employees`, { actual: value.table, expectedRows, expectedTotal });