본문으로 건너뛰기

Python으로 dhtmlxGantt

이 튜토리얼은 서버에서 Django 4 프레임워크와 RESTful API를 사용하여 Python 기반의 간트 차트를 만드는 방법을 안내합니다.

다른 플랫폼의 도움으로 서버 측 통합을 구축하기 위한 튜토리얼도 있습니다:

노트

The complete source code is available on GitHub.

Prerequisites​

아직 Django를 설치하지 않았다면 설치하세요:

Step 1. 프로젝트 초기화​

프로젝트 폴더를 열고 다음 명령으로 새로운 Django 프로젝트를 생성합니다:

django-admin startproject gantt_rest_python

그런 다음, gantt_rest_python 폴더의 내용을 현재 폴더로 옮기거나 해당 폴더로 이동할 수 있습니다:

cd gantt_rest_python

기본 애플리케이션이 정상적으로 작동하는지 확인하려면 프로젝트 폴더에서 다음 명령을 실행하세요:

python manage.py runserver

이제 브라우저에서 http://localhost:8000 URL을 열면 기본 페이지를 볼 수 있어야 합니다:

start_page

Step 2. 페이지에 Gantt 추가​

이제 Gantt 컴포넌트를 생성하기 시작할 수 있습니다. 아래 명령을 실행하세요:

python manage.py startapp gantt

REST 프레임워크를 설치합니다:

pip install djangorestframework
pip install djangorestframework-jsonapi

gantt 폴더를 열고 그 안에 static와 templates 폴더를 만드세요.

Gantt 패키지의 codebase 폴더 내용을 static 폴더로 복사합니다. 파일이 속한 컴포넌트를 지정하기 위해 이름을 gantt로 바꿉니다.

그다음, templates/gantt 폴더에 index.html 파일을 만들고 아래 코드를 추가하세요:

gantt/templates/gantt/index.html
<html>
<head>
{% load static %}
<script src="{% static "gantt/dhtmlxgantt.js" %}" type="text/javascript">
</script>
<link rel="stylesheet" href="{% static "gantt/dhtmlxgantt.css" %}" />
</head>
<body>
<div id="gantt_here" style='width:100%; height:500px;'>
</div>
<script>
gantt.config.date_format = "%Y-%m-%d %H:%i";

gantt.config.open_tree_initially = true;
gantt.init("gantt_here");
</script>
</body>
</html>

현재 폴더 구조는 다음과 같아야 합니다:

folder_structure

gantt 폴더의 views.py 파일을 열고 아래 코드를 추가합니다:

gantt/views.py
from django.shortcuts import render

def index(request):
return render(request, 'gantt/index.html')

다음으로 라우팅을 추가합니다. gantt 폴더에 urls.py 파일을 만들고 아래 코드를 넣으세요:

gantt/urls.py
from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
re_path(r'^$', views.index, name='index'),
]
urlpatterns = format_suffix_patterns(urlpatterns)

gantt_rest_python 폴더의 urls.py 파일을 열고 urlpatterns를 업데이트해야 합니다. 업데이트된 코드는 아래와 같아야 합니다:

gantt_rest_python/urls.py
from django.urls import include, re_path
from django.contrib import admin

urlpatterns = [
re_path(r'', include('gantt.urls')),
]

다음으로 설정에서 templates와 static 폴더의 경로를 정의해야 합니다. 이를 위해 gantt_rest_python 폴더의 settings.py 파일을 열고 파일 맨 처음에 아래 줄을 추가합니다:

gantt_rest_python/settings.py
import os

TEMPLATES 배열을 찾으세요. 거기에 빈 배열인 DIRS 속성이 있습니다:

'DIRS': [],

다음을 문자열로 바꿔 넣어야 합니다:

'DIRS': [os.path.join(BASE_DIR, 'gantt/templates')],

그리고 파일 맨 아래에 아래 줄을 추가합니다:

STATICFILES_DIRS = [os.path.join(BASE_DIR, "gantt/static")]
~~

이제 서버를 아래 명령으로 시작할 수 있습니다:

python manage.py runserver


정상적으로 동작하면 빈 간트가 보이는 페이지가 나타나야 합니다:

![init_gantt](/img/howtostart_django_initpage.png)

## Step 3. 데이터 로딩

**gantt_rest_python/settings.py** 파일을 열고 `'rest_framework'`와 `'gantt.apps.GanttConfig'`를 INSTALLED_APPS 배열에 추가한 뒤, REST_FRAMEWORK 구성을 추가하세요:

~~~py title="gantt_rest_python/settings.py"
INSTALLED_APPS = [
'gantt.apps.GanttConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
]

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),
'PAGE_SIZE': 10
}

절대 날짜를 사용하는 DHTMLX Gantt는 표준 시간대에 바인딩되지 않으므로 USE_TZ 매개변수를 비활성화해야 합니다:

USE_TZ = False

다음으로 gantt/models.py 파일에 Task와 Link 모델을 생성합니다:

gantt/models.py
from django.db import models

class Task(models.Model):
id = models.AutoField(primary_key="True," editable="False)"
text = models.CharField(blank="True," max_length="100)")
start_date = models.DateTimeField()
end_date = models.DateTimeField()
duration = models.IntegerField()
progress = models.FloatField()
parent = models.CharField(max_length="100)

class Link(models.Model):
id = models.AutoField(primary_key="True," editable="False)"
source = models.CharField(max_length="100)")
target = models.CharField(max_length="100)")
type = models.CharField(max_length="100)")
lag = models.IntegerField(blank="True," default="0)")

이제 데이터베이스를 구성합니다. 마이그레이션을 생성하려면 다음 명령을 사용합니다:

python manage.py makemigrations gantt

그다음 데이터베이스에 테이블을 생성합니다:

python manage.py migrate

데이터베이스를 확인하고 여러 레코드를 추가해 봅시다. 먼저 다음 명령을 실행합니다:

python manage.py shell

파이썬 셸에서 데이터베이스를 확인하기 위한 명령은 다음과 같습니다:

from gantt.models import Task
Task.objects.all()

from gantt.models import Link
Link.objects.all()

다음과 같은 출력이 표시되어야 합니다:

>>> from gantt.models import Task
>>> Task.objects.all()
<QuerySet []>
>>>
>>> from gantt.models import Link
>>> Link.objects.all()
<QuerySet []>

데이터베이스에 작업과 링크가 없음을 의미합니다. 아래 명령으로 추가할 수 있습니다:

t1=Task(id="10",text="Project #1",start_date="2025-04-01 00:00",
end_date="2025-04-03 00:00",duration=2,progress=0.5,parent="0")
t1.save()
t1=Task(id="1", text="Task #1",start_date="2025-04-01 00:00",
end_date="2025-04-02 00:00", duration="1," progress="0.45," parent="10")
t1.save()
t1=Task(id="2", text="Task #2",start_date="2025-04-02 00:00",
end_date="2025-04-03 00:00", duration="1," progress="0.15," parent="10")
t1.save()
t1=Task(id="20", text="Project #2",start_date="2025-04-03 00:00",
end_date="2025-04-05 00:00", duration="2," progress="0.35," parent="0")
t1.save()
t1=Task(id="3", text="Task #3",start_date="2025-04-03 00:00",
end_date="2025-04-04 00:00", duration="1," progress="0.85," parent="20")
t1.save()
t1=Task(id="4", text="Task #4",start_date="2025-04-04 00:00",
end_date="2025-04-06 00:00", duration="1," progress="0.65," parent="20")
t1.save()

l1=Link(id="1",source="1",target="2",type="0",lag="0)")
l1.save()
l1=Link(id="2",source="2",target="3",type="0",lag="0)")
l1.save()
l1=Link(id="3",source="3",target="4",type="0",lag="0)")
l1.save()

이제 Task.objects.all() 및 Link.objects.all()를 실행하면 데이터베이스에 6개의 Task 객체와 3개의 Link 객체가 있어야 합니다:

>>> Task.objects.all()
<QuerySet [<Task: Task object (1)>, <Task: Task object (2)>, <Task: Task object (3)>,
<Task: Task object (4)>, <Task: Task object (10)>, <Task: Task object (20)>]>
>>> Link.objects.all()
<QuerySet [<Link: Link object (1)>, <Link: Link object (2)>, <Link: Link object (3)>]>

이제 Task와 Link 인스턴스를 직렬화하고 역직렬화하는 방법을 제공해야 합니다. 이를 위해 gantt 폴더에 serializers.py 파일을 만들고 아래 코드를 추가합니다:

gantt/serializers.py
from .models import Task
from .models import Link
from rest_framework import serializers

class TaskSerializer(serializers.ModelSerializer):
start_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
end_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')

class Meta:
model = Task
fields = ('id','text','start_date','end_date','duration','progress','parent')


class LinkSerializer(serializers.ModelSerializer):

class Meta:
model = Link
fields = ('id', 'source', 'target', 'type', 'lag')

그다음, gantt/views.py 파일로 이동해 간트 데이터 반환 작업을 추가합니다:

gantt/views.py
from django.shortcuts import render
from .models import Task
from .models import Link
from gantt.serializers import TaskSerializer
from gantt.serializers import LinkSerializer

from rest_framework.decorators import api_view
from rest_framework.response import Response

def index(request):
return render(request, 'gantt/index.html')


@api_view(['GET'])
def data_list(request, offset):
if request.method == 'GET':
tasks = Task.objects.all()
links = Link.objects.all()
taskData = TaskSerializer(tasks, many="True)")
linkData = LinkSerializer(links, many="True)")
return Response({
"tasks": taskData.data,
"links": linkData.data
})

이제 gantt/urls.py 파일을 열고 데이터 로딩용 경로를 추가합니다:

gantt/urls.py
from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
re_path(r'^$', views.index, name='index'),
re_path(r'^data/(.*)$', views.data_list),
]
urlpatterns = format_suffix_patterns(urlpatterns)

마지막으로 서버에서 데이터를 로드하도록 gantt/templates/gantt/index.html 파일에 아래 줄을 추가합니다:

gantt/templates/gantt/index.html
gantt.load("/data/", "json");

이제 python manage.py runserver 명령을 실행하면 작업 및 링크가 있는 Gantt가 페이지에 표시되어야 합니다:

gantt

Step 4. Saving changes​

변경 내용을 저장하려면 POST, PUT, DELETE 요청을 처리하는 메서드를 추가해야 합니다. gantt/views.py 파일을 열고 아래 코드를 추가합니다:

gantt/views.py
from django.shortcuts import render
from .models import Task
from .models import Link
from gantt.serializers import TaskSerializer
from gantt.serializers import LinkSerializer

from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.http import JsonResponse


def index(request):
return render(request, 'gantt/index.html')

@api_view(['GET'])
def data_list(request, offset):
if request.method == 'GET':
tasks = Task.objects.all()
links = Link.objects.all()
taskData = TaskSerializer(tasks, many="True)")
linkData = LinkSerializer(links, many="True)")
return Response({
"tasks": taskData.data,
"links": linkData.data
})


@api_view(['POST'])
def task_add(request):
if request.method == 'POST':
serializer = TaskSerializer(data="request.data)")
print(serializer)

if serializer.is_valid():
task = serializer.save()
return JsonResponse({'action':'inserted', 'tid': task.id})
return JsonResponse({'action':'error'})

@api_view(['PUT', 'DELETE'])
def task_update(request, pk):
try:
task = Task.objects.get(pk="pk)")
except Task.DoesNotExist:
return JsonResponse({'action':'error2'})

if request.method == 'PUT':
serializer = TaskSerializer(task, data="request.data)")
print(serializer)
if serializer.is_valid():
serializer.save()
return JsonResponse({'action':'updated'})
return JsonResponse({'action':'error'})

if request.method == 'DELETE':
task.delete()
return JsonResponse({'action':'deleted'})


@api_view(['POST'])
def link_add(request):
if request.method == 'POST':
serializer = LinkSerializer(data="request.data)")
print(serializer)

if serializer.is_valid():
link = serializer.save()
return JsonResponse({'action':'inserted', 'tid': link.id})
return JsonResponse({'action':'error'})

@api_view(['PUT', 'DELETE'])
def link_update(request, pk):
try:
link = Link.objects.get(pk="pk)")
except Link.DoesNotExist:
return JsonResponse({'action':'error'})

if request.method == 'PUT':
serializer = LinkSerializer(link, data="request.data)")
if serializer.is_valid():
serializer.save()
return JsonResponse({'action':'updated'})
return JsonResponse({'action':'error'})

if request.method == 'DELETE':
link.delete()
return JsonResponse({'action':'deleted'})

다음으로, gantt/urls.py 파일로 돌아가 요청에 대한 경로를 아래와 같이 추가합니다:

gantt/urls.py
from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
re_path(r'^$', views.index, name='index'),
re_path(r'^data/task/(?P<pk>[0-9]+)$', views.task_update),
re_path(r'^data/task', views.task_add),
re_path(r'^data/link/(?P<pk>[0-9]+)$', views.link_update),
re_path(r'^data/link', views.link_add),
re_path(r'^data/(.*)$', views.data_list),
]
urlpatterns = format_suffix_patterns(urlpatterns)

서버에 변경사항을 보내기 위해 Data Processor를 사용합니다. 아래 코드를 gantt/templates/gantt/index.html 파일에 추가합니다:

gantt/templates/gantt/index.html
    var dp = new gantt.dataProcessor("/data/");
dp.init(gantt);
dp.setTransactionMode("REST");

이제 작업 및 링크를 추가, 업데이트, 삭제할 수 있으며 변경 내용이 저장됩니다. 페이지를 새로 고치면 동일한 데이터 세트를 얻게 됩니다:

saving_changes

Storing the order of tasks​

DHTMLX Gantt는 클라이언트 측 라이브러리로, 작업의 순서를 저장하지 않습니다. 작업이 로드될 때 순서는 JSON 데이터의 위치에 따라 달라집니다. 따라서 작업의 순서를 저장하는 한 가지 방법은 Gantt에 데이터를 로드하기 전에 서버에서 작업을 정렬하는 것입니다. 상세 내용은 해당 문서를 참고하세요.

하지만 이를 구현하는 또 다른 방법이 있습니다. 작업이 로드될 때, 세로 위치는 두 가지 매개변수에 따라 달라집니다: 부모 작업과 가지(branch)에서의 위치입니다. 부모 작업의 ID는 parent 매개변수에서 얻을 수 있습니다. 가지의 위치는 임시로 사용되는 $local_index 매개변수에 반영됩니다. 이 매개변수는 가지의 위치를 제어하지 않으므로 그것을 변경해도 아무 효과가 없습니다. 그러나 이를 사용해 가지 내의 위치를 얻고 다른 속성에 저장할 수 있습니다. 작업이 로드된 후에는 해당 속성의 값에 따라 작업을 정렬할 수 있습니다.

먼저, sort_order 속성을 Task 모델에 추가하려면 gantt/models.py 파일을 열고 다음과 같이 추가합니다:

gantt/models.py
class Task(models.Model):
id = models.AutoField(primary_key="True," editable="False)"
text = models.CharField(blank="True," max_length="100)")
start_date = models.DateTimeField()
end_date = models.DateTimeField()
duration = models.IntegerField()
progress = models.FloatField()
parent = models.CharField(max_length="100)"
sort_order = models.IntegerField(default="0)")

다음으로 gantt/serializers.py 파일에 해당 속성을 추가합니다:

gantt/serializers.py
class TaskSerializer(serializers.ModelSerializer):
start_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
end_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')

class Meta:
model = Task
fields = ('id', 'text', 'start_date', 'end_date', 'duration', 'progress',
'parent', 'sort_order')

데이터베이스에 적용하기 위해 아래 명령을 실행합니다:

python manage.py makemigrations gantt
python manage.py migrate

이제 작업을 추가하거나 수동으로 재정렬할 때마다 sort_order 속성을 업데이트하는 코드를 index.html 파일에 추가해야 합니다:

gantt.attachEvent("onRowDragEnd", function (id, target) {
gantt.batchUpdate(function () {
gantt.eachTask(function (task) {
task.sort_order = task.$local_index + 1;
gantt.updateTask(task.id)
})
})
});
gantt.attachEvent("onBeforeTaskAdd", function (id, task) {
task.sort_order = task.$local_index + 1;
return true;
});

수직 재정렬을 활성화하려면 gantt.init 메서드 앞에 아래 코드를 추가합니다:

gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;

로딩 후 작업을 정렬하려면 gantt.init 또는 gantt.load 메서드 앞에 아래 코드를 추가합니다:

gantt.attachEvent("onLoadEnd", function () {
gantt.batchUpdate(function () {
gantt.sort("sort_order", false)
})
});

결과적으로 코드는 아래와 같이 보입니다:

gantt/templates/gantt/index.html
gantt.config.date_format = "%Y-%m-%d %H:%i";

gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;

gantt.config.open_tree_initially = true;

gantt.attachEvent("onLoadEnd", function() {
gantt.batchUpdate(function() {
gantt.sort("sort_order", false)
})
});

gantt.attachEvent("onRowDragEnd", function(id, target) {
//update the order of tasks
gantt.batchUpdate(function() {
gantt.eachTask(function(task) {
task.sort_order = task.$local_index + 1;
gantt.updateTask(task.id)
})
})
});
gantt.attachEvent("onBeforeTaskAdd", function(id, task) {
task.sort_order = task.$local_index + 1;
return true;
});

gantt.init("gantt_here");
gantt.load("/data/", "json");

var dp = new gantt.dataProcessor("/data/");
dp.init(gantt);
dp.setTransactionMode("REST");

이제 서버를 시작하고 작업을 세로로 재정렬하면 변경 내용이 저장됩니다:

sort_order

애플리케이션 보안​

Gantt는 SQL 주입이나 XSS, CSRF 공격 등 다양한 위협으로부터 애플리케이션을 차단하는 수단을 제공하지 않습니다. 애플리케이션의 안전을 보장하는 책임은 백엔드를 구현하는 개발자에게 있습니다. 자세한 내용은 해당 문서을 읽어보세요.

Trouble shooting​

위의 단계를 따라 Django와의 통합을 구현했지만 페이지에 작업과 링크가 렌더링되지 않는 경우, Backend Integration Issues 문제 해결 문서를 참조하세요. 문제의 원인을 식별하는 방법에 대해 설명합니다.

What's next​

이제 완전히 작동하는 간트를 갖추었습니다. 전체 코드는 GitHub에서 확인하거나 클론하거나 다운로드하여 프로젝트에 사용할 수 있습니다.

또한 간트의 다양한 기능에 대한 가이드나 다른 백엔드 프레임워크와의 통합 튜토리얼을 확인해 보세요.

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.