Add step based program comparison

This commit is contained in:
2026-06-24 10:24:03 +09:00
parent c9142836e6
commit 0e32c930d8

View File

@@ -1238,11 +1238,33 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
const rightProgram = programs.find((program) => program.id === rightProgramId); const rightProgram = programs.find((program) => program.id === rightProgramId);
const comparison = const comparison =
comparisons.find((item) => item.leftProgramId === leftProgramId && item.rightProgramId === rightProgramId) ?? {}; comparisons.find((item) => item.leftProgramId === leftProgramId && item.rightProgramId === rightProgramId) ?? {};
const leftSteps = leftProgram?.steps ?? [];
const rightSteps = rightProgram?.steps ?? [];
const stepMatches = comparison.stepMatches ?? [];
const updateComparison = (field, value) => { const updateComparison = (field, value) => {
if (!leftProgramId || !rightProgramId || leftProgramId === rightProgramId) return; if (!leftProgramId || !rightProgramId || leftProgramId === rightProgramId) return;
onComparisonChange(leftProgramId, rightProgramId, field, value); onComparisonChange(leftProgramId, rightProgramId, field, value);
}; };
const createStepMatch = () => ({
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
leftStepIndex: '0',
rightStepIndex: '0',
reason: ''
});
const addStepMatch = () => {
updateComparison('stepMatches', [...stepMatches, createStepMatch()]);
};
const updateStepMatch = (matchIndex, field, value) => {
updateComparison(
'stepMatches',
stepMatches.map((match, index) => (index === matchIndex ? { ...match, [field]: value } : match))
);
};
const removeStepMatch = (matchIndex) => {
updateComparison('stepMatches', stepMatches.filter((_, index) => index !== matchIndex));
};
const getStepLabel = (step, index) => `${index + 1}. ${step?.title ?? '-'}`;
const renderProgramSummary = (program) => { const renderProgramSummary = (program) => {
if (!program) return null; if (!program) return null;
@@ -1298,7 +1320,13 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
<span className="text-[12px] font-black text-slate-500">비교 프로그램 A</span> <span className="text-[12px] font-black text-slate-500">비교 프로그램 A</span>
<select <select
value={leftProgramId} value={leftProgramId}
onChange={(event) => setLeftProgramId(event.target.value)} onChange={(event) => {
const nextProgramId = event.target.value;
setLeftProgramId(nextProgramId);
if (nextProgramId === rightProgramId) {
setRightProgramId(programs.find((program) => program.id !== nextProgramId)?.id ?? '');
}
}}
className="mt-1 w-full rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-extrabold text-slate-800 outline-none focus:border-blue-400" className="mt-1 w-full rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-extrabold text-slate-800 outline-none focus:border-blue-400"
> >
{programs.map((program) => ( {programs.map((program) => (
@@ -1329,44 +1357,99 @@ function ProgramComparePopup({ programs, comparisons, onComparisonChange, onClos
{renderProgramSummary(rightProgram)} {renderProgramSummary(rightProgram)}
</div> </div>
<div className="mt-4 grid gap-3 rounded-3xl bg-white p-4 ring-1 ring-slate-100 lg:grid-cols-2"> <div className="mt-4 rounded-3xl bg-white p-4 ring-1 ring-slate-100">
<label className="block"> <div className="flex items-center justify-between gap-3">
<span className="text-[12px] font-black text-amber-700">A에서 되는 기능</span> <div>
<textarea <h3 className="text-base font-black text-slate-950">스텝 1:1 매칭</h3>
value={comparison.missingFeature ?? ''} <p className="mt-1 text-[12px] font-bold text-slate-500">
onChange={(event) => updateComparison('missingFeature', event.target.value)} 왼쪽 스텝과 오른쪽 스텝을 연결하고, 가운데에 상용 프로그램을 쓰는 이유를 적습니다.
rows={4} </p>
placeholder="예: 3D 형상 직접 편집, obj/ifc 포맷 변환 등" </div>
className="mt-1 w-full resize-y rounded-2xl border border-amber-100 bg-amber-50/50 px-3 py-2 text-sm font-bold leading-6 text-slate-800 outline-none focus:border-amber-300" <button
/> type="button"
</label> onClick={addStepMatch}
<label className="block"> className="flex shrink-0 items-center gap-1.5 rounded-full bg-blue-600 px-3.5 py-2 text-[12px] font-black text-white shadow-sm hover:bg-blue-700"
<span className="text-[12px] font-black text-blue-700">B에서 수행하는 기능</span> >
<textarea <Plus className="h-3.5 w-3.5" />
value={comparison.alternativeFeature ?? ''} 매칭 추가
onChange={(event) => updateComparison('alternativeFeature', event.target.value)} </button>
rows={4} </div>
placeholder="예: 모델 방향 수정, 속성정보 입력, 포맷 변환"
className="mt-1 w-full resize-y rounded-2xl border border-blue-100 bg-blue-50/50 px-3 py-2 text-sm font-bold leading-6 text-slate-800 outline-none focus:border-blue-300" <div className="mt-4 space-y-3">
/> {stepMatches.length === 0 ? (
</label> <div className="rounded-2xl bg-slate-50 px-4 py-6 text-center text-sm font-bold text-slate-500 ring-1 ring-slate-100">
<label className="block lg:col-span-2"> 아직 매칭된 스텝이 없습니다. `매칭 추가` 눌러 스텝을 연결하세요.
<span className="text-[12px] font-black text-slate-700">사용 이유 / 결론</span> </div>
<textarea ) : (
value={comparison.reason ?? ''} stepMatches.map((match, matchIndex) => (
onChange={(event) => updateComparison('reason', event.target.value)} <div
rows={3} key={match.id ?? matchIndex}
placeholder="예: 사내 프로그램에서 해당 편집 기능이 없어 현재는 상용 프로그램을 보완 도구로 사용" className="grid gap-3 rounded-2xl border border-slate-100 bg-slate-50/80 p-3 lg:grid-cols-[minmax(0,1fr)_minmax(260px,1.35fr)_minmax(0,1fr)_40px]"
className="mt-1 w-full resize-y rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-bold leading-6 text-slate-800 outline-none focus:border-slate-400" >
/> <label className="block">
</label> <span className="text-[11px] font-black text-amber-700">{leftProgram?.name ?? 'A'} 스텝</span>
<label className="block lg:col-span-2"> <select
<span className="text-[12px] font-black text-slate-500">비고</span> value={match.leftStepIndex ?? '0'}
onChange={(event) => updateStepMatch(matchIndex, 'leftStepIndex', event.target.value)}
className="mt-1 w-full rounded-xl border border-amber-100 bg-white px-3 py-2 text-[12px] font-extrabold text-slate-800 outline-none focus:border-amber-300"
>
{leftSteps.map((step, index) => (
<option key={`${step.id ?? step.title}-${index}`} value={String(index)}>
{getStepLabel(step, index)}
</option>
))}
</select>
<p className="mt-2 whitespace-pre-line rounded-xl bg-white/70 px-3 py-2 text-[12px] font-bold leading-5 text-slate-600 ring-1 ring-amber-50">
{leftSteps[Number(match.leftStepIndex ?? 0)]?.feature || '-'}
</p>
</label>
<label className="block">
<span className="text-[11px] font-black text-slate-700"> 되는 기능 / 상용 사용 이유</span>
<textarea
value={match.reason ?? ''}
onChange={(event) => updateStepMatch(matchIndex, 'reason', event.target.value)}
rows={5}
placeholder="예: 이 스텝에서 3D 형상 수정과 obj/ifc 변환이 안 돼서 Rhino의 모델 편집/포맷 변환 기능을 사용"
className="mt-1 h-[128px] w-full resize-y rounded-xl border border-slate-200 bg-white px-3 py-2 text-[12px] font-bold leading-5 text-slate-800 outline-none focus:border-blue-300"
/>
</label>
<label className="block">
<span className="text-[11px] font-black text-blue-700">{rightProgram?.name ?? 'B'} 스텝</span>
<select
value={match.rightStepIndex ?? '0'}
onChange={(event) => updateStepMatch(matchIndex, 'rightStepIndex', event.target.value)}
className="mt-1 w-full rounded-xl border border-blue-100 bg-white px-3 py-2 text-[12px] font-extrabold text-slate-800 outline-none focus:border-blue-300"
>
{rightSteps.map((step, index) => (
<option key={`${step.id ?? step.title}-${index}`} value={String(index)}>
{getStepLabel(step, index)}
</option>
))}
</select>
<p className="mt-2 whitespace-pre-line rounded-xl bg-white/70 px-3 py-2 text-[12px] font-bold leading-5 text-slate-600 ring-1 ring-blue-50">
{rightSteps[Number(match.rightStepIndex ?? 0)]?.feature || '-'}
</p>
</label>
<button
type="button"
onClick={() => removeStepMatch(matchIndex)}
className="flex h-10 w-10 items-center justify-center self-start rounded-full bg-white text-slate-400 ring-1 ring-slate-200 hover:bg-red-50 hover:text-red-500"
aria-label="스텝 매칭 삭제"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))
)}
</div>
<label className="mt-4 block">
<span className="text-[12px] font-black text-slate-500">비교 결론</span>
<textarea <textarea
value={comparison.note ?? ''} value={comparison.note ?? ''}
onChange={(event) => updateComparison('note', event.target.value)} onChange={(event) => updateComparison('note', event.target.value)}
rows={2} rows={2}
placeholder="추가 검토사항이나 향후 대체 가능성 등을 적어주세요." placeholder="예: 현재는 해당 스텝 보완을 위해 상용 프로그램 사용이 필요함."
className="mt-1 w-full resize-y rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm font-bold leading-6 text-slate-700 outline-none focus:border-slate-400" className="mt-1 w-full resize-y rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm font-bold leading-6 text-slate-700 outline-none focus:border-slate-400"
/> />
</label> </label>