@tienne/gestalt 0.96.0 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/plugin/skills/architecture/SKILL.md +31 -5
- package/dist/plugin/skills/architecture/references/domain-flow.md +1 -0
- package/dist/plugin/skills/architecture/references/projections.md +56 -0
- package/dist/plugin/skills/architecture/references/rerun.md +3 -1
- package/dist/schemas/architecture-ir.schema.json +51 -3
- package/dist/src/architecture/blueprint-layout.d.ts +105 -0
- package/dist/src/architecture/blueprint-layout.d.ts.map +1 -0
- package/dist/src/architecture/blueprint-layout.js +297 -0
- package/dist/src/architecture/blueprint-layout.js.map +1 -0
- package/dist/src/architecture/delta-renderer.d.ts +35 -0
- package/dist/src/architecture/delta-renderer.d.ts.map +1 -0
- package/dist/src/architecture/delta-renderer.js +329 -0
- package/dist/src/architecture/delta-renderer.js.map +1 -0
- package/dist/src/architecture/delta.d.ts +70 -0
- package/dist/src/architecture/delta.d.ts.map +1 -0
- package/dist/src/architecture/delta.js +342 -0
- package/dist/src/architecture/delta.js.map +1 -0
- package/dist/src/architecture/drilldown.d.ts +19 -1
- package/dist/src/architecture/drilldown.d.ts.map +1 -1
- package/dist/src/architecture/drilldown.js +5 -5
- package/dist/src/architecture/drilldown.js.map +1 -1
- package/dist/src/architecture/er-layout.d.ts +97 -0
- package/dist/src/architecture/er-layout.d.ts.map +1 -0
- package/dist/src/architecture/er-layout.js +344 -0
- package/dist/src/architecture/er-layout.js.map +1 -0
- package/dist/src/architecture/flow-layout.d.ts +22 -1
- package/dist/src/architecture/flow-layout.d.ts.map +1 -1
- package/dist/src/architecture/flow-layout.js +13 -9
- package/dist/src/architecture/flow-layout.js.map +1 -1
- package/dist/src/architecture/html-renderer.d.ts +4 -0
- package/dist/src/architecture/html-renderer.d.ts.map +1 -1
- package/dist/src/architecture/html-renderer.js +462 -20
- package/dist/src/architecture/html-renderer.js.map +1 -1
- package/dist/src/architecture/html-theme.d.ts +18 -0
- package/dist/src/architecture/html-theme.d.ts.map +1 -1
- package/dist/src/architecture/html-theme.js +101 -1
- package/dist/src/architecture/html-theme.js.map +1 -1
- package/dist/src/architecture/ir-schema.d.ts +358 -117
- package/dist/src/architecture/ir-schema.d.ts.map +1 -1
- package/dist/src/architecture/ir-schema.js +33 -1
- package/dist/src/architecture/ir-schema.js.map +1 -1
- package/dist/src/architecture/lifecycle-layout.d.ts +94 -0
- package/dist/src/architecture/lifecycle-layout.d.ts.map +1 -0
- package/dist/src/architecture/lifecycle-layout.js +471 -0
- package/dist/src/architecture/lifecycle-layout.js.map +1 -0
- package/dist/src/architecture/merge.d.ts.map +1 -1
- package/dist/src/architecture/merge.js +72 -2
- package/dist/src/architecture/merge.js.map +1 -1
- package/dist/src/architecture/store.d.ts +6 -1
- package/dist/src/architecture/store.d.ts.map +1 -1
- package/dist/src/architecture/store.js +75 -8
- package/dist/src/architecture/store.js.map +1 -1
- package/dist/src/architecture/types.d.ts +35 -3
- package/dist/src/architecture/types.d.ts.map +1 -1
- package/dist/src/architecture/types.js +13 -1
- package/dist/src/architecture/types.js.map +1 -1
- package/dist/src/architecture/validator.d.ts +27 -3
- package/dist/src/architecture/validator.d.ts.map +1 -1
- package/dist/src/architecture/validator.js +331 -8
- package/dist/src/architecture/validator.js.map +1 -1
- package/dist/src/mcp/tools/architecture-passthrough.d.ts.map +1 -1
- package/dist/src/mcp/tools/architecture-passthrough.js +91 -2
- package/dist/src/mcp/tools/architecture-passthrough.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/skills/architecture/SKILL.md +31 -5
- package/plugin/skills/architecture/references/domain-flow.md +1 -0
- package/plugin/skills/architecture/references/projections.md +56 -0
- package/plugin/skills/architecture/references/rerun.md +3 -1
- package/schemas/architecture-ir.schema.json +51 -3
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { BAND_LINE_X, BAND_LINE_Y, BAND_TITLE_HALF, BAND_TITLE_X, CANVAS_PAD_TOP, CANVAS_PAD_X, LANE_INSET_Y, routeCanvas, } from './canvas-geometry.js';
|
|
2
|
+
import { BLUEPRINT_EMPTY_TEXT, BLUEPRINT_HEAD_W, BLUEPRINT_ROW_TEXT, BLUEPRINT_VISIBILITY_NOTE, BLUEPRINT_VISIBILITY_TEXT, blueprintStageTitle, computeBlueprintLayout, deriveBlueprint, } from './blueprint-layout.js';
|
|
2
3
|
import { flowCountByService, ROOT_LEVEL_ID } from './drilldown.js';
|
|
4
|
+
import { computeErLayout, deriveEr, erFoldText, isUnconfirmedFk, } from './er-layout.js';
|
|
3
5
|
import { computeFlowLayout } from './flow-layout.js';
|
|
4
6
|
import { renderClientScript, THEME_BOOT_SCRIPT } from './html-client.js';
|
|
5
|
-
import { datastoreEngine, iconUse, PRODUCT_COLORS, renderCss, renderIconSprite, VIEW_CSS, CAPTURE_CSS, DOC_CSS, FLOW_BRANCH_CSS, SEQ_VIEW_CSS, } from './html-theme.js';
|
|
7
|
+
import { datastoreEngine, iconUse, PRODUCT_COLORS, renderCss, renderIconSprite, VIEW_CSS, CAPTURE_CSS, DOC_CSS, FLOW_BRANCH_CSS, LIFECYCLE_CSS, BLUEPRINT_CSS, ER_CSS, SEQ_VIEW_CSS, } from './html-theme.js';
|
|
6
8
|
import { EDGE_KIND_ABOUT, LANE_ABOUT, LANE_TITLES, NODE_KIND_ABOUT, STAGE_LANE_ABOUT, flowBadgeText, MICRO_HOST_SHORT, NODE_KIND_SHORT, PLATFORM_CHIP_TEXT, PLATFORM_NAME, WEB_HOSTING_CHIP_TEXT, WEB_HOSTING_NAME, platformChipText, platformName, } from './kind-text.js';
|
|
7
9
|
import { compareEnvironment, FLAT_BACKWARD_EDGE_KINDS, textUnits, } from './layout.js';
|
|
8
10
|
import { indexMicroApps } from './micro-app.js';
|
|
9
11
|
import { ALL_PACKS_VOCABULARY, irVocabulary } from './packs/index.js';
|
|
10
12
|
import { KNOWLEDGE_COVERAGE_KINDS, KNOWLEDGE_DOC_KINDS } from './packs/knowledge.js';
|
|
11
13
|
import { computeCompareLayout } from './compare-layout.js';
|
|
14
|
+
import { computeLifecycleLayout, deriveLifecycle, LIFECYCLE_BAND_TEXT, } from './lifecycle-layout.js';
|
|
12
15
|
import { computeDataflowLayout } from './dataflow-layout.js';
|
|
13
16
|
import { computeSequenceLayout } from './sequence-layout.js';
|
|
14
17
|
import { clipText, computePhaseBoard, computeWalkLayout, phaseMetaText, resolveSequencePhases, sequenceCallDepths, } from './sequence-views.js';
|
|
15
18
|
import { computeServiceFacts } from './service-facts.js';
|
|
16
19
|
import { chipTextOverride, displayKindOf, hasCardDescription } from './types.js';
|
|
17
|
-
import { maskSharedText, redactForSharing } from './validator.js';
|
|
20
|
+
import { maskSharedQuestion, maskSharedText, redactForSharing, } from './validator.js';
|
|
18
21
|
const DASHED_PATTERN = '6 4';
|
|
19
22
|
const INFERRED_BADGE = '추정';
|
|
20
23
|
const EMPTY_LEVEL_SIZE = { width: 560, height: 240 };
|
|
@@ -25,7 +28,7 @@ const VIEW_TITLES = {
|
|
|
25
28
|
'knowledge-link': '지식과 아키텍처',
|
|
26
29
|
};
|
|
27
30
|
/** 하네스 레포는 화면이 없어 '화면별'이 틀린 말이 된다. 화면이 하나라도 있으면 웹 그림으로 친다 */
|
|
28
|
-
function viewTitle(ir) {
|
|
31
|
+
export function viewTitle(ir) {
|
|
29
32
|
if (ir.view !== 'screen-chain' || ir.nodes.some((n) => n.kind === 'screen')) {
|
|
30
33
|
return VIEW_TITLES[ir.view];
|
|
31
34
|
}
|
|
@@ -73,7 +76,7 @@ function compareStr(a, b) {
|
|
|
73
76
|
function byId(a, b) {
|
|
74
77
|
return compareStr(a.id, b.id);
|
|
75
78
|
}
|
|
76
|
-
function escapeHtml(s) {
|
|
79
|
+
export function escapeHtml(s) {
|
|
77
80
|
return s
|
|
78
81
|
.replace(/&/g, '&')
|
|
79
82
|
.replace(/</g, '<')
|
|
@@ -109,9 +112,7 @@ function round2(v) {
|
|
|
109
112
|
// ir은 공유본이면 이미 가린 사본이다. 자동 질문은 검증기가 원본 이름으로 지었으므로 여기서 한 번 더 가린다
|
|
110
113
|
function openQuestions(ir, validated, shared) {
|
|
111
114
|
const fromIr = ir.unresolved.filter((q) => q.answer === undefined || q.answer === '');
|
|
112
|
-
const auto = shared
|
|
113
|
-
? validated.autoUnresolved.map((q) => ({ ...q, question: maskSharedText(q.question) }))
|
|
114
|
-
: validated.autoUnresolved;
|
|
115
|
+
const auto = shared ? validated.autoUnresolved.map(maskSharedQuestion) : validated.autoUnresolved;
|
|
115
116
|
return [...fromIr, ...auto].sort(byId);
|
|
116
117
|
}
|
|
117
118
|
/** 지식과 아키텍처 그림에서 기술 노드 id → 그 노드를 설명하는 문서. 다른 그림이면 undefined */
|
|
@@ -391,6 +392,9 @@ const VIEW_SHAPE_NAME = {
|
|
|
391
392
|
sequence: '순서도',
|
|
392
393
|
dataflow: '데이터 이동',
|
|
393
394
|
compare: '견주기',
|
|
395
|
+
lifecycle: '상태 흐름',
|
|
396
|
+
blueprint: '서비스 블루프린트',
|
|
397
|
+
er: '테이블 관계',
|
|
394
398
|
};
|
|
395
399
|
const STRIP_CARD_W = 240;
|
|
396
400
|
const STRIP_GAP = 12;
|
|
@@ -403,7 +407,7 @@ function renderViewStrip(cards) {
|
|
|
403
407
|
`<span class="vc-shape">${VIEW_SHAPE_NAME[c.projection.shape]}</span>` +
|
|
404
408
|
`<b>${escapeHtml(c.projection.title)}</b>` +
|
|
405
409
|
`<span class="vc-q">${escapeHtml(c.projection.question)}</span>` +
|
|
406
|
-
`<span class="vc-n"
|
|
410
|
+
`<span class="vc-n">${c.summary !== undefined ? escapeHtml(c.summary) : `단계 ${c.steps}${c.actors > 0 ? `, 참여 ${c.actors}` : ''}`}</span></a>`)
|
|
407
411
|
.join('');
|
|
408
412
|
return {
|
|
409
413
|
html: `<nav class="view-strip" aria-label="질문별 그림" style="left:${CANVAS_PAD_X}px;top:-${STRIP_H}px">` +
|
|
@@ -411,6 +415,24 @@ function renderViewStrip(cards) {
|
|
|
411
415
|
width: CANVAS_PAD_X * 2 + cards.length * (STRIP_CARD_W + STRIP_GAP) - STRIP_GAP,
|
|
412
416
|
};
|
|
413
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* 레이아웃은 원본 IR로 계산해 레인 제목(구간 이름)과 제품 이름이 원문이다.
|
|
420
|
+
* 공유본은 좌표는 그대로 쓰고 글자만 IR과 같은 기준으로 가린다
|
|
421
|
+
*/
|
|
422
|
+
function maskLayoutText(layout) {
|
|
423
|
+
return {
|
|
424
|
+
...layout,
|
|
425
|
+
lanes: layout.lanes.map((l) => ({ ...l, title: maskSharedText(l.title) })),
|
|
426
|
+
...(layout.regions !== undefined
|
|
427
|
+
? {
|
|
428
|
+
regions: {
|
|
429
|
+
...layout.regions,
|
|
430
|
+
groups: layout.regions.groups.map((g) => ({ ...g, name: maskSharedText(g.name) })),
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
414
436
|
function productNames(layout, id) {
|
|
415
437
|
const regions = layout.regions;
|
|
416
438
|
return (regions?.productsOf[id] ?? []).map((i) => regions.groups[i].name);
|
|
@@ -551,13 +573,14 @@ function roundedPathD(points) {
|
|
|
551
573
|
* 흐름 레벨 하나. 행위자마다 가로줄을 깔고 단계 카드를 왼쪽에서 오른쪽으로 놓는다.
|
|
552
574
|
* 카드는 노드 카드와 같은 .node라 강조와 검색, 질문 이동을 그대로 탄다. data-node-id 자리에 단계 id가 들어간다
|
|
553
575
|
*/
|
|
554
|
-
function renderFlowSection(level, flow, questionCount, drawableNodes, captureOf) {
|
|
576
|
+
function renderFlowSection(level, flow, questionCount, drawableNodes, captureOf, mask) {
|
|
555
577
|
const { layout } = level;
|
|
556
578
|
const width = round2(layout.width + FLOW_PAD * 2);
|
|
557
579
|
const height = round2(layout.height + FLOW_PAD * 2);
|
|
558
580
|
const stepById = new Map(flow.steps.map((st) => [st.id, st]));
|
|
559
581
|
const actorById = new Map(flow.actors.map((a) => [a.id, a]));
|
|
560
|
-
|
|
582
|
+
// 상태 값은 키라서 공유본 사본에서도 원문이다. 찍는 자리에서 가린다
|
|
583
|
+
const stateText = (state) => flow.stateLabels?.[state] ?? mask(state);
|
|
561
584
|
// 이름이 붙은 상태는 코드 값이 아니라서 고정폭 글꼴을 안 쓴다
|
|
562
585
|
const named = (state) => flow.stateLabels?.[state] !== undefined ? ' named' : '';
|
|
563
586
|
const lanes = layout.lanes
|
|
@@ -671,7 +694,7 @@ function renderFlowSection(level, flow, questionCount, drawableNodes, captureOf)
|
|
|
671
694
|
`<span class="nm"><span class="t">${escapeHtml(st.label)}</span></span>` +
|
|
672
695
|
`<span class="l2">` +
|
|
673
696
|
(st.state !== undefined
|
|
674
|
-
? `<span class="st${named(st.state)}" title="${escapeHtml(st.state)}">${escapeHtml(stateText(st.state))}</span>`
|
|
697
|
+
? `<span class="st${named(st.state)}" title="${escapeHtml(mask(st.state))}">${escapeHtml(stateText(st.state))}</span>`
|
|
675
698
|
: '') +
|
|
676
699
|
(st.terminal ? `<span class="end" title="${FLOW_TEXT.end}">끝</span>` : '') +
|
|
677
700
|
(refs.length > 0
|
|
@@ -765,6 +788,9 @@ const VIEW_SVG_LABEL = {
|
|
|
765
788
|
sequence: '메시지',
|
|
766
789
|
dataflow: '데이터 이동',
|
|
767
790
|
compare: '견주기',
|
|
791
|
+
lifecycle: '상태 전이',
|
|
792
|
+
blueprint: '단계별 층',
|
|
793
|
+
er: '외래 키',
|
|
768
794
|
};
|
|
769
795
|
/** 투영 레벨 하나. 맨 위에 답하는 질문을 적고 그 아래 모양별 선과 카드를 얹는다 */
|
|
770
796
|
function viewSection(levelId, projection, width, height, under, links, cards) {
|
|
@@ -1109,6 +1135,334 @@ function renderCompareSection(levelId, projection, layout, nodeById, services) {
|
|
|
1109
1135
|
.join('');
|
|
1110
1136
|
return viewSection(levelId, projection, width, height, columns, '', viewCards(layout.cards, nodeById, services));
|
|
1111
1137
|
}
|
|
1138
|
+
const OUTCOME_TEXT = {
|
|
1139
|
+
done: '완료',
|
|
1140
|
+
cancel: '취소',
|
|
1141
|
+
fail: '실패',
|
|
1142
|
+
end: '끝',
|
|
1143
|
+
};
|
|
1144
|
+
/**
|
|
1145
|
+
* 상태 흐름. 진행 단계, 대기와 복구, 끝 상태 띠를 위에서부터 쌓는다.
|
|
1146
|
+
* 카드와 선은 흐름 레벨과 같은 .flow-step과 .flow-t라 누르면 대표 단계와 대표 전이의 서랍이 그대로 열린다.
|
|
1147
|
+
* 좌표는 원본 흐름으로 낸 layout을 쓰고 글자만 text 모델에서 꺼낸다. 공유본에서 가린 글자 폭에 따라 그림이 움직이지 않게 하려고다
|
|
1148
|
+
*/
|
|
1149
|
+
function renderLifecycleSection(levelId, projection, flow, text, layout, questionCount, mask) {
|
|
1150
|
+
const ox = CANVAS_PAD_X;
|
|
1151
|
+
const oy = CANVAS_PAD_TOP;
|
|
1152
|
+
const width = round2(layout.width + ox * 2);
|
|
1153
|
+
const height = round2(layout.height + oy + 24);
|
|
1154
|
+
const shift = (pts) => pts.map((p) => ({ x: p.x + ox - FLOW_PAD, y: p.y + oy - FLOW_PAD }));
|
|
1155
|
+
// 상태 값은 stateOf와 레이아웃의 키라 사본에서도 원문이다. 찍는 자리에서만 가린다
|
|
1156
|
+
const stateText = (state) => flow.stateLabels?.[state] ?? mask(state);
|
|
1157
|
+
const stepLabel = new Map(flow.steps.map((st) => [st.id, st.label]));
|
|
1158
|
+
const stateOf = new Map(text.states.map((st) => [st.state, st]));
|
|
1159
|
+
const bands = layout.bands
|
|
1160
|
+
.map((b) => `<g class="lc-band lc-${b.band}"><rect x="${round2(ox)}" y="${round2(b.y + oy)}" width="${layout.width}" height="${b.height}" rx="12"/>` +
|
|
1161
|
+
`<text class="lc-band-t" x="${round2(ox + 14)}" y="${round2(b.y + oy + 22)}">${LIFECYCLE_BAND_TEXT[b.band]}<tspan class="lc-band-n" dx="8">${b.count}</tspan></text></g>`)
|
|
1162
|
+
.join('');
|
|
1163
|
+
const links = layout.routes
|
|
1164
|
+
.map((r, i) => {
|
|
1165
|
+
const t = text.transitions[i];
|
|
1166
|
+
const from = stateOf.get(t.from);
|
|
1167
|
+
const to = stateOf.get(t.to);
|
|
1168
|
+
const dash = t.lineStyle === 'dashed' ? ` stroke-dasharray="${DASHED_PATTERN}"` : '';
|
|
1169
|
+
const pts = shift(r.points);
|
|
1170
|
+
const d = r.back ? roundedPathD(pts) : pathD(pts);
|
|
1171
|
+
const tip = r.tip.map((p) => `${round2(p.x + ox)},${round2(p.y + oy)}`).join(' ');
|
|
1172
|
+
const name = `${FLOW_TEXT.transition}${r.back ? `, ${FLOW_TEXT.back}` : ''}: ${stateText(from.state)} → ${stateText(to.state)}` +
|
|
1173
|
+
` (${t.label})`;
|
|
1174
|
+
return (`<g class="link flow-t p-${t.path}${r.back ? ' back' : ''}" data-from="${escapeHtml(r.from)}" data-to="${escapeHtml(r.to)}" ` +
|
|
1175
|
+
`data-transition-id="${escapeHtml(r.id)}" tabindex="0" role="button" aria-label="${escapeHtml(name)}">` +
|
|
1176
|
+
`<title>${escapeHtml(name)}</title><path class="hit" d="${d}" stroke-width="12"/>` +
|
|
1177
|
+
`<path class="edge" d="${d}" stroke-width="2"${dash}/><polygon class="tip" points="${tip}"/>` +
|
|
1178
|
+
`<text class="t-label${r.labelAnchor === 'start' ? ' at-start' : ''}${t.borrowed ? ' lc-borrowed' : ''}" x="${round2(r.labelAt.x + ox)}" y="${round2(r.labelAt.y + oy)}">` +
|
|
1179
|
+
(r.back ? '<tspan class="t-back">↩ </tspan>' : '') +
|
|
1180
|
+
`${escapeHtml(t.label)}</text></g>`);
|
|
1181
|
+
})
|
|
1182
|
+
.join('');
|
|
1183
|
+
const cards = layout.boxes
|
|
1184
|
+
.map((b) => {
|
|
1185
|
+
const st = stateOf.get(b.state);
|
|
1186
|
+
const outcome = st.terminal ? (st.outcome ?? 'end') : undefined;
|
|
1187
|
+
const qs = st.steps.reduce((n, id) => n + (questionCount.get(id) ?? 0), 0);
|
|
1188
|
+
const cls = ['node', 'flow-step', 'lc-state', `lc-${st.band}`];
|
|
1189
|
+
if (st.band === 'recovery')
|
|
1190
|
+
cls.push('p-side');
|
|
1191
|
+
if (outcome !== undefined)
|
|
1192
|
+
cls.push(`o-${outcome}`);
|
|
1193
|
+
if (st.docOnly)
|
|
1194
|
+
cls.push('doc-only');
|
|
1195
|
+
const steps = st.steps.map((id) => stepLabel.get(id) ?? id);
|
|
1196
|
+
const stepsText = steps.length > 1 ? `${steps[0]} 외 ${steps.length - 1}` : steps[0];
|
|
1197
|
+
const aria = `${stateText(st.state)}, 상태, ${LIFECYCLE_BAND_TEXT[st.band]}` +
|
|
1198
|
+
(outcome !== undefined ? `, ${OUTCOME_TEXT[outcome]}` : '') +
|
|
1199
|
+
`, 단계 ${steps.join(', ')}` +
|
|
1200
|
+
(st.docOnly ? ', 문서로만 확인' : '');
|
|
1201
|
+
return (`<div class="${cls.join(' ')}" data-node-id="${escapeHtml(st.rep)}" role="button" tabindex="0" ` +
|
|
1202
|
+
`aria-label="${escapeHtml(aria)}" title="${escapeHtml(steps.join(', '))}" ` +
|
|
1203
|
+
`style="left:${round2(b.x + ox)}px;top:${round2(b.y + oy)}px;width:${b.width}px;height:${b.height}px">` +
|
|
1204
|
+
`<span class="nm"><span class="t">${escapeHtml(stateText(st.state))}</span></span>` +
|
|
1205
|
+
`<span class="l2">` +
|
|
1206
|
+
(outcome !== undefined ? `<span class="end lc-out">${OUTCOME_TEXT[outcome]}</span>` : '') +
|
|
1207
|
+
(flow.stateLabels?.[st.state] !== undefined
|
|
1208
|
+
? `<span class="st" title="${escapeHtml(mask(st.state))}">${escapeHtml(mask(st.state))}</span>`
|
|
1209
|
+
: '') +
|
|
1210
|
+
`<span class="lc-steps">${escapeHtml(stepsText)}</span>` +
|
|
1211
|
+
(qs > 0
|
|
1212
|
+
? `<span class="qb" title="확인할 질문">${iconUse('u-question')}${qs}</span>`
|
|
1213
|
+
: '') +
|
|
1214
|
+
`</span></div>`);
|
|
1215
|
+
})
|
|
1216
|
+
.join('');
|
|
1217
|
+
return viewSection(levelId, projection, width, height, bands, links, cards);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* 서비스 블루프린트. 단계 열과 층 행으로 된 격자이고 선은 긋지 않는다. 열 순서가 곧 순서다.
|
|
1221
|
+
* 가시선은 라벨 글자와 굵은 선으로 긋고 층 이름도 글자로 적어 색을 못 봐도 위아래가 읽힌다.
|
|
1222
|
+
* 좌표는 원본으로 낸 모델에서 나오고 글자는 그린 쪽(공유본이면 가린 사본) 흐름과 노드에서 꺼낸다
|
|
1223
|
+
*/
|
|
1224
|
+
function renderBlueprintSection(levelId, projection, flow, model, layout, nodeById, questionCount, mask) {
|
|
1225
|
+
const ox = CANVAS_PAD_X;
|
|
1226
|
+
const oy = CANVAS_PAD_TOP;
|
|
1227
|
+
const width = round2(layout.width + ox * 2);
|
|
1228
|
+
const height = round2(layout.height + oy + 24);
|
|
1229
|
+
const stepById = new Map(flow.steps.map((st) => [st.id, st]));
|
|
1230
|
+
const actorById = new Map(flow.actors.map((a) => [a.id, a]));
|
|
1231
|
+
const itemName = (type, id) => {
|
|
1232
|
+
if (type === 'step')
|
|
1233
|
+
return stepById.get(id)?.label ?? id;
|
|
1234
|
+
const n = nodeById.get(id);
|
|
1235
|
+
return n ? nodeName(n) : id;
|
|
1236
|
+
};
|
|
1237
|
+
const bands = layout.rows
|
|
1238
|
+
.map((r) => `<g class="bp-row bp-${r.row}" data-layer="${r.row}"><rect x="${round2(ox)}" y="${round2(r.y + oy)}" width="${layout.width}" height="${r.height}" rx="10"/></g>`)
|
|
1239
|
+
.join('');
|
|
1240
|
+
const top = layout.rows[0].y;
|
|
1241
|
+
const bottom = layout.rows[layout.rows.length - 1];
|
|
1242
|
+
const guides = layout.stages
|
|
1243
|
+
.slice(1)
|
|
1244
|
+
.map((st) => {
|
|
1245
|
+
const x = round2(st.x + ox - 6);
|
|
1246
|
+
return `<line class="bp-col-line" x1="${x}" x2="${x}" y1="${round2(top + oy)}" y2="${round2(bottom.y + bottom.height + oy)}"/>`;
|
|
1247
|
+
})
|
|
1248
|
+
.join('');
|
|
1249
|
+
const lines = layout.lines
|
|
1250
|
+
.map((l) => {
|
|
1251
|
+
const y = round2(l.y + oy);
|
|
1252
|
+
const line = `<line class="bp-line bp-${l.role}" data-role="${l.role}" x1="${round2(ox)}" x2="${round2(ox + layout.width)}" y1="${y}" y2="${y}"/>`;
|
|
1253
|
+
if (l.role !== 'visibility')
|
|
1254
|
+
return line;
|
|
1255
|
+
return (`<g class="bp-vis">${line}` +
|
|
1256
|
+
`<text class="bp-vis-t" x="${round2(ox + 12)}" y="${round2(y + 4)}">${BLUEPRINT_VISIBILITY_TEXT}</text>` +
|
|
1257
|
+
`<text class="bp-vis-n" x="${round2(ox + layout.width - 12)}" y="${round2(y + 4)}" text-anchor="end">${BLUEPRINT_VISIBILITY_NOTE}</text></g>`);
|
|
1258
|
+
})
|
|
1259
|
+
.join('');
|
|
1260
|
+
const customer = model.customer !== undefined ? actorById.get(model.customer) : undefined;
|
|
1261
|
+
const rowLabels = layout.rows
|
|
1262
|
+
.map((r) => `<div class="bp-row-t" style="left:${round2(ox + 12)}px;top:${round2(r.y + oy + 14)}px;width:${BLUEPRINT_HEAD_W - 24}px">` +
|
|
1263
|
+
`<b>${BLUEPRINT_ROW_TEXT[r.row]}</b>` +
|
|
1264
|
+
(r.row === 'customer' && customer !== undefined
|
|
1265
|
+
? `<span>${escapeHtml(customer.label)}</span>`
|
|
1266
|
+
: '') +
|
|
1267
|
+
`</div>`)
|
|
1268
|
+
.join('');
|
|
1269
|
+
const stages = layout.stages
|
|
1270
|
+
.map((st, i) => {
|
|
1271
|
+
const title = mask(blueprintStageTitle(flow, model.columns[i]));
|
|
1272
|
+
return (`<div class="bp-stage" style="left:${round2(st.x + ox)}px;top:${round2(st.y + oy)}px;width:${st.width}px;height:${st.height}px" title="${escapeHtml(title)}">` +
|
|
1273
|
+
`<span class="bp-stage-n">${i + 1}</span><span class="bp-stage-t">${escapeHtml(title)}</span></div>`);
|
|
1274
|
+
})
|
|
1275
|
+
.join('');
|
|
1276
|
+
const cells = layout.cells
|
|
1277
|
+
.map((c) => {
|
|
1278
|
+
const rowText = BLUEPRINT_ROW_TEXT[c.row];
|
|
1279
|
+
const at = `data-bp-row="${c.row}" data-bp-col="${c.column}"`;
|
|
1280
|
+
if (c.cards.length === 0) {
|
|
1281
|
+
return (`<div class="bp-empty" ${at} title="근거 있는 연결이 없어요" ` +
|
|
1282
|
+
`style="left:${round2(c.x + ox + 10)}px;top:${round2(c.y + oy + 14)}px;width:${round2(c.width - 20)}px">${BLUEPRINT_EMPTY_TEXT}</div>`);
|
|
1283
|
+
}
|
|
1284
|
+
const pos = (b) => `style="left:${round2(b.x + ox)}px;top:${round2(b.y + oy)}px;width:${b.width}px;height:${b.height}px"`;
|
|
1285
|
+
const cards = c.cards
|
|
1286
|
+
.map((b) => {
|
|
1287
|
+
const it = b.item;
|
|
1288
|
+
if (it.type === 'step') {
|
|
1289
|
+
const st = stepById.get(it.id);
|
|
1290
|
+
const label = st?.label ?? it.id;
|
|
1291
|
+
const actor = st !== undefined ? actorById.get(st.actor) : undefined;
|
|
1292
|
+
const qs = questionCount.get(it.id) ?? 0;
|
|
1293
|
+
const cls = ['node', 'flow-step', 'bp-card', 'bp-step'];
|
|
1294
|
+
if (it.docOnly)
|
|
1295
|
+
cls.push('doc-only');
|
|
1296
|
+
const aria = `${label}, ${rowText}, ${actor?.label ?? st?.actor ?? ''}` +
|
|
1297
|
+
(it.docOnly ? ', 문서로만 확인' : '');
|
|
1298
|
+
return (`<div class="${cls.join(' ')}" data-node-id="${escapeHtml(it.id)}" ${at} role="button" tabindex="0" ` +
|
|
1299
|
+
`aria-label="${escapeHtml(aria)}" title="${escapeHtml(label)}" ${pos(b)}>` +
|
|
1300
|
+
`<span class="nm"><span class="t">${escapeHtml(label)}</span></span>` +
|
|
1301
|
+
`<span class="l2"><span class="bp-who">${escapeHtml(actor?.label ?? '')}</span>` +
|
|
1302
|
+
(qs > 0
|
|
1303
|
+
? `<span class="qb" title="확인할 질문">${iconUse('u-question')}${qs}</span>`
|
|
1304
|
+
: '') +
|
|
1305
|
+
`</span></div>`);
|
|
1306
|
+
}
|
|
1307
|
+
const n = nodeById.get(it.id);
|
|
1308
|
+
const dk = displayKindOf(n);
|
|
1309
|
+
const name = nodeName(n);
|
|
1310
|
+
const guess = isGuess(n);
|
|
1311
|
+
const engine = datastoreEngine(n);
|
|
1312
|
+
const kindText = chipTextOverride(n) ?? NODE_KIND_TEXT[dk];
|
|
1313
|
+
const cls = ['node', `k-${dk}`, 'bp-card'];
|
|
1314
|
+
if (it.docOnly)
|
|
1315
|
+
cls.push('doc-only');
|
|
1316
|
+
const aria = `${name}, ${kindText}, ${rowText}` +
|
|
1317
|
+
(guess ? `, ${INFERRED_BADGE} 이름` : '') +
|
|
1318
|
+
(it.docOnly ? ', 문서로만 확인' : '');
|
|
1319
|
+
const tooltip = n.displayName === undefined ? n.label : `${n.displayName}\n${n.label}`;
|
|
1320
|
+
return (`<div class="${cls.join(' ')}" data-node-id="${escapeHtml(it.id)}" ${at} role="button" tabindex="0" ` +
|
|
1321
|
+
`aria-label="${escapeHtml(aria)}" title="${escapeHtml(tooltip)}" ${pos(b)}>` +
|
|
1322
|
+
`<span class="kc">${engine !== undefined ? iconUse(`e-${engine}`, `brand b-${engine}`) : iconUse(`i-${dk}`)}${escapeHtml(chipTextOverride(n) ?? NODE_KIND_SHORT[dk])}</span>` +
|
|
1323
|
+
`<span class="nm"><span class="t">${escapeHtml(name)}</span>${guess ? `<span class="guess">${INFERRED_BADGE}</span>` : ''}</span>` +
|
|
1324
|
+
(n.displayName !== undefined ? `<span class="tc">${escapeHtml(n.label)}</span>` : '') +
|
|
1325
|
+
`</div>`);
|
|
1326
|
+
})
|
|
1327
|
+
.join('');
|
|
1328
|
+
const more = c.moreAt !== undefined
|
|
1329
|
+
? (() => {
|
|
1330
|
+
const names = c.hidden.map((it) => itemName(it.type, it.id));
|
|
1331
|
+
return (`<div class="bp-more" ${at} title="${escapeHtml(names.join('\n'))}" aria-label="${escapeHtml(`그 밖에 ${names.length}개: ${names.join(', ')}`)}" ` +
|
|
1332
|
+
`style="left:${round2(c.moreAt.x + ox)}px;top:${round2(c.moreAt.y + oy)}px">+${names.length}</div>`);
|
|
1333
|
+
})()
|
|
1334
|
+
: '';
|
|
1335
|
+
return cards + more;
|
|
1336
|
+
})
|
|
1337
|
+
.join('');
|
|
1338
|
+
return viewSection(levelId, projection, width, height, bands + guides + lines, '', stages + rowLabels + cells);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* 테이블 관계 그림. 테이블 상자에 컬럼 행을 세우고 외래 키 선은 컬럼 행에서 대상 컬럼 행으로 긋는다.
|
|
1342
|
+
* 근거가 코드나 스펙이면 실선, 문서 같은 약한 근거면 점선이고 근거가 없으면 긋지 않고 행 배지와 질문으로만 남긴다.
|
|
1343
|
+
* 좌표는 원본에서 낸 모델로 정하고 글자는 그린 쪽(공유본이면 가린 사본) 노드의 같은 자리 컬럼에서 꺼낸다
|
|
1344
|
+
*/
|
|
1345
|
+
const ER_ARIA_COLUMNS = 6;
|
|
1346
|
+
function renderErSection(levelId, projection, model, layout, nodeById, questions) {
|
|
1347
|
+
const ox = CANVAS_PAD_X;
|
|
1348
|
+
const oy = CANVAS_PAD_TOP;
|
|
1349
|
+
const width = round2(layout.width + ox * 2);
|
|
1350
|
+
const height = round2(layout.height + oy + 24);
|
|
1351
|
+
const name = (id) => {
|
|
1352
|
+
const n = nodeById.get(id);
|
|
1353
|
+
return n ? nodeName(n) : id;
|
|
1354
|
+
};
|
|
1355
|
+
const columnsOf = (id) => nodeById.get(id)?.columns ?? [];
|
|
1356
|
+
const colName = (id, index) => index !== undefined ? columnsOf(id)[index]?.name : undefined;
|
|
1357
|
+
const linkById = new Map(model.links.map((l) => [l.id, l]));
|
|
1358
|
+
const links = layout.routes
|
|
1359
|
+
.map((r) => {
|
|
1360
|
+
const l = linkById.get(r.id);
|
|
1361
|
+
const from = `${name(l.from)}.${colName(l.from, l.fromIndex) ?? ''}`;
|
|
1362
|
+
const toCol = colName(l.to, l.toIndex);
|
|
1363
|
+
const to = toCol !== undefined ? `${name(l.to)}.${toCol}` : name(l.to);
|
|
1364
|
+
const onDelete = l.onDelete !== undefined ? columnsOf(l.from)[l.fromIndex]?.references?.onDelete : undefined;
|
|
1365
|
+
const aria = `외래 키 ${from} → ${to}` +
|
|
1366
|
+
(onDelete !== undefined ? `, ON DELETE ${onDelete}` : '') +
|
|
1367
|
+
(l.lineStyle === 'dashed' ? ', 문서로만 확인' : '');
|
|
1368
|
+
const d = roundedPathD(r.points.map((p) => ({ x: p.x + ox - FLOW_PAD, y: p.y + oy - FLOW_PAD })));
|
|
1369
|
+
const dash = l.lineStyle === 'dashed' ? ` stroke-dasharray="${DASHED_PATTERN}"` : '';
|
|
1370
|
+
const tip = r.tip.map((p) => `${round2(p.x + ox)},${round2(p.y + oy)}`).join(' ');
|
|
1371
|
+
const label = r.label !== undefined && onDelete !== undefined
|
|
1372
|
+
? `<text class="er-od" x="${round2(r.label.x + ox)}" y="${round2(r.label.y + oy)}" text-anchor="${r.label.anchor}">ON DELETE ${escapeHtml(onDelete)}</text>`
|
|
1373
|
+
: '';
|
|
1374
|
+
return (`<g class="link er-fk${l.lineStyle === 'dashed' ? ' doc-only' : ''}" data-from="${escapeHtml(l.from)}" data-to="${escapeHtml(l.to)}" ` +
|
|
1375
|
+
`data-fk-id="${escapeHtml(l.id)}" data-line="${l.lineStyle}" tabindex="0" role="img" aria-label="${escapeHtml(aria)}">` +
|
|
1376
|
+
`<title>${escapeHtml(aria)}</title><path class="hit" d="${d}" stroke-width="12"/>` +
|
|
1377
|
+
`<path class="edge" d="${d}" stroke-width="1.6"${dash}/><polygon class="tip" points="${tip}"/>${label}</g>`);
|
|
1378
|
+
})
|
|
1379
|
+
.join('');
|
|
1380
|
+
const drawnTables = new Set(model.tables.map((t) => t.id));
|
|
1381
|
+
const boxById = new Map(layout.tables.map((b) => [b.id, b]));
|
|
1382
|
+
const refTarget = (ref) => `${name(ref.table)}${ref.column !== undefined ? `.${ref.column}` : ''}`;
|
|
1383
|
+
// 카드가 button이라 안쪽 행은 스크린리더에 안 읽힌다. 컬럼 요약을 이름에 싣는다
|
|
1384
|
+
const columnAria = (cols) => {
|
|
1385
|
+
const shown = cols.slice(0, ER_ARIA_COLUMNS).map((c) => {
|
|
1386
|
+
const tags = [];
|
|
1387
|
+
if (c.pk === true)
|
|
1388
|
+
tags.push('PK');
|
|
1389
|
+
if (c.references !== undefined)
|
|
1390
|
+
tags.push(`${isUnconfirmedFk(c) ? 'FK?' : 'FK'}→${refTarget(c.references)}`);
|
|
1391
|
+
if (c.unique === true)
|
|
1392
|
+
tags.push('UQ');
|
|
1393
|
+
if (c.nullable === true)
|
|
1394
|
+
tags.push('NULL');
|
|
1395
|
+
return tags.length > 0 ? `${c.name}(${tags.join(' ')})` : c.name;
|
|
1396
|
+
});
|
|
1397
|
+
const rest = cols.length - shown.length;
|
|
1398
|
+
return shown.join(', ') + (rest > 0 ? ` 외 ${rest}개` : '');
|
|
1399
|
+
};
|
|
1400
|
+
const cards = model.tables
|
|
1401
|
+
.map((t) => {
|
|
1402
|
+
const n = nodeById.get(t.id);
|
|
1403
|
+
const b = boxById.get(t.id);
|
|
1404
|
+
const tname = nodeName(n);
|
|
1405
|
+
const cols = n.columns ?? [];
|
|
1406
|
+
const qs = questions.filter((q) => q.subject.nodeId === t.id).length;
|
|
1407
|
+
const rows = t.rows
|
|
1408
|
+
.map((row, i) => {
|
|
1409
|
+
const cls = ['er-row'];
|
|
1410
|
+
if (i % 2 === 1)
|
|
1411
|
+
cls.push('even');
|
|
1412
|
+
if (row.type === 'fold') {
|
|
1413
|
+
const names = row.indexes.map((k) => cols[k]?.name ?? '');
|
|
1414
|
+
return (`<span class="${cls.join(' ')} er-fold" title="${escapeHtml(names.join('\n'))}">` +
|
|
1415
|
+
`${escapeHtml(erFoldText(names.length))}</span>`);
|
|
1416
|
+
}
|
|
1417
|
+
const c = cols[row.index];
|
|
1418
|
+
const ref = c.references;
|
|
1419
|
+
const badges = [];
|
|
1420
|
+
if (c.pk === true)
|
|
1421
|
+
badges.push({ text: 'PK', title: '기본 키' });
|
|
1422
|
+
if (ref !== undefined) {
|
|
1423
|
+
const target = refTarget(ref);
|
|
1424
|
+
if (isUnconfirmedFk(c))
|
|
1425
|
+
badges.push({ text: 'FK?', title: `${target}를 가리킨다는 근거를 못 찾은 외래 키` });
|
|
1426
|
+
else
|
|
1427
|
+
badges.push({
|
|
1428
|
+
text: 'FK',
|
|
1429
|
+
title: drawnTables.has(ref.table) ? `→ ${target}` : `→ ${target} (이 그림 밖)`,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
if (c.unique === true)
|
|
1433
|
+
badges.push({ text: 'UQ', title: '유일' });
|
|
1434
|
+
if (c.nullable === true)
|
|
1435
|
+
badges.push({ text: 'NULL', title: '비어 있을 수 있음' });
|
|
1436
|
+
const rowTitle = [c.name, c.type, ...badges.map((x) => x.title)]
|
|
1437
|
+
.filter((x) => x !== undefined)
|
|
1438
|
+
.join('\n');
|
|
1439
|
+
return (`<span class="${cls.join(' ')}" data-column="${escapeHtml(c.name)}" title="${escapeHtml(rowTitle)}">` +
|
|
1440
|
+
`<span class="er-n">${escapeHtml(c.name)}</span>` +
|
|
1441
|
+
badges
|
|
1442
|
+
.map((x) => `<span class="er-b${x.text === 'FK?' ? ' unconfirmed' : ''}">${x.text}</span>`)
|
|
1443
|
+
.join('') +
|
|
1444
|
+
`<span class="er-ty">${c.type !== undefined ? escapeHtml(c.type) : ''}</span></span>`);
|
|
1445
|
+
})
|
|
1446
|
+
.join('');
|
|
1447
|
+
const aria = `${tname}, 테이블, 컬럼 ${t.total}개` +
|
|
1448
|
+
(cols.length > 0 ? `: ${columnAria(cols)}` : '') +
|
|
1449
|
+
(qs > 0 ? `, 확인할 질문 ${qs}개` : '');
|
|
1450
|
+
const tooltip = n.displayName === undefined ? n.label : `${n.displayName}\n${n.label}`;
|
|
1451
|
+
return (`<div class="node k-db_table er-table" data-node-id="${escapeHtml(t.id)}" role="button" tabindex="0" ` +
|
|
1452
|
+
`aria-label="${escapeHtml(aria)}" title="${escapeHtml(tooltip)}" ` +
|
|
1453
|
+
`style="left:${round2(b.x + ox)}px;top:${round2(b.y + oy)}px;width:${b.width}px;height:${b.height}px">` +
|
|
1454
|
+
`<span class="er-head"><span class="kc">${iconUse('i-db_table')}${escapeHtml(NODE_KIND_SHORT.db_table)}</span>` +
|
|
1455
|
+
`<span class="nm"><span class="t">${escapeHtml(tname)}</span>${isGuess(n) ? `<span class="guess">${INFERRED_BADGE}</span>` : ''}</span>` +
|
|
1456
|
+
(qs > 0
|
|
1457
|
+
? `<span class="qb" title="확인할 질문">${iconUse('u-question')}${qs}</span>`
|
|
1458
|
+
: '') +
|
|
1459
|
+
`</span>` +
|
|
1460
|
+
(rows !== '' ? rows : `<span class="er-row er-fold">컬럼을 못 찾았어요</span>`) +
|
|
1461
|
+
`</div>`);
|
|
1462
|
+
})
|
|
1463
|
+
.join('');
|
|
1464
|
+
return viewSection(levelId, projection, width, height, '', links, cards);
|
|
1465
|
+
}
|
|
1112
1466
|
/** 흐름 단계와 전이마다 열린 질문 수. 카드 배지에 쓴다. 전이 질문은 출발 단계에 센다 */
|
|
1113
1467
|
function flowQuestionCount(questions, flows) {
|
|
1114
1468
|
const fromOf = new Map(flows.flatMap((f) => f.transitions.map((t) => [t.id, t.from])));
|
|
@@ -1122,7 +1476,7 @@ function flowQuestionCount(questions, flows) {
|
|
|
1122
1476
|
return out;
|
|
1123
1477
|
}
|
|
1124
1478
|
/** 클라이언트가 서랍과 흐름 버튼에 쓰는 흐름 데이터. 그린 단계와 전이만 싣는다 */
|
|
1125
|
-
function flowPayload(levels, flows, questions) {
|
|
1479
|
+
function flowPayload(levels, flows, questions, mask) {
|
|
1126
1480
|
const byId = new Map(flows.map((f) => [f.id, f]));
|
|
1127
1481
|
// 패널에서 그 단계나 전이에 걸린 질문을 바로 보여주려고 문장만 붙인다. 배지 숫자만으로는 무엇을 물어야 하는지 모른다
|
|
1128
1482
|
const asked = new Map();
|
|
@@ -1150,11 +1504,20 @@ function flowPayload(levels, flows, questions) {
|
|
|
1150
1504
|
...(f.service !== undefined ? { service: f.service } : {}),
|
|
1151
1505
|
title: f.title,
|
|
1152
1506
|
...(f.description !== undefined ? { description: f.description } : {}),
|
|
1153
|
-
|
|
1507
|
+
// 패널은 상태 값을 글자로만 쓰므로 키와 값을 같은 규칙으로 가려도 이름 찾기가 맞물린다
|
|
1508
|
+
...(f.stateLabels !== undefined
|
|
1509
|
+
? {
|
|
1510
|
+
stateLabels: Object.fromEntries(Object.entries(f.stateLabels).map(([k, v]) => [mask(k), v])),
|
|
1511
|
+
}
|
|
1512
|
+
: {}),
|
|
1154
1513
|
actors: f.actors,
|
|
1155
1514
|
steps: f.steps
|
|
1156
1515
|
.filter((st) => drawnSteps.has(st.id))
|
|
1157
|
-
.map((st) => withQuestions('s', {
|
|
1516
|
+
.map((st) => withQuestions('s', {
|
|
1517
|
+
...st,
|
|
1518
|
+
...(st.state !== undefined ? { state: mask(st.state) } : {}),
|
|
1519
|
+
path: pathOf.get(st.id),
|
|
1520
|
+
})),
|
|
1158
1521
|
transitions: f.transitions
|
|
1159
1522
|
.filter((t) => drawnTransitions.has(t.id))
|
|
1160
1523
|
.map((t) => withQuestions('t', t)),
|
|
@@ -1358,7 +1721,7 @@ function renderViewsPop(views) {
|
|
|
1358
1721
|
.join('');
|
|
1359
1722
|
return `<div id="views" class="pop" role="dialog" aria-label="질문별 그림" tabindex="-1" hidden><h3>질문별 그림</h3><ul class="flow-list view-list">${items}</ul></div>`;
|
|
1360
1723
|
}
|
|
1361
|
-
function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, sequences = [], }) {
|
|
1724
|
+
function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, sequences = [], lifecycles = false, blueprints = false, ers = false, }) {
|
|
1362
1725
|
const title = viewTitle(ir);
|
|
1363
1726
|
const vocab = irVocabulary(ir);
|
|
1364
1727
|
const nodeById = new Map(payload.nodes.map((n) => [n.id, n]));
|
|
@@ -1382,7 +1745,7 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
|
|
|
1382
1745
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
1383
1746
|
`<title>${escapeHtml(title)}</title>`,
|
|
1384
1747
|
`<script>${THEME_BOOT_SCRIPT}</script>`,
|
|
1385
|
-
`<style>${renderCss(vocab)}${hasViews ? `\n${VIEW_CSS}` : ''}${hasSequences ? `\n${SEQ_VIEW_CSS}` : ''}${vocab.packIds.includes('knowledge') ? `\n${DOC_CSS}` : ''}${hasCaptures ? `\n${CAPTURE_CSS}` : ''}${flowBranches || soloActor ? `\n${FLOW_BRANCH_CSS}` : ''}</style>`,
|
|
1748
|
+
`<style>${renderCss(vocab)}${hasViews ? `\n${VIEW_CSS}` : ''}${hasSequences ? `\n${SEQ_VIEW_CSS}` : ''}${vocab.packIds.includes('knowledge') ? `\n${DOC_CSS}` : ''}${hasCaptures ? `\n${CAPTURE_CSS}` : ''}${flowBranches || soloActor ? `\n${FLOW_BRANCH_CSS}` : ''}${lifecycles ? `\n${LIFECYCLE_CSS}` : ''}${blueprints ? `\n${BLUEPRINT_CSS}` : ''}${ers ? `\n${ER_CSS}` : ''}</style>`,
|
|
1386
1749
|
'</head>',
|
|
1387
1750
|
'<body>',
|
|
1388
1751
|
renderIconSprite(vocab),
|
|
@@ -1429,7 +1792,7 @@ export function renderArchitectureHtml(validated, layout, opts) {
|
|
|
1429
1792
|
title: viewTitle(ir),
|
|
1430
1793
|
nodes: payload.nodes,
|
|
1431
1794
|
edges: payload.edges.map(asCanvasEdge),
|
|
1432
|
-
layout,
|
|
1795
|
+
layout: shared ? maskLayoutText(layout) : layout,
|
|
1433
1796
|
enter: {},
|
|
1434
1797
|
hidden: false,
|
|
1435
1798
|
services: payload.services,
|
|
@@ -1445,6 +1808,7 @@ export function renderArchitectureHtml(validated, layout, opts) {
|
|
|
1445
1808
|
export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
1446
1809
|
const shared = opts.audience === 'shared';
|
|
1447
1810
|
const ir = shared ? redactForSharing(validated.ir) : validated.ir;
|
|
1811
|
+
const maskText = shared ? maskSharedText : (text) => text;
|
|
1448
1812
|
// 공유본은 캡처를 싣지 않으니 썸네일 자리도 비운다. 자리만 남기면 빈 칸이 어디 있는지 드러난다
|
|
1449
1813
|
const drilldown = shared
|
|
1450
1814
|
? {
|
|
@@ -1464,7 +1828,7 @@ export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
|
1464
1828
|
const docCover = docCoverSpec(base.nodes, base.docOverlay);
|
|
1465
1829
|
const nodeById = new Map(base.nodes.map((n) => [n.id, n]));
|
|
1466
1830
|
// 레벨 제목은 원본 이름으로 지었으므로 공유본에서는 노드 이름과 같이 가린다
|
|
1467
|
-
const levels = drilldown.levels.map((l) => shared ? { ...l, title: maskSharedText(l.title) } : l);
|
|
1831
|
+
const levels = drilldown.levels.map((l) => shared ? { ...l, title: maskSharedText(l.title), layout: maskLayoutText(l.layout) } : l);
|
|
1468
1832
|
const payload = {
|
|
1469
1833
|
...base,
|
|
1470
1834
|
levels: levels.map((l) => ({
|
|
@@ -1497,7 +1861,7 @@ export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
|
1497
1861
|
edges: [],
|
|
1498
1862
|
})),
|
|
1499
1863
|
],
|
|
1500
|
-
flows: flowPayload(drilldown.flows, drawnFlows, base.unresolved),
|
|
1864
|
+
flows: flowPayload(drilldown.flows, drawnFlows, base.unresolved, maskText),
|
|
1501
1865
|
});
|
|
1502
1866
|
}
|
|
1503
1867
|
const drawnMessages = validated.drawableMessageIds ?? new Set();
|
|
@@ -1522,6 +1886,49 @@ export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
|
1522
1886
|
});
|
|
1523
1887
|
}
|
|
1524
1888
|
const sequenceSteps = [];
|
|
1889
|
+
// 좌표는 원본 흐름으로 내고 글자는 그린 쪽(공유본이면 가린 사본) 흐름에서 꺼낸다
|
|
1890
|
+
const sourceFlows = new Map((validated.ir.flows ?? []).map((f) => [f.id, f]));
|
|
1891
|
+
const lifecycleOf = (p) => {
|
|
1892
|
+
if (p.shape !== 'lifecycle' || p.flow === undefined)
|
|
1893
|
+
return undefined;
|
|
1894
|
+
const source = sourceFlows.get(p.flow);
|
|
1895
|
+
const drawnFlow = flowById.get(p.flow);
|
|
1896
|
+
if (source === undefined || drawnFlow === undefined)
|
|
1897
|
+
return undefined;
|
|
1898
|
+
const model = deriveLifecycle(source, validated.drawableStepIds, validated.drawableTransitionIds);
|
|
1899
|
+
const text = shared
|
|
1900
|
+
? deriveLifecycle(drawnFlow, validated.drawableStepIds, validated.drawableTransitionIds)
|
|
1901
|
+
: model;
|
|
1902
|
+
return { model, text, flow: drawnFlow };
|
|
1903
|
+
};
|
|
1904
|
+
// 칸은 원본 지도로 채우고 글자는 그린 쪽 흐름과 노드에서 꺼낸다. 노드 id는 공유본에서도 그대로라 맞물린다
|
|
1905
|
+
const blueprintOf = (p) => {
|
|
1906
|
+
if (p.shape !== 'blueprint' || p.flow === undefined)
|
|
1907
|
+
return undefined;
|
|
1908
|
+
const source = sourceFlows.get(p.flow);
|
|
1909
|
+
const drawnFlow = flowById.get(p.flow);
|
|
1910
|
+
if (source === undefined || drawnFlow === undefined)
|
|
1911
|
+
return undefined;
|
|
1912
|
+
const model = deriveBlueprint(source, {
|
|
1913
|
+
nodes: validated.ir.nodes,
|
|
1914
|
+
edges: validated.ir.edges,
|
|
1915
|
+
drawableNodeIds: validated.drawableNodeIds,
|
|
1916
|
+
drawableEdgeIds: validated.drawableEdgeIds,
|
|
1917
|
+
drawableStepIds: validated.drawableStepIds,
|
|
1918
|
+
drawableTransitionIds: validated.drawableTransitionIds,
|
|
1919
|
+
});
|
|
1920
|
+
return { model, flow: drawnFlow };
|
|
1921
|
+
};
|
|
1922
|
+
// 테이블과 선 좌표는 원본 노드로 내고 글자는 그린 쪽 노드의 같은 자리 컬럼에서 꺼낸다
|
|
1923
|
+
const erOf = (p) => {
|
|
1924
|
+
if (p.shape !== 'er')
|
|
1925
|
+
return undefined;
|
|
1926
|
+
const model = deriveEr(p, {
|
|
1927
|
+
nodes: validated.ir.nodes,
|
|
1928
|
+
drawableNodeIds: validated.drawableNodeIds,
|
|
1929
|
+
});
|
|
1930
|
+
return model.tables.length > 0 ? model : undefined;
|
|
1931
|
+
};
|
|
1525
1932
|
const rootFlowLinks = flowLevels
|
|
1526
1933
|
.filter(({ flow }) => flow.service === undefined)
|
|
1527
1934
|
.map(({ level, flow }) => ({ level: level.id, title: flow.title }))
|
|
@@ -1548,19 +1955,51 @@ export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
|
1548
1955
|
? {
|
|
1549
1956
|
viewStrip: views.map((v) => {
|
|
1550
1957
|
const drawn = v.projection.messages.filter((m) => drawnMessages.has(m.id));
|
|
1958
|
+
const life = lifecycleOf(v.projection);
|
|
1959
|
+
const bp = blueprintOf(v.projection);
|
|
1960
|
+
const er = erOf(v.projection);
|
|
1551
1961
|
return {
|
|
1552
1962
|
...v,
|
|
1553
1963
|
steps: drawn.length,
|
|
1554
1964
|
actors: v.projection.participants?.length ??
|
|
1555
1965
|
new Set(drawn.flatMap((m) => [m.from, m.to])).size,
|
|
1966
|
+
...(life !== undefined
|
|
1967
|
+
? {
|
|
1968
|
+
summary: `상태 ${life.model.states.length}, 전이 ${life.model.transitions.length}`,
|
|
1969
|
+
}
|
|
1970
|
+
: {}),
|
|
1971
|
+
...(bp !== undefined
|
|
1972
|
+
? {
|
|
1973
|
+
summary: `단계 ${bp.model.columns.length}, 빈 칸 ${bp.model.columns
|
|
1974
|
+
.flatMap((c) => Object.values(c.cells))
|
|
1975
|
+
.filter((items) => items.length === 0).length}`,
|
|
1976
|
+
}
|
|
1977
|
+
: {}),
|
|
1978
|
+
...(er !== undefined
|
|
1979
|
+
? { summary: `테이블 ${er.tables.length}, 외래 키 ${er.links.length}` }
|
|
1980
|
+
: {}),
|
|
1556
1981
|
};
|
|
1557
1982
|
}),
|
|
1558
1983
|
}
|
|
1559
1984
|
: {}),
|
|
1560
1985
|
}))
|
|
1561
|
-
.concat(flowLevels.map(({ level, flow }) => renderFlowSection(level, flow, questionCount, drawableNodes, captureOf)))
|
|
1986
|
+
.concat(flowLevels.map(({ level, flow }) => renderFlowSection(level, flow, questionCount, drawableNodes, captureOf, maskText)))
|
|
1562
1987
|
.concat(views.map((v) => {
|
|
1563
1988
|
const p = v.projection;
|
|
1989
|
+
const life = lifecycleOf(p);
|
|
1990
|
+
if (life !== undefined) {
|
|
1991
|
+
const layout = computeLifecycleLayout(life.model);
|
|
1992
|
+
return renderLifecycleSection(v.level, p, life.flow, life.text, layout, questionCount, maskText);
|
|
1993
|
+
}
|
|
1994
|
+
const bp = blueprintOf(p);
|
|
1995
|
+
if (bp !== undefined) {
|
|
1996
|
+
const layout = computeBlueprintLayout(bp.model);
|
|
1997
|
+
return renderBlueprintSection(v.level, p, bp.flow, bp.model, layout, nodeById, questionCount, maskText);
|
|
1998
|
+
}
|
|
1999
|
+
const er = erOf(p);
|
|
2000
|
+
if (er !== undefined) {
|
|
2001
|
+
return renderErSection(v.level, p, er, computeErLayout(er), nodeById, base.unresolved);
|
|
2002
|
+
}
|
|
1564
2003
|
if (p.shape === 'dataflow') {
|
|
1565
2004
|
const layout = computeDataflowLayout(p, drawnMessages, nodeById);
|
|
1566
2005
|
return renderDataflowSection(v.level, p, layout, nodeById, base.services);
|
|
@@ -1587,6 +2026,9 @@ export function renderDrilldownHtml(validated, drilldownIn, opts) {
|
|
|
1587
2026
|
...(views.length > 0 ? { views } : {}),
|
|
1588
2027
|
mixedBundles: levels.some((l) => l.edges.some((e) => e.inferred !== undefined)),
|
|
1589
2028
|
...(sequenceSteps.length > 0 ? { sequences: sequenceSteps } : {}),
|
|
2029
|
+
...(views.some((v) => v.projection.shape === 'lifecycle') ? { lifecycles: true } : {}),
|
|
2030
|
+
...(views.some((v) => v.projection.shape === 'blueprint') ? { blueprints: true } : {}),
|
|
2031
|
+
...(views.some((v) => v.projection.shape === 'er') ? { ers: true } : {}),
|
|
1590
2032
|
});
|
|
1591
2033
|
}
|
|
1592
2034
|
//# sourceMappingURL=html-renderer.js.map
|