From 6ee328e4fc009f934e5f7eee0f5935167d1c143a Mon Sep 17 00:00:00 2001 From: Hyein Date: Wed, 24 Jun 2026 16:02:52 +0900 Subject: [PATCH] Sync relation map selections --- src/App.jsx | 127 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 15 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 076803b..29f7e87 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -176,8 +176,25 @@ const initialGates = { const channelName = 'program-flow-state'; const contentStorageKey = 'program-flow-content'; const programStateStorageKey = 'program-flow-gates'; +const selectedRelationStorageKey = 'program-flow-selected-relation'; +const selectedProgramStorageKey = 'program-flow-selected-program'; const serverStateEndpoint = '/api/state'; const disabledFlowStep = '__disabled__'; + +function createSelectionSignal(id) { + return JSON.stringify({ id, timestamp: Date.now() }); +} + +function readSelectionSignal(value) { + if (!value) return ''; + try { + const parsed = JSON.parse(value); + return parsed?.id || ''; + } catch { + return value; + } +} + const programTypeOptions = { internal: { label: '사내프로그램', @@ -2543,21 +2560,55 @@ export default function App() { const targetName = targetPrograms.map((program) => program.name).join(' / '); return storedTarget?.linkLabel || `${fromProgram.name} 결과물을 ${targetName}로 연계`; }; + const focusRelation = (relationId, shouldScroll = true) => { + setSelectedRelationId(relationId); + if (shouldScroll) { + requestAnimationFrame(() => { + relationBlockRefs.current[relationId]?.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + }); + } + }; + const broadcastRelationSelection = (relationId) => { + try { + window.localStorage.setItem(selectedRelationStorageKey, createSelectionSignal(relationId)); + window.opener?.postMessage?.({ type: 'program-flow-selected-relation', relationId }, window.location.origin); + const channel = new BroadcastChannel(channelName); + channel.postMessage({ selectedRelationId: relationId }); + channel.close(); + } catch { + window.localStorage.setItem(selectedRelationStorageKey, createSelectionSignal(relationId)); + } + }; const selectRelation = (fromId, toId) => { const relationId = getRelationId(fromId, toId); - setSelectedRelationId(relationId); - requestAnimationFrame(() => { - relationBlockRefs.current[relationId]?.scrollIntoView({ + focusRelation(relationId, !isRelationMapWindow); + broadcastRelationSelection(relationId); + }; + const focusProgram = (programId, shouldScroll = true) => { + if (shouldScroll) { + programSectionRefs.current[programId]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); + } }; - const scrollProgramIntoView = (programId) => { - programSectionRefs.current[programId]?.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); + const broadcastProgramSelection = (programId) => { + try { + window.localStorage.setItem(selectedProgramStorageKey, createSelectionSignal(programId)); + window.opener?.postMessage?.({ type: 'program-flow-selected-program', programId }, window.location.origin); + const channel = new BroadcastChannel(channelName); + channel.postMessage({ selectedProgramId: programId }); + channel.close(); + } catch { + window.localStorage.setItem(selectedProgramStorageKey, createSelectionSignal(programId)); + } + }; + const selectProgramFromMap = (programId) => { + focusProgram(programId, !isRelationMapWindow); + broadcastProgramSelection(programId); }; const renderRelationBlock = (fromProgram) => { const successorGroups = Object.values( @@ -2661,11 +2712,18 @@ export default function App() { useEffect(() => { const channel = new BroadcastChannel(channelName); channel.onmessage = (event) => { - if (!event.data?.states) return; - setProgramStates(event.data.states); + if (event.data?.states) { + setProgramStates(event.data.states); + } + if (event.data?.selectedRelationId) { + focusRelation(event.data.selectedRelationId, !isRelationMapWindow); + } + if (event.data?.selectedProgramId) { + focusProgram(event.data.selectedProgramId, !isRelationMapWindow); + } }; return () => channel.close(); - }, []); + }, [isRelationMapWindow]); useEffect(() => { let isMounted = true; @@ -2736,11 +2794,50 @@ export default function App() { if (event.key === programStateStorageKey) { setProgramStates(readStoredProgramStates()); } + if (event.key === selectedRelationStorageKey && event.newValue) { + focusRelation(readSelectionSignal(event.newValue), !isRelationMapWindow); + } + if (event.key === selectedProgramStorageKey && event.newValue) { + focusProgram(readSelectionSignal(event.newValue), !isRelationMapWindow); + } }; window.addEventListener('storage', syncStoredContent); return () => window.removeEventListener('storage', syncStoredContent); - }, []); + }, [isRelationMapWindow]); + + useEffect(() => { + if (isRelationMapWindow) return undefined; + const syncProgramMessage = (event) => { + if (event.origin !== window.location.origin) return; + if (event.data?.type === 'program-flow-selected-program' && event.data.programId) { + focusProgram(event.data.programId, true); + } + if (event.data?.type === 'program-flow-selected-relation' && event.data.relationId) { + focusRelation(event.data.relationId, true); + } + }; + let lastRelationSignal = window.localStorage.getItem(selectedRelationStorageKey) || ''; + let lastProgramSignal = window.localStorage.getItem(selectedProgramStorageKey) || ''; + const syncSelectionSignals = () => { + const relationSignal = window.localStorage.getItem(selectedRelationStorageKey) || ''; + const programSignal = window.localStorage.getItem(selectedProgramStorageKey) || ''; + if (relationSignal && relationSignal !== lastRelationSignal) { + lastRelationSignal = relationSignal; + focusRelation(readSelectionSignal(relationSignal), true); + } + if (programSignal && programSignal !== lastProgramSignal) { + lastProgramSignal = programSignal; + focusProgram(readSelectionSignal(programSignal), true); + } + }; + const timer = window.setInterval(syncSelectionSignals, 300); + window.addEventListener('message', syncProgramMessage); + return () => { + window.clearInterval(timer); + window.removeEventListener('message', syncProgramMessage); + }; + }, [isRelationMapWindow]); useEffect(() => { window.localStorage.setItem(programStateStorageKey, JSON.stringify(programStates)); @@ -3329,7 +3426,7 @@ export default function App() {