Sync relation map selections
This commit is contained in:
127
src/App.jsx
127
src/App.jsx
@@ -176,8 +176,25 @@ const initialGates = {
|
|||||||
const channelName = 'program-flow-state';
|
const channelName = 'program-flow-state';
|
||||||
const contentStorageKey = 'program-flow-content';
|
const contentStorageKey = 'program-flow-content';
|
||||||
const programStateStorageKey = 'program-flow-gates';
|
const programStateStorageKey = 'program-flow-gates';
|
||||||
|
const selectedRelationStorageKey = 'program-flow-selected-relation';
|
||||||
|
const selectedProgramStorageKey = 'program-flow-selected-program';
|
||||||
const serverStateEndpoint = '/api/state';
|
const serverStateEndpoint = '/api/state';
|
||||||
const disabledFlowStep = '__disabled__';
|
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 = {
|
const programTypeOptions = {
|
||||||
internal: {
|
internal: {
|
||||||
label: '사내프로그램',
|
label: '사내프로그램',
|
||||||
@@ -2543,21 +2560,55 @@ export default function App() {
|
|||||||
const targetName = targetPrograms.map((program) => program.name).join(' / ');
|
const targetName = targetPrograms.map((program) => program.name).join(' / ');
|
||||||
return storedTarget?.linkLabel || `${fromProgram.name} 결과물을 ${targetName}로 연계`;
|
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 selectRelation = (fromId, toId) => {
|
||||||
const relationId = getRelationId(fromId, toId);
|
const relationId = getRelationId(fromId, toId);
|
||||||
setSelectedRelationId(relationId);
|
focusRelation(relationId, !isRelationMapWindow);
|
||||||
requestAnimationFrame(() => {
|
broadcastRelationSelection(relationId);
|
||||||
relationBlockRefs.current[relationId]?.scrollIntoView({
|
};
|
||||||
|
const focusProgram = (programId, shouldScroll = true) => {
|
||||||
|
if (shouldScroll) {
|
||||||
|
programSectionRefs.current[programId]?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'center'
|
block: 'center'
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
const scrollProgramIntoView = (programId) => {
|
const broadcastProgramSelection = (programId) => {
|
||||||
programSectionRefs.current[programId]?.scrollIntoView({
|
try {
|
||||||
behavior: 'smooth',
|
window.localStorage.setItem(selectedProgramStorageKey, createSelectionSignal(programId));
|
||||||
block: 'center'
|
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 renderRelationBlock = (fromProgram) => {
|
||||||
const successorGroups = Object.values(
|
const successorGroups = Object.values(
|
||||||
@@ -2661,11 +2712,18 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const channel = new BroadcastChannel(channelName);
|
const channel = new BroadcastChannel(channelName);
|
||||||
channel.onmessage = (event) => {
|
channel.onmessage = (event) => {
|
||||||
if (!event.data?.states) return;
|
if (event.data?.states) {
|
||||||
setProgramStates(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();
|
return () => channel.close();
|
||||||
}, []);
|
}, [isRelationMapWindow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -2736,11 +2794,50 @@ export default function App() {
|
|||||||
if (event.key === programStateStorageKey) {
|
if (event.key === programStateStorageKey) {
|
||||||
setProgramStates(readStoredProgramStates());
|
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);
|
window.addEventListener('storage', syncStoredContent);
|
||||||
return () => window.removeEventListener('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(() => {
|
useEffect(() => {
|
||||||
window.localStorage.setItem(programStateStorageKey, JSON.stringify(programStates));
|
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">
|
<main className="min-h-screen bg-white text-slate-900">
|
||||||
<RelationTreePanel
|
<RelationTreePanel
|
||||||
programs={programs}
|
programs={programs}
|
||||||
onProgramClick={openProgramWindow}
|
onProgramClick={selectProgramFromMap}
|
||||||
onOpenComparePair={openComparePair}
|
onOpenComparePair={openComparePair}
|
||||||
onRelationSelect={selectRelation}
|
onRelationSelect={selectRelation}
|
||||||
selectedRelationId={selectedRelationId}
|
selectedRelationId={selectedRelationId}
|
||||||
@@ -3379,7 +3476,7 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
<RelationTreePanel
|
<RelationTreePanel
|
||||||
programs={programs}
|
programs={programs}
|
||||||
onProgramClick={scrollProgramIntoView}
|
onProgramClick={selectProgramFromMap}
|
||||||
onOpenComparePair={openComparePair}
|
onOpenComparePair={openComparePair}
|
||||||
onRelationSelect={selectRelation}
|
onRelationSelect={selectRelation}
|
||||||
selectedRelationId={selectedRelationId}
|
selectedRelationId={selectedRelationId}
|
||||||
|
|||||||
Reference in New Issue
Block a user