(function () { function createProcessMapRenderer(options) { const { elements, getState, onMainClick, onStepClick, onCommonRootClick, isActiveFocus = () => false, linkWidths = { auto: 132, manual: 132 }, sharedSegment = ['전표작성', '검토', '출금'] } = options; const { fullContentEl, legendEl, teamSelectEl, systemSelectEl } = elements; let systemColorMap = new Map(); let filters = { team: '', system: '' }; function parseMultiValues(text) { return String(text || '') .split(',') .map((value) => value.trim()) .filter(Boolean); } function getSecondFlowMeta(mainStep, step) { const { stepMeta = {} } = getState(); return stepMeta[`0|${mainStep}|${step}`] || {}; } function collectSystemsForLegend() { const { mainSteps = [], subFlow = {}, commonItems = [], commonSubFlow = {} } = getState(); const set = new Set(); mainSteps.forEach((mainStep) => { (subFlow[mainStep] || []).forEach((step) => { parseMultiValues(getSecondFlowMeta(mainStep, step).system).forEach((system) => set.add(system)); }); }); commonItems.forEach((item) => { (commonSubFlow[item] || []).forEach((step) => { parseMultiValues(getSecondFlowMeta(item, step).system).forEach((system) => set.add(system)); }); }); return Array.from(set).sort((a, b) => a.localeCompare(b, 'ko')); } function normalizeSystemName(name) { return String(name || '').trim().toLowerCase(); } function getSystemCategory(systemName) { const normalized = normalizeSystemName(systemName); if (!normalized) return 'unknown'; if (normalized.includes('미사용')) return 'unused'; if (normalized.includes('외부')) return 'external'; if (normalized.includes('내부') || normalized.includes('erp') || normalized.includes('pq')) return 'internal'; return 'other'; } const INTERNAL_PALETTE = [ { bg: '#8CCBF7', border: '#69AFDF', text: '#173f6f' }, // pastel dodger { bg: '#A7D7F9', border: '#84C0E5', text: '#173f6f' }, // pastel picton { bg: '#BFE2FB', border: '#98CAE9', text: '#173f6f' }, // pastel maya { bg: '#D3EBFC', border: '#AFCFE6', text: '#173f6f' }, // pastel uranian { bg: '#E9F5FE', border: '#C5DBEC', text: '#173f6f' } // pastel beau ]; const UNUSED_PALETTE = [ { bg: '#fff1f1', border: '#e6a3a3', text: '#762222' }, // red-1 { bg: '#fff3f3', border: '#e8acac', text: '#7a2424' }, // red-2 { bg: '#fff5f5', border: '#e9b4b4', text: '#7f2f2f' } // rose-red ]; const EXTERNAL_PALETTE = [ { bg: '#eaf8ef', border: '#87ca9c', text: '#11492a' }, // green-1 { bg: '#e8f7f1', border: '#7ac8ae', text: '#0f544d' }, // green-teal { bg: '#eef9f4', border: '#8bcdb0', text: '#184f39' } // green-3 ]; const OTHER_PALETTE = [ { bg: '#edf3fb', border: '#9fb8d7', text: '#26384a' }, { bg: '#f1f4fa', border: '#a9b8cd', text: '#2b3a4d' } ]; const SPECIAL_STEP_CATEGORY = new Map([ ['입찰공고', 'unused'], ['외주발주의뢰', 'unused'], ['외주발주검토', 'unused'] ]); function pickColorFromCategory(category, seedName) { let palette = OTHER_PALETTE; if (category === 'internal') palette = INTERNAL_PALETTE; if (category === 'unused') palette = UNUSED_PALETTE; if (category === 'external') palette = EXTERNAL_PALETTE; const seed = hashString(seedName || category) % palette.length; return palette[seed]; } function hashString(value) { const str = String(value || ''); let hash = 0; for (let i = 0; i < str.length; i += 1) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return Math.abs(hash); } function makeDistinctSystemColor(systemName, index = 0) { const category = getSystemCategory(systemName); let palette = OTHER_PALETTE; if (category === 'internal') palette = INTERNAL_PALETTE; if (category === 'unused') palette = UNUSED_PALETTE; if (category === 'external') palette = EXTERNAL_PALETTE; const seed = (hashString(systemName) + (index * 7)) % palette.length; return palette[seed]; } function rebuildSystemColorMap() { const systems = collectSystemsForLegend(); systemColorMap = new Map(); systems.forEach((system, index) => { systemColorMap.set(system, makeDistinctSystemColor(system, index)); }); } function getRandomLikeColorBySystem(systemName) { if (!systemColorMap.has(systemName)) { systemColorMap.set(systemName, makeDistinctSystemColor(systemName, systemColorMap.size)); } return systemColorMap.get(systemName); } function applyChipSystemColor(chipEl, info, stepLabel = '') { const systems = parseMultiValues(info.system); if (!systems.length) { const specialCategory = SPECIAL_STEP_CATEGORY.get(String(stepLabel || '').trim()) || ''; if (specialCategory) { const color = pickColorFromCategory(specialCategory, `${specialCategory}:${stepLabel}`); chipEl.classList.add('system-colored'); chipEl.style.setProperty('--sys-bg', color.bg); chipEl.style.setProperty('--sys-border', color.border); chipEl.style.setProperty('--sys-text', color.text); return; } const color = pickColorFromCategory('other', `no-system:${stepLabel}`); chipEl.classList.add('system-colored', 'no-system'); chipEl.style.setProperty('--sys-bg', color.bg); chipEl.style.setProperty('--sys-border', color.border); chipEl.style.setProperty('--sys-text', color.text); return; } if (systems.length >= 2) { const colorA = getRandomLikeColorBySystem(systems[0]); const colorB = getRandomLikeColorBySystem(systems[1]); chipEl.classList.add('system-colored', 'multi-system'); chipEl.style.setProperty('--sys-bg-a', colorA.bg); chipEl.style.setProperty('--sys-bg-b', colorB.bg); chipEl.style.setProperty('--sys-border-a', colorA.border); chipEl.style.setProperty('--sys-border-b', colorB.border); chipEl.style.setProperty('--sys-text', colorA.text || '#1f4f83'); return; } const color = getRandomLikeColorBySystem(systems[0]); chipEl.classList.add('system-colored'); chipEl.style.setProperty('--sys-bg', color.bg); chipEl.style.setProperty('--sys-border', color.border); chipEl.style.setProperty('--sys-text', color.text); } function isMetaMatchedByFilter(info) { const teams = parseMultiValues(info.team); const systems = parseMultiValues(info.system); const hasAnyMeta = teams.length > 0 || systems.length > 0; if (!filters.team && !filters.system) return true; if (!hasAnyMeta) return false; const teamMatch = !filters.team || teams.includes(filters.team); const systemMatch = !filters.system || systems.includes(filters.system); return teamMatch && systemMatch; } function findSharedSegmentStart(list) { for (let index = 0; index <= list.length - sharedSegment.length; index += 1) { const matched = sharedSegment.every((value, offset) => String(list[index + offset] || '').trim() === value); if (matched) return index; } return -1; } function subFlowLinkKey(mainStep, fromStep, toStep) { return `${mainStep}||${fromStep}>>${toStep}`; } function getSubFlowLinkInfo(mainStep, fromStep, toStep) { const { subFlowLinks = {} } = getState(); return subFlowLinks[subFlowLinkKey(mainStep, fromStep, toStep)] || { reason: '' }; } function formatConnectorReason(reason) { return String(reason || '').trim(); } function renderLegend() { if (!legendEl) return; legendEl.innerHTML = ''; const categoryOrder = { internal: 0, external: 1, unused: 2, other: 3, unknown: 4 }; const systems = collectSystemsForLegend().sort((a, b) => { const catA = getSystemCategory(a); const catB = getSystemCategory(b); const rankA = categoryOrder[catA] ?? 99; const rankB = categoryOrder[catB] ?? 99; if (rankA !== rankB) return rankA - rankB; return a.localeCompare(b, 'ko'); }); const applyLegendSystemFilter = (systemValue) => { const nextSystem = String(systemValue || ''); if (systemSelectEl) { const hasOption = Array.from(systemSelectEl.options || []).some((option) => option.value === nextSystem); systemSelectEl.value = hasOption ? nextSystem : ''; systemSelectEl.dispatchEvent(new Event('change', { bubbles: true })); return; } setFilters({ system: nextSystem }); render(); }; const makeItem = (label, bg, border, opts = {}) => { const { clickable = false, selected = false, onClick = null } = opts; const item = document.createElement('span'); item.className = 'sm-legend-item'; if (selected) item.classList.add('active-focus'); const swatch = document.createElement('span'); swatch.className = 'sm-legend-swatch'; swatch.style.background = bg; swatch.style.borderColor = border; const text = document.createElement('span'); text.textContent = label; item.appendChild(swatch); item.appendChild(text); if (clickable && typeof onClick === 'function') { item.style.cursor = 'pointer'; item.title = `${label} 필터 적용`; item.setAttribute('role', 'button'); item.setAttribute('tabindex', '0'); item.addEventListener('click', onClick); item.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); onClick(); }); } legendEl.appendChild(item); }; systems.forEach((system) => { const color = getRandomLikeColorBySystem(system); makeItem(system, color.bg, color.border, { clickable: true, selected: filters.system === system, onClick: () => applyLegendSystemFilter(system) }); }); makeItem('시스템 미입력', '#e5e7eb', '#cbd5e1', { clickable: true, selected: !filters.system, onClick: () => applyLegendSystemFilter('') }); } function refreshFilterOptions() { if (!teamSelectEl || !systemSelectEl) return; const { mainSteps = [], subFlow = {}, commonItems = [], commonSubFlow = {} } = getState(); const teamSet = new Set(); const systemSet = new Set(); mainSteps.forEach((mainStep) => { (subFlow[mainStep] || []).forEach((step) => { const info = getSecondFlowMeta(mainStep, step); parseMultiValues(info.team).forEach((value) => teamSet.add(value)); parseMultiValues(info.system).forEach((value) => systemSet.add(value)); }); }); commonItems.forEach((item) => { (commonSubFlow[item] || []).forEach((step) => { const info = getSecondFlowMeta(item, step); parseMultiValues(info.team).forEach((value) => teamSet.add(value)); parseMultiValues(info.system).forEach((value) => systemSet.add(value)); }); }); const previousTeam = filters.team; const previousSystem = filters.system; teamSelectEl.innerHTML = ''; Array.from(teamSet).sort((a, b) => a.localeCompare(b, 'ko')).forEach((value) => { const option = document.createElement('option'); option.value = value; option.textContent = value; teamSelectEl.appendChild(option); }); systemSelectEl.innerHTML = ''; Array.from(systemSet).sort((a, b) => a.localeCompare(b, 'ko')).forEach((value) => { const option = document.createElement('option'); option.value = value; option.textContent = value; systemSelectEl.appendChild(option); }); filters.team = Array.from(teamSet).includes(previousTeam) ? previousTeam : ''; filters.system = Array.from(systemSet).includes(previousSystem) ? previousSystem : ''; teamSelectEl.value = filters.team; systemSelectEl.value = filters.system; } function markFlowItem(el, kind, colIndex) { if (!el) return el; el.dataset.flowKind = kind; el.dataset.flowCol = String(colIndex); return el; } function markInteractiveTarget(el, action, mainStep, step = '', isCommon = false) { if (!el) return el; el.dataset.mapAction = action; el.dataset.mainStep = String(mainStep || ''); if (step) el.dataset.step = String(step); if (isCommon) el.dataset.isCommon = '1'; el.style.cursor = 'pointer'; return el; } const STEP_BOX_SPACING_MULTIPLIER = 1.7; function getSegmentBaseWidth(hasPreviousStep, isManual) { if (!hasPreviousStep) return 0; const baseWidth = isManual ? Number(linkWidths.manual || 156) : Number(linkWidths.auto || 132); return baseWidth * STEP_BOX_SPACING_MULTIPLIER; } function applyColumnAlignment() { const segments = Array.from(fullContentEl.querySelectorAll('.sm-segment[data-flow-col]')); if (!segments.length) return; const maxWidthByCol = new Map(); segments.forEach((segment) => { const colIndex = Number(segment.dataset.flowCol || '-1'); const baseWidth = Number(segment.dataset.flowWidth || '0'); if (colIndex < 0 || baseWidth <= 0) return; const current = maxWidthByCol.get(colIndex) || 0; if (baseWidth > current) maxWidthByCol.set(colIndex, baseWidth); }); segments.forEach((segment) => { const chip = segment.querySelector('.sm-chip'); const chipWidth = chip ? Math.ceil(chip.getBoundingClientRect().width) : 102; const hasLink = segment.dataset.hasLink === '1'; const colIndex = Number(segment.dataset.flowCol || '-1'); const totalWidth = maxWidthByCol.get(colIndex) || chipWidth; const safeTotalWidth = Math.max(totalWidth, chipWidth); const linkWidth = hasLink ? Math.max(safeTotalWidth - chipWidth, 24) : 0; segment.style.setProperty('--flow-col-width', `${safeTotalWidth}px`); segment.style.setProperty('--flow-link-width', `${linkWidth}px`); }); } function buildLink(mainStep, fromStep, toStep, dim) { const link = getSubFlowLinkInfo(mainStep, fromStep, toStep); const linkWrap = document.createElement('span'); linkWrap.className = `sm-link-wrap${link.reason ? ' manual' : ''}`; if (dim) linkWrap.classList.add('dim'); if (link.reason) { const prefix = document.createElement('span'); prefix.className = 'sm-link-arrow-frag'; const reason = document.createElement('span'); reason.className = 'sm-link-reason'; reason.textContent = formatConnectorReason(link.reason); reason.title = link.reason; const suffix = document.createElement('span'); suffix.className = 'sm-link-arrow-frag'; if (dim) { prefix.classList.add('dim'); reason.classList.add('dim'); suffix.classList.add('dim'); } linkWrap.appendChild(prefix); linkWrap.appendChild(reason); linkWrap.appendChild(suffix); } else { const arrow = document.createElement('span'); arrow.className = 'sm-arrow'; if (dim) arrow.classList.add('dim'); linkWrap.appendChild(arrow); } return linkWrap; } function buildStepChip(mainStep, step, info, isCommon, isMatch, hasFilter) { const chip = document.createElement('span'); chip.className = 'sm-chip'; chip.textContent = step; chip.title = `담당팀: ${info.team || '-'} / 시스템: ${info.system || '-'}`; applyChipSystemColor(chip, info, step); if (isActiveFocus('step', mainStep, step, isCommon)) chip.classList.add('active-focus'); if (typeof onStepClick === 'function') { markInteractiveTarget(chip, 'step', mainStep, step, isCommon); } if (hasFilter) chip.classList.add(isMatch ? 'match' : 'dim'); return chip; } function buildStepSegment(mainStep, step, info, isCommon, isMatch, hasFilter, previousStep = '', dimLink = false, colIndex = 0) { const segment = document.createElement('span'); segment.className = 'sm-segment'; markFlowItem(segment, 'segment', colIndex); segment.dataset.hasLink = previousStep ? '1' : '0'; if (previousStep) { const linkEl = buildLink(mainStep, previousStep, step, dimLink); if (linkEl.classList.contains('manual')) segment.classList.add('has-manual-link'); segment.dataset.flowWidth = String(getSegmentBaseWidth(true, linkEl.classList.contains('manual'))); segment.appendChild(linkEl); } else { segment.classList.add('is-first'); } if (!segment.dataset.flowWidth) segment.dataset.flowWidth = '0'; segment.appendChild(buildStepChip(mainStep, step, info, isCommon, isMatch, hasFilter)); return segment; } function render() { fullContentEl.innerHTML = ''; const { mainSteps = [], subFlow = {}, commonItems = [], commonSubFlow = {} } = getState(); if (!mainSteps.length) { const empty = document.createElement('div'); empty.className = 'empty-state'; empty.textContent = '표시할 프로세스 데이터가 없습니다.'; fullContentEl.appendChild(empty); return; } rebuildSystemColorMap(); renderLegend(); const hasFilter = Boolean(filters.team || filters.system); const projectTitle = document.createElement('p'); projectTitle.className = 'sm-section-title'; projectTitle.textContent = '프로젝트'; fullContentEl.appendChild(projectTitle); mainSteps.forEach((mainStep) => { const row = document.createElement('div'); row.className = 'sm-row'; const main = document.createElement('div'); main.className = 'sm-main'; main.textContent = mainStep; if (isActiveFocus('main', mainStep)) main.classList.add('active-focus'); if (typeof onMainClick === 'function') { main.title = `${mainStep} DETAIL 열기`; markInteractiveTarget(main, 'main', mainStep); } row.appendChild(main); const sub = subFlow[mainStep] || []; let rowHasMatch = false; let flowColIndex = 0; if (sub.length > 0) { const chain = document.createElement('div'); chain.className = 'sm-chain'; const sharedStart = findSharedSegmentStart(sub); sub.forEach((step, index) => { if (sharedStart >= 0 && index > sharedStart && index < sharedStart + sharedSegment.length) return; const info = getSecondFlowMeta(mainStep, step); const isMatch = isMetaMatchedByFilter(info); if (isMatch) rowHasMatch = true; if (sharedStart >= 0 && index === sharedStart) { const box = document.createElement('span'); box.className = 'sm-shared-box'; const sharedMatches = sharedSegment.map((segmentName) => { const segmentInfo = getSecondFlowMeta(mainStep, segmentName); const segmentMatch = isMetaMatchedByFilter(segmentInfo); if (segmentMatch) rowHasMatch = true; return segmentMatch; }); for (let offset = 0; offset < sharedSegment.length; offset += 1) { const segmentName = sharedSegment[offset]; const segmentInfo = getSecondFlowMeta(mainStep, segmentName); const previousStep = offset > 0 ? sharedSegment[offset - 1] : (sharedStart > 0 ? sub[sharedStart - 1] : ''); const previousMatch = offset > 0 ? sharedMatches[offset - 1] : (previousStep ? isMetaMatchedByFilter(getSecondFlowMeta(mainStep, previousStep)) : false); const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || sharedMatches[offset])); box.appendChild(buildStepSegment(mainStep, segmentName, segmentInfo, false, sharedMatches[offset], hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; } chain.appendChild(box); } else { const previousStep = index > 0 ? sub[index - 1] : ''; const previousMatch = previousStep ? isMetaMatchedByFilter(getSecondFlowMeta(mainStep, previousStep)) : false; const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || isMatch)); chain.appendChild(buildStepSegment(mainStep, step, info, false, isMatch, hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; } }); if (hasFilter && !rowHasMatch) main.classList.add('dim'); row.appendChild(chain); } fullContentEl.appendChild(row); }); const commonTitle = document.createElement('p'); commonTitle.className = 'sm-section-title'; commonTitle.textContent = '공통'; fullContentEl.appendChild(commonTitle); if (commonItems.length) { commonItems.forEach((item) => { const row = document.createElement('div'); row.className = 'sm-row'; const main = document.createElement('div'); main.className = 'sm-main'; main.textContent = item; if (isActiveFocus('common-root', item, '', true)) main.classList.add('active-focus'); if (typeof onCommonRootClick === 'function') { main.title = `${item} DETAIL 열기`; markInteractiveTarget(main, 'common-root', item, '', true); } row.appendChild(main); const chain = document.createElement('div'); chain.className = 'sm-chain'; const sub = commonSubFlow[item] || []; let rowHasMatch = false; let flowColIndex = 0; const isCommonStepMatch = (step) => isMetaMatchedByFilter(getSecondFlowMeta(item, step)); const getStepRenderMeta = (label) => { const info = getSecondFlowMeta(item, label); const isMatch = isCommonStepMatch(label); if (isMatch) rowHasMatch = true; return { info, isMatch }; }; const sharedStart = findSharedSegmentStart(sub); if (sharedStart < 0) { sub.forEach((step, index) => { const { info, isMatch } = getStepRenderMeta(step); const previousStep = index > 0 ? sub[index - 1] : ''; const previousMatch = previousStep ? isCommonStepMatch(previousStep) : false; const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || isMatch)); chain.appendChild(buildStepSegment(item, step, info, true, isMatch, hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; }); } else { const prefixSteps = sub.slice(0, sharedStart); const sharedSteps = sub.slice(sharedStart, sharedStart + sharedSegment.length); const suffixSteps = sub.slice(sharedStart + sharedSegment.length); prefixSteps.forEach((step, index) => { const { info, isMatch } = getStepRenderMeta(step); const previousStep = index > 0 ? prefixSteps[index - 1] : ''; const previousMatch = previousStep ? isCommonStepMatch(previousStep) : false; const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || isMatch)); chain.appendChild(buildStepSegment(item, step, info, true, isMatch, hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; }); const box = document.createElement('span'); box.className = 'sm-shared-box'; sharedSteps.forEach((step, index) => { const { info, isMatch } = getStepRenderMeta(step); const previousStep = index > 0 ? sharedSteps[index - 1] : (prefixSteps.length ? prefixSteps[prefixSteps.length - 1] : ''); const previousMatch = previousStep ? isCommonStepMatch(previousStep) : false; const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || isMatch)); box.appendChild(buildStepSegment(item, step, info, true, isMatch, hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; }); if (hasFilter && !rowHasMatch) box.classList.add('dim'); chain.appendChild(box); suffixSteps.forEach((step, index) => { const { info, isMatch } = getStepRenderMeta(step); const previousStep = index === 0 ? sharedSteps[sharedSteps.length - 1] : suffixSteps[index - 1]; const previousMatch = previousStep ? isCommonStepMatch(previousStep) : false; const dimLink = Boolean(previousStep && hasFilter && !(previousMatch || isMatch)); chain.appendChild(buildStepSegment(item, step, info, true, isMatch, hasFilter, previousStep, dimLink, flowColIndex)); flowColIndex += 1; }); } if (hasFilter && !rowHasMatch) main.classList.add('dim'); row.appendChild(chain); fullContentEl.appendChild(row); }); } else { const row = document.createElement('div'); row.className = 'sm-row'; const main = document.createElement('div'); main.className = 'sm-main'; main.textContent = '공통'; row.appendChild(main); const empty = document.createElement('div'); empty.className = 'empty-state'; empty.style.minHeight = '120px'; empty.textContent = '공통 플로우가 없습니다.'; row.appendChild(empty); fullContentEl.appendChild(row); } requestAnimationFrame(() => applyColumnAlignment()); } function setFilters(nextFilters) { filters = { ...filters, ...nextFilters }; } return { render, refreshFilterOptions, setFilters, realign: applyColumnAlignment }; } window.createProcessMapRenderer = createProcessMapRenderer; })();