헤드리스 코어
차트의 트리 계산과 달력 계산을 Node, 워커, 테스트에서 실행해요.
차트가 쓰는 트리 계산과 행 순서 계산, 달력 계산은 React도 스타일시트도 DOM도 필요 없는 순수 함수예요. 서버에서 그대로 실행하면 리포트와 차트가 같은 날짜를 내놓아요.
헬퍼 가져오기
컴포넌트와 같은 패키지 루트에서 가져와요.
import { buildTaskTree, rollUpTasks, createWorkingCalendar } from '@jaeungkim/gantt-chart';헤드리스로 쓸 수 있는 항목은 함수와 값 8개예요. buildTaskTree, collectSubtreeIds,
rollUpTasks, sortTasksBySequence, validateMove, moveTask, CALENDAR_DAYS,
createWorkingCalendar가 전부예요. 런타임 의존성은 dayjs와 utc 플러그인뿐이에요.
/core 서브패스는 없어요. 패키지가 선언하는 진입점은 ., ./style.css, ./package.json이고,
React 컴포넌트까지 담은 번들 하나로 배포돼요. react와 react-dom은 빌드에서 빠지니
rollUpTasks만 가져오는 Node 스크립트에도 둘(^18 또는 ^19)이 설치돼 있어야 해요.
트리 롤업하기
buildTaskTree는 부모와 자식, 깊이를 한 번에 정규화해요. collectSubtreeIds는 한 행 아래 id를
모두 모아요. rollUpTasks는 차트가 그리는 요약 행의 날짜와 진행률을 그대로 반환해요.
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
buildTaskTree,
createWorkingCalendar,
rollUpTasks,
type Task,
} from '@jaeungkim/gantt-chart';
dayjs.extend(utc);
const tasks: Task[] = [
{ id: 'P', name: 'Phase 1', parentId: null, sequence: '1',
startDate: '2025-06-02', endDate: '2025-06-03' },
{ id: 'A', name: 'Survey', parentId: 'P', sequence: '1.1', progress: 100,
startDate: '2025-06-02', endDate: '2025-06-05' },
{ id: 'B', name: 'Frame', parentId: 'P', sequence: '1.2', progress: 40,
startDate: '2025-06-05', endDate: '2025-06-10' },
];
const calendar = createWorkingCalendar({ holidays: ['2025-06-06'] });
for (const task of rollUpTasks(tasks, buildTaskTree(tasks))) {
const days = calendar.daysBetween(dayjs.utc(task.startDate), dayjs.utc(task.endDate));
console.log(task.id, task.startDate.slice(0, 10), task.endDate.slice(0, 10), `${days}wd`, task.progress);
}출력은 다음과 같아요.
P 2025-06-02 2025-06-10 5wd 63
A 2025-06-02 2025-06-05 3wd 100
B 2025-06-05 2025-06-10 2wd 40P는 2025-06-03에 끝난다고 적혀 있었지만 2025-06-10에 끝나는 값으로 돌아와요. 입력에 없던
progress 63도 함께 붙어요. 다시 쓰인 부모는 완전한 ISO 문자열로 돌아오고, 롤업이 건드리지 않은
행은 넘긴 문자열을 그대로 유지해요.
tree 인자는 선택 사항이고 기본값이 새 buildTaskTree(tasks)라서 반복문 안에서 호출하면 매번
트리를 다시 만들어요. 요약 행이 무엇을 덮어쓰는지는 작업 목록과 계층 구조에, 롤업
규칙은 트리 헬퍼에 있어요.
근무일 세기
달력은 앱이 직접 호출하는 객체예요. CALENDAR_DAYS는 7일을 모두 세어요. createWorkingCalendar는
주말과 휴일을 건너뛰어서 주말을 낀 5일 구간이 3일로 세져요.
isWorkingDay, addDays, daysBetween, snapForward와 skipsNonWorkingDays 플래그는 공개
API예요. 코어의 다른 함수는 달력을 받지 않고, 트리 헬퍼와 이동 헬퍼는 날짜를 세지 않아요.
차트도 workingWeekdays와 holidays prop으로 같은 달력을 만들기 때문에 리포트가 세는 날과 차트가
음영으로 칠하는 날이 어긋나지 않아요. 여기서 holidays는 UTC YYYY-MM-DD 문자열 목록이에요.
이름과 색, 여러 날에 걸친 구간은 차트 prop의 몫이에요. 음영은 타임라인, 차트의 스냅
동작은 근무일 달력, 옵션은 근무일 달력 헬퍼에서 확인해요.
차트 밖에서 행 순서 바꾸기
행 순서는 sequence에 있어요. "2.1"이 "2"의 첫 자식인 점 구분 경로예요.
sortTasksBySequence는 각 구간을 숫자로 비교해서 "1.10"이 "1.9" 뒤에 와요.
validateMove는 거부 사유를 반환하고, 이동이 허용되면 null을 반환해요. moveTask는 이동을
적용해서 새 배열과 함께 이전 자리와 새 자리를 담은 변경 정보를 반환해요. validateMove가 거부하는
이동에는 null을 반환하니 이유가 필요하면 validateMove를 먼저 호출하세요.
const result = moveTask(tasks, { taskId: 'B', toParentId: null, toIndex: 0 }, { hierarchy: true });
if (result) await save(result.tasks);moveTask는 경로가 바뀐 작업의 번호를 다시 매기고 parentId는 옮긴 작업에만 다시 써요. 자리가
그대로인 작업은 객체 동일성을 유지해요. 차트 안의 행 드래그는 행 순서 바꾸기에서
다뤄요.
적용 전에 확인하기
rollUpTasks는 파싱할 수 없는 자식 날짜를 건너뛰고, 유한한 날짜를 내는 자식이 하나도 없으면 부모를 그대로 둬요. 그래서 잘못된 입력은 오류 대신 낡은 요약 행으로 나타나요.buildTaskTree는 망가진 부모 링크를 알리지 않고 끊어요. 없는 작업을 가리키는parentId, 자기 자신을 부모로 둔 작업, 순환하는 부모 체인은 모두 루트가 되고 경고도 남지 않아요.createWorkingCalendar는workingWeekdays가 비어 있으면 모든 날을 비근무일로 봐요. 그러면addDays는 평범한 날짜 계산으로 돌아가고snapForward는 입력을 그대로 반환해요.validateMove와moveTask는 호출마다 작업을 다시 정렬하고 형제 목록을 다시 만들어요. 둘 다 날짜는 바꾸지 않아요. 캐싱은 앱의 몫이에요.- 모든 날짜는 UTC예요. 타임존이 없는 문자열은 서버의 로컬 시각이 아니라 UTC로 읽혀서 Asia/Seoul에서 로컬 타임스탬프를 쓰는 프로세스는 9시간 어긋나요. 날짜 계약 전체는 작업 데이터에 있어요.
렌더링과 형식 지정은 코어 밖의 일이에요. 막대 좌표와 화살표, 음영, 화면에 보이는 날짜 문자열은
차트가 만들어요. 코어의 어떤 함수도 Task.dependencies를 읽거나 로케일을 다루지 않아요. 위 목록에
없는 헬퍼는 내부용이니 가져다 쓸 수 있다고 보고 설계하지 마세요. prop과 기본값은
GanttProps에 있어요.