1285 lines
40 KiB
HTML
1285 lines
40 KiB
HTML
<!doctype html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>HM Biz Process Map</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--sm-chip-width: 102px;
|
|
--sm-chip-height: 42px;
|
|
--bg: #04140f;
|
|
--panel: rgba(7, 43, 33, 0.82);
|
|
--panel-border: rgba(132, 187, 161, 0.18);
|
|
--text: #f3f1e8;
|
|
--muted: #a8c0b3;
|
|
--line: rgba(132, 187, 161, 0.16);
|
|
--accent: #d3a45f;
|
|
--accent-soft: rgba(211, 164, 95, 0.16);
|
|
--warning: #ffe09b;
|
|
--warning-bg: rgba(79, 58, 24, 0.58);
|
|
--surface-soft: #f4f1e8;
|
|
--surface-plain: #fbf8f1;
|
|
--ink-strong: #183128;
|
|
--ink-soft: #55695f;
|
|
--shadow: 0 28px 64px rgba(0, 0, 0, 0.34);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html {
|
|
min-height: 100%;
|
|
overflow-y: auto;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
font-family: "Pretendard Variable", "Pretendard", "Noto Sans KR", system-ui, sans-serif;
|
|
color: var(--text);
|
|
background:
|
|
linear-gradient(rgba(120, 186, 158, 0.08) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(120, 186, 158, 0.08) 1px, transparent 1px),
|
|
radial-gradient(circle at 72% 22%, rgba(28, 110, 83, 0.38), transparent 28%),
|
|
radial-gradient(circle at 24% 68%, rgba(201, 152, 80, 0.12), transparent 24%),
|
|
linear-gradient(135deg, #04120d 0%, #0a241b 45%, #0f4534 100%);
|
|
background-size: 40px 40px, 40px 40px, auto, auto, auto;
|
|
position: relative;
|
|
overflow-x: auto;
|
|
overflow-y: auto;
|
|
}
|
|
body::before,
|
|
body::after {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
body::before {
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(circle at 26% 24%, rgba(239, 192, 109, 0.12), transparent 18%),
|
|
linear-gradient(120deg, transparent 0 64%, rgba(255, 244, 220, 0.05) 64% 76%, transparent 76% 100%);
|
|
opacity: 1;
|
|
box-shadow: none;
|
|
}
|
|
body::after {
|
|
background:
|
|
linear-gradient(90deg, rgba(0, 0, 0, 0.34), transparent 22%, transparent 78%, rgba(0, 0, 0, 0.42)),
|
|
radial-gradient(circle at center, transparent 50%, rgba(0, 0, 0, 0.2) 100%);
|
|
}
|
|
.page {
|
|
min-height: 100vh;
|
|
padding: 18px;
|
|
position: relative;
|
|
z-index: 1;
|
|
overflow: visible;
|
|
}
|
|
.shell {
|
|
min-height: calc(100vh - 36px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
padding: 16px 18px 18px;
|
|
border: 1px solid var(--panel-border);
|
|
border-radius: 24px;
|
|
background: var(--panel);
|
|
box-shadow: var(--shadow);
|
|
overflow: visible;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.topbar {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.title-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
}
|
|
.eyebrow {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 38px;
|
|
line-height: 1.08;
|
|
letter-spacing: -.03em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
text-shadow: 0 10px 22px rgba(0, 0, 0, 0.28);
|
|
}
|
|
.subcopy {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
max-width: 560px;
|
|
}
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
min-width: 340px;
|
|
}
|
|
.status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-height: 42px;
|
|
padding: 0 14px;
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(132, 187, 161, 0.22);
|
|
background: rgba(7, 41, 32, 0.92);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
.status::before {
|
|
content: "";
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: rgba(168, 192, 179, 0.9);
|
|
box-shadow: 0 0 0 5px rgba(168, 192, 179, 0.14);
|
|
}
|
|
.status[data-state="ready"]::before {
|
|
background: #6fe0ab;
|
|
box-shadow: 0 0 0 5px rgba(111, 224, 171, 0.14);
|
|
}
|
|
.status[data-state="loading"]::before {
|
|
background: #ffd28b;
|
|
box-shadow: 0 0 0 5px rgba(255, 210, 139, 0.14);
|
|
}
|
|
.status[data-state="error"]::before {
|
|
background: #ff9d8f;
|
|
box-shadow: 0 0 0 5px rgba(255, 157, 143, 0.14);
|
|
}
|
|
.stamp {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
padding: 0 4px;
|
|
}
|
|
.btn,
|
|
select {
|
|
height: 42px;
|
|
border-radius: 14px;
|
|
border: 1px solid #d3dbd4;
|
|
background: #fffdf8;
|
|
color: var(--ink-strong);
|
|
font: inherit;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
padding: 0 14px;
|
|
}
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
|
|
}
|
|
.btn:hover {
|
|
transform: translateY(-1px);
|
|
border-color: rgba(229, 188, 114, 0.48);
|
|
box-shadow: 0 12px 22px rgba(0, 0, 0, 0.2);
|
|
}
|
|
.btn.primary {
|
|
background: linear-gradient(135deg, #b8833d 0%, #efc06d 100%);
|
|
color: #10241c;
|
|
border-color: rgba(241, 202, 128, 0.7);
|
|
}
|
|
.filters {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filters-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filters-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.sm-legend {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
min-height: 24px;
|
|
}
|
|
.sm-legend-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border-radius: 14px;
|
|
border: 1px solid #d7ddd7;
|
|
background: linear-gradient(180deg, #fffdf9 0%, #f4f6f1 100%);
|
|
color: var(--ink-strong);
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
box-shadow:
|
|
0 6px 16px rgba(16, 36, 28, 0.06),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
|
}
|
|
.sm-legend-swatch {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 5px;
|
|
border: 1px solid rgba(62, 86, 72, 0.18);
|
|
background: rgba(168, 192, 179, 0.55);
|
|
flex: 0 0 auto;
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.45),
|
|
0 1px 2px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.map-panel {
|
|
min-height: 0;
|
|
flex: 1 1 auto;
|
|
border: 1px solid #e2e7e1;
|
|
border-radius: 22px;
|
|
background: linear-gradient(180deg, #fcfaf5 0%, #f7f8f4 100%);
|
|
overflow: auto;
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
|
}
|
|
.map-panel.loading {
|
|
opacity: .8;
|
|
}
|
|
.map-body {
|
|
min-width: 0;
|
|
padding: 22px 22px 28px;
|
|
background:
|
|
linear-gradient(180deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0.62) 100%);
|
|
}
|
|
.sitemap-full {
|
|
transform-origin: top left;
|
|
will-change: transform;
|
|
}
|
|
.empty-state {
|
|
display: grid;
|
|
place-items: center;
|
|
min-height: 320px;
|
|
text-align: center;
|
|
color: var(--ink-soft);
|
|
font-weight: 700;
|
|
}
|
|
.sm-section-title {
|
|
margin: 4px 0 0;
|
|
color: #7f683f;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
letter-spacing: .03em;
|
|
text-transform: uppercase;
|
|
}
|
|
.sm-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(128px, 146px) minmax(0, 1fr);
|
|
align-items: start;
|
|
gap: 20px;
|
|
min-width: 0;
|
|
padding: 15px 0;
|
|
border-bottom: 1px solid #d9dfd8;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
}
|
|
.sm-main {
|
|
min-width: 146px;
|
|
font-weight: 900;
|
|
color: var(--ink-strong);
|
|
font-size: 18px;
|
|
line-height: 1.25;
|
|
padding-top: 3px;
|
|
align-self: flex-start;
|
|
}
|
|
.sm-main.active-focus {
|
|
color: #9d7640;
|
|
text-shadow: none;
|
|
}
|
|
.sm-chain {
|
|
display: flex;
|
|
align-items: center;
|
|
align-content: flex-start;
|
|
gap: 10px 0;
|
|
flex-wrap: wrap;
|
|
min-width: 0;
|
|
flex: 1 1 auto;
|
|
}
|
|
.sm-segment {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex: 0 0 var(--flow-col-width, auto);
|
|
width: var(--flow-col-width, auto);
|
|
min-width: var(--flow-col-width, auto);
|
|
max-width: var(--flow-col-width, auto);
|
|
}
|
|
.sm-segment.is-first {
|
|
width: var(--sm-chip-width);
|
|
min-width: var(--sm-chip-width);
|
|
max-width: var(--sm-chip-width);
|
|
}
|
|
.sm-segment.has-manual-link {
|
|
gap: 0;
|
|
}
|
|
.sm-arrow {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 30px;
|
|
height: 2px;
|
|
margin: 0;
|
|
background: rgba(108, 129, 116, 0.58);
|
|
font-size: 0;
|
|
line-height: 0;
|
|
flex: 0 0 auto;
|
|
}
|
|
.sm-arrow::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: -1px;
|
|
top: -4px;
|
|
width: 0;
|
|
height: 0;
|
|
border-top: 5px solid transparent;
|
|
border-bottom: 5px solid transparent;
|
|
border-left: 8px solid rgba(108, 129, 116, 0.58);
|
|
}
|
|
.sm-link-wrap {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
width: var(--flow-link-width, 30px);
|
|
min-width: var(--flow-link-width, 30px);
|
|
max-width: var(--flow-link-width, 30px);
|
|
flex: 0 0 auto;
|
|
justify-content: stretch;
|
|
}
|
|
.sm-link-arrow-frag {
|
|
color: var(--accent);
|
|
font-size: 0;
|
|
line-height: 0;
|
|
flex: 1 1 12px;
|
|
min-width: 12px;
|
|
height: 0;
|
|
border-top: 2px dashed rgba(171, 134, 73, 0.86);
|
|
display: inline-block;
|
|
}
|
|
.sm-chip {
|
|
border: 1px solid #cfd7d0;
|
|
border-radius: 16px;
|
|
width: var(--sm-chip-width);
|
|
min-width: var(--sm-chip-width);
|
|
min-height: var(--sm-chip-height);
|
|
padding: 5px 7px;
|
|
background: #fffdf8;
|
|
font-size: 13px;
|
|
font-weight: 900;
|
|
color: #183026;
|
|
line-height: 1.25;
|
|
white-space: normal;
|
|
word-break: keep-all;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
|
}
|
|
.sm-chip.system-colored {
|
|
background: var(--sys-bg, #f8fbff);
|
|
border-color: var(--sys-border, #d2ddeb);
|
|
color: var(--sys-text, #1f2937);
|
|
}
|
|
.sm-chip.multi-system {
|
|
background: linear-gradient(135deg, var(--sys-bg-a, #eef2ff) 0 49.5%, var(--sys-bg-b, #ecfeff) 50% 100%);
|
|
border-color: var(--sys-border-a, #b6c6e0);
|
|
color: var(--sys-text, #1f4f83);
|
|
box-shadow: inset -1px -1px 0 0 var(--sys-border-b, #b6c6e0);
|
|
}
|
|
.sm-chip.no-system {
|
|
background: var(--sys-bg, #edf3fb);
|
|
border-color: var(--sys-border, #9fb8d7);
|
|
color: var(--sys-text, #334155);
|
|
}
|
|
.sm-chip.match {
|
|
border-color: rgba(171, 134, 73, 0.72);
|
|
box-shadow: 0 0 0 2px rgba(171, 134, 73, 0.12) inset;
|
|
}
|
|
.sm-chip.active-focus,
|
|
.sm-chip.active-focus.system-colored,
|
|
.sm-chip.active-focus.multi-system,
|
|
.sm-chip.active-focus.no-system,
|
|
.sm-chip.active-focus.match {
|
|
border-color: rgba(171, 134, 73, 0.78) !important;
|
|
box-shadow:
|
|
0 0 0 4px rgba(171, 134, 73, 0.16),
|
|
0 12px 18px rgba(0, 0, 0, 0.08),
|
|
inset 0 0 0 1px rgba(255, 255, 255, 0.68) !important;
|
|
transform: translateY(-1px);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
.sm-chip.dim,
|
|
.sm-arrow.dim,
|
|
.sm-main.dim,
|
|
.sm-link-wrap.dim,
|
|
.sm-shared-box.dim,
|
|
.sm-link-reason.dim {
|
|
opacity: 0.14;
|
|
filter: grayscale(0.2);
|
|
}
|
|
.sm-link-reason {
|
|
border: 0;
|
|
background: #f7edd7;
|
|
color: #5f4618;
|
|
border-radius: 12px;
|
|
padding: 3px 7px;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
min-width: 0;
|
|
max-width: calc(var(--flow-link-width, 54px) - 20px);
|
|
white-space: normal;
|
|
overflow: visible;
|
|
text-overflow: clip;
|
|
text-align: center;
|
|
word-break: keep-all;
|
|
overflow-wrap: anywhere;
|
|
flex: 0 1 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.sm-link-wrap.manual {
|
|
gap: 4px;
|
|
min-width: 0;
|
|
flex: 0 0 auto;
|
|
justify-content: center;
|
|
}
|
|
.sm-link-wrap.manual .sm-link-arrow-frag:last-child {
|
|
position: relative;
|
|
}
|
|
.sm-link-wrap.manual .sm-link-arrow-frag:last-child::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: -2px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 0;
|
|
height: 0;
|
|
border-top: 5px solid transparent;
|
|
border-bottom: 5px solid transparent;
|
|
border-left: 8px solid rgba(171, 134, 73, 0.86);
|
|
}
|
|
.sm-link-wrap:not(.manual) .sm-arrow {
|
|
width: 100%;
|
|
flex: 1 1 auto;
|
|
}
|
|
.sm-shared-box {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
flex-wrap: wrap;
|
|
max-width: 100%;
|
|
border: 1px dashed rgba(171, 134, 73, 0.6);
|
|
border-radius: 12px;
|
|
padding: 5px 7px;
|
|
background: #f5ecda;
|
|
}
|
|
@media (max-width: 1120px) {
|
|
:root {
|
|
--sm-chip-width: 96px;
|
|
--sm-chip-height: 40px;
|
|
}
|
|
.page { padding: 10px; }
|
|
.shell {
|
|
min-height: calc(100vh - 20px);
|
|
padding: 14px;
|
|
border-radius: 18px;
|
|
}
|
|
.topbar { flex-direction: column; }
|
|
.toolbar { min-width: 0; justify-content: flex-start; }
|
|
.filters { align-items: flex-start; }
|
|
.map-body { padding: 16px; }
|
|
.sm-row { gap: 18px; }
|
|
.sm-main { min-width: 128px; font-size: 18px; }
|
|
.sm-chip { font-size: 13px; padding: 6px 8px; }
|
|
.sm-link-reason { min-width: 52px; max-width: 132px; font-size: 10px; padding: 3px 6px; }
|
|
.sm-segment.has-manual-link { gap: 5px; }
|
|
.sm-link-arrow-frag { min-width: 8px; }
|
|
.sm-arrow,
|
|
.sm-link-wrap {
|
|
min-width: 28px;
|
|
width: 28px;
|
|
}
|
|
}
|
|
@media (max-width: 820px) {
|
|
.sm-row {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
gap: 12px;
|
|
}
|
|
.sm-main {
|
|
min-width: 0;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
<div class="shell">
|
|
<div class="topbar">
|
|
<div class="title-wrap">
|
|
<h1>Process Map</h1>
|
|
</div>
|
|
<div class="toolbar">
|
|
<div id="syncStatus" class="status" data-state="loading">DB 불러오는 중</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<div id="sitemapLegend" class="sm-legend"></div>
|
|
<div class="filters-right">
|
|
<select id="sitemapTeamSelect">
|
|
<option value="">전체 팀 필터</option>
|
|
</select>
|
|
<select id="sitemapSystemSelect">
|
|
<option value="">전체 시스템 필터</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mapPanel" class="map-panel loading">
|
|
<div id="mapBody" class="map-body">
|
|
<div id="sitemapFullContent" class="sitemap-full">
|
|
<div class="empty-state">데이터를 불러오는 중입니다.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./app_client_config.js?v=20260327-1110"></script>
|
|
<script src="./process_map_shared.js"></script>
|
|
<script>
|
|
const APP_CONFIG = window.HMBIZ_CONFIG || {};
|
|
const API_BASE_URL = APP_CONFIG.apiBaseUrl || '';
|
|
const API_FLOW_DATA_URL = `${API_BASE_URL}/api/flow-data`;
|
|
const DEFAULT_JSON_CANDIDATES = APP_CONFIG.defaultJsonCandidates || [
|
|
'/flow-data-2026-03-23.json',
|
|
'/flow-data-2026-03-21_4.json'
|
|
];
|
|
const BROADCAST_CHANNEL_NAME = APP_CONFIG.processMapBroadcastChannel || 'hm-biz-process-map';
|
|
const STORAGE_SIGNAL_KEY = APP_CONFIG.processMapStorageSignalKey || 'hm-biz-process-map:refresh';
|
|
const NAVIGATION_SIGNAL_KEY = APP_CONFIG.processMapNavigationStorageKey || 'hm-biz-process-map:navigate';
|
|
const FOCUS_SIGNAL_KEY = APP_CONFIG.processMapFocusStorageKey || 'hm-biz-process-map:focus';
|
|
const POLL_INTERVAL_MS = APP_CONFIG.processMapPollIntervalMs || 15000;
|
|
const MAIN_APP_PATH = APP_CONFIG.mainAppPath || '/';
|
|
const URL_CONTEXT = new URL(window.location.href);
|
|
const MAIN_APP_ORIGIN = (() => {
|
|
const value = String(URL_CONTEXT.searchParams.get('mainOrigin') || '').trim();
|
|
if (!value) return window.location.origin;
|
|
try {
|
|
const parsed = new URL(value);
|
|
if (!/^https?:$/i.test(parsed.protocol)) return window.location.origin;
|
|
return parsed.origin;
|
|
} catch (error) {
|
|
return window.location.origin;
|
|
}
|
|
})();
|
|
const MAIN_APP_PATH_OVERRIDE = (() => {
|
|
const value = String(URL_CONTEXT.searchParams.get('mainPath') || '').trim();
|
|
if (!value) return '';
|
|
return value.startsWith('/') ? value : `/${value}`;
|
|
})();
|
|
const SHARED_SEGMENT = APP_CONFIG.sharedSegment || ['전표작성', '검토', '출금'];
|
|
const SITEMAP_AUTO_LINK_WIDTH = APP_CONFIG.sitemapAutoLinkWidth || 132;
|
|
const SITEMAP_MANUAL_LINK_WIDTH = APP_CONFIG.sitemapManualLinkWidth || 156;
|
|
const FORCE_REMOTE_SYNC = Boolean(APP_CONFIG.forceRemoteSync);
|
|
const API_FETCH_TIMEOUT_MS = Number(APP_CONFIG.apiFetchTimeoutMs || 4500);
|
|
const STEP_META_KEY = 'flow_step_meta_v1';
|
|
const FLOW_MODEL_KEY = 'flow_model_v1';
|
|
|
|
const syncStatusEl = document.getElementById('syncStatus');
|
|
const sitemapTeamSelectEl = document.getElementById('sitemapTeamSelect');
|
|
const sitemapSystemSelectEl = document.getElementById('sitemapSystemSelect');
|
|
const sitemapFullContentEl = document.getElementById('sitemapFullContent');
|
|
const sitemapLegendEl = document.getElementById('sitemapLegend');
|
|
const mapPanelEl = document.getElementById('mapPanel');
|
|
const mapBodyEl = document.getElementById('mapBody');
|
|
|
|
let MAIN_STEPS = [];
|
|
let SUB_FLOW = {};
|
|
let COMMON_ITEMS = [];
|
|
let COMMON_SUB_FLOW = {};
|
|
let stepMeta = {};
|
|
let subFlowLinks = {};
|
|
let loadingInFlight = false;
|
|
let pendingReload = false;
|
|
let currentUpdatedAt = '';
|
|
let activeFocus = null;
|
|
let fitTimer = null;
|
|
let fitObserver = null;
|
|
let hasRenderableData = false;
|
|
let lastPayloadSignature = '';
|
|
let lastInlineDataAt = 0;
|
|
let lastFocusRequestAt = 0;
|
|
|
|
function buildNoCacheUrl(url) {
|
|
const target = String(url || '').trim();
|
|
if (!target) return target;
|
|
const joiner = target.includes('?') ? '&' : '?';
|
|
return `${target}${joiner}_ts=${Date.now()}`;
|
|
}
|
|
|
|
function hashPayloadText(text) {
|
|
const value = String(text || '');
|
|
let hash = 0;
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
hash = ((hash << 5) - hash) + value.charCodeAt(i);
|
|
hash |= 0;
|
|
}
|
|
return String(hash);
|
|
}
|
|
|
|
function computePayloadSignature(payload) {
|
|
if (!payload || typeof payload !== 'object') return '';
|
|
const flowModel = payload.flowModel && typeof payload.flowModel === 'object' ? payload.flowModel : {};
|
|
const stepMetaPayload = payload.stepMeta && typeof payload.stepMeta === 'object'
|
|
? payload.stepMeta
|
|
: {};
|
|
return [
|
|
hashPayloadText(JSON.stringify(flowModel)),
|
|
hashPayloadText(JSON.stringify(stepMetaPayload))
|
|
].join('|');
|
|
}
|
|
|
|
function applyPayloadToView(payload, sourceLabel = '') {
|
|
const signature = computePayloadSignature(payload);
|
|
const changed = !hasRenderableData || !signature || signature !== lastPayloadSignature;
|
|
const applied = applyRemotePayload(payload);
|
|
if (!applied) return false;
|
|
if (changed) {
|
|
processMapRenderer.refreshFilterOptions();
|
|
processMapRenderer.render();
|
|
scheduleMapFit();
|
|
}
|
|
hasRenderableData = true;
|
|
if (signature) lastPayloadSignature = signature;
|
|
if (sourceLabel) setSyncStatus('ready', sourceLabel);
|
|
return true;
|
|
}
|
|
|
|
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = API_FETCH_TIMEOUT_MS) {
|
|
const target = String(url || '').trim();
|
|
if (!target) return null;
|
|
const finalUrl = buildNoCacheUrl(target);
|
|
if (typeof AbortController === 'undefined') {
|
|
const response = await fetch(finalUrl, options);
|
|
if (!response.ok) return null;
|
|
return response.json();
|
|
}
|
|
const controller = new AbortController();
|
|
const timerId = window.setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const response = await fetch(finalUrl, {
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
if (!response.ok) return null;
|
|
return await response.json();
|
|
} finally {
|
|
window.clearTimeout(timerId);
|
|
}
|
|
}
|
|
const processMapRenderer = window.createProcessMapRenderer({
|
|
elements: {
|
|
fullContentEl: sitemapFullContentEl,
|
|
legendEl: sitemapLegendEl,
|
|
teamSelectEl: sitemapTeamSelectEl,
|
|
systemSelectEl: sitemapSystemSelectEl
|
|
},
|
|
getState: () => ({
|
|
mainSteps: MAIN_STEPS,
|
|
subFlow: SUB_FLOW,
|
|
commonItems: COMMON_ITEMS,
|
|
commonSubFlow: COMMON_SUB_FLOW,
|
|
stepMeta,
|
|
subFlowLinks
|
|
}),
|
|
onMainClick: jumpToMainDetail,
|
|
onStepClick: jumpToMainStep,
|
|
onCommonRootClick: jumpToCommonRoot,
|
|
isActiveFocus,
|
|
linkWidths: {
|
|
auto: SITEMAP_AUTO_LINK_WIDTH,
|
|
manual: SITEMAP_MANUAL_LINK_WIDTH
|
|
},
|
|
sharedSegment: SHARED_SEGMENT
|
|
});
|
|
|
|
function setActiveFocus(nextFocus) {
|
|
activeFocus = nextFocus;
|
|
}
|
|
|
|
function clearMapFit() {
|
|
if (!sitemapFullContentEl || !mapBodyEl) return;
|
|
sitemapFullContentEl.style.zoom = '';
|
|
sitemapFullContentEl.style.transform = '';
|
|
mapBodyEl.style.width = '';
|
|
mapBodyEl.style.height = '';
|
|
}
|
|
|
|
function fitMapToViewport() {
|
|
if (!sitemapFullContentEl || !mapPanelEl || !mapBodyEl) return;
|
|
clearMapFit();
|
|
if (!sitemapFullContentEl.children.length) return;
|
|
}
|
|
|
|
function scheduleMapFit() {
|
|
window.clearTimeout(fitTimer);
|
|
fitTimer = window.setTimeout(() => {
|
|
// Wait for any deferred layout work (e.g. column alignment) to settle.
|
|
window.requestAnimationFrame(() => {
|
|
window.requestAnimationFrame(() => {
|
|
fitMapToViewport();
|
|
});
|
|
});
|
|
}, 30);
|
|
}
|
|
|
|
function ensureFitObserver() {
|
|
if (fitObserver || typeof ResizeObserver === 'undefined') return;
|
|
fitObserver = new ResizeObserver(() => {
|
|
scheduleMapFit();
|
|
});
|
|
fitObserver.observe(sitemapFullContentEl);
|
|
fitObserver.observe(mapPanelEl);
|
|
}
|
|
|
|
function isSameFocus(left, right) {
|
|
if (!left || !right) return false;
|
|
return left.type === right.type
|
|
&& left.mainStep === right.mainStep
|
|
&& (left.step || '') === (right.step || '')
|
|
&& Boolean(left.isCommon) === Boolean(right.isCommon);
|
|
}
|
|
|
|
function applyExternalFocus(payload) {
|
|
if (!payload || payload.type !== 'focus-updated' || !payload.focus) return;
|
|
if (isSameFocus(activeFocus, payload.focus)) return;
|
|
setActiveFocus(payload.focus);
|
|
processMapRenderer.render();
|
|
}
|
|
window.applyProcessMapExternalFocus = applyExternalFocus;
|
|
|
|
function applyExternalData(payload) {
|
|
if (!payload || payload.type !== 'process-map-data' || !payload.payload) return false;
|
|
const applied = applyPayloadToView(payload.payload, '메인 화면 데이터 표시 중');
|
|
if (applied) lastInlineDataAt = Date.now();
|
|
return applied;
|
|
}
|
|
window.applyProcessMapExternalData = applyExternalData;
|
|
|
|
function syncFocusFromOpener() {
|
|
try {
|
|
if (!window.opener || window.opener.closed) return false;
|
|
if (typeof window.opener.getCurrentProcessMapFocusPayload !== 'function') return false;
|
|
const payload = window.opener.getCurrentProcessMapFocusPayload();
|
|
if (!payload || payload.type !== 'focus-updated' || !payload.focus) return false;
|
|
const changed = !isSameFocus(activeFocus, payload.focus);
|
|
if (changed) applyExternalFocus(payload);
|
|
return changed;
|
|
} catch (error) {
|
|
requestFocusFromMainViaPostMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function requestDataFromMainViaPostMessage() {
|
|
try {
|
|
if (!window.opener || window.opener.closed) return false;
|
|
window.opener.postMessage({ type: 'process-map-request-data', at: Date.now() }, MAIN_APP_ORIGIN);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function pullDataFromOpenerSync() {
|
|
try {
|
|
if (!window.opener || window.opener.closed) return null;
|
|
if (typeof window.opener.getCurrentProcessMapDataPayload !== 'function') return null;
|
|
const payload = window.opener.getCurrentProcessMapDataPayload();
|
|
if (!payload || payload.type !== 'process-map-data') return null;
|
|
return payload;
|
|
} catch (error) {
|
|
requestDataFromMainViaPostMessage();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function requestFocusFromMainViaPostMessage() {
|
|
try {
|
|
if (!window.opener || window.opener.closed) return false;
|
|
const now = Date.now();
|
|
if (now - lastFocusRequestAt < 1200) return false;
|
|
lastFocusRequestAt = now;
|
|
window.opener.postMessage({ type: 'process-map-request-focus', at: Date.now() }, MAIN_APP_ORIGIN);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isActiveFocus(type, mainStep, step = '', isCommon = false) {
|
|
if (!activeFocus) return false;
|
|
return activeFocus.type === type
|
|
&& activeFocus.mainStep === mainStep
|
|
&& (activeFocus.step || '') === (step || '')
|
|
&& Boolean(activeFocus.isCommon) === Boolean(isCommon);
|
|
}
|
|
|
|
function buildMainFocusUrl(focusType, mainStep, step = '', isCommon = false) {
|
|
const basePath = String(MAIN_APP_PATH_OVERRIDE || MAIN_APP_PATH || '/').trim() || '/';
|
|
const normalizedPath = basePath.startsWith('/') ? basePath : `/${basePath}`;
|
|
const url = new URL(normalizedPath, MAIN_APP_ORIGIN);
|
|
url.searchParams.set('focusType', focusType);
|
|
url.searchParams.set('mainStep', mainStep);
|
|
if (step) url.searchParams.set('step', step);
|
|
if (isCommon) url.searchParams.set('isCommon', '1');
|
|
return url.toString();
|
|
}
|
|
|
|
function buildMainFocusHash(focusType, mainStep, step = '', isCommon = false) {
|
|
const params = new URLSearchParams();
|
|
params.set('focusType', focusType);
|
|
params.set('mainStep', mainStep);
|
|
if (step) params.set('step', step);
|
|
if (isCommon) params.set('isCommon', '1');
|
|
params.set('at', String(Date.now()));
|
|
return `#${params.toString()}`;
|
|
}
|
|
|
|
function notifyMainNavigation(focusType, mainStep, step = '', isCommon = false) {
|
|
const payload = {
|
|
type: 'navigate-to-focus',
|
|
focusType,
|
|
mainStep,
|
|
step,
|
|
isCommon: Boolean(isCommon),
|
|
at: Date.now()
|
|
};
|
|
try {
|
|
if (window.opener && !window.opener.closed) {
|
|
window.opener.postMessage(payload, MAIN_APP_ORIGIN);
|
|
}
|
|
} catch (error) {
|
|
// ignore and continue with the other channels below
|
|
}
|
|
try {
|
|
if (window.BroadcastChannel) {
|
|
const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
|
channel.postMessage(payload);
|
|
channel.close();
|
|
}
|
|
} catch (error) {
|
|
// ignore and fall back to storage
|
|
}
|
|
try {
|
|
localStorage.setItem(NAVIGATION_SIGNAL_KEY, JSON.stringify(payload));
|
|
} catch (error) {
|
|
// ignore storage failures
|
|
}
|
|
}
|
|
|
|
function focusMainWindowOrOpen(url, hash = '') {
|
|
try {
|
|
if (window.opener && !window.opener.closed) {
|
|
try {
|
|
if (hash) {
|
|
window.opener.location.hash = hash;
|
|
}
|
|
} catch (innerError) {
|
|
// ignore hash assignment failures
|
|
}
|
|
window.opener.focus();
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
// ignore and open fallback below
|
|
}
|
|
window.open(url, '_blank');
|
|
return false;
|
|
}
|
|
|
|
function jumpToMainDetail(mainStep) {
|
|
setActiveFocus({ type: 'main', mainStep, step: '', isCommon: false });
|
|
processMapRenderer.render();
|
|
const fallbackUrl = buildMainFocusUrl('main', mainStep);
|
|
const focusHash = buildMainFocusHash('main', mainStep);
|
|
try {
|
|
if (window.opener && !window.opener.closed && typeof window.opener.jumpFromSitemapToMainDetail === 'function') {
|
|
window.opener.jumpFromSitemapToMainDetail(mainStep);
|
|
window.opener.focus();
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// use message and url fallback below
|
|
}
|
|
notifyMainNavigation('main', mainStep);
|
|
focusMainWindowOrOpen(fallbackUrl, focusHash);
|
|
}
|
|
|
|
function jumpToMainStep(mainStep, step, isCommon = false) {
|
|
setActiveFocus({ type: 'step', mainStep, step, isCommon });
|
|
processMapRenderer.render();
|
|
const fallbackUrl = buildMainFocusUrl('step', mainStep, step, isCommon);
|
|
const focusHash = buildMainFocusHash('step', mainStep, step, isCommon);
|
|
try {
|
|
if (window.opener && !window.opener.closed && typeof window.opener.jumpFromSitemapToStepDetail === 'function') {
|
|
window.opener.jumpFromSitemapToStepDetail(mainStep, step, isCommon);
|
|
window.opener.focus();
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// use message and url fallback below
|
|
}
|
|
notifyMainNavigation('step', mainStep, step, isCommon);
|
|
focusMainWindowOrOpen(fallbackUrl, focusHash);
|
|
}
|
|
|
|
function jumpToCommonRoot(item) {
|
|
setActiveFocus({ type: 'common-root', mainStep: item, step: '', isCommon: true });
|
|
processMapRenderer.render();
|
|
const fallbackUrl = buildMainFocusUrl('common-root', item);
|
|
const focusHash = buildMainFocusHash('common-root', item, '', true);
|
|
try {
|
|
if (window.opener && !window.opener.closed && typeof window.opener.openFirstSubStepFromCommonFlow === 'function') {
|
|
window.opener.openFirstSubStepFromCommonFlow(item, false);
|
|
window.opener.focus();
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// use message and url fallback below
|
|
}
|
|
notifyMainNavigation('common-root', item, '', true);
|
|
focusMainWindowOrOpen(fallbackUrl, focusHash);
|
|
}
|
|
|
|
function setSyncStatus(state, message) {
|
|
syncStatusEl.dataset.state = state;
|
|
syncStatusEl.textContent = message;
|
|
}
|
|
|
|
function applyFlowModelObject(parsed) {
|
|
MAIN_STEPS = Array.isArray(parsed.mainSteps) ? parsed.mainSteps.map((v) => String(v)) : [];
|
|
|
|
SUB_FLOW = {};
|
|
if (parsed.subFlow && typeof parsed.subFlow === 'object') {
|
|
Object.entries(parsed.subFlow).forEach(([key, value]) => {
|
|
if (Array.isArray(value)) SUB_FLOW[key] = value.map((item) => String(item));
|
|
});
|
|
}
|
|
|
|
COMMON_ITEMS = Array.isArray(parsed.commonItems) ? parsed.commonItems.map((v) => String(v)).filter(Boolean) : [];
|
|
|
|
COMMON_SUB_FLOW = {};
|
|
if (parsed.commonSubFlow && typeof parsed.commonSubFlow === 'object') {
|
|
Object.entries(parsed.commonSubFlow).forEach(([key, value]) => {
|
|
if (Array.isArray(value)) COMMON_SUB_FLOW[key] = value.map((item) => String(item));
|
|
});
|
|
}
|
|
|
|
subFlowLinks = {};
|
|
if (parsed.subFlowLinks && typeof parsed.subFlowLinks === 'object') {
|
|
Object.entries(parsed.subFlowLinks).forEach(([key, value]) => {
|
|
if (value && typeof value === 'object' && typeof value.reason === 'string') {
|
|
subFlowLinks[key] = { reason: value.reason };
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function applyRemotePayload(payload) {
|
|
const flowModel = (payload.flowModel && typeof payload.flowModel === 'object')
|
|
? payload.flowModel
|
|
: ((Array.isArray(payload.mainSteps) || (payload.subFlow && typeof payload.subFlow === 'object')) ? payload : null);
|
|
if (!flowModel) return false;
|
|
applyFlowModelObject(flowModel);
|
|
stepMeta = (payload.stepMeta && typeof payload.stepMeta === 'object') ? payload.stepMeta : {};
|
|
currentUpdatedAt = payload.exportedAt || '';
|
|
return true;
|
|
}
|
|
|
|
|
|
async function fetchJsonFallback() {
|
|
for (const path of DEFAULT_JSON_CANDIDATES) {
|
|
try {
|
|
const parsed = await fetchJsonWithTimeout(path, { cache: 'no-store' });
|
|
if (parsed) return parsed;
|
|
} catch (error) {
|
|
// try next
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function fetchLocalSnapshot() {
|
|
try {
|
|
const flowRaw = localStorage.getItem(FLOW_MODEL_KEY);
|
|
if (!flowRaw) return null;
|
|
const flowModel = JSON.parse(flowRaw);
|
|
if (!flowModel || typeof flowModel !== 'object') return null;
|
|
|
|
let meta = {};
|
|
try {
|
|
const metaRaw = localStorage.getItem(STEP_META_KEY);
|
|
if (metaRaw) {
|
|
const parsedMeta = JSON.parse(metaRaw);
|
|
if (parsedMeta && typeof parsedMeta === 'object') meta = parsedMeta;
|
|
}
|
|
} catch (error) {
|
|
// ignore malformed meta
|
|
}
|
|
|
|
return {
|
|
flowModel,
|
|
stepMeta: meta,
|
|
exportedAt: ''
|
|
};
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function loadAndRender() {
|
|
if (loadingInFlight) {
|
|
pendingReload = true;
|
|
return;
|
|
}
|
|
|
|
loadingInFlight = true;
|
|
if (!hasRenderableData) {
|
|
mapPanelEl.classList.add('loading');
|
|
setSyncStatus('loading', 'DB 불러오는 중');
|
|
} else {
|
|
setSyncStatus('loading', '기존 화면 유지 중, 백그라운드 동기화 중');
|
|
}
|
|
|
|
try {
|
|
const openerPayload = pullDataFromOpenerSync();
|
|
if (!hasRenderableData && openerPayload && applyExternalData(openerPayload)) {
|
|
mapPanelEl.classList.remove('loading');
|
|
}
|
|
const snapshotPayload = fetchLocalSnapshot();
|
|
if (!hasRenderableData && snapshotPayload && applyPayloadToView(snapshotPayload)) {
|
|
setSyncStatus('loading', '마지막 동기화 데이터 확인 중');
|
|
}
|
|
|
|
let payload = null;
|
|
let dataSource = 'remote';
|
|
try {
|
|
payload = await fetchJsonWithTimeout(API_FLOW_DATA_URL, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Cache-Control': 'no-cache',
|
|
Pragma: 'no-cache'
|
|
}
|
|
});
|
|
if (payload) {
|
|
dataSource = 'remote';
|
|
}
|
|
} catch (error) {
|
|
// DB/API 접근 실패 시 로컬 스냅샷 또는 시드로 폴백한다.
|
|
}
|
|
|
|
if (!payload && openerPayload && applyPayloadToView(openerPayload.payload)) {
|
|
payload = openerPayload.payload;
|
|
dataSource = 'main-window';
|
|
}
|
|
|
|
if (!payload && snapshotPayload) {
|
|
payload = snapshotPayload;
|
|
dataSource = 'local';
|
|
}
|
|
|
|
if (!payload) {
|
|
payload = await fetchJsonFallback();
|
|
if (payload) dataSource = 'fallback';
|
|
}
|
|
|
|
if (!payload || !applyPayloadToView(payload)) {
|
|
throw new Error('invalid flow payload');
|
|
}
|
|
|
|
if (dataSource === 'local') {
|
|
setSyncStatus('ready', '마지막 동기화 데이터 표시 중');
|
|
} else if (dataSource === 'main-window') {
|
|
setSyncStatus('ready', '메인 화면 데이터 표시 중');
|
|
} else if (dataSource === 'fallback') {
|
|
setSyncStatus('ready', 'Gitea 데이터 불러옴');
|
|
} else {
|
|
setSyncStatus('ready', 'DB 연결됨');
|
|
}
|
|
} catch (error) {
|
|
console.warn('Process map load failed', error);
|
|
setSyncStatus('error', '동기화 실패');
|
|
if (!sitemapFullContentEl.children.length) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'empty-state';
|
|
empty.textContent = 'Process Map 데이터를 불러오지 못했습니다.';
|
|
sitemapFullContentEl.innerHTML = '';
|
|
sitemapFullContentEl.appendChild(empty);
|
|
}
|
|
} finally {
|
|
loadingInFlight = false;
|
|
mapPanelEl.classList.remove('loading');
|
|
if (pendingReload) {
|
|
pendingReload = false;
|
|
loadAndRender();
|
|
}
|
|
}
|
|
}
|
|
|
|
function triggerReloadSoon() {
|
|
if (Date.now() - lastInlineDataAt < 1500) return;
|
|
window.clearTimeout(triggerReloadSoon.timerId);
|
|
triggerReloadSoon.timerId = window.setTimeout(() => {
|
|
loadAndRender();
|
|
}, 450);
|
|
}
|
|
triggerReloadSoon.timerId = null;
|
|
|
|
sitemapFullContentEl.addEventListener('click', (event) => {
|
|
const target = event.target.closest('[data-map-action]');
|
|
if (!target || !sitemapFullContentEl.contains(target)) return;
|
|
const action = target.dataset.mapAction || '';
|
|
const mainStep = target.dataset.mainStep || '';
|
|
const step = target.dataset.step || '';
|
|
const isCommon = target.dataset.isCommon === '1';
|
|
if (!mainStep) return;
|
|
|
|
if (action === 'main') {
|
|
jumpToMainDetail(mainStep);
|
|
return;
|
|
}
|
|
if (action === 'step') {
|
|
jumpToMainStep(mainStep, step, isCommon);
|
|
return;
|
|
}
|
|
if (action === 'common-root') {
|
|
jumpToCommonRoot(mainStep);
|
|
}
|
|
});
|
|
sitemapTeamSelectEl.addEventListener('change', () => {
|
|
processMapRenderer.setFilters({ team: sitemapTeamSelectEl.value });
|
|
processMapRenderer.render();
|
|
scheduleMapFit();
|
|
});
|
|
sitemapSystemSelectEl.addEventListener('change', () => {
|
|
processMapRenderer.setFilters({ system: sitemapSystemSelectEl.value });
|
|
processMapRenderer.render();
|
|
scheduleMapFit();
|
|
});
|
|
|
|
if (window.BroadcastChannel) {
|
|
const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
|
channel.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'process-map-data') {
|
|
applyExternalData(event.data);
|
|
return;
|
|
}
|
|
if (event.data && event.data.type === 'flow-data-updated') {
|
|
triggerReloadSoon();
|
|
return;
|
|
}
|
|
if (event.data && event.data.type === 'focus-updated') {
|
|
applyExternalFocus(event.data);
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.origin !== MAIN_APP_ORIGIN && event.origin !== window.location.origin) return;
|
|
if (!event.data || !event.data.type) return;
|
|
if (event.data.type === 'process-map-data') {
|
|
applyExternalData(event.data);
|
|
return;
|
|
}
|
|
if (event.data.type === 'focus-updated') {
|
|
applyExternalFocus(event.data);
|
|
return;
|
|
}
|
|
if (event.data.type === 'flow-data-updated') {
|
|
triggerReloadSoon();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key === STORAGE_SIGNAL_KEY) {
|
|
triggerReloadSoon();
|
|
return;
|
|
}
|
|
if (event.key === FOCUS_SIGNAL_KEY && event.newValue) {
|
|
try {
|
|
applyExternalFocus(JSON.parse(event.newValue));
|
|
} catch (error) {
|
|
// ignore malformed payloads
|
|
}
|
|
}
|
|
});
|
|
|
|
window.addEventListener('focus', () => {
|
|
syncFocusFromOpener();
|
|
requestFocusFromMainViaPostMessage();
|
|
triggerReloadSoon();
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (processMapRenderer && typeof processMapRenderer.realign === 'function') {
|
|
processMapRenderer.realign();
|
|
}
|
|
scheduleMapFit();
|
|
});
|
|
|
|
window.setInterval(() => {
|
|
if (!document.hidden) {
|
|
syncFocusFromOpener();
|
|
requestFocusFromMainViaPostMessage();
|
|
loadAndRender();
|
|
}
|
|
}, POLL_INTERVAL_MS);
|
|
|
|
try {
|
|
if (window.opener && !window.opener.closed && typeof window.opener.registerProcessMapPopup === 'function') {
|
|
window.opener.registerProcessMapPopup(window);
|
|
}
|
|
} catch (error) {
|
|
// ignore opener registration failures
|
|
}
|
|
|
|
ensureFitObserver();
|
|
syncFocusFromOpener();
|
|
requestDataFromMainViaPostMessage();
|
|
loadAndRender();
|
|
</script>
|
|
</body>
|
|
</html>
|