diff --git a/src/App.jsx b/src/App.jsx
index e373e85..341c7d9 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1178,7 +1178,7 @@ function RelationPopup({ programs, onToggleRelation, onMergeGroupChange, onClose
@@ -1296,6 +1296,12 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
const pairComparison = initialPair ? findSavedComparisonByPair(defaultLeftProgramId, defaultRightProgramId) : null;
return pairComparison?.id ?? (initialPair ? '' : savedComparisons[0]?.id ?? '');
});
+ const [isCompareEditing, setIsCompareEditing] = useState(() => {
+ const pairComparison = initialPair ? findSavedComparisonByPair(defaultLeftProgramId, defaultRightProgramId) : null;
+ return !pairComparison && !savedComparisons[0];
+ });
+ const [formatMenu, setFormatMenu] = useState(null);
+ const [lastTextSelection, setLastTextSelection] = useState(null);
const leftProgram = programs.find((program) => program.id === draft.leftProgramId);
const rightProgram = programs.find((program) => program.id === draft.rightProgramId);
const leftSteps = leftProgram?.steps ?? [];
@@ -1303,6 +1309,10 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
const stepMatches = draft.stepMatches ?? [];
const maxStepCount = Math.max(leftSteps.length, rightSteps.length);
const isSavedDraft = savedComparisons.some((comparison) => comparison.id === draft.id);
+ const hasDraftContent =
+ draft.title?.trim() ||
+ (draft.stepMatches ?? []).some((match) => match.reason?.trim()) ||
+ draft.note?.trim();
const updateDraft = (field, value) => {
setDraft((current) => ({ ...current, [field]: value }));
@@ -1311,6 +1321,7 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
const nextDraft = createEmptyDraft();
setDraft(nextDraft);
setLoadTargetId('');
+ setIsCompareEditing(true);
};
const loadSelectedComparison = () => {
const selected = savedComparisons.find((comparison) => comparison.id === loadTargetId);
@@ -1321,6 +1332,7 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
note: selected.note ?? '',
title: selected.title ?? ''
});
+ setIsCompareEditing(false);
};
const saveDraft = () => {
if (!draft.leftProgramId || !draft.rightProgramId || draft.leftProgramId === draft.rightProgramId) return;
@@ -1333,6 +1345,7 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
updatedAt: new Date().toISOString()
});
setLoadTargetId(draft.id);
+ setIsCompareEditing(false);
};
const deleteDraft = () => {
if (isSavedDraft) onComparisonDelete(draft.id);
@@ -1376,6 +1389,174 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
comparison.title?.trim() || `${getProgramName(comparison.leftProgramId)} ↔ ${getProgramName(comparison.rightProgramId)}`;
const getComparisonMemoCount = (comparison) =>
(comparison.stepMatches ?? []).filter((match) => match.reason?.trim()).length;
+ const stripFormatTags = (text = '') => text.replace(/\[(red|redbold|blue|bold|mark)\]([\s\S]*?)\[\/\1\]/g, '$2').trim();
+ const generateComparisonConclusion = () => {
+ const reasonMatches = (draft.stepMatches ?? [])
+ .map((match) => {
+ const stepIndex = Number(match.leftStepIndex);
+ return {
+ ...match,
+ stepIndex,
+ reason: stripFormatTags(match.reason)
+ };
+ })
+ .filter((match) => match.reason);
+
+ if (reasonMatches.length === 0) {
+ updateDraft('note', '상용 활용 사유가 아직 작성되지 않아 자동 결론을 생성할 수 없습니다.');
+ return;
+ }
+
+ const stepNames = reasonMatches
+ .map((match) => leftSteps[match.stepIndex]?.title || rightSteps[match.stepIndex]?.title)
+ .filter(Boolean);
+ const uniqueStepNames = [...new Set(stepNames)];
+ const stepSummary = uniqueStepNames.length > 0
+ ? uniqueStepNames.slice(0, 4).join(', ')
+ : '해당 비교 스텝';
+ const overflowText = uniqueStepNames.length > 4 ? ` 외 ${uniqueStepNames.length - 4}개 스텝` : '';
+ const keyReasons = reasonMatches
+ .slice(0, 3)
+ .map((match) => `- ${match.reason}`)
+ .join('\n');
+ const conclusion = `${leftProgram?.name ?? '사내 프로그램'}은 ${stepSummary}${overflowText}에서 기능 구현 또는 자동화 범위에 한계가 있어 ${rightProgram?.name ?? '상용 프로그램'}의 기능 보완이 필요합니다.\n\n주요 사유\n${keyReasons}\n\n따라서 현재 업무 흐름에서는 해당 구간을 ${rightProgram?.name ?? '상용 프로그램'}으로 연계하여 결과 품질과 작업 연속성을 확보하는 것이 적절합니다.`;
+ updateDraft('note', conclusion);
+ };
+ const renderFormattedText = (text, emptyText = '작성된 내용이 없습니다.') => {
+ const source = text?.trim();
+ if (!source) return {emptyText};
+ const pattern = /\[(red|redbold|blue|bold|mark)\]([\s\S]*?)\[\/\1\]/g;
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+ while ((match = pattern.exec(source)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push({ type: 'text', value: source.slice(lastIndex, match.index) });
+ }
+ parts.push({ type: match[1], value: match[2] });
+ lastIndex = pattern.lastIndex;
+ }
+ if (lastIndex < source.length) {
+ parts.push({ type: 'text', value: source.slice(lastIndex) });
+ }
+ const classMap = {
+ red: 'font-black text-red-600',
+ redbold: 'font-black text-red-700',
+ blue: 'font-black text-blue-600',
+ bold: 'font-black text-slate-950',
+ mark: 'rounded bg-yellow-200/80 px-1 font-black text-slate-950'
+ };
+ return parts.map((part, index) => {
+ if (part.type === 'text') {
+ return
1:1 프로그램 비교
비교 보고서
++ 두 프로그램의 동일/연계 스텝에서 사내 한계와 상용 프로그램 활용 사유를 정리합니다. +
+{leftStep.note}
)}+ {renderFormattedText(match.reason, '작성된 비교 내용이 없습니다.')} +
++ {draft.note?.trim() + ? renderFormattedText(draft.note) + : hasDraftContent + ? '별도 결론이 작성되지 않았습니다.' + : '아직 작성된 비교 보고서가 없습니다. 편집을 눌러 내용을 입력하세요.'} +
+