Open relation map in separate window

This commit is contained in:
2026-06-24 10:56:11 +09:00
parent 186fc06906
commit be0a85136c

View File

@@ -1499,7 +1499,15 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
);
}
function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, sidebarWidth, onSidebarWidthChange }) {
function RelationTreePanel({
programs,
onProgramClick,
onOpenRelationPopup,
onOpenMapWindow,
sidebarWidth,
onSidebarWidthChange,
fullPage = false
}) {
const [expandedProgramIds, setExpandedProgramIds] = useState(() => new Set(programs.map((program) => program.id)));
const programMap = Object.fromEntries(programs.map((program) => [program.id, program]));
const relations = programs.flatMap((program) =>
@@ -1536,14 +1544,14 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
const miniSideLaneWidth = 96;
const sidebarSizes = [280, 420, 560];
const resolvedSidebarWidth = sidebarWidth ?? sidebarSizes[0];
const miniViewportWidth = Math.max(230, resolvedSidebarWidth - 32);
const miniViewportWidth = Math.max(230, fullPage ? 0 : resolvedSidebarWidth - 32);
const miniMaxLevelCount = Math.max(1, ...graphLevels.map((level) => level?.length ?? 0));
const miniGraphWidth = Math.max(
560,
fullPage ? 1120 : 560,
miniPadding * 2 + miniSideLaneWidth + miniMaxLevelCount * miniNodeWidth + Math.max(0, miniMaxLevelCount - 1) * miniColumnGap
);
const miniGraphHeight = Math.max(
660,
fullPage ? 720 : 660,
miniPadding * 2 + graphLevels.length * miniNodeHeight + Math.max(0, graphLevels.length - 1) * miniRowGap
);
const miniNodePositions = Object.fromEntries(
@@ -1560,9 +1568,8 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
]);
})
);
const mapOffset = Math.max(0, Math.round((miniGraphWidth - miniViewportWidth) / 2));
const mapOffset = fullPage ? 0 : Math.max(0, Math.round((miniGraphWidth - miniViewportWidth) / 2));
const canShrinkSidebar = resolvedSidebarWidth > sidebarSizes[0];
const canGrowSidebar = resolvedSidebarWidth < sidebarSizes[sidebarSizes.length - 1];
const setSidebarSize = (size) => {
onSidebarWidthChange?.(size);
};
@@ -1651,24 +1658,26 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
};
return (
<aside className="sticky top-5 max-h-[calc(100vh-40px)] overflow-y-auto overflow-x-hidden rounded-[26px] border border-white/75 bg-white/75 p-3.5 shadow-sm backdrop-blur transition-[width] duration-300">
<aside className={`${fullPage ? 'min-h-screen overflow-auto rounded-none border-0 bg-white/80 p-5' : 'sticky top-5 max-h-[calc(100vh-40px)] overflow-y-auto overflow-x-hidden rounded-[26px] border border-white/75 bg-white/75 p-3.5 shadow-sm'} backdrop-blur transition-[width] duration-300`}>
<div className="mb-4 flex items-start justify-between gap-3">
<div>
<p className="text-[11px] font-black uppercase tracking-wide text-blue-700">Program Map</p>
<h2 className="mt-1 text-lg font-black text-slate-950">프로그램 연결</h2>
<h2 className={`${fullPage ? 'text-2xl' : 'text-lg'} mt-1 font-black text-slate-950`}>프로그램 연결</h2>
<p className="mt-1 text-xs font-semibold leading-5 text-slate-500">
필요할 연결도 영역을 크게 펼칩니다.
{fullPage ? '연결 관계를 큰 화면에서 확인합니다.' : '필요할 때 연결도 영역을 새창으로 크게 펼칩니다.'}
</p>
</div>
<button
type="button"
onClick={onOpenRelationPopup}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-base font-black text-amber-700 ring-1 ring-amber-200 hover:bg-amber-200"
title="프로그램 연결도 및 관계 수정"
aria-label="프로그램 연결도 및 관계 수정"
>
!
</button>
{!fullPage && (
<button
type="button"
onClick={onOpenRelationPopup}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-base font-black text-amber-700 ring-1 ring-amber-200 hover:bg-amber-200"
title="프로그램 연결도 및 관계 수정"
aria-label="프로그램 연결도 및 관계 수정"
>
!
</button>
)}
</div>
<div className="rounded-3xl bg-white/70 p-2 shadow-inner ring-1 ring-slate-100">
<div className="mb-2 flex items-center justify-between gap-2 px-1">
@@ -1682,6 +1691,7 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
상용
</span>
</div>
{!fullPage && (
<div className="flex items-center justify-between gap-2">
<div className="flex shrink-0 items-center gap-1">
<button
@@ -1696,21 +1706,21 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
</button>
<button
type="button"
disabled={!canGrowSidebar}
onClick={() => setSidebarSize(sidebarSizes[sidebarSizes.length - 1])}
className="flex h-7 w-7 items-center justify-center rounded-full bg-white text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
aria-label="연결도 영역 최대"
title="연결도 영역 최대"
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
onClick={onOpenMapWindow}
className="flex h-7 w-7 items-center justify-center rounded-full bg-white text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
aria-label="연결도 새창으로 보기"
title="연결도 새창으로 보기"
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
)}
</div>
{relations.length > 0 ? (
<div className="relative h-[660px] overflow-hidden rounded-2xl bg-gradient-to-br from-slate-50 to-white ring-1 ring-slate-100">
<div className={`${fullPage ? 'h-[calc(100vh-150px)] overflow-auto' : 'h-[660px] overflow-hidden'} relative rounded-2xl bg-gradient-to-br from-slate-50 to-white ring-1 ring-slate-100`}>
<div
className="absolute left-0 top-0 transition-transform duration-300 ease-out"
className={`${fullPage ? 'relative' : 'absolute left-0 top-0 transition-transform duration-300 ease-out'}`}
style={{
width: miniGraphWidth,
height: miniGraphHeight,
@@ -1832,6 +1842,7 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, side
export default function App() {
const urlParams = new URLSearchParams(window.location.search);
const isDetailWindow = urlParams.get('view') === 'program-detail' || urlParams.get('view') === 'cheonjiin-detail';
const isRelationMapWindow = urlParams.get('view') === 'relation-map';
const detailProgramId = urlParams.get('program') ?? 'cheonjiin';
const [programStates, setProgramStates] = useState(readStoredProgramStates);
const [isEditing, setIsEditing] = useState(false);
@@ -2479,6 +2490,14 @@ export default function App() {
);
};
const openRelationMapWindow = () => {
window.open(
'/?view=relation-map',
'program-relation-map',
'popup=yes,width=1280,height=920,left=120,top=40,resizable=yes,scrollbars=yes'
);
};
if (isDetailWindow) {
const detailProgram = programs.find((program) => program.id === detailProgramId) ?? programs[0];
const storedProgram = getStoredProgram(detailProgram.id);
@@ -2506,6 +2525,21 @@ export default function App() {
);
}
if (isRelationMapWindow) {
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,#dffcf4_0,#f7f8fa_34%,#eef2ff_100%)] text-slate-900">
<RelationTreePanel
programs={programs}
onProgramClick={openProgramWindow}
onOpenRelationPopup={() => setIsRelationPopupOpen(true)}
sidebarWidth={1280}
onSidebarWidthChange={() => {}}
fullPage
/>
</main>
);
}
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,#dffcf4_0,#f7f8fa_34%,#eef2ff_100%)] text-slate-900">
<div className="mx-auto max-w-[1760px] space-y-4 px-3 py-4">
@@ -2540,6 +2574,7 @@ export default function App() {
programs={programs}
onProgramClick={openProgramWindow}
onOpenRelationPopup={() => setIsRelationPopupOpen(true)}
onOpenMapWindow={openRelationMapWindow}
sidebarWidth={sidebarWidth}
onSidebarWidthChange={setSidebarWidth}
/>