-
-
-
연결도
-
- 입력한 선행/후행 관계를 화살표로 표시합니다
-
-
-
-
- {relations.length}개 연결
-
-
-
-
-
- {relations.length > 0 ? (
-
-
-
- {graphPrograms.map((program) => {
- const position = nodePositions[program.id];
- if (!position) return null;
- const typeMeta = getProgramTypeMeta(program.programType);
- return (
-
-
{program.name}
-
- {typeMeta.shortLabel}
-
-
- );
- })}
-
-
- ) : (
-
- 아직 연결된 프로그램이 없습니다. 아래에서 선행/후행을 선택하세요.
-
- )}
- {isolatedPrograms.length > 0 && (
-
- 미연결
- {isolatedPrograms.map((program) => (
-
- {program.name}
-
- ))}
-
- )}
-
+
+
+
프로그램 연결 수정
+
+ 각 프로그램의 선행/후행을 선택하면 메인 연결도에 바로 반영됩니다.
+
-
- {isRelationEditorOpen && (
-
-
-
+
+ {programs.map((program) => {
+ const candidates = programs.filter((item) => item.id !== program.id);
+ return (
+
-
프로그램 연결 수정
-
- 각 프로그램의 선행/후행을 선택하면 연결도가 바로 반영됩니다
-
+
프로그램
+
{program.name}
-
-
-
- {programs.map((program) => {
- const candidates = programs.filter((item) => item.id !== program.id);
- return (
-
-
-
프로그램
-
{program.name}
-
-
-
선행
-
- {candidates.map((candidate) => (
-
- ))}
-
-
-
-
후행
-
- {candidates.map((candidate) => (
-
- ))}
-
-
+
+
선행
+
+ {candidates.map((candidate) => (
+
+ ))}
- );
- })}
-
-
-
- )}
+
+
+
후행
+
+ {candidates.map((candidate) => (
+
+ ))}
+
+
+
+ );
+ })}
+
);
}
-function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
+function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClose }) {
+ const internalProgram = programs.find((program) => getProgramType(program.programType) === 'internal');
+ const commercialProgram = programs.find((program) => getProgramType(program.programType) === 'commercial');
+ const [leftProgramId, setLeftProgramId] = useState(internalProgram?.id ?? programs[0]?.id ?? '');
+ const [rightProgramId, setRightProgramId] = useState(
+ commercialProgram?.id ?? programs.find((program) => program.id !== (internalProgram?.id ?? programs[0]?.id))?.id ?? ''
+ );
+ const leftProgram = programs.find((program) => program.id === leftProgramId);
+ const rightProgram = programs.find((program) => program.id === rightProgramId);
+ const comparison =
+ comparisons.find((item) => item.leftProgramId === leftProgramId && item.rightProgramId === rightProgramId) ?? {};
+
+ const updateComparison = (field, value) => {
+ if (!leftProgramId || !rightProgramId || leftProgramId === rightProgramId) return;
+ onComparisonChange(leftProgramId, rightProgramId, field, value);
+ };
+
+ const renderProgramSummary = (program) => {
+ if (!program) return null;
+ const typeMeta = getProgramTypeMeta(program.programType);
+ return (
+
+
+
+
선택 프로그램
+
{program.name}
+
+
+ {typeMeta.shortLabel}
+
+
+
{program.description}
+
+ {(program.deliverables ?? []).map((deliverable) => (
+
+ {deliverable}
+
+ ))}
+
+
+ 포맷: {program.format || '-'}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ 1:1 프로그램 비교
+
+
사내 한계와 상용 활용 기능 정리
+
+
+
+
+
+
+
+
+
+
+ {renderProgramSummary(leftProgram)}
+ {renderProgramSummary(rightProgram)}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, sidebarWidth, onSidebarWidthChange }) {
const [expandedProgramIds, setExpandedProgramIds] = useState(() => new Set(programs.map((program) => program.id)));
- const [mapOffset, setMapOffset] = useState(0);
const programMap = Object.fromEntries(programs.map((program) => [program.id, program]));
const relations = programs.flatMap((program) =>
(program.successors ?? [])
@@ -1483,17 +1409,19 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
const miniNodeWidth = 128;
const miniNodeHeight = 38;
const miniColumnGap = 52;
- const miniRowGap = 78;
+ const miniRowGap = 96;
const miniPadding = 34;
const miniSideLaneWidth = 96;
- const miniViewportWidth = 248;
+ const sidebarSizes = [280, 420, 560];
+ const resolvedSidebarWidth = sidebarWidth ?? sidebarSizes[0];
+ const miniViewportWidth = Math.max(230, resolvedSidebarWidth - 32);
const miniMaxLevelCount = Math.max(1, ...graphLevels.map((level) => level?.length ?? 0));
const miniGraphWidth = Math.max(
560,
miniPadding * 2 + miniSideLaneWidth + miniMaxLevelCount * miniNodeWidth + Math.max(0, miniMaxLevelCount - 1) * miniColumnGap
);
const miniGraphHeight = Math.max(
- 500,
+ 660,
miniPadding * 2 + graphLevels.length * miniNodeHeight + Math.max(0, graphLevels.length - 1) * miniRowGap
);
const miniNodePositions = Object.fromEntries(
@@ -1510,19 +1438,11 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
]);
})
);
- const maxMapOffset = Math.max(0, miniGraphWidth - miniViewportWidth);
- const defaultMapOffset = Math.max(0, Math.round((miniGraphWidth - miniViewportWidth) / 2));
-
- useEffect(() => {
- setMapOffset((current) => (
- current === 0
- ? Math.min(defaultMapOffset, maxMapOffset)
- : Math.min(current, maxMapOffset)
- ));
- }, [defaultMapOffset, maxMapOffset]);
-
- const moveMap = (direction) => {
- setMapOffset((current) => Math.min(maxMapOffset, Math.max(0, current + direction * 150)));
+ const mapOffset = 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);
};
const toggleExpanded = (programId) => {
@@ -1609,13 +1529,13 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
};
return (
-