Sync relation map selections

This commit is contained in:
2026-06-24 16:02:52 +09:00
parent 5fefad6c3c
commit 6ee328e4fc

View File

@@ -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() {
<main className="min-h-screen bg-white text-slate-900">
<RelationTreePanel
programs={programs}
onProgramClick={openProgramWindow}
onProgramClick={selectProgramFromMap}
onOpenComparePair={openComparePair}
onRelationSelect={selectRelation}
selectedRelationId={selectedRelationId}
@@ -3379,7 +3476,7 @@ export default function App() {
>
<RelationTreePanel
programs={programs}
onProgramClick={scrollProgramIntoView}
onProgramClick={selectProgramFromMap}
onOpenComparePair={openComparePair}
onRelationSelect={selectRelation}
selectedRelationId={selectedRelationId}