From 30d6d2cbb8e348ccfbb3a36d50ed50a2694aa222 Mon Sep 17 00:00:00 2001 From: Hyein Date: Wed, 24 Jun 2026 13:49:22 +0900 Subject: [PATCH] Improve program relation map comparison flow --- src/App.jsx | 773 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 559 insertions(+), 214 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 09536fc..e373e85 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ArrowDown, ArrowRight, @@ -207,7 +207,8 @@ const defaultContent = { format: 'glb', programType: 'internal', predecessors: [], - successors: [] + successors: [], + mergeGroup: '' }, wayPrimal: { name: 'WayPrimal', @@ -218,6 +219,7 @@ const defaultContent = { programType: 'internal', predecessors: [], successors: [], + mergeGroup: '', linkLabel: '천지인 산출 모델을 WayPrimal 설계 입력으로 연계' }, comparisons: [], @@ -233,6 +235,7 @@ function normalizeStoredContent(parsed) { programType: getProgramType(parsed.cheonjiin?.programType ?? defaultContent.cheonjiin.programType), predecessors: parsed.cheonjiin?.predecessors ?? [], successors: parsed.cheonjiin?.successors ?? [], + mergeGroup: parsed.cheonjiin?.mergeGroup ?? '', format: !parsed.cheonjiin?.format || parsed.cheonjiin.format === '예: DXF, SHP, GeoTIFF, 수치지형도 v2 등' ? defaultContent.cheonjiin.format @@ -244,17 +247,23 @@ function normalizeStoredContent(parsed) { programType: getProgramType(parsed.wayPrimal?.programType ?? defaultContent.wayPrimal.programType), predecessors: parsed.wayPrimal?.predecessors ?? [], successors: parsed.wayPrimal?.successors ?? [], + mergeGroup: parsed.wayPrimal?.mergeGroup ?? '', format: parsed.wayPrimal?.format === '예: DWG, LandXML, XLSX, 도공계산서, 기본설계 모델 등' ? defaultContent.wayPrimal.format : (parsed.wayPrimal?.format ?? defaultContent.wayPrimal.format) }, - comparisons: parsed.comparisons ?? [], + comparisons: (parsed.comparisons ?? []).map((comparison, index) => ({ + ...comparison, + id: comparison.id ?? `comparison-${comparison.leftProgramId}-${comparison.rightProgramId}-${index}`, + title: comparison.title ?? '' + })), extraPrograms: (parsed.extraPrograms ?? []).map((program, index, programs) => ({ ...program, programType: getProgramType(program.programType), predecessors: program.predecessors ?? [], successors: program.successors ?? [], + mergeGroup: program.mergeGroup ?? '', linkLabel: program.linkLabel ?? `이전 프로그램 산출물을 ${program.name} 입력으로 연계` })) }; @@ -1149,7 +1158,7 @@ function DetailPopup({ ); } -function RelationPopup({ programs, onToggleRelation, onClose }) { +function RelationPopup({ programs, onToggleRelation, onMergeGroupChange, onClose }) { return (
@@ -1180,7 +1189,7 @@ function RelationPopup({ programs, onToggleRelation, onClose }) { {programs.map((program) => { const candidates = programs.filter((item) => item.id !== program.id); return ( -
+

프로그램

{program.name}

@@ -1217,6 +1226,27 @@ function RelationPopup({ programs, onToggleRelation, onClose }) { ))}
+
+

하나로 인식

+ +

+ 같은 단계로 보는 프로그램을 묶으면 연결도에서는 하나의 박스로 표시됩니다. +

+
); })} @@ -1227,67 +1257,125 @@ function RelationPopup({ programs, onToggleRelation, onClose }) { ); } -function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClose }) { +function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonSave, onComparisonDelete, onClose }) { const internalProgram = programs.find((program) => getProgramType(program.programType) === 'internal'); const commercialProgram = programs.find((program) => getProgramType(program.programType) === 'commercial'); - const savedComparisons = comparisons.filter( - (comparison) => - programs.some((program) => program.id === comparison.leftProgramId) && - programs.some((program) => program.id === comparison.rightProgramId) && - ((comparison.stepMatches ?? []).some((match) => match.reason?.trim()) || comparison.note?.trim()) - ).sort((left, right) => (right.updatedAt ?? '').localeCompare(left.updatedAt ?? '')); - const [leftProgramId, setLeftProgramId] = useState(savedComparisons[0]?.leftProgramId ?? internalProgram?.id ?? programs[0]?.id ?? ''); - const [rightProgramId, setRightProgramId] = useState( - savedComparisons[0]?.rightProgramId ?? - 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 hasSavedComparison = Boolean((comparison.stepMatches ?? []).some((match) => match.reason?.trim()) || comparison.note?.trim()); + const savedComparisons = comparisons + .filter( + (comparison) => + programs.some((program) => program.id === comparison.leftProgramId) && + programs.some((program) => program.id === comparison.rightProgramId) && + (comparison.title?.trim() || (comparison.stepMatches ?? []).some((match) => match.reason?.trim()) || comparison.note?.trim()) + ) + .sort((left, right) => (right.updatedAt ?? '').localeCompare(left.updatedAt ?? '')); + const createComparisonId = (leftProgramId, rightProgramId) => `comparison-${[leftProgramId, rightProgramId].sort().join('__')}`; + const isValidProgramId = (programId) => programs.some((program) => program.id === programId); + const pairMatches = (comparison, leftProgramId, rightProgramId) => + [comparison.leftProgramId, comparison.rightProgramId].sort().join('__') === [leftProgramId, rightProgramId].sort().join('__'); + const defaultLeftProgramId = isValidProgramId(initialPair?.leftProgramId) + ? initialPair.leftProgramId + : internalProgram?.id ?? programs[0]?.id ?? ''; + const defaultRightProgramId = isValidProgramId(initialPair?.rightProgramId) && initialPair.rightProgramId !== defaultLeftProgramId + ? initialPair.rightProgramId + : commercialProgram?.id ?? programs.find((program) => program.id !== defaultLeftProgramId)?.id ?? ''; + const findSavedComparisonByPair = (leftProgramId, rightProgramId) => + savedComparisons.find((comparison) => pairMatches(comparison, leftProgramId, rightProgramId)); + const createEmptyDraft = (leftProgramId = defaultLeftProgramId, rightProgramId = defaultRightProgramId) => ({ + id: createComparisonId(leftProgramId, rightProgramId), + title: '', + leftProgramId, + rightProgramId, + stepMatches: [], + note: '' + }); + const [draft, setDraft] = useState(() => { + const pairComparison = initialPair ? findSavedComparisonByPair(defaultLeftProgramId, defaultRightProgramId) : null; + return pairComparison ?? (initialPair ? createEmptyDraft(defaultLeftProgramId, defaultRightProgramId) : savedComparisons[0] ?? createEmptyDraft()); + }); + const [loadTargetId, setLoadTargetId] = useState(() => { + const pairComparison = initialPair ? findSavedComparisonByPair(defaultLeftProgramId, defaultRightProgramId) : null; + return pairComparison?.id ?? (initialPair ? '' : savedComparisons[0]?.id ?? ''); + }); + const leftProgram = programs.find((program) => program.id === draft.leftProgramId); + const rightProgram = programs.find((program) => program.id === draft.rightProgramId); const leftSteps = leftProgram?.steps ?? []; const rightSteps = rightProgram?.steps ?? []; - const stepMatches = comparison.stepMatches ?? []; + const stepMatches = draft.stepMatches ?? []; const maxStepCount = Math.max(leftSteps.length, rightSteps.length); + const isSavedDraft = savedComparisons.some((comparison) => comparison.id === draft.id); - const updateComparison = (field, value) => { - if (!leftProgramId || !rightProgramId || leftProgramId === rightProgramId) return; - onComparisonChange(leftProgramId, rightProgramId, field, value); + const updateDraft = (field, value) => { + setDraft((current) => ({ ...current, [field]: value })); + }; + const startNewComparison = () => { + const nextDraft = createEmptyDraft(); + setDraft(nextDraft); + setLoadTargetId(''); + }; + const loadSelectedComparison = () => { + const selected = savedComparisons.find((comparison) => comparison.id === loadTargetId); + if (!selected) return; + setDraft({ + ...selected, + stepMatches: selected.stepMatches ?? [], + note: selected.note ?? '', + title: selected.title ?? '' + }); + }; + const saveDraft = () => { + if (!draft.leftProgramId || !draft.rightProgramId || draft.leftProgramId === draft.rightProgramId) return; + onComparisonSave({ + ...draft, + id: createComparisonId(draft.leftProgramId, draft.rightProgramId), + title: draft.title ?? '', + stepMatches: (draft.stepMatches ?? []).filter((match) => match.reason?.trim()), + note: draft.note ?? '', + updatedAt: new Date().toISOString() + }); + setLoadTargetId(draft.id); + }; + const deleteDraft = () => { + if (isSavedDraft) onComparisonDelete(draft.id); + startNewComparison(); }; const getStepMatch = (stepIndex) => stepMatches.find( (match) => String(match.leftStepIndex) === String(stepIndex) && String(match.rightStepIndex) === String(stepIndex) ) ?? {}; const updateStepReason = (stepIndex, value) => { - const existingIndex = stepMatches.findIndex( - (match) => String(match.leftStepIndex) === String(stepIndex) && String(match.rightStepIndex) === String(stepIndex) - ); - if (!value.trim() && existingIndex >= 0) { - updateComparison('stepMatches', stepMatches.filter((_, index) => index !== existingIndex)); - return; - } - if (!value.trim()) return; - const nextMatch = { - ...(existingIndex >= 0 ? stepMatches[existingIndex] : {}), - id: `step-${stepIndex}`, - leftStepIndex: String(stepIndex), - rightStepIndex: String(stepIndex), - reason: value - }; - updateComparison( - 'stepMatches', - existingIndex >= 0 - ? stepMatches.map((match, index) => (index === existingIndex ? nextMatch : match)) - : [...stepMatches, nextMatch] - ); + setDraft((current) => { + const currentMatches = current.stepMatches ?? []; + const existingIndex = currentMatches.findIndex( + (match) => String(match.leftStepIndex) === String(stepIndex) && String(match.rightStepIndex) === String(stepIndex) + ); + if (!value.trim()) { + return { + ...current, + stepMatches: existingIndex >= 0 ? currentMatches.filter((_, index) => index !== existingIndex) : currentMatches + }; + } + const nextMatch = { + ...(existingIndex >= 0 ? currentMatches[existingIndex] : {}), + id: `step-${stepIndex}`, + leftStepIndex: String(stepIndex), + rightStepIndex: String(stepIndex), + reason: value + }; + return { + ...current, + stepMatches: + existingIndex >= 0 + ? currentMatches.map((match, index) => (index === existingIndex ? nextMatch : match)) + : [...currentMatches, nextMatch] + }; + }); }; const getStepLabel = (step, index) => `${index + 1}. ${step?.title ?? '-'}`; const getProgramName = (programId) => programs.find((program) => program.id === programId)?.name ?? programId; - const getComparisonMemoCount = (savedComparison) => - (savedComparison.stepMatches ?? []).filter((match) => match.reason?.trim()).length; + const getComparisonTitle = (comparison) => + comparison.title?.trim() || `${getProgramName(comparison.leftProgramId)} ↔ ${getProgramName(comparison.rightProgramId)}`; + const getComparisonMemoCount = (comparison) => + (comparison.stepMatches ?? []).filter((match) => match.reason?.trim()).length; const renderProgramSummary = (program) => { if (!program) return null; @@ -1323,9 +1411,7 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
-

- 1:1 프로그램 비교 -

+

1:1 프로그램 비교

사내 한계와 상용 활용 기능 정리

-
- +
- - {savedComparisons.length > 0 && ( -
-
- 저장된 비교 - {savedComparisons.map((savedComparison) => { - const active = - savedComparison.leftProgramId === leftProgramId && - savedComparison.rightProgramId === rightProgramId; - const memoCount = getComparisonMemoCount(savedComparison); - return ( - - ); - })} -
+ 새 비교 +
- )} +
+ + {isSavedDraft && ( + + )} +
+
+ + +
+
{renderProgramSummary(leftProgram)} @@ -1414,13 +1520,10 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
-
-

전체 스텝 1:1 비교

-

- 두 프로그램의 스텝을 전부 펼친 뒤, 가운데에 해당 스텝에서 상용 프로그램을 쓰는 이유를 적습니다. - {hasSavedComparison ? ' 저장된 비교 내용을 불러왔습니다.' : ''} -

-
+

전체 스텝 1:1 비교

+

+ 두 프로그램의 스텝을 전부 펼친 뒤, 가운데에 해당 스텝에서 상용 프로그램을 쓰는 이유를 적습니다. +

@@ -1434,49 +1537,45 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos const rightStep = rightSteps[stepIndex]; const match = getStepMatch(stepIndex); return ( -
-
- {leftProgram?.name ?? 'A'} 스텝 -

- {leftStep ? getStepLabel(leftStep, stepIndex) : `${stepIndex + 1}. -`} -

-

- {leftStep?.feature || '해당 스텝 없음'} -

- {leftStep?.note && ( -

- {leftStep.note} +

+
+ {leftProgram?.name ?? 'A'} 스텝 +

+ {leftStep ? getStepLabel(leftStep, stepIndex) : `${stepIndex + 1}. -`} +

+

+ {leftStep?.feature || '해당 스텝 없음'}

- )} -
-