상세 패널

도킹된 사이드 패널에 작업 하나의 필드를 표시하고 그 자리에서 편집해요.

막대를 드래그하면 날짜를 빠르게 정할 수 있지만 정확하지는 않아요. 상세 패널을 켜면 타임라인 옆에 작업 하나의 필드가 표시되고, 값을 읽거나 직접 입력할 수 있어요.

패널 표시하기

showDetail을 추가해요. 패널은 타임라인 오른쪽에 붙고, 패널이 열리면 타임라인이 좁아져요.

ProjectChart.tsx
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';

const initialTasks: Task[] = [
  { id: 'design', name: 'Design', parentId: null, sequence: '1',
    startDate: '2026-09-01', endDate: '2026-09-08', progress: 100 },
  { id: 'build', name: 'Build', parentId: null, sequence: '2',
    startDate: '2026-09-08', endDate: '2026-09-18', progress: 40,
    dependencies: [{ targetId: 'design', type: 'FS' }] },
];

export function ProjectChart() {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    <ReactGanttChart
      tasks={tasks}
      onTasksChange={setTasks}
      showTaskList
      showDetail
      height={420}
    />
  );
}

패널의 기본값은 꺼짐이에요. showDetail은 내장 본문을 그리고, renderDetail만 넘겨도 패널이 켜져요. showDetail={false}를 명시하면 어느 쪽이든 계속 숨겨져요.

막대나 작업 목록의 행을 클릭하면 패널이 열려요. 포커스된 행에서 Enter나 Space를 눌러도 열려요. 펼칠 수 있는 행에서는 그 키가 행을 접거나 펼쳐요. Escape는 페이지 어디에서든 패널을 닫고 키를 막지 않아서 앱의 핸들러도 그대로 실행돼요. 닫으면 포커스는 패널이 열리기 전 위치로 돌아가요. 그사이 포커스가 다른 곳으로 옮겨갔다면 옮기지 않아요.

패널을 켜면 행 선택도 함께 켜져서 열린 작업의 행이 강조돼요. selectable={false}를 넘기면 강조 없이 패널만 사용해요. 클릭에서는 onTaskClick이 패널보다 먼저 발생해요. 이벤트를 참고해요.

패널은 dialog가 아니에요. 포커스 트랩과 배경막이 없고 뒤에 있는 차트도 그대로 조작할 수 있어요. 키보드와 스크린 리더를 참고해요.

기본 패널이 보여주는 정보

본문은 맨 위에 작업 이름을 두고 그 아래에 짧은 필드 목록을 보여줘요.

필드값
Start, End현재 배율의 툴팁 형식을 거친 날짜예요.
Duration두 날짜 사이의 기간이에요. 하루 미만은 시간, 그 이상은 일 단위로 표시해요.
Progresstask.progress를 0에서 100 사이로 제한한 값이에요.
Depends ondependencies가 가리키는 작업의 이름이에요. 가리키는 작업이 없으면 id를 그대로 표시해요.

Progress와 Depends on은 작업에 값이 있을 때만 나타나요. 다른 필드는 표시하지 않고 행을 추가하는 prop도 없어요. 캡션은 고정된 영어 문자열이에요. 로케일과 날짜 형식을 참고해요.

패널에서 필드 편집하기

패널은 자체 플래그를 추가하지 않아요. 그래서 작업의 플래그가 같은 제스처를 이미 허용할 때만 필드가 입력란이 돼요. 작업이나 차트에 readOnly가 없으면 이름은 텍스트 입력란이에요.

날짜를 바꾸는 것은 크기 조절이에요. 크기를 조절할 수 있는 작업에서만 Start와 End가 날짜 입력란이 돼요. 요약 행의 날짜는 텍스트로 남아요.

Progress는 진행률 핸들을 끌 수 있을 때 숫자 입력란이 돼요. Duration과 Depends on은 계산된 값이라 텍스트로 남아요. 우선순위는 상호작용 설정에서 확인해요.

필드는 포커스가 빠질 때와 Enter를 누를 때 확정돼요. 변경은 드래그와 같은 전체 배열로 onTasksChange에 전달되고, 별도의 콜백은 없어요. Escape는 패널을 닫지 않고 필드 값만 되돌려요. 빈 이름, 불완전한 날짜, 시작보다 뒤가 아닌 종료 날짜, 비워진 진행률은 확정되지 않고 안내 없이 되돌아가요.

날짜를 편집하면 날짜 부분만 바뀌고 작업의 시각은 그대로예요. 결과는 드래그와 같은 minDate와 maxDate 범위로 제한돼요. 날짜 입력란은 locale과 관계없이 YYYY-MM-DD로 표시해요.

본문 직접 그리기

renderDetail은 패널 안쪽을 통째로 교체해요. 너비와 접근성 이름, 닫을 때의 포커스 처리는 차트가 유지해요. 닫기 버튼은 직접 그려야 하고 Escape는 그대로 패널을 닫아요.

<ReactGanttChart
  tasks={tasks}
  onTasksChange={setTasks}
  showTaskList
  renderDetail={({ task, close, update }) => (
    <>
      <h2>{task.name}</h2>
      <button type="button" onClick={() => update({ progress: 100 })}>
        Mark done
      </button>
      <button type="button" onClick={close}>
        Close
      </button>
    </>
  )}
/>

렌더러는 차트가 변환한 열린 task와 함께 close, 현재 scale, update를 받아요. update(patch)는 모든 제스처와 같은 경로로 확정하고 상호작용 플래그를 검사하지 않아요. 그래서 커스텀 본문은 자기 규칙을 직접 적용해요. 차트의 유일한 렌더 prop이에요. 상세 렌더러를 참고해요.

열린 작업 제어하기

열린 작업을 앱 상태로 들고 있으려면 detailTaskId를 넘기고, 열고 닫는 시점은 onDetailChange로 받아요. detailTaskId가 undefined가 아니면 제어 모드예요. 그래서 null은 기능을 끄는 값이 아니라 제어 모드이면서 닫힌 상태를 뜻해요.

제어 모드에서 차트는 스스로 아무것도 열지 않고 새 detailTaskId를 기다려요. onDetailChange를 처리하지 않고 detailTaskId만 넘기면 모든 클릭이 무시되는 것처럼 보여요.

onDetailChange는 두 모드에서 모두 발생해요. 마운트 시점에는 발생하지 않고, 열린 작업의 데이터가 바뀌어도 발생하지 않아요. 그 작업의 막대를 옮겨도 마찬가지예요.

차트 ref의 openDetail(taskId)와 closeDetail()은 같은 규칙을 따라요. 패널이 꺼져 있으면 아무 일도 하지 않고, 모르는 id는 무시해요. openDetail은 접혀 있는 행도 찾아요. 명령형 API를 참고해요.

적용 전에 확인하기

  • 열린 작업이 tasks에서 빠지면 패널이 닫히고 onDetailChange는 발생하지 않아요.
  • detailTaskId가 없는 작업을 가리키면 오류 없이 닫힌 채로 있다가, 찾을 수 있는 id가 들어오면 열려요.
  • Escape는 패널만 닫고 행은 선택된 채로 남아요. 빈 타임라인 공간을 클릭하면 선택도 함께 해제돼요.
  • 너비는 --gantt-detail-width로 정하고 달라지는 만큼은 타임라인이 가져가요. 드래그 핸들이나 너비 prop은 없어요. 테마를 참고해요.
  • 패널은 오른쪽에 붙어서 한 번에 작업 하나를 보여주고 클릭 한 번으로 열려요. 더블 클릭 모드, 트리거 prop, 탭 목록, 다른 위치는 없어요.

prop과 기본값은 GanttProps를 참고해요.