import React, { useEffect, useState } from 'react';
import {
  ArrowDown,
  ArrowRight,
  ArrowUp,
  Building2,
  ClipboardList,
  DraftingCompass,
  Edit3,
  Layers3,
  Map,
  Maximize2,
  Minimize2,
  Mountain,
  Plus,
  Route,
  Save,
  ScanLine,
  Trash2,
  Waypoints,
  X
} from 'lucide-react';

const cheonjiinFlow = [
  {
    id: '1-1',
    title: '범위입력',
    icon: ScanLine,
    feature: '직접선택, 도면 불러오기',
    note: '사업 대상지와 원본 도면 범위를 먼저 확정합니다.'
  },
  {
    id: '1-2',
    title: '선택/검토',
    icon: Layers3,
    feature: '등고선, 레이어 선택',
    note: '해석에 필요한 도면 요소와 레이어만 정리합니다.'
  },
  {
    id: '1-3',
    title: '현장조사',
    icon: Mountain,
    feature: '지표, 지층, 상세측량\n파일 불러오기\n지형, 지층 보정 모델',
    note: '실측 자료를 반영해 지형과 지층 정보를 보정합니다.'
  },
  {
    id: '1-4',
    title: '구조물편집',
    icon: Building2,
    feature: '도로, 건물, 하천, 지형\n수치지형도 v2 다운로드 시 적용\n구조물 포함 모델',
    note: '구조물과 지형 요소를 편집해 통합 모델을 만듭니다.'
  },
  {
    id: '1-5',
    title: '유역면적',
    icon: Map,
    feature: '유역 구분 및 면적 산정',
    note: '정리된 공간 정보를 바탕으로 유역면적 결과를 산정합니다.'
  }
];

const wayPrimalFlow = [
  {
    id: 'wp-1',
    title: '설계조건',
    icon: ClipboardList,
    feature: '전용조건 입력(도로, 포장 등)\n정온시설 위치 입력',
    note: '도로와 주변 조건을 설계 입력값으로 등록합니다.'
  },
  {
    id: 'wp-2',
    title: '노폭계획',
    icon: Route,
    feature: '선형계획, 노즈, 편경사,\n구조물, 시설계획 입력',
    note: '노선의 폭, 선형, 구조물 조건을 계획합니다.'
  },
  {
    id: 'wp-3',
    title: '기본계획',
    icon: Waypoints,
    feature: '길어깨편경사, 비탈면계획,\n부체도로, 횡단등도',
    note: '기본 단면과 주변 계획 조건을 정리합니다.'
  },
  {
    id: 'wp-4',
    title: '기본설계',
    icon: DraftingCompass,
    feature: '배수공, 포장공, 부대공 자동설치\n횡단면도 생성, 도공계산서 내보내기\n평면도, 횡단면도, 종평면도, 기본설계모델',
    note: '자동 설계 성과물과 기본설계 모델을 생성합니다.'
  }
];

const cheonjiinDeliverables = [
  '지형도',
  '수치지형도',
  '정사영상',
  '지형,지층 보정 모델'
];

const detailSteps = [
  {
    id: '1-1',
    title: '범위 입력',
    feature: '직접 선택 · 도면 불러오기',
    detail: '대상 유역 영역의 좌표 체계와 경계를 지정하고 원천 도면 파일을 등록합니다.',
    output: '활성 작업 영역 레이어'
  },
  {
    id: '1-2',
    title: '선택·검토',
    feature: '등고선 · 적용 레이어 선택',
    detail: '설계 연산에 사용할 등고선 데이터와 속성별 물리 레이어를 선별합니다.',
    output: '정제된 등고선 추출본'
  },
  {
    id: '1-3',
    title: '현장조사',
    feature: '지표 · 지층 · 상세측량 자료 입력',
    detail: 'GPS 고도 측량치와 시추 분석 데이터를 입력해 지형과 지층을 보정합니다.',
    output: '3차원 지표·지층 보정 모델'
  },
  {
    id: '1-4',
    title: '구조물 편집',
    feature: '도로 · 건물 · 하천 · 지형 편집',
    detail: '도로 중심선, 건물 폴리곤, 하천 경계 등 구조물 정보를 지형면에 정합합니다.',
    output: '구조물 포함 통합 3D GIS 모델'
  },
  {
    id: '1-5',
    title: '유역면적 산정',
    feature: '유역 구분 · 면적 산정 · 결과 검토',
    detail: '정립된 지형과 구조물 모델을 분석해 유역 경계를 구분하고 면적을 계산합니다.',
    output: '유역면적 최종 산정 결과'
  }
];

const reviewGates = [
  {
    key: 'gate1',
    stepId: '1-1',
    question: '범위 또는 원본 도면 변경?',
    yes: '예: 1-1 재진입',
    no: '아니오: 다음 검토'
  },
  {
    key: 'gate2',
    stepId: '1-2',
    question: '등고선 또는 적용 레이어 변경?',
    yes: '예: 1-2 재진입',
    no: '아니오: 다음 검토'
  },
  {
    key: 'gate3',
    stepId: '1-3',
    question: '지표·지층 또는 상세측량 정보 변경?',
    yes: '예: 1-3 재진입',
    no: '아니오: 다음 검토'
  },
  {
    key: 'gate4',
    stepId: '1-4',
    question: '도로·건물·하천 또는 지형 정보 변경?',
    yes: '예: 1-4 재진입',
    no: '아니오: 변경사항 없음'
  }
];

const initialGates = {
  gate1: 'no',
  gate2: 'no',
  gate3: 'no',
  gate4: 'no'
};

const channelName = 'program-flow-state';
const contentStorageKey = 'program-flow-content';
const programStateStorageKey = 'program-flow-gates';
const serverStateEndpoint = '/api/state';
const disabledFlowStep = '__disabled__';
const programTypeOptions = {
  internal: {
    label: '사내프로그램',
    shortLabel: '사내',
    rowClass: 'border-emerald-100 bg-emerald-50/55',
    badgeClass: 'bg-emerald-100 text-emerald-700 ring-emerald-200',
    dotClass: 'bg-emerald-500'
  },
  commercial: {
    label: '상용프로그램',
    shortLabel: '상용',
    rowClass: 'border-violet-100 bg-violet-50/55',
    badgeClass: 'bg-violet-100 text-violet-700 ring-violet-200',
    dotClass: 'bg-violet-500'
  }
};

const getProgramType = (type) => (type === 'commercial' ? 'commercial' : 'internal');
const getProgramTypeMeta = (type) => programTypeOptions[getProgramType(type)];

const defaultContent = {
  cheonjiin: {
    name: '천지인',
    description: '공간정보 입력·검토·보정·유역면적 산정',
    steps: cheonjiinFlow.map(({ title, feature, note }) => ({ title, feature, note })),
    deliverables: cheonjiinDeliverables,
    format: 'glb',
    programType: 'internal',
    predecessors: [],
    successors: []
  },
  wayPrimal: {
    name: 'WayPrimal',
    description: '설계조건 입력부터 기본설계 성과물 생성',
    steps: wayPrimalFlow.map(({ title, feature, note }) => ({ title, feature, note })),
    format: '',
    deliverables: ['기본설계 모델'],
    programType: 'internal',
    predecessors: [],
    successors: [],
    linkLabel: '천지인 산출 모델을 WayPrimal 설계 입력으로 연계'
  },
  comparisons: [],
  extraPrograms: []
};

function normalizeStoredContent(parsed) {
  if (!parsed) return defaultContent;
  return {
      cheonjiin: {
        ...defaultContent.cheonjiin,
        ...(parsed.cheonjiin ?? {}),
        programType: getProgramType(parsed.cheonjiin?.programType ?? defaultContent.cheonjiin.programType),
        predecessors: parsed.cheonjiin?.predecessors ?? [],
        successors: parsed.cheonjiin?.successors ?? [],
        format:
          !parsed.cheonjiin?.format || parsed.cheonjiin.format === '예: DXF, SHP, GeoTIFF, 수치지형도 v2 등'
            ? defaultContent.cheonjiin.format
            : parsed.cheonjiin.format
      },
      wayPrimal: {
        ...defaultContent.wayPrimal,
        ...(parsed.wayPrimal ?? {}),
        programType: getProgramType(parsed.wayPrimal?.programType ?? defaultContent.wayPrimal.programType),
        predecessors: parsed.wayPrimal?.predecessors ?? [],
        successors: parsed.wayPrimal?.successors ?? [],
        format:
          parsed.wayPrimal?.format === '예: DWG, LandXML, XLSX, 도공계산서, 기본설계 모델 등'
            ? defaultContent.wayPrimal.format
            : (parsed.wayPrimal?.format ?? defaultContent.wayPrimal.format)
      },
      comparisons: parsed.comparisons ?? [],
      extraPrograms: (parsed.extraPrograms ?? []).map((program, index, programs) => ({
        ...program,
        programType: getProgramType(program.programType),
        predecessors: program.predecessors ?? [],
        successors: program.successors ?? [],
        linkLabel: program.linkLabel ?? `이전 프로그램 산출물을 ${program.name} 입력으로 연계`
      }))
    };
}

function readStoredContent() {
  try {
    const stored = window.localStorage.getItem(contentStorageKey);
    if (!stored) return defaultContent;
    return normalizeStoredContent(JSON.parse(stored));
  } catch {
    return defaultContent;
  }
}

function mergeStepContent(flow, storedSteps = []) {
  const mergedBaseSteps = flow.map((step, index) => ({
    ...step,
    ...(storedSteps[index] ?? {})
  }));
  const addedSteps = storedSteps.slice(flow.length).map((step, index) => ({
    ...step,
    id: step.id ?? `added-step-${flow.length + index + 1}`,
    icon: step.icon ?? pickStepIcon(step)
  }));
  return [...mergedBaseSteps, ...addedSteps];
}

function pickStepIcon(step) {
  const text = `${step.title ?? ''} ${step.feature ?? ''} ${step.note ?? ''}`.toLowerCase();

  if (/모델|3d|glb|ifc|wbpifc|grasshopper|rhino|객체|형상/.test(text)) return Layers3;
  if (/포맷|파일|저장|출력|내보내기|생성|변환/.test(text)) return ClipboardList;
  if (/속성|정보|입력|데이터|plugin|플러그인/.test(text)) return Waypoints;
  if (/편집|수정|보정|변경|정합/.test(text)) return DraftingCompass;
  if (/도로|노선|선형|중심선|경로/.test(text)) return Route;
  if (/지도|수치지형도|유역|면적|범위|도면/.test(text)) return Map;
  if (/측량|지형|지층|고도|산/.test(text)) return Mountain;
  if (/스캔|영상|정사영상|레이어/.test(text)) return ScanLine;
  if (/구조물|건물|하천/.test(text)) return Building2;

  return ClipboardList;
}

function normalizeProgramSteps(program) {
  return program.steps.map((step, index) => ({
    id: step.id ?? `${program.id}-step-${index + 1}`,
    icon: pickStepIcon(step),
    title: step.title,
    feature: step.feature,
    note: step.note
  }));
}

function createInitialGates(steps) {
  return steps.slice(0, Math.max(1, steps.length - 1)).reduce((gates, step, index) => ({
    ...gates,
    [`gate${index + 1}`]: 'no'
  }), {});
}

function buildReviewItems(programId, steps, detailGates) {
  const hasStoredDetailGates = Array.isArray(detailGates);
  const storedDetailGates = hasStoredDetailGates ? detailGates : [];

  if (programId === 'cheonjiin') {
    const count = hasStoredDetailGates ? Math.max(1, storedDetailGates.length) : reviewGates.length;
    return Array.from({ length: count }, (_, index) => {
      const baseGate = reviewGates[index] ?? {
        key: `gate${index + 1}`,
        stepId: steps[Math.min(index, Math.max(0, steps.length - 2))]?.id ?? steps[0]?.id,
        question: `${index + 1}-${index + 1} 변경?`,
        yes: `예: ${index + 1}-${index + 1} 재진입`,
        no: index === count - 1 ? '아니오: 변경사항 없음' : '아니오: 다음 검토'
      };
      return {
        ...baseGate,
        ...(storedDetailGates[index] ?? {}),
        targetProgramId: storedDetailGates[index] ? storedDetailGates[index].targetProgramId : programId,
        key: storedDetailGates[index]?.key ?? baseGate.key ?? `gate${index + 1}`
      };
    });
  }

  const defaultCount = Math.max(1, steps.length - 1);
  const count = hasStoredDetailGates ? Math.max(1, storedDetailGates.length) : defaultCount;
  return Array.from({ length: count }, (_, index) => {
    const step = steps[Math.min(index, Math.max(0, steps.length - 2))] ?? steps[0];
    return {
      key: storedDetailGates[index]?.key ?? `gate${index + 1}`,
      targetProgramId: storedDetailGates[index] ? storedDetailGates[index].targetProgramId : programId,
      stepId: storedDetailGates[index]?.stepId ?? step?.id,
      question: storedDetailGates[index]?.question ?? `${step?.title ?? `${index + 1}단계`} 변경?`,
      yes: storedDetailGates[index]?.yes ?? `예: ${step?.title ?? `${index + 1}단계`} 재진입`,
      no: storedDetailGates[index]?.no ?? (index === count - 1 ? '아니오: 변경사항 없음' : '아니오: 다음 검토')
    };
  });
}

function readStoredProgramStates() {
  try {
    const stored = window.localStorage.getItem(programStateStorageKey);
    return stored ? JSON.parse(stored) : {};
  } catch {
    return {};
  }
}

function publishProgramStates(states) {
  try {
    window.localStorage.setItem(programStateStorageKey, JSON.stringify(states));
    patchServerState({ programStates: states });
    const channel = new BroadcastChannel(channelName);
    channel.postMessage({ states });
    channel.close();
  } catch {
    window.localStorage.setItem(programStateStorageKey, JSON.stringify(states));
    patchServerState({ programStates: states });
  }
}

function saveServerState(payload, method = 'PUT') {
  return fetch(serverStateEndpoint, {
    method,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });
}

function patchServerState(payload) {
  return saveServerState(payload, 'PATCH').catch(() => undefined);
}

function getProgramGates(states, programId, steps) {
  return {
    ...createInitialGates(steps),
    ...(states[programId] ?? {})
  };
}

function getActiveGate(gates, reviewItems) {
  return reviewItems.find((gate) => gates[gate.key] === 'yes');
}

function getActiveStep(gates, reviewItems) {
  return getActiveGate(gates, reviewItems)?.stepId ?? 'completed';
}

function getReentryContext(programs, programStates, getProgramReviewItems) {
  return programs
    .map((sourceProgram) => {
      const reviewItems = getProgramReviewItems(sourceProgram);
      const gates = getProgramGates(programStates, sourceProgram.id, sourceProgram.steps);
      const activeGate = getActiveGate(gates, reviewItems);
      if (!activeGate) return null;
      const targetProgramId = resolveGateTargetProgramId(activeGate, sourceProgram.id, programs);
      const targetProgramIndex = programs.findIndex((program) => program.id === targetProgramId);
      if (targetProgramIndex < 0) return null;
      return {
        sourceProgramId: sourceProgram.id,
        targetProgramId,
        targetProgramIndex,
        targetStepId: activeGate.stepId
      };
    })
    .find(Boolean);
}

function resolveGateTargetProgramId(gate, sourceProgramId, allPrograms) {
  if (gate?.targetProgramId) return gate.targetProgramId;
  return allPrograms.find((program) => program.steps.some((step) => step.id === gate?.stepId))?.id ?? sourceProgramId;
}

function getStepLabel(program, stepId) {
  return program?.steps.find((step) => step.id === stepId)?.title ?? stepId ?? '';
}

function moveItem(items, fromIndex, toIndex) {
  if (toIndex < 0 || toIndex >= items.length) return items;
  const nextItems = [...items];
  const [movedItem] = nextItems.splice(fromIndex, 1);
  nextItems.splice(toIndex, 0, movedItem);
  return nextItems;
}

function splitDeliverableText(item) {
  return String(item ?? '')
    .split(', ')
    .map((value) => value.trim())
    .filter(Boolean);
}

function FlowCard({ step, index, total, accent, status, isEditing, onChange }) {
  const Icon = step.icon ?? pickStepIcon(step);
  return (
    <div className="relative z-10 h-full w-full">
      <div
        className={`relative h-full overflow-hidden rounded-2xl border px-4 py-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md ${
          status === 'active'
            ? 'border-amber-200 bg-gradient-to-br from-amber-50 to-white shadow-amber-100'
            : status === 'disabled'
              ? 'border-slate-100 bg-white/45 opacity-45 shadow-none'
              : status === 'completed'
                ? 'border-emerald-100 bg-gradient-to-br from-emerald-50 to-white'
                : 'border-white/80 bg-white/80'
        }`}
      >
        <div
          className={`absolute inset-x-0 top-0 h-1 ${
            status === 'active'
              ? 'bg-amber-400'
              : status === 'completed'
                ? 'bg-emerald-400'
                : 'bg-slate-200'
          }`}
        />
        <div className="mb-3 flex items-center justify-between">
          <div className={`flex h-9 w-9 items-center justify-center rounded-2xl ${accent.iconBg}`}>
            <Icon className={`h-4 w-4 ${accent.iconText}`} />
          </div>
          <span
            className={`text-[11px] font-bold ${
              status === 'active' ? 'text-amber-700' : status === 'completed' ? 'text-emerald-700' : 'text-slate-400'
            }`}
          >
            {status === 'active' ? '재진입' : status === 'completed' ? '통과' : `${String(index + 1).padStart(2, '0')} / ${String(total).padStart(2, '0')}`}
          </span>
        </div>
        {isEditing ? (
          <div className="space-y-2">
            <input
              value={step.title}
              onChange={(event) => onChange(index, 'title', event.target.value)}
              className="w-full rounded-md border border-slate-200 bg-white px-2 py-1.5 text-sm font-extrabold text-slate-950 outline-none focus:border-teal-400"
            />
            <textarea
              value={step.feature}
              onChange={(event) => onChange(index, 'feature', event.target.value)}
              rows={3}
              className="w-full resize-none rounded-md border border-slate-200 bg-white px-2 py-1.5 text-[13px] font-semibold leading-5 text-slate-700 outline-none focus:border-teal-400"
            />
            <textarea
              value={step.note}
              onChange={(event) => onChange(index, 'note', event.target.value)}
              rows={3}
              className="w-full resize-none rounded-md border border-slate-200 bg-white px-2 py-1.5 text-[12px] leading-5 text-slate-500 outline-none focus:border-teal-400"
            />
          </div>
        ) : (
          <>
            <h3 className="text-base font-extrabold text-slate-950">{step.title}</h3>
            <p className="mt-2 min-h-[72px] whitespace-pre-line text-[13px] font-semibold leading-5 text-slate-700">
              {step.feature}
            </p>
            <p className="mt-4 whitespace-pre-line border-t border-dashed border-slate-200 pt-3 text-[12px] leading-5 text-slate-500">
              {step.note}
            </p>
          </>
        )}
      </div>
    </div>
  );
}

function FlowRow({
  label,
  description,
  steps,
  accent,
  onLabelClick,
  activeStep,
  isEditing,
  onStepChange,
  format,
  onFormatChange,
  deliverables = [],
  onDeliverableChange,
  onLabelChange,
  onDescriptionChange,
  programType,
  onProgramTypeChange,
  onAddStep,
  onRemoveStep,
  onMoveStep,
  onProgramDelete
}) {
  const activeIndex = steps.findIndex((step) => step.id === activeStep);

  const getStatus = (index) => {
    if (activeStep === disabledFlowStep) return 'disabled';
    if (!activeStep || activeStep === 'completed') return activeStep === 'completed' ? 'completed' : 'idle';
    if (index === activeIndex) return 'active';
    if (index < activeIndex) return 'completed';
    return 'disabled';
  };
  const isRowClickable = Boolean(onLabelClick) && !isEditing;
  const programTypeKey = getProgramType(programType);
  const programTypeMeta = getProgramTypeMeta(programTypeKey);
  const maxVisibleSteps = 4;
  const [stepWindowStart, setStepWindowStart] = useState(0);
  const maxWindowStart = Math.max(0, steps.length - maxVisibleSteps);
  const visibleStepItems = steps
    .map((step, index) => ({ step, index }))
    .slice(stepWindowStart, stepWindowStart + maxVisibleSteps);
  const placeholderCount = Math.max(0, maxVisibleSteps - visibleStepItems.length);
  const canMovePrev = stepWindowStart > 0;
  const canMoveNext = stepWindowStart < maxWindowStart;

  useEffect(() => {
    setStepWindowStart((current) => Math.min(current, maxWindowStart));
  }, [maxWindowStart]);

  const moveStepWindow = (direction) => {
    setStepWindowStart((current) => Math.min(maxWindowStart, Math.max(0, current + direction)));
  };

  const renderStepCard = (step, index, className) => (
    <div className={`relative z-10 flex min-h-[258px] flex-col gap-2 ${className}`}>
      <FlowCard
        step={step}
        index={index}
        total={steps.length}
        accent={accent}
        status={getStatus(index)}
        isEditing={isEditing}
        onChange={onStepChange}
      />
      {isEditing && (onMoveStep || onRemoveStep) && (
        <div className="flex flex-wrap justify-center gap-1.5">
          {onMoveStep && (
            <>
              <button
                type="button"
                disabled={index === 0}
                onClick={() => onMoveStep(index, index - 1)}
                className="rounded-full bg-white px-2.5 py-1 text-[10px] font-extrabold text-slate-600 ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-35"
              >
                위
              </button>
              <button
                type="button"
                disabled={index === steps.length - 1}
                onClick={() => onMoveStep(index, index + 1)}
                className="rounded-full bg-white px-2.5 py-1 text-[10px] font-extrabold text-slate-600 ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-35"
              >
                아래
              </button>
            </>
          )}
          {onRemoveStep && steps.length > 1 && (
            <button
              type="button"
              onClick={() => onRemoveStep(index)}
              className="rounded-full bg-rose-50 px-2.5 py-1 text-[10px] font-extrabold text-rose-600 hover:bg-rose-100"
            >
              삭제
            </button>
          )}
        </div>
      )}
    </div>
  );

  return (
    <section
      onClick={isRowClickable ? onLabelClick : undefined}
      className={`rounded-[28px] border p-5 shadow-[0_18px_50px_rgba(15,23,42,0.08)] backdrop-blur ${programTypeMeta.rowClass} ${
        isRowClickable ? 'cursor-pointer transition hover:-translate-y-0.5 hover:shadow-[0_22px_60px_rgba(15,23,42,0.12)]' : ''
      }`}
    >
      <div className="mb-5 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
        <div>
          <div className="flex flex-wrap items-center gap-2">
            {isEditing && onLabelChange ? (
              <input
                value={label}
                onChange={(event) => onLabelChange(event.target.value)}
                className={`w-44 rounded-xl border border-slate-200 bg-white/85 px-3 py-2 text-xl font-extrabold tracking-tight outline-none focus:border-teal-400 ${accent.labelText}`}
              />
            ) : (
              <button
                type="button"
                onClick={(event) => {
                  event.stopPropagation();
                  onLabelClick?.();
                }}
                className={`rounded-xl px-1 py-0.5 text-left text-xl font-extrabold tracking-tight ${accent.labelText} ${
                  onLabelClick ? 'cursor-pointer hover:bg-white/70' : ''
                }`}
              >
                {label}
              </button>
            )}
            {isEditing && onProgramDelete && (
              <button
                type="button"
                onClick={onProgramDelete}
                className="flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-extrabold text-rose-700 hover:bg-rose-100"
              >
                <Trash2 className="h-3.5 w-3.5" />
                프로그램 삭제
              </button>
            )}
            {isEditing && onProgramTypeChange ? (
              <div className="flex rounded-full bg-white/80 p-1 text-[11px] font-black shadow-sm ring-1 ring-slate-200">
                {Object.entries(programTypeOptions).map(([typeKey, typeMeta]) => (
                  <button
                    key={typeKey}
                    type="button"
                    onClick={(event) => {
                      event.stopPropagation();
                      onProgramTypeChange(typeKey);
                    }}
                    className={`rounded-full px-2.5 py-1 transition ${
                      programTypeKey === typeKey
                        ? typeMeta.badgeClass
                        : 'text-slate-400 hover:bg-slate-50'
                    }`}
                  >
                    {typeMeta.shortLabel}
                  </button>
                ))}
              </div>
            ) : (
              <span className={`rounded-full px-2.5 py-1 text-[11px] font-black ring-1 ${programTypeMeta.badgeClass}`}>
                {programTypeMeta.label}
              </span>
            )}
          </div>
          {isEditing && onDescriptionChange ? (
            <textarea
              value={description}
              onChange={(event) => onDescriptionChange(event.target.value)}
              rows={2}
              className="mt-1 w-full min-w-[360px] resize-none rounded-xl border border-slate-200 bg-white/85 px-3 py-1.5 text-sm font-bold tracking-tight text-slate-800 outline-none focus:border-teal-400"
            />
          ) : (
            <h2 className="mt-1 whitespace-pre-line text-sm font-bold tracking-tight text-slate-800">{description}</h2>
          )}
        </div>
        <div className="flex flex-col items-start gap-2 md:items-end">
          {onFormatChange && (
            <div className="flex items-center gap-2">
              <span className="text-[11px] font-extrabold text-slate-400">포맷</span>
              {isEditing ? (
                <input
                  value={format}
                  onChange={(event) => onFormatChange(event.target.value)}
                  placeholder="예: glb"
                  className="h-8 w-28 rounded-full border border-slate-200 bg-white/85 px-3 text-[12px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
                />
              ) : (
                <span className="min-w-16 rounded-full bg-slate-100 px-3 py-1.5 text-center text-[12px] font-extrabold text-slate-700">
                  {format || '-'}
                </span>
              )}
            </div>
          )}
          {(deliverables.length > 0 || isEditing) && (
            <div className="flex max-w-[720px] flex-wrap items-center justify-start gap-1.5 md:justify-end">
              <span className={`mr-1 text-[11px] font-extrabold ${accent.labelText}`}>성과물</span>
              {(deliverables.length > 0 ? deliverables : ['']).map((item, index) => (
                isEditing ? (
                  <input
                    key={index}
                    value={item}
                    onChange={(event) => onDeliverableChange?.(index, event.target.value)}
                    placeholder="성과물"
                    className="h-7 w-28 rounded-full border border-slate-200 bg-white/85 px-3 text-[11px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
                  />
                ) : splitDeliverableText(item).map((deliverable, deliverableIndex) => (
                    <span
                      key={`${deliverable}-${index}-${deliverableIndex}`}
                      className="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-extrabold text-slate-700"
                    >
                      {deliverable}
                    </span>
                  ))
              ))}
            </div>
          )}
          {isEditing && onAddStep && (
            <button
              type="button"
              onClick={onAddStep}
              className="flex items-center gap-1 rounded-full bg-blue-50 px-3 py-2 text-[12px] font-extrabold text-blue-700 hover:bg-blue-100"
            >
              <Plus className="h-3.5 w-3.5" />
              단계 추가
            </button>
          )}
        </div>
      </div>
      <div className="relative overflow-hidden rounded-[24px] bg-white/25 px-9 pb-5 pt-1 ring-1 ring-white/70">
        {steps.length > maxVisibleSteps && (
          <>
            <button
              type="button"
              disabled={!canMovePrev}
              onClick={(event) => {
                event.stopPropagation();
                moveStepWindow(-1);
              }}
              className="absolute left-2 top-1/2 z-20 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
              aria-label="이전 스텝 보기"
            >
              ‹
            </button>
            <button
              type="button"
              disabled={!canMoveNext}
              onClick={(event) => {
                event.stopPropagation();
                moveStepWindow(1);
              }}
              className="absolute right-2 top-1/2 z-20 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
              aria-label="다음 스텝 보기"
            >
              ›
            </button>
          </>
        )}
        <div className="relative grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4 xl:gap-5">
          {visibleStepItems.map(({ step, index }, itemIndex) => (
            <div key={step.id} className="relative">
              {renderStepCard(step, index, 'min-w-0')}
              {itemIndex < visibleStepItems.length - 1 && (
                <ArrowDown className={`mx-auto mt-2 h-5 w-5 xl:hidden ${accent.arrowText}`} />
              )}
            </div>
          ))}
          {Array.from({ length: placeholderCount }).map((_, index) => (
            <div
              key={`placeholder-${index}`}
              className="hidden min-h-[258px] rounded-2xl border border-dashed border-transparent xl:block"
            />
          ))}
          {visibleStepItems.slice(0, -1).map((item, index) => (
            <ArrowRight
              key={`arrow-${item.step.id}`}
              className={`pointer-events-none absolute top-1/2 z-20 hidden h-5 w-5 -translate-x-1/2 -translate-y-1/2 xl:block ${accent.arrowText}`}
              style={{ left: `${25 * (index + 1)}%` }}
            />
          ))}
        </div>
        {steps.length > 1 && (
          <div className="mt-3 flex items-center justify-center gap-1.5">
            {steps.map((step, index) => {
              const isVisible = index >= stepWindowStart && index < stepWindowStart + maxVisibleSteps;
              return (
                <button
                  key={`${step.id}-dot`}
                  type="button"
                  onClick={(event) => {
                    event.stopPropagation();
                    setStepWindowStart(Math.min(maxWindowStart, Math.max(0, Math.min(index, steps.length - maxVisibleSteps))));
                  }}
                  className={`h-2.5 w-2.5 rounded-full transition ${
                    isVisible
                      ? `${accent.iconText} bg-current shadow-sm`
                      : 'bg-slate-200 hover:bg-slate-300'
                  }`}
                  aria-label={`${index + 1}번째 스텝 보기`}
                />
              );
            })}
          </div>
        )}
      </div>
    </section>
  );
}

function DetailPopup({
  program,
  allPrograms,
  gates,
  setGates,
  selectedStep,
  setSelectedStep,
  reviewItems,
  onReviewItemChange,
  onAddReviewItem,
  onRemoveReviewItem,
  onMoveReviewItem,
  onClose,
  standalone = false
}) {
  const [position, setPosition] = useState({ x: 24, y: 72 });
  const [dragStart, setDragStart] = useState(null);
  const [isDetailEditing, setIsDetailEditing] = useState(false);
  const [changeSearch, setChangeSearch] = useState('');

  const normalizedGates = reviewItems.reduce((nextGates, item) => ({
    ...nextGates,
    [item.key]: gates[item.key] ?? 'no'
  }), {});
  const activeStep = getActiveStep(normalizedGates, reviewItems);
  const getTargetProgram = (gate) => allPrograms.find((item) => item.id === resolveGateTargetProgramId(gate, program.id, allPrograms)) ?? program;
  const getTargetLabel = (gate) => {
    const targetProgram = getTargetProgram(gate);
    return `${targetProgram.name} · ${getStepLabel(targetProgram, gate.stepId)}`;
  };
  const normalizedSearch = changeSearch.trim().toLowerCase();
  const isSearchMatched = (gate) => Boolean(normalizedSearch) && [
    gate.question,
    gate.yes,
    gate.no,
    getTargetLabel(gate)
  ].join(' ').toLowerCase().includes(normalizedSearch);
  const searchMatchCount = normalizedSearch
    ? reviewItems.filter((gate) => isSearchMatched(gate)).length
    : 0;

  const setGate = (key, value) => {
    const nextGates = { ...normalizedGates, [key]: value };
    setGates(nextGates);
    const gate = reviewItems.find((item) => item.key === key);
    if (value === 'yes' && gate) setSelectedStep(gate.stepId);
    if (value === 'no' && key === reviewItems[reviewItems.length - 1]?.key) setSelectedStep('completed');
  };

  useEffect(() => {
    if (!dragStart) return undefined;

    const movePopup = (event) => {
      const nextX = dragStart.originX + event.clientX - dragStart.startX;
      const nextY = dragStart.originY + event.clientY - dragStart.startY;
      const maxX = Math.max(12, window.innerWidth - 360);
      const maxY = Math.max(12, window.innerHeight - 96);

      setPosition({
        x: Math.min(Math.max(12, nextX), maxX),
        y: Math.min(Math.max(12, nextY), maxY)
      });
    };

    const stopDrag = () => setDragStart(null);

    window.addEventListener('pointermove', movePopup);
    window.addEventListener('pointerup', stopDrag);
    window.addEventListener('pointercancel', stopDrag);
    document.body.style.userSelect = 'none';

    return () => {
      window.removeEventListener('pointermove', movePopup);
      window.removeEventListener('pointerup', stopDrag);
      window.removeEventListener('pointercancel', stopDrag);
      document.body.style.userSelect = '';
    };
  }, [dragStart]);

  const startDrag = (event) => {
    if (event.target.closest('button')) return;
    setDragStart({
      startX: event.clientX,
      startY: event.clientY,
      originX: position.x,
      originY: position.y
    });
  };

  const header = (
    <div
      onPointerDown={standalone ? undefined : startDrag}
      className={`flex items-center justify-between border-b border-white/10 bg-gradient-to-r from-slate-950 via-slate-900 to-teal-950 px-4 py-3 text-white ${
        standalone ? '' : 'cursor-move'
      }`}
    >
      <div>
        <h2 className="text-sm font-extrabold">{program.name} 상세 플로우</h2>
        <p className="mt-0.5 text-[11px] font-medium text-slate-300">
          마지막 단계 완료 후 변경사항 검토 피드백 엔진
        </p>
      </div>
      <div className="flex items-center gap-2">
        <button
          type="button"
          onPointerDown={(event) => event.stopPropagation()}
          onClick={(event) => {
            event.stopPropagation();
            setIsDetailEditing((current) => !current);
          }}
          className="rounded-full bg-white/10 px-3 py-1.5 text-[11px] font-extrabold text-white hover:bg-white/20"
        >
          {isDetailEditing ? '수정 완료' : '내용 수정'}
        </button>
      <button
        type="button"
        onPointerDown={(event) => event.stopPropagation()}
        onClick={(event) => {
          event.stopPropagation();
          onClose();
        }}
        className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
        aria-label="상세 플로우 닫기"
      >
        <X className="h-4 w-4" />
      </button>
      </div>
    </div>
  );

  const content = (
    <>
      {header}
      <div className="overflow-auto bg-[radial-gradient(circle_at_top_left,#eefdfa_0,#fcfbf8_42%,#f8fafc_100%)] p-4">
        <section>
            <div className="rounded-2xl border border-white/80 bg-white/75 px-4 py-4 shadow-sm backdrop-blur">
              <div className="mb-5 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
                <h3 className="text-base font-extrabold text-slate-950">변경사항 검토 및 재진입 흐름</h3>
                <div className="flex flex-wrap items-center gap-2">
                  <input
                    value={changeSearch}
                    onChange={(event) => setChangeSearch(event.target.value)}
                    placeholder="변경사항 검색 예: 노선변경"
                    className="h-8 w-56 rounded-full border border-slate-200 bg-white/90 px-3 text-[12px] font-bold text-slate-700 outline-none focus:border-teal-400"
                  />
                  {changeSearch && (
                    <button
                      type="button"
                      onClick={() => setChangeSearch('')}
                      className="rounded-full bg-slate-100 px-3 py-1.5 text-[11px] font-extrabold text-slate-500 hover:bg-slate-200"
                    >
                      검색 해제
                    </button>
                  )}
                  {isDetailEditing && (
                    <button
                      type="button"
                      onClick={onAddReviewItem}
                      className="rounded-full bg-slate-900 px-3 py-1.5 text-[11px] font-extrabold text-white hover:bg-slate-700"
                    >
                      스텝 추가
                    </button>
                  )}
                </div>
              </div>
              <div className="space-y-4">
                {normalizedSearch && (
                  <p className="text-center text-[12px] font-bold text-slate-500">
                    `{changeSearch}` 매칭 스텝 {searchMatchCount}개
                  </p>
                )}
                {reviewItems.map((gate, index) => {
                  const blocked = reviewItems.slice(0, index).some((item) => normalizedGates[item.key] === 'yes');
                  const active = normalizedGates[gate.key] === 'yes';
                  const passed = normalizedGates[gate.key] === 'no' && !blocked;
                  const matched = isSearchMatched(gate);
                  return (
                  <div key={gate.key} className={`grid grid-cols-[0.85fr_minmax(220px,1.25fr)_0.85fr] items-center gap-2.5 ${blocked ? 'opacity-35' : ''} ${matched ? 'rounded-3xl bg-yellow-50/60 p-2 ring-2 ring-yellow-300' : ''}`}>
                    <div className={`rounded-2xl px-3 py-3.5 ${matched ? 'bg-yellow-100/90 ring-2 ring-yellow-300' : active ? 'bg-amber-100/90 ring-2 ring-amber-300' : 'bg-amber-50/80 ring-1 ring-amber-100'}`}>
                      {isDetailEditing ? (
                        <div className="space-y-1.5">
                          <input
                            value={gate.yes}
                            onChange={(event) => onReviewItemChange(index, 'yes', event.target.value)}
                            className="w-full rounded border border-amber-200 bg-white px-2 py-1 text-[11px] font-extrabold text-amber-700 outline-none"
                          />
                          <div className="flex flex-wrap gap-1.5">
                            <button
                              type="button"
                              disabled={index === 0}
                              onClick={() => onMoveReviewItem(index, index - 1)}
                              className="rounded-full bg-white px-2 py-1 text-[10px] font-extrabold text-slate-600 ring-1 ring-amber-100 hover:bg-amber-50 disabled:opacity-35"
                            >
                              위
                            </button>
                            <button
                              type="button"
                              disabled={index === reviewItems.length - 1}
                              onClick={() => onMoveReviewItem(index, index + 1)}
                              className="rounded-full bg-white px-2 py-1 text-[10px] font-extrabold text-slate-600 ring-1 ring-amber-100 hover:bg-amber-50 disabled:opacity-35"
                            >
                              아래
                            </button>
                            {reviewItems.length > 1 && (
                              <button
                                type="button"
                                onClick={() => onRemoveReviewItem(index)}
                                className="rounded-full bg-rose-50 px-2 py-1 text-[10px] font-extrabold text-rose-600 hover:bg-rose-100"
                              >
                                삭제
                              </button>
                            )}
                          </div>
                        </div>
                      ) : (
                        <div>
                          <p className="text-[12px] font-extrabold text-amber-700">{gate.yes}</p>
                          <p className="mt-1 text-[10px] font-bold text-amber-500">대상: {getTargetLabel(gate)}</p>
                        </div>
                      )}
                    </div>
                    <div className={`rounded-2xl bg-white/95 px-4 py-4 text-center shadow-sm ring-1 ${matched ? 'ring-yellow-300' : active ? 'ring-amber-300' : passed ? 'ring-emerald-300' : 'ring-slate-200'}`}>
                      {isDetailEditing ? (
                        <div className="space-y-1.5">
                          <textarea
                            value={gate.question}
                            onChange={(event) => onReviewItemChange(index, 'question', event.target.value)}
                            rows={3}
                            className="w-full resize-none rounded border border-slate-200 bg-slate-50 px-2 py-1 text-center text-[11px] font-extrabold text-slate-800 outline-none"
                          />
                          <select
                            value={`${resolveGateTargetProgramId(gate, program.id, allPrograms)}::${gate.stepId}`}
                            onChange={(event) => {
                              const [targetProgramId, stepId] = event.target.value.split('::');
                              onReviewItemChange(index, 'targetProgramId', targetProgramId);
                              onReviewItemChange(index, 'stepId', stepId);
                            }}
                            className="w-full rounded border border-slate-200 bg-white px-1 py-1 text-[10px] font-bold text-slate-700 outline-none"
                          >
                            {allPrograms.map((targetProgram) => (
                              <optgroup key={targetProgram.id} label={targetProgram.name}>
                                {targetProgram.steps.map((step) => (
                                  <option key={`${targetProgram.id}-${step.id}`} value={`${targetProgram.id}::${step.id}`}>
                                    {step.title}
                                  </option>
                                ))}
                              </optgroup>
                            ))}
                          </select>
                        </div>
                      ) : (
                        <p className="whitespace-pre-line text-[13px] font-extrabold leading-5 text-slate-800">{gate.question}</p>
                      )}
                      <div className="mt-2 flex justify-center gap-1.5">
                        <button
                          type="button"
                          disabled={blocked}
                          onClick={() => setGate(gate.key, 'yes')}
                          className={`rounded-full px-2 py-1 text-[10px] font-extrabold disabled:cursor-not-allowed ${
                            active ? 'bg-amber-500 text-white' : 'bg-slate-100 text-slate-600 hover:bg-amber-100'
                          }`}
                        >
                          예
                        </button>
                        <button
                          type="button"
                          disabled={blocked}
                          onClick={() => setGate(gate.key, 'no')}
                          className={`rounded-full px-2 py-1 text-[10px] font-extrabold disabled:cursor-not-allowed ${
                            passed ? 'bg-emerald-500 text-white' : 'bg-slate-100 text-slate-600 hover:bg-emerald-100'
                          }`}
                        >
                          아니오
                        </button>
                      </div>
                    </div>
                    <div className={`rounded-2xl px-3 py-3.5 ${matched ? 'bg-yellow-100/90 ring-2 ring-yellow-300' : passed ? 'bg-emerald-100/90 ring-2 ring-emerald-300' : 'bg-emerald-50/80 ring-1 ring-emerald-100'}`}>
                      {isDetailEditing ? (
                        <input
                          value={gate.no}
                          onChange={(event) => onReviewItemChange(index, 'no', event.target.value)}
                          className="w-full rounded border border-emerald-200 bg-white px-2 py-1 text-[11px] font-extrabold text-emerald-700 outline-none"
                        />
                      ) : (
                        <p className="text-[12px] font-extrabold text-emerald-700">{gate.no}</p>
                      )}
                    </div>
                    {index < reviewItems.length - 1 && (
                      <div className="hidden md:col-start-2 md:flex md:justify-center">
                        <ArrowDown className="h-4 w-4 text-slate-400" />
                      </div>
                    )}
                  </div>
                  );
                })}
              </div>
            </div>

          </section>
        </div>
    </>
  );

  if (standalone) {
    return <main className="min-h-screen bg-[radial-gradient(circle_at_top_left,#eefdfa_0,#fcfbf8_42%,#f8fafc_100%)]">{content}</main>;
  }

  return (
    <div className="fixed inset-0 z-50 bg-slate-950/20 backdrop-blur-sm">
      <div
        className="fixed flex max-h-[calc(100vh-32px)] w-[min(1320px,calc(100vw-24px))] flex-col overflow-hidden rounded-3xl border border-white/70 bg-white shadow-2xl"
        style={{ left: position.x, top: position.y }}
      >
        {content}
      </div>
    </div>
  );
}

function RelationPopup({ programs, onToggleRelation, onClose }) {
  return (
    <div className="fixed inset-0 z-[60] flex items-center justify-center bg-slate-950/30 px-4 backdrop-blur-sm">
      <section className="relative flex max-h-[calc(100vh-48px)] w-full max-w-4xl flex-col overflow-hidden rounded-3xl border border-white/70 bg-white shadow-2xl">
        <div className="flex items-center justify-between bg-slate-950 px-5 py-4 text-white">
          <div>
            <p className="text-[11px] font-extrabold uppercase tracking-wide text-amber-300">
              프로그램 연관관계
            </p>
            <h2 className="mt-1 text-lg font-extrabold">프로그램 선행·후행 설정</h2>
          </div>
          <button
            type="button"
            onClick={onClose}
            className="flex h-9 w-9 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
            aria-label="관계 설정 닫기"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
        <div className="overflow-auto bg-slate-50 p-5">
          <div className="mb-4">
            <h3 className="text-base font-extrabold text-slate-950">프로그램 연결 수정</h3>
            <p className="mt-1 text-[12px] font-bold text-slate-500">
              각 프로그램의 선행/후행을 선택하면 메인 연결도에 바로 반영됩니다.
            </p>
          </div>
          <div className="space-y-3">
            {programs.map((program) => {
              const candidates = programs.filter((item) => item.id !== program.id);
              return (
                <div key={program.id} className="grid gap-3 rounded-2xl bg-white p-4 ring-1 ring-slate-100 lg:grid-cols-[180px_1fr_1fr]">
                  <div>
                    <p className="text-[11px] font-extrabold uppercase tracking-wide text-slate-400">프로그램</p>
                    <h4 className="mt-1 text-base font-extrabold text-slate-950">{program.name}</h4>
                  </div>
                  <div>
                    <p className="mb-2 text-[12px] font-extrabold text-amber-700">선행</p>
                    <div className="flex flex-wrap gap-1.5">
                      {candidates.map((candidate) => (
                        <label key={candidate.id} className="flex cursor-pointer items-center gap-1.5 rounded-full bg-amber-50 px-2.5 py-1.5 text-[11px] font-extrabold text-amber-800 ring-1 ring-amber-100">
                          <input
                            type="checkbox"
                            checked={(program.predecessors ?? []).includes(candidate.id)}
                            onChange={(event) => onToggleRelation(program.id, 'predecessors', candidate.id, event.target.checked)}
                            className="h-3.5 w-3.5 accent-amber-500"
                          />
                          {candidate.name}
                        </label>
                      ))}
                    </div>
                  </div>
                  <div>
                    <p className="mb-2 text-[12px] font-extrabold text-blue-700">후행</p>
                    <div className="flex flex-wrap gap-1.5">
                      {candidates.map((candidate) => (
                        <label key={candidate.id} className="flex cursor-pointer items-center gap-1.5 rounded-full bg-blue-50 px-2.5 py-1.5 text-[11px] font-extrabold text-blue-800 ring-1 ring-blue-100">
                          <input
                            type="checkbox"
                            checked={(program.successors ?? []).includes(candidate.id)}
                            onChange={(event) => onToggleRelation(program.id, 'successors', candidate.id, event.target.checked)}
                            className="h-3.5 w-3.5 accent-blue-500"
                          />
                          {candidate.name}
                        </label>
                      ))}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </section>
    </div>
  );
}

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 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 leftSteps = leftProgram?.steps ?? [];
  const rightSteps = rightProgram?.steps ?? [];
  const stepMatches = comparison.stepMatches ?? [];
  const maxStepCount = Math.max(leftSteps.length, rightSteps.length);

  const updateComparison = (field, value) => {
    if (!leftProgramId || !rightProgramId || leftProgramId === rightProgramId) return;
    onComparisonChange(leftProgramId, rightProgramId, field, value);
  };
  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]
    );
  };
  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 renderProgramSummary = (program) => {
    if (!program) return null;
    const typeMeta = getProgramTypeMeta(program.programType);
    return (
      <div className={`rounded-2xl border p-4 ${typeMeta.rowClass}`}>
        <div className="flex items-start justify-between gap-3">
          <div>
            <p className="text-[11px] font-extrabold uppercase tracking-wide text-slate-400">선택 프로그램</p>
            <h4 className="mt-1 text-lg font-black text-slate-950">{program.name}</h4>
          </div>
          <span className={`rounded-full px-2.5 py-1 text-[11px] font-black ring-1 ${typeMeta.badgeClass}`}>
            {typeMeta.shortLabel}
          </span>
        </div>
        <p className="mt-2 text-sm font-bold leading-5 text-slate-700">{program.description}</p>
        <div className="mt-3 flex flex-wrap gap-1.5">
          {(program.deliverables ?? []).map((deliverable) => (
            <span key={deliverable} className="rounded-full bg-white/80 px-2.5 py-1 text-[11px] font-black text-slate-600 ring-1 ring-white">
              {deliverable}
            </span>
          ))}
        </div>
        <p className="mt-3 text-[12px] font-extrabold text-slate-500">
          포맷: <span className="text-slate-800">{program.format || '-'}</span>
        </p>
      </div>
    );
  };

  return (
    <div className="fixed inset-0 z-[60] flex items-center justify-center bg-slate-950/30 px-4 backdrop-blur-sm">
      <section className="relative flex max-h-[calc(100vh-48px)] w-full max-w-5xl flex-col overflow-hidden rounded-3xl border border-white/70 bg-white shadow-2xl">
        <div className="flex items-center justify-between bg-slate-950 px-5 py-4 text-white">
          <div>
            <p className="text-[11px] font-extrabold uppercase tracking-wide text-blue-300">
              1:1 프로그램 비교
            </p>
            <h2 className="mt-1 text-lg font-extrabold">사내 한계와 상용 활용 기능 정리</h2>
          </div>
          <button
            type="button"
            onClick={onClose}
            className="flex h-9 w-9 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
            aria-label="비교 닫기"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
        <div className="overflow-auto bg-slate-50 p-5">
          <div className="grid gap-3 rounded-3xl bg-white p-4 ring-1 ring-slate-100 lg:grid-cols-2">
            <label className="block">
              <span className="text-[12px] font-black text-slate-500">비교 프로그램 A</span>
              <select
                value={leftProgramId}
                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"
              >
                {programs.map((program) => (
                  <option key={program.id} value={program.id}>
                    {program.name}
                  </option>
                ))}
              </select>
            </label>
            <label className="block">
              <span className="text-[12px] font-black text-slate-500">비교 프로그램 B</span>
              <select
                value={rightProgramId}
                onChange={(event) => setRightProgramId(event.target.value)}
                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) => (
                  <option key={program.id} value={program.id} disabled={program.id === leftProgramId}>
                    {program.name}
                  </option>
                ))}
              </select>
            </label>
          </div>

          {savedComparisons.length > 0 && (
            <div className="mt-3 rounded-3xl bg-white px-4 py-3 ring-1 ring-slate-100">
              <div className="flex flex-wrap items-center gap-2">
                <span className="mr-1 text-[12px] font-black text-slate-500">저장된 비교</span>
                {savedComparisons.map((savedComparison) => {
                  const active =
                    savedComparison.leftProgramId === leftProgramId &&
                    savedComparison.rightProgramId === rightProgramId;
                  const memoCount = getComparisonMemoCount(savedComparison);
                  return (
                    <button
                      key={`${savedComparison.leftProgramId}-${savedComparison.rightProgramId}`}
                      type="button"
                      onClick={() => {
                        setLeftProgramId(savedComparison.leftProgramId);
                        setRightProgramId(savedComparison.rightProgramId);
                      }}
                      className={`rounded-full px-3 py-1.5 text-[12px] font-black ring-1 transition ${
                        active
                          ? 'bg-slate-950 text-white ring-slate-950'
                          : 'bg-slate-50 text-slate-600 ring-slate-200 hover:bg-white'
                      }`}
                    >
                      {getProgramName(savedComparison.leftProgramId)} ↔ {getProgramName(savedComparison.rightProgramId)}
                      {memoCount > 0 ? ` · ${memoCount}개 메모` : ''}
                    </button>
                  );
                })}
              </div>
            </div>
          )}

          <div className="mt-4 grid gap-4 lg:grid-cols-2">
            {renderProgramSummary(leftProgram)}
            {renderProgramSummary(rightProgram)}
          </div>

          <div className="mt-4 rounded-3xl bg-white p-4 ring-1 ring-slate-100">
            <div>
              <div>
                <h3 className="text-base font-black text-slate-950">전체 스텝 1:1 비교</h3>
                <p className="mt-1 text-[12px] font-bold text-slate-500">
                  두 프로그램의 스텝을 전부 펼친 뒤, 가운데에 해당 스텝에서 상용 프로그램을 쓰는 이유를 적습니다.
                  {hasSavedComparison ? ' 저장된 비교 내용을 불러왔습니다.' : ''}
                </p>
              </div>
            </div>

            <div className="mt-4 space-y-3">
              {maxStepCount === 0 ? (
                <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">
                  비교할 스텝이 없습니다.
                </div>
              ) : (
                Array.from({ length: maxStepCount }).map((_, stepIndex) => {
                  const leftStep = leftSteps[stepIndex];
                  const rightStep = rightSteps[stepIndex];
                  const match = getStepMatch(stepIndex);
                  return (
                  <div
                    key={`step-compare-${stepIndex}`}
                    className="grid gap-3 rounded-2xl border border-slate-100 bg-slate-50/80 p-3 lg:grid-cols-[minmax(0,1fr)_minmax(280px,1.35fr)_minmax(0,1fr)]"
                  >
                    <div className="rounded-2xl bg-amber-50/70 p-3 ring-1 ring-amber-100">
                      <span className="text-[11px] font-black text-amber-700">{leftProgram?.name ?? 'A'} 스텝</span>
                      <h4 className="mt-1 text-sm font-black text-slate-950">
                        {leftStep ? getStepLabel(leftStep, stepIndex) : `${stepIndex + 1}. -`}
                      </h4>
                      <p className="mt-2 whitespace-pre-line rounded-xl bg-white/75 px-3 py-2 text-[12px] font-bold leading-5 text-slate-700 ring-1 ring-amber-50">
                        {leftStep?.feature || '해당 스텝 없음'}
                      </p>
                      {leftStep?.note && (
                        <p className="mt-2 whitespace-pre-line text-[11px] font-bold leading-5 text-slate-500">
                          {leftStep.note}
                        </p>
                      )}
                    </div>
                    <label className="block">
                      <span className="text-[11px] font-black text-slate-700">안 되는 기능 / 상용 사용 이유</span>
                      <textarea
                        value={match.reason ?? ''}
                        onChange={(event) => updateStepReason(stepIndex, event.target.value)}
                        rows={5}
                        placeholder="예: 이 스텝에서 3D 형상 수정과 obj/ifc 변환이 안 돼서 오른쪽 프로그램의 기능을 사용"
                        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>
                    <div className="rounded-2xl bg-blue-50/70 p-3 ring-1 ring-blue-100">
                      <span className="text-[11px] font-black text-blue-700">{rightProgram?.name ?? 'B'} 스텝</span>
                      <h4 className="mt-1 text-sm font-black text-slate-950">
                        {rightStep ? getStepLabel(rightStep, stepIndex) : `${stepIndex + 1}. -`}
                      </h4>
                      <p className="mt-2 whitespace-pre-line rounded-xl bg-white/75 px-3 py-2 text-[12px] font-bold leading-5 text-slate-700 ring-1 ring-blue-50">
                        {rightStep?.feature || '해당 스텝 없음'}
                      </p>
                      {rightStep?.note && (
                        <p className="mt-2 whitespace-pre-line text-[11px] font-bold leading-5 text-slate-500">
                          {rightStep.note}
                        </p>
                      )}
                    </div>
                  </div>
                  );
                })
              )}
            </div>

            <label className="mt-4 block">
              <span className="text-[12px] font-black text-slate-500">비교 결론</span>
              <textarea
                value={comparison.note ?? ''}
                onChange={(event) => updateComparison('note', event.target.value)}
                rows={2}
                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"
              />
            </label>
          </div>
        </div>
      </section>
    </div>
  );
}

function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup, sidebarWidth, onSidebarWidthChange }) {
  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) =>
    (program.successors ?? [])
      .map((successorId) => ({
        from: program,
        to: programs.find((item) => item.id === successorId)
      }))
      .filter((relation) => relation.to)
  );
  const connectedProgramIds = new Set(relations.flatMap((relation) => [relation.from.id, relation.to.id]));
  const linkedChildIds = new Set(programs.flatMap((program) => program.successors ?? []));
  const rootPrograms = programs.filter((program) => !linkedChildIds.has(program.id));
  const visibleRoots = rootPrograms.length ? rootPrograms : programs.slice(0, 1);
  const levelMap = programs.reduce((levels, program) => ({ ...levels, [program.id]: 0 }), {});

  for (let pass = 0; pass < programs.length; pass += 1) {
    relations.forEach((relation) => {
      levelMap[relation.to.id] = Math.max(levelMap[relation.to.id], levelMap[relation.from.id] + 1);
    });
  }

  const graphPrograms = programs.filter((program) => connectedProgramIds.has(program.id));
  const graphLevels = graphPrograms.reduce((levels, program) => {
    const level = levelMap[program.id] ?? 0;
    levels[level] = [...(levels[level] ?? []), program];
    return levels;
  }, []);
  const miniNodeWidth = 128;
  const miniNodeHeight = 38;
  const miniColumnGap = 52;
  const miniRowGap = 96;
  const miniPadding = 34;
  const miniSideLaneWidth = 96;
  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(
    660,
    miniPadding * 2 + graphLevels.length * miniNodeHeight + Math.max(0, graphLevels.length - 1) * miniRowGap
  );
  const miniNodePositions = Object.fromEntries(
    graphLevels.flatMap((levelPrograms = [], levelIndex) => {
      const rowWidth = levelPrograms.length * miniNodeWidth + Math.max(0, levelPrograms.length - 1) * miniColumnGap;
      const startX = (miniGraphWidth - rowWidth) / 2;
      const y = miniPadding + levelIndex * (miniNodeHeight + miniRowGap);
      return levelPrograms.map((program, rowIndex) => [
        program.id,
        {
          x: startX + rowIndex * (miniNodeWidth + miniColumnGap),
          y
        }
      ]);
    })
  );
  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) => {
    setExpandedProgramIds((current) => {
      const next = new Set(current);
      if (next.has(programId)) {
        next.delete(programId);
      } else {
        next.add(programId);
      }
      return next;
    });
  };

  const renderNode = (program, depth = 0, visitedIds = new Set()) => {
    const successors = (program.successors ?? [])
      .map((successorId) => programMap[successorId])
      .filter(Boolean);
    const hasChildren = successors.length > 0;
    const isExpanded = expandedProgramIds.has(program.id);
    const isRepeated = visitedIds.has(program.id);
    const hasMultipleInputs = (program.predecessors ?? []).length > 1;
    const typeMeta = getProgramTypeMeta(program.programType);
    const nextVisitedIds = new Set(visitedIds);
    nextVisitedIds.add(program.id);

    const indent = Math.min(depth, 5) * 13;

    return (
      <div key={`${program.id}-${depth}-${[...visitedIds].join('-')}`} className="relative">
        {depth > 0 && (
          <span
            className="absolute left-[11px] top-0 h-full border-l border-dashed border-slate-200"
            aria-hidden="true"
          />
        )}
        <div
          className="relative flex items-center gap-2 py-1.5"
          style={{ paddingLeft: `${indent}px` }}
        >
          {hasChildren && !isRepeated ? (
            <button
              type="button"
              onClick={() => toggleExpanded(program.id)}
              className="relative z-10 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-white text-[11px] font-black text-slate-500 ring-1 ring-slate-200 hover:bg-slate-50"
              aria-label={`${program.name} 하위 프로그램 ${isExpanded ? '접기' : '펼치기'}`}
            >
              {isExpanded ? '−' : '+'}
            </button>
          ) : (
            <span className="relative z-10 h-5 w-5 shrink-0 rounded-full bg-slate-100 ring-1 ring-slate-200" />
          )}
          <button
            type="button"
            onClick={() => onProgramClick(program.id)}
            className="min-w-0 flex-1 overflow-hidden rounded-xl bg-white/85 px-3 py-2 text-left shadow-sm ring-1 ring-slate-100 transition hover:-translate-y-0.5 hover:bg-white hover:shadow"
          >
            <span className="flex min-w-0 items-center gap-1.5">
              <span className={`h-2 w-2 shrink-0 rounded-full ${typeMeta.dotClass}`} />
              <span className="block truncate text-[13px] font-black text-slate-900">
                {program.name}
              </span>
            </span>
            <span className="mt-0.5 block truncate text-[11px] font-semibold text-slate-500">
              {program.description}
            </span>
          </button>
          <span className={`rounded-full px-2 py-1 text-[10px] font-black ring-1 ${typeMeta.badgeClass}`}>
            {typeMeta.shortLabel}
          </span>
          {hasMultipleInputs && (
            <span className="rounded-full bg-violet-50 px-2 py-1 text-[10px] font-black text-violet-600 ring-1 ring-violet-100">
              다중
            </span>
          )}
        </div>
        {hasChildren && isExpanded && !isRepeated && (
          <div className="ml-2">
            {successors.map((successor) => renderNode(successor, depth + 1, nextVisitedIds))}
          </div>
        )}
      </div>
    );
  };

  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">
      <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>
          <p className="mt-1 text-xs font-semibold leading-5 text-slate-500">
            필요할 때 연결도 영역을 크게 펼칩니다.
          </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>
      </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">
          <div className="flex min-w-0 items-center gap-1.5">
            <span className="flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-1 text-[10px] font-black text-emerald-700 ring-1 ring-emerald-100">
              <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
              사내
            </span>
            <span className="flex items-center gap-1 rounded-full bg-violet-50 px-2 py-1 text-[10px] font-black text-violet-700 ring-1 ring-violet-100">
              <span className="h-1.5 w-1.5 rounded-full bg-violet-500" />
              상용
            </span>
          </div>
          <div className="flex items-center justify-between gap-2">
            <div className="flex shrink-0 items-center gap-1">
              <button
                type="button"
                disabled={!canShrinkSidebar}
                onClick={() => setSidebarSize(sidebarSizes[0])}
                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="연결도 영역 최소"
              >
                <Minimize2 className="h-3.5 w-3.5" />
              </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>
            </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="absolute left-0 top-0 transition-transform duration-300 ease-out"
              style={{
                width: miniGraphWidth,
                height: miniGraphHeight,
                transform: `translateX(-${mapOffset}px)`
              }}
            >
              <svg
                className="absolute inset-0 h-full w-full overflow-visible"
                viewBox={`0 0 ${miniGraphWidth} ${miniGraphHeight}`}
                preserveAspectRatio="none"
              >
                <defs>
                  <marker id="sidebar-relation-arrow" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto" markerUnits="strokeWidth">
                    <path d="M 0 0 L 9 4.5 L 0 9 z" fill="#2563eb" />
                  </marker>
                  <marker id="sidebar-relation-arrow-skip" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto" markerUnits="strokeWidth">
                    <path d="M 0 0 L 9 4.5 L 0 9 z" fill="#7c3aed" />
                  </marker>
                </defs>
                {relations.map((relation) => {
                  const from = miniNodePositions[relation.from.id];
                  const to = miniNodePositions[relation.to.id];
                  if (!from || !to) return null;
                  const fromLevel = levelMap[relation.from.id] ?? 0;
                  const toLevel = levelMap[relation.to.id] ?? 0;
                  const isSkipEdge = toLevel - fromLevel > 1;
                  const fromCenterX = from.x + miniNodeWidth / 2;
                  const toCenterX = to.x + miniNodeWidth / 2;
                  const startY = from.y + miniNodeHeight;
                  const endY = to.y;
                  const midY = startY + Math.max(30, (endY - startY) / 2);
                  const skipEdges = relations.filter((item) => (levelMap[item.to.id] ?? 0) - (levelMap[item.from.id] ?? 0) > 1);
                  const isRightSkipEdge = fromLevel >= 2 || toCenterX > fromCenterX;
                  const sideSkipEdges = skipEdges.filter((item) => {
                    const skipFrom = miniNodePositions[item.from.id];
                    const skipTo = miniNodePositions[item.to.id];
                    if (!skipFrom || !skipTo) return false;
                    const skipFromCenterX = skipFrom.x + miniNodeWidth / 2;
                    const skipToCenterX = skipTo.x + miniNodeWidth / 2;
                    return (skipToCenterX > skipFromCenterX) === isRightSkipEdge;
                  });
                  const skipFromLevels = [...new Set(sideSkipEdges.map((item) => levelMap[item.from.id] ?? 0))].sort((a, b) => a - b);
                  const leftMostNodeX = Math.min(...Object.values(miniNodePositions).map((position) => position.x));
                  const rightMostNodeX = Math.max(...Object.values(miniNodePositions).map((position) => position.x + miniNodeWidth));
                  const leftOuterLaneX = 16;
                  const leftInnerLaneX = Math.max(leftOuterLaneX + 28, leftMostNodeX - 42);
                  const rightOuterLaneX = miniGraphWidth - 16;
                  const rightInnerLaneX = Math.min(rightOuterLaneX - 28, rightMostNodeX + 42);
                  const getSkipLaneX = (level) => {
                    const levelIndex = Math.max(0, skipFromLevels.indexOf(level));
                    const laneRatio = skipFromLevels.length <= 1 ? 0 : levelIndex / (skipFromLevels.length - 1);
                    return isRightSkipEdge
                      ? rightOuterLaneX + (rightInnerLaneX - rightOuterLaneX) * laneRatio
                      : leftOuterLaneX + (leftInnerLaneX - leftOuterLaneX) * laneRatio;
                  };
                  const skipIncoming = sideSkipEdges.filter((item) => item.to.id === relation.to.id);
                  const innermostSkipLevel = Math.max(...skipIncoming.map((item) => levelMap[item.from.id] ?? 0));
                  const isInnermostSkipEdge = fromLevel === innermostSkipLevel;
                  const innermostLaneX = getSkipLaneX(innermostSkipLevel);
                  const laneX = isRightSkipEdge && fromLevel >= 2
                    ? Math.min(rightOuterLaneX - 12, from.x + miniNodeWidth + 40)
                    : getSkipLaneX(fromLevel);
                  const pathD = isSkipEdge
                    ? isRightSkipEdge
                      ? `M ${from.x + miniNodeWidth} ${from.y + miniNodeHeight / 2} C ${laneX} ${from.y + miniNodeHeight + 34}, ${laneX} ${to.y - 48}, ${toCenterX} ${to.y - 8}`
                      : `M ${from.x} ${from.y + miniNodeHeight / 2} H ${laneX} V ${to.y + miniNodeHeight / 2} H ${isInnermostSkipEdge ? to.x - 8 : innermostLaneX}`
                    : Math.abs(fromCenterX - toCenterX) < 6
                      ? `M ${fromCenterX} ${startY} L ${toCenterX} ${endY - 8}`
                      : `M ${fromCenterX} ${startY} C ${fromCenterX} ${midY}, ${toCenterX} ${midY}, ${toCenterX} ${endY - 8}`;
                  return (
                    <path
                      key={`${relation.from.id}-${relation.to.id}`}
                      d={pathD}
                      fill="none"
                      stroke={isSkipEdge ? '#7c3aed' : '#2563eb'}
                      strokeWidth={isSkipEdge ? '2.2' : '2'}
                      strokeDasharray={isSkipEdge ? '6 5' : undefined}
                      opacity={isSkipEdge ? '0.82' : '1'}
                      markerEnd={isSkipEdge ? (isRightSkipEdge || isInnermostSkipEdge ? 'url(#sidebar-relation-arrow-skip)' : undefined) : 'url(#sidebar-relation-arrow)'}
                    />
                  );
                })}
              </svg>
              {graphPrograms.map((program) => {
                const position = miniNodePositions[program.id];
                if (!position) return null;
                const typeMeta = getProgramTypeMeta(program.programType);
                return (
                  <button
                    key={program.id}
                    type="button"
                    onClick={() => onProgramClick(program.id)}
                    className={`absolute flex flex-col items-center justify-center rounded-xl border px-2 text-center shadow-sm transition hover:-translate-y-0.5 ${typeMeta.rowClass}`}
                    style={{
                      left: position.x,
                      top: position.y,
                      width: miniNodeWidth,
                      height: miniNodeHeight
                    }}
                  >
                    <span className="max-w-full truncate text-[11px] font-black text-slate-950">
                      {program.name}
                    </span>
                  </button>
                );
              })}
            </div>
          </div>
        ) : (
          <div className="rounded-2xl bg-white px-3 py-6 text-center text-xs font-bold text-slate-500 ring-1 ring-slate-100">
            아직 연결된 프로그램이 없습니다.
          </div>
        )}
      </div>
    </aside>
  );
}

export default function App() {
  const urlParams = new URLSearchParams(window.location.search);
  const isDetailWindow = urlParams.get('view') === 'program-detail' || urlParams.get('view') === 'cheonjiin-detail';
  const detailProgramId = urlParams.get('program') ?? 'cheonjiin';
  const [programStates, setProgramStates] = useState(readStoredProgramStates);
  const [isEditing, setIsEditing] = useState(false);
  const [content, setContent] = useState(readStoredContent);
  const [isRelationPopupOpen, setIsRelationPopupOpen] = useState(false);
  const [isComparePopupOpen, setIsComparePopupOpen] = useState(false);
  const [sidebarWidth, setSidebarWidth] = useState(280);
  const [isServerLoaded, setIsServerLoaded] = useState(false);
  const editableCheonjiinFlow = mergeStepContent(cheonjiinFlow, content.cheonjiin.steps);
  const editableWayPrimalFlow = mergeStepContent(wayPrimalFlow, content.wayPrimal.steps);
  const programs = [
    {
      id: 'cheonjiin',
      name: content.cheonjiin.name,
      description: content.cheonjiin.description,
      steps: editableCheonjiinFlow,
      format: content.cheonjiin.format,
      programType: getProgramType(content.cheonjiin.programType),
      predecessors: content.cheonjiin.predecessors ?? [],
      successors: content.cheonjiin.successors ?? [],
      accent: {
        iconBg: 'bg-teal-50',
        iconText: 'text-teal-700',
        labelText: 'text-teal-700',
        arrowText: 'text-teal-600'
      }
    },
    {
      id: 'wayPrimal',
      name: content.wayPrimal.name,
      description: content.wayPrimal.description,
      steps: editableWayPrimalFlow,
      format: content.wayPrimal.format,
      programType: getProgramType(content.wayPrimal.programType),
      predecessors: content.wayPrimal.predecessors ?? [],
      successors: content.wayPrimal.successors ?? [],
      accent: {
        iconBg: 'bg-indigo-50',
        iconText: 'text-indigo-700',
        labelText: 'text-indigo-700',
        arrowText: 'text-indigo-600'
      }
    },
    ...content.extraPrograms.map((program) => ({
      id: program.id,
      name: program.name,
      description: program.description,
      steps: normalizeProgramSteps(program),
      format: program.format,
      programType: getProgramType(program.programType),
      predecessors: program.predecessors ?? [],
      successors: program.successors ?? [],
      accent: {
        iconBg: 'bg-blue-50',
        iconText: 'text-blue-700',
        labelText: 'text-blue-700',
        arrowText: 'text-blue-600'
      }
    }))
  ];
  const getStoredProgram = (programId) => (
    programId === 'cheonjiin'
      ? content.cheonjiin
      : programId === 'wayPrimal'
        ? content.wayPrimal
        : content.extraPrograms.find((program) => program.id === programId)
  );
  const getProgramReviewItems = (program) => buildReviewItems(
    program.id,
    program.steps,
    getStoredProgram(program.id)?.detailGates
  );
  const reentryContext = getReentryContext(programs, programStates, getProgramReviewItems);
  const getMainActiveStep = (programId) => {
    if (!reentryContext) return 'completed';
    const programIndex = programs.findIndex((program) => program.id === programId);
    if (programId === reentryContext.targetProgramId) return reentryContext.targetStepId;
    if (programIndex > reentryContext.targetProgramIndex) return disabledFlowStep;
    return 'completed';
  };

  useEffect(() => {
    const channel = new BroadcastChannel(channelName);
    channel.onmessage = (event) => {
      if (!event.data?.states) return;
      setProgramStates(event.data.states);
    };
    return () => channel.close();
  }, []);

  useEffect(() => {
    let isMounted = true;

    const loadServerState = async () => {
      try {
        const response = await fetch(serverStateEndpoint);
        if (response.status === 404) {
          await saveServerState({
            content,
            programStates
          });
          if (isMounted) setIsServerLoaded(true);
          return;
        }

        if (!response.ok) throw new Error('Failed to load server state');

        const serverState = await response.json();
        if (!isMounted) return;

        if (serverState.content) {
          const nextContent = normalizeStoredContent(serverState.content);
          setContent(nextContent);
          window.localStorage.setItem(contentStorageKey, JSON.stringify(nextContent));
        }

        if (serverState.programStates) {
          setProgramStates(serverState.programStates);
          window.localStorage.setItem(programStateStorageKey, JSON.stringify(serverState.programStates));
        }

        setIsServerLoaded(true);
      } catch {
        if (isMounted) setIsServerLoaded(true);
      }
    };

    loadServerState();

    return () => {
      isMounted = false;
    };
  }, []);

  useEffect(() => {
    window.localStorage.setItem(contentStorageKey, JSON.stringify(content));
  }, [content]);

  useEffect(() => {
    if (!isServerLoaded) return;

    const saveTimer = window.setTimeout(() => {
      saveServerState({
        content,
        programStates
      }).catch(() => undefined);
    }, 250);

    return () => window.clearTimeout(saveTimer);
  }, [content, programStates, isServerLoaded]);

  useEffect(() => {
    const syncStoredContent = (event) => {
      if (event.key === contentStorageKey) {
        setContent(readStoredContent());
      }
      if (event.key === programStateStorageKey) {
        setProgramStates(readStoredProgramStates());
      }
    };

    window.addEventListener('storage', syncStoredContent);
    return () => window.removeEventListener('storage', syncStoredContent);
  }, []);

  useEffect(() => {
    window.localStorage.setItem(programStateStorageKey, JSON.stringify(programStates));
  }, [programStates]);

  useEffect(() => {
    if (!isDetailWindow) return undefined;

    const resetMainFlowState = () => {
      publishProgramStates({});
    };

    window.addEventListener('beforeunload', resetMainFlowState);
    return () => window.removeEventListener('beforeunload', resetMainFlowState);
  }, [isDetailWindow]);

  const updateStep = (program, index, field, value) => {
    setContent((current) => ({
      ...current,
      [program]: {
        ...current[program],
        steps: current[program].steps.map((step, stepIndex) =>
          stepIndex === index ? { ...step, [field]: value } : step
        )
      }
    }));
  };

  const createStep = (programId, nextIndex) => ({
    id: `${programId}-${Date.now()}-${nextIndex}`,
    title: `${nextIndex}단계`,
    feature: '주요 기능 입력',
    note: '단계 설명을 입력합니다.'
  });

  const addStep = (programId) => {
    setContent((current) => {
      if (programId === 'cheonjiin' || programId === 'wayPrimal') {
        const steps = current[programId].steps ?? [];
        return {
          ...current,
          [programId]: {
            ...current[programId],
            steps: [...steps, createStep(programId, steps.length + 1)]
          }
        };
      }

      return {
        ...current,
        extraPrograms: current.extraPrograms.map((program) =>
          program.id === programId
            ? {
                ...program,
                steps: [...program.steps, createStep(program.id, program.steps.length + 1)]
              }
            : program
        )
      };
    });
  };

  const removeStep = (programId, stepIndex) => {
    setContent((current) => {
      if (programId === 'cheonjiin' || programId === 'wayPrimal') {
        const steps = current[programId].steps ?? [];
        if (steps.length <= 1) return current;
        return {
          ...current,
          [programId]: {
            ...current[programId],
            steps: steps.filter((_, index) => index !== stepIndex)
          }
        };
      }

      return {
        ...current,
        extraPrograms: current.extraPrograms.map((program) =>
          program.id === programId && program.steps.length > 1
            ? { ...program, steps: program.steps.filter((_, index) => index !== stepIndex) }
            : program
        )
      };
    });
  };

  const moveStep = (programId, stepIndex, nextIndex) => {
    setContent((current) => {
      if (programId === 'cheonjiin' || programId === 'wayPrimal') {
        return {
          ...current,
          [programId]: {
            ...current[programId],
            steps: moveItem(current[programId].steps ?? [], stepIndex, nextIndex)
          }
        };
      }

      return {
        ...current,
        extraPrograms: current.extraPrograms.map((program) =>
          program.id === programId
            ? { ...program, steps: moveItem(program.steps, stepIndex, nextIndex) }
            : program
        )
      };
    });
  };

  const updateFormat = (program, value) => {
    setContent((current) => ({
      ...current,
      [program]: {
        ...current[program],
        format: value
      }
    }));
  };

  const updateDeliverable = (index, value) => {
    setContent((current) => ({
      ...current,
      cheonjiin: {
        ...current.cheonjiin,
        deliverables: current.cheonjiin.deliverables.map((item, itemIndex) =>
          itemIndex === index ? value : item
        )
      }
    }));
  };

  const updateProgramDeliverable = (programId, itemIndex, value) => {
    if (programId === 'cheonjiin') {
      updateDeliverable(itemIndex, value);
      return;
    }

    if (programId === 'wayPrimal') {
      setContent((current) => ({
        ...current,
        wayPrimal: {
          ...current.wayPrimal,
          deliverables: (current.wayPrimal.deliverables ?? ['']).map((item, index) =>
            index === itemIndex ? value : item
          )
        }
      }));
      return;
    }

    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.map((program) =>
        program.id === programId
          ? {
              ...program,
              deliverables: (program.deliverables ?? ['']).map((item, index) =>
                index === itemIndex ? value : item
              )
            }
          : program
      )
    }));
  };

  const updateProgramLinkLabel = (programId, value) => {
    if (programId === 'wayPrimal') {
      setContent((current) => ({
        ...current,
        wayPrimal: {
          ...current.wayPrimal,
          linkLabel: value
        }
      }));
      return;
    }

    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.map((program) =>
        program.id === programId
          ? { ...program, linkLabel: value }
          : program
      )
    }));
  };

  const updateProgramRecord = (current, programId, updater) => {
    if (programId === 'cheonjiin' || programId === 'wayPrimal') {
      return {
        ...current,
        [programId]: updater(current[programId])
      };
    }

    return {
      ...current,
      extraPrograms: current.extraPrograms.map((program) =>
        program.id === programId ? updater(program) : program
      )
    };
  };

  const toggleProgramRelation = (programId, field, relatedProgramId, checked) => {
    const oppositeField = field === 'predecessors' ? 'successors' : 'predecessors';
    const updateList = (list = [], value, shouldAdd) => (
      shouldAdd
        ? [...new Set([...list, value])]
        : list.filter((item) => item !== value)
    );

    setContent((current) => {
      const nextContent = updateProgramRecord(current, programId, (program) => ({
        ...program,
        [field]: updateList(program[field], relatedProgramId, checked)
      }));

      return updateProgramRecord(nextContent, relatedProgramId, (program) => ({
        ...program,
        [oppositeField]: updateList(program[oppositeField], programId, checked)
      }));
    });
  };

  const updateComparison = (leftProgramId, rightProgramId, field, value) => {
    setContent((current) => {
      const comparisons = current.comparisons ?? [];
      const comparisonIndex = comparisons.findIndex(
        (comparison) => comparison.leftProgramId === leftProgramId && comparison.rightProgramId === rightProgramId
      );
      const nextComparison = {
        ...(comparisonIndex >= 0 ? comparisons[comparisonIndex] : {}),
        leftProgramId,
        rightProgramId,
        updatedAt: new Date().toISOString(),
        [field]: value
      };
      const hasComparisonContent =
        (nextComparison.stepMatches ?? []).some((match) => match.reason?.trim()) ||
        nextComparison.note?.trim();
      return {
        ...current,
        comparisons:
          !hasComparisonContent
            ? comparisons.filter((_, index) => index !== comparisonIndex)
            :
          comparisonIndex >= 0
            ? comparisons.map((comparison, index) => (index === comparisonIndex ? nextComparison : comparison))
            : [...comparisons, nextComparison]
      };
    });
  };

  const updateProgramTitle = (programId, field, value) => {
    if (programId === 'cheonjiin' || programId === 'wayPrimal') {
      const target = programId === 'cheonjiin' ? 'cheonjiin' : 'wayPrimal';
      setContent((current) => ({
        ...current,
        [target]: {
          ...current[target],
          [field]: value
        }
      }));
      return;
    }

    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.map((program) =>
        program.id === programId
          ? { ...program, [field]: value }
          : program
      )
    }));
  };

  const addProgram = () => {
    const id = `program-${Date.now()}`;
    setContent((current) => ({
      ...current,
      extraPrograms: [
        ...current.extraPrograms,
        {
          id,
          name: '새 프로그램',
          description: '새 프로그램 업무 플로우',
          steps: [
            {
              id: `${id}-1`,
              title: '1단계',
              feature: '주요 기능 입력',
              note: '단계 설명을 입력합니다.'
            },
            {
              id: `${id}-2`,
              title: '2단계',
              feature: '주요 기능 입력',
              note: '단계 설명을 입력합니다.'
            }
          ],
          format: '',
          deliverables: ['성과물'],
          programType: 'internal',
          predecessors: [],
          successors: [],
          linkLabel: '이전 프로그램 산출물을 새 프로그램 입력으로 연계'
        }
      ]
    }));
    setIsEditing(true);
  };

  const toggleProgramEdit = () => {
    setIsEditing((current) => !current);
  };

  const updateProgram = (programIndex, field, value) => {
    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.map((program, index) =>
        index === programIndex ? { ...program, [field]: value } : program
      )
    }));
  };

  const updateProgramStep = (programIndex, stepIndex, field, value) => {
    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.map((program, index) =>
        index === programIndex
          ? {
              ...program,
              steps: program.steps.map((step, currentStepIndex) =>
                currentStepIndex === stepIndex ? { ...step, [field]: value } : step
              )
            }
          : program
      )
    }));
  };

  const removeProgram = (programIndex) => {
    setContent((current) => ({
      ...current,
      extraPrograms: current.extraPrograms.filter((_, index) => index !== programIndex)
    }));
  };

  const moveProgram = (programIndex, nextIndex) => {
    setContent((current) => ({
      ...current,
      extraPrograms: moveItem(current.extraPrograms, programIndex, nextIndex)
    }));
  };

  const updateProgramGates = (programId, gates) => {
    setProgramStates((current) => {
      const nextStates = { ...current, [programId]: gates };
      publishProgramStates(nextStates);
      return nextStates;
    });
  };

  const updateDetailGate = (programId, gateIndex, field, value) => {
    setContent((current) => {
      if (programId === 'cheonjiin' || programId === 'wayPrimal') {
        const target = programId === 'cheonjiin' ? 'cheonjiin' : 'wayPrimal';
        const nextDetailGates = [
          ...(current[target].detailGates ?? buildReviewItems(programId, programs.find((program) => program.id === programId)?.steps ?? []))
        ];
        nextDetailGates[gateIndex] = { ...nextDetailGates[gateIndex], [field]: value };
        return {
          ...current,
          [target]: {
            ...current[target],
            detailGates: nextDetailGates
          }
        };
      }

      return {
        ...current,
        extraPrograms: current.extraPrograms.map((program) => {
          if (program.id !== programId) return program;
          const baseSteps = normalizeProgramSteps(program);
          const nextDetailGates = [
            ...(program.detailGates ?? buildReviewItems(program.id, baseSteps))
          ];
          nextDetailGates[gateIndex] = { ...nextDetailGates[gateIndex], [field]: value };
          return { ...program, detailGates: nextDetailGates };
        })
      };
    });
  };

  const updateDetailGates = (programId, getNextDetailGates) => {
    setContent((current) => {
      if (programId === 'cheonjiin' || programId === 'wayPrimal') {
        const target = programId === 'cheonjiin' ? 'cheonjiin' : 'wayPrimal';
        const currentProgram = programs.find((program) => program.id === programId);
        const currentGates = current[target].detailGates ?? buildReviewItems(programId, currentProgram?.steps ?? []);
        return {
          ...current,
          [target]: {
            ...current[target],
            detailGates: getNextDetailGates(currentGates, currentProgram?.steps ?? [])
          }
        };
      }

      return {
        ...current,
        extraPrograms: current.extraPrograms.map((program) => {
          if (program.id !== programId) return program;
          const baseSteps = normalizeProgramSteps(program);
          const currentGates = program.detailGates ?? buildReviewItems(program.id, baseSteps);
          return {
            ...program,
            detailGates: getNextDetailGates(currentGates, baseSteps)
          };
        })
      };
    });
  };

  const addDetailGate = (programId) => {
    updateDetailGates(programId, (currentGates, steps) => {
      const nextIndex = currentGates.length;
      const targetStep = steps[Math.min(nextIndex, Math.max(0, steps.length - 2))] ?? steps[0];
      return [
        ...currentGates.map((gate, index) => ({
          ...gate,
          no: index === currentGates.length - 1 ? '아니오: 다음 검토' : gate.no
        })),
        {
          key: `gate${nextIndex + 1}-${Date.now()}`,
          targetProgramId: programId,
          stepId: targetStep?.id,
          question: `${targetStep?.title ?? `${nextIndex + 1}단계`} 변경?`,
          yes: `예: ${targetStep?.title ?? `${nextIndex + 1}단계`} 재진입`,
          no: '아니오: 변경사항 없음'
        }
      ];
    });
  };

  const removeDetailGate = (programId, gateIndex) => {
    updateDetailGates(programId, (currentGates) => {
      const nextGates = currentGates.filter((_, index) => index !== gateIndex);
      return nextGates.map((gate, index) => ({
        ...gate,
        no: index === nextGates.length - 1 ? '아니오: 변경사항 없음' : gate.no
      }));
    });
  };

  const moveDetailGate = (programId, gateIndex, nextIndex) => {
    updateDetailGates(programId, (currentGates) => {
      const nextGates = moveItem(currentGates, gateIndex, nextIndex);
      return nextGates.map((gate, index) => ({
        ...gate,
        no: index === nextGates.length - 1 ? '아니오: 변경사항 없음' : gate.no
      }));
    });
  };

  const resetContent = () => {
    setContent(defaultContent);
    window.localStorage.removeItem(contentStorageKey);
    setProgramStates({});
    window.localStorage.removeItem(programStateStorageKey);
  };

  const openProgramWindow = (programId) => {
    publishProgramStates(programStates);
    window.open(
      `/?view=program-detail&program=${encodeURIComponent(programId)}`,
      'program-detail-flow',
      'popup=yes,width=620,height=990,left=80,top=40,resizable=yes,scrollbars=yes'
    );
  };

  if (isDetailWindow) {
    const detailProgram = programs.find((program) => program.id === detailProgramId) ?? programs[0];
    const storedProgram = getStoredProgram(detailProgram.id);
    const reviewItems = buildReviewItems(detailProgram.id, detailProgram.steps, storedProgram?.detailGates);
    const gates = getProgramGates(programStates, detailProgram.id, detailProgram.steps);
    return (
      <DetailPopup
        program={detailProgram}
        allPrograms={programs}
        gates={gates}
        setGates={(nextGates) => updateProgramGates(detailProgram.id, nextGates)}
        selectedStep={getActiveStep(gates, reviewItems)}
        setSelectedStep={() => {}}
        reviewItems={reviewItems}
        onReviewItemChange={(index, field, value) => updateDetailGate(detailProgram.id, index, field, value)}
        onAddReviewItem={() => addDetailGate(detailProgram.id)}
        onRemoveReviewItem={(index) => removeDetailGate(detailProgram.id, index)}
        onMoveReviewItem={(index, nextIndex) => moveDetailGate(detailProgram.id, index, nextIndex)}
        onClose={() => {
          publishProgramStates({});
          window.close();
        }}
        standalone
      />
    );
  }

  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">
        <div className="flex justify-end gap-2">
          <button
            type="button"
            onClick={() => setIsComparePopupOpen(true)}
            className="flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-bold text-slate-700 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50"
          >
            <Waypoints className="h-4 w-4" />
            1:1 비교
          </button>
          <button
            type="button"
            onClick={toggleProgramEdit}
            className={`flex items-center gap-2 rounded-full px-4 py-2 text-sm font-bold shadow-sm ${
              isEditing
                ? 'bg-emerald-600 text-white hover:bg-emerald-700'
                : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
          >
            {isEditing ? <Save className="h-4 w-4" /> : <Edit3 className="h-4 w-4" />}
            {isEditing ? '수정 완료' : '프로그램 추가 및 수정'}
          </button>
        </div>

        <div
          className="grid gap-4 xl:items-start"
          style={{ gridTemplateColumns: `${sidebarWidth}px minmax(0, 1fr)` }}
        >
          <RelationTreePanel
            programs={programs}
            onProgramClick={openProgramWindow}
            onOpenRelationPopup={() => setIsRelationPopupOpen(true)}
            sidebarWidth={sidebarWidth}
            onSidebarWidthChange={setSidebarWidth}
          />

          <section className="min-w-0 space-y-5">
        {isEditing && (
          <div className="flex items-center justify-between rounded-[24px] border border-white/70 bg-white/70 px-5 py-4 shadow-sm backdrop-blur">
            <div>
              <p className="text-[12px] font-extrabold uppercase tracking-wide text-blue-700">
                프로그램 편집
              </p>
              <h2 className="mt-1 text-lg font-extrabold text-slate-950">
                프로그램을 추가하거나 기존 내용을 수정합니다
              </h2>
            </div>
            <button
              type="button"
              onClick={addProgram}
              className="flex items-center gap-2 rounded-full bg-blue-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-blue-700"
            >
              <Plus className="h-4 w-4" />
              프로그램 추가
            </button>
          </div>
        )}

        <FlowRow
          label={content.cheonjiin.name}
          description={content.cheonjiin.description}
          steps={editableCheonjiinFlow}
          onLabelClick={() => openProgramWindow('cheonjiin')}
          activeStep={getMainActiveStep('cheonjiin')}
          isEditing={isEditing}
          onStepChange={(index, field, value) => updateStep('cheonjiin', index, field, value)}
          onLabelChange={(value) => updateProgramTitle('cheonjiin', 'name', value)}
          onDescriptionChange={(value) => updateProgramTitle('cheonjiin', 'description', value)}
          programType={content.cheonjiin.programType}
          onProgramTypeChange={(value) => updateProgramTitle('cheonjiin', 'programType', value)}
          format={content.cheonjiin.format}
          onFormatChange={(value) => updateFormat('cheonjiin', value)}
          deliverables={content.cheonjiin.deliverables}
          onDeliverableChange={(index, value) => updateProgramDeliverable('cheonjiin', index, value)}
          onAddStep={() => addStep('cheonjiin')}
          onRemoveStep={(index) => removeStep('cheonjiin', index)}
          onMoveStep={(index, nextIndex) => moveStep('cheonjiin', index, nextIndex)}
          accent={{
            iconBg: 'bg-teal-50',
            iconText: 'text-teal-700',
            labelText: 'text-teal-700',
            arrowText: 'text-teal-600'
          }}
        />

        <div className="flex items-center gap-4 px-3 py-1">
          <div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-white/85 text-slate-500 shadow-sm ring-1 ring-white">
            <ArrowDown className="h-5 w-5" />
          </div>
          {isEditing ? (
              <input
                value={content.wayPrimal.linkLabel}
                onChange={(event) => updateProgramLinkLabel('wayPrimal', event.target.value)}
                className="min-w-[360px] rounded-full border border-slate-200 bg-white/85 px-3 py-2 text-sm font-bold text-slate-700 outline-none focus:border-indigo-400"
              />
          ) : (
            <div className="text-sm font-bold text-slate-600">
              {content.wayPrimal.linkLabel}
            </div>
          )}
        </div>

        <FlowRow
          label={content.wayPrimal.name}
          description={content.wayPrimal.description}
          steps={editableWayPrimalFlow}
          onLabelClick={() => openProgramWindow('wayPrimal')}
          activeStep={getMainActiveStep('wayPrimal')}
          isEditing={isEditing}
          onStepChange={(index, field, value) => updateStep('wayPrimal', index, field, value)}
          onLabelChange={(value) => updateProgramTitle('wayPrimal', 'name', value)}
          onDescriptionChange={(value) => updateProgramTitle('wayPrimal', 'description', value)}
          programType={content.wayPrimal.programType}
          onProgramTypeChange={(value) => updateProgramTitle('wayPrimal', 'programType', value)}
          format={content.wayPrimal.format}
          onFormatChange={(value) => updateFormat('wayPrimal', value)}
          deliverables={content.wayPrimal.deliverables ?? []}
          onDeliverableChange={(index, value) => updateProgramDeliverable('wayPrimal', index, value)}
          onAddStep={() => addStep('wayPrimal')}
          onRemoveStep={(index) => removeStep('wayPrimal', index)}
          onMoveStep={(index, nextIndex) => moveStep('wayPrimal', index, nextIndex)}
          accent={{
            iconBg: 'bg-indigo-50',
            iconText: 'text-indigo-700',
            labelText: 'text-indigo-700',
            arrowText: 'text-indigo-600'
          }}
        />

        {content.extraPrograms.map((program, programIndex) => (
          <React.Fragment key={program.id}>
            <div className="flex items-center gap-4 px-3 py-1">
              <div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-white/85 text-slate-500 shadow-sm ring-1 ring-white">
                <ArrowDown className="h-5 w-5" />
              </div>
              {isEditing ? (
                  <input
                    value={program.linkLabel ?? `이전 프로그램 산출물을 ${program.name} 입력으로 연계`}
                    onChange={(event) => updateProgramLinkLabel(program.id, event.target.value)}
                    className="min-w-[360px] rounded-full border border-slate-200 bg-white/85 px-3 py-2 text-sm font-bold text-slate-700 outline-none focus:border-blue-400"
                  />
              ) : (
                <div className="text-sm font-bold text-slate-600">
                  {program.linkLabel ?? `이전 프로그램 산출물을 ${program.name} 입력으로 연계`}
                </div>
              )}
            </div>

            {isEditing && (
              <section className="rounded-[24px] border border-white/70 bg-white/70 px-5 py-4 shadow-sm backdrop-blur">
                <div className="grid gap-3 lg:grid-cols-[220px_1fr_auto] lg:items-start">
                  <div>
                    <p className="text-[12px] font-extrabold uppercase tracking-wide text-blue-700">
                      추가 프로그램
                    </p>
                    <h2 className="mt-1 text-lg font-extrabold text-slate-950">프로그램 정보</h2>
                  </div>
                  <div className="grid gap-2 md:grid-cols-2">
                    <input
                      value={program.name}
                      onChange={(event) => updateProgram(programIndex, 'name', event.target.value)}
                      className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-extrabold text-slate-900 outline-none focus:border-blue-400"
                    />
                    <input
                      value={program.description}
                      onChange={(event) => updateProgram(programIndex, 'description', event.target.value)}
                      className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-bold text-slate-700 outline-none focus:border-blue-400"
                    />
                  </div>
                  <div className="flex gap-2">
                    <button
                      type="button"
                      disabled={programIndex === 0}
                      onClick={() => moveProgram(programIndex, programIndex - 1)}
                      className="flex items-center gap-1 rounded-full bg-white px-3 py-2 text-[12px] font-extrabold text-slate-600 ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-35"
                    >
                      <ArrowUp className="h-3.5 w-3.5" />
                      위
                    </button>
                    <button
                      type="button"
                      disabled={programIndex === content.extraPrograms.length - 1}
                      onClick={() => moveProgram(programIndex, programIndex + 1)}
                      className="flex items-center gap-1 rounded-full bg-white px-3 py-2 text-[12px] font-extrabold text-slate-600 ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-35"
                    >
                      <ArrowDown className="h-3.5 w-3.5" />
                      아래
                    </button>
                  </div>
                </div>
              </section>
            )}

            <FlowRow
              label={program.name}
              description={program.description}
              steps={normalizeProgramSteps(program)}
              onLabelClick={() => openProgramWindow(program.id)}
              activeStep={getMainActiveStep(program.id)}
              isEditing={isEditing}
              onStepChange={(index, field, value) => updateProgramStep(programIndex, index, field, value)}
              onLabelChange={(value) => updateProgramTitle(program.id, 'name', value)}
              onDescriptionChange={(value) => updateProgramTitle(program.id, 'description', value)}
              programType={program.programType}
              onProgramTypeChange={(value) => updateProgram(programIndex, 'programType', value)}
              format={program.format}
              onFormatChange={(value) => updateProgram(programIndex, 'format', value)}
              deliverables={program.deliverables ?? []}
              onDeliverableChange={(index, value) => updateProgramDeliverable(program.id, index, value)}
              onAddStep={() => addStep(program.id)}
              onRemoveStep={(index) => removeStep(program.id, index)}
              onMoveStep={(index, nextIndex) => moveStep(program.id, index, nextIndex)}
              onProgramDelete={() => removeProgram(programIndex)}
              accent={{
                iconBg: 'bg-blue-50',
                iconText: 'text-blue-700',
                labelText: 'text-blue-700',
                arrowText: 'text-blue-600'
              }}
            />

          </React.Fragment>
        ))}

          </section>
        </div>

        {isRelationPopupOpen && (
          <RelationPopup
            programs={programs}
            onToggleRelation={toggleProgramRelation}
            onClose={() => setIsRelationPopupOpen(false)}
          />
        )}

        {isComparePopupOpen && (
          <ProgramComparePopup
            programs={programs}
            comparisons={content.comparisons ?? []}
            onComparisonChange={updateComparison}
            onClose={() => setIsComparePopupOpen(false)}
          />
        )}
      </div>
    </main>
  );
}
